[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/page/ -> WikiPage.php (source)

   1  <?php
   2  /**
   3   * Base representation for a MediaWiki page.
   4   *
   5   * This program is free software; you can redistribute it and/or modify
   6   * it under the terms of the GNU General Public License as published by
   7   * the Free Software Foundation; either version 2 of the License, or
   8   * (at your option) any later version.
   9   *
  10   * This program is distributed in the hope that it will be useful,
  11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13   * GNU General Public License for more details.
  14   *
  15   * You should have received a copy of the GNU General Public License along
  16   * with this program; if not, write to the Free Software Foundation, Inc.,
  17   * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18   * http://www.gnu.org/copyleft/gpl.html
  19   *
  20   * @file
  21   */
  22  
  23  /**
  24   * Abstract class for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
  25   */
  26  interface Page {
  27  }
  28  
  29  /**
  30   * Class representing a MediaWiki article and history.
  31   *
  32   * Some fields are public only for backwards-compatibility. Use accessors.
  33   * In the past, this class was part of Article.php and everything was public.
  34   *
  35   * @internal documentation reviewed 15 Mar 2010
  36   */
  37  class WikiPage implements Page, IDBAccessObject {
  38      // Constants for $mDataLoadedFrom and related
  39  
  40      /**
  41       * @var Title
  42       */
  43      public $mTitle = null;
  44  
  45      /**@{{
  46       * @protected
  47       */
  48      public $mDataLoaded = false;         // !< Boolean
  49      public $mIsRedirect = false;         // !< Boolean
  50      public $mLatest = false;             // !< Integer (false means "not loaded")
  51      /**@}}*/
  52  
  53      /** @var stdclass Map of cache fields (text, parser output, ect) for a proposed/new edit */
  54      public $mPreparedEdit = false;
  55  
  56      /**
  57       * @var int
  58       */
  59      protected $mId = null;
  60  
  61      /**
  62       * @var int One of the READ_* constants
  63       */
  64      protected $mDataLoadedFrom = self::READ_NONE;
  65  
  66      /**
  67       * @var Title
  68       */
  69      protected $mRedirectTarget = null;
  70  
  71      /**
  72       * @var Revision
  73       */
  74      protected $mLastRevision = null;
  75  
  76      /**
  77       * @var string Timestamp of the current revision or empty string if not loaded
  78       */
  79      protected $mTimestamp = '';
  80  
  81      /**
  82       * @var string
  83       */
  84      protected $mTouched = '19700101000000';
  85  
  86      /**
  87       * @var string
  88       */
  89      protected $mLinksUpdated = '19700101000000';
  90  
  91      /**
  92       * @var int|null
  93       */
  94      protected $mCounter = null;
  95  
  96      /**
  97       * Constructor and clear the article
  98       * @param Title $title Reference to a Title object.
  99       */
 100  	public function __construct( Title $title ) {
 101          $this->mTitle = $title;
 102      }
 103  
 104      /**
 105       * Create a WikiPage object of the appropriate class for the given title.
 106       *
 107       * @param Title $title
 108       *
 109       * @throws MWException
 110       * @return WikiPage Object of the appropriate type
 111       */
 112  	public static function factory( Title $title ) {
 113          $ns = $title->getNamespace();
 114  
 115          if ( $ns == NS_MEDIA ) {
 116              throw new MWException( "NS_MEDIA is a virtual namespace; use NS_FILE." );
 117          } elseif ( $ns < 0 ) {
 118              throw new MWException( "Invalid or virtual namespace $ns given." );
 119          }
 120  
 121          switch ( $ns ) {
 122              case NS_FILE:
 123                  $page = new WikiFilePage( $title );
 124                  break;
 125              case NS_CATEGORY:
 126                  $page = new WikiCategoryPage( $title );
 127                  break;
 128              default:
 129                  $page = new WikiPage( $title );
 130          }
 131  
 132          return $page;
 133      }
 134  
 135      /**
 136       * Constructor from a page id
 137       *
 138       * @param int $id Article ID to load
 139       * @param string|int $from One of the following values:
 140       *        - "fromdb" or WikiPage::READ_NORMAL to select from a slave database
 141       *        - "fromdbmaster" or WikiPage::READ_LATEST to select from the master database
 142       *
 143       * @return WikiPage|null
 144       */
 145  	public static function newFromID( $id, $from = 'fromdb' ) {
 146          // page id's are never 0 or negative, see bug 61166
 147          if ( $id < 1 ) {
 148              return null;
 149          }
 150  
 151          $from = self::convertSelectType( $from );
 152          $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_SLAVE );
 153          $row = $db->selectRow( 'page', self::selectFields(), array( 'page_id' => $id ), __METHOD__ );
 154          if ( !$row ) {
 155              return null;
 156          }
 157          return self::newFromRow( $row, $from );
 158      }
 159  
 160      /**
 161       * Constructor from a database row
 162       *
 163       * @since 1.20
 164       * @param object $row Database row containing at least fields returned by selectFields().
 165       * @param string|int $from Source of $data:
 166       *        - "fromdb" or WikiPage::READ_NORMAL: from a slave DB
 167       *        - "fromdbmaster" or WikiPage::READ_LATEST: from the master DB
 168       *        - "forupdate" or WikiPage::READ_LOCKING: from the master DB using SELECT FOR UPDATE
 169       * @return WikiPage
 170       */
 171  	public static function newFromRow( $row, $from = 'fromdb' ) {
 172          $page = self::factory( Title::newFromRow( $row ) );
 173          $page->loadFromRow( $row, $from );
 174          return $page;
 175      }
 176  
 177      /**
 178       * Convert 'fromdb', 'fromdbmaster' and 'forupdate' to READ_* constants.
 179       *
 180       * @param object|string|int $type
 181       * @return mixed
 182       */
 183  	private static function convertSelectType( $type ) {
 184          switch ( $type ) {
 185          case 'fromdb':
 186              return self::READ_NORMAL;
 187          case 'fromdbmaster':
 188              return self::READ_LATEST;
 189          case 'forupdate':
 190              return self::READ_LOCKING;
 191          default:
 192              // It may already be an integer or whatever else
 193              return $type;
 194          }
 195      }
 196  
 197      /**
 198       * Returns overrides for action handlers.
 199       * Classes listed here will be used instead of the default one when
 200       * (and only when) $wgActions[$action] === true. This allows subclasses
 201       * to override the default behavior.
 202       *
 203       * @todo Move this UI stuff somewhere else
 204       *
 205       * @return array
 206       */
 207  	public function getActionOverrides() {
 208          $content_handler = $this->getContentHandler();
 209          return $content_handler->getActionOverrides();
 210      }
 211  
 212      /**
 213       * Returns the ContentHandler instance to be used to deal with the content of this WikiPage.
 214       *
 215       * Shorthand for ContentHandler::getForModelID( $this->getContentModel() );
 216       *
 217       * @return ContentHandler
 218       *
 219       * @since 1.21
 220       */
 221  	public function getContentHandler() {
 222          return ContentHandler::getForModelID( $this->getContentModel() );
 223      }
 224  
 225      /**
 226       * Get the title object of the article
 227       * @return Title Title object of this page
 228       */
 229  	public function getTitle() {
 230          return $this->mTitle;
 231      }
 232  
 233      /**
 234       * Clear the object
 235       * @return void
 236       */
 237  	public function clear() {
 238          $this->mDataLoaded = false;
 239          $this->mDataLoadedFrom = self::READ_NONE;
 240  
 241          $this->clearCacheFields();
 242      }
 243  
 244      /**
 245       * Clear the object cache fields
 246       * @return void
 247       */
 248  	protected function clearCacheFields() {
 249          $this->mId = null;
 250          $this->mCounter = null;
 251          $this->mRedirectTarget = null; // Title object if set
 252          $this->mLastRevision = null; // Latest revision
 253          $this->mTouched = '19700101000000';
 254          $this->mLinksUpdated = '19700101000000';
 255          $this->mTimestamp = '';
 256          $this->mIsRedirect = false;
 257          $this->mLatest = false;
 258          // Bug 57026: do not clear mPreparedEdit since prepareTextForEdit() already checks
 259          // the requested rev ID and content against the cached one for equality. For most
 260          // content types, the output should not change during the lifetime of this cache.
 261          // Clearing it can cause extra parses on edit for no reason.
 262      }
 263  
 264      /**
 265       * Clear the mPreparedEdit cache field, as may be needed by mutable content types
 266       * @return void
 267       * @since 1.23
 268       */
 269  	public function clearPreparedEdit() {
 270          $this->mPreparedEdit = false;
 271      }
 272  
 273      /**
 274       * Return the list of revision fields that should be selected to create
 275       * a new page.
 276       *
 277       * @return array
 278       */
 279  	public static function selectFields() {
 280          global $wgContentHandlerUseDB, $wgPageLanguageUseDB;
 281  
 282          $fields = array(
 283              'page_id',
 284              'page_namespace',
 285              'page_title',
 286              'page_restrictions',
 287              'page_counter',
 288              'page_is_redirect',
 289              'page_is_new',
 290              'page_random',
 291              'page_touched',
 292              'page_links_updated',
 293              'page_latest',
 294              'page_len',
 295          );
 296  
 297          if ( $wgContentHandlerUseDB ) {
 298              $fields[] = 'page_content_model';
 299          }
 300  
 301          if ( $wgPageLanguageUseDB ) {
 302              $fields[] = 'page_lang';
 303          }
 304  
 305          return $fields;
 306      }
 307  
 308      /**
 309       * Fetch a page record with the given conditions
 310       * @param DatabaseBase $dbr
 311       * @param array $conditions
 312       * @param array $options
 313       * @return object|bool Database result resource, or false on failure
 314       */
 315  	protected function pageData( $dbr, $conditions, $options = array() ) {
 316          $fields = self::selectFields();
 317  
 318          wfRunHooks( 'ArticlePageDataBefore', array( &$this, &$fields ) );
 319  
 320          $row = $dbr->selectRow( 'page', $fields, $conditions, __METHOD__, $options );
 321  
 322          wfRunHooks( 'ArticlePageDataAfter', array( &$this, &$row ) );
 323  
 324          return $row;
 325      }
 326  
 327      /**
 328       * Fetch a page record matching the Title object's namespace and title
 329       * using a sanitized title string
 330       *
 331       * @param DatabaseBase $dbr
 332       * @param Title $title
 333       * @param array $options
 334       * @return object|bool Database result resource, or false on failure
 335       */
 336  	public function pageDataFromTitle( $dbr, $title, $options = array() ) {
 337          return $this->pageData( $dbr, array(
 338              'page_namespace' => $title->getNamespace(),
 339              'page_title' => $title->getDBkey() ), $options );
 340      }
 341  
 342      /**
 343       * Fetch a page record matching the requested ID
 344       *
 345       * @param DatabaseBase $dbr
 346       * @param int $id
 347       * @param array $options
 348       * @return object|bool Database result resource, or false on failure
 349       */
 350  	public function pageDataFromId( $dbr, $id, $options = array() ) {
 351          return $this->pageData( $dbr, array( 'page_id' => $id ), $options );
 352      }
 353  
 354      /**
 355       * Set the general counter, title etc data loaded from
 356       * some source.
 357       *
 358       * @param object|string|int $from One of the following:
 359       *   - A DB query result object.
 360       *   - "fromdb" or WikiPage::READ_NORMAL to get from a slave DB.
 361       *   - "fromdbmaster" or WikiPage::READ_LATEST to get from the master DB.
 362       *   - "forupdate"  or WikiPage::READ_LOCKING to get from the master DB
 363       *     using SELECT FOR UPDATE.
 364       *
 365       * @return void
 366       */
 367  	public function loadPageData( $from = 'fromdb' ) {
 368          $from = self::convertSelectType( $from );
 369          if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
 370              // We already have the data from the correct location, no need to load it twice.
 371              return;
 372          }
 373  
 374          if ( $from === self::READ_LOCKING ) {
 375              $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle, array( 'FOR UPDATE' ) );
 376          } elseif ( $from === self::READ_LATEST ) {
 377              $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle );
 378          } elseif ( $from === self::READ_NORMAL ) {
 379              $data = $this->pageDataFromTitle( wfGetDB( DB_SLAVE ), $this->mTitle );
 380              // Use a "last rev inserted" timestamp key to diminish the issue of slave lag.
 381              // Note that DB also stores the master position in the session and checks it.
 382              $touched = $this->getCachedLastEditTime();
 383              if ( $touched ) { // key set
 384                  if ( !$data || $touched > wfTimestamp( TS_MW, $data->page_touched ) ) {
 385                      $from = self::READ_LATEST;
 386                      $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle );
 387                  }
 388              }
 389          } else {
 390              // No idea from where the caller got this data, assume slave database.
 391              $data = $from;
 392              $from = self::READ_NORMAL;
 393          }
 394  
 395          $this->loadFromRow( $data, $from );
 396      }
 397  
 398      /**
 399       * Load the object from a database row
 400       *
 401       * @since 1.20
 402       * @param object $data Database row containing at least fields returned by selectFields()
 403       * @param string|int $from One of the following:
 404       *        - "fromdb" or WikiPage::READ_NORMAL if the data comes from a slave DB
 405       *        - "fromdbmaster" or WikiPage::READ_LATEST if the data comes from the master DB
 406       *        - "forupdate"  or WikiPage::READ_LOCKING if the data comes from from
 407       *          the master DB using SELECT FOR UPDATE
 408       */
 409  	public function loadFromRow( $data, $from ) {
 410          $lc = LinkCache::singleton();
 411          $lc->clearLink( $this->mTitle );
 412  
 413          if ( $data ) {
 414              $lc->addGoodLinkObjFromRow( $this->mTitle, $data );
 415  
 416              $this->mTitle->loadFromRow( $data );
 417  
 418              // Old-fashioned restrictions
 419              $this->mTitle->loadRestrictions( $data->page_restrictions );
 420  
 421              $this->mId = intval( $data->page_id );
 422              $this->mCounter = intval( $data->page_counter );
 423              $this->mTouched = wfTimestamp( TS_MW, $data->page_touched );
 424              $this->mLinksUpdated = wfTimestampOrNull( TS_MW, $data->page_links_updated );
 425              $this->mIsRedirect = intval( $data->page_is_redirect );
 426              $this->mLatest = intval( $data->page_latest );
 427              // Bug 37225: $latest may no longer match the cached latest Revision object.
 428              // Double-check the ID of any cached latest Revision object for consistency.
 429              if ( $this->mLastRevision && $this->mLastRevision->getId() != $this->mLatest ) {
 430                  $this->mLastRevision = null;
 431                  $this->mTimestamp = '';
 432              }
 433          } else {
 434              $lc->addBadLinkObj( $this->mTitle );
 435  
 436              $this->mTitle->loadFromRow( false );
 437  
 438              $this->clearCacheFields();
 439  
 440              $this->mId = 0;
 441          }
 442  
 443          $this->mDataLoaded = true;
 444          $this->mDataLoadedFrom = self::convertSelectType( $from );
 445      }
 446  
 447      /**
 448       * @return int Page ID
 449       */
 450  	public function getId() {
 451          if ( !$this->mDataLoaded ) {
 452              $this->loadPageData();
 453          }
 454          return $this->mId;
 455      }
 456  
 457      /**
 458       * @return bool Whether or not the page exists in the database
 459       */
 460  	public function exists() {
 461          if ( !$this->mDataLoaded ) {
 462              $this->loadPageData();
 463          }
 464          return $this->mId > 0;
 465      }
 466  
 467      /**
 468       * Check if this page is something we're going to be showing
 469       * some sort of sensible content for. If we return false, page
 470       * views (plain action=view) will return an HTTP 404 response,
 471       * so spiders and robots can know they're following a bad link.
 472       *
 473       * @return bool
 474       */
 475  	public function hasViewableContent() {
 476          return $this->exists() || $this->mTitle->isAlwaysKnown();
 477      }
 478  
 479      /**
 480       * @return int The view count for the page
 481       */
 482  	public function getCount() {
 483          if ( !$this->mDataLoaded ) {
 484              $this->loadPageData();
 485          }
 486  
 487          return $this->mCounter;
 488      }
 489  
 490      /**
 491       * Tests if the article content represents a redirect
 492       *
 493       * @return bool
 494       */
 495  	public function isRedirect() {
 496          $content = $this->getContent();
 497          if ( !$content ) {
 498              return false;
 499          }
 500  
 501          return $content->isRedirect();
 502      }
 503  
 504      /**
 505       * Returns the page's content model id (see the CONTENT_MODEL_XXX constants).
 506       *
 507       * Will use the revisions actual content model if the page exists,
 508       * and the page's default if the page doesn't exist yet.
 509       *
 510       * @return string
 511       *
 512       * @since 1.21
 513       */
 514  	public function getContentModel() {
 515          if ( $this->exists() ) {
 516              // look at the revision's actual content model
 517              $rev = $this->getRevision();
 518  
 519              if ( $rev !== null ) {
 520                  return $rev->getContentModel();
 521              } else {
 522                  $title = $this->mTitle->getPrefixedDBkey();
 523                  wfWarn( "Page $title exists but has no (visible) revisions!" );
 524              }
 525          }
 526  
 527          // use the default model for this page
 528          return $this->mTitle->getContentModel();
 529      }
 530  
 531      /**
 532       * Loads page_touched and returns a value indicating if it should be used
 533       * @return bool True if not a redirect
 534       */
 535  	public function checkTouched() {
 536          if ( !$this->mDataLoaded ) {
 537              $this->loadPageData();
 538          }
 539          return !$this->mIsRedirect;
 540      }
 541  
 542      /**
 543       * Get the page_touched field
 544       * @return string Containing GMT timestamp
 545       */
 546  	public function getTouched() {
 547          if ( !$this->mDataLoaded ) {
 548              $this->loadPageData();
 549          }
 550          return $this->mTouched;
 551      }
 552  
 553      /**
 554       * Get the page_links_updated field
 555       * @return string|null Containing GMT timestamp
 556       */
 557  	public function getLinksTimestamp() {
 558          if ( !$this->mDataLoaded ) {
 559              $this->loadPageData();
 560          }
 561          return $this->mLinksUpdated;
 562      }
 563  
 564      /**
 565       * Get the page_latest field
 566       * @return int The rev_id of current revision
 567       */
 568  	public function getLatest() {
 569          if ( !$this->mDataLoaded ) {
 570              $this->loadPageData();
 571          }
 572          return (int)$this->mLatest;
 573      }
 574  
 575      /**
 576       * Get the Revision object of the oldest revision
 577       * @return Revision|null
 578       */
 579  	public function getOldestRevision() {
 580          wfProfileIn( __METHOD__ );
 581  
 582          // Try using the slave database first, then try the master
 583          $continue = 2;
 584          $db = wfGetDB( DB_SLAVE );
 585          $revSelectFields = Revision::selectFields();
 586  
 587          $row = null;
 588          while ( $continue ) {
 589              $row = $db->selectRow(
 590                  array( 'page', 'revision' ),
 591                  $revSelectFields,
 592                  array(
 593                      'page_namespace' => $this->mTitle->getNamespace(),
 594                      'page_title' => $this->mTitle->getDBkey(),
 595                      'rev_page = page_id'
 596                  ),
 597                  __METHOD__,
 598                  array(
 599                      'ORDER BY' => 'rev_timestamp ASC'
 600                  )
 601              );
 602  
 603              if ( $row ) {
 604                  $continue = 0;
 605              } else {
 606                  $db = wfGetDB( DB_MASTER );
 607                  $continue--;
 608              }
 609          }
 610  
 611          wfProfileOut( __METHOD__ );
 612          return $row ? Revision::newFromRow( $row ) : null;
 613      }
 614  
 615      /**
 616       * Loads everything except the text
 617       * This isn't necessary for all uses, so it's only done if needed.
 618       */
 619  	protected function loadLastEdit() {
 620          if ( $this->mLastRevision !== null ) {
 621              return; // already loaded
 622          }
 623  
 624          $latest = $this->getLatest();
 625          if ( !$latest ) {
 626              return; // page doesn't exist or is missing page_latest info
 627          }
 628  
 629          // Bug 37225: if session S1 loads the page row FOR UPDATE, the result always includes the
 630          // latest changes committed. This is true even within REPEATABLE-READ transactions, where
 631          // S1 normally only sees changes committed before the first S1 SELECT. Thus we need S1 to
 632          // also gets the revision row FOR UPDATE; otherwise, it may not find it since a page row
 633          // UPDATE and revision row INSERT by S2 may have happened after the first S1 SELECT.
 634          // http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read.
 635          $flags = ( $this->mDataLoadedFrom == self::READ_LOCKING ) ? Revision::READ_LOCKING : 0;
 636          $revision = Revision::newFromPageId( $this->getId(), $latest, $flags );
 637          if ( $revision ) { // sanity
 638              $this->setLastEdit( $revision );
 639          }
 640      }
 641  
 642      /**
 643       * Set the latest revision
 644       * @param Revision $revision
 645       */
 646  	protected function setLastEdit( Revision $revision ) {
 647          $this->mLastRevision = $revision;
 648          $this->mTimestamp = $revision->getTimestamp();
 649      }
 650  
 651      /**
 652       * Get the latest revision
 653       * @return Revision|null
 654       */
 655  	public function getRevision() {
 656          $this->loadLastEdit();
 657          if ( $this->mLastRevision ) {
 658              return $this->mLastRevision;
 659          }
 660          return null;
 661      }
 662  
 663      /**
 664       * Get the content of the current revision. No side-effects...
 665       *
 666       * @param int $audience One of:
 667       *   Revision::FOR_PUBLIC       to be displayed to all users
 668       *   Revision::FOR_THIS_USER    to be displayed to $wgUser
 669       *   Revision::RAW              get the text regardless of permissions
 670       * @param User $user User object to check for, only if FOR_THIS_USER is passed
 671       *   to the $audience parameter
 672       * @return Content|null The content of the current revision
 673       *
 674       * @since 1.21
 675       */
 676  	public function getContent( $audience = Revision::FOR_PUBLIC, User $user = null ) {
 677          $this->loadLastEdit();
 678          if ( $this->mLastRevision ) {
 679              return $this->mLastRevision->getContent( $audience, $user );
 680          }
 681          return null;
 682      }
 683  
 684      /**
 685       * Get the text of the current revision. No side-effects...
 686       *
 687       * @param int $audience One of:
 688       *   Revision::FOR_PUBLIC       to be displayed to all users
 689       *   Revision::FOR_THIS_USER    to be displayed to the given user
 690       *   Revision::RAW              get the text regardless of permissions
 691       * @param User $user User object to check for, only if FOR_THIS_USER is passed
 692       *   to the $audience parameter
 693       * @return string|bool The text of the current revision
 694       * @deprecated since 1.21, getContent() should be used instead.
 695       */
 696  	public function getText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
 697          ContentHandler::deprecated( __METHOD__, '1.21' );
 698  
 699          $this->loadLastEdit();
 700          if ( $this->mLastRevision ) {
 701              return $this->mLastRevision->getText( $audience, $user );
 702          }
 703          return false;
 704      }
 705  
 706      /**
 707       * Get the text of the current revision. No side-effects...
 708       *
 709       * @return string|bool The text of the current revision. False on failure
 710       * @deprecated since 1.21, getContent() should be used instead.
 711       */
 712  	public function getRawText() {
 713          ContentHandler::deprecated( __METHOD__, '1.21' );
 714  
 715          return $this->getText( Revision::RAW );
 716      }
 717  
 718      /**
 719       * @return string MW timestamp of last article revision
 720       */
 721  	public function getTimestamp() {
 722          // Check if the field has been filled by WikiPage::setTimestamp()
 723          if ( !$this->mTimestamp ) {
 724              $this->loadLastEdit();
 725          }
 726  
 727          return wfTimestamp( TS_MW, $this->mTimestamp );
 728      }
 729  
 730      /**
 731       * Set the page timestamp (use only to avoid DB queries)
 732       * @param string $ts MW timestamp of last article revision
 733       * @return void
 734       */
 735  	public function setTimestamp( $ts ) {
 736          $this->mTimestamp = wfTimestamp( TS_MW, $ts );
 737      }
 738  
 739      /**
 740       * @param int $audience One of:
 741       *   Revision::FOR_PUBLIC       to be displayed to all users
 742       *   Revision::FOR_THIS_USER    to be displayed to the given user
 743       *   Revision::RAW              get the text regardless of permissions
 744       * @param User $user User object to check for, only if FOR_THIS_USER is passed
 745       *   to the $audience parameter
 746       * @return int User ID for the user that made the last article revision
 747       */
 748  	public function getUser( $audience = Revision::FOR_PUBLIC, User $user = null ) {
 749          $this->loadLastEdit();
 750          if ( $this->mLastRevision ) {
 751              return $this->mLastRevision->getUser( $audience, $user );
 752          } else {
 753              return -1;
 754          }
 755      }
 756  
 757      /**
 758       * Get the User object of the user who created the page
 759       * @param int $audience One of:
 760       *   Revision::FOR_PUBLIC       to be displayed to all users
 761       *   Revision::FOR_THIS_USER    to be displayed to the given user
 762       *   Revision::RAW              get the text regardless of permissions
 763       * @param User $user User object to check for, only if FOR_THIS_USER is passed
 764       *   to the $audience parameter
 765       * @return User|null
 766       */
 767  	public function getCreator( $audience = Revision::FOR_PUBLIC, User $user = null ) {
 768          $revision = $this->getOldestRevision();
 769          if ( $revision ) {
 770              $userName = $revision->getUserText( $audience, $user );
 771              return User::newFromName( $userName, false );
 772          } else {
 773              return null;
 774          }
 775      }
 776  
 777      /**
 778       * @param int $audience One of:
 779       *   Revision::FOR_PUBLIC       to be displayed to all users
 780       *   Revision::FOR_THIS_USER    to be displayed to the given user
 781       *   Revision::RAW              get the text regardless of permissions
 782       * @param User $user User object to check for, only if FOR_THIS_USER is passed
 783       *   to the $audience parameter
 784       * @return string Username of the user that made the last article revision
 785       */
 786  	public function getUserText( $audience = Revision::FOR_PUBLIC, User $user = null ) {
 787          $this->loadLastEdit();
 788          if ( $this->mLastRevision ) {
 789              return $this->mLastRevision->getUserText( $audience, $user );
 790          } else {
 791              return '';
 792          }
 793      }
 794  
 795      /**
 796       * @param int $audience One of:
 797       *   Revision::FOR_PUBLIC       to be displayed to all users
 798       *   Revision::FOR_THIS_USER    to be displayed to the given user
 799       *   Revision::RAW              get the text regardless of permissions
 800       * @param User $user User object to check for, only if FOR_THIS_USER is passed
 801       *   to the $audience parameter
 802       * @return string Comment stored for the last article revision
 803       */
 804  	public function getComment( $audience = Revision::FOR_PUBLIC, User $user = null ) {
 805          $this->loadLastEdit();
 806          if ( $this->mLastRevision ) {
 807              return $this->mLastRevision->getComment( $audience, $user );
 808          } else {
 809              return '';
 810          }
 811      }
 812  
 813      /**
 814       * Returns true if last revision was marked as "minor edit"
 815       *
 816       * @return bool Minor edit indicator for the last article revision.
 817       */
 818  	public function getMinorEdit() {
 819          $this->loadLastEdit();
 820          if ( $this->mLastRevision ) {
 821              return $this->mLastRevision->isMinor();
 822          } else {
 823              return false;
 824          }
 825      }
 826  
 827      /**
 828       * Get the cached timestamp for the last time the page changed.
 829       * This is only used to help handle slave lag by comparing to page_touched.
 830       * @return string MW timestamp
 831       */
 832  	protected function getCachedLastEditTime() {
 833          global $wgMemc;
 834          $key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) );
 835          return $wgMemc->get( $key );
 836      }
 837  
 838      /**
 839       * Set the cached timestamp for the last time the page changed.
 840       * This is only used to help handle slave lag by comparing to page_touched.
 841       * @param string $timestamp
 842       * @return void
 843       */
 844  	public function setCachedLastEditTime( $timestamp ) {
 845          global $wgMemc;
 846          $key = wfMemcKey( 'page-lastedit', md5( $this->mTitle->getPrefixedDBkey() ) );
 847          $wgMemc->set( $key, wfTimestamp( TS_MW, $timestamp ), 60 * 15 );
 848      }
 849  
 850      /**
 851       * Determine whether a page would be suitable for being counted as an
 852       * article in the site_stats table based on the title & its content
 853       *
 854       * @param object|bool $editInfo (false): object returned by prepareTextForEdit(),
 855       *   if false, the current database state will be used
 856       * @return bool
 857       */
 858  	public function isCountable( $editInfo = false ) {
 859          global $wgArticleCountMethod;
 860  
 861          if ( !$this->mTitle->isContentPage() ) {
 862              return false;
 863          }
 864  
 865          if ( $editInfo ) {
 866              $content = $editInfo->pstContent;
 867          } else {
 868              $content = $this->getContent();
 869          }
 870  
 871          if ( !$content || $content->isRedirect() ) {
 872              return false;
 873          }
 874  
 875          $hasLinks = null;
 876  
 877          if ( $wgArticleCountMethod === 'link' ) {
 878              // nasty special case to avoid re-parsing to detect links
 879  
 880              if ( $editInfo ) {
 881                  // ParserOutput::getLinks() is a 2D array of page links, so
 882                  // to be really correct we would need to recurse in the array
 883                  // but the main array should only have items in it if there are
 884                  // links.
 885                  $hasLinks = (bool)count( $editInfo->output->getLinks() );
 886              } else {
 887                  $hasLinks = (bool)wfGetDB( DB_SLAVE )->selectField( 'pagelinks', 1,
 888                      array( 'pl_from' => $this->getId() ), __METHOD__ );
 889              }
 890          }
 891  
 892          return $content->isCountable( $hasLinks );
 893      }
 894  
 895      /**
 896       * If this page is a redirect, get its target
 897       *
 898       * The target will be fetched from the redirect table if possible.
 899       * If this page doesn't have an entry there, call insertRedirect()
 900       * @return Title|null Title object, or null if this page is not a redirect
 901       */
 902  	public function getRedirectTarget() {
 903          if ( !$this->mTitle->isRedirect() ) {
 904              return null;
 905          }
 906  
 907          if ( $this->mRedirectTarget !== null ) {
 908              return $this->mRedirectTarget;
 909          }
 910  
 911          // Query the redirect table
 912          $dbr = wfGetDB( DB_SLAVE );
 913          $row = $dbr->selectRow( 'redirect',
 914              array( 'rd_namespace', 'rd_title', 'rd_fragment', 'rd_interwiki' ),
 915              array( 'rd_from' => $this->getId() ),
 916              __METHOD__
 917          );
 918  
 919          // rd_fragment and rd_interwiki were added later, populate them if empty
 920          if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) {
 921              $this->mRedirectTarget = Title::makeTitle(
 922                  $row->rd_namespace, $row->rd_title,
 923                  $row->rd_fragment, $row->rd_interwiki );
 924              return $this->mRedirectTarget;
 925          }
 926  
 927          // This page doesn't have an entry in the redirect table
 928          $this->mRedirectTarget = $this->insertRedirect();
 929          return $this->mRedirectTarget;
 930      }
 931  
 932      /**
 933       * Insert an entry for this page into the redirect table.
 934       *
 935       * Don't call this function directly unless you know what you're doing.
 936       * @return Title|null Title object or null if not a redirect
 937       */
 938  	public function insertRedirect() {
 939          // recurse through to only get the final target
 940          $content = $this->getContent();
 941          $retval = $content ? $content->getUltimateRedirectTarget() : null;
 942          if ( !$retval ) {
 943              return null;
 944          }
 945          $this->insertRedirectEntry( $retval );
 946          return $retval;
 947      }
 948  
 949      /**
 950       * Insert or update the redirect table entry for this page to indicate
 951       * it redirects to $rt .
 952       * @param Title $rt Redirect target
 953       */
 954  	public function insertRedirectEntry( $rt ) {
 955          $dbw = wfGetDB( DB_MASTER );
 956          $dbw->replace( 'redirect', array( 'rd_from' ),
 957              array(
 958                  'rd_from' => $this->getId(),
 959                  'rd_namespace' => $rt->getNamespace(),
 960                  'rd_title' => $rt->getDBkey(),
 961                  'rd_fragment' => $rt->getFragment(),
 962                  'rd_interwiki' => $rt->getInterwiki(),
 963              ),
 964              __METHOD__
 965          );
 966      }
 967  
 968      /**
 969       * Get the Title object or URL this page redirects to
 970       *
 971       * @return bool|Title|string False, Title of in-wiki target, or string with URL
 972       */
 973  	public function followRedirect() {
 974          return $this->getRedirectURL( $this->getRedirectTarget() );
 975      }
 976  
 977      /**
 978       * Get the Title object or URL to use for a redirect. We use Title
 979       * objects for same-wiki, non-special redirects and URLs for everything
 980       * else.
 981       * @param Title $rt Redirect target
 982       * @return bool|Title|string False, Title object of local target, or string with URL
 983       */
 984  	public function getRedirectURL( $rt ) {
 985          if ( !$rt ) {
 986              return false;
 987          }
 988  
 989          if ( $rt->isExternal() ) {
 990              if ( $rt->isLocal() ) {
 991                  // Offsite wikis need an HTTP redirect.
 992                  //
 993                  // This can be hard to reverse and may produce loops,
 994                  // so they may be disabled in the site configuration.
 995                  $source = $this->mTitle->getFullURL( 'redirect=no' );
 996                  return $rt->getFullURL( array( 'rdfrom' => $source ) );
 997              } else {
 998                  // External pages pages without "local" bit set are not valid
 999                  // redirect targets
1000                  return false;
1001              }
1002          }
1003  
1004          if ( $rt->isSpecialPage() ) {
1005              // Gotta handle redirects to special pages differently:
1006              // Fill the HTTP response "Location" header and ignore
1007              // the rest of the page we're on.
1008              //
1009              // Some pages are not valid targets
1010              if ( $rt->isValidRedirectTarget() ) {
1011                  return $rt->getFullURL();
1012              } else {
1013                  return false;
1014              }
1015          }
1016  
1017          return $rt;
1018      }
1019  
1020      /**
1021       * Get a list of users who have edited this article, not including the user who made
1022       * the most recent revision, which you can get from $article->getUser() if you want it
1023       * @return UserArrayFromResult
1024       */
1025  	public function getContributors() {
1026          // @todo FIXME: This is expensive; cache this info somewhere.
1027  
1028          $dbr = wfGetDB( DB_SLAVE );
1029  
1030          if ( $dbr->implicitGroupby() ) {
1031              $realNameField = 'user_real_name';
1032          } else {
1033              $realNameField = 'MIN(user_real_name) AS user_real_name';
1034          }
1035  
1036          $tables = array( 'revision', 'user' );
1037  
1038          $fields = array(
1039              'user_id' => 'rev_user',
1040              'user_name' => 'rev_user_text',
1041              $realNameField,
1042              'timestamp' => 'MAX(rev_timestamp)',
1043          );
1044  
1045          $conds = array( 'rev_page' => $this->getId() );
1046  
1047          // The user who made the top revision gets credited as "this page was last edited by
1048          // John, based on contributions by Tom, Dick and Harry", so don't include them twice.
1049          $user = $this->getUser();
1050          if ( $user ) {
1051              $conds[] = "rev_user != $user";
1052          } else {
1053              $conds[] = "rev_user_text != {$dbr->addQuotes( $this->getUserText() )}";
1054          }
1055  
1056          $conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0"; // username hidden?
1057  
1058          $jconds = array(
1059              'user' => array( 'LEFT JOIN', 'rev_user = user_id' ),
1060          );
1061  
1062          $options = array(
1063              'GROUP BY' => array( 'rev_user', 'rev_user_text' ),
1064              'ORDER BY' => 'timestamp DESC',
1065          );
1066  
1067          $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $options, $jconds );
1068          return new UserArrayFromResult( $res );
1069      }
1070  
1071      /**
1072       * Get the last N authors
1073       * @param int $num Number of revisions to get
1074       * @param int|string $revLatest The latest rev_id, selected from the master (optional)
1075       * @return array Array of authors, duplicates not removed
1076       */
1077  	public function getLastNAuthors( $num, $revLatest = 0 ) {
1078          wfProfileIn( __METHOD__ );
1079          // First try the slave
1080          // If that doesn't have the latest revision, try the master
1081          $continue = 2;
1082          $db = wfGetDB( DB_SLAVE );
1083  
1084          do {
1085              $res = $db->select( array( 'page', 'revision' ),
1086                  array( 'rev_id', 'rev_user_text' ),
1087                  array(
1088                      'page_namespace' => $this->mTitle->getNamespace(),
1089                      'page_title' => $this->mTitle->getDBkey(),
1090                      'rev_page = page_id'
1091                  ), __METHOD__,
1092                  array(
1093                      'ORDER BY' => 'rev_timestamp DESC',
1094                      'LIMIT' => $num
1095                  )
1096              );
1097  
1098              if ( !$res ) {
1099                  wfProfileOut( __METHOD__ );
1100                  return array();
1101              }
1102  
1103              $row = $db->fetchObject( $res );
1104  
1105              if ( $continue == 2 && $revLatest && $row->rev_id != $revLatest ) {
1106                  $db = wfGetDB( DB_MASTER );
1107                  $continue--;
1108              } else {
1109                  $continue = 0;
1110              }
1111          } while ( $continue );
1112  
1113          $authors = array( $row->rev_user_text );
1114  
1115          foreach ( $res as $row ) {
1116              $authors[] = $row->rev_user_text;
1117          }
1118  
1119          wfProfileOut( __METHOD__ );
1120          return $authors;
1121      }
1122  
1123      /**
1124       * Should the parser cache be used?
1125       *
1126       * @param ParserOptions $parserOptions ParserOptions to check
1127       * @param int $oldid
1128       * @return bool
1129       */
1130  	public function isParserCacheUsed( ParserOptions $parserOptions, $oldid ) {
1131          global $wgEnableParserCache;
1132  
1133          return $wgEnableParserCache
1134              && $parserOptions->getStubThreshold() == 0
1135              && $this->exists()
1136              && ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() )
1137              && $this->getContentHandler()->isParserCacheSupported();
1138      }
1139  
1140      /**
1141       * Get a ParserOutput for the given ParserOptions and revision ID.
1142       * The parser cache will be used if possible.
1143       *
1144       * @since 1.19
1145       * @param ParserOptions $parserOptions ParserOptions to use for the parse operation
1146       * @param null|int $oldid Revision ID to get the text from, passing null or 0 will
1147       *   get the current revision (default value)
1148       *
1149       * @return ParserOutput|bool ParserOutput or false if the revision was not found
1150       */
1151  	public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) {
1152          wfProfileIn( __METHOD__ );
1153  
1154          $useParserCache = $this->isParserCacheUsed( $parserOptions, $oldid );
1155          wfDebug( __METHOD__ . ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
1156          if ( $parserOptions->getStubThreshold() ) {
1157              wfIncrStats( 'pcache_miss_stub' );
1158          }
1159  
1160          if ( $useParserCache ) {
1161              $parserOutput = ParserCache::singleton()->get( $this, $parserOptions );
1162              if ( $parserOutput !== false ) {
1163                  wfProfileOut( __METHOD__ );
1164                  return $parserOutput;
1165              }
1166          }
1167  
1168          if ( $oldid === null || $oldid === 0 ) {
1169              $oldid = $this->getLatest();
1170          }
1171  
1172          $pool = new PoolWorkArticleView( $this, $parserOptions, $oldid, $useParserCache );
1173          $pool->execute();
1174  
1175          wfProfileOut( __METHOD__ );
1176  
1177          return $pool->getParserOutput();
1178      }
1179  
1180      /**
1181       * Do standard deferred updates after page view (existing or missing page)
1182       * @param User $user The relevant user
1183       * @param int $oldid The revision id being viewed. If not given or 0, latest revision is assumed.
1184       */
1185  	public function doViewUpdates( User $user, $oldid = 0 ) {
1186          global $wgDisableCounters;
1187          if ( wfReadOnly() ) {
1188              return;
1189          }
1190  
1191          // Don't update page view counters on views from bot users (bug 14044)
1192          if ( !$wgDisableCounters && !$user->isAllowed( 'bot' ) && $this->exists() ) {
1193              DeferredUpdates::addUpdate( new ViewCountUpdate( $this->getId() ) );
1194              DeferredUpdates::addUpdate( new SiteStatsUpdate( 1, 0, 0 ) );
1195          }
1196  
1197          // Update newtalk / watchlist notification status
1198          $user->clearNotification( $this->mTitle, $oldid );
1199      }
1200  
1201      /**
1202       * Perform the actions of a page purging
1203       * @return bool
1204       */
1205  	public function doPurge() {
1206          global $wgUseSquid;
1207  
1208          if ( !wfRunHooks( 'ArticlePurge', array( &$this ) ) ) {
1209              return false;
1210          }
1211  
1212          // Invalidate the cache
1213          $this->mTitle->invalidateCache();
1214  
1215          if ( $wgUseSquid ) {
1216              // Commit the transaction before the purge is sent
1217              $dbw = wfGetDB( DB_MASTER );
1218              $dbw->commit( __METHOD__ );
1219  
1220              // Send purge
1221              $update = SquidUpdate::newSimplePurge( $this->mTitle );
1222              $update->doUpdate();
1223          }
1224  
1225          if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
1226              // @todo move this logic to MessageCache
1227  
1228              if ( $this->exists() ) {
1229                  // NOTE: use transclusion text for messages.
1230                  //       This is consistent with  MessageCache::getMsgFromNamespace()
1231  
1232                  $content = $this->getContent();
1233                  $text = $content === null ? null : $content->getWikitextForTransclusion();
1234  
1235                  if ( $text === null ) {
1236                      $text = false;
1237                  }
1238              } else {
1239                  $text = false;
1240              }
1241  
1242              MessageCache::singleton()->replace( $this->mTitle->getDBkey(), $text );
1243          }
1244          return true;
1245      }
1246  
1247      /**
1248       * Insert a new empty page record for this article.
1249       * This *must* be followed up by creating a revision
1250       * and running $this->updateRevisionOn( ... );
1251       * or else the record will be left in a funky state.
1252       * Best if all done inside a transaction.
1253       *
1254       * @param DatabaseBase $dbw
1255       * @return int The newly created page_id key, or false if the title already existed
1256       */
1257  	public function insertOn( $dbw ) {
1258          wfProfileIn( __METHOD__ );
1259  
1260          $page_id = $dbw->nextSequenceValue( 'page_page_id_seq' );
1261          $dbw->insert( 'page', array(
1262              'page_id'           => $page_id,
1263              'page_namespace'    => $this->mTitle->getNamespace(),
1264              'page_title'        => $this->mTitle->getDBkey(),
1265              'page_counter'      => 0,
1266              'page_restrictions' => '',
1267              'page_is_redirect'  => 0, // Will set this shortly...
1268              'page_is_new'       => 1,
1269              'page_random'       => wfRandom(),
1270              'page_touched'      => $dbw->timestamp(),
1271              'page_latest'       => 0, // Fill this in shortly...
1272              'page_len'          => 0, // Fill this in shortly...
1273          ), __METHOD__, 'IGNORE' );
1274  
1275          $affected = $dbw->affectedRows();
1276  
1277          if ( $affected ) {
1278              $newid = $dbw->insertId();
1279              $this->mId = $newid;
1280              $this->mTitle->resetArticleID( $newid );
1281          }
1282          wfProfileOut( __METHOD__ );
1283  
1284          return $affected ? $newid : false;
1285      }
1286  
1287      /**
1288       * Update the page record to point to a newly saved revision.
1289       *
1290       * @param DatabaseBase $dbw
1291       * @param Revision $revision For ID number, and text used to set
1292       *   length and redirect status fields
1293       * @param int $lastRevision If given, will not overwrite the page field
1294       *   when different from the currently set value.
1295       *   Giving 0 indicates the new page flag should be set on.
1296       * @param bool $lastRevIsRedirect If given, will optimize adding and
1297       *   removing rows in redirect table.
1298       * @return bool True on success, false on failure
1299       */
1300  	public function updateRevisionOn( $dbw, $revision, $lastRevision = null,
1301          $lastRevIsRedirect = null
1302      ) {
1303          global $wgContentHandlerUseDB;
1304  
1305          wfProfileIn( __METHOD__ );
1306  
1307          $content = $revision->getContent();
1308          $len = $content ? $content->getSize() : 0;
1309          $rt = $content ? $content->getUltimateRedirectTarget() : null;
1310  
1311          $conditions = array( 'page_id' => $this->getId() );
1312  
1313          if ( !is_null( $lastRevision ) ) {
1314              // An extra check against threads stepping on each other
1315              $conditions['page_latest'] = $lastRevision;
1316          }
1317  
1318          $now = wfTimestampNow();
1319          $row = array( /* SET */
1320              'page_latest'      => $revision->getId(),
1321              'page_touched'     => $dbw->timestamp( $now ),
1322              'page_is_new'      => ( $lastRevision === 0 ) ? 1 : 0,
1323              'page_is_redirect' => $rt !== null ? 1 : 0,
1324              'page_len'         => $len,
1325          );
1326  
1327          if ( $wgContentHandlerUseDB ) {
1328              $row['page_content_model'] = $revision->getContentModel();
1329          }
1330  
1331          $dbw->update( 'page',
1332              $row,
1333              $conditions,
1334              __METHOD__ );
1335  
1336          $result = $dbw->affectedRows() > 0;
1337          if ( $result ) {
1338              $this->updateRedirectOn( $dbw, $rt, $lastRevIsRedirect );
1339              $this->setLastEdit( $revision );
1340              $this->setCachedLastEditTime( $now );
1341              $this->mLatest = $revision->getId();
1342              $this->mIsRedirect = (bool)$rt;
1343              // Update the LinkCache.
1344              LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect,
1345                                                      $this->mLatest, $revision->getContentModel() );
1346          }
1347  
1348          wfProfileOut( __METHOD__ );
1349          return $result;
1350      }
1351  
1352      /**
1353       * Add row to the redirect table if this is a redirect, remove otherwise.
1354       *
1355       * @param DatabaseBase $dbw
1356       * @param Title $redirectTitle Title object pointing to the redirect target,
1357       *   or NULL if this is not a redirect
1358       * @param null|bool $lastRevIsRedirect If given, will optimize adding and
1359       *   removing rows in redirect table.
1360       * @return bool True on success, false on failure
1361       * @private
1362       */
1363  	public function updateRedirectOn( $dbw, $redirectTitle, $lastRevIsRedirect = null ) {
1364          // Always update redirects (target link might have changed)
1365          // Update/Insert if we don't know if the last revision was a redirect or not
1366          // Delete if changing from redirect to non-redirect
1367          $isRedirect = !is_null( $redirectTitle );
1368  
1369          if ( !$isRedirect && $lastRevIsRedirect === false ) {
1370              return true;
1371          }
1372  
1373          wfProfileIn( __METHOD__ );
1374          if ( $isRedirect ) {
1375              $this->insertRedirectEntry( $redirectTitle );
1376          } else {
1377              // This is not a redirect, remove row from redirect table
1378              $where = array( 'rd_from' => $this->getId() );
1379              $dbw->delete( 'redirect', $where, __METHOD__ );
1380          }
1381  
1382          if ( $this->getTitle()->getNamespace() == NS_FILE ) {
1383              RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $this->getTitle() );
1384          }
1385          wfProfileOut( __METHOD__ );
1386  
1387          return ( $dbw->affectedRows() != 0 );
1388      }
1389  
1390      /**
1391       * If the given revision is newer than the currently set page_latest,
1392       * update the page record. Otherwise, do nothing.
1393       *
1394       * @deprecated since 1.24, use updateRevisionOn instead
1395       *
1396       * @param DatabaseBase $dbw
1397       * @param Revision $revision
1398       * @return bool
1399       */
1400  	public function updateIfNewerOn( $dbw, $revision ) {
1401          wfProfileIn( __METHOD__ );
1402  
1403          $row = $dbw->selectRow(
1404              array( 'revision', 'page' ),
1405              array( 'rev_id', 'rev_timestamp', 'page_is_redirect' ),
1406              array(
1407                  'page_id' => $this->getId(),
1408                  'page_latest=rev_id' ),
1409              __METHOD__ );
1410  
1411          if ( $row ) {
1412              if ( wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) {
1413                  wfProfileOut( __METHOD__ );
1414                  return false;
1415              }
1416              $prev = $row->rev_id;
1417              $lastRevIsRedirect = (bool)$row->page_is_redirect;
1418          } else {
1419              // No or missing previous revision; mark the page as new
1420              $prev = 0;
1421              $lastRevIsRedirect = null;
1422          }
1423  
1424          $ret = $this->updateRevisionOn( $dbw, $revision, $prev, $lastRevIsRedirect );
1425  
1426          wfProfileOut( __METHOD__ );
1427          return $ret;
1428      }
1429  
1430      /**
1431       * Get the content that needs to be saved in order to undo all revisions
1432       * between $undo and $undoafter. Revisions must belong to the same page,
1433       * must exist and must not be deleted
1434       * @param Revision $undo
1435       * @param Revision $undoafter Must be an earlier revision than $undo
1436       * @return mixed String on success, false on failure
1437       * @since 1.21
1438       * Before we had the Content object, this was done in getUndoText
1439       */
1440  	public function getUndoContent( Revision $undo, Revision $undoafter = null ) {
1441          $handler = $undo->getContentHandler();
1442          return $handler->getUndoContent( $this->getRevision(), $undo, $undoafter );
1443      }
1444  
1445      /**
1446       * Get the text that needs to be saved in order to undo all revisions
1447       * between $undo and $undoafter. Revisions must belong to the same page,
1448       * must exist and must not be deleted
1449       * @param Revision $undo
1450       * @param Revision $undoafter Must be an earlier revision than $undo
1451       * @return string|bool String on success, false on failure
1452       * @deprecated since 1.21: use ContentHandler::getUndoContent() instead.
1453       */
1454  	public function getUndoText( Revision $undo, Revision $undoafter = null ) {
1455          ContentHandler::deprecated( __METHOD__, '1.21' );
1456  
1457          $this->loadLastEdit();
1458  
1459          if ( $this->mLastRevision ) {
1460              if ( is_null( $undoafter ) ) {
1461                  $undoafter = $undo->getPrevious();
1462              }
1463  
1464              $handler = $this->getContentHandler();
1465              $undone = $handler->getUndoContent( $this->mLastRevision, $undo, $undoafter );
1466  
1467              if ( !$undone ) {
1468                  return false;
1469              } else {
1470                  return ContentHandler::getContentText( $undone );
1471              }
1472          }
1473  
1474          return false;
1475      }
1476  
1477      /**
1478       * @param string|number|null|bool $sectionId Section identifier as a number or string
1479       * (e.g. 0, 1 or 'T-1'), null/false or an empty string for the whole page
1480       * or 'new' for a new section.
1481       * @param string $text New text of the section.
1482       * @param string $sectionTitle New section's subject, only if $section is "new".
1483       * @param string $edittime Revision timestamp or null to use the current revision.
1484       *
1485       * @throws MWException
1486       * @return string New complete article text, or null if error.
1487       *
1488       * @deprecated since 1.21, use replaceSectionAtRev() instead
1489       */
1490  	public function replaceSection( $sectionId, $text, $sectionTitle = '',
1491          $edittime = null
1492      ) {
1493          ContentHandler::deprecated( __METHOD__, '1.21' );
1494  
1495          //NOTE: keep condition in sync with condition in replaceSectionContent!
1496          if ( strval( $sectionId ) === '' ) {
1497              // Whole-page edit; let the whole text through
1498              return $text;
1499          }
1500  
1501          if ( !$this->supportsSections() ) {
1502              throw new MWException( "sections not supported for content model " .
1503                  $this->getContentHandler()->getModelID() );
1504          }
1505  
1506          // could even make section title, but that's not required.
1507          $sectionContent = ContentHandler::makeContent( $text, $this->getTitle() );
1508  
1509          $newContent = $this->replaceSectionContent( $sectionId, $sectionContent, $sectionTitle,
1510              $edittime );
1511  
1512          return ContentHandler::getContentText( $newContent );
1513      }
1514  
1515      /**
1516       * Returns true if this page's content model supports sections.
1517       *
1518       * @return bool
1519       *
1520       * @todo The skin should check this and not offer section functionality if
1521       *   sections are not supported.
1522       * @todo The EditPage should check this and not offer section functionality
1523       *   if sections are not supported.
1524       */
1525  	public function supportsSections() {
1526          return $this->getContentHandler()->supportsSections();
1527      }
1528  
1529      /**
1530       * @param string|number|null|bool $sectionId Section identifier as a number or string
1531       * (e.g. 0, 1 or 'T-1'), null/false or an empty string for the whole page
1532       * or 'new' for a new section.
1533       * @param Content $sectionContent New content of the section.
1534       * @param string $sectionTitle New section's subject, only if $section is "new".
1535       * @param string $edittime Revision timestamp or null to use the current revision.
1536       *
1537       * @throws MWException
1538       * @return Content New complete article content, or null if error.
1539       *
1540       * @since 1.21
1541       * @deprecated since 1.24, use replaceSectionAtRev instead
1542       */
1543  	public function replaceSectionContent( $sectionId, Content $sectionContent, $sectionTitle = '',
1544          $edittime = null ) {
1545          wfProfileIn( __METHOD__ );
1546  
1547          $baseRevId = null;
1548          if ( $edittime && $sectionId !== 'new' ) {
1549              $dbw = wfGetDB( DB_MASTER );
1550              $rev = Revision::loadFromTimestamp( $dbw, $this->mTitle, $edittime );
1551              if ( $rev ) {
1552                  $baseRevId = $rev->getId();
1553              }
1554          }
1555  
1556          wfProfileOut( __METHOD__ );
1557          return $this->replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId );
1558      }
1559  
1560      /**
1561       * @param string|number|null|bool $sectionId Section identifier as a number or string
1562       * (e.g. 0, 1 or 'T-1'), null/false or an empty string for the whole page
1563       * or 'new' for a new section.
1564       * @param Content $sectionContent New content of the section.
1565       * @param string $sectionTitle New section's subject, only if $section is "new".
1566       * @param int|null $baseRevId
1567       *
1568       * @throws MWException
1569       * @return Content New complete article content, or null if error.
1570       *
1571       * @since 1.24
1572       */
1573  	public function replaceSectionAtRev( $sectionId, Content $sectionContent,
1574          $sectionTitle = '', $baseRevId = null
1575      ) {
1576          wfProfileIn( __METHOD__ );
1577  
1578          if ( strval( $sectionId ) === '' ) {
1579              // Whole-page edit; let the whole text through
1580              $newContent = $sectionContent;
1581          } else {
1582              if ( !$this->supportsSections() ) {
1583                  wfProfileOut( __METHOD__ );
1584                  throw new MWException( "sections not supported for content model " .
1585                      $this->getContentHandler()->getModelID() );
1586              }
1587  
1588              // Bug 30711: always use current version when adding a new section
1589              if ( is_null( $baseRevId ) || $sectionId === 'new' ) {
1590                  $oldContent = $this->getContent();
1591              } else {
1592                  // TODO: try DB_SLAVE first
1593                  $dbw = wfGetDB( DB_MASTER );
1594                  $rev = Revision::loadFromId( $dbw, $baseRevId );
1595  
1596                  if ( !$rev ) {
1597                      wfDebug( __METHOD__ . " asked for bogus section (page: " .
1598                          $this->getId() . "; section: $sectionId)\n" );
1599                      wfProfileOut( __METHOD__ );
1600                      return null;
1601                  }
1602  
1603                  $oldContent = $rev->getContent();
1604              }
1605  
1606              if ( !$oldContent ) {
1607                  wfDebug( __METHOD__ . ": no page text\n" );
1608                  wfProfileOut( __METHOD__ );
1609                  return null;
1610              }
1611  
1612              $newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle );
1613          }
1614  
1615          wfProfileOut( __METHOD__ );
1616          return $newContent;
1617      }
1618  
1619      /**
1620       * Check flags and add EDIT_NEW or EDIT_UPDATE to them as needed.
1621       * @param int $flags
1622       * @return int Updated $flags
1623       */
1624  	public function checkFlags( $flags ) {
1625          if ( !( $flags & EDIT_NEW ) && !( $flags & EDIT_UPDATE ) ) {
1626              if ( $this->exists() ) {
1627                  $flags |= EDIT_UPDATE;
1628              } else {
1629                  $flags |= EDIT_NEW;
1630              }
1631          }
1632  
1633          return $flags;
1634      }
1635  
1636      /**
1637       * Change an existing article or create a new article. Updates RC and all necessary caches,
1638       * optionally via the deferred update array.
1639       *
1640       * @param string $text New text
1641       * @param string $summary Edit summary
1642       * @param int $flags Bitfield:
1643       *      EDIT_NEW
1644       *          Article is known or assumed to be non-existent, create a new one
1645       *      EDIT_UPDATE
1646       *          Article is known or assumed to be pre-existing, update it
1647       *      EDIT_MINOR
1648       *          Mark this edit minor, if the user is allowed to do so
1649       *      EDIT_SUPPRESS_RC
1650       *          Do not log the change in recentchanges
1651       *      EDIT_FORCE_BOT
1652       *          Mark the edit a "bot" edit regardless of user rights
1653       *      EDIT_DEFER_UPDATES
1654       *          Defer some of the updates until the end of index.php
1655       *      EDIT_AUTOSUMMARY
1656       *          Fill in blank summaries with generated text where possible
1657       *
1658       * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the
1659       * article will be detected. If EDIT_UPDATE is specified and the article
1660       * doesn't exist, the function will return an edit-gone-missing error. If
1661       * EDIT_NEW is specified and the article does exist, an edit-already-exists
1662       * error will be returned. These two conditions are also possible with
1663       * auto-detection due to MediaWiki's performance-optimised locking strategy.
1664       *
1665       * @param bool|int $baseRevId The revision ID this edit was based off, if any
1666       * @param User $user The user doing the edit
1667       *
1668       * @throws MWException
1669       * @return Status Possible errors:
1670       *   edit-hook-aborted: The ArticleSave hook aborted the edit but didn't
1671       *     set the fatal flag of $status
1672       *   edit-gone-missing: In update mode, but the article didn't exist.
1673       *   edit-conflict: In update mode, the article changed unexpectedly.
1674       *   edit-no-change: Warning that the text was the same as before.
1675       *   edit-already-exists: In creation mode, but the article already exists.
1676       *
1677       * Extensions may define additional errors.
1678       *
1679       * $return->value will contain an associative array with members as follows:
1680       *     new: Boolean indicating if the function attempted to create a new article.
1681       *     revision: The revision object for the inserted revision, or null.
1682       *
1683       * Compatibility note: this function previously returned a boolean value
1684       * indicating success/failure
1685       *
1686       * @deprecated since 1.21: use doEditContent() instead.
1687       */
1688  	public function doEdit( $text, $summary, $flags = 0, $baseRevId = false, $user = null ) {
1689          ContentHandler::deprecated( __METHOD__, '1.21' );
1690  
1691          $content = ContentHandler::makeContent( $text, $this->getTitle() );
1692  
1693          return $this->doEditContent( $content, $summary, $flags, $baseRevId, $user );
1694      }
1695  
1696      /**
1697       * Change an existing article or create a new article. Updates RC and all necessary caches,
1698       * optionally via the deferred update array.
1699       *
1700       * @param Content $content New content
1701       * @param string $summary Edit summary
1702       * @param int $flags Bitfield:
1703       *      EDIT_NEW
1704       *          Article is known or assumed to be non-existent, create a new one
1705       *      EDIT_UPDATE
1706       *          Article is known or assumed to be pre-existing, update it
1707       *      EDIT_MINOR
1708       *          Mark this edit minor, if the user is allowed to do so
1709       *      EDIT_SUPPRESS_RC
1710       *          Do not log the change in recentchanges
1711       *      EDIT_FORCE_BOT
1712       *          Mark the edit a "bot" edit regardless of user rights
1713       *      EDIT_DEFER_UPDATES
1714       *          Defer some of the updates until the end of index.php
1715       *      EDIT_AUTOSUMMARY
1716       *          Fill in blank summaries with generated text where possible
1717       *
1718       * If neither EDIT_NEW nor EDIT_UPDATE is specified, the status of the
1719       * article will be detected. If EDIT_UPDATE is specified and the article
1720       * doesn't exist, the function will return an edit-gone-missing error. If
1721       * EDIT_NEW is specified and the article does exist, an edit-already-exists
1722       * error will be returned. These two conditions are also possible with
1723       * auto-detection due to MediaWiki's performance-optimised locking strategy.
1724       *
1725       * @param bool|int $baseRevId The revision ID this edit was based off, if any
1726       * @param User $user The user doing the edit
1727       * @param string $serialisation_format Format for storing the content in the
1728       *   database.
1729       *
1730       * @throws MWException
1731       * @return Status Possible errors:
1732       *     edit-hook-aborted: The ArticleSave hook aborted the edit but didn't
1733       *       set the fatal flag of $status.
1734       *     edit-gone-missing: In update mode, but the article didn't exist.
1735       *     edit-conflict: In update mode, the article changed unexpectedly.
1736       *     edit-no-change: Warning that the text was the same as before.
1737       *     edit-already-exists: In creation mode, but the article already exists.
1738       *
1739       *  Extensions may define additional errors.
1740       *
1741       *  $return->value will contain an associative array with members as follows:
1742       *     new: Boolean indicating if the function attempted to create a new article.
1743       *     revision: The revision object for the inserted revision, or null.
1744       *
1745       * @since 1.21
1746       */
1747  	public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false,
1748          User $user = null, $serialisation_format = null
1749      ) {
1750          global $wgUser, $wgUseAutomaticEditSummaries, $wgUseRCPatrol, $wgUseNPPatrol;
1751  
1752          // Low-level sanity check
1753          if ( $this->mTitle->getText() === '' ) {
1754              throw new MWException( 'Something is trying to edit an article with an empty title' );
1755          }
1756  
1757          wfProfileIn( __METHOD__ );
1758  
1759          if ( !$content->getContentHandler()->canBeUsedOn( $this->getTitle() ) ) {
1760              wfProfileOut( __METHOD__ );
1761              return Status::newFatal( 'content-not-allowed-here',
1762                  ContentHandler::getLocalizedName( $content->getModel() ),
1763                  $this->getTitle()->getPrefixedText() );
1764          }
1765  
1766          $user = is_null( $user ) ? $wgUser : $user;
1767          $status = Status::newGood( array() );
1768  
1769          // Load the data from the master database if needed.
1770          // The caller may already loaded it from the master or even loaded it using
1771          // SELECT FOR UPDATE, so do not override that using clear().
1772          $this->loadPageData( 'fromdbmaster' );
1773  
1774          $flags = $this->checkFlags( $flags );
1775  
1776          // handle hook
1777          $hook_args = array( &$this, &$user, &$content, &$summary,
1778                              $flags & EDIT_MINOR, null, null, &$flags, &$status );
1779  
1780          if ( !wfRunHooks( 'PageContentSave', $hook_args )
1781              || !ContentHandler::runLegacyHooks( 'ArticleSave', $hook_args ) ) {
1782  
1783              wfDebug( __METHOD__ . ": ArticleSave or ArticleSaveContent hook aborted save!\n" );
1784  
1785              if ( $status->isOK() ) {
1786                  $status->fatal( 'edit-hook-aborted' );
1787              }
1788  
1789              wfProfileOut( __METHOD__ );
1790              return $status;
1791          }
1792  
1793          // Silently ignore EDIT_MINOR if not allowed
1794          $isminor = ( $flags & EDIT_MINOR ) && $user->isAllowed( 'minoredit' );
1795          $bot = $flags & EDIT_FORCE_BOT;
1796  
1797          $old_content = $this->getContent( Revision::RAW ); // current revision's content
1798  
1799          $oldsize = $old_content ? $old_content->getSize() : 0;
1800          $oldid = $this->getLatest();
1801          $oldIsRedirect = $this->isRedirect();
1802          $oldcountable = $this->isCountable();
1803  
1804          $handler = $content->getContentHandler();
1805  
1806          // Provide autosummaries if one is not provided and autosummaries are enabled.
1807          if ( $wgUseAutomaticEditSummaries && $flags & EDIT_AUTOSUMMARY && $summary == '' ) {
1808              if ( !$old_content ) {
1809                  $old_content = null;
1810              }
1811              $summary = $handler->getAutosummary( $old_content, $content, $flags );
1812          }
1813  
1814          $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialisation_format );
1815          $serialized = $editInfo->pst;
1816  
1817          /**
1818           * @var Content $content
1819           */
1820          $content = $editInfo->pstContent;
1821          $newsize = $content->getSize();
1822  
1823          $dbw = wfGetDB( DB_MASTER );
1824          $now = wfTimestampNow();
1825          $this->mTimestamp = $now;
1826  
1827          if ( $flags & EDIT_UPDATE ) {
1828              // Update article, but only if changed.
1829              $status->value['new'] = false;
1830  
1831              if ( !$oldid ) {
1832                  // Article gone missing
1833                  wfDebug( __METHOD__ . ": EDIT_UPDATE specified but article doesn't exist\n" );
1834                  $status->fatal( 'edit-gone-missing' );
1835  
1836                  wfProfileOut( __METHOD__ );
1837                  return $status;
1838              } elseif ( !$old_content ) {
1839                  // Sanity check for bug 37225
1840                  wfProfileOut( __METHOD__ );
1841                  throw new MWException( "Could not find text for current revision {$oldid}." );
1842              }
1843  
1844              $revision = new Revision( array(
1845                  'page'       => $this->getId(),
1846                  'title'      => $this->getTitle(), // for determining the default content model
1847                  'comment'    => $summary,
1848                  'minor_edit' => $isminor,
1849                  'text'       => $serialized,
1850                  'len'        => $newsize,
1851                  'parent_id'  => $oldid,
1852                  'user'       => $user->getId(),
1853                  'user_text'  => $user->getName(),
1854                  'timestamp'  => $now,
1855                  'content_model' => $content->getModel(),
1856                  'content_format' => $serialisation_format,
1857              ) ); // XXX: pass content object?!
1858  
1859              $changed = !$content->equals( $old_content );
1860  
1861              if ( $changed ) {
1862                  if ( !$content->isValid() ) {
1863                      wfProfileOut( __METHOD__ );
1864                      throw new MWException( "New content failed validity check!" );
1865                  }
1866  
1867                  $dbw->begin( __METHOD__ );
1868                  try {
1869  
1870                      $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user );
1871                      $status->merge( $prepStatus );
1872  
1873                      if ( !$status->isOK() ) {
1874                          $dbw->rollback( __METHOD__ );
1875  
1876                          wfProfileOut( __METHOD__ );
1877                          return $status;
1878                      }
1879                      $revisionId = $revision->insertOn( $dbw );
1880  
1881                      // Update page
1882                      //
1883                      // We check for conflicts by comparing $oldid with the current latest revision ID.
1884                      $ok = $this->updateRevisionOn( $dbw, $revision, $oldid, $oldIsRedirect );
1885  
1886                      if ( !$ok ) {
1887                          // Belated edit conflict! Run away!!
1888                          $status->fatal( 'edit-conflict' );
1889  
1890                          $dbw->rollback( __METHOD__ );
1891  
1892                          wfProfileOut( __METHOD__ );
1893                          return $status;
1894                      }
1895  
1896                      wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) );
1897                      // Update recentchanges
1898                      if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
1899                          // Mark as patrolled if the user can do so
1900                          $patrolled = $wgUseRCPatrol && !count(
1901                          $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
1902                          // Add RC row to the DB
1903                          $rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary,
1904                              $oldid, $this->getTimestamp(), $bot, '', $oldsize, $newsize,
1905                              $revisionId, $patrolled
1906                          );
1907  
1908                          // Log auto-patrolled edits
1909                          if ( $patrolled ) {
1910                              PatrolLog::record( $rc, true, $user );
1911                          }
1912                      }
1913                      $user->incEditCount();
1914                  } catch ( MWException $e ) {
1915                      $dbw->rollback( __METHOD__ );
1916                      // Question: Would it perhaps be better if this method turned all
1917                      // exceptions into $status's?
1918                      throw $e;
1919                  }
1920                  $dbw->commit( __METHOD__ );
1921              } else {
1922                  // Bug 32948: revision ID must be set to page {{REVISIONID}} and
1923                  // related variables correctly
1924                  $revision->setId( $this->getLatest() );
1925              }
1926  
1927              // Update links tables, site stats, etc.
1928              $this->doEditUpdates(
1929                  $revision,
1930                  $user,
1931                  array(
1932                      'changed' => $changed,
1933                      'oldcountable' => $oldcountable
1934                  )
1935              );
1936  
1937              if ( !$changed ) {
1938                  $status->warning( 'edit-no-change' );
1939                  $revision = null;
1940                  // Update page_touched, this is usually implicit in the page update
1941                  // Other cache updates are done in onArticleEdit()
1942                  $this->mTitle->invalidateCache();
1943              }
1944          } else {
1945              // Create new article
1946              $status->value['new'] = true;
1947  
1948              $dbw->begin( __METHOD__ );
1949              try {
1950  
1951                  $prepStatus = $content->prepareSave( $this, $flags, $baseRevId, $user );
1952                  $status->merge( $prepStatus );
1953  
1954                  if ( !$status->isOK() ) {
1955                      $dbw->rollback( __METHOD__ );
1956  
1957                      wfProfileOut( __METHOD__ );
1958                      return $status;
1959                  }
1960  
1961                  $status->merge( $prepStatus );
1962  
1963                  // Add the page record; stake our claim on this title!
1964                  // This will return false if the article already exists
1965                  $newid = $this->insertOn( $dbw );
1966  
1967                  if ( $newid === false ) {
1968                      $dbw->rollback( __METHOD__ );
1969                      $status->fatal( 'edit-already-exists' );
1970  
1971                      wfProfileOut( __METHOD__ );
1972                      return $status;
1973                  }
1974  
1975                  // Save the revision text...
1976                  $revision = new Revision( array(
1977                      'page'       => $newid,
1978                      'title'      => $this->getTitle(), // for determining the default content model
1979                      'comment'    => $summary,
1980                      'minor_edit' => $isminor,
1981                      'text'       => $serialized,
1982                      'len'        => $newsize,
1983                      'user'       => $user->getId(),
1984                      'user_text'  => $user->getName(),
1985                      'timestamp'  => $now,
1986                      'content_model' => $content->getModel(),
1987                      'content_format' => $serialisation_format,
1988                  ) );
1989                  $revisionId = $revision->insertOn( $dbw );
1990  
1991                  // Bug 37225: use accessor to get the text as Revision may trim it
1992                  $content = $revision->getContent(); // sanity; get normalized version
1993  
1994                  if ( $content ) {
1995                      $newsize = $content->getSize();
1996                  }
1997  
1998                  // Update the page record with revision data
1999                  $this->updateRevisionOn( $dbw, $revision, 0 );
2000  
2001                  wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) );
2002  
2003                  // Update recentchanges
2004                  if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
2005                      // Mark as patrolled if the user can do so
2006                      $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && !count(
2007                          $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) );
2008                      // Add RC row to the DB
2009                      $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot,
2010                          '', $newsize, $revisionId, $patrolled );
2011  
2012                      // Log auto-patrolled edits
2013                      if ( $patrolled ) {
2014                          PatrolLog::record( $rc, true, $user );
2015                      }
2016                  }
2017                  $user->incEditCount();
2018  
2019              } catch ( MWException $e ) {
2020                  $dbw->rollback( __METHOD__ );
2021                  throw $e;
2022              }
2023              $dbw->commit( __METHOD__ );
2024  
2025              // Update links, etc.
2026              $this->doEditUpdates( $revision, $user, array( 'created' => true ) );
2027  
2028              $hook_args = array( &$this, &$user, $content, $summary,
2029                                  $flags & EDIT_MINOR, null, null, &$flags, $revision );
2030  
2031              ContentHandler::runLegacyHooks( 'ArticleInsertComplete', $hook_args );
2032              wfRunHooks( 'PageContentInsertComplete', $hook_args );
2033          }
2034  
2035          // Do updates right now unless deferral was requested
2036          if ( !( $flags & EDIT_DEFER_UPDATES ) ) {
2037              DeferredUpdates::doUpdates();
2038          }
2039  
2040          // Return the new revision (or null) to the caller
2041          $status->value['revision'] = $revision;
2042  
2043          $hook_args = array( &$this, &$user, $content, $summary,
2044                              $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId );
2045  
2046          ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $hook_args );
2047          wfRunHooks( 'PageContentSaveComplete', $hook_args );
2048  
2049          // Promote user to any groups they meet the criteria for
2050          $dbw->onTransactionIdle( function () use ( $user ) {
2051              $user->addAutopromoteOnceGroups( 'onEdit' );
2052          } );
2053  
2054          wfProfileOut( __METHOD__ );
2055          return $status;
2056      }
2057  
2058      /**
2059       * Get parser options suitable for rendering the primary article wikitext
2060       *
2061       * @see ContentHandler::makeParserOptions
2062       *
2063       * @param IContextSource|User|string $context One of the following:
2064       *        - IContextSource: Use the User and the Language of the provided
2065       *          context
2066       *        - User: Use the provided User object and $wgLang for the language,
2067       *          so use an IContextSource object if possible.
2068       *        - 'canonical': Canonical options (anonymous user with default
2069       *          preferences and content language).
2070       * @return ParserOptions
2071       */
2072  	public function makeParserOptions( $context ) {
2073          $options = $this->getContentHandler()->makeParserOptions( $context );
2074  
2075          if ( $this->getTitle()->isConversionTable() ) {
2076              // @todo ConversionTable should become a separate content model, so
2077              // we don't need special cases like this one.
2078              $options->disableContentConversion();
2079          }
2080  
2081          return $options;
2082      }
2083  
2084      /**
2085       * Prepare text which is about to be saved.
2086       * Returns a stdclass with source, pst and output members
2087       *
2088       * @deprecated since 1.21: use prepareContentForEdit instead.
2089       * @return object
2090       */
2091  	public function prepareTextForEdit( $text, $revid = null, User $user = null ) {
2092          ContentHandler::deprecated( __METHOD__, '1.21' );
2093          $content = ContentHandler::makeContent( $text, $this->getTitle() );
2094          return $this->prepareContentForEdit( $content, $revid, $user );
2095      }
2096  
2097      /**
2098       * Prepare content which is about to be saved.
2099       * Returns a stdclass with source, pst and output members
2100       *
2101       * @param Content $content
2102       * @param int|null $revid
2103       * @param User|null $user
2104       * @param string|null $serialization_format
2105       *
2106       * @return bool|object
2107       *
2108       * @since 1.21
2109       */
2110  	public function prepareContentForEdit( Content $content, $revid = null, User $user = null,
2111          $serialization_format = null
2112      ) {
2113          global $wgContLang, $wgUser;
2114          $user = is_null( $user ) ? $wgUser : $user;
2115          //XXX: check $user->getId() here???
2116  
2117          // Use a sane default for $serialization_format, see bug 57026
2118          if ( $serialization_format === null ) {
2119              $serialization_format = $content->getContentHandler()->getDefaultFormat();
2120          }
2121  
2122          if ( $this->mPreparedEdit
2123              && $this->mPreparedEdit->newContent
2124              && $this->mPreparedEdit->newContent->equals( $content )
2125              && $this->mPreparedEdit->revid == $revid
2126              && $this->mPreparedEdit->format == $serialization_format
2127              // XXX: also check $user here?
2128          ) {
2129              // Already prepared
2130              return $this->mPreparedEdit;
2131          }
2132  
2133          $popts = ParserOptions::newFromUserAndLang( $user, $wgContLang );
2134          wfRunHooks( 'ArticlePrepareTextForEdit', array( $this, $popts ) );
2135  
2136          $edit = (object)array();
2137          $edit->revid = $revid;
2138          $edit->timestamp = wfTimestampNow();
2139  
2140          $edit->pstContent = $content ? $content->preSaveTransform( $this->mTitle, $user, $popts ) : null;
2141  
2142          $edit->format = $serialization_format;
2143          $edit->popts = $this->makeParserOptions( 'canonical' );
2144          $edit->output = $edit->pstContent
2145              ? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts )
2146              : null;
2147  
2148          $edit->newContent = $content;
2149          $edit->oldContent = $this->getContent( Revision::RAW );
2150  
2151          // NOTE: B/C for hooks! don't use these fields!
2152          $edit->newText = $edit->newContent ? ContentHandler::getContentText( $edit->newContent ) : '';
2153          $edit->oldText = $edit->oldContent ? ContentHandler::getContentText( $edit->oldContent ) : '';
2154          $edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialization_format ) : '';
2155  
2156          $this->mPreparedEdit = $edit;
2157          return $edit;
2158      }
2159  
2160      /**
2161       * Do standard deferred updates after page edit.
2162       * Update links tables, site stats, search index and message cache.
2163       * Purges pages that include this page if the text was changed here.
2164       * Every 100th edit, prune the recent changes table.
2165       *
2166       * @param Revision $revision
2167       * @param User $user User object that did the revision
2168       * @param array $options Array of options, following indexes are used:
2169       * - changed: boolean, whether the revision changed the content (default true)
2170       * - created: boolean, whether the revision created the page (default false)
2171       * - oldcountable: boolean or null (default null):
2172       *   - boolean: whether the page was counted as an article before that
2173       *     revision, only used in changed is true and created is false
2174       *   - null: don't change the article count
2175       */
2176  	public function doEditUpdates( Revision $revision, User $user, array $options = array() ) {
2177          global $wgEnableParserCache;
2178  
2179          wfProfileIn( __METHOD__ );
2180  
2181          $options += array( 'changed' => true, 'created' => false, 'oldcountable' => null );
2182          $content = $revision->getContent();
2183  
2184          // Parse the text
2185          // Be careful not to do pre-save transform twice: $text is usually
2186          // already pre-save transformed once.
2187          if ( !$this->mPreparedEdit || $this->mPreparedEdit->output->getFlag( 'vary-revision' ) ) {
2188              wfDebug( __METHOD__ . ": No prepared edit or vary-revision is set...\n" );
2189              $editInfo = $this->prepareContentForEdit( $content, $revision->getId(), $user );
2190          } else {
2191              wfDebug( __METHOD__ . ": No vary-revision, using prepared edit...\n" );
2192              $editInfo = $this->mPreparedEdit;
2193          }
2194  
2195          // Save it to the parser cache
2196          if ( $wgEnableParserCache ) {
2197              $parserCache = ParserCache::singleton();
2198              $parserCache->save(
2199                  $editInfo->output, $this, $editInfo->popts, $editInfo->timestamp, $editInfo->revid
2200              );
2201          }
2202  
2203          // Update the links tables and other secondary data
2204          if ( $content ) {
2205              $recursive = $options['changed']; // bug 50785
2206              $updates = $content->getSecondaryDataUpdates(
2207                  $this->getTitle(), null, $recursive, $editInfo->output );
2208              DataUpdate::runUpdates( $updates );
2209          }
2210  
2211          wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) );
2212  
2213          if ( wfRunHooks( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) {
2214              if ( 0 == mt_rand( 0, 99 ) ) {
2215                  // Flush old entries from the `recentchanges` table; we do this on
2216                  // random requests so as to avoid an increase in writes for no good reason
2217                  RecentChange::purgeExpiredChanges();
2218              }
2219          }
2220  
2221          if ( !$this->exists() ) {
2222              wfProfileOut( __METHOD__ );
2223              return;
2224          }
2225  
2226          $id = $this->getId();
2227          $title = $this->mTitle->getPrefixedDBkey();
2228          $shortTitle = $this->mTitle->getDBkey();
2229  
2230          if ( !$options['changed'] ) {
2231              $good = 0;
2232          } elseif ( $options['created'] ) {
2233              $good = (int)$this->isCountable( $editInfo );
2234          } elseif ( $options['oldcountable'] !== null ) {
2235              $good = (int)$this->isCountable( $editInfo ) - (int)$options['oldcountable'];
2236          } else {
2237              $good = 0;
2238          }
2239          $edits = $options['changed'] ? 1 : 0;
2240          $total = $options['created'] ? 1 : 0;
2241  
2242          DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, $edits, $good, $total ) );
2243          DeferredUpdates::addUpdate( new SearchUpdate( $id, $title, $content ) );
2244  
2245          // If this is another user's talk page, update newtalk.
2246          // Don't do this if $options['changed'] = false (null-edits) nor if
2247          // it's a minor edit and the user doesn't want notifications for those.
2248          if ( $options['changed']
2249              && $this->mTitle->getNamespace() == NS_USER_TALK
2250              && $shortTitle != $user->getTitleKey()
2251              && !( $revision->isMinor() && $user->isAllowed( 'nominornewtalk' ) )
2252          ) {
2253              $recipient = User::newFromName( $shortTitle, false );
2254              if ( !$recipient ) {
2255                  wfDebug( __METHOD__ . ": invalid username\n" );
2256              } else {
2257                  // Allow extensions to prevent user notification when a new message is added to their talk page
2258                  if ( wfRunHooks( 'ArticleEditUpdateNewTalk', array( &$this, $recipient ) ) ) {
2259                      if ( User::isIP( $shortTitle ) ) {
2260                          // An anonymous user
2261                          $recipient->setNewtalk( true, $revision );
2262                      } elseif ( $recipient->isLoggedIn() ) {
2263                          $recipient->setNewtalk( true, $revision );
2264                      } else {
2265                          wfDebug( __METHOD__ . ": don't need to notify a nonexistent user\n" );
2266                      }
2267                  }
2268              }
2269          }
2270  
2271          if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) {
2272              // XXX: could skip pseudo-messages like js/css here, based on content model.
2273              $msgtext = $content ? $content->getWikitextForTransclusion() : null;
2274              if ( $msgtext === false || $msgtext === null ) {
2275                  $msgtext = '';
2276              }
2277  
2278              MessageCache::singleton()->replace( $shortTitle, $msgtext );
2279          }
2280  
2281          if ( $options['created'] ) {
2282              self::onArticleCreate( $this->mTitle );
2283          } elseif ( $options['changed'] ) { // bug 50785
2284              self::onArticleEdit( $this->mTitle );
2285          }
2286  
2287          wfProfileOut( __METHOD__ );
2288      }
2289  
2290      /**
2291       * Edit an article without doing all that other stuff
2292       * The article must already exist; link tables etc
2293       * are not updated, caches are not flushed.
2294       *
2295       * @param string $text Text submitted
2296       * @param User $user The relevant user
2297       * @param string $comment Comment submitted
2298       * @param bool $minor Whereas it's a minor modification
2299       *
2300       * @deprecated since 1.21, use doEditContent() instead.
2301       */
2302  	public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) {
2303          ContentHandler::deprecated( __METHOD__, "1.21" );
2304  
2305          $content = ContentHandler::makeContent( $text, $this->getTitle() );
2306          $this->doQuickEditContent( $content, $user, $comment, $minor );
2307      }
2308  
2309      /**
2310       * Edit an article without doing all that other stuff
2311       * The article must already exist; link tables etc
2312       * are not updated, caches are not flushed.
2313       *
2314       * @param Content $content Content submitted
2315       * @param User $user The relevant user
2316       * @param string $comment Comment submitted
2317       * @param bool $minor Whereas it's a minor modification
2318       * @param string $serialisation_format Format for storing the content in the database
2319       */
2320  	public function doQuickEditContent( Content $content, User $user, $comment = '', $minor = false,
2321          $serialisation_format = null
2322      ) {
2323          wfProfileIn( __METHOD__ );
2324  
2325          $serialized = $content->serialize( $serialisation_format );
2326  
2327          $dbw = wfGetDB( DB_MASTER );
2328          $revision = new Revision( array(
2329              'title'      => $this->getTitle(), // for determining the default content model
2330              'page'       => $this->getId(),
2331              'user_text'  => $user->getName(),
2332              'user'       => $user->getId(),
2333              'text'       => $serialized,
2334              'length'     => $content->getSize(),
2335              'comment'    => $comment,
2336              'minor_edit' => $minor ? 1 : 0,
2337          ) ); // XXX: set the content object?
2338          $revision->insertOn( $dbw );
2339          $this->updateRevisionOn( $dbw, $revision );
2340  
2341          wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) );
2342  
2343          wfProfileOut( __METHOD__ );
2344      }
2345  
2346      /**
2347       * Update the article's restriction field, and leave a log entry.
2348       * This works for protection both existing and non-existing pages.
2349       *
2350       * @param array $limit Set of restriction keys
2351       * @param array $expiry Per restriction type expiration
2352       * @param int &$cascade Set to false if cascading protection isn't allowed.
2353       * @param string $reason
2354       * @param User $user The user updating the restrictions
2355       * @return Status
2356       */
2357  	public function doUpdateRestrictions( array $limit, array $expiry,
2358          &$cascade, $reason, User $user
2359      ) {
2360          global $wgCascadingRestrictionLevels, $wgContLang;
2361  
2362          if ( wfReadOnly() ) {
2363              return Status::newFatal( 'readonlytext', wfReadOnlyReason() );
2364          }
2365  
2366          $this->loadPageData( 'fromdbmaster' );
2367          $restrictionTypes = $this->mTitle->getRestrictionTypes();
2368          $id = $this->getId();
2369  
2370          if ( !$cascade ) {
2371              $cascade = false;
2372          }
2373  
2374          // Take this opportunity to purge out expired restrictions
2375          Title::purgeExpiredRestrictions();
2376  
2377          // @todo FIXME: Same limitations as described in ProtectionForm.php (line 37);
2378          // we expect a single selection, but the schema allows otherwise.
2379          $isProtected = false;
2380          $protect = false;
2381          $changed = false;
2382  
2383          $dbw = wfGetDB( DB_MASTER );
2384  
2385          foreach ( $restrictionTypes as $action ) {
2386              if ( !isset( $expiry[$action] ) ) {
2387                  $expiry[$action] = $dbw->getInfinity();
2388              }
2389              if ( !isset( $limit[$action] ) ) {
2390                  $limit[$action] = '';
2391              } elseif ( $limit[$action] != '' ) {
2392                  $protect = true;
2393              }
2394  
2395              // Get current restrictions on $action
2396              $current = implode( '', $this->mTitle->getRestrictions( $action ) );
2397              if ( $current != '' ) {
2398                  $isProtected = true;
2399              }
2400  
2401              if ( $limit[$action] != $current ) {
2402                  $changed = true;
2403              } elseif ( $limit[$action] != '' ) {
2404                  // Only check expiry change if the action is actually being
2405                  // protected, since expiry does nothing on an not-protected
2406                  // action.
2407                  if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2408                      $changed = true;
2409                  }
2410              }
2411          }
2412  
2413          if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2414              $changed = true;
2415          }
2416  
2417          // If nothing has changed, do nothing
2418          if ( !$changed ) {
2419              return Status::newGood();
2420          }
2421  
2422          if ( !$protect ) { // No protection at all means unprotection
2423              $revCommentMsg = 'unprotectedarticle';
2424              $logAction = 'unprotect';
2425          } elseif ( $isProtected ) {
2426              $revCommentMsg = 'modifiedarticleprotection';
2427              $logAction = 'modify';
2428          } else {
2429              $revCommentMsg = 'protectedarticle';
2430              $logAction = 'protect';
2431          }
2432  
2433          // Truncate for whole multibyte characters
2434          $reason = $wgContLang->truncate( $reason, 255 );
2435  
2436          $logRelationsValues = array();
2437          $logRelationsField = null;
2438  
2439          if ( $id ) { // Protection of existing page
2440              if ( !wfRunHooks( 'ArticleProtect', array( &$this, &$user, $limit, $reason ) ) ) {
2441                  return Status::newGood();
2442              }
2443  
2444              // Only certain restrictions can cascade...
2445              $editrestriction = isset( $limit['edit'] )
2446                  ? array( $limit['edit'] )
2447                  : $this->mTitle->getRestrictions( 'edit' );
2448              foreach ( array_keys( $editrestriction, 'sysop' ) as $key ) {
2449                  $editrestriction[$key] = 'editprotected'; // backwards compatibility
2450              }
2451              foreach ( array_keys( $editrestriction, 'autoconfirmed' ) as $key ) {
2452                  $editrestriction[$key] = 'editsemiprotected'; // backwards compatibility
2453              }
2454  
2455              $cascadingRestrictionLevels = $wgCascadingRestrictionLevels;
2456              foreach ( array_keys( $cascadingRestrictionLevels, 'sysop' ) as $key ) {
2457                  $cascadingRestrictionLevels[$key] = 'editprotected'; // backwards compatibility
2458              }
2459              foreach ( array_keys( $cascadingRestrictionLevels, 'autoconfirmed' ) as $key ) {
2460                  $cascadingRestrictionLevels[$key] = 'editsemiprotected'; // backwards compatibility
2461              }
2462  
2463              // The schema allows multiple restrictions
2464              if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2465                  $cascade = false;
2466              }
2467  
2468              // insert null revision to identify the page protection change as edit summary
2469              $latest = $this->getLatest();
2470              $nullRevision = $this->insertProtectNullRevision(
2471                  $revCommentMsg,
2472                  $limit,
2473                  $expiry,
2474                  $cascade,
2475                  $reason,
2476                  $user
2477              );
2478  
2479              if ( $nullRevision === null ) {
2480                  return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
2481              }
2482  
2483              $logRelationsField = 'pr_id';
2484  
2485              // Update restrictions table
2486              foreach ( $limit as $action => $restrictions ) {
2487                  $dbw->delete(
2488                      'page_restrictions',
2489                      array(
2490                          'pr_page' => $id,
2491                          'pr_type' => $action
2492                      ),
2493                      __METHOD__
2494                  );
2495                  if ( $restrictions != '' ) {
2496                      $dbw->insert(
2497                          'page_restrictions',
2498                          array(
2499                              'pr_id' => $dbw->nextSequenceValue( 'page_restrictions_pr_id_seq' ),
2500                              'pr_page' => $id,
2501                              'pr_type' => $action,
2502                              'pr_level' => $restrictions,
2503                              'pr_cascade' => ( $cascade && $action == 'edit' ) ? 1 : 0,
2504                              'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2505                          ),
2506                          __METHOD__
2507                      );
2508                      $logRelationsValues[] = $dbw->insertId();
2509                  }
2510              }
2511  
2512              // Clear out legacy restriction fields
2513              $dbw->update(
2514                  'page',
2515                  array( 'page_restrictions' => '' ),
2516                  array( 'page_id' => $id ),
2517                  __METHOD__
2518              );
2519  
2520              wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $nullRevision, $latest, $user ) );
2521              wfRunHooks( 'ArticleProtectComplete', array( &$this, &$user, $limit, $reason ) );
2522          } else { // Protection of non-existing page (also known as "title protection")
2523              // Cascade protection is meaningless in this case
2524              $cascade = false;
2525  
2526              if ( $limit['create'] != '' ) {
2527                  $dbw->replace( 'protected_titles',
2528                      array( array( 'pt_namespace', 'pt_title' ) ),
2529                      array(
2530                          'pt_namespace' => $this->mTitle->getNamespace(),
2531                          'pt_title' => $this->mTitle->getDBkey(),
2532                          'pt_create_perm' => $limit['create'],
2533                          'pt_timestamp' => $dbw->timestamp(),
2534                          'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
2535                          'pt_user' => $user->getId(),
2536                          'pt_reason' => $reason,
2537                      ), __METHOD__
2538                  );
2539              } else {
2540                  $dbw->delete( 'protected_titles',
2541                      array(
2542                          'pt_namespace' => $this->mTitle->getNamespace(),
2543                          'pt_title' => $this->mTitle->getDBkey()
2544                      ), __METHOD__
2545                  );
2546              }
2547          }
2548  
2549          $this->mTitle->flushRestrictions();
2550          InfoAction::invalidateCache( $this->mTitle );
2551  
2552          if ( $logAction == 'unprotect' ) {
2553              $params = array();
2554          } else {
2555              $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
2556              $params = array( $protectDescriptionLog, $cascade ? 'cascade' : '' );
2557          }
2558  
2559          // Update the protection log
2560          $log = new LogPage( 'protect' );
2561          $logId = $log->addEntry( $logAction, $this->mTitle, $reason, $params, $user );
2562          if ( $logRelationsField !== null && count( $logRelationsValues ) ) {
2563              $log->addRelations( $logRelationsField, $logRelationsValues, $logId );
2564          }
2565  
2566          return Status::newGood();
2567      }
2568  
2569      /**
2570       * Insert a new null revision for this page.
2571       *
2572       * @param string $revCommentMsg Comment message key for the revision
2573       * @param array $limit Set of restriction keys
2574       * @param array $expiry Per restriction type expiration
2575       * @param int $cascade Set to false if cascading protection isn't allowed.
2576       * @param string $reason
2577       * @param User|null $user
2578       * @return Revision|null Null on error
2579       */
2580  	public function insertProtectNullRevision( $revCommentMsg, array $limit,
2581          array $expiry, $cascade, $reason, $user = null
2582      ) {
2583          global $wgContLang;
2584          $dbw = wfGetDB( DB_MASTER );
2585  
2586          // Prepare a null revision to be added to the history
2587          $editComment = $wgContLang->ucfirst(
2588              wfMessage(
2589                  $revCommentMsg,
2590                  $this->mTitle->getPrefixedText()
2591              )->inContentLanguage()->text()
2592          );
2593          if ( $reason ) {
2594              $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
2595          }
2596          $protectDescription = $this->protectDescription( $limit, $expiry );
2597          if ( $protectDescription ) {
2598              $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2599              $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )
2600                  ->inContentLanguage()->text();
2601          }
2602          if ( $cascade ) {
2603              $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2604              $editComment .= wfMessage( 'brackets' )->params(
2605                  wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
2606              )->inContentLanguage()->text();
2607          }
2608  
2609          $nullRev = Revision::newNullRevision( $dbw, $this->getId(), $editComment, true, $user );
2610          if ( $nullRev ) {
2611              $nullRev->insertOn( $dbw );
2612  
2613              // Update page record and touch page
2614              $oldLatest = $nullRev->getParentId();
2615              $this->updateRevisionOn( $dbw, $nullRev, $oldLatest );
2616          }
2617  
2618          return $nullRev;
2619      }
2620  
2621      /**
2622       * @param string $expiry 14-char timestamp or "infinity", or false if the input was invalid
2623       * @return string
2624       */
2625  	protected function formatExpiry( $expiry ) {
2626          global $wgContLang;
2627          $dbr = wfGetDB( DB_SLAVE );
2628  
2629          $encodedExpiry = $dbr->encodeExpiry( $expiry );
2630          if ( $encodedExpiry != 'infinity' ) {
2631              return wfMessage(
2632                  'protect-expiring',
2633                  $wgContLang->timeanddate( $expiry, false, false ),
2634                  $wgContLang->date( $expiry, false, false ),
2635                  $wgContLang->time( $expiry, false, false )
2636              )->inContentLanguage()->text();
2637          } else {
2638              return wfMessage( 'protect-expiry-indefinite' )
2639                  ->inContentLanguage()->text();
2640          }
2641      }
2642  
2643      /**
2644       * Builds the description to serve as comment for the edit.
2645       *
2646       * @param array $limit Set of restriction keys
2647       * @param array $expiry Per restriction type expiration
2648       * @return string
2649       */
2650  	public function protectDescription( array $limit, array $expiry ) {
2651          $protectDescription = '';
2652  
2653          foreach ( array_filter( $limit ) as $action => $restrictions ) {
2654              # $action is one of $wgRestrictionTypes = array( 'create', 'edit', 'move', 'upload' ).
2655              # All possible message keys are listed here for easier grepping:
2656              # * restriction-create
2657              # * restriction-edit
2658              # * restriction-move
2659              # * restriction-upload
2660              $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
2661              # $restrictions is one of $wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' ),
2662              # with '' filtered out. All possible message keys are listed below:
2663              # * protect-level-autoconfirmed
2664              # * protect-level-sysop
2665              $restrictionsText = wfMessage( 'protect-level-' . $restrictions )->inContentLanguage()->text();
2666  
2667              $expiryText = $this->formatExpiry( $expiry[$action] );
2668  
2669              if ( $protectDescription !== '' ) {
2670                  $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
2671              }
2672              $protectDescription .= wfMessage( 'protect-summary-desc' )
2673                  ->params( $actionText, $restrictionsText, $expiryText )
2674                  ->inContentLanguage()->text();
2675          }
2676  
2677          return $protectDescription;
2678      }
2679  
2680      /**
2681       * Builds the description to serve as comment for the log entry.
2682       *
2683       * Some bots may parse IRC lines, which are generated from log entries which contain plain
2684       * protect description text. Keep them in old format to avoid breaking compatibility.
2685       * TODO: Fix protection log to store structured description and format it on-the-fly.
2686       *
2687       * @param array $limit Set of restriction keys
2688       * @param array $expiry Per restriction type expiration
2689       * @return string
2690       */
2691  	public function protectDescriptionLog( array $limit, array $expiry ) {
2692          global $wgContLang;
2693  
2694          $protectDescriptionLog = '';
2695  
2696          foreach ( array_filter( $limit ) as $action => $restrictions ) {
2697              $expiryText = $this->formatExpiry( $expiry[$action] );
2698              $protectDescriptionLog .= $wgContLang->getDirMark() . "[$action=$restrictions] ($expiryText)";
2699          }
2700  
2701          return trim( $protectDescriptionLog );
2702      }
2703  
2704      /**
2705       * Take an array of page restrictions and flatten it to a string
2706       * suitable for insertion into the page_restrictions field.
2707       *
2708       * @param string[] $limit
2709       *
2710       * @throws MWException
2711       * @return string
2712       */
2713  	protected static function flattenRestrictions( $limit ) {
2714          if ( !is_array( $limit ) ) {
2715              throw new MWException( 'WikiPage::flattenRestrictions given non-array restriction set' );
2716          }
2717  
2718          $bits = array();
2719          ksort( $limit );
2720  
2721          foreach ( array_filter( $limit ) as $action => $restrictions ) {
2722              $bits[] = "$action=$restrictions";
2723          }
2724  
2725          return implode( ':', $bits );
2726      }
2727  
2728      /**
2729       * Same as doDeleteArticleReal(), but returns a simple boolean. This is kept around for
2730       * backwards compatibility, if you care about error reporting you should use
2731       * doDeleteArticleReal() instead.
2732       *
2733       * Deletes the article with database consistency, writes logs, purges caches
2734       *
2735       * @param string $reason Delete reason for deletion log
2736       * @param bool $suppress Suppress all revisions and log the deletion in
2737       *        the suppression log instead of the deletion log
2738       * @param int $id Article ID
2739       * @param bool $commit Defaults to true, triggers transaction end
2740       * @param array &$error Array of errors to append to
2741       * @param User $user The deleting user
2742       * @return bool True if successful
2743       */
2744  	public function doDeleteArticle(
2745          $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null
2746      ) {
2747          $status = $this->doDeleteArticleReal( $reason, $suppress, $id, $commit, $error, $user );
2748          return $status->isGood();
2749      }
2750  
2751      /**
2752       * Back-end article deletion
2753       * Deletes the article with database consistency, writes logs, purges caches
2754       *
2755       * @since 1.19
2756       *
2757       * @param string $reason Delete reason for deletion log
2758       * @param bool $suppress Suppress all revisions and log the deletion in
2759       *   the suppression log instead of the deletion log
2760       * @param int $id Article ID
2761       * @param bool $commit Defaults to true, triggers transaction end
2762       * @param array &$error Array of errors to append to
2763       * @param User $user The deleting user
2764       * @return Status Status object; if successful, $status->value is the log_id of the
2765       *   deletion log entry. If the page couldn't be deleted because it wasn't
2766       *   found, $status is a non-fatal 'cannotdelete' error
2767       */
2768  	public function doDeleteArticleReal(
2769          $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null
2770      ) {
2771          global $wgUser, $wgContentHandlerUseDB;
2772  
2773          wfDebug( __METHOD__ . "\n" );
2774  
2775          $status = Status::newGood();
2776  
2777          if ( $this->mTitle->getDBkey() === '' ) {
2778              $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2779              return $status;
2780          }
2781  
2782          $user = is_null( $user ) ? $wgUser : $user;
2783          if ( !wfRunHooks( 'ArticleDelete', array( &$this, &$user, &$reason, &$error, &$status ) ) ) {
2784              if ( $status->isOK() ) {
2785                  // Hook aborted but didn't set a fatal status
2786                  $status->fatal( 'delete-hook-aborted' );
2787              }
2788              return $status;
2789          }
2790  
2791          $dbw = wfGetDB( DB_MASTER );
2792          $dbw->begin( __METHOD__ );
2793  
2794          if ( $id == 0 ) {
2795              $this->loadPageData( 'forupdate' );
2796              $id = $this->getID();
2797              if ( $id == 0 ) {
2798                  $dbw->rollback( __METHOD__ );
2799                  $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2800                  return $status;
2801              }
2802          }
2803  
2804          // we need to remember the old content so we can use it to generate all deletion updates.
2805          $content = $this->getContent( Revision::RAW );
2806  
2807          // Bitfields to further suppress the content
2808          if ( $suppress ) {
2809              $bitfield = 0;
2810              // This should be 15...
2811              $bitfield |= Revision::DELETED_TEXT;
2812              $bitfield |= Revision::DELETED_COMMENT;
2813              $bitfield |= Revision::DELETED_USER;
2814              $bitfield |= Revision::DELETED_RESTRICTED;
2815          } else {
2816              $bitfield = 'rev_deleted';
2817          }
2818  
2819          // For now, shunt the revision data into the archive table.
2820          // Text is *not* removed from the text table; bulk storage
2821          // is left intact to avoid breaking block-compression or
2822          // immutable storage schemes.
2823          //
2824          // For backwards compatibility, note that some older archive
2825          // table entries will have ar_text and ar_flags fields still.
2826          //
2827          // In the future, we may keep revisions and mark them with
2828          // the rev_deleted field, which is reserved for this purpose.
2829  
2830          $row = array(
2831              'ar_namespace'  => 'page_namespace',
2832              'ar_title'      => 'page_title',
2833              'ar_comment'    => 'rev_comment',
2834              'ar_user'       => 'rev_user',
2835              'ar_user_text'  => 'rev_user_text',
2836              'ar_timestamp'  => 'rev_timestamp',
2837              'ar_minor_edit' => 'rev_minor_edit',
2838              'ar_rev_id'     => 'rev_id',
2839              'ar_parent_id'  => 'rev_parent_id',
2840              'ar_text_id'    => 'rev_text_id',
2841              'ar_text'       => '\'\'', // Be explicit to appease
2842              'ar_flags'      => '\'\'', // MySQL's "strict mode"...
2843              'ar_len'        => 'rev_len',
2844              'ar_page_id'    => 'page_id',
2845              'ar_deleted'    => $bitfield,
2846              'ar_sha1'       => 'rev_sha1',
2847          );
2848  
2849          if ( $wgContentHandlerUseDB ) {
2850              $row['ar_content_model'] = 'rev_content_model';
2851              $row['ar_content_format'] = 'rev_content_format';
2852          }
2853  
2854          $dbw->insertSelect( 'archive', array( 'page', 'revision' ),
2855              $row,
2856              array(
2857                  'page_id' => $id,
2858                  'page_id = rev_page'
2859              ), __METHOD__
2860          );
2861  
2862          // Now that it's safely backed up, delete it
2863          $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__ );
2864          $ok = ( $dbw->affectedRows() > 0 ); // $id could be laggy
2865  
2866          if ( !$ok ) {
2867              $dbw->rollback( __METHOD__ );
2868              $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
2869              return $status;
2870          }
2871  
2872          if ( !$dbw->cascadingDeletes() ) {
2873              $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
2874          }
2875  
2876          // Clone the title, so we have the information we need when we log
2877          $logTitle = clone $this->mTitle;
2878  
2879          // Log the deletion, if the page was suppressed, log it at Oversight instead
2880          $logtype = $suppress ? 'suppress' : 'delete';
2881  
2882          $logEntry = new ManualLogEntry( $logtype, 'delete' );
2883          $logEntry->setPerformer( $user );
2884          $logEntry->setTarget( $logTitle );
2885          $logEntry->setComment( $reason );
2886          $logid = $logEntry->insert();
2887  
2888          $dbw->onTransactionPreCommitOrIdle( function () use ( $dbw, $logEntry, $logid ) {
2889              // Bug 56776: avoid deadlocks (especially from FileDeleteForm)
2890              $logEntry->publish( $logid );
2891          } );
2892  
2893          if ( $commit ) {
2894              $dbw->commit( __METHOD__ );
2895          }
2896  
2897          $this->doDeleteUpdates( $id, $content );
2898  
2899          wfRunHooks( 'ArticleDeleteComplete', array( &$this, &$user, $reason, $id, $content, $logEntry ) );
2900          $status->value = $logid;
2901          return $status;
2902      }
2903  
2904      /**
2905       * Do some database updates after deletion
2906       *
2907       * @param int $id The page_id value of the page being deleted
2908       * @param Content $content Optional page content to be used when determining
2909       *   the required updates. This may be needed because $this->getContent()
2910       *   may already return null when the page proper was deleted.
2911       */
2912  	public function doDeleteUpdates( $id, Content $content = null ) {
2913          // update site status
2914          DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) );
2915  
2916          // remove secondary indexes, etc
2917          $updates = $this->getDeletionUpdates( $content );
2918          DataUpdate::runUpdates( $updates );
2919  
2920          // Reparse any pages transcluding this page
2921          LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'templatelinks' );
2922  
2923          // Reparse any pages including this image
2924          if ( $this->mTitle->getNamespace() == NS_FILE ) {
2925              LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'imagelinks' );
2926          }
2927  
2928          // Clear caches
2929          WikiPage::onArticleDelete( $this->mTitle );
2930  
2931          // Reset this object and the Title object
2932          $this->loadFromRow( false, self::READ_LATEST );
2933  
2934          // Search engine
2935          DeferredUpdates::addUpdate( new SearchUpdate( $id, $this->mTitle ) );
2936      }
2937  
2938      /**
2939       * Roll back the most recent consecutive set of edits to a page
2940       * from the same user; fails if there are no eligible edits to
2941       * roll back to, e.g. user is the sole contributor. This function
2942       * performs permissions checks on $user, then calls commitRollback()
2943       * to do the dirty work
2944       *
2945       * @todo Separate the business/permission stuff out from backend code
2946       *
2947       * @param string $fromP Name of the user whose edits to rollback.
2948       * @param string $summary Custom summary. Set to default summary if empty.
2949       * @param string $token Rollback token.
2950       * @param bool $bot If true, mark all reverted edits as bot.
2951       *
2952       * @param array $resultDetails Array contains result-specific array of additional values
2953       *    'alreadyrolled' : 'current' (rev)
2954       *    success        : 'summary' (str), 'current' (rev), 'target' (rev)
2955       *
2956       * @param User $user The user performing the rollback
2957       * @return array Array of errors, each error formatted as
2958       *   array(messagekey, param1, param2, ...).
2959       * On success, the array is empty.  This array can also be passed to
2960       * OutputPage::showPermissionsErrorPage().
2961       */
2962  	public function doRollback(
2963          $fromP, $summary, $token, $bot, &$resultDetails, User $user
2964      ) {
2965          $resultDetails = null;
2966  
2967          // Check permissions
2968          $editErrors = $this->mTitle->getUserPermissionsErrors( 'edit', $user );
2969          $rollbackErrors = $this->mTitle->getUserPermissionsErrors( 'rollback', $user );
2970          $errors = array_merge( $editErrors, wfArrayDiff2( $rollbackErrors, $editErrors ) );
2971  
2972          if ( !$user->matchEditToken( $token, array( $this->mTitle->getPrefixedText(), $fromP ) ) ) {
2973              $errors[] = array( 'sessionfailure' );
2974          }
2975  
2976          if ( $user->pingLimiter( 'rollback' ) || $user->pingLimiter() ) {
2977              $errors[] = array( 'actionthrottledtext' );
2978          }
2979  
2980          // If there were errors, bail out now
2981          if ( !empty( $errors ) ) {
2982              return $errors;
2983          }
2984  
2985          return $this->commitRollback( $fromP, $summary, $bot, $resultDetails, $user );
2986      }
2987  
2988      /**
2989       * Backend implementation of doRollback(), please refer there for parameter
2990       * and return value documentation
2991       *
2992       * NOTE: This function does NOT check ANY permissions, it just commits the
2993       * rollback to the DB. Therefore, you should only call this function direct-
2994       * ly if you want to use custom permissions checks. If you don't, use
2995       * doRollback() instead.
2996       * @param string $fromP Name of the user whose edits to rollback.
2997       * @param string $summary Custom summary. Set to default summary if empty.
2998       * @param bool $bot If true, mark all reverted edits as bot.
2999       *
3000       * @param array $resultDetails Contains result-specific array of additional values
3001       * @param User $guser The user performing the rollback
3002       * @return array
3003       */
3004  	public function commitRollback( $fromP, $summary, $bot, &$resultDetails, User $guser ) {
3005          global $wgUseRCPatrol, $wgContLang;
3006  
3007          $dbw = wfGetDB( DB_MASTER );
3008  
3009          if ( wfReadOnly() ) {
3010              return array( array( 'readonlytext' ) );
3011          }
3012  
3013          // Get the last editor
3014          $current = $this->getRevision();
3015          if ( is_null( $current ) ) {
3016              // Something wrong... no page?
3017              return array( array( 'notanarticle' ) );
3018          }
3019  
3020          $from = str_replace( '_', ' ', $fromP );
3021          // User name given should match up with the top revision.
3022          // If the user was deleted then $from should be empty.
3023          if ( $from != $current->getUserText() ) {
3024              $resultDetails = array( 'current' => $current );
3025              return array( array( 'alreadyrolled',
3026                  htmlspecialchars( $this->mTitle->getPrefixedText() ),
3027                  htmlspecialchars( $fromP ),
3028                  htmlspecialchars( $current->getUserText() )
3029              ) );
3030          }
3031  
3032          // Get the last edit not by this guy...
3033          // Note: these may not be public values
3034          $user = intval( $current->getRawUser() );
3035          $user_text = $dbw->addQuotes( $current->getRawUserText() );
3036          $s = $dbw->selectRow( 'revision',
3037              array( 'rev_id', 'rev_timestamp', 'rev_deleted' ),
3038              array( 'rev_page' => $current->getPage(),
3039                  "rev_user != {$user} OR rev_user_text != {$user_text}"
3040              ), __METHOD__,
3041              array( 'USE INDEX' => 'page_timestamp',
3042                  'ORDER BY' => 'rev_timestamp DESC' )
3043              );
3044          if ( $s === false ) {
3045              // No one else ever edited this page
3046              return array( array( 'cantrollback' ) );
3047          } elseif ( $s->rev_deleted & Revision::DELETED_TEXT
3048              || $s->rev_deleted & Revision::DELETED_USER
3049          ) {
3050              // Only admins can see this text
3051              return array( array( 'notvisiblerev' ) );
3052          }
3053  
3054          // Set patrolling and bot flag on the edits, which gets rollbacked.
3055          // This is done before the rollback edit to have patrolling also on failure (bug 62157).
3056          $set = array();
3057          if ( $bot && $guser->isAllowed( 'markbotedits' ) ) {
3058              // Mark all reverted edits as bot
3059              $set['rc_bot'] = 1;
3060          }
3061  
3062          if ( $wgUseRCPatrol ) {
3063              // Mark all reverted edits as patrolled
3064              $set['rc_patrolled'] = 1;
3065          }
3066  
3067          if ( count( $set ) ) {
3068              $dbw->update( 'recentchanges', $set,
3069                  array( /* WHERE */
3070                      'rc_cur_id' => $current->getPage(),
3071                      'rc_user_text' => $current->getUserText(),
3072                      'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ),
3073                  ), __METHOD__
3074              );
3075          }
3076  
3077          // Generate the edit summary if necessary
3078          $target = Revision::newFromId( $s->rev_id );
3079          if ( empty( $summary ) ) {
3080              if ( $from == '' ) { // no public user name
3081                  $summary = wfMessage( 'revertpage-nouser' );
3082              } else {
3083                  $summary = wfMessage( 'revertpage' );
3084              }
3085          }
3086  
3087          // Allow the custom summary to use the same args as the default message
3088          $args = array(
3089              $target->getUserText(), $from, $s->rev_id,
3090              $wgContLang->timeanddate( wfTimestamp( TS_MW, $s->rev_timestamp ) ),
3091              $current->getId(), $wgContLang->timeanddate( $current->getTimestamp() )
3092          );
3093          if ( $summary instanceof Message ) {
3094              $summary = $summary->params( $args )->inContentLanguage()->text();
3095          } else {
3096              $summary = wfMsgReplaceArgs( $summary, $args );
3097          }
3098  
3099          // Trim spaces on user supplied text
3100          $summary = trim( $summary );
3101  
3102          // Truncate for whole multibyte characters.
3103          $summary = $wgContLang->truncate( $summary, 255 );
3104  
3105          // Save
3106          $flags = EDIT_UPDATE;
3107  
3108          if ( $guser->isAllowed( 'minoredit' ) ) {
3109              $flags |= EDIT_MINOR;
3110          }
3111  
3112          if ( $bot && ( $guser->isAllowedAny( 'markbotedits', 'bot' ) ) ) {
3113              $flags |= EDIT_FORCE_BOT;
3114          }
3115  
3116          // Actually store the edit
3117          $status = $this->doEditContent(
3118              $target->getContent(),
3119              $summary,
3120              $flags,
3121              $target->getId(),
3122              $guser
3123          );
3124  
3125          if ( !$status->isOK() ) {
3126              return $status->getErrorsArray();
3127          }
3128  
3129          // raise error, when the edit is an edit without a new version
3130          if ( empty( $status->value['revision'] ) ) {
3131              $resultDetails = array( 'current' => $current );
3132              return array( array( 'alreadyrolled',
3133                      htmlspecialchars( $this->mTitle->getPrefixedText() ),
3134                      htmlspecialchars( $fromP ),
3135                      htmlspecialchars( $current->getUserText() )
3136              ) );
3137          }
3138  
3139          $revId = $status->value['revision']->getId();
3140  
3141          wfRunHooks( 'ArticleRollbackComplete', array( $this, $guser, $target, $current ) );
3142  
3143          $resultDetails = array(
3144              'summary' => $summary,
3145              'current' => $current,
3146              'target' => $target,
3147              'newid' => $revId
3148          );
3149  
3150          return array();
3151      }
3152  
3153      /**
3154       * The onArticle*() functions are supposed to be a kind of hooks
3155       * which should be called whenever any of the specified actions
3156       * are done.
3157       *
3158       * This is a good place to put code to clear caches, for instance.
3159       *
3160       * This is called on page move and undelete, as well as edit
3161       *
3162       * @param Title $title
3163       */
3164  	public static function onArticleCreate( $title ) {
3165          // Update existence markers on article/talk tabs...
3166          if ( $title->isTalkPage() ) {
3167              $other = $title->getSubjectPage();
3168          } else {
3169              $other = $title->getTalkPage();
3170          }
3171  
3172          $other->invalidateCache();
3173          $other->purgeSquid();
3174  
3175          $title->touchLinks();
3176          $title->purgeSquid();
3177          $title->deleteTitleProtection();
3178      }
3179  
3180      /**
3181       * Clears caches when article is deleted
3182       *
3183       * @param Title $title
3184       */
3185  	public static function onArticleDelete( $title ) {
3186          // Update existence markers on article/talk tabs...
3187          if ( $title->isTalkPage() ) {
3188              $other = $title->getSubjectPage();
3189          } else {
3190              $other = $title->getTalkPage();
3191          }
3192  
3193          $other->invalidateCache();
3194          $other->purgeSquid();
3195  
3196          $title->touchLinks();
3197          $title->purgeSquid();
3198  
3199          // File cache
3200          HTMLFileCache::clearFileCache( $title );
3201          InfoAction::invalidateCache( $title );
3202  
3203          // Messages
3204          if ( $title->getNamespace() == NS_MEDIAWIKI ) {
3205              MessageCache::singleton()->replace( $title->getDBkey(), false );
3206          }
3207  
3208          // Images
3209          if ( $title->getNamespace() == NS_FILE ) {
3210              $update = new HTMLCacheUpdate( $title, 'imagelinks' );
3211              $update->doUpdate();
3212          }
3213  
3214          // User talk pages
3215          if ( $title->getNamespace() == NS_USER_TALK ) {
3216              $user = User::newFromName( $title->getText(), false );
3217              if ( $user ) {
3218                  $user->setNewtalk( false );
3219              }
3220          }
3221  
3222          // Image redirects
3223          RepoGroup::singleton()->getLocalRepo()->invalidateImageRedirect( $title );
3224      }
3225  
3226      /**
3227       * Purge caches on page update etc
3228       *
3229       * @param Title $title
3230       * @todo Verify that $title is always a Title object (and never false or
3231       *   null), add Title hint to parameter $title.
3232       */
3233  	public static function onArticleEdit( $title ) {
3234          // Invalidate caches of articles which include this page
3235          DeferredUpdates::addHTMLCacheUpdate( $title, 'templatelinks' );
3236  
3237          // Invalidate the caches of all pages which redirect here
3238          DeferredUpdates::addHTMLCacheUpdate( $title, 'redirect' );
3239  
3240          // Purge squid for this page only
3241          $title->purgeSquid();
3242  
3243          // Clear file cache for this page only
3244          HTMLFileCache::clearFileCache( $title );
3245          InfoAction::invalidateCache( $title );
3246      }
3247  
3248      /**#@-*/
3249  
3250      /**
3251       * Returns a list of categories this page is a member of.
3252       * Results will include hidden categories
3253       *
3254       * @return TitleArray
3255       */
3256  	public function getCategories() {
3257          $id = $this->getId();
3258          if ( $id == 0 ) {
3259              return TitleArray::newFromResult( new FakeResultWrapper( array() ) );
3260          }
3261  
3262          $dbr = wfGetDB( DB_SLAVE );
3263          $res = $dbr->select( 'categorylinks',
3264              array( 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ),
3265              // Have to do that since DatabaseBase::fieldNamesWithAlias treats numeric indexes
3266              // as not being aliases, and NS_CATEGORY is numeric
3267              array( 'cl_from' => $id ),
3268              __METHOD__ );
3269  
3270          return TitleArray::newFromResult( $res );
3271      }
3272  
3273      /**
3274       * Returns a list of hidden categories this page is a member of.
3275       * Uses the page_props and categorylinks tables.
3276       *
3277       * @return array Array of Title objects
3278       */
3279  	public function getHiddenCategories() {
3280          $result = array();
3281          $id = $this->getId();
3282  
3283          if ( $id == 0 ) {
3284              return array();
3285          }
3286  
3287          $dbr = wfGetDB( DB_SLAVE );
3288          $res = $dbr->select( array( 'categorylinks', 'page_props', 'page' ),
3289              array( 'cl_to' ),
3290              array( 'cl_from' => $id, 'pp_page=page_id', 'pp_propname' => 'hiddencat',
3291                  'page_namespace' => NS_CATEGORY, 'page_title=cl_to' ),
3292              __METHOD__ );
3293  
3294          if ( $res !== false ) {
3295              foreach ( $res as $row ) {
3296                  $result[] = Title::makeTitle( NS_CATEGORY, $row->cl_to );
3297              }
3298          }
3299  
3300          return $result;
3301      }
3302  
3303      /**
3304       * Return an applicable autosummary if one exists for the given edit.
3305       * @param string|null $oldtext The previous text of the page.
3306       * @param string|null $newtext The submitted text of the page.
3307       * @param int $flags Bitmask: a bitmask of flags submitted for the edit.
3308       * @return string An appropriate autosummary, or an empty string.
3309       *
3310       * @deprecated since 1.21, use ContentHandler::getAutosummary() instead
3311       */
3312  	public static function getAutosummary( $oldtext, $newtext, $flags ) {
3313          // NOTE: stub for backwards-compatibility. assumes the given text is
3314          // wikitext. will break horribly if it isn't.
3315  
3316          ContentHandler::deprecated( __METHOD__, '1.21' );
3317  
3318          $handler = ContentHandler::getForModelID( CONTENT_MODEL_WIKITEXT );
3319          $oldContent = is_null( $oldtext ) ? null : $handler->unserializeContent( $oldtext );
3320          $newContent = is_null( $newtext ) ? null : $handler->unserializeContent( $newtext );
3321  
3322          return $handler->getAutosummary( $oldContent, $newContent, $flags );
3323      }
3324  
3325      /**
3326       * Auto-generates a deletion reason
3327       *
3328       * @param bool &$hasHistory Whether the page has a history
3329       * @return string|bool String containing deletion reason or empty string, or boolean false
3330       *    if no revision occurred
3331       */
3332  	public function getAutoDeleteReason( &$hasHistory ) {
3333          return $this->getContentHandler()->getAutoDeleteReason( $this->getTitle(), $hasHistory );
3334      }
3335  
3336      /**
3337       * Update all the appropriate counts in the category table, given that
3338       * we've added the categories $added and deleted the categories $deleted.
3339       *
3340       * @param array $added The names of categories that were added
3341       * @param array $deleted The names of categories that were deleted
3342       */
3343  	public function updateCategoryCounts( array $added, array $deleted ) {
3344          $that = $this;
3345          $method = __METHOD__;
3346          $dbw = wfGetDB( DB_MASTER );
3347  
3348          // Do this at the end of the commit to reduce lock wait timeouts
3349          $dbw->onTransactionPreCommitOrIdle(
3350              function () use ( $dbw, $that, $method, $added, $deleted ) {
3351                  $ns = $that->getTitle()->getNamespace();
3352  
3353                  $addFields = array( 'cat_pages = cat_pages + 1' );
3354                  $removeFields = array( 'cat_pages = cat_pages - 1' );
3355                  if ( $ns == NS_CATEGORY ) {
3356                      $addFields[] = 'cat_subcats = cat_subcats + 1';
3357                      $removeFields[] = 'cat_subcats = cat_subcats - 1';
3358                  } elseif ( $ns == NS_FILE ) {
3359                      $addFields[] = 'cat_files = cat_files + 1';
3360                      $removeFields[] = 'cat_files = cat_files - 1';
3361                  }
3362  
3363                  if ( count( $added ) ) {
3364                      $insertRows = array();
3365                      foreach ( $added as $cat ) {
3366                          $insertRows[] = array(
3367                              'cat_title'   => $cat,
3368                              'cat_pages'   => 1,
3369                              'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0,
3370                              'cat_files'   => ( $ns == NS_FILE ) ? 1 : 0,
3371                          );
3372                      }
3373                      $dbw->upsert(
3374                          'category',
3375                          $insertRows,
3376                          array( 'cat_title' ),
3377                          $addFields,
3378                          $method
3379                      );
3380                  }
3381  
3382                  if ( count( $deleted ) ) {
3383                      $dbw->update(
3384                          'category',
3385                          $removeFields,
3386                          array( 'cat_title' => $deleted ),
3387                          $method
3388                      );
3389                  }
3390  
3391                  foreach ( $added as $catName ) {
3392                      $cat = Category::newFromName( $catName );
3393                      wfRunHooks( 'CategoryAfterPageAdded', array( $cat, $that ) );
3394                  }
3395  
3396                  foreach ( $deleted as $catName ) {
3397                      $cat = Category::newFromName( $catName );
3398                      wfRunHooks( 'CategoryAfterPageRemoved', array( $cat, $that ) );
3399                  }
3400              }
3401          );
3402      }
3403  
3404      /**
3405       * Updates cascading protections
3406       *
3407       * @param ParserOutput $parserOutput ParserOutput object for the current version
3408       */
3409  	public function doCascadeProtectionUpdates( ParserOutput $parserOutput ) {
3410          if ( wfReadOnly() || !$this->mTitle->areRestrictionsCascading() ) {
3411              return;
3412          }
3413  
3414          // templatelinks or imagelinks tables may have become out of sync,
3415          // especially if using variable-based transclusions.
3416          // For paranoia, check if things have changed and if
3417          // so apply updates to the database. This will ensure
3418          // that cascaded protections apply as soon as the changes
3419          // are visible.
3420  
3421          // Get templates from templatelinks and images from imagelinks
3422          $id = $this->getId();
3423  
3424          $dbLinks = array();
3425  
3426          $dbr = wfGetDB( DB_SLAVE );
3427          $res = $dbr->select( array( 'templatelinks' ),
3428              array( 'tl_namespace', 'tl_title' ),
3429              array( 'tl_from' => $id ),
3430              __METHOD__
3431          );
3432  
3433          foreach ( $res as $row ) {
3434              $dbLinks["{$row->tl_namespace}:{$row->tl_title}"] = true;
3435          }
3436  
3437          $dbr = wfGetDB( DB_SLAVE );
3438          $res = $dbr->select( array( 'imagelinks' ),
3439              array( 'il_to' ),
3440              array( 'il_from' => $id ),
3441              __METHOD__
3442          );
3443  
3444          foreach ( $res as $row ) {
3445              $dbLinks[NS_FILE . ":{$row->il_to}"] = true;
3446          }
3447  
3448          // Get templates and images from parser output.
3449          $poLinks = array();
3450          foreach ( $parserOutput->getTemplates() as $ns => $templates ) {
3451              foreach ( $templates as $dbk => $id ) {
3452                  $poLinks["$ns:$dbk"] = true;
3453              }
3454          }
3455          foreach ( $parserOutput->getImages() as $dbk => $id ) {
3456              $poLinks[NS_FILE . ":$dbk"] = true;
3457          }
3458  
3459          // Get the diff
3460          $links_diff = array_diff_key( $poLinks, $dbLinks );
3461  
3462          if ( count( $links_diff ) > 0 ) {
3463              // Whee, link updates time.
3464              // Note: we are only interested in links here. We don't need to get
3465              // other DataUpdate items from the parser output.
3466              $u = new LinksUpdate( $this->mTitle, $parserOutput, false );
3467              $u->doUpdate();
3468          }
3469      }
3470  
3471      /**
3472       * Return a list of templates used by this article.
3473       * Uses the templatelinks table
3474       *
3475       * @deprecated since 1.19; use Title::getTemplateLinksFrom()
3476       * @return array Array of Title objects
3477       */
3478  	public function getUsedTemplates() {
3479          return $this->mTitle->getTemplateLinksFrom();
3480      }
3481  
3482      /**
3483       * This function is called right before saving the wikitext,
3484       * so we can do things like signatures and links-in-context.
3485       *
3486       * @deprecated since 1.19; use Parser::preSaveTransform() instead
3487       * @param string $text Article contents
3488       * @param User $user User doing the edit
3489       * @param ParserOptions $popts Parser options, default options for
3490       *   the user loaded if null given
3491       * @return string Article contents with altered wikitext markup (signatures
3492       *     converted, {{subst:}}, templates, etc.)
3493       */
3494  	public function preSaveTransform( $text, User $user = null, ParserOptions $popts = null ) {
3495          global $wgParser, $wgUser;
3496  
3497          wfDeprecated( __METHOD__, '1.19' );
3498  
3499          $user = is_null( $user ) ? $wgUser : $user;
3500  
3501          if ( $popts === null ) {
3502              $popts = ParserOptions::newFromUser( $user );
3503          }
3504  
3505          return $wgParser->preSaveTransform( $text, $this->mTitle, $user, $popts );
3506      }
3507  
3508      /**
3509       * Update the article's restriction field, and leave a log entry.
3510       *
3511       * @deprecated since 1.19
3512       * @param array $limit Set of restriction keys
3513       * @param string $reason
3514       * @param int &$cascade Set to false if cascading protection isn't allowed.
3515       * @param array $expiry Per restriction type expiration
3516       * @param User $user The user updating the restrictions
3517       * @return bool True on success
3518       */
3519  	public function updateRestrictions(
3520          $limit = array(), $reason = '', &$cascade = 0, $expiry = array(), User $user = null
3521      ) {
3522          global $wgUser;
3523  
3524          $user = is_null( $user ) ? $wgUser : $user;
3525  
3526          return $this->doUpdateRestrictions( $limit, $expiry, $cascade, $reason, $user )->isOK();
3527      }
3528  
3529      /**
3530       * Returns a list of updates to be performed when this page is deleted. The
3531       * updates should remove any information about this page from secondary data
3532       * stores such as links tables.
3533       *
3534       * @param Content|null $content Optional Content object for determining the
3535       *   necessary updates.
3536       * @return array An array of DataUpdates objects
3537       */
3538  	public function getDeletionUpdates( Content $content = null ) {
3539          if ( !$content ) {
3540              // load content object, which may be used to determine the necessary updates
3541              // XXX: the content may not be needed to determine the updates, then this would be overhead.
3542              $content = $this->getContent( Revision::RAW );
3543          }
3544  
3545          if ( !$content ) {
3546              $updates = array();
3547          } else {
3548              $updates = $content->getDeletionUpdates( $this );
3549          }
3550  
3551          wfRunHooks( 'WikiPageDeletionUpdates', array( $this, $content, &$updates ) );
3552          return $updates;
3553      }
3554  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1