Merge "Generalizing LinksUpdate to allow extensions to add arbitrary update handlers."
authorAaron Schulz <aschulz@wikimedia.org>
Mon, 14 May 2012 22:20:04 +0000 (22:20 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Mon, 14 May 2012 22:20:04 +0000 (22:20 +0000)
1  2 
includes/AutoLoader.php
includes/LinksUpdate.php
includes/WikiPage.php
includes/api/ApiPurge.php

diff --combined includes/AutoLoader.php
@@@ -56,7 -56,6 +56,7 @@@ $wgAutoloadLocalClasses = array
        'DeferredUpdates' => 'includes/DeferredUpdates.php',
        'DeprecatedGlobal' => 'includes/DeprecatedGlobal.php',
        'DerivativeRequest' => 'includes/WebRequest.php',
 +      'DeviceDetection' => 'includes/DeviceDetection.php',
        'DiffHistoryBlob' => 'includes/HistoryBlob.php',
  
        'DoubleReplacer' => 'includes/StringUtils.php',
        'Linker' => 'includes/Linker.php',
        'LinkFilter' => 'includes/LinkFilter.php',
        'LinksUpdate' => 'includes/LinksUpdate.php',
+       'LinksDeletionUpdate' => 'includes/LinksUpdate.php',
        'LocalisationCache' => 'includes/LocalisationCache.php',
        'LocalisationCache_BulkLoad' => 'includes/LocalisationCache.php',
        'MagicWord' => 'includes/MagicWord.php',
        'RevisionList' => 'includes/RevisionList.php',
        'RSSFeed' => 'includes/Feed.php',
        'Sanitizer' => 'includes/Sanitizer.php',
+       'DataUpdate' => 'includes/DataUpdate.php',
+       'SqlDataUpdate' => 'includes/SqlDataUpdate.php',
        'ScopedPHPTimeout' => 'includes/ScopedPHPTimeout.php',
        'SiteConfiguration' => 'includes/SiteConfiguration.php',
        'SiteStats' => 'includes/SiteStats.php',
        'FileBackendStoreShardDirIterator' => 'includes/filerepo/backend/FileBackendStore.php',
        'FileBackendStoreShardFileIterator' => 'includes/filerepo/backend/FileBackendStore.php',
        'FileBackendMultiWrite' => 'includes/filerepo/backend/FileBackendMultiWrite.php',
 +      'FileBackendStoreOpHandle' => 'includes/filerepo/backend/FileBackendStore.php',
        'FSFileBackend' => 'includes/filerepo/backend/FSFileBackend.php',
        'FSFileBackendList' => 'includes/filerepo/backend/FSFileBackend.php',
        'FSFileBackendDirList' => 'includes/filerepo/backend/FSFileBackend.php',
        'FSFileBackendFileList' => 'includes/filerepo/backend/FSFileBackend.php',
 +      'FSFileOpHandle' => 'includes/filerepo/backend/FSFileBackend.php',
        'SwiftFileBackend' => 'includes/filerepo/backend/SwiftFileBackend.php',
        'SwiftFileBackendList' => 'includes/filerepo/backend/SwiftFileBackend.php',
        'SwiftFileBackendDirList' => 'includes/filerepo/backend/SwiftFileBackend.php',
        'SwiftFileBackendFileList' => 'includes/filerepo/backend/SwiftFileBackend.php',
 +      'SwiftFileOpHandle' => 'includes/filerepo/backend/SwiftFileBackend.php',
        'FileJournal' => 'includes/filerepo/backend/filejournal/FileJournal.php',
        'DBFileJournal' => 'includes/filerepo/backend/filejournal/DBFileJournal.php',
        'NullFileJournal' => 'includes/filerepo/backend/filejournal/FileJournal.php',
        'MySqlLockManager'=> 'includes/filerepo/backend/lockmanager/DBLockManager.php',
        'NullLockManager' => 'includes/filerepo/backend/lockmanager/LockManager.php',
        'FileOp' => 'includes/filerepo/backend/FileOp.php',
 +      'FileOpBatch' => 'includes/filerepo/backend/FileOpBatch.php',
        'StoreFileOp' => 'includes/filerepo/backend/FileOp.php',
        'CopyFileOp' => 'includes/filerepo/backend/FileOp.php',
        'MoveFileOp' => 'includes/filerepo/backend/FileOp.php',
diff --combined includes/LinksUpdate.php
@@@ -1,6 -1,6 +1,6 @@@
  <?php
  /**
 - * See docs/deferred.txt
 + * Updater for link tracking tables after a page edit.
   *
   * This program is free software; you can redistribute it and/or modify
   * it under the terms of the GNU General Public License as published by
   * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
   * http://www.gnu.org/copyleft/gpl.html
   *
 + * @file
 + */
 +
 +/**
 + * See docs/deferred.txt
 + *
   * @todo document (e.g. one-sentence top-level class description).
   */
- class LinksUpdate {
+ class LinksUpdate extends SqlDataUpdate {
  
-       /**@{{
-        * @private
-        */
-       var $mId,            //!< Page ID of the article linked from
+       // @todo: make members protected, but make sure extensions don't break
+       public $mId,         //!< Page ID of the article linked from
                $mTitle,         //!< Title object of the article linked from
                $mParserOutput,  //!< Parser output
                $mLinks,         //!< Map of title strings to IDs for the links in the document
@@@ -43,7 -36,6 +42,6 @@@
                $mDb,            //!< Database connection reference
                $mOptions,       //!< SELECT options to be used (array)
                $mRecursive;     //!< Whether to queue jobs for recursive updates
-       /**@}}*/
  
        /**
         * Constructor
         * @param $recursive Boolean: queue jobs for recursive updates?
         */
        function __construct( $title, $parserOutput, $recursive = true ) {
-               global $wgAntiLockFlags;
+               parent::__construct( );
  
-               if ( $wgAntiLockFlags & ALF_NO_LINK_LOCK ) {
-                       $this->mOptions = array();
-               } else {
-                       $this->mOptions = array( 'FOR UPDATE' );
+               if ( !( $title instanceof Title ) ) {
+                       throw new MWException( "The calling convention to LinksUpdate::LinksUpdate() has changed. " .
+                               "Please see Article::editUpdates() for an invocation example.\n" );
                }
-               $this->mDb = wfGetDB( DB_MASTER );
  
-               if ( !is_object( $title ) ) {
+               if ( !( $parserOutput instanceof ParserOutput ) ) {
                        throw new MWException( "The calling convention to LinksUpdate::__construct() has changed. " .
                                "Please see WikiPage::doEditUpdates() for an invocation example.\n" );
                }
                $this->mTitle = $title;
                $this->mId = $title->getArticleID();
  
                wfProfileOut( __METHOD__ );
        }
  
-       /**
-        * Invalidate the cache of a list of pages from a single namespace
-        *
-        * @param $namespace Integer
-        * @param $dbkeys Array
-        */
-       function invalidatePages( $namespace, $dbkeys ) {
-               if ( !count( $dbkeys ) ) {
-                       return;
-               }
-               /**
-                * Determine which pages need to be updated
-                * This is necessary to prevent the job queue from smashing the DB with
-                * large numbers of concurrent invalidations of the same page
-                */
-               $now = $this->mDb->timestamp();
-               $ids = array();
-               $res = $this->mDb->select( 'page', array( 'page_id' ),
-                       array(
-                               'page_namespace' => $namespace,
-                               'page_title IN (' . $this->mDb->makeList( $dbkeys ) . ')',
-                               'page_touched < ' . $this->mDb->addQuotes( $now )
-                       ), __METHOD__
-               );
-               foreach ( $res as $row ) {
-                       $ids[] = $row->page_id;
-               }
-               if ( !count( $ids ) ) {
-                       return;
-               }
-               /**
-                * Do the update
-                * We still need the page_touched condition, in case the row has changed since
-                * the non-locking select above.
-                */
-               $this->mDb->update( 'page', array( 'page_touched' => $now ),
-                       array(
-                               'page_id IN (' . $this->mDb->makeList( $ids ) . ')',
-                               'page_touched < ' . $this->mDb->addQuotes( $now )
-                       ), __METHOD__
-               );
-       }
        /**
         * @param $cats
         */
                }
        }
  }
 -}
+ /**
+  * Update object handling the cleanup of links tables after a page was deleted.
+  **/
+ class LinksDeletionUpdate extends SqlDataUpdate {
+       protected $mPage;     //!< WikiPage the wikipage that was deleted
+       /**
+        * Constructor
+        *
+        * @param $title Title of the page we're updating
+        * @param $parserOutput ParserOutput: output from a full parse of this page
+        * @param $recursive Boolean: queue jobs for recursive updates?
+        */
+       function __construct( WikiPage $page ) {
+               parent::__construct( );
+               $this->mPage = $page;
+       }
+       /**
+        * Do some database updates after deletion
+        */
+       public function doUpdate() {
+               $title = $this->mPage->getTitle();
+               $id = $this->mPage->getId();
+               # Delete restrictions for it
+               $this->mDb->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ );
+               # Fix category table counts
+               $cats = array();
+               $res = $this->mDb->select( 'categorylinks', 'cl_to', array( 'cl_from' => $id ), __METHOD__ );
+               foreach ( $res as $row ) {
+                       $cats [] = $row->cl_to;
+               }
+               $this->mPage->updateCategoryCounts( array(), $cats );
+               # If using cascading deletes, we can skip some explicit deletes
+               if ( !$this->mDb->cascadingDeletes() ) {
+                       $this->mDb->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
+                       # Delete outgoing links
+                       $this->mDb->delete( 'pagelinks', array( 'pl_from' => $id ), __METHOD__ );
+                       $this->mDb->delete( 'imagelinks', array( 'il_from' => $id ), __METHOD__ );
+                       $this->mDb->delete( 'categorylinks', array( 'cl_from' => $id ), __METHOD__ );
+                       $this->mDb->delete( 'templatelinks', array( 'tl_from' => $id ), __METHOD__ );
+                       $this->mDb->delete( 'externallinks', array( 'el_from' => $id ), __METHOD__ );
+                       $this->mDb->delete( 'langlinks', array( 'll_from' => $id ), __METHOD__ );
+                       $this->mDb->delete( 'iwlinks', array( 'iwl_from' => $id ), __METHOD__ );
+                       $this->mDb->delete( 'redirect', array( 'rd_from' => $id ), __METHOD__ );
+                       $this->mDb->delete( 'page_props', array( 'pp_page' => $id ), __METHOD__ );
+               }
+               # If using cleanup triggers, we can skip some manual deletes
+               if ( !$this->mDb->cleanupTriggers() ) {
+                       # Clean up recentchanges entries...
+                       $this->mDb->delete( 'recentchanges',
+                               array( 'rc_type != ' . RC_LOG,
+                                       'rc_namespace' => $title->getNamespace(),
+                                       'rc_title' => $title->getDBkey() ),
+                               __METHOD__ );
+                       $this->mDb->delete( 'recentchanges',
+                               array( 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ),
+                               __METHOD__ );
+               }
+       }
++}
diff --combined includes/WikiPage.php
@@@ -1,25 -1,4 +1,25 @@@
  <?php
 +/**
 + * Base representation for a MediaWiki page.
 + *
 + * This program is free software; you can redistribute it and/or modify
 + * it under the terms of the GNU General Public License as published by
 + * the Free Software Foundation; either version 2 of the License, or
 + * (at your option) any later version.
 + *
 + * This program is distributed in the hope that it will be useful,
 + * but WITHOUT ANY WARRANTY; without even the implied warranty of
 + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 + * GNU General Public License for more details.
 + *
 + * You should have received a copy of the GNU General Public License along
 + * with this program; if not, write to the Free Software Foundation, Inc.,
 + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 + * http://www.gnu.org/copyleft/gpl.html
 + *
 + * @file
 + */
 +
  /**
   * Abstract class for type hinting (accepts WikiPage, Article, ImagePage, CategoryPage)
   */
