Merge "Deleting a page and then immediately create-protecting it caused a PHP Fatal...
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 10 Dec 2013 20:50:37 +0000 (20:50 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 10 Dec 2013 20:50:37 +0000 (20:50 +0000)
1  2 
includes/WikiPage.php

diff --combined includes/WikiPage.php
@@@ -23,8 -23,7 +23,8 @@@
  /**
   * Abstract class for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
   */
 -interface Page {}
 +interface Page {
 +}
  
  /**
   * Class representing a MediaWiki article and history.
@@@ -48,11 -47,9 +48,11 @@@ class WikiPage implements Page, IDBAcce
        public $mDataLoaded = false;         // !< Boolean
        public $mIsRedirect = false;         // !< Boolean
        public $mLatest = false;             // !< Integer (false means "not loaded")
 -      public $mPreparedEdit = false;       // !< Array
        /**@}}*/
  
 +      /** @var stdclass Map of cache fields (text, parser output, ect) for a proposed/new edit */
 +      protected $mPreparedEdit = false;
 +
        /**
         * @var int
         */
                $this->mTimestamp = '';
                $this->mIsRedirect = false;
                $this->mLatest = false;
 +              // Bug 57026: do not clear mPreparedEdit since prepareTextForEdit() already checks
 +              // the requested rev ID and content against the cached one for equality. For most
 +              // content types, the output should not change during the lifetime of this cache.
 +              // Clearing it can cause extra parses on edit for no reason.
 +      }
 +
 +      /**
 +       * Clear the mPreparedEdit cache field, as may be needed by mutable content types
 +       * @return void
 +       * @since 1.23
 +       */
 +      public function clearPreparedEdit() {
                $this->mPreparedEdit = false;
        }
  
                $db = wfGetDB( DB_SLAVE );
                $revSelectFields = Revision::selectFields();
  
 +              $row = null;
                while ( $continue ) {
                        $row = $db->selectRow(
                                array( 'page', 'revision' ),
  
        /**
         * Get the last N authors
 -       * @param $num Integer: number of revisions to get
 -       * @param string $revLatest the latest rev_id, selected from the master (optional)
 +       * @param int $num Number of revisions to get
 +       * @param int|string $revLatest the latest rev_id, selected from the master (optional)
         * @return array Array of authors, duplicates not removed
         */
        public function getLastNAuthors( $num, $revLatest = 0 ) {
         * The parser cache will be used if possible.
         *
         * @since 1.19
 -       * @param $parserOptions ParserOptions to use for the parse operation
 -       * @param $oldid Revision ID to get the text from, passing null or 0 will
 +       * @param ParserOptions $parserOptions ParserOptions to use for the parse operation
 +       * @param null|int $oldid Revision ID to get the text from, passing null or 0 will
         *               get the current revision (default value)
         *
         * @return ParserOutput or false if the revision was not found
  
        /**
         * Do standard deferred updates after page view
 -       * @param $user User The relevant user
 +       * @param User $user The relevant user
 +       * @param int $oldid The revision id being viewed. If not given or 0, latest revision is assumed.
         */
 -      public function doViewUpdates( User $user ) {
 +      public function doViewUpdates( User $user, $oldid = 0 ) {
                global $wgDisableCounters;
                if ( wfReadOnly() ) {
                        return;
                }
  
                // Update newtalk / watchlist notification status
 -              $user->clearNotification( $this->mTitle );
 +              $user->clearNotification( $this->mTitle, $oldid );
        }
  
        /**
        }
  
        /**
 -       * Returns true iff this page's content model supports sections.
 +       * Returns true if this page's content model supports sections.
         *
         * @return boolean whether sections are supported.
         *
         * edit-already-exists error will be returned. These two conditions are also possible with
         * auto-detection due to MediaWiki's performance-optimised locking strategy.
         *
 -       * @param bool|\the $baseRevId the revision ID this edit was based off, if any
 +       * @param bool|int $baseRevId the revision ID this edit was based off, if any
         * @param $user User the user doing the edit
         * @param $serialisation_format String: format for storing the content in the database
         *
         * @since 1.21
         */
        public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false,
 -                                                                 User $user = null, $serialisation_format = null ) {
 +              User $user = null, $serialisation_format = null
 +      ) {
                global $wgUser, $wgUseAutomaticEditSummaries, $wgUseRCPatrol, $wgUseNPPatrol;
  
                // Low-level sanity check
  
                $editInfo = $this->prepareContentForEdit( $content, null, $user, $serialisation_format );
                $serialized = $editInfo->pst;
 +
 +              /**
 +               * @var Content $content
 +               */
                $content = $editInfo->pstContent;
                $newsize = $content->getSize();
  
         * Prepare content which is about to be saved.
         * Returns a stdclass with source, pst and output members
         *
 -       * @param \Content $content
 -       * @param null $revid
 -       * @param null|\User $user
 -       * @param null $serialization_format
 +       * @param Content $content
 +       * @param int|null $revid
 +       * @param User|null $user
 +       * @param string|null $serialization_format
         *
         * @return bool|object
         *
         * @since 1.21
         */
 -      public function prepareContentForEdit( Content $content, $revid = null, User $user = null, $serialization_format = null ) {
 +      public function prepareContentForEdit( Content $content, $revid = null, User $user = null,
 +              $serialization_format = null
 +      ) {
                global $wgContLang, $wgUser;
                $user = is_null( $user ) ? $wgUser : $user;
                //XXX: check $user->getId() here???
                ContentHandler::deprecated( __METHOD__, "1.21" );
  
                $content = ContentHandler::makeContent( $text, $this->getTitle() );
 -              return $this->doQuickEditContent( $content, $user, $comment, $minor );
 +              $this->doQuickEditContent( $content, $user, $comment, $minor );
        }
  
        /**
         * The article must already exist; link tables etc
         * are not updated, caches are not flushed.
         *
 -       * @param $content Content: content submitted
 -       * @param $user User The relevant user
 +       * @param Content $content Content submitted
 +       * @param User $user The relevant user
         * @param string $comment comment submitted
 -       * @param $serialisation_format String: format for storing the content in the database
 -       * @param $minor Boolean: whereas it's a minor modification
 +       * @param string $serialisation_format Format for storing the content in the database
 +       * @param bool $minor Whereas it's a minor modification
         */
 -      public function doQuickEditContent( Content $content, User $user, $comment = '', $minor = 0, $serialisation_format = null ) {
 +      public function doQuickEditContent( Content $content, User $user, $comment = '', $minor = false,
 +              $serialisation_format = null
 +      ) {
                wfProfileIn( __METHOD__ );
  
                $serialized = $content->serialize( $serialisation_format );
         * This works for protection both existing and non-existing pages.
         *
         * @param array $limit set of restriction keys
 -       * @param $reason String
 -       * @param &$cascade Integer. Set to false if cascading protection isn't allowed.
         * @param array $expiry per restriction type expiration
 -       * @param $user User The user updating the restrictions
 +       * @param int &$cascade Set to false if cascading protection isn't allowed.
 +       * @param string $reason
 +       * @param User $user The user updating the restrictions
         * @return Status
         */
        public function doUpdateRestrictions( array $limit, array $expiry, &$cascade, $reason, User $user ) {
 -              global $wgContLang, $wgCascadingRestrictionLevels;
 +              global $wgCascadingRestrictionLevels, $wgContLang;
  
                if ( wfReadOnly() ) {
                        return Status::newFatal( 'readonlytext', wfReadOnlyReason() );
                }
  
+               $this->loadPageData( 'fromdbmaster' );
                $restrictionTypes = $this->mTitle->getRestrictionTypes();
                $id = $this->getId();
  
                if ( !$cascade ) {
                        $logAction = 'protect';
                }
  
 -              $encodedExpiry = array();
 -              $protectDescription = '';
 -              # Some bots may parse IRC lines, which are generated from log entries which contain plain
 -              # protect description text. Keep them in old format to avoid breaking compatibility.
 -              # TODO: Fix protection log to store structured description and format it on-the-fly.
 -              $protectDescriptionLog = '';
 -              foreach ( $limit as $action => $restrictions ) {
 -                      $encodedExpiry[$action] = $dbw->encodeExpiry( $expiry[$action] );
 -                      if ( $restrictions != '' ) {
 -                              $protectDescriptionLog .= $wgContLang->getDirMark() . "[$action=$restrictions] (";
 -                              # $action is one of $wgRestrictionTypes = array( 'create', 'edit', 'move', 'upload' ).
 -                              # All possible message keys are listed here for easier grepping:
 -                              # * restriction-create
 -                              # * restriction-edit
 -                              # * restriction-move
 -                              # * restriction-upload
 -                              $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
 -                              # $restrictions is one of $wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' ),
 -                              # with '' filtered out. All possible message keys are listed below:
 -                              # * protect-level-autoconfirmed
 -                              # * protect-level-sysop
 -                              $restrictionsText = wfMessage( 'protect-level-' . $restrictions )->inContentLanguage()->text();
 -                              if ( $encodedExpiry[$action] != 'infinity' ) {
 -                                      $expiryText = wfMessage(
 -                                              'protect-expiring',
 -                                              $wgContLang->timeanddate( $expiry[$action], false, false ),
 -                                              $wgContLang->date( $expiry[$action], false, false ),
 -                                              $wgContLang->time( $expiry[$action], false, false )
 -                                      )->inContentLanguage()->text();
 -                              } else {
 -                                      $expiryText = wfMessage( 'protect-expiry-indefinite' )
 -                                              ->inContentLanguage()->text();
 -                              }
 -
 -                              if ( $protectDescription !== '' ) {
 -                                      $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
 -                              }
 -                              $protectDescription .= wfMessage( 'protect-summary-desc' )
 -                                      ->params( $actionText, $restrictionsText, $expiryText )
 -                                      ->inContentLanguage()->text();
 -                              $protectDescriptionLog .= $expiryText . ') ';
 -                      }
 -              }
 -              $protectDescriptionLog = trim( $protectDescriptionLog );
 +              // Truncate for whole multibyte characters
 +              $reason = $wgContLang->truncate( $reason, 255 );
  
                if ( $id ) { // Protection of existing page
                        if ( !wfRunHooks( 'ArticleProtect', array( &$this, &$user, $limit, $reason ) ) ) {
                                $cascade = false;
                        }
  
 -                      // Prepare a null revision to be added to the history
 -                      $editComment = $wgContLang->ucfirst(
 -                              wfMessage(
 -                                      $revCommentMsg,
 -                                      $this->mTitle->getPrefixedText()
 -                              )->inContentLanguage()->text()
 -                      );
 -                      if ( $reason ) {
 -                              $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
 -                      }
 -                      if ( $protectDescription ) {
 -                              $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
 -                              $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )->inContentLanguage()->text();
 -                      }
 -                      if ( $cascade ) {
 -                              $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
 -                              $editComment .= wfMessage( 'brackets' )->params(
 -                                      wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
 -                              )->inContentLanguage()->text();
 -                      }
 -
 -                      $nullRevision = Revision::newNullRevision( $dbw, $id, $editComment, true );
 +                      // insert null revision to identify the page protection change as edit summary
 +                      $latest = $this->getLatest();
 +                      $nullRevision = $this->insertProtectNullRevision( $revCommentMsg, $limit, $expiry, $cascade, $reason );
                        if ( $nullRevision === null ) {
                                return Status::newFatal( 'no-null-revision', $this->mTitle->getPrefixedText() );
                        }
                                                        'pr_type' => $action,
                                                        'pr_level' => $restrictions,
                                                        'pr_cascade' => ( $cascade && $action == 'edit' ) ? 1 : 0,
 -                                                      'pr_expiry' => $encodedExpiry[$action]
 +                                                      'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
                                                ),
                                                __METHOD__
                                        );
                                }
                        }
  
 -                      // Insert a null revision
 -                      $nullRevId = $nullRevision->insertOn( $dbw );
 -
 -                      $latest = $this->getLatest();
 -                      // Update page record
 -                      $dbw->update( 'page',
 -                              array( /* SET */
 -                                      'page_touched' => $dbw->timestamp(),
 -                                      'page_restrictions' => '',
 -                                      'page_latest' => $nullRevId
 -                              ), array( /* WHERE */
 -                                      'page_id' => $id
 -                              ), __METHOD__
 +                      // Clear out legacy restriction fields
 +                      $dbw->update(
 +                              'page',
 +                              array( 'page_restrictions' => '' ),
 +                              array( 'page_id' => $id ),
 +                              __METHOD__
                        );
  
                        wfRunHooks( 'NewRevisionFromEditComplete', array( $this, $nullRevision, $latest, $user ) );
                                                'pt_namespace' => $this->mTitle->getNamespace(),
                                                'pt_title' => $this->mTitle->getDBkey(),
                                                'pt_create_perm' => $limit['create'],
 -                                              'pt_timestamp' => $dbw->encodeExpiry( wfTimestampNow() ),
 -                                              'pt_expiry' => $encodedExpiry['create'],
 +                                              'pt_timestamp' => $dbw->timestamp(),
 +                                              'pt_expiry' => $dbw->encodeExpiry( $expiry['create'] ),
                                                'pt_user' => $user->getId(),
                                                'pt_reason' => $reason,
                                        ), __METHOD__
                InfoAction::invalidateCache( $this->mTitle );
  
                if ( $logAction == 'unprotect' ) {
 -                      $logParams = array();
 +                      $params = array();
                } else {
 -                      $logParams = array( $protectDescriptionLog, $cascade ? 'cascade' : '' );
 +                      $protectDescriptionLog = $this->protectDescriptionLog( $limit, $expiry );
 +                      $params = array( $protectDescriptionLog, $cascade ? 'cascade' : '' );
                }
  
                // Update the protection log
                $log = new LogPage( 'protect' );
 -              $log->addEntry( $logAction, $this->mTitle, trim( $reason ), $logParams, $user );
 +              $log->addEntry( $logAction, $this->mTitle, $reason, $params, $user );
  
                return Status::newGood();
        }
  
 +      /**
 +       * Insert a new null revision for this page.
 +       *
 +       * @param string $revCommentMsg comment message key for the revision
 +       * @param array $limit set of restriction keys
 +       * @param array $expiry per restriction type expiration
 +       * @param int $cascade Set to false if cascading protection isn't allowed.
 +       * @param string $reason
 +       * @return Revision|null on error
 +       */
 +      public function insertProtectNullRevision( $revCommentMsg, array $limit, array $expiry, $cascade, $reason ) {
 +              global $wgContLang;
 +              $dbw = wfGetDB( DB_MASTER );
 +
 +              // Prepare a null revision to be added to the history
 +              $editComment = $wgContLang->ucfirst(
 +                      wfMessage(
 +                              $revCommentMsg,
 +                              $this->mTitle->getPrefixedText()
 +                      )->inContentLanguage()->text()
 +              );
 +              if ( $reason ) {
 +                      $editComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . $reason;
 +              }
 +              $protectDescription = $this->protectDescription( $limit, $expiry );
 +              if ( $protectDescription ) {
 +                      $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
 +                      $editComment .= wfMessage( 'parentheses' )->params( $protectDescription )->inContentLanguage()->text();
 +              }
 +              if ( $cascade ) {
 +                      $editComment .= wfMessage( 'word-separator' )->inContentLanguage()->text();
 +                      $editComment .= wfMessage( 'brackets' )->params(
 +                              wfMessage( 'protect-summary-cascade' )->inContentLanguage()->text()
 +                      )->inContentLanguage()->text();
 +              }
 +
 +              $nullRev = Revision::newNullRevision( $dbw, $this->getId(), $editComment, true );
 +              if ( $nullRev ) {
 +                      $nullRev->insertOn( $dbw );
 +
 +                      // Update page record and touch page
 +                      $oldLatest = $nullRev->getParentId();
 +                      $this->updateRevisionOn( $dbw, $nullRev, $oldLatest );
 +              }
 +
 +              return $nullRev;
 +      }
 +
 +      /**
 +       * @param string $expiry 14-char timestamp or "infinity", or false if the input was invalid
 +       * @return string
 +       */
 +      protected function formatExpiry( $expiry ) {
 +              global $wgContLang;
 +              $dbr = wfGetDB( DB_SLAVE );
 +
 +              $encodedExpiry = $dbr->encodeExpiry( $expiry );
 +              if ( $encodedExpiry != 'infinity' ) {
 +                      return wfMessage(
 +                              'protect-expiring',
 +                              $wgContLang->timeanddate( $expiry, false, false ),
 +                              $wgContLang->date( $expiry, false, false ),
 +                              $wgContLang->time( $expiry, false, false )
 +                      )->inContentLanguage()->text();
 +              } else {
 +                      return wfMessage( 'protect-expiry-indefinite' )
 +                              ->inContentLanguage()->text();
 +              }
 +      }
 +
 +      /**
 +       * Builds the description to serve as comment for the edit.
 +       *
 +       * @param array $limit set of restriction keys
 +       * @param array $expiry per restriction type expiration
 +       * @return string
 +       */
 +      public function protectDescription( array $limit, array $expiry ) {
 +              $protectDescription = '';
 +
 +              foreach ( array_filter( $limit ) as $action => $restrictions ) {
 +                      # $action is one of $wgRestrictionTypes = array( 'create', 'edit', 'move', 'upload' ).
 +                      # All possible message keys are listed here for easier grepping:
 +                      # * restriction-create
 +                      # * restriction-edit
 +                      # * restriction-move
 +                      # * restriction-upload
 +                      $actionText = wfMessage( 'restriction-' . $action )->inContentLanguage()->text();
 +                      # $restrictions is one of $wgRestrictionLevels = array( '', 'autoconfirmed', 'sysop' ),
 +                      # with '' filtered out. All possible message keys are listed below:
 +                      # * protect-level-autoconfirmed
 +                      # * protect-level-sysop
 +                      $restrictionsText = wfMessage( 'protect-level-' . $restrictions )->inContentLanguage()->text();
 +
 +                      $expiryText = $this->formatExpiry( $expiry[$action] );
 +
 +                      if ( $protectDescription !== '' ) {
 +                              $protectDescription .= wfMessage( 'word-separator' )->inContentLanguage()->text();
 +                      }
 +                      $protectDescription .= wfMessage( 'protect-summary-desc' )
 +                              ->params( $actionText, $restrictionsText, $expiryText )
 +                              ->inContentLanguage()->text();
 +              }
 +
 +              return $protectDescription;
 +      }
 +
 +      /**
 +       * Builds the description to serve as comment for the log entry.
 +       *
 +       * Some bots may parse IRC lines, which are generated from log entries which contain plain
 +       * protect description text. Keep them in old format to avoid breaking compatibility.
 +       * TODO: Fix protection log to store structured description and format it on-the-fly.
 +       *
 +       * @param array $limit set of restriction keys
 +       * @param array $expiry per restriction type expiration
 +       * @return string
 +       */
 +      public function protectDescriptionLog( array $limit, array $expiry ) {
 +              global $wgContLang;
 +
 +              $protectDescriptionLog = '';
 +
 +              foreach ( array_filter( $limit ) as $action => $restrictions ) {
 +                      $expiryText = $this->formatExpiry( $expiry[$action] );
 +                      $protectDescriptionLog .= $wgContLang->getDirMark() . "[$action=$restrictions] ($expiryText)";
 +              }
 +
 +              return trim( $protectDescriptionLog );
 +      }
 +
        /**
         * Take an array of page restrictions and flatten it to a string
         * suitable for insertion into the page_restrictions field.
                $bits = array();
                ksort( $limit );
  
 -              foreach ( $limit as $action => $restrictions ) {
 -                      if ( $restrictions != '' ) {
 -                              $bits[] = "$action=$restrictions";
 -                      }
 +              foreach ( array_filter( $limit ) as $action => $restrictions ) {
 +                      $bits[] = "$action=$restrictions";
                }
  
                return implode( ':', $bits );
                        return $status;
                }
  
 +              if ( !$dbw->cascadingDeletes() ) {
 +                      $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
 +              }
 +
                $this->doDeleteUpdates( $id, $content );
  
                // Log the deletion, if the page was suppressed, log it at Oversight instead
                $updates = $this->getDeletionUpdates( $content );
                DataUpdate::runUpdates( $updates );
  
 +              // Reparse any pages transcluding this page
 +              LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'templatelinks' );
 +
                // Clear caches
                WikiPage::onArticleDelete( $this->mTitle );
  
  
        /**#@-*/
  
 +      /**
 +       * Returns a list of categories this page is a member of.
 +       * Results will include hidden categories
 +       *
 +       * @return TitleArray
 +       */
 +      public function getCategories() {
 +              $id = $this->getId();
 +              if ( $id == 0 ) {
 +                      return TitleArray::newFromResult( new FakeResultWrapper( array() ) );
 +              }
 +
 +              $dbr = wfGetDB( DB_SLAVE );
 +              $res = $dbr->select( 'categorylinks',
 +                      array( 'cl_to AS page_title, ' . NS_CATEGORY . ' AS page_namespace' ),
 +                      // Have to do that since DatabaseBase::fieldNamesWithAlias treats numeric indexes
 +                      // as not being aliases, and NS_CATEGORY is numeric
 +                      array( 'cl_from' => $id ),
 +                      __METHOD__ );
 +
 +              return TitleArray::newFromResult( $res );
 +      }
 +
        /**
         * Returns a list of hidden categories this page is a member of.
         * Uses the page_props and categorylinks tables.
        public function viewUpdates() {
                wfDeprecated( __METHOD__, '1.18' );
                global $wgUser;
 -              return $this->doViewUpdates( $wgUser );
 +              $this->doViewUpdates( $wgUser );
        }
  
        /**
@@@ -3474,7 -3356,7 +3474,7 @@@ class PoolWorkArticleView extends PoolC
        /**
         * Constructor
         *
 -       * @param $page Page
 +       * @param $page Page|WikiPage
         * @param $revid Integer: ID of the revision being parsed
         * @param $useParserCache Boolean: whether to use the parser cache
         * @param $parserOptions parserOptions to use for the parse operation
                        return false;
                }
  
 +              // Reduce effects of race conditions for slow parses (bug 46014)
 +              $cacheTime = wfTimestampNow();
 +
                $time = - microtime( true );
                $this->parserOutput = $content->getParserOutput( $this->page->getTitle(), $this->revid, $this->parserOptions );
                $time += microtime( true );
                }
  
                if ( $this->cacheable && $this->parserOutput->isCacheable() ) {
 -                      ParserCache::singleton()->save( $this->parserOutput, $this->page, $this->parserOptions );
 +                      ParserCache::singleton()->save(
 +                              $this->parserOutput, $this->page, $this->parserOptions, $cacheTime );
                }
  
                // Make sure file cache is not used on uncacheable content.