diff options
author | Pierre Schmitz <pierre@archlinux.de> | 2015-12-17 09:15:42 +0100 |
---|---|---|
committer | Pierre Schmitz <pierre@archlinux.de> | 2015-12-17 09:44:51 +0100 |
commit | a1789ddde42033f1b05cc4929491214ee6e79383 (patch) | |
tree | 63615735c4ddffaaabf2428946bb26f90899f7bf /includes/page | |
parent | 9e06a62f265e3a2aaabecc598d4bc617e06fa32d (diff) |
Update to MediaWiki 1.26.0
Diffstat (limited to 'includes/page')
-rw-r--r-- | includes/page/Article.php | 129 | ||||
-rw-r--r-- | includes/page/ImagePage.php | 35 | ||||
-rw-r--r-- | includes/page/WikiPage.php | 417 |
3 files changed, 322 insertions, 259 deletions
diff --git a/includes/page/Article.php b/includes/page/Article.php index 4cde5ad8..56b9520a 100644 --- a/includes/page/Article.php +++ b/includes/page/Article.php @@ -333,7 +333,9 @@ class Article implements Page { * @return string|bool String containing article contents, or false if null * @deprecated since 1.21, use WikiPage::getContent() instead */ - function fetchContent() { #BC cruft! + function fetchContent() { + // BC cruft! + ContentHandler::deprecated( __METHOD__, '1.21' ); if ( $this->mContentLoaded && $this->mContent ) { @@ -572,7 +574,7 @@ class Article implements Page { } # Should the parser cache be used? - $useParserCache = $this->mPage->isParserCacheUsed( $parserOptions, $oldid ); + $useParserCache = $this->mPage->shouldCheckParserCache( $parserOptions, $oldid ); wfDebug( 'Article::view using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" ); if ( $user->getStubThreshold() ) { $this->getContext()->getStats()->increment( 'pcache_miss_stub' ); @@ -998,7 +1000,7 @@ class Article implements Page { $outputPage->addModules( 'mediawiki.action.view.redirect' ); // Add a <link rel="canonical"> tag - $outputPage->setCanonicalUrl( $this->getTitle()->getLocalURL() ); + $outputPage->setCanonicalUrl( $this->getTitle()->getCanonicalURL() ); // Tell the output object that the user arrived at this article through a redirect $outputPage->setRedirectedFrom( $this->mRedirectedFrom ); @@ -1089,11 +1091,6 @@ class Article implements Page { // to get the recentchanges row belonging to that entry // (with rc_new = 1). - // Check for cached results - if ( $cache->get( wfMemcKey( 'NotPatrollablePage', $this->getTitle()->getArticleID() ) ) ) { - return false; - } - if ( $this->mRevision && !RecentChange::isInRCLifespan( $this->mRevision->getTimestamp(), 21600 ) ) { @@ -1102,6 +1099,12 @@ class Article implements Page { return false; } + // Check for cached results + $key = wfMemcKey( 'NotPatrollablePage', $this->getTitle()->getArticleID() ); + if ( $cache->get( $key ) ) { + return false; + } + $dbr = wfGetDB( DB_SLAVE ); $oldestRevisionTimestamp = $dbr->selectField( 'revision', @@ -1119,20 +1122,30 @@ class Article implements Page { 'rc_new' => 1, 'rc_timestamp' => $oldestRevisionTimestamp, 'rc_namespace' => $this->getTitle()->getNamespace(), - 'rc_cur_id' => $this->getTitle()->getArticleID(), - 'rc_patrolled' => 0 + 'rc_cur_id' => $this->getTitle()->getArticleID() ), __METHOD__, array( 'USE INDEX' => 'new_name_timestamp' ) ); + } else { + // Cache the information we gathered above in case we can't patrol + // Don't cache in case we can patrol as this could change + $cache->set( $key, '1' ); } if ( !$rc ) { - // No RC entry around + // Don't cache: This can be hit if the page gets accessed very fast after + // its creation or in case we have high slave lag. In case the revision is + // too old, we will already return above. + return false; + } + + if ( $rc->getAttribute( 'rc_patrolled' ) ) { + // Patrolled RC entry around // Cache the information we gathered above in case we can't patrol // Don't cache in case we can patrol as this could change - $cache->set( wfMemcKey( 'NotPatrollablePage', $this->getTitle()->getArticleID() ), '1' ); + $cache->set( $key, '1' ); return false; } @@ -1222,23 +1235,38 @@ class Article implements Page { Hooks::run( 'ShowMissingArticle', array( $this ) ); - // Give extensions a chance to hide their (unrelated) log entries - $logTypes = array( 'delete', 'move' ); - $conds = array( "log_action != 'revision'" ); - Hooks::run( 'Article::MissingArticleConditions', array( &$conds, $logTypes ) ); - - # Show delete and move logs - LogEventsList::showLogExtract( $outputPage, $logTypes, $title, '', - array( 'lim' => 10, - 'conds' => $conds, - 'showIfEmpty' => false, - 'msgKey' => array( 'moveddeleted-notice' ) ) - ); + # Show delete and move logs if there were any such events. + # The logging query can DOS the site when bots/crawlers cause 404 floods, + # so be careful showing this. 404 pages must be cheap as they are hard to cache. + $cache = ObjectCache::getMainStashInstance(); + $key = wfMemcKey( 'page-recent-delete', md5( $title->getPrefixedText() ) ); + $loggedIn = $this->getContext()->getUser()->isLoggedIn(); + if ( $loggedIn || $cache->get( $key ) ) { + $logTypes = array( 'delete', 'move' ); + $conds = array( "log_action != 'revision'" ); + // Give extensions a chance to hide their (unrelated) log entries + Hooks::run( 'Article::MissingArticleConditions', array( &$conds, $logTypes ) ); + LogEventsList::showLogExtract( + $outputPage, + $logTypes, + $title, + '', + array( + 'lim' => 10, + 'conds' => $conds, + 'showIfEmpty' => false, + 'msgKey' => array( $loggedIn + ? 'moveddeleted-notice' + : 'moveddeleted-notice-recent' + ) + ) + ); + } if ( !$this->mPage->hasViewableContent() && $wgSend404Code && !$validUserPage ) { // If there's no backing content, send a 404 Not Found // for better machine handling of broken links. - $this->getContext()->getRequest()->response()->header( "HTTP/1.1 404 Not Found" ); + $this->getContext()->getRequest()->response()->statusHeader( 404 ); } // Also apply the robot policy for nonexisting pages (even if a 404 was used for sanity) @@ -1254,22 +1282,28 @@ class Article implements Page { # Show error message $oldid = $this->getOldID(); - if ( $oldid ) { - $text = wfMessage( 'missing-revision', $oldid )->plain(); - } elseif ( $title->getNamespace() === NS_MEDIAWIKI ) { - // Use the default message text - $text = $title->getDefaultMessageText(); - } elseif ( $title->quickUserCan( 'create', $this->getContext()->getUser() ) - && $title->quickUserCan( 'edit', $this->getContext()->getUser() ) - ) { - $message = $this->getContext()->getUser()->isLoggedIn() ? 'noarticletext' : 'noarticletextanon'; - $text = wfMessage( $message )->plain(); + if ( !$oldid && $title->getNamespace() === NS_MEDIAWIKI && $title->hasSourceText() ) { + $outputPage->addParserOutput( $this->getContentObject()->getParserOutput( $title ) ); } else { - $text = wfMessage( 'noarticletext-nopermission' )->plain(); - } - $text = "<div class='noarticletext'>\n$text\n</div>"; + if ( $oldid ) { + $text = wfMessage( 'missing-revision', $oldid )->plain(); + } elseif ( $title->quickUserCan( 'create', $this->getContext()->getUser() ) + && $title->quickUserCan( 'edit', $this->getContext()->getUser() ) + ) { + $message = $this->getContext()->getUser()->isLoggedIn() ? 'noarticletext' : 'noarticletextanon'; + $text = wfMessage( $message )->plain(); + } else { + $text = wfMessage( 'noarticletext-nopermission' )->plain(); + } - $outputPage->addWikiText( $text ); + $dir = $this->getContext()->getLanguage()->getDir(); + $lang = $this->getContext()->getLanguage()->getCode(); + $outputPage->addWikiText( Xml::openElement( 'div', array( + 'class' => "noarticletext mw-content-$dir", + 'dir' => $dir, + 'lang' => $lang, + ) ) . "\n$text\n</div>" ); + } } /** @@ -1704,10 +1738,8 @@ class Article implements Page { if ( $user->isAllowed( 'suppressrevision' ) ) { $suppress = Html::openElement( 'div', array( 'id' => 'wpDeleteSuppressRow' ) ) . - "<strong>" . - Xml::checkLabel( wfMessage( 'revdelete-suppress' )->text(), - 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '4' ) ) . - "</strong>" . + Xml::checkLabel( wfMessage( 'revdelete-suppress' )->text(), + 'wpSuppress', 'wpSuppress', false, array( 'tabindex' => '4' ) ) . Html::closeElement( 'div' ); } else { $suppress = ''; @@ -1772,9 +1804,8 @@ class Article implements Page { Xml::closeElement( 'form' ); if ( $user->isAllowed( 'editinterface' ) ) { - $dropdownTitle = Title::makeTitle( NS_MEDIAWIKI, 'Deletereason-dropdown' ); - $link = Linker::link( - $dropdownTitle, + $link = Linker::linkKnown( + $ctx->msg( 'deletereason-dropdown' )->inContentLanguage()->getTitle(), wfMessage( 'delete-edit-reasonlist' )->escaped(), array(), array( 'action' => 'edit' ) @@ -1796,8 +1827,10 @@ class Article implements Page { */ public function doDelete( $reason, $suppress = false ) { $error = ''; - $outputPage = $this->getContext()->getOutput(); - $status = $this->mPage->doDeleteArticleReal( $reason, $suppress, 0, true, $error ); + $context = $this->getContext(); + $outputPage = $context->getOutput(); + $user = $context->getUser(); + $status = $this->mPage->doDeleteArticleReal( $reason, $suppress, 0, true, $error, $user ); if ( $status->isGood() ) { $deleted = $this->getTitle()->getPrefixedText(); diff --git a/includes/page/ImagePage.php b/includes/page/ImagePage.php index 8f635cfa..9b9e3cbd 100644 --- a/includes/page/ImagePage.php +++ b/includes/page/ImagePage.php @@ -219,6 +219,9 @@ class ImagePage extends Article { } // always show the local local Filepage.css, bug 29277 $out->addModuleStyles( 'filepage' ); + + // Add MediaWiki styles for a file page + $out->addModuleStyles( 'mediawiki.action.view.filepage' ); } /** @@ -296,7 +299,7 @@ class ImagePage extends Article { } protected function openShowImage() { - global $wgEnableUploads, $wgSend404Code; + global $wgEnableUploads, $wgSend404Code, $wgSVGMaxSize; $this->loadFile(); $out = $this->getContext()->getOutput(); @@ -351,7 +354,7 @@ class ImagePage extends Article { ); $linktext = $this->getContext()->msg( 'show-big-image' )->escaped(); - $thumbSizes = $this->getThumbSizes( $width, $height, $width_orig, $height_orig ); + $thumbSizes = $this->getThumbSizes( $width_orig, $height_orig ); # Generate thumbnails or thumbnail links as needed... $otherSizes = array(); foreach ( $thumbSizes as $size ) { @@ -361,10 +364,12 @@ class ImagePage extends Article { // the current thumbnail's size ($width/$height) // since that is added to the message separately, so // it can be denoted as the current size being shown. - // Vectorized images are "infinitely" big, so all thumb - // sizes are shown. + // Vectorized images are limited by $wgSVGMaxSize big, + // so all thumbs less than or equal that are shown. if ( ( ( $size[0] <= $width_orig && $size[1] <= $height_orig ) - || $this->displayImg->isVectorized() ) + || ( $this->displayImg->isVectorized() + && max( $size[0], $size[1] ) <= $wgSVGMaxSize ) + ) && $size[0] != $width && $size[1] != $height ) { $sizeLink = $this->makeSizeLink( $params, $size[0], $size[1] ); @@ -614,8 +619,8 @@ EOT $out->wrapWikiMsg( "<div id='mw-imagepage-nofile' class='plainlinks'>\n$1\n</div>", $nofile ); if ( !$this->getID() && $wgSend404Code ) { // If there is no image, no shared image, and no description page, - // output a 404, to be consistent with articles. - $request->response()->header( 'HTTP/1.1 404 Not Found' ); + // output a 404, to be consistent with Article::showMissingArticle. + $request->response()->statusHeader( 404 ); } } $out->setFileVersion( $this->displayImg ); @@ -704,10 +709,10 @@ EOT $out->addHTML( "<ul>\n" ); # "Upload a new version of this file" link - $canUpload = $this->getTitle()->userCan( 'upload', $this->getContext()->getUser() ); + $canUpload = $this->getTitle()->quickUserCan( 'upload', $this->getContext()->getUser() ); if ( $canUpload && UploadBase::userCanReUpload( $this->getContext()->getUser(), - $this->mPage->getFile()->name ) + $this->mPage->getFile() ) ) { $ulink = Linker::makeExternalLink( $this->getUploadUrl(), @@ -1515,6 +1520,18 @@ class ImageHistoryPseudoPager extends ReverseChronologicalPager { $s = ''; $this->doQuery(); if ( count( $this->mHist ) ) { + if ( $this->mImg->isLocal() ) { + // Do a batch existence check for user pages and talkpages + $linkBatch = new LinkBatch(); + for ( $i = $this->mRange[0]; $i <= $this->mRange[1]; $i++ ) { + $file = $this->mHist[$i]; + $user = $file->getUser( 'text' ); + $linkBatch->add( NS_USER, $user ); + $linkBatch->add( NS_USER_TALK, $user ); + } + $linkBatch->execute(); + } + $list = new ImageHistoryList( $this->mImagePage ); # Generate prev/next links $navLink = $this->getNavigationBar(); diff --git a/includes/page/WikiPage.php b/includes/page/WikiPage.php index 7c789249..d1cec60b 100644 --- a/includes/page/WikiPage.php +++ b/includes/page/WikiPage.php @@ -361,18 +361,18 @@ class WikiPage implements Page, IDBAccessObject { return; } - if ( $from === self::READ_LOCKING ) { - $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle, array( 'FOR UPDATE' ) ); - } elseif ( $from === self::READ_LATEST ) { - $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle ); - } elseif ( $from === self::READ_NORMAL ) { - $data = $this->pageDataFromTitle( wfGetDB( DB_SLAVE ), $this->mTitle ); + if ( is_int( $from ) ) { + list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from ); + $data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts ); + if ( !$data + && $index == DB_SLAVE && wfGetLB()->getServerCount() > 1 && wfGetLB()->hasOrMadeRecentMasterChanges() ) { $from = self::READ_LATEST; - $data = $this->pageDataFromTitle( wfGetDB( DB_MASTER ), $this->mTitle ); + list( $index, $opts ) = DBAccessObjectUtils::getDBOptions( $from ); + $data = $this->pageDataFromTitle( wfGetDB( $index ), $this->mTitle, $opts ); } } else { // No idea from where the caller got this data, assume slave database. @@ -1082,16 +1082,13 @@ class WikiPage implements Page, IDBAccessObject { * Should the parser cache be used? * * @param ParserOptions $parserOptions ParserOptions to check - * @param int $oldid + * @param int $oldId * @return bool */ - public function isParserCacheUsed( ParserOptions $parserOptions, $oldid ) { - global $wgEnableParserCache; - - return $wgEnableParserCache - && $parserOptions->getStubThreshold() == 0 + public function shouldCheckParserCache( ParserOptions $parserOptions, $oldId ) { + return $parserOptions->getStubThreshold() == 0 && $this->exists() - && ( $oldid === null || $oldid === 0 || $oldid === $this->getLatest() ) + && ( $oldId === null || $oldId === 0 || $oldId === $this->getLatest() ) && $this->getContentHandler()->isParserCacheSupported(); } @@ -1108,10 +1105,10 @@ class WikiPage implements Page, IDBAccessObject { */ public function getParserOutput( ParserOptions $parserOptions, $oldid = null ) { - $useParserCache = $this->isParserCacheUsed( $parserOptions, $oldid ); + $useParserCache = $this->shouldCheckParserCache( $parserOptions, $oldid ); wfDebug( __METHOD__ . ': using parser cache: ' . ( $useParserCache ? 'yes' : 'no' ) . "\n" ); if ( $parserOptions->getStubThreshold() ) { - wfIncrStats( 'pcache_miss_stub' ); + wfIncrStats( 'pcache.miss.stub' ); } if ( $useParserCache ) { @@ -1143,7 +1140,12 @@ class WikiPage implements Page, IDBAccessObject { Hooks::run( 'PageViewUpdates', array( $this, $user ) ); // Update newtalk / watchlist notification status - $user->clearNotification( $this->mTitle, $oldid ); + try { + $user->clearNotification( $this->mTitle, $oldid ); + } catch ( DBError $e ) { + // Avoid outage if the master is not reachable + MWExceptionHandler::logException( $e ); + } } /** @@ -1151,28 +1153,24 @@ class WikiPage implements Page, IDBAccessObject { * @return bool */ public function doPurge() { - global $wgUseSquid; - if ( !Hooks::run( 'ArticlePurge', array( &$this ) ) ) { return false; } - // Invalidate the cache - $this->mTitle->invalidateCache(); - - if ( $wgUseSquid ) { - // Commit the transaction before the purge is sent - $dbw = wfGetDB( DB_MASTER ); - $dbw->commit( __METHOD__ ); - - // Send purge - $update = SquidUpdate::newSimplePurge( $this->mTitle ); - $update->doUpdate(); - } + $title = $this->mTitle; + wfGetDB( DB_MASTER )->onTransactionIdle( function() use ( $title ) { + global $wgUseSquid; + // Invalidate the cache in auto-commit mode + $title->invalidateCache(); + if ( $wgUseSquid ) { + // Send purge now that page_touched update was committed above + $update = SquidUpdate::newSimplePurge( $title ); + $update->doUpdate(); + } + } ); if ( $this->mTitle->getNamespace() == NS_MEDIAWIKI ) { // @todo move this logic to MessageCache - if ( $this->exists() ) { // NOTE: use transclusion text for messages. // This is consistent with MessageCache::getMsgFromNamespace() @@ -1189,6 +1187,7 @@ class WikiPage implements Page, IDBAccessObject { MessageCache::singleton()->replace( $this->mTitle->getDBkey(), $text ); } + return true; } @@ -1200,10 +1199,9 @@ class WikiPage implements Page, IDBAccessObject { * Best if all done inside a transaction. * * @param DatabaseBase $dbw - * @return int The newly created page_id key, or false if the title already existed + * @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, @@ -1224,9 +1222,11 @@ class WikiPage implements Page, IDBAccessObject { $newid = $dbw->insertId(); $this->mId = $newid; $this->mTitle->resetArticleID( $newid ); - } - return $affected ? $newid : false; + return $newid; + } else { + return false; + } } /** @@ -1265,10 +1265,9 @@ class WikiPage implements Page, IDBAccessObject { $conditions['page_latest'] = $lastRevision; } - $now = wfTimestampNow(); $row = array( /* SET */ 'page_latest' => $revision->getId(), - 'page_touched' => $dbw->timestamp( $now ), + 'page_touched' => $dbw->timestamp( $revision->getTimestamp() ), 'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0, 'page_is_redirect' => $rt !== null ? 1 : 0, 'page_len' => $len, @@ -1690,6 +1689,7 @@ class WikiPage implements Page, IDBAccessObject { * revision: The revision object for the inserted revision, or null. * * @since 1.21 + * @throws MWException */ public function doEditContent( Content $content, $summary, $flags = 0, $baseRevId = false, User $user = null, $serialFormat = null @@ -1765,7 +1765,6 @@ class WikiPage implements Page, IDBAccessObject { $dbw = wfGetDB( DB_MASTER ); $now = wfTimestampNow(); - $this->mTimestamp = $now; if ( $flags & EDIT_UPDATE ) { // Update article, but only if changed. @@ -1801,57 +1800,50 @@ class WikiPage implements Page, IDBAccessObject { if ( $changed ) { $dbw->begin( __METHOD__ ); - try { - $prepStatus = $content->prepareSave( $this, $flags, $oldid, $user ); - $status->merge( $prepStatus ); + $prepStatus = $content->prepareSave( $this, $flags, $oldid, $user ); + $status->merge( $prepStatus ); - if ( !$status->isOK() ) { - $dbw->rollback( __METHOD__ ); + if ( !$status->isOK() ) { + $dbw->rollback( __METHOD__ ); - return $status; - } - $revisionId = $revision->insertOn( $dbw ); + 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 ); + // Update page + // + // We check for conflicts by comparing $oldid with the current latest revision ID. + $ok = $this->updateRevisionOn( $dbw, $revision, $oldid, $oldIsRedirect ); - if ( !$ok ) { - // Belated edit conflict! Run away!! - $status->fatal( 'edit-conflict' ); + if ( !$ok ) { + // Belated edit conflict! Run away!! + $status->fatal( 'edit-conflict' ); - $dbw->rollback( __METHOD__ ); + $dbw->rollback( __METHOD__ ); - return $status; - } + return $status; + } - Hooks::run( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) ); - // Update recentchanges - if ( !( $flags & EDIT_SUPPRESS_RC ) ) { - // Mark as patrolled if the user can do so - $patrolled = $wgUseRCPatrol && !count( - $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); - // Add RC row to the DB - $rc = RecentChange::notifyEdit( $now, $this->mTitle, $isminor, $user, $summary, - $oldid, $this->getTimestamp(), $bot, '', $oldsize, $newsize, - $revisionId, $patrolled - ); + Hooks::run( 'NewRevisionFromEditComplete', array( $this, $revision, $baseRevId, $user ) ); - // Log auto-patrolled edits - if ( $patrolled ) { - PatrolLog::record( $rc, true, $user ); - } - } - $user->incEditCount(); - } catch ( Exception $e ) { - $dbw->rollback( __METHOD__ ); - // Question: Would it perhaps be better if this method turned all - // exceptions into $status's? - throw $e; + // Update recentchanges + if ( !( $flags & EDIT_SUPPRESS_RC ) ) { + // Mark as patrolled if the user can do so + $patrolled = $wgUseRCPatrol && !count( + $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); + // Add RC row to the DB + RecentChange::notifyEdit( + $now, $this->mTitle, $isminor, $user, $summary, + $oldid, $this->getTimestamp(), $bot, '', $oldsize, $newsize, + $revisionId, $patrolled + ); } + + $user->incEditCount(); + $dbw->commit( __METHOD__ ); + $this->mTimestamp = $now; } else { // Bug 32948: revision ID must be set to page {{REVISIONID}} and // related variables correctly @@ -1873,86 +1865,80 @@ class WikiPage implements Page, IDBAccessObject { $revision = null; // Update page_touched, this is usually implicit in the page update // Other cache updates are done in onArticleEdit() - $this->mTitle->invalidateCache(); + $this->mTitle->invalidateCache( $now ); } } else { // Create new article $status->value['new'] = true; $dbw->begin( __METHOD__ ); - try { - $prepStatus = $content->prepareSave( $this, $flags, $oldid, $user ); - $status->merge( $prepStatus ); + $prepStatus = $content->prepareSave( $this, $flags, $oldid, $user ); + $status->merge( $prepStatus ); - if ( !$status->isOK() ) { - $dbw->rollback( __METHOD__ ); + if ( !$status->isOK() ) { + $dbw->rollback( __METHOD__ ); - return $status; - } + return $status; + } - $status->merge( $prepStatus ); + $status->merge( $prepStatus ); - // Add the page record; stake our claim on this title! - // This will return false if the article already exists - $newid = $this->insertOn( $dbw ); + // Add the page record; stake our claim on this title! + // This will return false if the article already exists + $newid = $this->insertOn( $dbw ); - if ( $newid === false ) { - $dbw->rollback( __METHOD__ ); - $status->fatal( 'edit-already-exists' ); + if ( $newid === false ) { + $dbw->rollback( __METHOD__ ); + $status->fatal( 'edit-already-exists' ); - return $status; - } + return $status; + } - // Save the revision text... - $revision = new Revision( array( - 'page' => $newid, - 'title' => $this->getTitle(), // for determining the default content model - 'comment' => $summary, - 'minor_edit' => $isminor, - 'text' => $serialized, - 'len' => $newsize, - 'user' => $user->getId(), - 'user_text' => $user->getName(), - 'timestamp' => $now, - 'content_model' => $content->getModel(), - 'content_format' => $serialFormat, - ) ); - $revisionId = $revision->insertOn( $dbw ); + // Save the revision text... + $revision = new Revision( array( + 'page' => $newid, + 'title' => $this->getTitle(), // for determining the default content model + 'comment' => $summary, + 'minor_edit' => $isminor, + 'text' => $serialized, + 'len' => $newsize, + 'user' => $user->getId(), + 'user_text' => $user->getName(), + 'timestamp' => $now, + 'content_model' => $content->getModel(), + 'content_format' => $serialFormat, + ) ); + $revisionId = $revision->insertOn( $dbw ); - // Bug 37225: use accessor to get the text as Revision may trim it - $content = $revision->getContent(); // sanity; get normalized version + // Bug 37225: use accessor to get the text as Revision may trim it + $content = $revision->getContent(); // sanity; get normalized version - if ( $content ) { - $newsize = $content->getSize(); - } + if ( $content ) { + $newsize = $content->getSize(); + } - // Update the page record with revision data - $this->updateRevisionOn( $dbw, $revision, 0 ); + // Update the page record with revision data + $this->updateRevisionOn( $dbw, $revision, 0 ); - Hooks::run( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) ); + Hooks::run( 'NewRevisionFromEditComplete', array( $this, $revision, false, $user ) ); - // Update recentchanges - if ( !( $flags & EDIT_SUPPRESS_RC ) ) { - // Mark as patrolled if the user can do so - $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && !count( - $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); - // Add RC row to the DB - $rc = RecentChange::notifyNew( $now, $this->mTitle, $isminor, $user, $summary, $bot, - '', $newsize, $revisionId, $patrolled ); + // Update recentchanges + if ( !( $flags & EDIT_SUPPRESS_RC ) ) { + // Mark as patrolled if the user can do so + $patrolled = ( $wgUseRCPatrol || $wgUseNPPatrol ) && !count( + $this->mTitle->getUserPermissionsErrors( 'autopatrol', $user ) ); + // Add RC row to the DB + RecentChange::notifyNew( + $now, $this->mTitle, $isminor, $user, $summary, $bot, + '', $newsize, $revisionId, $patrolled + ); + } - // Log auto-patrolled edits - if ( $patrolled ) { - PatrolLog::record( $rc, true, $user ); - } - } - $user->incEditCount(); + $user->incEditCount(); - } catch ( Exception $e ) { - $dbw->rollback( __METHOD__ ); - throw $e; - } $dbw->commit( __METHOD__ ); + $this->mTimestamp = $now; // Update links, etc. $this->doEditUpdates( $revision, $user, array( 'created' => true ) ); @@ -1973,13 +1959,13 @@ class WikiPage implements Page, IDBAccessObject { $status->value['revision'] = $revision; $hook_args = array( &$this, &$user, $content, $summary, - $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ); + $flags & EDIT_MINOR, null, null, &$flags, $revision, &$status, $baseRevId ); ContentHandler::runLegacyHooks( 'ArticleSaveComplete', $hook_args ); Hooks::run( 'PageContentSaveComplete', $hook_args ); // Promote user to any groups they meet the criteria for - $dbw->onTransactionIdle( function () use ( $user ) { + DeferredUpdates::addCallableUpdate( function () use ( $user ) { $user->addAutopromoteOnceGroups( 'onEdit' ); $user->addAutopromoteOnceGroups( 'onView' ); // b/c } ); @@ -2079,7 +2065,7 @@ class WikiPage implements Page, IDBAccessObject { } // The edit may have already been prepared via api.php?action=stashedit - $cachedEdit = $useCache && $wgAjaxEditStash + $cachedEdit = $useCache && $wgAjaxEditStash && !$user->isAllowed( 'bot' ) ? ApiStashEdit::checkCache( $this->getTitle(), $content, $user ) : false; @@ -2164,8 +2150,6 @@ class WikiPage implements Page, IDBAccessObject { * - 'no-change': don't update the article count, ever */ public function doEditUpdates( Revision $revision, User $user, array $options = array() ) { - global $wgEnableParserCache; - $options += array( 'changed' => true, 'created' => false, @@ -2185,31 +2169,30 @@ class WikiPage implements Page, IDBAccessObject { $editInfo = $this->mPreparedEdit; } - // Save it to the parser cache - if ( $wgEnableParserCache ) { - $parserCache = ParserCache::singleton(); - $parserCache->save( - $editInfo->output, $this, $editInfo->popts, $editInfo->timestamp, $editInfo->revid - ); - } + // Save it to the parser cache. + // Make sure the cache time matches page_touched to avoid double parsing. + ParserCache::singleton()->save( + $editInfo->output, $this, $editInfo->popts, + $revision->getTimestamp(), $editInfo->revid + ); // Update the links tables and other secondary data if ( $content ) { $recursive = $options['changed']; // bug 50785 $updates = $content->getSecondaryDataUpdates( $this->getTitle(), null, $recursive, $editInfo->output ); - DataUpdate::runUpdates( $updates ); + foreach ( $updates as $update ) { + DeferredUpdates::addUpdate( $update ); + } } Hooks::run( 'ArticleEditUpdates', array( &$this, &$editInfo, $options['changed'] ) ); if ( Hooks::run( 'ArticleEditUpdatesDeleteFromRecentchanges', array( &$this ) ) ) { - JobQueueGroup::singleton()->push( array( - // Flush old entries from the `recentchanges` table - RecentChangesUpdateJob::newPurgeJob(), - // Update the cached list of active users - RecentChangesUpdateJob::newCacheUpdateJob() - ) ); + // Flush old entries from the `recentchanges` table + if ( mt_rand( 0, 9 ) == 0 ) { + JobQueueGroup::singleton()->lazyPush( RecentChangesUpdateJob::newPurgeJob() ); + } } if ( !$this->exists() ) { @@ -2276,9 +2259,8 @@ class WikiPage implements Page, IDBAccessObject { if ( $options['created'] ) { self::onArticleCreate( $this->mTitle ); } elseif ( $options['changed'] ) { // bug 50785 - self::onArticleEdit( $this->mTitle ); + self::onArticleEdit( $this->mTitle, $revision ); } - } /** @@ -2375,8 +2357,8 @@ class WikiPage implements Page, IDBAccessObject { $dbw = wfGetDB( DB_MASTER ); foreach ( $restrictionTypes as $action ) { - if ( !isset( $expiry[$action] ) ) { - $expiry[$action] = $dbw->getInfinity(); + if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) { + $expiry[$action] = 'infinity'; } if ( !isset( $limit[$action] ) ) { $limit[$action] = ''; @@ -2616,10 +2598,8 @@ class WikiPage implements Page, IDBAccessObject { */ protected function formatExpiry( $expiry ) { global $wgContLang; - $dbr = wfGetDB( DB_SLAVE ); - $encodedExpiry = $dbr->encodeExpiry( $expiry ); - if ( $encodedExpiry != 'infinity' ) { + if ( $expiry != 'infinity' ) { return wfMessage( 'protect-expiring', $wgContLang->timeanddate( $expiry, false, false ), @@ -2784,9 +2764,16 @@ class WikiPage implements Page, IDBAccessObject { $dbw->begin( __METHOD__ ); if ( $id == 0 ) { - $this->loadPageData( 'forupdate' ); + // 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 ) { + 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; @@ -2868,7 +2855,7 @@ class WikiPage implements Page, IDBAccessObject { // Clone the title, so we have the information we need when we log $logTitle = clone $this->mTitle; - // Log the deletion, if the page was suppressed, log it at Oversight instead + // Log the deletion, if the page was suppressed, put it in the suppression log instead $logtype = $suppress ? 'suppress' : 'delete'; $logEntry = new ManualLogEntry( $logtype, 'delete' ); @@ -2886,6 +2873,10 @@ class WikiPage implements Page, IDBAccessObject { $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 ); + $this->doDeleteUpdates( $id, $content ); Hooks::run( 'ArticleDeleteComplete', array( &$this, &$user, $reason, $id, $content, $logEntry ) ); @@ -2894,6 +2885,24 @@ class WikiPage implements Page, IDBAccessObject { } /** + * Lock the page row for this title and return page_latest (or 0) + * + * @return integer + */ + protected function lock() { + return (int)wfGetDB( DB_MASTER )->selectField( + 'page', + 'page_latest', + array( + 'page_namespace' => $this->getTitle()->getNamespace(), + 'page_title' => $this->getTitle()->getDBkey() + ), + __METHOD__, + array( 'FOR UPDATE' ) + ); + } + + /** * Do some database updates after deletion * * @param int $id The page_id value of the page being deleted @@ -3021,7 +3030,7 @@ class WikiPage implements Page, IDBAccessObject { ) ); } - // Get the last edit not by this guy... + // Get the last edit not by this person... // Note: these may not be public values $user = intval( $current->getUser( Revision::RAW ) ); $user_text = $dbw->addQuotes( $current->getUserText( Revision::RAW ) ); @@ -3043,29 +3052,6 @@ class WikiPage implements Page, IDBAccessObject { return array( array( 'notvisiblerev' ) ); } - // Set patrolling and bot flag on the edits, which gets rollbacked. - // This is done before the rollback edit to have patrolling also on failure (bug 62157). - $set = array(); - if ( $bot && $guser->isAllowed( 'markbotedits' ) ) { - // Mark all reverted edits as bot - $set['rc_bot'] = 1; - } - - if ( $wgUseRCPatrol ) { - // Mark all reverted edits as patrolled - $set['rc_patrolled'] = 1; - } - - if ( count( $set ) ) { - $dbw->update( 'recentchanges', $set, - array( /* WHERE */ - 'rc_cur_id' => $current->getPage(), - 'rc_user_text' => $current->getUserText(), - 'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ), - ), __METHOD__ - ); - } - // Generate the edit summary if necessary $target = Revision::newFromId( $s->rev_id, Revision::READ_LATEST ); if ( empty( $summary ) ) { @@ -3114,6 +3100,30 @@ class WikiPage implements Page, IDBAccessObject { $guser ); + // Set patrolling and bot flag on the edits, which gets rollbacked. + // This is done even on edit failure to have patrolling in that case (bug 62157). + $set = array(); + if ( $bot && $guser->isAllowed( 'markbotedits' ) ) { + // Mark all reverted edits as bot + $set['rc_bot'] = 1; + } + + if ( $wgUseRCPatrol ) { + // Mark all reverted edits as patrolled + $set['rc_patrolled'] = 1; + } + + if ( count( $set ) ) { + $dbw->update( 'recentchanges', $set, + array( /* WHERE */ + 'rc_cur_id' => $current->getPage(), + 'rc_user_text' => $current->getUserText(), + 'rc_timestamp > ' . $dbw->addQuotes( $s->rev_timestamp ), + ), + __METHOD__ + ); + } + if ( !$status->isOK() ) { return $status->getErrorsArray(); } @@ -3157,7 +3167,6 @@ class WikiPage implements Page, IDBAccessObject { // Update existence markers on article/talk tabs... $other = $title->getOtherPage(); - $other->invalidateCache(); $other->purgeSquid(); $title->touchLinks(); @@ -3174,7 +3183,6 @@ class WikiPage implements Page, IDBAccessObject { // Update existence markers on article/talk tabs... $other = $title->getOtherPage(); - $other->invalidateCache(); $other->purgeSquid(); $title->touchLinks(); @@ -3211,20 +3219,24 @@ class WikiPage implements Page, IDBAccessObject { * Purge caches on page update etc * * @param Title $title + * @param Revision|null $revision Revision that was just saved, may be null */ - public static function onArticleEdit( Title $title ) { + public static function onArticleEdit( Title $title, Revision $revision = null ) { // Invalidate caches of articles which include this page - DeferredUpdates::addHTMLCacheUpdate( $title, 'templatelinks' ); + DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'templatelinks' ) ); // Invalidate the caches of all pages which redirect here - DeferredUpdates::addHTMLCacheUpdate( $title, 'redirect' ); + DeferredUpdates::addUpdate( new HTMLCacheUpdate( $title, 'redirect' ) ); // Purge squid for this page only $title->purgeSquid(); - // Clear file cache for this page only HTMLFileCache::clearFileCache( $title ); - InfoAction::invalidateCache( $title ); + + $revid = $revision ? $revision->getId() : null; + DeferredUpdates::addCallableUpdate( function() use ( $title, $revid ) { + InfoAction::invalidateCache( $title, $revid ); + } ); } /**#@-*/ @@ -3411,13 +3423,14 @@ class WikiPage implements Page, IDBAccessObject { // Check if the last link refresh was before page_touched if ( $this->getLinksTimestamp() < $this->getTouched() ) { - JobQueueGroup::singleton()->push( EnqueueJob::newFromLocalJobs( - new JobSpecification( 'refreshLinks', $params, array(), $this->mTitle ) + $params['isOpportunistic'] = true; + $params['rootJobTimestamp'] = $parserOutput->getCacheTime(); + + JobQueueGroup::singleton()->lazyPush( EnqueueJob::newFromLocalJobs( + new JobSpecification( 'refreshLinks', $params, + array( 'removeDuplicates' => true ), $this->mTitle ) ) ); - return; } - - return; } /** |