@@@ -494,49 -473,6 +494,49 @@@ class WikiPage extends Page 
                return (int)$this->mLatest;
        }
  
 +      /**
 +       * Get the Revision object of the oldest revision
 +       * @return Revision|null
 +       */
 +      public function getOldestRevision() {
 +              wfProfileIn( __METHOD__ );
 +
 +              // Try using the slave database first, then try the master
 +              $continue = 2;
 +              $db = wfGetDB( DB_SLAVE );
 +              $revSelectFields = Revision::selectFields();
 +
 +              while ( $continue ) {
 +                      $row = $db->selectRow(
 +                              array( 'page', 'revision' ),
 +                              $revSelectFields,
 +                              array(
 +                                      'page_namespace' => $this->mTitle->getNamespace(),
 +                                      'page_title' => $this->mTitle->getDBkey(),
 +                                      'rev_page = page_id'
 +                              ),
 +                              __METHOD__,
 +                              array(
 +                                      'ORDER BY' => 'rev_timestamp ASC'
 +                              )
 +                      );
 +
 +                      if ( $row ) {
 +                              $continue = 0;
 +                      } else {
 +                              $db = wfGetDB( DB_MASTER );
 +                              $continue--;
 +                      }
 +              }
 +
 +              wfProfileOut( __METHOD__ );
 +              if ( $row ) {
 +                      return Revision::newFromRow( $row );
 +              } else {
 +                      return null;
 +              }
 +      }
 +
        /**
         * Loads everything except the text
         * This isn't necessary for all uses, so it's only done if needed.
                }
        }
  
 +      /**
 +       * Get the User object of the user who created the page
 +       * @param $audience Integer: one of:
 +       *      Revision::FOR_PUBLIC       to be displayed to all users
 +       *      Revision::FOR_THIS_USER    to be displayed to $wgUser
 +       *      Revision::RAW              get the text regardless of permissions
 +       * @return User|null
 +       */
 +      public function getCreator( $audience = Revision::FOR_PUBLIC ) {
 +              $revision = $this->getOldestRevision();
 +              if ( $revision ) {
 +                      $userName = $revision->getUserText( $audience );
 +                      return User::newFromName( $userName, false );
 +              } else {
 +                      return null;
 +              }
 +      }
 +
        /**
         * @param $audience Integer: one of:
         *      Revision::FOR_PUBLIC       to be displayed to all users
                        $parserCache->save( $editInfo->output, $this, $editInfo->popts );
                }
  
-               # Update the links tables
-               $u = new LinksUpdate( $this->mTitle, $editInfo->output );
-               $u->doUpdate();
+               # Update the links tables and other secondary data
+               $updates = $editInfo->output->getSecondaryDataUpdates( $this->mTitle );
+               DataUpdate::runUpdates( $updates );
  
                wfRunHooks( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) );
  
                        return WikiPage::DELETE_NO_REVISIONS;
                }
  
-               $this->doDeleteUpdates( $id );
+               # update site status
+               DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) );
+               # remove secondary indexes, etc
+               $updates = $this->getDeletionUpdates( );
+               DataUpdate::runUpdates( $updates );
+               # Clear caches
+               WikiPage::onArticleDelete( $this->mTitle );
+               # Reset this object
+               $this->clear();
+               # Clear the cached article id so the interface doesn't act like we exist
+               $this->mTitle->resetArticleID( 0 );
  
                # Log the deletion, if the page was suppressed, log it at Oversight instead
                $logtype = $suppress ? 'suppress' : 'delete';
                return WikiPage::DELETE_SUCCESS;
        }
  
-       /**
-        * Do some database updates after deletion
-        *
-        * @param $id Int: page_id value of the page being deleted
-        */
-       public function doDeleteUpdates( $id ) {
-               DeferredUpdates::addUpdate( new SiteStatsUpdate( 0, 1, - (int)$this->isCountable(), -1 ) );
-               $dbw = wfGetDB( DB_MASTER );
-               # Delete restrictions for it
-               $dbw->delete( 'page_restrictions', array ( 'pr_page' => $id ), __METHOD__ );
-               # Fix category table counts
-               $cats = array();
-               $res = $dbw->select( 'categorylinks', 'cl_to', array( 'cl_from' => $id ), __METHOD__ );
-               foreach ( $res as $row ) {
-                       $cats [] = $row->cl_to;
-               }
-               $this->updateCategoryCounts( array(), $cats );
-               # If using cascading deletes, we can skip some explicit deletes
-               if ( !$dbw->cascadingDeletes() ) {
-                       $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
-                       # Delete outgoing links
-                       $dbw->delete( 'pagelinks', array( 'pl_from' => $id ), __METHOD__ );
-                       $dbw->delete( 'imagelinks', array( 'il_from' => $id ), __METHOD__ );
-                       $dbw->delete( 'categorylinks', array( 'cl_from' => $id ), __METHOD__ );
-                       $dbw->delete( 'templatelinks', array( 'tl_from' => $id ), __METHOD__ );
-                       $dbw->delete( 'externallinks', array( 'el_from' => $id ), __METHOD__ );
-                       $dbw->delete( 'langlinks', array( 'll_from' => $id ), __METHOD__ );
-                       $dbw->delete( 'iwlinks', array( 'iwl_from' => $id ), __METHOD__ );
-                       $dbw->delete( 'redirect', array( 'rd_from' => $id ), __METHOD__ );
-                       $dbw->delete( 'page_props', array( 'pp_page' => $id ), __METHOD__ );
-               }
-               # If using cleanup triggers, we can skip some manual deletes
-               if ( !$dbw->cleanupTriggers() ) {
-                       # Clean up recentchanges entries...
-                       $dbw->delete( 'recentchanges',
-                               array( 'rc_type != ' . RC_LOG,
-                                       'rc_namespace' => $this->mTitle->getNamespace(),
-                                       'rc_title' => $this->mTitle->getDBkey() ),
-                               __METHOD__ );
-                       $dbw->delete( 'recentchanges',
-                               array( 'rc_type != ' . RC_LOG, 'rc_cur_id' => $id ),
-                               __METHOD__ );
-               }
-               # Clear caches
-               self::onArticleDelete( $this->mTitle );
-               # Reset this object
-               $this->clear();
-               # Clear the cached article id so the interface doesn't act like we exist
-               $this->mTitle->resetArticleID( 0 );
-       }
        /**
         * Roll back the most recent consecutive set of edits to a page
         * from the same user; fails if there are no eligible edits to
  
                if ( count( $templates_diff ) > 0 ) {
                        # Whee, link updates time.
+                       # Note: we are only interested in links here. We don't need to get other DataUpdate items from the parser output.
                        $u = new LinksUpdate( $this->mTitle, $parserOutput, false );
                        $u->doUpdate();
                }
                global $wgUser;
                return $this->isParserCacheUsed( ParserOptions::newFromUser( $wgUser ), $oldid );
        }
+       public function getDeletionUpdates() {
+               $updates = array(
+                       new LinksDeletionUpdate( $this ),
+               );
+               //@todo: make a hook to add update objects
+               //NOTE: deletion updates will be determined by the ContentHandler in the future
+               return $updates;
+       }
  }
  
  class PoolWorkArticleView extends PoolCounterWork {
@@@ -89,13 -89,12 +89,13 @@@ class ApiPurge extends ApiBase 
                                        global $wgParser, $wgEnableParserCache;
  
                                        $popts = ParserOptions::newFromContext( $this->getContext() );
 +                                      $popts->setTidy( true );
                                        $p_result = $wgParser->parse( $page->getRawText(), $title, $popts,
                                                true, true, $page->getLatest() );
  
                                        # Update the links tables
-                                       $u = new LinksUpdate( $title, $p_result );
-                                       $u->doUpdate();
+                                       $updates = $p_result->getSecondaryDataUpdates( $title );
+                                       DataUpdate::runUpdates( $updates );
  
                                        $r['linkupdate'] = '';