]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blob - includes/Article.php
MediaWiki 1.16.1
[autoinstallsdev/mediawiki.git] / includes / Article.php
1 <?php
2 /**
3  * File for articles
4  * @file
5  */
6
7 /**
8  * Class representing a MediaWiki article and history.
9  *
10  * See design.txt for an overview.
11  * Note: edit user interface and cache support functions have been
12  * moved to separate EditPage and HTMLFileCache classes.
13  *
14  */
15 class Article {
16         /**@{{
17          * @private
18          */
19         var $mComment = '';               // !<
20         var $mContent;                    // !<
21         var $mContentLoaded = false;      // !<
22         var $mCounter = -1;               // !< Not loaded
23         var $mCurID = -1;                 // !< Not loaded
24         var $mDataLoaded = false;         // !<
25         var $mForUpdate = false;          // !<
26         var $mGoodAdjustment = 0;         // !<
27         var $mIsRedirect = false;         // !<
28         var $mLatest = false;             // !<
29         var $mMinorEdit;                  // !<
30         var $mOldId;                      // !<
31         var $mPreparedEdit = false;       // !< Title object if set
32         var $mRedirectedFrom = null;      // !< Title object if set
33         var $mRedirectTarget = null;      // !< Title object if set
34         var $mRedirectUrl = false;        // !<
35         var $mRevIdFetched = 0;           // !<
36         var $mRevision;                   // !<
37         var $mTimestamp = '';             // !<
38         var $mTitle;                      // !<
39         var $mTotalAdjustment = 0;        // !<
40         var $mTouched = '19700101000000'; // !<
41         var $mUser = -1;                  // !< Not loaded
42         var $mUserText = '';              // !<
43         var $mParserOptions;              // !<
44         var $mParserOutput;               // !<
45         /**@}}*/
46
47         /**
48          * Constructor and clear the article
49          * @param $title Reference to a Title object.
50          * @param $oldId Integer revision ID, null to fetch from request, zero for current
51          */
52         public function __construct( Title $title, $oldId = null ) {
53                 $this->mTitle =& $title;
54                 $this->mOldId = $oldId;
55         }
56
57         /**
58          * Constructor from an article article
59          * @param $id The article ID to load
60          */
61         public static function newFromID( $id ) {
62                 $t = Title::newFromID( $id );
63                 # FIXME: doesn't inherit right
64                 return $t == null ? null : new self( $t );
65                 # return $t == null ? null : new static( $t ); // PHP 5.3
66         }
67
68         /**
69          * Tell the page view functions that this view was redirected
70          * from another page on the wiki.
71          * @param $from Title object.
72          */
73         public function setRedirectedFrom( $from ) {
74                 $this->mRedirectedFrom = $from;
75         }
76
77         /**
78          * If this page is a redirect, get its target
79          *
80          * The target will be fetched from the redirect table if possible.
81          * If this page doesn't have an entry there, call insertRedirect()
82          * @return mixed Title object, or null if this page is not a redirect
83          */
84         public function getRedirectTarget() {
85                 if ( !$this->mTitle || !$this->mTitle->isRedirect() )
86                         return null;
87                 if ( !is_null( $this->mRedirectTarget ) )
88                         return $this->mRedirectTarget;
89                 # Query the redirect table
90                 $dbr = wfGetDB( DB_SLAVE );
91                 $row = $dbr->selectRow( 'redirect',
92                         array( 'rd_namespace', 'rd_title' ),
93                         array( 'rd_from' => $this->getID() ),
94                         __METHOD__
95                 );
96                 if ( $row ) {
97                         return $this->mRedirectTarget = Title::makeTitle( $row->rd_namespace, $row->rd_title );
98                 }
99                 # This page doesn't have an entry in the redirect table
100                 return $this->mRedirectTarget = $this->insertRedirect();
101         }
102
103         /**
104          * Insert an entry for this page into the redirect table.
105          *
106          * Don't call this function directly unless you know what you're doing.
107          * @return Title object
108          */
109         public function insertRedirect() {
110                 $retval = Title::newFromRedirect( $this->getContent() );
111                 if ( !$retval ) {
112                         return null;
113                 }
114                 $dbw = wfGetDB( DB_MASTER );
115                 $dbw->replace( 'redirect', array( 'rd_from' ),
116                         array(
117                                 'rd_from' => $this->getID(),
118                                 'rd_namespace' => $retval->getNamespace(),
119                                 'rd_title' => $retval->getDBkey()
120                         ),
121                         __METHOD__
122                 );
123                 return $retval;
124         }
125
126         /**
127          * Get the Title object this page redirects to
128          *
129          * @return mixed false, Title of in-wiki target, or string with URL
130          */
131         public function followRedirect() {
132                 $text = $this->getContent();
133                 return $this->followRedirectText( $text );
134         }
135
136         /**
137          * Get the Title object this text redirects to
138          *
139          * @return mixed false, Title of in-wiki target, or string with URL
140          */
141         public function followRedirectText( $text ) {
142                 $rt = Title::newFromRedirectRecurse( $text ); // recurse through to only get the final target
143                 # process if title object is valid and not special:userlogout
144                 if ( $rt ) {
145                         if ( $rt->getInterwiki() != '' ) {
146                                 if ( $rt->isLocal() ) {
147                                         // Offsite wikis need an HTTP redirect.
148                                         //
149                                         // This can be hard to reverse and may produce loops,
150                                         // so they may be disabled in the site configuration.
151                                         $source = $this->mTitle->getFullURL( 'redirect=no' );
152                                         return $rt->getFullURL( 'rdfrom=' . urlencode( $source ) );
153                                 }
154                         } else {
155                                 if ( $rt->getNamespace() == NS_SPECIAL ) {
156                                         // Gotta handle redirects to special pages differently:
157                                         // Fill the HTTP response "Location" header and ignore
158                                         // the rest of the page we're on.
159                                         //
160                                         // This can be hard to reverse, so they may be disabled.
161                                         if ( $rt->isSpecial( 'Userlogout' ) ) {
162                                                 // rolleyes
163                                         } else {
164                                                 return $rt->getFullURL();
165                                         }
166                                 }
167                                 return $rt;
168                         }
169                 }
170                 // No or invalid redirect
171                 return false;
172         }
173
174         /**
175          * get the title object of the article
176          */
177         public function getTitle() {
178                 return $this->mTitle;
179         }
180
181         /**
182          * Clear the object
183          * @private
184          */
185         public function clear() {
186                 $this->mDataLoaded    = false;
187                 $this->mContentLoaded = false;
188
189                 $this->mCurID = $this->mUser = $this->mCounter = -1; # Not loaded
190                 $this->mRedirectedFrom = null; # Title object if set
191                 $this->mRedirectTarget = null; # Title object if set
192                 $this->mUserText =
193                 $this->mTimestamp = $this->mComment = '';
194                 $this->mGoodAdjustment = $this->mTotalAdjustment = 0;
195                 $this->mTouched = '19700101000000';
196                 $this->mForUpdate = false;
197                 $this->mIsRedirect = false;
198                 $this->mRevIdFetched = 0;
199                 $this->mRedirectUrl = false;
200                 $this->mLatest = false;
201                 $this->mPreparedEdit = false;
202         }
203
204         /**
205          * Note that getContent/loadContent do not follow redirects anymore.
206          * If you need to fetch redirectable content easily, try
207          * the shortcut in Article::followContent()
208          *
209          * @return Return the text of this revision
210          */
211         public function getContent() {
212                 global $wgUser, $wgContLang, $wgOut, $wgMessageCache;
213                 wfProfileIn( __METHOD__ );
214                 if ( $this->getID() === 0 ) {
215                         # If this is a MediaWiki:x message, then load the messages
216                         # and return the message value for x.
217                         if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
218                                 # If this is a system message, get the default text.
219                                 list( $message, $lang ) = $wgMessageCache->figureMessage( $wgContLang->lcfirst( $this->mTitle->getText() ) );
220                                 $wgMessageCache->loadAllMessages( $lang );
221                                 $text = wfMsgGetKey( $message, false, $lang, false );
222                                 if ( wfEmptyMsg( $message, $text ) )
223                                         $text = '';
224                         } else {
225                                 $text = wfMsgExt( $wgUser->isLoggedIn() ? 'noarticletext' : 'noarticletextanon', 'parsemag' );
226                         }
227                         wfProfileOut( __METHOD__ );
228                         return $text;
229                 } else {
230                         $this->loadContent();
231                         wfProfileOut( __METHOD__ );
232                         return $this->mContent;
233                 }
234         }
235
236         /**
237          * Get the text of the current revision. No side-effects...
238          *
239          * @return Return the text of the current revision
240          */
241         public function getRawText() {
242                 // Check process cache for current revision
243                 if ( $this->mContentLoaded && $this->mOldId == 0 ) {
244                         return $this->mContent;
245                 }
246                 $rev = Revision::newFromTitle( $this->mTitle );
247                 $text = $rev ? $rev->getRawText() : false;
248                 return $text;
249         }
250
251         /**
252          * This function returns the text of a section, specified by a number ($section).
253          * A section is text under a heading like == Heading == or \<h1\>Heading\</h1\>, or
254          * the first section before any such heading (section 0).
255          *
256          * If a section contains subsections, these are also returned.
257          *
258          * @param $text String: text to look in
259          * @param $section Integer: section number
260          * @return string text of the requested section
261          * @deprecated
262          */
263         public function getSection( $text, $section ) {
264                 global $wgParser;
265                 return $wgParser->getSection( $text, $section );
266         }
267
268         /**
269          * Get the text that needs to be saved in order to undo all revisions
270          * between $undo and $undoafter. Revisions must belong to the same page,
271          * must exist and must not be deleted
272          * @param $undo Revision
273          * @param $undoafter Revision Must be an earlier revision than $undo
274          * @return mixed string on success, false on failure
275          */
276         public function getUndoText( Revision $undo, Revision $undoafter = null ) {
277                 $undo_text = $undo->getText();
278                 $undoafter_text = $undoafter->getText();
279                 $cur_text = $this->getContent();
280                 if ( $cur_text == $undo_text ) {
281                         # No use doing a merge if it's just a straight revert.
282                         return $undoafter_text;
283                 }
284                 $undone_text = '';
285                 if ( !wfMerge( $undo_text, $undoafter_text, $cur_text, $undone_text ) )
286                         return false;
287                 return $undone_text;
288         }
289
290         /**
291          * @return int The oldid of the article that is to be shown, 0 for the
292          *             current revision
293          */
294         public function getOldID() {
295                 if ( is_null( $this->mOldId ) ) {
296                         $this->mOldId = $this->getOldIDFromRequest();
297                 }
298                 return $this->mOldId;
299         }
300
301         /**
302          * Sets $this->mRedirectUrl to a correct URL if the query parameters are incorrect
303          *
304          * @return int The old id for the request
305          */
306         public function getOldIDFromRequest() {
307                 global $wgRequest;
308                 $this->mRedirectUrl = false;
309                 $oldid = $wgRequest->getVal( 'oldid' );
310                 if ( isset( $oldid ) ) {
311                         $oldid = intval( $oldid );
312                         if ( $wgRequest->getVal( 'direction' ) == 'next' ) {
313                                 $nextid = $this->mTitle->getNextRevisionID( $oldid );
314                                 if ( $nextid  ) {
315                                         $oldid = $nextid;
316                                 } else {
317                                         $this->mRedirectUrl = $this->mTitle->getFullURL( 'redirect=no' );
318                                 }
319                         } elseif ( $wgRequest->getVal( 'direction' ) == 'prev' ) {
320                                 $previd = $this->mTitle->getPreviousRevisionID( $oldid );
321                                 if ( $previd ) {
322                                         $oldid = $previd;
323                                 }
324                         }
325                 }
326                 if ( !$oldid ) {
327                         $oldid = 0;
328                 }
329                 return $oldid;
330         }
331
332         /**
333          * Load the revision (including text) into this object
334          */
335         function loadContent() {
336                 if ( $this->mContentLoaded ) return;
337                 wfProfileIn( __METHOD__ );
338                 # Query variables :P
339                 $oldid = $this->getOldID();
340                 # Pre-fill content with error message so that if something
341                 # fails we'll have something telling us what we intended.
342                 $this->mOldId = $oldid;
343                 $this->fetchContent( $oldid );
344                 wfProfileOut( __METHOD__ );
345         }
346
347
348         /**
349          * Fetch a page record with the given conditions
350          * @param $dbr Database object
351          * @param $conditions Array
352          */
353         protected function pageData( $dbr, $conditions ) {
354                 $fields = array(
355                                 'page_id',
356                                 'page_namespace',
357                                 'page_title',
358                                 'page_restrictions',
359                                 'page_counter',
360                                 'page_is_redirect',
361                                 'page_is_new',
362                                 'page_random',
363                                 'page_touched',
364                                 'page_latest',
365                                 'page_len',
366                 );
367                 wfRunHooks( 'ArticlePageDataBefore', array( &$this, &$fields ) );
368                 $row = $dbr->selectRow(
369                         'page',
370                         $fields,
371                         $conditions,
372                         __METHOD__
373                 );
374                 wfRunHooks( 'ArticlePageDataAfter', array( &$this, &$row ) );
375                 return $row ;
376         }
377
378         /**
379          * @param $dbr Database object
380          * @param $title Title object
381          */
382         public function pageDataFromTitle( $dbr, $title ) {
383                 return $this->pageData( $dbr, array(
384                         'page_namespace' => $title->getNamespace(),
385                         'page_title'     => $title->getDBkey() ) );
386         }
387
388         /**
389          * @param $dbr Database
390          * @param $id Integer
391          */
392         protected function pageDataFromId( $dbr, $id ) {
393                 return $this->pageData( $dbr, array( 'page_id' => $id ) );
394         }
395
396         /**
397          * Set the general counter, title etc data loaded from
398          * some source.
399          *
400          * @param $data Database row object or "fromdb"
401          */
402         public function loadPageData( $data = 'fromdb' ) {
403                 if ( $data === 'fromdb' ) {
404                         $dbr = wfGetDB( DB_MASTER );
405                         $data = $this->pageDataFromId( $dbr, $this->getId() );
406                 }
407
408                 $lc = LinkCache::singleton();
409                 if ( $data ) {
410                         $lc->addGoodLinkObj( $data->page_id, $this->mTitle, $data->page_len, $data->page_is_redirect );
411
412                         $this->mTitle->mArticleID = intval( $data->page_id );
413
414                         # Old-fashioned restrictions
415                         $this->mTitle->loadRestrictions( $data->page_restrictions );
416
417                         $this->mCounter     = intval( $data->page_counter );
418                         $this->mTouched     = wfTimestamp( TS_MW, $data->page_touched );
419                         $this->mIsRedirect  = intval( $data->page_is_redirect );
420                         $this->mLatest      = intval( $data->page_latest );
421                 } else {
422                         if ( is_object( $this->mTitle ) ) {
423                                 $lc->addBadLinkObj( $this->mTitle );
424                         }
425                         $this->mTitle->mArticleID = 0;
426                 }
427
428                 $this->mDataLoaded  = true;
429         }
430
431         /**
432          * Get text of an article from database
433          * Does *NOT* follow redirects.
434          * @param $oldid Int: 0 for whatever the latest revision is
435          * @return string
436          */
437         function fetchContent( $oldid = 0 ) {
438                 if ( $this->mContentLoaded ) {
439                         return $this->mContent;
440                 }
441
442                 $dbr = wfGetDB( DB_MASTER );
443
444                 # Pre-fill content with error message so that if something
445                 # fails we'll have something telling us what we intended.
446                 $t = $this->mTitle->getPrefixedText();
447                 $d = $oldid ? wfMsgExt( 'missingarticle-rev', array( 'escape' ), $oldid ) : '';
448                 $this->mContent = wfMsgNoTrans( 'missing-article', $t, $d ) ;
449
450                 if ( $oldid ) {
451                         $revision = Revision::newFromId( $oldid );
452                         if ( is_null( $revision ) ) {
453                                 wfDebug( __METHOD__ . " failed to retrieve specified revision, id $oldid\n" );
454                                 return false;
455                         }
456                         $data = $this->pageDataFromId( $dbr, $revision->getPage() );
457                         if ( !$data ) {
458                                 wfDebug( __METHOD__ . " failed to get page data linked to revision id $oldid\n" );
459                                 return false;
460                         }
461                         $this->mTitle = Title::makeTitle( $data->page_namespace, $data->page_title );
462                         $this->loadPageData( $data );
463                 } else {
464                         if ( !$this->mDataLoaded ) {
465                                 $data = $this->pageDataFromTitle( $dbr, $this->mTitle );
466                                 if ( !$data ) {
467                                         wfDebug( __METHOD__ . " failed to find page data for title " . $this->mTitle->getPrefixedText() . "\n" );
468                                         return false;
469                                 }
470                                 $this->loadPageData( $data );
471                         }
472                         $revision = Revision::newFromId( $this->mLatest );
473                         if ( is_null( $revision ) ) {
474                                 wfDebug( __METHOD__ . " failed to retrieve current page, rev_id {$this->mLatest}\n" );
475                                 return false;
476                         }
477                 }
478
479                 // FIXME: Horrible, horrible! This content-loading interface just plain sucks.
480                 // We should instead work with the Revision object when we need it...
481                 $this->mContent   = $revision->getText( Revision::FOR_THIS_USER ); // Loads if user is allowed
482
483                 $this->mUser      = $revision->getUser();
484                 $this->mUserText  = $revision->getUserText();
485                 $this->mComment   = $revision->getComment();
486                 $this->mTimestamp = wfTimestamp( TS_MW, $revision->getTimestamp() );
487
488                 $this->mRevIdFetched = $revision->getId();
489                 $this->mContentLoaded = true;
490                 $this->mRevision =& $revision;
491
492                 wfRunHooks( 'ArticleAfterFetchContent', array( &$this, &$this->mContent ) ) ;
493
494                 return $this->mContent;
495         }
496
497         /**
498          * Read/write accessor to select FOR UPDATE
499          *
500          * @param $x Mixed: FIXME
501          */
502         public function forUpdate( $x = null ) {
503                 return wfSetVar( $this->mForUpdate, $x );
504         }
505
506         /**
507          * Get the database which should be used for reads
508          *
509          * @return Database
510          * @deprecated - just call wfGetDB( DB_MASTER ) instead
511          */
512         function getDB() {
513                 wfDeprecated( __METHOD__ );
514                 return wfGetDB( DB_MASTER );
515         }
516
517         /**
518          * Get options for all SELECT statements
519          *
520          * @param $options Array: an optional options array which'll be appended to
521          *                       the default
522          * @return Array: options
523          */
524         protected function getSelectOptions( $options = '' ) {
525                 if ( $this->mForUpdate ) {
526                         if ( is_array( $options ) ) {
527                                 $options[] = 'FOR UPDATE';
528                         } else {
529                                 $options = 'FOR UPDATE';
530                         }
531                 }
532                 return $options;
533         }
534
535         /**
536          * @return int Page ID
537          */
538         public function getID() {
539                 if ( $this->mTitle ) {
540                         return $this->mTitle->getArticleID();
541                 } else {
542                         return 0;
543                 }
544         }
545
546         /**
547          * @return bool Whether or not the page exists in the database
548          */
549         public function exists() {
550                 return $this->getId() > 0;
551         }
552
553         /**
554          * Check if this page is something we're going to be showing
555          * some sort of sensible content for. If we return false, page
556          * views (plain action=view) will return an HTTP 404 response,
557          * so spiders and robots can know they're following a bad link.
558          *
559          * @return bool
560          */
561         public function hasViewableContent() {
562                 return $this->exists() || $this->mTitle->isAlwaysKnown();
563         }
564
565         /**
566          * @return int The view count for the page
567          */
568         public function getCount() {
569                 if ( -1 == $this->mCounter ) {
570                         $id = $this->getID();
571                         if ( $id == 0 ) {
572                                 $this->mCounter = 0;
573                         } else {
574                                 $dbr = wfGetDB( DB_SLAVE );
575                                 $this->mCounter = $dbr->selectField( 'page',
576                                         'page_counter',
577                                         array( 'page_id' => $id ),
578                                         __METHOD__,
579                                         $this->getSelectOptions()
580                                 );
581                         }
582                 }
583                 return $this->mCounter;
584         }
585
586         /**
587          * Determine whether a page  would be suitable for being counted as an
588          * article in the site_stats table based on the title & its content
589          *
590          * @param $text String: text to analyze
591          * @return bool
592          */
593         public function isCountable( $text ) {
594                 global $wgUseCommaCount;
595
596                 $token = $wgUseCommaCount ? ',' : '[[';
597                 return $this->mTitle->isContentPage() && !$this->isRedirect( $text ) && in_string( $token, $text );
598         }
599
600         /**
601          * Tests if the article text represents a redirect
602          *
603          * @param $text String: FIXME
604          * @return bool
605          */
606         public function isRedirect( $text = false ) {
607                 if ( $text === false ) {
608                         if ( $this->mDataLoaded ) {
609                                 return $this->mIsRedirect;
610                         }
611                         // Apparently loadPageData was never called
612                         $this->loadContent();
613                         $titleObj = Title::newFromRedirectRecurse( $this->fetchContent() );
614                 } else {
615                         $titleObj = Title::newFromRedirect( $text );
616                 }
617                 return $titleObj !== null;
618         }
619
620         /**
621          * Returns true if the currently-referenced revision is the current edit
622          * to this page (and it exists).
623          * @return bool
624          */
625         public function isCurrent() {
626                 # If no oldid, this is the current version.
627                 if ( $this->getOldID() == 0 ) {
628                         return true;
629                 }
630                 return $this->exists() && isset( $this->mRevision ) && $this->mRevision->isCurrent();
631         }
632
633         /**
634          * Loads everything except the text
635          * This isn't necessary for all uses, so it's only done if needed.
636          */
637         protected function loadLastEdit() {
638                 if ( -1 != $this->mUser )
639                         return;
640
641                 # New or non-existent articles have no user information
642                 $id = $this->getID();
643                 if ( 0 == $id ) return;
644
645                 $this->mLastRevision = Revision::loadFromPageId( wfGetDB( DB_MASTER ), $id );
646                 if ( !is_null( $this->mLastRevision ) ) {
647                         $this->mUser      = $this->mLastRevision->getUser();
648                         $this->mUserText  = $this->mLastRevision->getUserText();
649                         $this->mTimestamp = $this->mLastRevision->getTimestamp();
650                         $this->mComment   = $this->mLastRevision->getComment();
651                         $this->mMinorEdit = $this->mLastRevision->isMinor();
652                         $this->mRevIdFetched = $this->mLastRevision->getId();
653                 }
654         }
655
656         public function getTimestamp() {
657                 // Check if the field has been filled by ParserCache::get()
658                 if ( !$this->mTimestamp ) {
659                         $this->loadLastEdit();
660                 }
661                 return wfTimestamp( TS_MW, $this->mTimestamp );
662         }
663
664         public function getUser() {
665                 $this->loadLastEdit();
666                 return $this->mUser;
667         }
668
669         public function getUserText() {
670                 $this->loadLastEdit();
671                 return $this->mUserText;
672         }
673
674         public function getComment() {
675                 $this->loadLastEdit();
676                 return $this->mComment;
677         }
678
679         public function getMinorEdit() {
680                 $this->loadLastEdit();
681                 return $this->mMinorEdit;
682         }
683
684         /* Use this to fetch the rev ID used on page views */
685         public function getRevIdFetched() {
686                 $this->loadLastEdit();
687                 return $this->mRevIdFetched;
688         }
689
690         /**
691          * @param $limit Integer: default 0.
692          * @param $offset Integer: default 0.
693          */
694         public function getContributors( $limit = 0, $offset = 0 ) {
695                 # XXX: this is expensive; cache this info somewhere.
696
697                 $dbr = wfGetDB( DB_SLAVE );
698                 $revTable = $dbr->tableName( 'revision' );
699                 $userTable = $dbr->tableName( 'user' );
700
701                 $pageId = $this->getId();
702
703                 $user = $this->getUser();
704                 if ( $user ) {
705                         $excludeCond = "AND rev_user != $user";
706                 } else {
707                         $userText = $dbr->addQuotes( $this->getUserText() );
708                         $excludeCond = "AND rev_user_text != $userText";
709                 }
710
711                 $deletedBit = $dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER ); // username hidden?
712
713                 $sql = "SELECT {$userTable}.*, rev_user_text as user_name, MAX(rev_timestamp) as timestamp
714                         FROM $revTable LEFT JOIN $userTable ON rev_user = user_id
715                         WHERE rev_page = $pageId
716                         $excludeCond
717                         AND $deletedBit = 0
718                         GROUP BY rev_user, rev_user_text
719                         ORDER BY timestamp DESC";
720
721                 if ( $limit > 0 )
722                         $sql = $dbr->limitResult( $sql, $limit, $offset );
723
724                 $sql .= ' ' . $this->getSelectOptions();
725                 $res = $dbr->query( $sql, __METHOD__ );
726
727                 return new UserArrayFromResult( $res );
728         }
729
730         /**
731          * This is the default action of the index.php entry point: just view the
732          * page of the given title.
733          */
734         public function view() {
735                 global $wgUser, $wgOut, $wgRequest, $wgContLang;
736                 global $wgEnableParserCache, $wgStylePath, $wgParser;
737                 global $wgUseTrackbacks, $wgUseFileCache;
738
739                 wfProfileIn( __METHOD__ );
740
741                 # Get variables from query string
742                 $oldid = $this->getOldID();
743                 $parserCache = ParserCache::singleton();
744
745                 $parserOptions = clone $this->getParserOptions();
746                 # Render printable version, use printable version cache
747                 if ( $wgOut->isPrintable() ) {
748                         $parserOptions->setIsPrintable( true );
749                 }
750
751                 # Try client and file cache
752                 if ( $oldid === 0 && $this->checkTouched() ) {
753                         global $wgUseETag;
754                         if ( $wgUseETag ) {
755                                 $wgOut->setETag( $parserCache->getETag( $this, $parserOptions ) );
756                         }
757                         # Is is client cached?
758                         if ( $wgOut->checkLastModified( $this->getTouched() ) ) {
759                                 wfDebug( __METHOD__ . ": done 304\n" );
760                                 wfProfileOut( __METHOD__ );
761                                 return;
762                         # Try file cache
763                         } else if ( $wgUseFileCache && $this->tryFileCache() ) {
764                                 wfDebug( __METHOD__ . ": done file cache\n" );
765                                 # tell wgOut that output is taken care of
766                                 $wgOut->disable();
767                                 $this->viewUpdates();
768                                 wfProfileOut( __METHOD__ );
769                                 return;
770                         }
771                 }
772
773                 $sk = $wgUser->getSkin();
774
775                 # getOldID may want us to redirect somewhere else
776                 if ( $this->mRedirectUrl ) {
777                         $wgOut->redirect( $this->mRedirectUrl );
778                         wfDebug( __METHOD__ . ": redirecting due to oldid\n" );
779                         wfProfileOut( __METHOD__ );
780                         return;
781                 }
782
783                 $wgOut->setArticleFlag( true );
784                 # Set page title (may be overridden by DISPLAYTITLE)
785                 $wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
786
787                 # If we got diff in the query, we want to see a diff page instead of the article.
788                 if ( !is_null( $wgRequest->getVal( 'diff' ) ) ) {
789                         wfDebug( __METHOD__ . ": showing diff page\n" );
790                         $this->showDiffPage();
791                         wfProfileOut( __METHOD__ );
792                         return;
793                 }
794
795                 # Allow frames by default
796                 $wgOut->allowClickjacking();
797
798                 # Should the parser cache be used?
799                 $useParserCache = $this->useParserCache( $oldid );
800                 wfDebug( 'Article::view using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
801                 if ( $wgUser->getOption( 'stubthreshold' ) ) {
802                         wfIncrStats( 'pcache_miss_stub' );
803                 }
804
805                 $wasRedirected = $this->showRedirectedFromHeader();
806                 $this->showNamespaceHeader();
807
808                 # Iterate through the possible ways of constructing the output text.
809                 # Keep going until $outputDone is set, or we run out of things to do.
810                 $pass = 0;
811                 $outputDone = false;
812                 $this->mParserOutput = false;
813                 while ( !$outputDone && ++$pass ) {
814                         switch( $pass ) {
815                                 case 1:
816                                         wfRunHooks( 'ArticleViewHeader', array( &$this, &$outputDone, &$useParserCache ) );
817                                         break;
818
819                                 case 2:
820                                         # Try the parser cache
821                                         if ( $useParserCache ) {
822                                                 $this->mParserOutput = $parserCache->get( $this, $parserOptions );
823                                                 if ( $this->mParserOutput !== false ) {
824                                                         wfDebug( __METHOD__ . ": showing parser cache contents\n" );
825                                                         $wgOut->addParserOutput( $this->mParserOutput );
826                                                         # Ensure that UI elements requiring revision ID have
827                                                         # the correct version information.
828                                                         $wgOut->setRevisionId( $this->mLatest );
829                                                         $outputDone = true;
830                                                 }
831                                         }
832                                         break;
833
834                                 case 3:
835                                         $text = $this->getContent();
836                                         if ( $text === false || $this->getID() == 0 ) {
837                                                 wfDebug( __METHOD__ . ": showing missing article\n" );
838                                                 $this->showMissingArticle();
839                                                 wfProfileOut( __METHOD__ );
840                                                 return;
841                                         }
842
843                                         # Another whitelist check in case oldid is altering the title
844                                         if ( !$this->mTitle->userCanRead() ) {
845                                                 wfDebug( __METHOD__ . ": denied on secondary read check\n" );
846                                                 $wgOut->loginToUse();
847                                                 $wgOut->output();
848                                                 $wgOut->disable();
849                                                 wfProfileOut( __METHOD__ );
850                                                 return;
851                                         }
852
853                                         # Are we looking at an old revision
854                                         if ( $oldid && !is_null( $this->mRevision ) ) {
855                                                 $this->setOldSubtitle( $oldid );
856                                                 if ( !$this->showDeletedRevisionHeader() ) {
857                                                         wfDebug( __METHOD__ . ": cannot view deleted revision\n" );
858                                                         wfProfileOut( __METHOD__ );
859                                                         return;
860                                                 }
861                                                 # If this "old" version is the current, then try the parser cache...
862                                                 if ( $oldid === $this->getLatest() && $this->useParserCache( false ) ) {
863                                                         $this->mParserOutput = $parserCache->get( $this, $parserOptions );
864                                                         if ( $this->mParserOutput ) {
865                                                                 wfDebug( __METHOD__ . ": showing parser cache for current rev permalink\n" );
866                                                                 $wgOut->addParserOutput( $this->mParserOutput );
867                                                                 $wgOut->setRevisionId( $this->mLatest );
868                                                                 $this->showViewFooter();
869                                                                 $this->viewUpdates();
870                                                                 wfProfileOut( __METHOD__ );
871                                                                 return;
872                                                         }
873                                                 }
874                                         }
875
876                                         # Ensure that UI elements requiring revision ID have
877                                         # the correct version information.
878                                         $wgOut->setRevisionId( $this->getRevIdFetched() );
879
880                                         # Pages containing custom CSS or JavaScript get special treatment
881                                         if ( $this->mTitle->isCssOrJsPage() || $this->mTitle->isCssJsSubpage() ) {
882                                                 wfDebug( __METHOD__ . ": showing CSS/JS source\n" );
883                                                 $this->showCssOrJsPage();
884                                                 $outputDone = true;
885                                         } else if ( $rt = Title::newFromRedirectArray( $text ) ) {
886                                                 wfDebug( __METHOD__ . ": showing redirect=no page\n" );
887                                                 # Viewing a redirect page (e.g. with parameter redirect=no)
888                                                 # Don't append the subtitle if this was an old revision
889                                                 $wgOut->addHTML( $this->viewRedirect( $rt, !$wasRedirected && $this->isCurrent() ) );
890                                                 # Parse just to get categories, displaytitle, etc.
891                                                 $this->mParserOutput = $wgParser->parse( $text, $this->mTitle, $parserOptions );
892                                                 $wgOut->addParserOutputNoText( $this->mParserOutput );
893                                                 $outputDone = true;
894                                         }
895                                         break;
896
897                                 case 4:
898                                         # Run the parse, protected by a pool counter
899                                         wfDebug( __METHOD__ . ": doing uncached parse\n" );
900                                         $key = $parserCache->getKey( $this, $parserOptions );
901                                         $poolCounter = PoolCounter::factory( 'Article::view', $key );
902                                         $dirtyCallback = $useParserCache ? array( $this, 'tryDirtyCache' ) : false;
903                                         $status = $poolCounter->executeProtected( array( $this, 'doViewParse' ), $dirtyCallback );
904
905                                         if ( !$status->isOK() ) {
906                                                 # Connection or timeout error
907                                                 $this->showPoolError( $status );
908                                                 wfProfileOut( __METHOD__ );
909                                                 return;
910                                         } else {
911                                                 $outputDone = true;
912                                         }
913                                         break;
914
915                                 # Should be unreachable, but just in case...
916                                 default:
917                                         break 2;
918                         }
919                 }
920
921                 # Adjust the title if it was set by displaytitle, -{T|}- or language conversion
922                 if ( $this->mParserOutput ) {
923                         $titleText = $this->mParserOutput->getTitleText();
924                         if ( strval( $titleText ) !== '' ) {
925                                 $wgOut->setPageTitle( $titleText );
926                         }
927                 }
928
929                 # For the main page, overwrite the <title> element with the con-
930                 # tents of 'pagetitle-view-mainpage' instead of the default (if
931                 # that's not empty).
932                 if ( $this->mTitle->equals( Title::newMainPage() )
933                         && ( $m = wfMsgForContent( 'pagetitle-view-mainpage' ) ) !== '' )
934                 {
935                         $wgOut->setHTMLTitle( $m );
936                 }
937
938                 # Now that we've filled $this->mParserOutput, we know whether
939                 # there are any __NOINDEX__ tags on the page
940                 $policy = $this->getRobotPolicy( 'view' );
941                 $wgOut->setIndexPolicy( $policy['index'] );
942                 $wgOut->setFollowPolicy( $policy['follow'] );
943
944                 $this->showViewFooter();
945                 $this->viewUpdates();
946                 wfProfileOut( __METHOD__ );
947         }
948
949         /**
950          * Show a diff page according to current request variables. For use within
951          * Article::view() only, other callers should use the DifferenceEngine class.
952          */
953         public function showDiffPage() {
954                 global $wgOut, $wgRequest, $wgUser;
955
956                 $diff = $wgRequest->getVal( 'diff' );
957                 $rcid = $wgRequest->getVal( 'rcid' );
958                 $diffOnly = $wgRequest->getBool( 'diffonly', $wgUser->getOption( 'diffonly' ) );
959                 $purge = $wgRequest->getVal( 'action' ) == 'purge';
960                 $unhide = $wgRequest->getInt( 'unhide' ) == 1;
961                 $oldid = $this->getOldID();
962
963                 $de = new DifferenceEngine( $this->mTitle, $oldid, $diff, $rcid, $purge, $unhide );
964                 // DifferenceEngine directly fetched the revision:
965                 $this->mRevIdFetched = $de->mNewid;
966                 $de->showDiffPage( $diffOnly );
967
968                 // Needed to get the page's current revision
969                 $this->loadPageData();
970                 if ( $diff == 0 || $diff == $this->mLatest ) {
971                         # Run view updates for current revision only
972                         $this->viewUpdates();
973                 }
974         }
975
976         /**
977          * Show a page view for a page formatted as CSS or JavaScript. To be called by
978          * Article::view() only.
979          *
980          * This is hooked by SyntaxHighlight_GeSHi to do syntax highlighting of these
981          * page views.
982          */
983         public function showCssOrJsPage() {
984                 global $wgOut;
985                 $wgOut->addHTML( wfMsgExt( 'clearyourcache', 'parse' ) );
986                 // Give hooks a chance to customise the output
987                 if ( wfRunHooks( 'ShowRawCssJs', array( $this->mContent, $this->mTitle, $wgOut ) ) ) {
988                         // Wrap the whole lot in a <pre> and don't parse
989                         $m = array();
990                         preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m );
991                         $wgOut->addHTML( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
992                         $wgOut->addHTML( htmlspecialchars( $this->mContent ) );
993                         $wgOut->addHTML( "\n</pre>\n" );
994                 }
995         }
996
997         /**
998          * Get the robot policy to be used for the current action=view request.
999          * @return String the policy that should be set
1000          * @deprecated use getRobotPolicy() instead, which returns an associative
1001          *    array
1002          */
1003         public function getRobotPolicyForView() {
1004                 wfDeprecated( __FUNC__ );
1005                 $policy = $this->getRobotPolicy( 'view' );
1006                 return $policy['index'] . ',' . $policy['follow'];
1007         }
1008
1009         /**
1010          * Get the robot policy to be used for the current view
1011          * @param $action String the action= GET parameter
1012          * @return Array the policy that should be set
1013          * TODO: actions other than 'view'
1014          */
1015         public function getRobotPolicy( $action ) {
1016
1017                 global $wgOut, $wgArticleRobotPolicies, $wgNamespaceRobotPolicies;
1018                 global $wgDefaultRobotPolicy, $wgRequest;
1019
1020                 $ns = $this->mTitle->getNamespace();
1021                 if ( $ns == NS_USER || $ns == NS_USER_TALK ) {
1022                         # Don't index user and user talk pages for blocked users (bug 11443)
1023                         if ( !$this->mTitle->isSubpage() ) {
1024                                 $block = new Block();
1025                                 if ( $block->load( $this->mTitle->getText() ) ) {
1026                                         return array( 'index'  => 'noindex',
1027                                                       'follow' => 'nofollow' );
1028                                 }
1029                         }
1030                 }
1031
1032                 if ( $this->getID() === 0 || $this->getOldID() ) {
1033                         # Non-articles (special pages etc), and old revisions
1034                         return array( 'index'  => 'noindex',
1035                                       'follow' => 'nofollow' );
1036                 } elseif ( $wgOut->isPrintable() ) {
1037                         # Discourage indexing of printable versions, but encourage following
1038                         return array( 'index'  => 'noindex',
1039                                       'follow' => 'follow' );
1040                 } elseif ( $wgRequest->getInt( 'curid' ) ) {
1041                         # For ?curid=x urls, disallow indexing
1042                         return array( 'index'  => 'noindex',
1043                                       'follow' => 'follow' );
1044                 }
1045
1046                 # Otherwise, construct the policy based on the various config variables.
1047                 $policy = self::formatRobotPolicy( $wgDefaultRobotPolicy );
1048
1049                 if ( isset( $wgNamespaceRobotPolicies[$ns] ) ) {
1050                         # Honour customised robot policies for this namespace
1051                         $policy = array_merge( $policy,
1052                                                self::formatRobotPolicy( $wgNamespaceRobotPolicies[$ns] ) );
1053                 }
1054                 if ( $this->mTitle->canUseNoindex() && is_object( $this->mParserOutput ) && $this->mParserOutput->getIndexPolicy() ) {
1055                         # __INDEX__ and __NOINDEX__ magic words, if allowed. Incorporates
1056                         # a final sanity check that we have really got the parser output.
1057                         $policy = array_merge( $policy,
1058                                                array( 'index' => $this->mParserOutput->getIndexPolicy() ) );
1059                 }
1060
1061                 if ( isset( $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()] ) ) {
1062                         # (bug 14900) site config can override user-defined __INDEX__ or __NOINDEX__
1063                         $policy = array_merge( $policy,
1064                                                self::formatRobotPolicy( $wgArticleRobotPolicies[$this->mTitle->getPrefixedText()] ) );
1065                 }
1066
1067                 return $policy;
1068
1069         }
1070
1071         /**
1072          * Converts a String robot policy into an associative array, to allow
1073          * merging of several policies using array_merge().
1074          * @param $policy Mixed, returns empty array on null/false/'', transparent
1075          *            to already-converted arrays, converts String.
1076          * @return associative Array: 'index' => <indexpolicy>, 'follow' => <followpolicy>
1077          */
1078         public static function formatRobotPolicy( $policy ) {
1079                 if ( is_array( $policy ) ) {
1080                         return $policy;
1081                 } elseif ( !$policy ) {
1082                         return array();
1083                 }
1084
1085                 $policy = explode( ',', $policy );
1086                 $policy = array_map( 'trim', $policy );
1087
1088                 $arr = array();
1089                 foreach ( $policy as $var ) {
1090                         if ( in_array( $var, array( 'index', 'noindex' ) ) ) {
1091                                 $arr['index'] = $var;
1092                         } elseif ( in_array( $var, array( 'follow', 'nofollow' ) ) ) {
1093                                 $arr['follow'] = $var;
1094                         }
1095                 }
1096                 return $arr;
1097         }
1098
1099         /**
1100          * If this request is a redirect view, send "redirected from" subtitle to
1101          * $wgOut. Returns true if the header was needed, false if this is not a
1102          * redirect view. Handles both local and remote redirects.
1103          */
1104         public function showRedirectedFromHeader() {
1105                 global $wgOut, $wgUser, $wgRequest, $wgRedirectSources;
1106
1107                 $rdfrom = $wgRequest->getVal( 'rdfrom' );
1108                 $sk = $wgUser->getSkin();
1109                 if ( isset( $this->mRedirectedFrom ) ) {
1110                         // This is an internally redirected page view.
1111                         // We'll need a backlink to the source page for navigation.
1112                         if ( wfRunHooks( 'ArticleViewRedirect', array( &$this ) ) ) {
1113                                 $redir = $sk->link(
1114                                         $this->mRedirectedFrom,
1115                                         null,
1116                                         array(),
1117                                         array( 'redirect' => 'no' ),
1118                                         array( 'known', 'noclasses' )
1119                                 );
1120                                 $s = wfMsgExt( 'redirectedfrom', array( 'parseinline', 'replaceafter' ), $redir );
1121                                 $wgOut->setSubtitle( $s );
1122
1123                                 // Set the fragment if one was specified in the redirect
1124                                 if ( strval( $this->mTitle->getFragment() ) != '' ) {
1125                                         $fragment = Xml::escapeJsString( $this->mTitle->getFragmentForURL() );
1126                                         $wgOut->addInlineScript( "redirectToFragment(\"$fragment\");" );
1127                                 }
1128
1129                                 // Add a <link rel="canonical"> tag
1130                                 $wgOut->addLink( array( 'rel' => 'canonical',
1131                                         'href' => $this->mTitle->getLocalURL() )
1132                                 );
1133                                 return true;
1134                         }
1135                 } elseif ( $rdfrom ) {
1136                         // This is an externally redirected view, from some other wiki.
1137                         // If it was reported from a trusted site, supply a backlink.
1138                         if ( $wgRedirectSources && preg_match( $wgRedirectSources, $rdfrom ) ) {
1139                                 $redir = $sk->makeExternalLink( $rdfrom, $rdfrom );
1140                                 $s = wfMsgExt( 'redirectedfrom', array( 'parseinline', 'replaceafter' ), $redir );
1141                                 $wgOut->setSubtitle( $s );
1142                                 return true;
1143                         }
1144                 }
1145                 return false;
1146         }
1147
1148         /**
1149          * Show a header specific to the namespace currently being viewed, like
1150          * [[MediaWiki:Talkpagetext]]. For Article::view().
1151          */
1152         public function showNamespaceHeader() {
1153                 global $wgOut;
1154                 if ( $this->mTitle->isTalkPage() ) {
1155                         $msg = wfMsgNoTrans( 'talkpageheader' );
1156                         if ( $msg !== '-' && !wfEmptyMsg( 'talkpageheader', $msg ) ) {
1157                                 $wgOut->wrapWikiMsg( "<div class=\"mw-talkpageheader\">\n$1</div>", array( 'talkpageheader' ) );
1158                         }
1159                 }
1160         }
1161
1162         /**
1163          * Show the footer section of an ordinary page view
1164          */
1165         public function showViewFooter() {
1166                 global $wgOut, $wgUseTrackbacks, $wgRequest;
1167                 # check if we're displaying a [[User talk:x.x.x.x]] anonymous talk page
1168                 if ( $this->mTitle->getNamespace() == NS_USER_TALK && IP::isValid( $this->mTitle->getText() ) ) {
1169                         $wgOut->addWikiMsg( 'anontalkpagetext' );
1170                 }
1171
1172                 # If we have been passed an &rcid= parameter, we want to give the user a
1173                 # chance to mark this new article as patrolled.
1174                 $this->showPatrolFooter();
1175
1176                 # Trackbacks
1177                 if ( $wgUseTrackbacks ) {
1178                         $this->addTrackbacks();
1179                 }
1180         }
1181
1182         /**
1183          * If patrol is possible, output a patrol UI box. This is called from the
1184          * footer section of ordinary page views. If patrol is not possible or not
1185          * desired, does nothing.
1186          */
1187         public function showPatrolFooter() {
1188                 global $wgOut, $wgRequest, $wgUser;
1189                 $rcid = $wgRequest->getVal( 'rcid' );
1190
1191                 if ( !$rcid || !$this->mTitle->exists() || !$this->mTitle->quickUserCan( 'patrol' ) ) {
1192                         return;
1193                 }
1194
1195                 $sk = $wgUser->getSkin();
1196
1197                 $wgOut->addHTML(
1198                         "<div class='patrollink'>" .
1199                                 wfMsgHtml(
1200                                         'markaspatrolledlink',
1201                                         $sk->link(
1202                                                 $this->mTitle,
1203                                                 wfMsgHtml( 'markaspatrolledtext' ),
1204                                                 array(),
1205                                                 array(
1206                                                         'action' => 'markpatrolled',
1207                                                         'rcid' => $rcid
1208                                                 ),
1209                                                 array( 'known', 'noclasses' )
1210                                         )
1211                                 ) .
1212                         '</div>'
1213                 );
1214         }
1215
1216         /**
1217          * Show the error text for a missing article. For articles in the MediaWiki
1218          * namespace, show the default message text. To be called from Article::view().
1219          */
1220         public function showMissingArticle() {
1221                 global $wgOut, $wgRequest, $wgUser;
1222
1223                 # Show info in user (talk) namespace. Does the user exist? Is he blocked?
1224                 if ( $this->mTitle->getNamespace() == NS_USER || $this->mTitle->getNamespace() == NS_USER_TALK ) {
1225                         $parts = explode( '/', $this->mTitle->getText() );
1226                         $rootPart = $parts[0];
1227                         $user = User::newFromName( $rootPart, false /* allow IP users*/ );
1228                         $ip = User::isIP( $rootPart );
1229                         if ( !$user->isLoggedIn() && !$ip ) { # User does not exist
1230                                 $wgOut->wrapWikiMsg( "<div class=\"mw-userpage-userdoesnotexist error\">\n\$1</div>",
1231                                         array( 'userpage-userdoesnotexist-view', $rootPart ) );
1232                         } else if ( $user->isBlocked() ) { # Show log extract if the user is currently blocked
1233                                 LogEventsList::showLogExtract(
1234                                         $wgOut,
1235                                         'block',
1236                                         $user->getUserPage()->getPrefixedText(),
1237                                         '',
1238                                         array(
1239                                                 'lim' => 1,
1240                                                 'showIfEmpty' => false,
1241                                                 'msgKey' => array(
1242                                                         'blocked-notice-logextract',
1243                                                         $user->getName() # Support GENDER in notice
1244                                                 )
1245                                         )
1246                                 );
1247                         }
1248                 }
1249                 wfRunHooks( 'ShowMissingArticle', array( $this ) );
1250                 # Show delete and move logs
1251                 LogEventsList::showLogExtract( $wgOut, array( 'delete', 'move' ), $this->mTitle->getPrefixedText(), '',
1252                         array(  'lim' => 10,
1253                                 'conds' => array( "log_action != 'revision'" ),
1254                                 'showIfEmpty' => false,
1255                                 'msgKey' => array( 'moveddeleted-notice' ) )
1256                 );
1257
1258                 # Show error message
1259                 $oldid = $this->getOldID();
1260                 if ( $oldid ) {
1261                         $text = wfMsgNoTrans( 'missing-article',
1262                                 $this->mTitle->getPrefixedText(),
1263                                 wfMsgNoTrans( 'missingarticle-rev', $oldid ) );
1264                 } elseif ( $this->mTitle->getNamespace() === NS_MEDIAWIKI ) {
1265                         // Use the default message text
1266                         $text = $this->getContent();
1267                 } else {
1268                         $createErrors = $this->mTitle->getUserPermissionsErrors( 'create', $wgUser );
1269                         $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser );
1270                         $errors = array_merge( $createErrors, $editErrors );
1271
1272                         if ( !count( $errors ) )
1273                                 $text = wfMsgNoTrans( 'noarticletext' );
1274                         else
1275                                 $text = wfMsgNoTrans( 'noarticletext-nopermission' );
1276                 }
1277                 $text = "<div class='noarticletext'>\n$text\n</div>";
1278                 if ( !$this->hasViewableContent() ) {
1279                         // If there's no backing content, send a 404 Not Found
1280                         // for better machine handling of broken links.
1281                         $wgRequest->response()->header( "HTTP/1.x 404 Not Found" );
1282                 }
1283                 $wgOut->addWikiText( $text );
1284         }
1285
1286         /**
1287          * If the revision requested for view is deleted, check permissions.
1288          * Send either an error message or a warning header to $wgOut.
1289          * Returns true if the view is allowed, false if not.
1290          */
1291         public function showDeletedRevisionHeader() {
1292                 global $wgOut, $wgRequest;
1293                 if ( !$this->mRevision->isDeleted( Revision::DELETED_TEXT ) ) {
1294                         // Not deleted
1295                         return true;
1296                 }
1297                 // If the user is not allowed to see it...
1298                 if ( !$this->mRevision->userCan( Revision::DELETED_TEXT ) ) {
1299                         $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n",
1300                                 'rev-deleted-text-permission' );
1301                         return false;
1302                 // If the user needs to confirm that they want to see it...
1303                 } else if ( $wgRequest->getInt( 'unhide' ) != 1 ) {
1304                         # Give explanation and add a link to view the revision...
1305                         $oldid = intval( $this->getOldID() );
1306                         $link = $this->mTitle->getFullUrl( "oldid={$oldid}&unhide=1" );
1307                         $msg = $this->mRevision->isDeleted( Revision::DELETED_RESTRICTED ) ?
1308                                 'rev-suppressed-text-unhide' : 'rev-deleted-text-unhide';
1309                         $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n",
1310                                 array( $msg, $link ) );
1311                         return false;
1312                 // We are allowed to see...
1313                 } else {
1314                         $msg = $this->mRevision->isDeleted( Revision::DELETED_RESTRICTED ) ?
1315                                 'rev-suppressed-text-view' : 'rev-deleted-text-view';
1316                         $wgOut->wrapWikiMsg( "<div class='mw-warning plainlinks'>\n$1</div>\n", $msg );
1317                         return true;
1318                 }
1319         }
1320
1321         /*
1322         * Should the parser cache be used?
1323         */
1324         public function useParserCache( $oldid ) {
1325                 global $wgUser, $wgEnableParserCache;
1326
1327                 return $wgEnableParserCache
1328                         && intval( $wgUser->getOption( 'stubthreshold' ) ) == 0
1329                         && $this->exists()
1330                         && empty( $oldid )
1331                         && !$this->mTitle->isCssOrJsPage()
1332                         && !$this->mTitle->isCssJsSubpage();
1333         }
1334
1335         /**
1336          * Execute the uncached parse for action=view
1337          */
1338         public function doViewParse() {
1339                 global $wgOut;
1340                 $oldid = $this->getOldID();
1341                 $useParserCache = $this->useParserCache( $oldid );
1342                 $parserOptions = clone $this->getParserOptions();
1343                 # Render printable version, use printable version cache
1344                 $parserOptions->setIsPrintable( $wgOut->isPrintable() );
1345                 # Don't show section-edit links on old revisions... this way lies madness.
1346                 $parserOptions->setEditSection( $this->isCurrent() );
1347                 $useParserCache = $this->useParserCache( $oldid );
1348                 $this->outputWikiText( $this->getContent(), $useParserCache, $parserOptions );
1349         }
1350
1351         /**
1352          * Try to fetch an expired entry from the parser cache. If it is present,
1353          * output it and return true. If it is not present, output nothing and
1354          * return false. This is used as a callback function for
1355          * PoolCounter::executeProtected().
1356          */
1357         public function tryDirtyCache() {
1358                 global $wgOut;
1359                 $parserCache = ParserCache::singleton();
1360                 $options = $this->getParserOptions();
1361                 $options->setIsPrintable( $wgOut->isPrintable() );
1362                 $output = $parserCache->getDirty( $this, $options );
1363                 if ( $output ) {
1364                         wfDebug( __METHOD__ . ": sending dirty output\n" );
1365                         wfDebugLog( 'dirty', "dirty output " . $parserCache->getKey( $this, $options ) . "\n" );
1366                         $wgOut->setSquidMaxage( 0 );
1367                         $this->mParserOutput = $output;
1368                         $wgOut->addParserOutput( $output );
1369                         $wgOut->addHTML( "<!-- parser cache is expired, sending anyway due to pool overload-->\n" );
1370                         return true;
1371                 } else {
1372                         wfDebugLog( 'dirty', "dirty missing\n" );
1373                         wfDebug( __METHOD__ . ": no dirty cache\n" );
1374                         return false;
1375                 }
1376         }
1377
1378         /**
1379          * Show an error page for an error from the pool counter.
1380          * @param $status Status
1381          */
1382         public function showPoolError( $status ) {
1383                 global $wgOut;
1384                 $wgOut->clearHTML(); // for release() errors
1385                 $wgOut->enableClientCache( false );
1386                 $wgOut->setRobotPolicy( 'noindex,nofollow' );
1387                 $wgOut->addWikiText(
1388                         '<div class="errorbox">' .
1389                         $status->getWikiText( false, 'view-pool-error' ) .
1390                         '</div>'
1391                 );
1392         }
1393
1394         /**
1395          * View redirect
1396          * @param $target Title object or Array of destination(s) to redirect
1397          * @param $appendSubtitle Boolean [optional]
1398          * @param $forceKnown Boolean: should the image be shown as a bluelink regardless of existence?
1399          */
1400         public function viewRedirect( $target, $appendSubtitle = true, $forceKnown = false ) {
1401                 global $wgOut, $wgContLang, $wgStylePath, $wgUser;
1402                 # Display redirect
1403                 if ( !is_array( $target ) ) {
1404                         $target = array( $target );
1405                 }
1406                 $imageDir = $wgContLang->getDir();
1407                 $imageUrl = $wgStylePath . '/common/images/redirect' . $imageDir . '.png';
1408                 $imageUrl2 = $wgStylePath . '/common/images/nextredirect' . $imageDir . '.png';
1409                 $alt2 = $wgContLang->isRTL() ? '&larr;' : '&rarr;'; // should -> and <- be used instead of entities?
1410
1411                 if ( $appendSubtitle ) {
1412                         $wgOut->appendSubtitle( wfMsgHtml( 'redirectpagesub' ) );
1413                 }
1414                 $sk = $wgUser->getSkin();
1415                 // the loop prepends the arrow image before the link, so the first case needs to be outside
1416                 $title = array_shift( $target );
1417                 if ( $forceKnown ) {
1418                         $link = $sk->link(
1419                                 $title,
1420                                 htmlspecialchars( $title->getFullText() ),
1421                                 array(),
1422                                 array(),
1423                                 array( 'known', 'noclasses' )
1424                         );
1425                 } else {
1426                         $link = $sk->link( $title, htmlspecialchars( $title->getFullText() ) );
1427                 }
1428                 // automatically append redirect=no to each link, since most of them are redirect pages themselves
1429                 foreach ( $target as $rt ) {
1430                         if ( $forceKnown ) {
1431                                 $link .= '<img src="' . $imageUrl2 . '" alt="' . $alt2 . ' " />'
1432                                         . $sk->link(
1433                                                 $rt,
1434                                                 htmlspecialchars( $rt->getFullText() ),
1435                                                 array(),
1436                                                 array(),
1437                                                 array( 'known', 'noclasses' )
1438                                         );
1439                         } else {
1440                                 $link .= '<img src="' . $imageUrl2 . '" alt="' . $alt2 . ' " />'
1441                                         . $sk->link( $rt, htmlspecialchars( $rt->getFullText() ) );
1442                         }
1443                 }
1444                 return '<img src="' . $imageUrl . '" alt="#REDIRECT " />' .
1445                         '<span class="redirectText">' . $link . '</span>';
1446
1447         }
1448
1449         public function addTrackbacks() {
1450                 global $wgOut, $wgUser;
1451                 $dbr = wfGetDB( DB_SLAVE );
1452                 $tbs = $dbr->select( 'trackbacks',
1453                         array( 'tb_id', 'tb_title', 'tb_url', 'tb_ex', 'tb_name' ),
1454                         array( 'tb_page' => $this->getID() )
1455                 );
1456                 if ( !$dbr->numRows( $tbs ) ) return;
1457
1458                 $wgOut->preventClickjacking();
1459
1460                 $tbtext = "";
1461                 while ( $o = $dbr->fetchObject( $tbs ) ) {
1462                         $rmvtxt = "";
1463                         if ( $wgUser->isAllowed( 'trackback' ) ) {
1464                                 $delurl = $this->mTitle->getFullURL( "action=deletetrackback&tbid=" .
1465                                         $o->tb_id . "&token=" . urlencode( $wgUser->editToken() ) );
1466                                 $rmvtxt = wfMsg( 'trackbackremove', htmlspecialchars( $delurl ) );
1467                         }
1468                         $tbtext .= "\n";
1469                         $tbtext .= wfMsg( strlen( $o->tb_ex ) ? 'trackbackexcerpt' : 'trackback',
1470                                         $o->tb_title,
1471                                         $o->tb_url,
1472                                         $o->tb_ex,
1473                                         $o->tb_name,
1474                                         $rmvtxt );
1475                 }
1476                 $wgOut->wrapWikiMsg( "<div id='mw_trackbacks'>$1</div>\n", array( 'trackbackbox', $tbtext ) );
1477                 $this->mTitle->invalidateCache();
1478         }
1479
1480         public function deletetrackback() {
1481                 global $wgUser, $wgRequest, $wgOut;
1482                 if ( !$wgUser->matchEditToken( $wgRequest->getVal( 'token' ) ) ) {
1483                         $wgOut->addWikiMsg( 'sessionfailure' );
1484                         return;
1485                 }
1486
1487                 $permission_errors = $this->mTitle->getUserPermissionsErrors( 'delete', $wgUser );
1488                 if ( count( $permission_errors ) ) {
1489                         $wgOut->showPermissionsErrorPage( $permission_errors );
1490                         return;
1491                 }
1492
1493                 $db = wfGetDB( DB_MASTER );
1494                 $db->delete( 'trackbacks', array( 'tb_id' => $wgRequest->getInt( 'tbid' ) ) );
1495
1496                 $wgOut->addWikiMsg( 'trackbackdeleteok' );
1497                 $this->mTitle->invalidateCache();
1498         }
1499
1500         public function render() {
1501                 global $wgOut;
1502                 $wgOut->setArticleBodyOnly( true );
1503                 $this->view();
1504         }
1505
1506         /**
1507          * Handle action=purge
1508          */
1509         public function purge() {
1510                 global $wgUser, $wgRequest, $wgOut;
1511                 if ( $wgUser->isAllowed( 'purge' ) || $wgRequest->wasPosted() ) {
1512                         if ( wfRunHooks( 'ArticlePurge', array( &$this ) ) ) {
1513                                 $this->doPurge();
1514                                 $this->view();
1515                         }
1516                 } else {
1517                         $action = htmlspecialchars( $wgRequest->getRequestURL() );
1518                         $button = wfMsgExt( 'confirm_purge_button', array( 'escapenoentities' ) );
1519                         $form = "<form method=\"post\" action=\"$action\">\n" .
1520                                         "<input type=\"submit\" name=\"submit\" value=\"$button\" />\n" .
1521                                         "</form>\n";
1522                         $top = wfMsgExt( 'confirm-purge-top', array( 'parse' ) );
1523                         $bottom = wfMsgExt( 'confirm-purge-bottom', array( 'parse' ) );
1524                         $wgOut->setPageTitle( $this->mTitle->getPrefixedText() );
1525                         $wgOut->setRobotPolicy( 'noindex,nofollow' );
1526                         $wgOut->addHTML( $top . $form . $bottom );
1527                 }
1528         }
1529
1530         /**
1531          * Perform the actions of a page purging
1532          */
1533         public function doPurge() {
1534                 global $wgUseSquid;
1535                 // Invalidate the cache
1536                 $this->mTitle->invalidateCache();
1537
1538                 if ( $wgUseSquid ) {
1539                         // Commit the transaction before the purge is sent
1540                         $dbw = wfGetDB( DB_MASTER );
1541                         $dbw->commit();
1542
1543                         // Send purge
1544                         $update = SquidUpdate::newSimplePurge( $this->mTitle );
1545                         $update->doUpdate();
1546                 }
1547                 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
1548                         global $wgMessageCache;
1549                         if ( $this->getID() == 0 ) {
1550                                 $text = false;
1551                         } else {
1552                                 $text = $this->getRawText();
1553                         }
1554                         $wgMessageCache->replace( $this->mTitle->getDBkey(), $text );
1555                 }
1556         }
1557
1558         /**
1559          * Insert a new empty page record for this article.
1560          * This *must* be followed up by creating a revision
1561          * and running $this->updateToLatest( $rev_id );
1562          * or else the record will be left in a funky state.
1563          * Best if all done inside a transaction.
1564          *
1565          * @param $dbw Database
1566          * @return int The newly created page_id key, or false if the title already existed
1567          * @private
1568          */
1569         public function insertOn( $dbw ) {
1570                 wfProfileIn( __METHOD__ );
1571
1572                 $page_id = $dbw->nextSequenceValue( 'page_page_id_seq' );
1573                 $dbw->insert( 'page', array(
1574                         'page_id'           => $page_id,
1575                         'page_namespace'    => $this->mTitle->getNamespace(),
1576                         'page_title'        => $this->mTitle->getDBkey(),
1577                         'page_counter'      => 0,
1578                         'page_restrictions' => '',
1579                         'page_is_redirect'  => 0, # Will set this shortly...
1580                         'page_is_new'       => 1,
1581                         'page_random'       => wfRandom(),
1582                         'page_touched'      => $dbw->timestamp(),
1583                         'page_latest'       => 0, # Fill this in shortly...
1584                         'page_len'          => 0, # Fill this in shortly...
1585                 ), __METHOD__, 'IGNORE' );
1586
1587                 $affected = $dbw->affectedRows();
1588                 if ( $affected ) {
1589                         $newid = $dbw->insertId();
1590                         $this->mTitle->resetArticleId( $newid );
1591                 }
1592                 wfProfileOut( __METHOD__ );
1593                 return $affected ? $newid : false;
1594         }
1595
1596         /**
1597          * Update the page record to point to a newly saved revision.
1598          *
1599          * @param $dbw Database object
1600          * @param $revision Revision: For ID number, and text used to set
1601                             length and redirect status fields
1602          * @param $lastRevision Integer: if given, will not overwrite the page field
1603          *                      when different from the currently set value.
1604          *                      Giving 0 indicates the new page flag should be set
1605          *                      on.
1606          * @param $lastRevIsRedirect Boolean: if given, will optimize adding and
1607          *                           removing rows in redirect table.
1608          * @return bool true on success, false on failure
1609          * @private
1610          */
1611         public function updateRevisionOn( &$dbw, $revision, $lastRevision = null, $lastRevIsRedirect = null ) {
1612                 wfProfileIn( __METHOD__ );
1613
1614                 $text = $revision->getText();
1615                 $rt = Title::newFromRedirect( $text );
1616
1617                 $conditions = array( 'page_id' => $this->getId() );
1618                 if ( !is_null( $lastRevision ) ) {
1619                         # An extra check against threads stepping on each other
1620                         $conditions['page_latest'] = $lastRevision;
1621                 }
1622
1623                 $dbw->update( 'page',
1624                         array( /* SET */
1625                                 'page_latest'      => $revision->getId(),
1626                                 'page_touched'     => $dbw->timestamp(),
1627                                 'page_is_new'      => ( $lastRevision === 0 ) ? 1 : 0,
1628                                 'page_is_redirect' => $rt !== null ? 1 : 0,
1629                                 'page_len'         => strlen( $text ),
1630                         ),
1631                         $conditions,
1632                         __METHOD__ );
1633
1634                 $result = $dbw->affectedRows() != 0;
1635                 if ( $result ) {
1636                         $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
1637                 }
1638
1639                 wfProfileOut( __METHOD__ );
1640                 return $result;
1641         }
1642
1643         /**
1644          * Add row to the redirect table if this is a redirect, remove otherwise.
1645          *
1646          * @param $dbw Database
1647          * @param $redirectTitle a title object pointing to the redirect target,
1648          *                       or NULL if this is not a redirect
1649          * @param $lastRevIsRedirect If given, will optimize adding and
1650          *                           removing rows in redirect table.
1651          * @return bool true on success, false on failure
1652          * @private
1653          */
1654         public function updateRedirectOn( &$dbw, $redirectTitle, $lastRevIsRedirect = null ) {
1655                 // Always update redirects (target link might have changed)
1656                 // Update/Insert if we don't know if the last revision was a redirect or not
1657                 // Delete if changing from redirect to non-redirect
1658                 $isRedirect = !is_null( $redirectTitle );
1659                 if ( $isRedirect || is_null( $lastRevIsRedirect ) || $lastRevIsRedirect !== $isRedirect ) {
1660                         wfProfileIn( __METHOD__ );
1661                         if ( $isRedirect ) {
1662                                 // This title is a redirect, Add/Update row in the redirect table
1663                                 $set = array( /* SET */
1664                                         'rd_namespace' => $redirectTitle->getNamespace(),
1665                                         'rd_title'     => $redirectTitle->getDBkey(),
1666                                         'rd_from'      => $this->getId(),
1667                                 );
1668                                 $dbw->replace( 'redirect', array( 'rd_from' ), $set, __METHOD__ );
1669                         } else {
1670                                 // This is not a redirect, remove row from redirect table
1671                                 $where = array( 'rd_from' => $this->getId() );
1672                                 $dbw->delete( 'redirect', $where, __METHOD__ );
1673                         }
1674                         if ( $this->getTitle()->getNamespace() == NS_FILE ) {
1675                                 RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() );
1676                         }
1677                         wfProfileOut( __METHOD__ );
1678                         return ( $dbw->affectedRows() != 0 );
1679                 }
1680                 return true;
1681         }
1682
1683         /**
1684          * If the given revision is newer than the currently set page_latest,
1685          * update the page record. Otherwise, do nothing.
1686          *
1687          * @param $dbw Database object
1688          * @param $revision Revision object
1689          */
1690         public function updateIfNewerOn( &$dbw, $revision ) {
1691                 wfProfileIn( __METHOD__ );
1692                 $row = $dbw->selectRow(
1693                         array( 'revision', 'page' ),
1694                         array( 'rev_id', 'rev_timestamp', 'page_is_redirect' ),
1695                         array(
1696                                 'page_id' => $this->getId(),
1697                                 'page_latest=rev_id' ),
1698                         __METHOD__ );
1699                 if ( $row ) {
1700                         if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) {
1701                                 wfProfileOut( __METHOD__ );
1702                                 return false;
1703                         }
1704                         $prev = $row->rev_id;
1705                         $lastRevIsRedirect = (bool)$row->page_is_redirect;
1706                 } else {
1707                         # No or missing previous revision; mark the page as new
1708                         $prev = 0;
1709                         $lastRevIsRedirect = null;
1710                 }
1711                 $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect );
1712                 wfProfileOut( __METHOD__ );
1713                 return $ret;
1714         }
1715
1716         /**
1717          * @param $section empty/null/false or a section number (0, 1, 2, T1, T2...)
1718          * @return string Complete article text, or null if error
1719          */
1720         public function replaceSection( $section, $text, $summary = '', $edittime = null ) {
1721                 wfProfileIn( __METHOD__ );
1722                 if ( strval( $section ) == '' ) {
1723                         // Whole-page edit; let the whole text through
1724                 } else {
1725                         if ( is_null( $edittime ) ) {
1726                                 $rev = Revision::newFromTitle( $this->mTitle );
1727                         } else {
1728                                 $dbw = wfGetDB( DB_MASTER );
1729                                 $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
1730                         }
1731                         if ( !$rev ) {
1732                                 wfDebug( "Article::replaceSection asked for bogus section (page: " .
1733                                         $this->getId() . "; section: $section; edittime: $edittime)\n" );
1734                                 return null;
1735                         }
1736                         $oldtext = $rev->getText();
1737
1738                         if ( $section == 'new' ) {
1739                                 # Inserting a new section
1740                                 $subject = $summary ? wfMsgForContent( 'newsectionheaderdefaultlevel', $summary ) . "\n\n" : '';
1741                                 $text = strlen( trim( $oldtext ) ) > 0
1742                                                 ? "{$oldtext}\n\n{$subject}{$text}"
1743                                                 : "{$subject}{$text}";
1744                         } else {
1745                                 # Replacing an existing section; roll out the big guns
1746                                 global $wgParser;
1747                                 $text = $wgParser->replaceSection( $oldtext, $section, $text );
1748                         }
1749                 }
1750                 wfProfileOut( __METHOD__ );
1751                 return $text;
1752         }
1753
1754         /**
1755          * This function is not deprecated until somebody fixes the core not to use
1756          * it. Nevertheless, use Article::doEdit() instead.
1757          */
1758         function insertNewArticle( $text, $summary, $isminor, $watchthis, $suppressRC = false, $comment = false, $bot = false ) {
1759                 $flags = EDIT_NEW | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY |
1760                         ( $isminor ? EDIT_MINOR : 0 ) |
1761                         ( $suppressRC ? EDIT_SUPPRESS_RC : 0 ) |
1762                         ( $bot ? EDIT_FORCE_BOT : 0 );
1763
1764                 # If this is a comment, add the summary as headline
1765                 if ( $comment && $summary != "" ) {
1766                         $text = wfMsgForContent( 'newsectionheaderdefaultlevel', $summary ) . "\n\n" . $text;
1767                 }
1768
1769                 $this->doEdit( $text, $summary, $flags );
1770
1771                 $dbw = wfGetDB( DB_MASTER );
1772                 if ( $watchthis ) {
1773                         if ( !$this->mTitle->userIsWatching() ) {
1774                                 $dbw->begin();
1775                                 $this->doWatch();
1776                                 $dbw->commit();
1777                         }
1778                 } else {
1779                         if ( $this->mTitle->userIsWatching() ) {
1780                                 $dbw->begin();
1781                                 $this->doUnwatch();
1782                                 $dbw->commit();
1783                         }
1784                 }
1785                 $this->doRedirect( $this->isRedirect( $text ) );
1786         }
1787
1788         /**
1789          * @deprecated use Article::doEdit()
1790          */
1791         function updateArticle( $text, $summary, $minor, $watchthis, $forceBot = false, $sectionanchor = '' ) {
1792                 $flags = EDIT_UPDATE | EDIT_DEFER_UPDATES | EDIT_AUTOSUMMARY |
1793                         ( $minor ? EDIT_MINOR : 0 ) |
1794                         ( $forceBot ? EDIT_FORCE_BOT : 0 );
1795
1796                 $status = $this->doEdit( $text, $summary, $flags );
1797                 if ( !$status->isOK() ) {
1798                         return false;
1799                 }
1800
1801                 $dbw = wfGetDB( DB_MASTER );
1802                 if ( $watchthis ) {
1803                         if ( !$this->mTitle->userIsWatching() ) {
1804                                 $dbw->begin();
1805                                 $this->doWatch();
1806                                 $dbw->commit();
1807                         }
1808                 } else {
1809                         if ( $this->mTitle->userIsWatching() ) {
1810                                 $dbw->begin();
1811                                 $this->doUnwatch();
1812                                 $dbw->commit();
1813                         }
1814                 }
1815
1816                 $extraQuery = ''; // Give extensions a chance to modify URL query on update
1817                 wfRunHooks( 'ArticleUpdateBeforeRedirect', array( $this, &$sectionanchor, &$extraQuery ) );
1818
1819                 $this->doRedirect( $this->isRedirect( $text ), $sectionanchor, $extraQuery );
1820                 return true;
1821         }
1822
1823         /**
1824          * Article::doEdit()
1825          *
1826          * Change an existing article or create a new article. Updates RC and all necessary caches,
1827          * optionally via the deferred update array.
1828          *
1829          * $wgUser must be set before calling this function.
1830          *
1831          * @param $text String: new text
1832          * @param $summary String: edit summary
1833          * @param $flags Integer bitfield:
1834          *      EDIT_NEW
1835          *          Article is known or assumed to be non-existent, create a new one
1836          *      EDIT_UPDATE
1837          *          Article is known or assumed to be pre-existing, update it
1838          *      EDIT_MINOR
1839          *          Mark this edit minor, if the user is allowed to do so
1840          *      EDIT_SUPPRESS_RC
1841          *          Do not log the change in recentchanges
1842          *      EDIT_FORCE_BOT
1843          *          Mark the edit a "bot" edit regardless of user rights
1844          *      EDIT_DEFER_UPDATES
1845          *          Defer some of the updates until the end of index.php
1846          *      EDIT_AUTOSUMMARY
1847          *          Fill in blank summaries with generated text where possible
1848          *
1849          * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the article will be detected.
1850          * If EDIT_UPDATE is specified and the article doesn't exist, the function will an
1851          * edit-gone-missing error. If EDIT_NEW is specified and the article does exist, an
1852          * edit-already-exists error will be returned. These two conditions are also possible with
1853          * auto-detection due to MediaWiki's performance-optimised locking strategy.
1854          *
1855          * @param $baseRevId the revision ID this edit was based off, if any
1856          * @param $user Optional user object, $wgUser will be used if not passed
1857          *
1858          * @return Status object. Possible errors:
1859          *     edit-hook-aborted:       The ArticleSave hook aborted the edit but didn't set the fatal flag of $status
1860          *     edit-gone-missing:       In update mode, but the article didn't exist
1861          *     edit-conflict:           In update mode, the article changed unexpectedly
1862          *     edit-no-change:          Warning that the text was the same as before
1863          *     edit-already-exists:     In creation mode, but the article already exists
1864          *
1865          *  Extensions may define additional errors.
1866          *
1867          *  $return->value will contain an associative array with members as follows:
1868          *     new:                     Boolean indicating if the function attempted to create a new article
1869          *     revision:                The revision object for the inserted revision, or null
1870          *
1871          *  Compatibility note: this function previously returned a boolean value indicating success/failure
1872          */
1873         public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
1874                 global $wgUser, $wgDBtransactions, $wgUseAutomaticEditSummaries;
1875
1876                 # Low-level sanity check
1877                 if ( $this->mTitle->getText() == '' ) {
1878                         throw new MWException( 'Something is trying to edit an article with an empty title' );
1879                 }
1880
1881                 wfProfileIn( __METHOD__ );
1882
1883                 $user = is_null( $user ) ? $wgUser : $user;
1884                 $status = Status::newGood( array() );
1885
1886                 # Load $this->mTitle->getArticleID() and $this->mLatest if it's not already
1887                 $this->loadPageData();
1888
1889                 if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
1890                         $aid = $this->mTitle->getArticleID();
1891                         if ( $aid ) {
1892                                 $flags |= EDIT_UPDATE;
1893                         } else {
1894                                 $flags |= EDIT_NEW;
1895                         }
1896                 }
1897
1898                 if ( !wfRunHooks( 'ArticleSave', array( &$this, &$user, &$text, &$summary,
1899                         $flags & EDIT_MINOR, null, null, &$flags, &$status ) ) )
1900                 {
1901                         wfDebug( __METHOD__ . ": ArticleSave hook aborted save!\n" );
1902                         wfProfileOut( __METHOD__ );
1903                         if ( $status->isOK() ) {
1904                                 $status->fatal( 'edit-hook-aborted' );
1905                         }
1906                         return $status;
1907                 }
1908
1909                 # Silently ignore EDIT_MINOR if not allowed
1910                 $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' );
1911                 $bot = $flags & EDIT_FORCE_BOT;
1912
1913                 $oldtext = $this->getRawText(); // current revision
1914                 $oldsize = strlen( $oldtext );
1915
1916                 # Provide autosummaries if one is not provided and autosummaries are enabled.
1917                 if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) {
1918                         $summary = $this->getAutosummary( $oldtext, $text, $flags );
1919                 }
1920
1921                 $editInfo = $this->prepareTextForEdit( $text );
1922                 $text = $editInfo->pst;
1923                 $newsize = strlen( $text );
1924
1925                 $dbw = wfGetDB( DB_MASTER );
1926                 $now = wfTimestampNow();
1927                 $this->mTimestamp = $now;
1928
1929                 if ( $flags & EDIT_UPDATE ) {
1930                         # Update article, but only if changed.
1931                         $status->value['new'] = false;
1932                         # Make sure the revision is either completely inserted or not inserted at all
1933                         if ( !$wgDBtransactions ) {
1934                                 $userAbort = ignore_user_abort( true );
1935                         }
1936
1937                         $revisionId = 0;
1938
1939                         $changed = ( strcmp( $text, $oldtext ) != 0 );
1940
1941                         if ( $changed ) {
1942                                 $this->mGoodAdjustment = (int)$this->isCountable( $text )
1943                                   - (int)$this->isCountable( $oldtext );
1944                                 $this->mTotalAdjustment = 0;
1945
1946                                 if ( !$this->mLatest ) {
1947                                         # Article gone missing
1948                                         wfDebug( __METHOD__ . ": EDIT_UPDATE specified but article doesn't exist\n" );
1949                                         $status->fatal( 'edit-gone-missing' );
1950                                         wfProfileOut( __METHOD__ );
1951                                         return $status;
1952                                 }
1953
1954                                 $revision = new Revision( array(
1955                                         'page'       => $this->getId(),
1956                                         'comment'    => $summary,
1957                                         'minor_edit' => $isminor,
1958                                         'text'       => $text,
1959                                         'parent_id'  => $this->mLatest,
1960                                         'user'       => $user->getId(),
1961                                         'user_text'  => $user->getName(),
1962                                         ) );
1963
1964                                 $dbw->begin();
1965                                 $revisionId = $revision->insertOn( $dbw );
1966
1967                                 # Update page
1968                                 #
1969                                 # Note that we use $this->mLatest instead of fetching a value from the master DB
1970                                 # during the course of this function. This makes sure that EditPage can detect
1971                                 # edit conflicts reliably, either by $ok here, or by $article->getTimestamp()
1972                                 # before this function is called. A previous function used a separate query, this
1973                                 # creates a window where concurrent edits can cause an ignored edit conflict.
1974                                 $ok = $this->updateRevisionOn( $dbw, $revision, $this->mLatest );
1975
1976                                 if ( !$ok ) {
1977                                         /* Belated edit conflict! Run away!! */
1978                                         $status->fatal( 'edit-conflict' );
1979                                         # Delete the invalid revision if the DB is not transactional
1980                                         if ( !$wgDBtransactions ) {
1981                                                 $dbw->delete( 'revision', array( 'rev_id' => $revisionId ), __METHOD__ );
1982                                         }
1983                                         $revisionId = 0;
1984                                         $dbw->rollback();
1985                                 } else {
1986                                         global $wgUseRCPatrol;
1987                                         wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) );
1988                                         # Update recentchanges
1989                                         if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1990                                                 # Mark as patrolled if the user can do so
1991                                                 $patrolled = $wgUseRCPatrol && $this->mTitle->userCan( 'autopatrol' );
1992                                                 # Add RC row to the DB
1993                                                 $rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary,
1994                                                         $this->mLatest, $this->getTimestamp(), $bot, '', $oldsize, $newsize,
1995                                                         $revisionId, $patrolled
1996                                                 );
1997                                                 # Log auto-patrolled edits
1998                                                 if ( $patrolled ) {
1999                                                         PatrolLog::record( $rc, true );
2000                                                 }
2001                                         }
2002                                         $user->incEditCount();
2003                                         $dbw->commit();
2004                                 }
2005                         } else {
2006                                 $status->warning( 'edit-no-change' );
2007                                 $revision = null;
2008                                 // Keep the same revision ID, but do some updates on it
2009                                 $revisionId = $this->getRevIdFetched();
2010                                 // Update page_touched, this is usually implicit in the page update
2011                                 // Other cache updates are done in onArticleEdit()
2012                                 $this->mTitle->invalidateCache();
2013                         }
2014
2015                         if ( !$wgDBtransactions ) {
2016                                 ignore_user_abort( $userAbort );
2017                         }
2018                         // Now that ignore_user_abort is restored, we can respond to fatal errors
2019                         if ( !$status->isOK() ) {
2020                                 wfProfileOut( __METHOD__ );
2021                                 return $status;
2022                         }
2023
2024                         # Invalidate cache of this article and all pages using this article
2025                         # as a template. Partly deferred.
2026                         Article::onArticleEdit( $this->mTitle );
2027                         # Update links tables, site stats, etc.
2028                         $this->editUpdates( $text, $summary, $isminor, $now, $revisionId, $changed );
2029                 } else {
2030                         # Create new article
2031                         $status->value['new'] = true;
2032
2033                         # Set statistics members
2034                         # We work out if it's countable after PST to avoid counter drift
2035                         # when articles are created with {{subst:}}
2036                         $this->mGoodAdjustment = (int)$this->isCountable( $text );
2037                         $this->mTotalAdjustment = 1;
2038
2039                         $dbw->begin();
2040
2041                         # Add the page record; stake our claim on this title!
2042                         # This will return false if the article already exists
2043                         $newid = $this->insertOn( $dbw );
2044
2045                         if ( $newid === false ) {
2046                                 $dbw->rollback();
2047                                 $status->fatal( 'edit-already-exists' );
2048                                 wfProfileOut( __METHOD__ );
2049                                 return $status;
2050                         }
2051
2052                         # Save the revision text...
2053                         $revision = new Revision( array(
2054                                 'page'       => $newid,
2055                                 'comment'    => $summary,
2056                                 'minor_edit' => $isminor,
2057                                 'text'       => $text,
2058                                 'user'       => $user->getId(),
2059                                 'user_text'  => $user->getName(),
2060                                 ) );
2061                         $revisionId = $revision->insertOn( $dbw );
2062
2063                         $this->mTitle->resetArticleID( $newid );
2064
2065                         # Update the page record with revision data
2066                         $this->updateRevisionOn( $dbw, $revision, 0 );
2067
2068                         wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) );
2069                         # Update recentchanges
2070                         if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
2071                                 global $wgUseRCPatrol, $wgUseNPPatrol;
2072                                 # Mark as patrolled if the user can do so
2073                                 $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && $this->mTitle->userCan( 'autopatrol' );
2074                                 # Add RC row to the DB
2075                                 $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot,
2076                                         '', strlen( $text ), $revisionId, $patrolled );
2077                                 # Log auto-patrolled edits
2078                                 if ( $patrolled ) {
2079                                         PatrolLog::record( $rc, true );
2080                                 }
2081                         }
2082                         $user->incEditCount();
2083                         $dbw->commit();
2084
2085                         # Update links, etc.
2086                         $this->editUpdates( $text, $summary, $isminor, $now, $revisionId, true );
2087
2088                         # Clear caches
2089                         Article::onArticleCreate( $this->mTitle );
2090
2091                         wfRunHooks( 'ArticleInsertComplete', array( &$this, &$user, $text, $summary,
2092                                 $flags & EDIT_MINOR, null, null, &$flags, $revision ) );
2093                 }
2094
2095                 # Do updates right now unless deferral was requested
2096                 if ( !( $flags & EDIT_DEFER_UPDATES ) ) {
2097                         wfDoUpdates();
2098                 }
2099
2100                 // Return the new revision (or null) to the caller
2101                 $status->value['revision'] = $revision;
2102
2103                 wfRunHooks( 'ArticleSaveComplete', array( &$this, &$user, $text, $summary,
2104                         $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ) );
2105
2106                 wfProfileOut( __METHOD__ );
2107                 return $status;
2108         }
2109
2110         /**
2111          * @deprecated wrapper for doRedirect
2112          */
2113         public function showArticle( $text, $subtitle , $sectionanchor = '', $me2, $now, $summary, $oldid ) {
2114                 wfDeprecated( __METHOD__ );
2115                 $this->doRedirect( $this->isRedirect( $text ), $sectionanchor );
2116         }
2117
2118         /**
2119          * Output a redirect back to the article.
2120          * This is typically used after an edit.
2121          *
2122          * @param $noRedir Boolean: add redirect=no
2123          * @param $sectionAnchor String: section to redirect to, including "#"
2124          * @param $extraQuery String: extra query params
2125          */
2126         public function doRedirect( $noRedir = false, $sectionAnchor = '', $extraQuery = '' ) {
2127                 global $wgOut;
2128                 if ( $noRedir ) {
2129                         $query = 'redirect=no';
2130                         if ( $extraQuery )
2131                                 $query .= "&$query";
2132                 } else {
2133                         $query = $extraQuery;
2134                 }
2135                 $wgOut->redirect( $this->mTitle->getFullURL( $query ) . $sectionAnchor );
2136         }
2137
2138         /**
2139          * Mark this particular edit/page as patrolled
2140          */
2141         public function markpatrolled() {
2142                 global $wgOut, $wgRequest, $wgUseRCPatrol, $wgUseNPPatrol, $wgUser;
2143                 $wgOut->setRobotPolicy( 'noindex,nofollow' );
2144
2145                 # If we haven't been given an rc_id value, we can't do anything
2146                 $rcid = (int) $wgRequest->getVal( 'rcid' );
2147                 $rc = RecentChange::newFromId( $rcid );
2148                 if ( is_null( $rc ) ) {
2149                         $wgOut->showErrorPage( 'markedaspatrollederror', 'markedaspatrollederrortext' );
2150                         return;
2151                 }
2152
2153                 # It would be nice to see where the user had actually come from, but for now just guess
2154                 $returnto = $rc->getAttribute( 'rc_type' ) == RC_NEW ? 'Newpages' : 'Recentchanges';
2155                 $return = SpecialPage::getTitleFor( $returnto );
2156
2157                 $dbw = wfGetDB( DB_MASTER );
2158                 $errors = $rc->doMarkPatrolled();
2159
2160                 if ( in_array( array( 'rcpatroldisabled' ), $errors ) ) {
2161                         $wgOut->showErrorPage( 'rcpatroldisabled', 'rcpatroldisabledtext' );
2162                         return;
2163                 }
2164
2165                 if ( in_array( array( 'hookaborted' ), $errors ) ) {
2166                         // The hook itself has handled any output
2167                         return;
2168                 }
2169
2170                 if ( in_array( array( 'markedaspatrollederror-noautopatrol' ), $errors ) ) {
2171                         $wgOut->setPageTitle( wfMsg( 'markedaspatrollederror' ) );
2172                         $wgOut->addWikiMsg( 'markedaspatrollederror-noautopatrol' );
2173                         $wgOut->returnToMain( false, $return );
2174                         return;
2175                 }
2176
2177                 if ( !empty( $errors ) ) {
2178                         $wgOut->showPermissionsErrorPage( $errors );
2179                         return;
2180                 }
2181
2182                 # Inform the user
2183                 $wgOut->setPageTitle( wfMsg( 'markedaspatrolled' ) );
2184                 $wgOut->addWikiMsg( 'markedaspatrolledtext', $rc->getTitle()->getPrefixedText() );
2185                 $wgOut->returnToMain( false, $return );
2186         }
2187
2188         /**
2189          * User-interface handler for the "watch" action
2190          */
2191         public function watch() {
2192                 global $wgUser, $wgOut;
2193                 if ( $wgUser->isAnon() ) {
2194                         $wgOut->showErrorPage( 'watchnologin', 'watchnologintext' );
2195                         return;
2196                 }
2197                 if ( wfReadOnly() ) {
2198                         $wgOut->readOnlyPage();
2199                         return;
2200                 }
2201                 if ( $this->doWatch() ) {
2202                         $wgOut->setPagetitle( wfMsg( 'addedwatch' ) );
2203                         $wgOut->setRobotPolicy( 'noindex,nofollow' );
2204                         $wgOut->addWikiMsg( 'addedwatchtext', $this->mTitle->getPrefixedText() );
2205                 }
2206                 $wgOut->returnToMain( true, $this->mTitle->getPrefixedText() );
2207         }
2208
2209         /**
2210          * Add this page to $wgUser's watchlist
2211          * @return bool true on successful watch operation
2212          */
2213         public function doWatch() {
2214                 global $wgUser;
2215                 if ( $wgUser->isAnon() ) {
2216                         return false;
2217                 }
2218                 if ( wfRunHooks( 'WatchArticle', array( &$wgUser, &$this ) ) ) {
2219                         $wgUser->addWatch( $this->mTitle );
2220                         return wfRunHooks( 'WatchArticleComplete', array( &$wgUser, &$this ) );
2221                 }
2222                 return false;
2223         }
2224
2225         /**
2226          * User interface handler for the "unwatch" action.
2227          */
2228         public function unwatch() {
2229                 global $wgUser, $wgOut;
2230                 if ( $wgUser->isAnon() ) {
2231                         $wgOut->showErrorPage( 'watchnologin', 'watchnologintext' );
2232                         return;
2233                 }
2234                 if ( wfReadOnly() ) {
2235                         $wgOut->readOnlyPage();
2236                         return;
2237                 }
2238                 if ( $this->doUnwatch() ) {
2239                         $wgOut->setPagetitle( wfMsg( 'removedwatch' ) );
2240                         $wgOut->setRobotPolicy( 'noindex,nofollow' );
2241                         $wgOut->addWikiMsg( 'removedwatchtext', $this->mTitle->getPrefixedText() );
2242                 }
2243                 $wgOut->returnToMain( true, $this->mTitle->getPrefixedText() );
2244         }
2245
2246         /**
2247          * Stop watching a page
2248          * @return bool true on successful unwatch
2249          */
2250         public function doUnwatch() {
2251                 global $wgUser;
2252                 if ( $wgUser->isAnon() ) {
2253                         return false;
2254                 }
2255                 if ( wfRunHooks( 'UnwatchArticle', array( &$wgUser, &$this ) ) ) {
2256                         $wgUser->removeWatch( $this->mTitle );
2257                         return wfRunHooks( 'UnwatchArticleComplete', array( &$wgUser, &$this ) );
2258                 }
2259                 return false;
2260         }
2261
2262         /**
2263          * action=protect handler
2264          */
2265         public function protect() {
2266                 $form = new ProtectionForm( $this );
2267                 $form->execute();
2268         }
2269
2270         /**
2271          * action=unprotect handler (alias)
2272          */
2273         public function unprotect() {
2274                 $this->protect();
2275         }
2276
2277         /**
2278          * Update the article's restriction field, and leave a log entry.
2279          *
2280          * @param $limit Array: set of restriction keys
2281          * @param $reason String
2282          * @param &$cascade Integer. Set to false if cascading protection isn't allowed.
2283          * @param $expiry Array: per restriction type expiration
2284          * @return bool true on success
2285          */
2286         public function updateRestrictions( $limit = array(), $reason = '', &$cascade = 0, $expiry = array() ) {
2287                 global $wgUser, $wgContLang;
2288
2289                 $restrictionTypes = $this->mTitle->getRestrictionTypes();
2290
2291                 $id = $this->mTitle->getArticleID();
2292                 if ( $id <= 0 ) {
2293                         wfDebug( "updateRestrictions failed: $id <= 0\n" );
2294                         return false;
2295                 }
2296
2297                 if ( wfReadOnly() ) {
2298                         wfDebug( "updateRestrictions failed: read-only\n" );
2299                         return false;
2300                 }
2301
2302                 if ( !$this->mTitle->userCan( 'protect' ) ) {
2303                         wfDebug( "updateRestrictions failed: insufficient permissions\n" );
2304                         return false;
2305                 }
2306
2307                 if ( !$cascade ) {
2308                         $cascade = false;
2309                 }
2310
2311                 // Take this opportunity to purge out expired restrictions
2312                 Title::purgeExpiredRestrictions();
2313
2314                 # FIXME: Same limitations as described in ProtectionForm.php (line 37);
2315                 # we expect a single selection, but the schema allows otherwise.
2316                 $current = array();
2317                 $updated = Article::flattenRestrictions( $limit );
2318                 $changed = false;
2319                 foreach ( $restrictionTypes as $action ) {
2320                         if ( isset( $expiry[$action] ) ) {
2321                                 # Get current restrictions on $action
2322                                 $aLimits = $this->mTitle->getRestrictions( $action );
2323                                 $current[$action] = implode( '', $aLimits );
2324                                 # Are any actual restrictions being dealt with here?
2325                                 $aRChanged = count( $aLimits ) || !empty( $limit[$action] );
2326                                 # If something changed, we need to log it. Checking $aRChanged
2327                                 # assures that "unprotecting" a page that is not protected does
2328                                 # not log just because the expiry was "changed".
2329                                 if ( $aRChanged && $this->mTitle->mRestrictionsExpiry[$action] != $expiry[$action] ) {
2330                                         $changed = true;
2331                                 }
2332                         }
2333                 }
2334
2335                 $current = Article::flattenRestrictions( $current );
2336
2337                 $changed = ( $changed || $current != $updated );
2338                 $changed = $changed || ( $updated && $this->mTitle->areRestrictionsCascading() != $cascade );
2339                 $protect = ( $updated != '' );
2340
2341                 # If nothing's changed, do nothing
2342                 if ( $changed ) {
2343                         if ( wfRunHooks( 'ArticleProtect', array( &$this, &$wgUser, $limit, $reason ) ) ) {
2344
2345                                 $dbw = wfGetDB( DB_MASTER );
2346
2347                                 # Prepare a null revision to be added to the history
2348                                 $modified = $current != '' && $protect;
2349                                 if ( $protect ) {
2350                                         $comment_type = $modified ? 'modifiedarticleprotection' : 'protectedarticle';
2351                                 } else {
2352                                         $comment_type = 'unprotectedarticle';
2353                                 }
2354                                 $comment = $wgContLang->ucfirst( wfMsgForContent( $comment_type, $this->mTitle->getPrefixedText() ) );
2355
2356                                 # Only restrictions with the 'protect' right can cascade...
2357                                 # Otherwise, people who cannot normally protect can "protect" pages via transclusion
2358                                 $editrestriction = isset( $limit['edit'] ) ? array( $limit['edit'] ) : $this->mTitle->getRestrictions( 'edit' );
2359                                 # The schema allows multiple restrictions
2360                                 if ( !in_array( 'protect', $editrestriction ) && !in_array( 'sysop', $editrestriction ) )
2361                                         $cascade = false;
2362                                 $cascade_description = '';
2363                                 if ( $cascade ) {
2364                                         $cascade_description = ' [' . wfMsgForContent( 'protect-summary-cascade' ) . ']';
2365                                 }
2366
2367                                 if ( $reason )
2368                                         $comment .= ": $reason";
2369
2370                                 $editComment = $comment;
2371                                 $encodedExpiry = array();
2372                                 $protect_description = '';
2373                                 foreach ( $limit as $action => $restrictions  ) {
2374                                         if ( !isset( $expiry[$action] ) )
2375                                                 $expiry[$action] = 'infinite';
2376
2377                                         $encodedExpiry[$action] = Block::encodeExpiry( $expiry[$action], $dbw );
2378                                         if ( $restrictions != '' ) {
2379                                                 $protect_description .= "[$action=$restrictions] (";
2380                                                 if ( $encodedExpiry[$action] != 'infinity' ) {
2381                                                         $protect_description .= wfMsgForContent( 'protect-expiring',
2382                                                                 $wgContLang->timeanddate( $expiry[$action], false, false ) ,
2383                                                                 $wgContLang->date( $expiry[$action], false, false ) ,
2384                                                                 $wgContLang->time( $expiry[$action], false, false ) );
2385                                                 } else {
2386                                                         $protect_description .= wfMsgForContent( 'protect-expiry-indefinite' );
2387                                                 }
2388                                                 $protect_description .= ') ';
2389                                         }
2390                                 }
2391                                 $protect_description = trim( $protect_description );
2392
2393                                 if ( $protect_description && $protect )
2394                                         $editComment .= " ($protect_description)";
2395                                 if ( $cascade )
2396                                         $editComment .= "$cascade_description";
2397                                 # Update restrictions table
2398                                 foreach ( $limit as $action => $restrictions ) {
2399                                         if ( $restrictions != '' ) {
2400                                                 $dbw->replace( 'page_restrictions', array( array( 'pr_page', 'pr_type' ) ),
2401                                                         array( 'pr_page' => $id,
2402                                                                 'pr_type' => $action,
2403                                                                 'pr_level' => $restrictions,
2404                                                                 'pr_cascade' => ( $cascade && $action == 'edit' ) ? 1 : 0,
2405                                                                 'pr_expiry' => $encodedExpiry[$action] ), __METHOD__  );
2406                                         } else {
2407                                                 $dbw->delete( 'page_restrictions', array( 'pr_page' => $id,
2408                                                         'pr_type' => $action ), __METHOD__ );
2409                                         }
2410                                 }
2411
2412                                 # Insert a null revision
2413                                 $nullRevision = Revision::newNullRevision( $dbw, $id, $editComment, true );
2414                                 $nullRevId = $nullRevision->insertOn( $dbw );
2415
2416                                 $latest = $this->getLatest();
2417                                 # Update page record
2418                                 $dbw->update( 'page',
2419                                         array( /* SET */
2420                                                 'page_touched' => $dbw->timestamp(),
2421                                                 'page_restrictions' => '',
2422                                                 'page_latest' => $nullRevId
2423                                         ), array( /* WHERE */
2424                                                 'page_id' => $id
2425                                         ), 'Article::protect'
2426                                 );
2427
2428                                 wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $nullRevision, $latest, $wgUser ) );
2429                                 wfRunHooks( 'ArticleProtectComplete', array( &$this, &$wgUser, $limit, $reason ) );
2430
2431                                 # Update the protection log
2432                                 $log = new LogPage( 'protect' );
2433                                 if ( $protect ) {
2434                                         $params = array( $protect_description, $cascade ? 'cascade' : '' );
2435                                         $log->addEntry( $modified ? 'modify' : 'protect', $this->mTitle, trim( $reason ), $params );
2436                                 } else {
2437                                         $log->addEntry( 'unprotect', $this->mTitle, $reason );
2438                                 }
2439
2440                         } # End hook
2441                 } # End "changed" check
2442
2443                 return true;
2444         }
2445
2446         /**
2447          * Take an array of page restrictions and flatten it to a string
2448          * suitable for insertion into the page_restrictions field.
2449          * @param $limit Array
2450          * @return String
2451          */
2452         protected static function flattenRestrictions( $limit ) {
2453                 if ( !is_array( $limit ) ) {
2454                         throw new MWException( 'Article::flattenRestrictions given non-array restriction set' );
2455                 }
2456                 $bits = array();
2457                 ksort( $limit );
2458                 foreach ( $limit as $action => $restrictions ) {
2459                         if ( $restrictions != '' ) {
2460                                 $bits[] = "$action=$restrictions";
2461                         }
2462                 }
2463                 return implode( ':', $bits );
2464         }
2465
2466         /**
2467          * Auto-generates a deletion reason
2468          * @param &$hasHistory Boolean: whether the page has a history
2469          */
2470         public function generateReason( &$hasHistory ) {
2471                 global $wgContLang;
2472                 $dbw = wfGetDB( DB_MASTER );
2473                 // Get the last revision
2474                 $rev = Revision::newFromTitle( $this->mTitle );
2475                 if ( is_null( $rev ) )
2476                         return false;
2477
2478                 // Get the article's contents
2479                 $contents = $rev->getText();
2480                 $blank = false;
2481                 // If the page is blank, use the text from the previous revision,
2482                 // which can only be blank if there's a move/import/protect dummy revision involved
2483                 if ( $contents == '' ) {
2484                         $prev = $rev->getPrevious();
2485                         if ( $prev )    {
2486                                 $contents = $prev->getText();
2487                                 $blank = true;
2488                         }
2489                 }
2490
2491                 // Find out if there was only one contributor
2492                 // Only scan the last 20 revisions
2493                 $res = $dbw->select( 'revision', 'rev_user_text',
2494                         array( 'rev_page' => $this->getID(), $dbw->bitAnd( 'rev_deleted', Revision::DELETED_USER ) . ' = 0' ),
2495                         __METHOD__,
2496                         array( 'LIMIT' => 20 )
2497                 );
2498                 if ( $res === false )
2499                         // This page has no revisions, which is very weird
2500                         return false;
2501
2502                 $hasHistory = ( $res->numRows() > 1 );
2503                 $row = $dbw->fetchObject( $res );
2504                 $onlyAuthor = $row->rev_user_text;
2505                 // Try to find a second contributor
2506                 foreach ( $res as $row ) {
2507                         if ( $row->rev_user_text != $onlyAuthor ) {
2508                                 $onlyAuthor = false;
2509                                 break;
2510                         }
2511                 }
2512                 $dbw->freeResult( $res );
2513
2514                 // Generate the summary with a '$1' placeholder
2515                 if ( $blank ) {
2516                         // The current revision is blank and the one before is also
2517                         // blank. It's just not our lucky day
2518                         $reason = wfMsgForContent( 'exbeforeblank', '$1' );
2519                 } else {
2520                         if ( $onlyAuthor )
2521                                 $reason = wfMsgForContent( 'excontentauthor', '$1', $onlyAuthor );
2522                         else
2523                                 $reason = wfMsgForContent( 'excontent', '$1' );
2524                 }
2525
2526                 if ( $reason == '-' ) {
2527                         // Allow these UI messages to be blanked out cleanly
2528                         return '';
2529                 }
2530
2531                 // Replace newlines with spaces to prevent uglyness
2532                 $contents = preg_replace( "/[\n\r]/", ' ', $contents );
2533                 // Calculate the maximum amount of chars to get
2534                 // Max content length = max comment length - length of the comment (excl. $1) - '...'
2535                 $maxLength = 255 - ( strlen( $reason ) - 2 ) - 3;
2536                 $contents = $wgContLang->truncate( $contents, $maxLength );
2537                 // Remove possible unfinished links
2538                 $contents = preg_replace( '/\[\[([^\]]*)\]?$/', '$1', $contents );
2539                 // Now replace the '$1' placeholder
2540                 $reason = str_replace( '$1', $contents, $reason );
2541                 return $reason;
2542         }
2543
2544
2545         /*
2546          * UI entry point for page deletion
2547          */
2548         public function delete() {
2549                 global $wgUser, $wgOut, $wgRequest;
2550
2551                 $confirm = $wgRequest->wasPosted() &&
2552                                 $wgUser->matchEditToken( $wgRequest->getVal( 'wpEditToken' ) );
2553
2554                 $this->DeleteReasonList = $wgRequest->getText( 'wpDeleteReasonList', 'other' );
2555                 $this->DeleteReason = $wgRequest->getText( 'wpReason' );
2556
2557                 $reason = $this->DeleteReasonList;
2558
2559                 if ( $reason != 'other' && $this->DeleteReason != '' ) {
2560                         // Entry from drop down menu + additional comment
2561                         $reason .= wfMsgForContent( 'colon-separator' ) . $this->DeleteReason;
2562                 } elseif ( $reason == 'other' ) {
2563                         $reason = $this->DeleteReason;
2564                 }
2565                 # Flag to hide all contents of the archived revisions
2566                 $suppress = $wgRequest->getVal( 'wpSuppress' ) && $wgUser->isAllowed( 'suppressrevision' );
2567
2568                 # This code desperately needs to be totally rewritten
2569
2570                 # Read-only check...
2571                 if ( wfReadOnly() ) {
2572                         $wgOut->readOnlyPage();
2573                         return;
2574                 }
2575
2576                 # Check permissions
2577                 $permission_errors = $this->mTitle->getUserPermissionsErrors( 'delete', $wgUser );
2578
2579                 if ( count( $permission_errors ) > 0 ) {
2580                         $wgOut->showPermissionsErrorPage( $permission_errors );
2581                         return;
2582                 }
2583
2584                 $wgOut->setPagetitle( wfMsg( 'delete-confirm', $this->mTitle->getPrefixedText() ) );
2585
2586                 # Better double-check that it hasn't been deleted yet!
2587                 $dbw = wfGetDB( DB_MASTER );
2588                 $conds = $this->mTitle->pageCond();
2589                 $latest = $dbw->selectField( 'page', 'page_latest', $conds, __METHOD__ );
2590                 if ( $latest === false ) {
2591                         $wgOut->showFatalError(
2592                                 Html::rawElement(
2593                                         'div',
2594                                         array( 'class' => 'error mw-error-cannotdelete' ),
2595                                         wfMsgExt( 'cannotdelete', array( 'parse' ), $this->mTitle->getPrefixedText() )
2596                                 )
2597                         );
2598                         $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
2599                         LogEventsList::showLogExtract(
2600                                 $wgOut,
2601                                 'delete',
2602                                 $this->mTitle->getPrefixedText()
2603                         );
2604                         return;
2605                 }
2606
2607                 # Hack for big sites
2608                 $bigHistory = $this->isBigDeletion();
2609                 if ( $bigHistory && !$this->mTitle->userCan( 'bigdelete' ) ) {
2610                         global $wgLang, $wgDeleteRevisionsLimit;
2611                         $wgOut->wrapWikiMsg( "<div class='error'>\n$1</div>\n",
2612                                 array( 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) );
2613                         return;
2614                 }
2615
2616                 if ( $confirm ) {
2617                         $this->doDelete( $reason, $suppress );
2618                         if ( $wgRequest->getCheck( 'wpWatch' ) && $wgUser->isLoggedIn() ) {
2619                                 $this->doWatch();
2620                         } elseif ( $this->mTitle->userIsWatching() ) {
2621                                 $this->doUnwatch();
2622                         }
2623                         return;
2624                 }
2625
2626                 // Generate deletion reason
2627                 $hasHistory = false;
2628                 if ( !$reason ) $reason = $this->generateReason( $hasHistory );
2629
2630                 // If the page has a history, insert a warning
2631                 if ( $hasHistory && !$confirm ) {
2632                         global $wgLang;
2633                         $skin = $wgUser->getSkin();
2634                         $revisions = $this->estimateRevisionCount();
2635                         $wgOut->addHTML( '<strong class="mw-delete-warning-revisions">' .
2636                                 wfMsgExt( 'historywarning', array( 'parseinline' ), $wgLang->formatNum( $revisions ) ) .
2637                                 wfMsgHtml( 'word-separator' ) . $skin->historyLink() .
2638                                 '</strong>'
2639                         );
2640                         if ( $bigHistory ) {
2641                                 global $wgDeleteRevisionsLimit;
2642                                 $wgOut->wrapWikiMsg( "<div class='error'>\n$1</div>\n",
2643                                         array( 'delete-warning-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ) );
2644                         }
2645                 }
2646
2647                 return $this->confirmDelete( $reason );
2648         }
2649
2650         /**
2651          * @return bool whether or not the page surpasses $wgDeleteRevisionsLimit revisions
2652          */
2653         public function isBigDeletion() {
2654                 global $wgDeleteRevisionsLimit;
2655                 if ( $wgDeleteRevisionsLimit ) {
2656                         $revCount = $this->estimateRevisionCount();
2657                         return $revCount > $wgDeleteRevisionsLimit;
2658                 }
2659                 return false;
2660         }
2661
2662         /**
2663          * @return int approximate revision count
2664          */
2665         public function estimateRevisionCount() {
2666                 $dbr = wfGetDB( DB_SLAVE );
2667                 // For an exact count...
2668                 // return $dbr->selectField( 'revision', 'COUNT(*)',
2669                 //      array( 'rev_page' => $this->getId() ), __METHOD__ );
2670                 return $dbr->estimateRowCount( 'revision', '*',
2671                         array( 'rev_page' => $this->getId() ), __METHOD__ );
2672         }
2673
2674         /**
2675          * Get the last N authors
2676          * @param $num Integer: number of revisions to get
2677          * @param $revLatest String: the latest rev_id, selected from the master (optional)
2678          * @return array Array of authors, duplicates not removed
2679          */
2680         public function getLastNAuthors( $num, $revLatest = 0 ) {
2681                 wfProfileIn( __METHOD__ );
2682                 // First try the slave
2683                 // If that doesn't have the latest revision, try the master
2684                 $continue = 2;
2685                 $db = wfGetDB( DB_SLAVE );
2686                 do {
2687                         $res = $db->select( array( 'page', 'revision' ),
2688                                 array( 'rev_id', 'rev_user_text' ),
2689                                 array(
2690                                         'page_namespace' => $this->mTitle->getNamespace(),
2691                                         'page_title' => $this->mTitle->getDBkey(),
2692                                         'rev_page = page_id'
2693                                 ), __METHOD__, $this->getSelectOptions( array(
2694                                         'ORDER BY' => 'rev_timestamp DESC',
2695                                         'LIMIT' => $num
2696                                 ) )
2697                         );
2698                         if ( !$res ) {
2699                                 wfProfileOut( __METHOD__ );
2700                                 return array();
2701                         }
2702                         $row = $db->fetchObject( $res );
2703                         if ( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) {
2704                                 $db = wfGetDB( DB_MASTER );
2705                                 $continue--;
2706                         } else {
2707                                 $continue = 0;
2708                         }
2709                 } while ( $continue );
2710
2711                 $authors = array( $row->rev_user_text );
2712                 while ( $row = $db->fetchObject( $res ) ) {
2713                         $authors[] = $row->rev_user_text;
2714                 }
2715                 wfProfileOut( __METHOD__ );
2716                 return $authors;
2717         }
2718
2719         /**
2720          * Output deletion confirmation dialog
2721          * @param $reason String: prefilled reason
2722          */
2723         public function confirmDelete( $reason ) {
2724                 global $wgOut, $wgUser;
2725
2726                 wfDebug( "Article::confirmDelete\n" );
2727
2728                 $deleteBackLink = $wgUser->getSkin()->link(
2729                         $this->mTitle,
2730                         null,
2731                         array(),
2732                         array(),
2733                         array( 'known', 'noclasses' )
2734                 );
2735                 $wgOut->setSubtitle( wfMsgHtml( 'delete-backlink', $deleteBackLink ) );
2736                 $wgOut->setRobotPolicy( 'noindex,nofollow' );
2737                 $wgOut->addWikiMsg( 'confirmdeletetext' );
2738
2739                 wfRunHooks( 'ArticleConfirmDelete', array( $this, $wgOut, &$reason ) );
2740
2741                 if ( $wgUser->isAllowed( 'suppressrevision' ) ) {
2742                         $suppress = "<tr id=\"wpDeleteSuppressRow\" name=\"wpDeleteSuppressRow\">
2743                                         <td></td>
2744                                         <td class='mw-input'><strong>" .
2745                                                 Xml::checkLabel( wfMsg( 'revdelete-suppress' ),
2746                                                         'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '4' ) ) .
2747                                         "</strong></td>
2748                                 </tr>";
2749                 } else {
2750                         $suppress = '';
2751                 }
2752                 $checkWatch = $wgUser->getBoolOption( 'watchdeletion' ) || $this->mTitle->userIsWatching();
2753
2754                 $form = Xml::openElement( 'form', array( 'method' => 'post',
2755                         'action' => $this->mTitle->getLocalURL( 'action=delete' ), 'id' => 'deleteconfirm' ) ) .
2756                         Xml::openElement( 'fieldset', array( 'id' => 'mw-delete-table' ) ) .
2757                         Xml::tags( 'legend', null, wfMsgExt( 'delete-legend', array( 'parsemag', 'escapenoentities' ) ) ) .
2758                         Xml::openElement( 'table', array( 'id' => 'mw-deleteconfirm-table' ) ) .
2759                         "<tr id=\"wpDeleteReasonListRow\">
2760                                 <td class='mw-label'>" .
2761                                         Xml::label( wfMsg( 'deletecomment' ), 'wpDeleteReasonList' ) .
2762                                 "</td>
2763                                 <td class='mw-input'>" .
2764                                         Xml::listDropDown( 'wpDeleteReasonList',
2765                                                 wfMsgForContent( 'deletereason-dropdown' ),
2766                                                 wfMsgForContent( 'deletereasonotherlist' ), '', 'wpReasonDropDown', 1 ) .
2767                                 "</td>
2768                         </tr>
2769                         <tr id=\"wpDeleteReasonRow\">
2770                                 <td class='mw-label'>" .
2771                                         Xml::label( wfMsg( 'deleteotherreason' ), 'wpReason' ) .
2772                                 "</td>
2773                                 <td class='mw-input'>" .
2774                                 Html::input( 'wpReason', $reason, 'text', array(
2775                                         'size' => '60',
2776                                         'maxlength' => '255',
2777                                         'tabindex' => '2',
2778                                         'id' => 'wpReason',
2779                                         'autofocus'
2780                                 ) ) .
2781                                 "</td>
2782                         </tr>";
2783                 # Dissalow watching is user is not logged in
2784                 if ( $wgUser->isLoggedIn() ) {
2785                         $form .= "
2786                         <tr>
2787                                 <td></td>
2788                                 <td class='mw-input'>" .
2789                                         Xml::checkLabel( wfMsg( 'watchthis' ),
2790                                                 'wpWatch', 'wpWatch', $checkWatch, array( 'tabindex' => '3' ) ) .
2791                                 "</td>
2792                         </tr>";
2793                 }
2794                 $form .= "
2795                         $suppress
2796                         <tr>
2797                                 <td></td>
2798                                 <td class='mw-submit'>" .
2799                                         Xml::submitButton( wfMsg( 'deletepage' ),
2800                                                 array( 'name' => 'wpConfirmB', 'id' => 'wpConfirmB', 'tabindex' => '5' ) ) .
2801                                 "</td>
2802                         </tr>" .
2803                         Xml::closeElement( 'table' ) .
2804                         Xml::closeElement( 'fieldset' ) .
2805                         Xml::hidden( 'wpEditToken', $wgUser->editToken() ) .
2806                         Xml::closeElement( 'form' );
2807
2808                         if ( $wgUser->isAllowed( 'editinterface' ) ) {
2809                                 $skin = $wgUser->getSkin();
2810                                 $title = Title::makeTitle( NS_MEDIAWIKI, 'Deletereason-dropdown' );
2811                                 $link = $skin->link(
2812                                         $title,
2813                                         wfMsgHtml( 'delete-edit-reasonlist' ),
2814                                         array(),
2815                                         array( 'action' => 'edit' )
2816                                 );
2817                                 $form .= '<p class="mw-delete-editreasons">' . $link . '</p>';
2818                         }
2819
2820                 $wgOut->addHTML( $form );
2821                 $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
2822                 LogEventsList::showLogExtract(
2823                         $wgOut,
2824                         'delete',
2825                         $this->mTitle->getPrefixedText()
2826                 );
2827         }
2828
2829         /**
2830          * Perform a deletion and output success or failure messages
2831          */
2832         public function doDelete( $reason, $suppress = false ) {
2833                 global $wgOut, $wgUser;
2834                 $id = $this->mTitle->getArticleID( GAID_FOR_UPDATE );
2835
2836                 $error = '';
2837                 if ( wfRunHooks( 'ArticleDelete', array( &$this, &$wgUser, &$reason, &$error ) ) ) {
2838                         if ( $this->doDeleteArticle( $reason, $suppress, $id ) ) {
2839                                 $deleted = $this->mTitle->getPrefixedText();
2840
2841                                 $wgOut->setPagetitle( wfMsg( 'actioncomplete' ) );
2842                                 $wgOut->setRobotPolicy( 'noindex,nofollow' );
2843
2844                                 $loglink = '[[Special:Log/delete|' . wfMsgNoTrans( 'deletionlog' ) . ']]';
2845
2846                                 $wgOut->addWikiMsg( 'deletedtext', $deleted, $loglink );
2847                                 $wgOut->returnToMain( false );
2848                                 wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$wgUser, $reason, $id ) );
2849                         }
2850                 } else {
2851                         if ( $error == '' ) {
2852                                 $wgOut->showFatalError(
2853                                         Html::rawElement(
2854                                                 'div',
2855                                                 array( 'class' => 'error mw-error-cannotdelete' ),
2856                                                 wfMsgExt( 'cannotdelete', array( 'parse' ), $this->mTitle->getPrefixedText() )
2857                                         )
2858                                 );
2859                                 $wgOut->addHTML( Xml::element( 'h2', null, LogPage::logName( 'delete' ) ) );
2860                                 LogEventsList::showLogExtract(
2861                                         $wgOut,
2862                                         'delete',
2863                                         $this->mTitle->getPrefixedText()
2864                                 );
2865                         } else {
2866                                 $wgOut->showFatalError( $error );
2867                         }
2868                 }
2869         }
2870
2871         /**
2872          * Back-end article deletion
2873          * Deletes the article with database consistency, writes logs, purges caches
2874          * Returns success
2875          */
2876         public function doDeleteArticle( $reason, $suppress = false, $id = 0 ) {
2877                 global $wgUseSquid, $wgDeferredUpdateList;
2878                 global $wgUseTrackbacks;
2879
2880                 wfDebug( __METHOD__ . "\n" );
2881
2882                 $dbw = wfGetDB( DB_MASTER );
2883                 $ns = $this->mTitle->getNamespace();
2884                 $t = $this->mTitle->getDBkey();
2885                 $id = $id ? $id : $this->mTitle->getArticleID( GAID_FOR_UPDATE );
2886
2887                 if ( $t == '' || $id == 0 ) {
2888                         return false;
2889                 }
2890
2891                 $u = new SiteStatsUpdate( 0, 1, - (int)$this->isCountable( $this->getRawText() ), -1 );
2892                 array_push( $wgDeferredUpdateList, $u );
2893
2894                 // Bitfields to further suppress the content
2895                 if ( $suppress ) {
2896                         $bitfield = 0;
2897                         // This should be 15...
2898                         $bitfield |= Revision::DELETED_TEXT;
2899                         $bitfield |= Revision::DELETED_COMMENT;
2900                         $bitfield |= Revision::DELETED_USER;
2901                         $bitfield |= Revision::DELETED_RESTRICTED;
2902                 } else {
2903                         $bitfield = 'rev_deleted';
2904                 }
2905
2906                 $dbw->begin();
2907                 // For now, shunt the revision data into the archive table.
2908                 // Text is *not* removed from the text table; bulk storage
2909                 // is left intact to avoid breaking block-compression or
2910                 // immutable storage schemes.
2911                 //
2912                 // For backwards compatibility, note that some older archive
2913                 // table entries will have ar_text and ar_flags fields still.
2914                 //
2915                 // In the future, we may keep revisions and mark them with
2916                 // the rev_deleted field, which is reserved for this purpose.
2917                 $dbw->insertSelect( 'archive', array( 'page', 'revision' ),
2918                         array(
2919                                 'ar_namespace'  => 'page_namespace',
2920                                 'ar_title'      => 'page_title',
2921                                 'ar_comment'    => 'rev_comment',
2922                                 'ar_user'       => 'rev_user',
2923                                 'ar_user_text'  => 'rev_user_text',
2924                                 'ar_timestamp'  => 'rev_timestamp',
2925                                 'ar_minor_edit' => 'rev_minor_edit',
2926                                 'ar_rev_id'     => 'rev_id',
2927                                 'ar_text_id'    => 'rev_text_id',
2928                                 'ar_text'       => '\'\'', // Be explicit to appease
2929                                 'ar_flags'      => '\'\'', // MySQL's "strict mode"...
2930                                 'ar_len'        => 'rev_len',
2931                                 'ar_page_id'    => 'page_id',
2932                                 'ar_deleted'    => $bitfield
2933                         ), array(
2934                                 'page_id' => $id,
2935                                 'page_id = rev_page'
2936                         ), __METHOD__
2937                 );
2938
2939                 # Delete restrictions for it
2940                 $dbw->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ );
2941
2942                 # Now that it's safely backed up, delete it
2943                 $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__ );
2944                 $ok = ( $dbw->affectedRows() > 0 ); // getArticleId() uses slave, could be laggy
2945                 if ( !$ok ) {
2946                         $dbw->rollback();
2947                         return false;
2948                 }
2949
2950                 # Fix category table counts
2951                 $cats = array();
2952                 $res = $dbw->select( 'categorylinks', 'cl_to', array( 'cl_from' => $id ), __METHOD__ );
2953                 foreach ( $res as $row ) {
2954                         $cats [] = $row->cl_to;
2955                 }
2956                 $this->updateCategoryCounts( array(), $cats );
2957
2958                 # If using cascading deletes, we can skip some explicit deletes
2959                 if ( !$dbw->cascadingDeletes() ) {
2960                         $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
2961
2962                         if ( $wgUseTrackbacks )
2963                                 $dbw->delete( 'trackbacks', array( 'tb_page' => $id ), __METHOD__ );
2964
2965                         # Delete outgoing links
2966                         $dbw->delete( 'pagelinks', array( 'pl_from' => $id ) );
2967                         $dbw->delete( 'imagelinks', array( 'il_from' => $id ) );
2968                         $dbw->delete( 'categorylinks', array( 'cl_from' => $id ) );
2969                         $dbw->delete( 'templatelinks', array( 'tl_from' => $id ) );
2970                         $dbw->delete( 'externallinks', array( 'el_from' => $id ) );
2971                         $dbw->delete( 'langlinks', array( 'll_from' => $id ) );
2972                         $dbw->delete( 'redirect', array( 'rd_from' => $id ) );
2973                 }
2974
2975                 # If using cleanup triggers, we can skip some manual deletes
2976                 if ( !$dbw->cleanupTriggers() ) {
2977                         # Clean up recentchanges entries...
2978                         $dbw->delete( 'recentchanges',
2979                                 array( 'rc_type != ' . RC_LOG,
2980                                         'rc_namespace' => $this->mTitle->getNamespace(),
2981                                         'rc_title' => $this->mTitle->getDBkey() ),
2982                                 __METHOD__ );
2983                         $dbw->delete( 'recentchanges',
2984                                 array( 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ),
2985                                 __METHOD__ );
2986                 }
2987
2988                 # Clear caches
2989                 Article::onArticleDelete( $this->mTitle );
2990
2991                 # Clear the cached article id so the interface doesn't act like we exist
2992                 $this->mTitle->resetArticleID( 0 );
2993
2994                 # Log the deletion, if the page was suppressed, log it at Oversight instead
2995                 $logtype = $suppress ? 'suppress' : 'delete';
2996                 $log = new LogPage( $logtype );
2997
2998                 # Make sure logging got through
2999                 $log->addEntry( 'delete', $this->mTitle, $reason, array() );
3000
3001                 $dbw->commit();
3002
3003                 return true;
3004         }
3005
3006         /**
3007          * Roll back the most recent consecutive set of edits to a page
3008          * from the same user; fails if there are no eligible edits to
3009          * roll back to, e.g. user is the sole contributor. This function
3010          * performs permissions checks on $wgUser, then calls commitRollback()
3011          * to do the dirty work
3012          *
3013          * @param $fromP String: Name of the user whose edits to rollback.
3014          * @param $summary String: Custom summary. Set to default summary if empty.
3015          * @param $token String: Rollback token.
3016          * @param $bot Boolean: If true, mark all reverted edits as bot.
3017          *
3018          * @param $resultDetails Array: contains result-specific array of additional values
3019          *    'alreadyrolled' : 'current' (rev)
3020          *    success        : 'summary' (str), 'current' (rev), 'target' (rev)
3021          *
3022          * @return array of errors, each error formatted as
3023          *   array(messagekey, param1, param2, ...).
3024          * On success, the array is empty.  This array can also be passed to
3025          * OutputPage::showPermissionsErrorPage().
3026          */
3027         public function doRollback( $fromP, $summary, $token, $bot, &$resultDetails ) {
3028                 global $wgUser;
3029                 $resultDetails = null;
3030
3031                 # Check permissions
3032                 $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $wgUser );
3033                 $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $wgUser );
3034                 $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
3035
3036                 if ( !$wgUser->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) )
3037                         $errors[] = array( 'sessionfailure' );
3038
3039                 if ( $wgUser->pingLimiter( 'rollback' ) || $wgUser->pingLimiter() ) {
3040                         $errors[] = array( 'actionthrottledtext' );
3041                 }
3042                 # If there were errors, bail out now
3043                 if ( !empty( $errors ) )
3044                         return $errors;
3045
3046                 return $this->commitRollback( $fromP, $summary, $bot, $resultDetails );
3047         }
3048
3049         /**
3050          * Backend implementation of doRollback(), please refer there for parameter
3051          * and return value documentation
3052          *
3053          * NOTE: This function does NOT check ANY permissions, it just commits the
3054          * rollback to the DB Therefore, you should only call this function direct-
3055          * ly if you want to use custom permissions checks. If you don't, use
3056          * doRollback() instead.
3057          */
3058         public function commitRollback( $fromP, $summary, $bot, &$resultDetails ) {
3059                 global $wgUseRCPatrol, $wgUser, $wgLang;
3060                 $dbw = wfGetDB( DB_MASTER );
3061
3062                 if ( wfReadOnly() ) {
3063                         return array( array( 'readonlytext' ) );
3064                 }
3065
3066                 # Get the last editor
3067                 $current = Revision::newFromTitle( $this->mTitle );
3068                 if ( is_null( $current ) ) {
3069                         # Something wrong... no page?
3070                         return array( array( 'notanarticle' ) );
3071                 }
3072
3073                 $from = str_replace( '_', ' ', $fromP );
3074                 # User name given should match up with the top revision.
3075                 # If the user was deleted then $from should be empty.
3076                 if ( $from != $current->getUserText() ) {
3077                         $resultDetails = array( 'current' => $current );
3078                         return array( array( 'alreadyrolled',
3079                                 htmlspecialchars( $this->mTitle->getPrefixedText() ),
3080                                 htmlspecialchars( $fromP ),
3081                                 htmlspecialchars( $current->getUserText() )
3082                         ) );
3083                 }
3084
3085                 # Get the last edit not by this guy...
3086                 # Note: these may not be public values
3087                 $user = intval( $current->getRawUser() );
3088                 $user_text = $dbw->addQuotes( $current->getRawUserText() );
3089                 $s = $dbw->selectRow( 'revision',
3090                         array( 'rev_id', 'rev_timestamp', 'rev_deleted' ),
3091                         array( 'rev_page' => $current->getPage(),
3092                                 "rev_user != {$user} OR rev_user_text != {$user_text}"
3093                         ), __METHOD__,
3094                         array( 'USE INDEX' => 'page_timestamp',
3095                                 'ORDER BY'  => 'rev_timestamp DESC' )
3096                         );
3097                 if ( $s === false ) {
3098                         # No one else ever edited this page
3099                         return array( array( 'cantrollback' ) );
3100                 } else if ( $s->rev_deleted & REVISION::DELETED_TEXT || $s->rev_deleted & REVISION::DELETED_USER ) {
3101                         # Only admins can see this text
3102                         return array( array( 'notvisiblerev' ) );
3103                 }
3104
3105                 $set = array();
3106                 if ( $bot && $wgUser->isAllowed( 'markbotedits' ) ) {
3107                         # Mark all reverted edits as bot
3108                         $set['rc_bot'] = 1;
3109                 }
3110                 if ( $wgUseRCPatrol ) {
3111                         # Mark all reverted edits as patrolled
3112                         $set['rc_patrolled'] = 1;
3113                 }
3114
3115                 if ( count( $set ) ) {
3116                         $dbw->update( 'recentchanges', $set,
3117                                 array( /* WHERE */
3118                                         'rc_cur_id' => $current->getPage(),
3119                                         'rc_user_text' => $current->getUserText(),
3120                                         "rc_timestamp > '{$s->rev_timestamp}'",
3121                                 ), __METHOD__
3122                         );
3123                 }
3124
3125                 # Generate the edit summary if necessary
3126                 $target = Revision::newFromId( $s->rev_id );
3127                 if ( empty( $summary ) ) {
3128                         if ( $from == '' ) { // no public user name
3129                                 $summary = wfMsgForContent( 'revertpage-nouser' );
3130                         } else {
3131                                 $summary = wfMsgForContent( 'revertpage' );
3132                         }
3133                 }
3134
3135                 # Allow the custom summary to use the same args as the default message
3136                 $args = array(
3137                         $target->getUserText(), $from, $s->rev_id,
3138                         $wgLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ), true ),
3139                         $current->getId(), $wgLang->timeanddate( $current->getTimestamp() )
3140                 );
3141                 $summary = wfMsgReplaceArgs( $summary, $args );
3142
3143                 # Save
3144                 $flags = EDIT_UPDATE;
3145
3146                 if ( $wgUser->isAllowed( 'minoredit' ) )
3147                         $flags |= EDIT_MINOR;
3148
3149                 if ( $bot && ( $wgUser->isAllowed( 'markbotedits' ) || $wgUser->isAllowed( 'bot' ) ) )
3150                         $flags |= EDIT_FORCE_BOT;
3151                 # Actually store the edit
3152                 $status = $this->doEdit( $target->getText(), $summary, $flags, $target->getId() );
3153                 if ( !empty( $status->value['revision'] ) ) {
3154                         $revId = $status->value['revision']->getId();
3155                 } else {
3156                         $revId = false;
3157                 }
3158
3159                 wfRunHooks( 'ArticleRollbackComplete', array( $this, $wgUser, $target, $current ) );
3160
3161                 $resultDetails = array(
3162                         'summary' => $summary,
3163                         'current' => $current,
3164                         'target'  => $target,
3165                         'newid'   => $revId
3166                 );
3167                 return array();
3168         }
3169
3170         /**
3171          * User interface for rollback operations
3172          */
3173         public function rollback() {
3174                 global $wgUser, $wgOut, $wgRequest, $wgUseRCPatrol;
3175                 $details = null;
3176
3177                 $result = $this->doRollback(
3178                         $wgRequest->getVal( 'from' ),
3179                         $wgRequest->getText( 'summary' ),
3180                         $wgRequest->getVal( 'token' ),
3181                         $wgRequest->getBool( 'bot' ),
3182                         $details
3183                 );
3184
3185                 if ( in_array( array( 'actionthrottledtext' ), $result ) ) {
3186                         $wgOut->rateLimited();
3187                         return;
3188                 }
3189                 if ( isset( $result[0][0] ) && ( $result[0][0] == 'alreadyrolled' || $result[0][0] == 'cantrollback' ) ) {
3190                         $wgOut->setPageTitle( wfMsg( 'rollbackfailed' ) );
3191                         $errArray = $result[0];
3192                         $errMsg = array_shift( $errArray );
3193                         $wgOut->addWikiMsgArray( $errMsg, $errArray );
3194                         if ( isset( $details['current'] ) ) {
3195                                 $current = $details['current'];
3196                                 if ( $current->getComment() != '' ) {
3197                                         $wgOut->addWikiMsgArray( 'editcomment', array(
3198                                                 $wgUser->getSkin()->formatComment( $current->getComment() ) ), array( 'replaceafter' ) );
3199                                 }
3200                         }
3201                         return;
3202                 }
3203                 # Display permissions errors before read-only message -- there's no
3204                 # point in misleading the user into thinking the inability to rollback
3205                 # is only temporary.
3206                 if ( !empty( $result ) && $result !== array( array( 'readonlytext' ) ) ) {
3207                         # array_diff is completely broken for arrays of arrays, sigh.  Re-
3208                         # move any 'readonlytext' error manually.
3209                         $out = array();
3210                         foreach ( $result as $error ) {
3211                                 if ( $error != array( 'readonlytext' ) ) {
3212                                         $out [] = $error;
3213                                 }
3214                         }
3215                         $wgOut->showPermissionsErrorPage( $out );
3216                         return;
3217                 }
3218                 if ( $result == array( array( 'readonlytext' ) ) ) {
3219                         $wgOut->readOnlyPage();
3220                         return;
3221                 }
3222
3223                 $current = $details['current'];
3224                 $target = $details['target'];
3225                 $newId = $details['newid'];
3226                 $wgOut->setPageTitle( wfMsg( 'actioncomplete' ) );
3227                 $wgOut->setRobotPolicy( 'noindex,nofollow' );
3228                 if ( $current->getUserText() === '' ) {
3229                         $old = wfMsg( 'rev-deleted-user' );
3230                 } else {
3231                         $old = $wgUser->getSkin()->userLink( $current->getUser(), $current->getUserText() )
3232                                 . $wgUser->getSkin()->userToolLinks( $current->getUser(), $current->getUserText() );
3233                 }
3234                 $new = $wgUser->getSkin()->userLink( $target->getUser(), $target->getUserText() )
3235                         . $wgUser->getSkin()->userToolLinks( $target->getUser(), $target->getUserText() );
3236                 $wgOut->addHTML( wfMsgExt( 'rollback-success', array( 'parse', 'replaceafter' ), $old, $new ) );
3237                 $wgOut->returnToMain( false, $this->mTitle );
3238
3239                 if ( !$wgRequest->getBool( 'hidediff', false ) && !$wgUser->getBoolOption( 'norollbackdiff', false ) ) {
3240                         $de = new DifferenceEngine( $this->mTitle, $current->getId(), $newId, false, true );
3241                         $de->showDiff( '', '' );
3242                 }
3243         }
3244
3245
3246         /**
3247          * Do standard deferred updates after page view
3248          */
3249         public function viewUpdates() {
3250                 global $wgDeferredUpdateList, $wgDisableCounters, $wgUser;
3251                 if ( wfReadOnly() ) {
3252                         return;
3253                 }
3254                 # Don't update page view counters on views from bot users (bug 14044)
3255                 if ( !$wgDisableCounters && !$wgUser->isAllowed( 'bot' ) && $this->getID() ) {
3256                         Article::incViewCount( $this->getID() );
3257                         $u = new SiteStatsUpdate( 1, 0, 0 );
3258                         array_push( $wgDeferredUpdateList, $u );
3259                 }
3260                 # Update newtalk / watchlist notification status
3261                 $wgUser->clearNotification( $this->mTitle );
3262         }
3263
3264         /**
3265          * Prepare text which is about to be saved.
3266          * Returns a stdclass with source, pst and output members
3267          */
3268         public function prepareTextForEdit( $text, $revid = null ) {
3269                 if ( $this->mPreparedEdit && $this->mPreparedEdit->newText == $text && $this->mPreparedEdit->revid == $revid ) {
3270                         // Already prepared
3271                         return $this->mPreparedEdit;
3272                 }
3273                 global $wgParser;
3274                 $edit = (object)array();
3275                 $edit->revid = $revid;
3276                 $edit->newText = $text;
3277                 $edit->pst = $this->preSaveTransform( $text );
3278                 $options = $this->getParserOptions();
3279                 $edit->output = $wgParser->parse( $edit->pst, $this->mTitle, $options, true, true, $revid );
3280                 $edit->oldText = $this->getContent();
3281                 $this->mPreparedEdit = $edit;
3282                 return $edit;
3283         }
3284
3285         /**
3286          * Do standard deferred updates after page edit.
3287          * Update links tables, site stats, search index and message cache.
3288          * Purges pages that include this page if the text was changed here.
3289          * Every 100th edit, prune the recent changes table.
3290          *
3291          * @private
3292          * @param $text New text of the article
3293          * @param $summary Edit summary
3294          * @param $minoredit Minor edit
3295          * @param $timestamp_of_pagechange Timestamp associated with the page change
3296          * @param $newid rev_id value of the new revision
3297          * @param $changed Whether or not the content actually changed
3298          */
3299         public function editUpdates( $text, $summary, $minoredit, $timestamp_of_pagechange, $newid, $changed = true ) {
3300                 global $wgDeferredUpdateList, $wgMessageCache, $wgUser, $wgEnableParserCache;
3301
3302                 wfProfileIn( __METHOD__ );
3303
3304                 # Parse the text
3305                 # Be careful not to double-PST: $text is usually already PST-ed once
3306                 if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
3307                         wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" );
3308                         $editInfo = $this->prepareTextForEdit( $text, $newid );
3309                 } else {
3310                         wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" );
3311                         $editInfo = $this->mPreparedEdit;
3312                 }
3313
3314                 # Save it to the parser cache
3315                 if ( $wgEnableParserCache ) {
3316                         $popts = $this->getParserOptions();
3317                         $parserCache = ParserCache::singleton();
3318                         $parserCache->save( $editInfo->output, $this, $popts );
3319                 }
3320
3321                 # Update the links tables
3322                 $u = new LinksUpdate( $this->mTitle, $editInfo->output );
3323                 $u->doUpdate();
3324
3325                 wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $changed ) );
3326
3327                 if ( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) {
3328                         if ( 0 == mt_rand( 0, 99 ) ) {
3329                                 // Flush old entries from the `recentchanges` table; we do this on
3330                                 // random requests so as to avoid an increase in writes for no good reason
3331                                 global $wgRCMaxAge;
3332                                 $dbw = wfGetDB( DB_MASTER );
3333                                 $cutoff = $dbw->timestamp( time() - $wgRCMaxAge );
3334                                 $recentchanges = $dbw->tableName( 'recentchanges' );
3335                                 $sql = "DELETE FROM $recentchanges WHERE rc_timestamp < '{$cutoff}'";
3336                                 $dbw->query( $sql );
3337                         }
3338                 }
3339
3340                 $id = $this->getID();
3341                 $title = $this->mTitle->getPrefixedDBkey();
3342                 $shortTitle = $this->mTitle->getDBkey();
3343
3344                 if ( 0 == $id ) {
3345                         wfProfileOut( __METHOD__ );
3346                         return;
3347                 }
3348
3349                 $u = new SiteStatsUpdate( 0, 1, $this->mGoodAdjustment, $this->mTotalAdjustment );
3350                 array_push( $wgDeferredUpdateList, $u );
3351                 $u = new SearchUpdate( $id, $title, $text );
3352                 array_push( $wgDeferredUpdateList, $u );
3353
3354                 # If this is another user's talk page, update newtalk
3355                 # Don't do this if $changed = false otherwise some idiot can null-edit a
3356                 # load of user talk pages and piss people off, nor if it's a minor edit
3357                 # by a properly-flagged bot.
3358                 if ( $this->mTitle->getNamespace() == NS_USER_TALK && $shortTitle != $wgUser->getTitleKey() && $changed
3359                         && !( $minoredit && $wgUser->isAllowed( 'nominornewtalk' ) ) ) {
3360                         if ( wfRunHooks( 'ArticleEditUpdateNewTalk', array( &$this ) ) ) {
3361                                 $other = User::newFromName( $shortTitle, false );
3362                                 if ( !$other ) {
3363                                         wfDebug( __METHOD__ . ": invalid username\n" );
3364                                 } elseif ( User::isIP( $shortTitle ) ) {
3365                                         // An anonymous user
3366                                         $other->setNewtalk( true );
3367                                 } elseif ( $other->isLoggedIn() ) {
3368                                         $other->setNewtalk( true );
3369                                 } else {
3370                                         wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" );
3371                                 }
3372                         }
3373                 }
3374
3375                 if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
3376                         $wgMessageCache->replace( $shortTitle, $text );
3377                 }
3378
3379                 wfProfileOut( __METHOD__ );
3380         }
3381
3382         /**
3383          * Perform article updates on a special page creation.
3384          *
3385          * @param $rev Revision object
3386          *
3387          * @todo This is a shitty interface function. Kill it and replace the
3388          * other shitty functions like editUpdates and such so it's not needed
3389          * anymore.
3390          */
3391         public function createUpdates( $rev ) {
3392                 $this->mGoodAdjustment = $this->isCountable( $rev->getText() );
3393                 $this->mTotalAdjustment = 1;
3394                 $this->editUpdates( $rev->getText(), $rev->getComment(),
3395                         $rev->isMinor(), wfTimestamp(), $rev->getId(), true );
3396         }
3397
3398         /**
3399          * Generate the navigation links when browsing through an article revisions
3400          * It shows the information as:
3401          *   Revision as of \<date\>; view current revision
3402          *   \<- Previous version | Next Version -\>
3403          *
3404          * @param $oldid String: revision ID of this article revision
3405          */
3406         public function setOldSubtitle( $oldid = 0 ) {
3407                 global $wgLang, $wgOut, $wgUser, $wgRequest;
3408
3409                 if ( !wfRunHooks( 'DisplayOldSubtitle', array( &$this, &$oldid ) ) ) {
3410                         return;
3411                 }
3412
3413                 $unhide = $wgRequest->getInt( 'unhide' ) == 1 &&
3414                         $wgUser->matchEditToken( $wgRequest->getVal( 'token' ), $oldid );
3415                 # Cascade unhide param in links for easy deletion browsing
3416                 $extraParams = array();
3417                 if ( $wgRequest->getVal( 'unhide' ) ) {
3418                         $extraParams['unhide'] = 1;
3419                 }
3420                 $revision = Revision::newFromId( $oldid );
3421
3422                 $current = ( $oldid == $this->mLatest );
3423                 $td = $wgLang->timeanddate( $this->mTimestamp, true );
3424                 $tddate = $wgLang->date( $this->mTimestamp, true );
3425                 $tdtime = $wgLang->time( $this->mTimestamp, true );
3426                 $sk = $wgUser->getSkin();
3427                 $lnk = $current
3428                         ? wfMsgHtml( 'currentrevisionlink' )
3429                         : $sk->link(
3430                                 $this->mTitle,
3431                                 wfMsgHtml( 'currentrevisionlink' ),
3432                                 array(),
3433                                 $extraParams,
3434                                 array( 'known', 'noclasses' )
3435                         );
3436                 $curdiff = $current
3437                         ? wfMsgHtml( 'diff' )
3438                         : $sk->link(
3439                                 $this->mTitle,
3440                                 wfMsgHtml( 'diff' ),
3441                                 array(),
3442                                 array(
3443                                         'diff' => 'cur',
3444                                         'oldid' => $oldid
3445                                 ) + $extraParams,
3446                                 array( 'known', 'noclasses' )
3447                         );
3448                 $prev = $this->mTitle->getPreviousRevisionID( $oldid ) ;
3449                 $prevlink = $prev
3450                         ? $sk->link(
3451                                 $this->mTitle,
3452                                 wfMsgHtml( 'previousrevision' ),
3453                                 array(),
3454                                 array(
3455                                         'direction' => 'prev',
3456                                         'oldid' => $oldid
3457                                 ) + $extraParams,
3458                                 array( 'known', 'noclasses' )
3459                         )
3460                         : wfMsgHtml( 'previousrevision' );
3461                 $prevdiff = $prev
3462                         ? $sk->link(
3463                                 $this->mTitle,
3464                                 wfMsgHtml( 'diff' ),
3465                                 array(),
3466                                 array(
3467                                         'diff' => 'prev',
3468                                         'oldid' => $oldid
3469                                 ) + $extraParams,
3470                                 array( 'known', 'noclasses' )
3471                         )
3472                         : wfMsgHtml( 'diff' );
3473                 $nextlink = $current
3474                         ? wfMsgHtml( 'nextrevision' )
3475                         : $sk->link(
3476                                 $this->mTitle,
3477                                 wfMsgHtml( 'nextrevision' ),
3478                                 array(),
3479                                 array(
3480                                         'direction' => 'next',
3481                                         'oldid' => $oldid
3482                                 ) + $extraParams,
3483                                 array( 'known', 'noclasses' )
3484                         );
3485                 $nextdiff = $current
3486                         ? wfMsgHtml( 'diff' )
3487                         : $sk->link(
3488                                 $this->mTitle,
3489                                 wfMsgHtml( 'diff' ),
3490                                 array(),
3491                                 array(
3492                                         'diff' => 'next',
3493                                         'oldid' => $oldid
3494                                 ) + $extraParams,
3495                                 array( 'known', 'noclasses' )
3496                         );
3497
3498                 $cdel = '';
3499                 // User can delete revisions or view deleted revisions...
3500                 $canHide = $wgUser->isAllowed( 'deleterevision' );
3501                 if ( $canHide || ( $revision->getVisibility() && $wgUser->isAllowed( 'deletedhistory' ) ) ) {
3502                         if ( !$revision->userCan( Revision::DELETED_RESTRICTED ) ) {
3503                                 $cdel = $sk->revDeleteLinkDisabled( $canHide ); // rev was hidden from Sysops
3504                         } else {
3505                                 $query = array(
3506                                         'type'   => 'revision',
3507                                         'target' => $this->mTitle->getPrefixedDbkey(),
3508                                         'ids'    => $oldid
3509                                 );
3510                                 $cdel = $sk->revDeleteLink( $query, $revision->isDeleted( File::DELETED_RESTRICTED ), $canHide );
3511                         }
3512                         $cdel .= ' ';
3513                 }
3514
3515                 # Show user links if allowed to see them. If hidden, then show them only if requested...
3516                 $userlinks = $sk->revUserTools( $revision, !$unhide );
3517
3518                 $m = wfMsg( 'revision-info-current' );
3519                 $infomsg = $current && !wfEmptyMsg( 'revision-info-current', $m ) && $m != '-'
3520                         ? 'revision-info-current'
3521                         : 'revision-info';
3522
3523                 $r = "\n\t\t\t\t<div id=\"mw-{$infomsg}\">" .
3524                         wfMsgExt(
3525                                 $infomsg,
3526                                 array( 'parseinline', 'replaceafter' ),
3527                                 $td,
3528                                 $userlinks,
3529                                 $revision->getID(),
3530                                 $tddate,
3531                                 $tdtime,
3532                                 $revision->getUser()
3533                         ) .
3534                         "</div>\n" .
3535                         "\n\t\t\t\t<div id=\"mw-revision-nav\">" . $cdel . wfMsgExt( 'revision-nav', array( 'escapenoentities', 'parsemag', 'replaceafter' ),
3536                         $prevdiff, $prevlink, $lnk, $curdiff, $nextlink, $nextdiff ) . "</div>\n\t\t\t";
3537                 $wgOut->setSubtitle( $r );
3538         }
3539
3540         /**
3541          * This function is called right before saving the wikitext,
3542          * so we can do things like signatures and links-in-context.
3543          *
3544          * @param $text String
3545          */
3546         public function preSaveTransform( $text ) {
3547                 global $wgParser, $wgUser;
3548                 return $wgParser->preSaveTransform( $text, $this->mTitle, $wgUser, ParserOptions::newFromUser( $wgUser ) );
3549         }
3550
3551         /* Caching functions */
3552
3553         /**
3554          * checkLastModified returns true if it has taken care of all
3555          * output to the client that is necessary for this request.
3556          * (that is, it has sent a cached version of the page)
3557          */
3558         protected function tryFileCache() {
3559                 static $called = false;
3560                 if ( $called ) {
3561                         wfDebug( "Article::tryFileCache(): called twice!?\n" );
3562                         return false;
3563                 }
3564                 $called = true;
3565                 if ( $this->isFileCacheable() ) {
3566                         $cache = new HTMLFileCache( $this->mTitle );
3567                         if ( $cache->isFileCacheGood( $this->mTouched ) ) {
3568                                 wfDebug( "Article::tryFileCache(): about to load file\n" );
3569                                 $cache->loadFromFileCache();
3570                                 return true;
3571                         } else {
3572                                 wfDebug( "Article::tryFileCache(): starting buffer\n" );
3573                                 ob_start( array( &$cache, 'saveToFileCache' ) );
3574                         }
3575                 } else {
3576                         wfDebug( "Article::tryFileCache(): not cacheable\n" );
3577                 }
3578                 return false;
3579         }
3580
3581         /**
3582          * Check if the page can be cached
3583          * @return bool
3584          */
3585         public function isFileCacheable() {
3586                 $cacheable = false;
3587                 if ( HTMLFileCache::useFileCache() ) {
3588                         $cacheable = $this->getID() && !$this->mRedirectedFrom;
3589                         // Extension may have reason to disable file caching on some pages.
3590                         if ( $cacheable ) {
3591                                 $cacheable = wfRunHooks( 'IsFileCacheable', array( &$this ) );
3592                         }
3593                 }
3594                 return $cacheable;
3595         }
3596
3597         /**
3598          * Loads page_touched and returns a value indicating if it should be used
3599          *
3600          */
3601         public function checkTouched() {
3602                 if ( !$this->mDataLoaded ) {
3603                         $this->loadPageData();
3604                 }
3605                 return !$this->mIsRedirect;
3606         }
3607
3608         /**
3609          * Get the page_touched field
3610          */
3611         public function getTouched() {
3612                 # Ensure that page data has been loaded
3613                 if ( !$this->mDataLoaded ) {
3614                         $this->loadPageData();
3615                 }
3616                 return $this->mTouched;
3617         }
3618
3619         /**
3620          * Get the page_latest field
3621          */
3622         public function getLatest() {
3623                 if ( !$this->mDataLoaded ) {
3624                         $this->loadPageData();
3625                 }
3626                 return (int)$this->mLatest;
3627         }
3628
3629         /**
3630          * Edit an article without doing all that other stuff
3631          * The article must already exist; link tables etc
3632          * are not updated, caches are not flushed.
3633          *
3634          * @param $text String: text submitted
3635          * @param $comment String: comment submitted
3636          * @param $minor Boolean: whereas it's a minor modification
3637          */
3638         public function quickEdit( $text, $comment = '', $minor = 0 ) {
3639                 wfProfileIn( __METHOD__ );
3640
3641                 $dbw = wfGetDB( DB_MASTER );
3642                 $revision = new Revision( array(
3643                         'page'       => $this->getId(),
3644                         'text'       => $text,
3645                         'comment'    => $comment,
3646                         'minor_edit' => $minor ? 1 : 0,
3647                         ) );
3648                 $revision->insertOn( $dbw );
3649                 $this->updateRevisionOn( $dbw, $revision );
3650
3651                 wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $wgUser ) );
3652
3653                 wfProfileOut( __METHOD__ );
3654         }
3655
3656         /**
3657          * Used to increment the view counter
3658          *
3659          * @param $id Integer: article id
3660          */
3661         public static function incViewCount( $id ) {
3662                 $id = intval( $id );
3663                 global $wgHitcounterUpdateFreq;
3664
3665                 $dbw = wfGetDB( DB_MASTER );
3666                 $pageTable = $dbw->tableName( 'page' );
3667                 $hitcounterTable = $dbw->tableName( 'hitcounter' );
3668                 $acchitsTable = $dbw->tableName( 'acchits' );
3669                 $dbType = $dbw->getType();
3670
3671                 if ( $wgHitcounterUpdateFreq <= 1 || $dbType == 'sqlite' ) {
3672                         $dbw->query( "UPDATE $pageTable SET page_counter = page_counter + 1 WHERE page_id = $id" );
3673                         return;
3674                 }
3675
3676                 # Not important enough to warrant an error page in case of failure
3677                 $oldignore = $dbw->ignoreErrors( true );
3678
3679                 $dbw->query( "INSERT INTO $hitcounterTable (hc_id) VALUES ({$id})" );
3680
3681                 $checkfreq = intval( $wgHitcounterUpdateFreq / 25 + 1 );
3682                 if ( ( rand() % $checkfreq != 0 ) or ( $dbw->lastErrno() != 0 ) ) {
3683                         # Most of the time (or on SQL errors), skip row count check
3684                         $dbw->ignoreErrors( $oldignore );
3685                         return;
3686                 }
3687
3688                 $res = $dbw->query( "SELECT COUNT(*) as n FROM $hitcounterTable" );
3689                 $row = $dbw->fetchObject( $res );
3690                 $rown = intval( $row->n );
3691                 if ( $rown >= $wgHitcounterUpdateFreq ) {
3692                         wfProfileIn( 'Article::incViewCount-collect' );
3693                         $old_user_abort = ignore_user_abort( true );
3694
3695                         $dbw->lockTables( array(), array( 'hitcounter' ), __METHOD__, false );
3696                         $tabletype = $dbType == 'mysql' ? "ENGINE=HEAP " : '';
3697                         $dbw->query( "CREATE TEMPORARY TABLE $acchitsTable $tabletype AS " .
3698                                 "SELECT hc_id,COUNT(*) AS hc_n FROM $hitcounterTable " .
3699                                 'GROUP BY hc_id', __METHOD__ );
3700                         $dbw->delete( 'hitcounter', '*', __METHOD__ );
3701                         $dbw->unlockTables( __METHOD__ );
3702                         if ( $dbType == 'mysql' ) {
3703                                 $dbw->query( "UPDATE $pageTable,$acchitsTable SET page_counter=page_counter + hc_n " .
3704                                         'WHERE page_id = hc_id', __METHOD__ );
3705                         }
3706                         else {
3707                                 $dbw->query( "UPDATE $pageTable SET page_counter=page_counter + hc_n " .
3708                                         "FROM $acchitsTable WHERE page_id = hc_id", __METHOD__ );
3709                         }
3710                         $dbw->query( "DROP TABLE $acchitsTable", __METHOD__ );
3711
3712                         ignore_user_abort( $old_user_abort );
3713                         wfProfileOut( 'Article::incViewCount-collect' );
3714                 }
3715                 $dbw->ignoreErrors( $oldignore );
3716         }
3717
3718         /**#@+
3719          * The onArticle*() functions are supposed to be a kind of hooks
3720          * which should be called whenever any of the specified actions
3721          * are done.
3722          *
3723          * This is a good place to put code to clear caches, for instance.
3724          *
3725          * This is called on page move and undelete, as well as edit
3726          *
3727          * @param $title a title object
3728          */
3729         public static function onArticleCreate( $title ) {
3730                 # Update existence markers on article/talk tabs...
3731                 if ( $title->isTalkPage() ) {
3732                         $other = $title->getSubjectPage();
3733                 } else {
3734                         $other = $title->getTalkPage();
3735                 }
3736                 $other->invalidateCache();
3737                 $other->purgeSquid();
3738
3739                 $title->touchLinks();
3740                 $title->purgeSquid();
3741                 $title->deleteTitleProtection();
3742         }
3743
3744         public static function onArticleDelete( $title ) {
3745                 global $wgMessageCache;
3746                 # Update existence markers on article/talk tabs...
3747                 if ( $title->isTalkPage() ) {
3748                         $other = $title->getSubjectPage();
3749                 } else {
3750                         $other = $title->getTalkPage();
3751                 }
3752                 $other->invalidateCache();
3753                 $other->purgeSquid();
3754
3755                 $title->touchLinks();
3756                 $title->purgeSquid();
3757
3758                 # File cache
3759                 HTMLFileCache::clearFileCache( $title );
3760
3761                 # Messages
3762                 if ( $title->getNamespace() == NS_MEDIAWIKI ) {
3763                         $wgMessageCache->replace( $title->getDBkey(), false );
3764                 }
3765                 # Images
3766                 if ( $title->getNamespace() == NS_FILE ) {
3767                         $update = new HTMLCacheUpdate( $title, 'imagelinks' );
3768                         $update->doUpdate();
3769                 }
3770                 # User talk pages
3771                 if ( $title->getNamespace() == NS_USER_TALK ) {
3772                         $user = User::newFromName( $title->getText(), false );
3773                         $user->setNewtalk( false );
3774                 }
3775                 # Image redirects
3776                 RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
3777         }
3778
3779         /**
3780          * Purge caches on page update etc
3781          */
3782         public static function onArticleEdit( $title, $flags = '' ) {
3783                 global $wgDeferredUpdateList;
3784
3785                 // Invalidate caches of articles which include this page
3786                 $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'templatelinks' );
3787
3788                 // Invalidate the caches of all pages which redirect here
3789                 $wgDeferredUpdateList[] = new HTMLCacheUpdate( $title, 'redirect' );
3790
3791                 # Purge squid for this page only
3792                 $title->purgeSquid();
3793
3794                 # Clear file cache for this page only
3795                 HTMLFileCache::clearFileCache( $title );
3796         }
3797
3798         /**#@-*/
3799
3800         /**
3801          * Overriden by ImagePage class, only present here to avoid a fatal error
3802          * Called for ?action=revert
3803          */
3804         public function revert() {
3805                 global $wgOut;
3806                 $wgOut->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
3807         }
3808
3809         /**
3810          * Info about this page
3811          * Called for ?action=info when $wgAllowPageInfo is on.
3812          */
3813         public function info() {
3814                 global $wgLang, $wgOut, $wgAllowPageInfo, $wgUser;
3815
3816                 if ( !$wgAllowPageInfo ) {
3817                         $wgOut->showErrorPage( 'nosuchaction', 'nosuchactiontext' );
3818                         return;
3819                 }
3820
3821                 $page = $this->mTitle->getSubjectPage();
3822
3823                 $wgOut->setPagetitle( $page->getPrefixedText() );
3824                 $wgOut->setPageTitleActionText( wfMsg( 'info_short' ) );
3825                 $wgOut->setSubtitle( wfMsgHtml( 'infosubtitle' ) );
3826
3827                 if ( !$this->mTitle->exists() ) {
3828                         $wgOut->addHTML( '<div class="noarticletext">' );
3829                         if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
3830                                 // This doesn't quite make sense; the user is asking for
3831                                 // information about the _page_, not the message... -- RC
3832                                 $wgOut->addHTML( htmlspecialchars( wfMsgWeirdKey( $this->mTitle->getText() ) ) );
3833                         } else {
3834                                 $msg = $wgUser->isLoggedIn()
3835                                         ? 'noarticletext'
3836                                         : 'noarticletextanon';
3837                                 $wgOut->addHTML( wfMsgExt( $msg, 'parse' ) );
3838                         }
3839                         $wgOut->addHTML( '</div>' );
3840                 } else {
3841                         $dbr = wfGetDB( DB_SLAVE );
3842                         $wl_clause = array(
3843                                 'wl_title'     => $page->getDBkey(),
3844                                 'wl_namespace' => $page->getNamespace() );
3845                         $numwatchers = $dbr->selectField(
3846                                 'watchlist',
3847                                 'COUNT(*)',
3848                                 $wl_clause,
3849                                 __METHOD__,
3850                                 $this->getSelectOptions() );
3851
3852                         $pageInfo = $this->pageCountInfo( $page );
3853                         $talkInfo = $this->pageCountInfo( $page->getTalkPage() );
3854
3855                         $wgOut->addHTML( "<ul><li>" . wfMsg( "numwatchers", $wgLang->formatNum( $numwatchers ) ) . '</li>' );
3856                         $wgOut->addHTML( "<li>" . wfMsg( 'numedits', $wgLang->formatNum( $pageInfo['edits'] ) ) . '</li>' );
3857                         if ( $talkInfo ) {
3858                                 $wgOut->addHTML( '<li>' . wfMsg( "numtalkedits", $wgLang->formatNum( $talkInfo['edits'] ) ) . '</li>' );
3859                         }
3860                         $wgOut->addHTML( '<li>' . wfMsg( "numauthors", $wgLang->formatNum( $pageInfo['authors'] ) ) . '</li>' );
3861                         if ( $talkInfo ) {
3862                                 $wgOut->addHTML( '<li>' . wfMsg( 'numtalkauthors', $wgLang->formatNum( $talkInfo['authors'] ) ) . '</li>' );
3863                         }
3864                         $wgOut->addHTML( '</ul>' );
3865                 }
3866         }
3867
3868         /**
3869          * Return the total number of edits and number of unique editors
3870          * on a given page. If page does not exist, returns false.
3871          *
3872          * @param $title Title object
3873          * @return array
3874          */
3875         public function pageCountInfo( $title ) {
3876                 $id = $title->getArticleId();
3877                 if ( $id == 0 ) {
3878                         return false;
3879                 }
3880                 $dbr = wfGetDB( DB_SLAVE );
3881                 $rev_clause = array( 'rev_page' => $id );
3882                 $edits = $dbr->selectField(
3883                         'revision',
3884                         'COUNT(rev_page)',
3885                         $rev_clause,
3886                         __METHOD__,
3887                         $this->getSelectOptions()
3888                 );
3889                 $authors = $dbr->selectField(
3890                         'revision',
3891                         'COUNT(DISTINCT rev_user_text)',
3892                         $rev_clause,
3893                         __METHOD__,
3894                         $this->getSelectOptions()
3895                 );
3896                 return array( 'edits' => $edits, 'authors' => $authors );
3897         }
3898
3899         /**
3900          * Return a list of templates used by this article.
3901          * Uses the templatelinks table
3902          *
3903          * @return Array of Title objects
3904          */
3905         public function getUsedTemplates() {
3906                 $result = array();
3907                 $id = $this->mTitle->getArticleID();
3908                 if ( $id == 0 ) {
3909                         return array();
3910                 }
3911                 $dbr = wfGetDB( DB_SLAVE );
3912                 $res = $dbr->select( array( 'templatelinks' ),
3913                         array( 'tl_namespace', 'tl_title' ),
3914                         array( 'tl_from' => $id ),
3915                         __METHOD__ );
3916                 if ( $res !== false ) {
3917                         foreach ( $res as $row ) {
3918                                 $result[] = Title::makeTitle( $row->tl_namespace, $row->tl_title );
3919                         }
3920                 }
3921                 $dbr->freeResult( $res );
3922                 return $result;
3923         }
3924
3925         /**
3926          * Returns a list of hidden categories this page is a member of.
3927          * Uses the page_props and categorylinks tables.
3928          *
3929          * @return Array of Title objects
3930          */
3931         public function getHiddenCategories() {
3932                 $result = array();
3933                 $id = $this->mTitle->getArticleID();
3934                 if ( $id == 0 ) {
3935                         return array();
3936                 }
3937                 $dbr = wfGetDB( DB_SLAVE );
3938                 $res = $dbr->select( array( 'categorylinks', 'page_props', 'page' ),
3939                         array( 'cl_to' ),
3940                         array( 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3941                                 'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ),
3942                         __METHOD__ );
3943                 if ( $res !== false ) {
3944                         foreach ( $res as $row ) {
3945                                 $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3946                         }
3947                 }
3948                 $dbr->freeResult( $res );
3949                 return $result;
3950         }
3951
3952         /**
3953         * Return an applicable autosummary if one exists for the given edit.
3954         * @param $oldtext String: the previous text of the page.
3955         * @param $newtext String: The submitted text of the page.
3956         * @param $flags Bitmask: a bitmask of flags submitted for the edit.
3957         * @return string An appropriate autosummary, or an empty string.
3958         */
3959         public static function getAutosummary( $oldtext, $newtext, $flags ) {
3960                 # Decide what kind of autosummary is needed.
3961
3962                 # Redirect autosummaries
3963                 $ot = Title::newFromRedirect( $oldtext );
3964                 $rt = Title::newFromRedirect( $newtext );
3965                 if ( is_object( $rt ) && ( !is_object( $ot ) || !$rt->equals( $ot ) || $ot->getFragment() != $rt->getFragment() ) ) {
3966                         return wfMsgForContent( 'autoredircomment', $rt->getFullText() );
3967                 }
3968
3969                 # New page autosummaries
3970                 if ( $flags & EDIT_NEW && strlen( $newtext ) ) {
3971                         # If they're making a new article, give its text, truncated, in the summary.
3972                         global $wgContLang;
3973                         $truncatedtext = $wgContLang->truncate(
3974                                 str_replace( "\n", ' ', $newtext ),
3975                                 max( 0, 200 - strlen( wfMsgForContent( 'autosumm-new' ) ) ) );
3976                         return wfMsgForContent( 'autosumm-new', $truncatedtext );
3977                 }
3978
3979                 # Blanking autosummaries
3980                 if ( $oldtext != '' && $newtext == '' ) {
3981                         return wfMsgForContent( 'autosumm-blank' );
3982                 } elseif ( strlen( $oldtext ) > 10 * strlen( $newtext ) && strlen( $newtext ) < 500 ) {
3983                         # Removing more than 90% of the article
3984                         global $wgContLang;
3985                         $truncatedtext = $wgContLang->truncate(
3986                                 $newtext,
3987                                 max( 0, 200 - strlen( wfMsgForContent( 'autosumm-replace' ) ) ) );
3988                         return wfMsgForContent( 'autosumm-replace', $truncatedtext );
3989                 }
3990
3991                 # If we reach this point, there's no applicable autosummary for our case, so our
3992                 # autosummary is empty.
3993                 return '';
3994         }
3995
3996         /**
3997          * Add the primary page-view wikitext to the output buffer
3998          * Saves the text into the parser cache if possible.
3999          * Updates templatelinks if it is out of date.
4000          *
4001          * @param $text String
4002          * @param $cache Boolean
4003          */
4004         public function outputWikiText( $text, $cache = true, $parserOptions = false ) {
4005                 global $wgOut;
4006
4007                 $this->mParserOutput = $this->getOutputFromWikitext( $text, $cache, $parserOptions );
4008                 $wgOut->addParserOutput( $this->mParserOutput );
4009         }
4010
4011         /**
4012          * This does all the heavy lifting for outputWikitext, except it returns the parser
4013          * output instead of sending it straight to $wgOut. Makes things nice and simple for,
4014          * say, embedding thread pages within a discussion system (LiquidThreads)
4015          */
4016         public function getOutputFromWikitext( $text, $cache = true, $parserOptions = false ) {
4017                 global $wgParser, $wgOut, $wgEnableParserCache, $wgUseFileCache;
4018
4019                 if ( !$parserOptions ) {
4020                         $parserOptions = $this->getParserOptions();
4021                 }
4022
4023                 $time = - wfTime();
4024                 $this->mParserOutput = $wgParser->parse( $text, $this->mTitle,
4025                         $parserOptions, true, true, $this->getRevIdFetched() );
4026                 $time += wfTime();
4027
4028                 # Timing hack
4029                 if ( $time > 3 ) {
4030                         wfDebugLog( 'slow-parse', sprintf( "%-5.2f %s", $time,
4031                                 $this->mTitle->getPrefixedDBkey() ) );
4032                 }
4033
4034                 if ( $wgEnableParserCache && $cache && $this && $this->mParserOutput->getCacheTime() != -1 ) {
4035                         $parserCache = ParserCache::singleton();
4036                         $parserCache->save( $this->mParserOutput, $this, $parserOptions );
4037                 }
4038                 // Make sure file cache is not used on uncacheable content.
4039                 // Output that has magic words in it can still use the parser cache
4040                 // (if enabled), though it will generally expire sooner.
4041                 if ( $this->mParserOutput->getCacheTime() == -1 || $this->mParserOutput->containsOldMagic() ) {
4042                         $wgUseFileCache = false;
4043                 }
4044                 $this->doCascadeProtectionUpdates( $this->mParserOutput );
4045                 return $this->mParserOutput;
4046         }
4047
4048         /**
4049          * Get parser options suitable for rendering the primary article wikitext
4050          */
4051         public function getParserOptions() {
4052                 global $wgUser;
4053                 if ( !$this->mParserOptions ) {
4054                         $this->mParserOptions = new ParserOptions( $wgUser );
4055                         $this->mParserOptions->setTidy( true );
4056                         $this->mParserOptions->enableLimitReport();
4057                 }
4058                 return $this->mParserOptions;
4059         }
4060
4061         protected function doCascadeProtectionUpdates( $parserOutput ) {
4062                 if ( !$this->isCurrent() || wfReadOnly() || !$this->mTitle->areRestrictionsCascading() ) {
4063                         return;
4064                 }
4065
4066                 // templatelinks table may have become out of sync,
4067                 // especially if using variable-based transclusions.
4068                 // For paranoia, check if things have changed and if
4069                 // so apply updates to the database. This will ensure
4070                 // that cascaded protections apply as soon as the changes
4071                 // are visible.
4072
4073                 # Get templates from templatelinks
4074                 $id = $this->mTitle->getArticleID();
4075
4076                 $tlTemplates = array();
4077
4078                 $dbr = wfGetDB( DB_SLAVE );
4079                 $res = $dbr->select( array( 'templatelinks' ),
4080                         array( 'tl_namespace', 'tl_title' ),
4081                         array( 'tl_from' => $id ),
4082                         __METHOD__ );
4083
4084                 global $wgContLang;
4085                 foreach ( $res as $row ) {
4086                         $tlTemplates["{$row->tl_namespace}:{$row->tl_title}"] = true;
4087                 }
4088
4089                 # Get templates from parser output.
4090                 $poTemplates = array();
4091                 foreach ( $parserOutput->getTemplates() as $ns => $templates ) {
4092                         foreach ( $templates as $dbk => $id ) {
4093                                 $poTemplates["$ns:$dbk"] = true;
4094                         }
4095                 }
4096
4097                 # Get the diff
4098                 # Note that we simulate array_diff_key in PHP <5.0.x
4099                 $templates_diff = array_diff_key( $poTemplates, $tlTemplates );
4100
4101                 if ( count( $templates_diff ) > 0 ) {
4102                         # Whee, link updates time.
4103                         $u = new LinksUpdate( $this->mTitle, $parserOutput, false );
4104                         $u->doUpdate();
4105                 }
4106         }
4107
4108         /**
4109          * Update all the appropriate counts in the category table, given that
4110          * we've added the categories $added and deleted the categories $deleted.
4111          *
4112          * @param $added array   The names of categories that were added
4113          * @param $deleted array The names of categories that were deleted
4114          * @return null
4115          */
4116         public function updateCategoryCounts( $added, $deleted ) {
4117                 $ns = $this->mTitle->getNamespace();
4118                 $dbw = wfGetDB( DB_MASTER );
4119
4120                 # First make sure the rows exist.  If one of the "deleted" ones didn't
4121                 # exist, we might legitimately not create it, but it's simpler to just
4122                 # create it and then give it a negative value, since the value is bogus
4123                 # anyway.
4124                 #
4125                 # Sometimes I wish we had INSERT ... ON DUPLICATE KEY UPDATE.
4126                 $insertCats = array_merge( $added, $deleted );
4127                 if ( !$insertCats ) {
4128                         # Okay, nothing to do
4129                         return;
4130                 }
4131                 $insertRows = array();
4132                 foreach ( $insertCats as $cat ) {
4133                         $insertRows[] = array(
4134                                 'cat_id' => $dbw->nextSequenceValue( 'category_cat_id_seq' ),
4135                                 'cat_title' => $cat
4136                         );
4137                 }
4138                 $dbw->insert( 'category', $insertRows, __METHOD__, 'IGNORE' );
4139
4140                 $addFields    = array( 'cat_pages = cat_pages + 1' );
4141                 $removeFields = array( 'cat_pages = cat_pages - 1' );
4142                 if ( $ns == NS_CATEGORY ) {
4143                         $addFields[]    = 'cat_subcats = cat_subcats + 1';
4144                         $removeFields[] = 'cat_subcats = cat_subcats - 1';
4145                 } elseif ( $ns == NS_FILE ) {
4146                         $addFields[]    = 'cat_files = cat_files + 1';
4147                         $removeFields[] = 'cat_files = cat_files - 1';
4148                 }
4149
4150                 if ( $added ) {
4151                         $dbw->update(
4152                                 'category',
4153                                 $addFields,
4154                                 array( 'cat_title' => $added ),
4155                                 __METHOD__
4156                         );
4157                 }
4158                 if ( $deleted ) {
4159                         $dbw->update(
4160                                 'category',
4161                                 $removeFields,
4162                                 array( 'cat_title' => $deleted ),
4163                                 __METHOD__
4164                         );
4165                 }
4166         }
4167
4168         /** Lightweight method to get the parser output for a page, checking the parser cache
4169          * and so on. Doesn't consider most of the stuff that Article::view is forced to
4170          * consider, so it's not appropriate to use there.
4171          */
4172         function getParserOutput( $oldid = null ) {
4173                 global $wgEnableParserCache, $wgUser, $wgOut;
4174
4175                 // Should the parser cache be used?
4176                 $useParserCache = $wgEnableParserCache &&
4177                           intval( $wgUser->getOption( 'stubthreshold' ) ) == 0 &&
4178                           $this->exists() &&
4179                           $oldid === null;
4180
4181                 wfDebug( __METHOD__ . ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
4182                 if ( $wgUser->getOption( 'stubthreshold' ) ) {
4183                         wfIncrStats( 'pcache_miss_stub' );
4184                 }
4185
4186                 $parserOutput = false;
4187                 if ( $useParserCache ) {
4188                         $parserOutput = ParserCache::singleton()->get( $this, $this->getParserOptions() );
4189                 }
4190
4191                 if ( $parserOutput === false ) {
4192                         // Cache miss; parse and output it.
4193                         $rev = Revision::newFromTitle( $this->getTitle(), $oldid );
4194
4195                         return $this->getOutputFromWikitext( $rev->getText(), $useParserCache );
4196                 } else {
4197                         return $parserOutput;
4198                 }
4199         }
4200 }