Merge "Use interwiki cache directly to resolve transwiki import sources"
[lhc/web/wiklou.git] / includes / page / WikiPage.php
index b35ca51..ff8050b 100644 (file)
@@ -143,7 +143,8 @@ class WikiPage implements Page, IDBAccessObject {
 
                $from = self::convertSelectType( $from );
                $db = wfGetDB( $from === self::READ_LATEST ? DB_MASTER : DB_SLAVE );
-               $row = $db->selectRow( 'page', self::selectFields(), array( 'page_id' => $id ), __METHOD__ );
+               $row = $db->selectRow(
+                       'page', self::selectFields(), array( 'page_id' => $id ), __METHOD__ );
                if ( !$row ) {
                        return null;
                }
@@ -298,7 +299,7 @@ class WikiPage implements Page, IDBAccessObject {
 
        /**
         * Fetch a page record with the given conditions
-        * @param DatabaseBase $dbr
+        * @param IDatabase $dbr
         * @param array $conditions
         * @param array $options
         * @return object|bool Database result resource, or false on failure
@@ -319,7 +320,7 @@ class WikiPage implements Page, IDBAccessObject {
         * Fetch a page record matching the Title object's namespace and title
         * using a sanitized title string
         *
-        * @param DatabaseBase $dbr
+        * @param IDatabase $dbr
         * @param Title $title
         * @param array $options
         * @return object|bool Database result resource, or false on failure
@@ -333,7 +334,7 @@ class WikiPage implements Page, IDBAccessObject {
        /**
         * Fetch a page record matching the requested ID
         *
-        * @param DatabaseBase $dbr
+        * @param IDatabase $dbr
         * @param int $id
         * @param array $options
         * @return object|bool Database result resource, or false on failure
@@ -387,7 +388,7 @@ class WikiPage implements Page, IDBAccessObject {
         * Load the object from a database row
         *
         * @since 1.20
-        * @param object $data Database row containing at least fields returned by selectFields()
+        * @param object|bool $data DB row containing fields returned by selectFields() or false
         * @param string|int $from One of the following:
         *        - "fromdb" or WikiPage::READ_NORMAL if the data comes from a slave DB
         *        - "fromdbmaster" or WikiPage::READ_LATEST if the data comes from the master DB
@@ -606,7 +607,7 @@ class WikiPage implements Page, IDBAccessObject {
                        // SELECT. Thus we need S1 to also gets the revision row FOR UPDATE; otherwise, it
                        // may not find it since a page row UPDATE and revision row INSERT by S2 may have
                        // happened after the first S1 SELECT.
-                       // http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read.
+                       // http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html#isolevel_repeatable-read
                        $flags = Revision::READ_LOCKING;
                } elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
                        // Bug T93976: if page_latest was loaded from the master, fetch the
@@ -1010,7 +1011,8 @@ class WikiPage implements Page, IDBAccessObject {
                        $conds[] = "rev_user_text != {$dbr->addQuotes( $this->getUserText() )}";
                }
 
-               $conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0"; // username hidden?
+               // Username hidden?
+               $conds[] = "{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0";
 
                $jconds = array(
                        'user' => array( 'LEFT JOIN', 'rev_user = user_id' ),
@@ -1090,7 +1092,9 @@ class WikiPage implements Page, IDBAccessObject {
 
        /**
         * Get a ParserOutput for the given ParserOptions and revision ID.
-        * The parser cache will be used if possible.
+        *
+        * The parser cache will be used if possible. Cache misses that result
+        * in parser runs are debounced with PoolCounter.
         *
         * @since 1.19
         * @param ParserOptions $parserOptions ParserOptions to use for the parse operation
@@ -1102,7 +1106,8 @@ class WikiPage implements Page, IDBAccessObject {
        public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) {
 
                $useParserCache = $this->shouldCheckParserCache( $parserOptions, $oldid );
-               wfDebug( __METHOD__ . ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
+               wfDebug( __METHOD__ .
+                       ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" );
                if ( $parserOptions->getStubThreshold() ) {
                        wfIncrStats( 'pcache.miss.stub' );
                }
@@ -1127,7 +1132,7 @@ class WikiPage implements Page, IDBAccessObject {
        /**
         * Do standard deferred updates after page view (existing or missing page)
         * @param User $user The relevant user
-        * @param int $oldid The revision id being viewed. If not given or 0, latest revision is assumed.
+        * @param int $oldid Revision id being viewed; if not given or 0, latest revision is assumed
         */
        public function doViewUpdates( User $user, $oldid = 0 ) {
                if ( wfReadOnly() ) {
@@ -1160,7 +1165,7 @@ class WikiPage implements Page, IDBAccessObject {
                        $title->invalidateCache();
                        if ( $wgUseSquid ) {
                                // Send purge now that page_touched update was committed above
-                               $update = SquidUpdate::newSimplePurge( $title );
+                               $update = new SquidUpdate( $title->getSquidURLs() );
                                $update->doUpdate();
                        }
                } );
@@ -1186,7 +1191,6 @@ class WikiPage implements Page, IDBAccessObject {
 
                return true;
        }
-
        /**
         * Insert a new empty page record for this article.
         * This *must* be followed up by creating a revision
@@ -1194,41 +1198,43 @@ class WikiPage implements Page, IDBAccessObject {
         * or else the record will be left in a funky state.
         * Best if all done inside a transaction.
         *
-        * @param DatabaseBase $dbw
+        * @param IDatabase $dbw
         * @return int|bool The newly created page_id key; false if the title already existed
         */
        public function insertOn( $dbw ) {
-               $page_id = $dbw->nextSequenceValue( 'page_page_id_seq' );
-               $dbw->insert( 'page', array(
-                       'page_id'           => $page_id,
-                       'page_namespace'    => $this->mTitle->getNamespace(),
-                       'page_title'        => $this->mTitle->getDBkey(),
-                       'page_restrictions' => '',
-                       'page_is_redirect'  => 0, // Will set this shortly...
-                       'page_is_new'       => 1,
-                       'page_random'       => wfRandom(),
-                       'page_touched'      => $dbw->timestamp(),
-                       'page_latest'       => 0, // Fill this in shortly...
-                       'page_len'          => 0, // Fill this in shortly...
-               ), __METHOD__, 'IGNORE' );
-
-               $affected = $dbw->affectedRows();
-
-               if ( $affected ) {
+               $dbw->insert(
+                       'page',
+                       array(
+                               'page_id'           => $dbw->nextSequenceValue( 'page_page_id_seq' ),
+                               'page_namespace'    => $this->mTitle->getNamespace(),
+                               'page_title'        => $this->mTitle->getDBkey(),
+                               'page_restrictions' => '',
+                               'page_is_redirect'  => 0, // Will set this shortly...
+                               'page_is_new'       => 1,
+                               'page_random'       => wfRandom(),
+                               'page_touched'      => $dbw->timestamp(),
+                               'page_latest'       => 0, // Fill this in shortly...
+                               'page_len'          => 0, // Fill this in shortly...
+                       ),
+                       __METHOD__,
+                       'IGNORE'
+               );
+
+               if ( $dbw->affectedRows() > 0 ) {
                        $newid = $dbw->insertId();
                        $this->mId = $newid;
                        $this->mTitle->resetArticleID( $newid );
 
                        return $newid;
                } else {
-                       return false;
+                       return false; // nothing changed
                }
        }
 
        /**
         * Update the page record to point to a newly saved revision.
         *
-        * @param DatabaseBase $dbw
+        * @param IDatabase $dbw
         * @param Revision $revision For ID number, and text used to set
         *   length and redirect status fields
         * @param int $lastRevision If given, will not overwrite the page field
@@ -1236,7 +1242,7 @@ class WikiPage implements Page, IDBAccessObject {
         *   Giving 0 indicates the new page flag should be set on.
         * @param bool $lastRevIsRedirect If given, will optimize adding and
         *   removing rows in redirect table.
-        * @return bool True on success, false on failure
+        * @return bool Success; false if the page row was missing or page_latest changed
         */
        public function updateRevisionOn( $dbw, $revision, $lastRevision = null,
                $lastRevIsRedirect = null
@@ -1285,8 +1291,14 @@ class WikiPage implements Page, IDBAccessObject {
                        $this->mLatest = $revision->getId();
                        $this->mIsRedirect = (bool)$rt;
                        // Update the LinkCache.
-                       LinkCache::singleton()->addGoodLinkObj( $this->getId(), $this->mTitle, $len, $this->mIsRedirect,
-                                                                                                       $this->mLatest, $revision->getContentModel() );
+                       LinkCache::singleton()->addGoodLinkObj(
+                               $this->getId(),
+                               $this->mTitle,
+                               $len,
+                               $this->mIsRedirect,
+                               $this->mLatest,
+                               $revision->getContentModel()
+                       );
                }
 
                return $result;
@@ -1295,7 +1307,7 @@ class WikiPage implements Page, IDBAccessObject {
        /**
         * Add row to the redirect table if this is a redirect, remove otherwise.
         *
-        * @param DatabaseBase $dbw
+        * @param IDatabase $dbw
         * @param Title $redirectTitle Title object pointing to the redirect target,
         *   or NULL if this is not a redirect
         * @param null|bool $lastRevIsRedirect If given, will optimize adding and
@@ -1334,7 +1346,7 @@ class WikiPage implements Page, IDBAccessObject {
         *
         * @deprecated since 1.24, use updateRevisionOn instead
         *
-        * @param DatabaseBase $dbw
+        * @param IDatabase $dbw
         * @param Revision $revision
         * @return bool
         */
@@ -1421,7 +1433,7 @@ class WikiPage implements Page, IDBAccessObject {
         * @param string $edittime Revision timestamp or null to use the current revision.
         *
         * @throws MWException
-        * @return string New complete article text, or null if error.
+        * @return string|null New complete article text, or null if error.
         *
         * @deprecated since 1.21, use replaceSectionAtRev() instead
         */
@@ -1473,13 +1485,14 @@ class WikiPage implements Page, IDBAccessObject {
         * @param string $edittime Revision timestamp or null to use the current revision.
         *
         * @throws MWException
-        * @return Content New complete article content, or null if error.
+        * @return Content|null New complete article content, or null if error.
         *
         * @since 1.21
         * @deprecated since 1.24, use replaceSectionAtRev instead
         */
-       public function replaceSectionContent( $sectionId, Content $sectionContent, $sectionTitle = '',
-               $edittime = null ) {
+       public function replaceSectionContent(
+               $sectionId, Content $sectionContent, $sectionTitle = '', $edittime = null
+       ) {
 
                $baseRevId = null;
                if ( $edittime && $sectionId !== 'new' ) {
@@ -1512,7 +1525,7 @@ class WikiPage implements Page, IDBAccessObject {
         * @param int|null $baseRevId
         *
         * @throws MWException
-        * @return Content New complete article content, or null if error.
+        * @return Content|null New complete article content, or null if error.
         *
         * @since 1.24
         */
@@ -1588,8 +1601,6 @@ class WikiPage implements Page, IDBAccessObject {
         *          Do not log the change in recentchanges
         *      EDIT_FORCE_BOT
         *          Mark the edit a "bot" edit regardless of user rights
-        *      EDIT_DEFER_UPDATES
-        *          Defer some of the updates until the end of index.php
         *      EDIT_AUTOSUMMARY
         *          Fill in blank summaries with generated text where possible
         *
@@ -1650,8 +1661,6 @@ class WikiPage implements Page, IDBAccessObject {
         *          Do not log the change in recentchanges
         *      EDIT_FORCE_BOT
         *          Mark the edit a "bot" edit regardless of user rights
-        *      EDIT_DEFER_UPDATES
-        *          Defer some of the updates until the end of index.php
         *      EDIT_AUTOSUMMARY
         *          Fill in blank summaries with generated text where possible
         *
@@ -1795,32 +1804,41 @@ class WikiPage implements Page, IDBAccessObject {
                        $changed = !$content->equals( $old_content );
 
                        if ( $changed ) {
-                               $dbw->begin( __METHOD__ );
-
                                $prepStatus = $content->prepareSave( $this, $flags, $oldid, $user );
                                $status->merge( $prepStatus );
 
                                if ( !$status->isOK() ) {
-                                       $dbw->rollback( __METHOD__ );
+                                       return $status;
+                               }
+
+                               $dbw->begin( __METHOD__ );
+                               // Get the latest page_latest value while locking it.
+                               // Do a CAS style check to see if it's the same as when this method
+                               // started. If it changed then bail out before touching the DB.
+                               $latestNow = $this->lock();
+                               if ( $latestNow != $oldid ) {
+                                       $dbw->commit( __METHOD__ );
+                                       // Page updated or deleted in the mean time
+                                       $status->fatal( 'edit-conflict' );
 
                                        return $status;
                                }
-                               $revisionId = $revision->insertOn( $dbw );
 
-                               // Update page.
-                               // We check for conflicts by comparing $oldid with the current latest revision ID.
-                               $ok = $this->updateRevisionOn( $dbw, $revision, $oldid, $oldIsRedirect );
+                               // At this point we are now comitted to returning an OK
+                               // status unless some DB query error or other exception comes up.
+                               // This way callers don't have to call rollback() if $status is bad
+                               // unless they actually try to catch exceptions (which is rare).
 
-                               if ( !$ok ) {
-                                       // Belated edit conflict! Run away!!
-                                       $status->fatal( 'edit-conflict' );
+                               $revisionId = $revision->insertOn( $dbw );
 
+                               // Update page_latest and friends to reflect the new revision
+                               if ( !$this->updateRevisionOn( $dbw, $revision, null, $oldIsRedirect ) ) {
                                        $dbw->rollback( __METHOD__ );
-
-                                       return $status;
+                                       throw new MWException( "Failed to update page row to use new revision." );
                                }
 
-                               Hooks::run( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) );
+                               Hooks::run( 'NewRevisionFromEditComplete',
+                                       array( $this, $revision, $baseRevId, $user ) );
 
                                // Update recentchanges
                                if ( !( $flags & EDIT_SUPPRESS_RC ) ) {
@@ -1866,30 +1884,28 @@ class WikiPage implements Page, IDBAccessObject {
                        // Create new article
                        $status->value['new'] = true;
 
-                       $dbw->begin( __METHOD__ );
-
                        $prepStatus = $content->prepareSave( $this, $flags, $oldid, $user );
                        $status->merge( $prepStatus );
-
                        if ( !$status->isOK() ) {
-                               $dbw->rollback( __METHOD__ );
-
                                return $status;
                        }
 
-                       $status->merge( $prepStatus );
+                       $dbw->begin( __METHOD__ );
 
-                       // Add the page record; stake our claim on this title!
-                       // This will return false if the article already exists
+                       // Add the page record unless one already exists for the title
                        $newid = $this->insertOn( $dbw );
-
                        if ( $newid === false ) {
-                               $dbw->rollback( __METHOD__ );
+                               $dbw->commit( __METHOD__ ); // nothing inserted
                                $status->fatal( 'edit-already-exists' );
 
-                               return $status;
+                               return $status; // nothing done
                        }
 
+                       // At this point we are now comitted to returning an OK
+                       // status unless some DB query error or other exception comes up.
+                       // This way callers don't have to call rollback() if $status is bad
+                       // unless they actually try to catch exceptions (which is rare).
+
                        // Save the revision text...
                        $revision = new Revision( array(
                                'page'       => $newid,
@@ -1914,7 +1930,10 @@ class WikiPage implements Page, IDBAccessObject {
                        }
 
                        // Update the page record with revision data
-                       $this->updateRevisionOn( $dbw, $revision, 0 );
+                       if ( !$this->updateRevisionOn( $dbw, $revision, 0 ) ) {
+                               $dbw->rollback( __METHOD__ );
+                               throw new MWException( "Failed to update page row to use new revision." );
+                       }
 
                        Hooks::run( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) );
 
@@ -1945,11 +1964,6 @@ class WikiPage implements Page, IDBAccessObject {
                        Hooks::run( 'PageContentInsertComplete', $hook_args );
                }
 
-               // Do updates right now unless deferral was requested
-               if ( !( $flags & EDIT_DEFER_UPDATES ) ) {
-                       DeferredUpdates::doUpdates();
-               }
-
                // Return the new revision (or null) to the caller
                $status->value['revision'] = $revision;
 
@@ -2023,7 +2037,8 @@ class WikiPage implements Page, IDBAccessObject {
         * @since 1.21
         */
        public function prepareContentForEdit(
-               Content $content, $revision = null, User $user = null, $serialFormat = null, $useCache = true
+               Content $content, $revision = null, User $user = null,
+               $serialFormat = null, $useCache = true
        ) {
                global $wgContLang, $wgUser, $wgAjaxEditStash;
 
@@ -2094,16 +2109,13 @@ class WikiPage implements Page, IDBAccessObject {
                                // itself (such as via self-transclusion). In this case, we need to make sure
                                // that any such self-references refer to the newly-saved revision, and not
                                // to the previous one, which could otherwise happen due to slave lag.
-                               $oldCallback = $edit->popts->setCurrentRevisionCallback(
-                                       function ( $title, $parser = false ) use ( $revision, &$oldCallback ) {
+                               $oldCallback = $edit->popts->getCurrentRevisionCallback();
+                               $edit->popts->setCurrentRevisionCallback(
+                                       function ( Title $title, $parser = false ) use ( $revision, &$oldCallback ) {
                                                if ( $title->equals( $revision->getTitle() ) ) {
                                                        return $revision;
                                                } else {
-                                                       return call_user_func(
-                                                               $oldCallback,
-                                                               $title,
-                                                               $parser
-                                                       );
+                                                       return call_user_func( $oldCallback, $title, $parser );
                                                }
                                        }
                                );
@@ -2117,8 +2129,12 @@ class WikiPage implements Page, IDBAccessObject {
                $edit->oldContent = $this->getContent( Revision::RAW );
 
                // NOTE: B/C for hooks! don't use these fields!
-               $edit->newText = $edit->newContent ? ContentHandler::getContentText( $edit->newContent ) : '';
-               $edit->oldText = $edit->oldContent ? ContentHandler::getContentText( $edit->oldContent ) : '';
+               $edit->newText = $edit->newContent
+                       ? ContentHandler::getContentText( $edit->newContent )
+                       : '';
+               $edit->oldText = $edit->oldContent
+                       ? ContentHandler::getContentText( $edit->oldContent )
+                       : '';
                $edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialFormat ) : '';
 
                $this->mPreparedEdit = $edit;
@@ -2177,6 +2193,10 @@ class WikiPage implements Page, IDBAccessObject {
                        $updates = $content->getSecondaryDataUpdates(
                                $this->getTitle(), null, $recursive, $editInfo->output );
                        foreach ( $updates as $update ) {
+                               if ( $update instanceof LinksUpdate ) {
+                                       $update->setRevision( $revision );
+                                       $update->setTriggeringUser( $user );
+                               }
                                DeferredUpdates::addUpdate( $update );
                        }
                }
@@ -2227,7 +2247,8 @@ class WikiPage implements Page, IDBAccessObject {
                        if ( !$recipient ) {
                                wfDebug( __METHOD__ . ": invalid username\n" );
                        } else {
-                               // Allow extensions to prevent user notification when a new message is added to their talk page
+                               // Allow extensions to prevent user notification
+                               // when a new message is added to their talk page
                                if ( Hooks::run( 'ArticleEditUpdateNewTalk', array( &$this, $recipient ) ) ) {
                                        if ( User::isIP( $shortTitle ) ) {
                                                // An anonymous user
@@ -2258,25 +2279,6 @@ class WikiPage implements Page, IDBAccessObject {
                }
        }
 
-       /**
-        * Edit an article without doing all that other stuff
-        * The article must already exist; link tables etc
-        * are not updated, caches are not flushed.
-        *
-        * @param string $text Text submitted
-        * @param User $user The relevant user
-        * @param string $comment Comment submitted
-        * @param bool $minor Whereas it's a minor modification
-        *
-        * @deprecated since 1.21, use doEditContent() instead.
-        */
-       public function doQuickEdit( $text, User $user, $comment = '', $minor = 0 ) {
-               ContentHandler::deprecated( __METHOD__, "1.21" );
-
-               $content = ContentHandler::makeContent( $text, $this->getTitle() );
-               $this->doQuickEditContent( $content, $user, $comment, $minor );
-       }
-
        /**
         * Edit an article without doing all that other stuff
         * The article must already exist; link tables etc
@@ -2288,8 +2290,8 @@ class WikiPage implements Page, IDBAccessObject {
         * @param bool $minor Whereas it's a minor modification
         * @param string $serialFormat Format for storing the content in the database
         */
-       public function doQuickEditContent( Content $content, User $user, $comment = '', $minor = false,
-               $serialFormat = null
+       public function doQuickEditContent(
+               Content $content, User $user, $comment = '', $minor = false, $serialFormat = null
        ) {
 
                $serialized = $content->serialize( $serialFormat );
@@ -2494,7 +2496,8 @@ class WikiPage implements Page, IDBAccessObject {
                                __METHOD__
                        );
 
-                       Hooks::run( 'NewRevisionFromEditComplete', array( $this, $nullRevision, $latest, $user ) );
+                       Hooks::run( 'NewRevisionFromEditComplete',
+                               array( $this, $nullRevision, $latest, $user ) );
                        Hooks::run( 'ArticleProtectComplete', array( &$this, &$user, $limit, $reason ) );
                } else { // Protection of non-existing page (also known as "title protection")
                        // Cascade protection is meaningless in this case
@@ -2651,7 +2654,8 @@ class WikiPage implements Page, IDBAccessObject {
                        # with '' filtered out. All possible message keys are listed below:
                        # * protect-level-autoconfirmed
                        # * protect-level-sysop
-                       $restrictionsText = wfMessage( 'protect-level-' . $restrictions )->inContentLanguage()->text();
+                       $restrictionsText = wfMessage( 'protect-level-' . $restrictions )
+                               ->inContentLanguage()->text();
 
                        $expiryText = $this->formatExpiry( $expiry[$action] );
 
@@ -2684,7 +2688,8 @@ class WikiPage implements Page, IDBAccessObject {
 
                foreach ( array_filter( $limit ) as $action => $restrictions ) {
                        $expiryText = $this->formatExpiry( $expiry[$action] );
-                       $protectDescriptionLog .= $wgContLang->getDirMark() . "[$action=$restrictions] ($expiryText)";
+                       $protectDescriptionLog .= $wgContLang->getDirMark() .
+                               "[$action=$restrictions] ($expiryText)";
                }
 
                return trim( $protectDescriptionLog );
@@ -2701,7 +2706,7 @@ class WikiPage implements Page, IDBAccessObject {
         */
        protected static function flattenRestrictions( $limit ) {
                if ( !is_array( $limit ) ) {
-                       throw new MWException( 'WikiPage::flattenRestrictions given non-array restriction set' );
+                       throw new MWException( __METHOD__ . ' given non-array restriction set' );
                }
 
                $bits = array();
@@ -2724,16 +2729,16 @@ class WikiPage implements Page, IDBAccessObject {
         * @param string $reason Delete reason for deletion log
         * @param bool $suppress Suppress all revisions and log the deletion in
         *        the suppression log instead of the deletion log
-        * @param int $id Article ID
-        * @param bool $commit Defaults to true, triggers transaction end
-        * @param array &$error Array of errors to append to
+        * @param int $u1 Unused
+        * @param bool $u2 Unused
+        * @param array|string &$error Array of errors to append to
         * @param User $user The deleting user
         * @return bool True if successful
         */
        public function doDeleteArticle(
-               $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null
+               $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null
        ) {
-               $status = $this->doDeleteArticleReal( $reason, $suppress, $id, $commit, $error, $user );
+               $status = $this->doDeleteArticleReal( $reason, $suppress, $u1, $u2, $error, $user );
                return $status->isGood();
        }
 
@@ -2746,16 +2751,16 @@ class WikiPage implements Page, IDBAccessObject {
         * @param string $reason Delete reason for deletion log
         * @param bool $suppress Suppress all revisions and log the deletion in
         *   the suppression log instead of the deletion log
-        * @param int $id Article ID
-        * @param bool $commit Defaults to true, triggers transaction end
-        * @param array &$error Array of errors to append to
+        * @param int $u1 Unused
+        * @param bool $u2 Unused
+        * @param array|string &$error Array of errors to append to
         * @param User $user The deleting user
         * @return Status Status object; if successful, $status->value is the log_id of the
         *   deletion log entry. If the page couldn't be deleted because it wasn't
         *   found, $status is a non-fatal 'cannotdelete' error
         */
        public function doDeleteArticleReal(
-               $reason, $suppress = false, $id = 0, $commit = true, &$error = '', User $user = null
+               $reason, $suppress = false, $u1 = null, $u2 = null, &$error = '', User $user = null
        ) {
                global $wgUser, $wgContentHandlerUseDB;
 
@@ -2764,12 +2769,15 @@ class WikiPage implements Page, IDBAccessObject {
                $status = Status::newGood();
 
                if ( $this->mTitle->getDBkey() === '' ) {
-                       $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
+                       $status->error( 'cannotdelete',
+                               wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
                        return $status;
                }
 
                $user = is_null( $user ) ? $wgUser : $user;
-               if ( !Hooks::run( 'ArticleDelete', array( &$this, &$user, &$reason, &$error, &$status ) ) ) {
+               if ( !Hooks::run( 'ArticleDelete',
+                       array( &$this, &$user, &$reason, &$error, &$status, $suppress )
+               ) ) {
                        if ( $status->isOK() ) {
                                // Hook aborted but didn't set a fatal status
                                $status->fatal( 'delete-hook-aborted' );
@@ -2778,25 +2786,28 @@ class WikiPage implements Page, IDBAccessObject {
                }
 
                $dbw = wfGetDB( DB_MASTER );
-               $dbw->begin( __METHOD__ );
-
-               if ( $id == 0 ) {
-                       // T98706: lock the page from various other updates but avoid using
-                       // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
-                       // the revisions queries (which also JOIN on user). Only lock the page
-                       // row and CAS check on page_latest to see if the trx snapshot matches.
-                       $latest = $this->lock();
-
-                       $this->loadPageData( WikiPage::READ_LATEST );
-                       $id = $this->getID();
-                       if ( $id == 0 || $this->getLatest() != $latest ) {
-                               // Page not there or trx snapshot is stale
-                               $dbw->rollback( __METHOD__ );
-                               $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
-                               return $status;
-                       }
+               $dbw->startAtomic( __METHOD__ );
+
+               $this->loadPageData( self::READ_LATEST );
+               $id = $this->getID();
+               // T98706: lock the page from various other updates but avoid using
+               // WikiPage::READ_LOCKING as that will carry over the FOR UPDATE to
+               // the revisions queries (which also JOIN on user). Only lock the page
+               // row and CAS check on page_latest to see if the trx snapshot matches.
+               $lockedLatest = $this->lock();
+               if ( $id == 0 || $this->getLatest() != $lockedLatest ) {
+                       $dbw->endAtomic( __METHOD__ );
+                       // Page not there or trx snapshot is stale
+                       $status->error( 'cannotdelete',
+                               wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
+                       return $status;
                }
 
+               // At this point we are now comitted to returning an OK
+               // status unless some DB query error or other exception comes up.
+               // This way callers don't have to call rollback() if $status is bad
+               // unless they actually try to catch exceptions (which is rare).
+
                // we need to remember the old content so we can use it to generate all deletion updates.
                $content = $this->getContent( Revision::RAW );
 
@@ -2849,23 +2860,20 @@ class WikiPage implements Page, IDBAccessObject {
                        $row['ar_content_format'] = 'rev_content_format';
                }
 
-               $dbw->insertSelect( 'archive', array( 'page', 'revision' ),
+               // Copy all the page revisions into the archive table
+               $dbw->insertSelect(
+                       'archive',
+                       array( 'page', 'revision' ),
                        $row,
                        array(
                                'page_id' => $id,
                                'page_id = rev_page'
-                       ), __METHOD__
+                       ),
+                       __METHOD__
                );
 
                // Now that it's safely backed up, delete it
                $dbw->delete( 'page', array( 'page_id' => $id ), __METHOD__ );
-               $ok = ( $dbw->affectedRows() > 0 ); // $id could be laggy
-
-               if ( !$ok ) {
-                       $dbw->rollback( __METHOD__ );
-                       $status->error( 'cannotdelete', wfEscapeWikiText( $this->getTitle()->getPrefixedText() ) );
-                       return $status;
-               }
 
                if ( !$dbw->cascadingDeletes() ) {
                        $dbw->delete( 'revision', array( 'rev_page' => $id ), __METHOD__ );
@@ -2888,31 +2896,35 @@ class WikiPage implements Page, IDBAccessObject {
                        $logEntry->publish( $logid );
                } );
 
-               if ( $commit ) {
-                       $dbw->commit( __METHOD__ );
-               }
-
-               // Show log excerpt on 404 pages rather than just a link
-               $key = wfMemcKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
-               ObjectCache::getMainStashInstance()->set( $key, 1, 86400 );
+               $dbw->endAtomic( __METHOD__ );
 
                $this->doDeleteUpdates( $id, $content );
 
-               Hooks::run( 'ArticleDeleteComplete', array( &$this, &$user, $reason, $id, $content, $logEntry ) );
+               Hooks::run( 'ArticleDeleteComplete',
+                       array( &$this, &$user, $reason, $id, $content, $logEntry ) );
                $status->value = $logid;
+
+               // Show log excerpt on 404 pages rather than just a link
+               $cache = ObjectCache::getMainStashInstance();
+               $key = wfMemcKey( 'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
+               $cache->set( $key, 1, $cache::TTL_DAY );
+
                return $status;
        }
 
        /**
-        * Lock the page row for this title and return page_latest (or 0)
+        * Lock the page row for this title+id and return page_latest (or 0)
         *
-        * @return integer
+        * @return integer Returns 0 if no row was found with this title+id
         */
        protected function lock() {
                return (int)wfGetDB( DB_MASTER )->selectField(
                        'page',
                        'page_latest',
                        array(
+                               'page_id' => $this->getId(),
+                               // Typically page_id is enough, but some code might try to do
+                               // updates assuming the title is the same, so verify that
                                'page_namespace' => $this->getTitle()->getNamespace(),
                                'page_title' => $this->getTitle()->getDBkey()
                        ),
@@ -2935,10 +2947,9 @@ class WikiPage implements Page, IDBAccessObject {
 
                // Delete pagelinks, update secondary indexes, etc
                $updates = $this->getDeletionUpdates( $content );
-               // Make sure an enqueued jobs run after commit so they see the deletion
-               wfGetDB( DB_MASTER )->onTransactionIdle( function() use ( $updates ) {
-                       DataUpdate::runUpdates( $updates, 'enqueue' );
-               } );
+               foreach ( $updates as $update ) {
+                       DeferredUpdates::addUpdate( $update );
+               }
 
                // Reparse any pages transcluding this page
                LinksUpdate::queueRecursiveJobsForTable( $this->mTitle, 'templatelinks' );
@@ -3151,7 +3162,10 @@ class WikiPage implements Page, IDBAccessObject {
                }
 
                // raise error, when the edit is an edit without a new version
-               if ( empty( $status->value['revision'] ) ) {
+               $statusRev = isset( $status->value['revision'] )
+                       ? $status->value['revision']
+                       : null;
+               if ( !( $statusRev instanceof Revision ) ) {
                        $resultDetails = array( 'current' => $current );
                        return array( array( 'alreadyrolled',
                                        htmlspecialchars( $this->mTitle->getPrefixedText() ),
@@ -3160,7 +3174,7 @@ class WikiPage implements Page, IDBAccessObject {
                        ) );
                }
 
-               $revId = $status->value['revision']->getId();
+               $revId = $statusRev->getId();
 
                Hooks::run( 'ArticleRollbackComplete', array( $this, $guser, $target, $current ) );
 
@@ -3221,8 +3235,7 @@ class WikiPage implements Page, IDBAccessObject {
 
                // Images
                if ( $title->getNamespace() == NS_FILE ) {
-                       $update = new HTMLCacheUpdate( $title, 'imagelinks' );
-                       $update->doUpdate();
+                       DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'imagelinks' ) );
                }
 
                // User talk pages
@@ -3377,22 +3390,44 @@ class WikiPage implements Page, IDBAccessObject {
                                }
 
                                if ( count( $added ) ) {
-                                       $insertRows = array();
-                                       foreach ( $added as $cat ) {
-                                               $insertRows[] = array(
-                                                       'cat_title'   => $cat,
-                                                       'cat_pages'   => 1,
-                                                       'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0,
-                                                       'cat_files'   => ( $ns == NS_FILE ) ? 1 : 0,
-                                               );
-                                       }
-                                       $dbw->upsert(
+                                       $existingAdded = $dbw->selectFieldValues(
                                                'category',
-                                               $insertRows,
-                                               array( 'cat_title' ),
-                                               $addFields,
-                                               $method
+                                               'cat_title',
+                                               array( 'cat_title' => $added ),
+                                               __METHOD__
                                        );
+
+                                       // For category rows that already exist, do a plain
+                                       // UPDATE instead of INSERT...ON DUPLICATE KEY UPDATE
+                                       // to avoid creating gaps in the cat_id sequence.
+                                       if ( count( $existingAdded ) ) {
+                                               $dbw->update(
+                                                       'category',
+                                                       $addFields,
+                                                       array( 'cat_title' => $existingAdded ),
+                                                       __METHOD__
+                                               );
+                                       }
+
+                                       $missingAdded = array_diff( $added, $existingAdded );
+                                       if ( count( $missingAdded ) ) {
+                                               $insertRows = array();
+                                               foreach ( $missingAdded as $cat ) {
+                                                       $insertRows[] = array(
+                                                               'cat_title'   => $cat,
+                                                               'cat_pages'   => 1,
+                                                               'cat_subcats' => ( $ns == NS_CATEGORY ) ? 1 : 0,
+                                                               'cat_files'   => ( $ns == NS_FILE ) ? 1 : 0,
+                                                       );
+                                               }
+                                               $dbw->upsert(
+                                                       'category',
+                                                       $insertRows,
+                                                       array( 'cat_title' ),
+                                                       $addFields,
+                                                       $method
+                                               );
+                                       }
                                }
 
                                if ( count( $deleted ) ) {
@@ -3428,30 +3463,40 @@ class WikiPage implements Page, IDBAccessObject {
                        return;
                }
 
-               if ( !Hooks::run( 'OpportunisticLinksUpdate', array( $this, $this->mTitle, $parserOutput ) ) ) {
+               if ( !Hooks::run( 'OpportunisticLinksUpdate',
+                       array( $this, $this->mTitle, $parserOutput )
+               ) ) {
                        return;
                }
 
+               $params = array(
+                       'isOpportunistic' => true,
+                       'rootJobTimestamp' => $parserOutput->getCacheTime()
+               );
+
                if ( $this->mTitle->areRestrictionsCascading() ) {
                        // If the page is cascade protecting, the links should really be up-to-date
-                       $params = array( 'prioritize' => true );
+                       JobQueueGroup::singleton()->lazyPush(
+                               RefreshLinksJob::newPrioritized( $this->mTitle, $params )
+                       );
                } elseif ( $parserOutput->hasDynamicContent() ) {
-                       // Assume the output contains time/random based magic words
-                       $params = array();
-               } else {
-                       // If the inclusions are deterministic, the edit-triggered link jobs are enough
-                       return;
-               }
-
-               // Check if the last link refresh was before page_touched
-               if ( $this->getLinksTimestamp() < $this->getTouched() ) {
-                       $params['isOpportunistic'] = true;
-                       $params['rootJobTimestamp'] = $parserOutput->getCacheTime();
-
-                       JobQueueGroup::singleton()->lazyPush( EnqueueJob::newFromLocalJobs(
-                               new JobSpecification( 'refreshLinks', $params,
-                                       array( 'removeDuplicates' => true ), $this->mTitle )
-                       ) );
+                       // Assume the output contains "dynamic" time/random based magic words.
+                       // Only update pages that expired due to dynamic content and NOT due to edits
+                       // to referenced templates/files. When the cache expires due to dynamic content,
+                       // page_touched is unchanged. We want to avoid triggering redundant jobs due to
+                       // views of pages that were just purged via HTMLCacheUpdateJob. In that case, the
+                       // template/file edit already triggered recursive RefreshLinksJob jobs.
+                       if ( $this->getLinksTimestamp() > $this->getTouched() ) {
+                               // If a page is uncacheable, do not keep spamming a job for it.
+                               // Although it would be de-duplicated, it would still waste I/O.
+                               $cache = ObjectCache::getLocalClusterInstance();
+                               $key = $cache->makeKey( 'dynamic-linksupdate', 'last', $this->getId() );
+                               if ( $cache->add( $key, time(), 60 ) ) {
+                                       JobQueueGroup::singleton()->lazyPush(
+                                               RefreshLinksJob::newDynamic( $this->mTitle, $params )
+                                       );
+                               }
+                       }
                }
        }
 
@@ -3524,8 +3569,8 @@ class WikiPage implements Page, IDBAccessObject {
         */
        public function getDeletionUpdates( Content $content = null ) {
                if ( !$content ) {
-                       // load content object, which may be used to determine the necessary updates
-                       // XXX: the content may not be needed to determine the updates, then this would be overhead.
+                       // load content object, which may be used to determine the necessary updates.
+                       // XXX: the content may not be needed to determine the updates.
                        $content = $this->getContent( Revision::RAW );
                }