26 use Wikimedia\Assert\Assert;
116 $ns =
$title->getNamespace();
119 throw new MWException(
"NS_MEDIA is a virtual namespace; use NS_FILE." );
120 } elseif ( $ns < 0 ) {
121 throw new MWException(
"Invalid or virtual namespace $ns given." );
153 public static function newFromID( $id, $from =
'fromdb' ) {
162 $row = $db->selectRow(
163 $pageQuery[
'tables'], $pageQuery[
'fields'], [
'page_id' => $id ], __METHOD__,
164 [], $pageQuery[
'joins']
183 public static function newFromRow( $row, $from =
'fromdb' ) {
185 $page->loadFromRow( $row, $from );
198 return self::READ_NORMAL;
200 return self::READ_LATEST;
245 $this->mDataLoaded =
false;
257 $this->mRedirectTarget =
null;
258 $this->mLastRevision =
null;
259 $this->mTouched =
'19700101000000';
260 $this->mLinksUpdated =
'19700101000000';
261 $this->mTimestamp =
'';
262 $this->mIsRedirect =
false;
263 $this->mLatest =
false;
276 $this->mPreparedEdit =
false;
300 'page_links_updated',
306 $fields[] =
'page_content_model';
310 $fields[] =
'page_lang';
329 'tables' => [
'page' ],
339 'page_links_updated',
347 $ret[
'fields'][] =
'page_content_model';
351 $ret[
'fields'][] =
'page_lang';
371 &$wikiPage, &$pageQuery[
'fields'], &$pageQuery[
'tables'], &$pageQuery[
'joins']
374 $row =
$dbr->selectRow(
375 $pageQuery[
'tables'],
376 $pageQuery[
'fields'],
383 Hooks::run(
'ArticlePageDataAfter', [ &$wikiPage, &$row ] );
399 'page_namespace' =>
$title->getNamespace(),
429 if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
434 if ( is_int( $from ) ) {
436 $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
437 $db = $loadBalancer->getConnection( $index );
442 && $loadBalancer->getServerCount() > 1
443 && $loadBalancer->hasOrMadeRecentMasterChanges()
445 $from = self::READ_LATEST;
447 $db = $loadBalancer->getConnection( $index );
453 $from = self::READ_NORMAL;
472 $lc->clearLink( $this->mTitle );
475 $lc->addGoodLinkObjFromRow( $this->mTitle, $data );
477 $this->mTitle->loadFromRow( $data );
480 $this->mTitle->loadRestrictions( $data->page_restrictions );
482 $this->mId = intval( $data->page_id );
483 $this->mTouched =
wfTimestamp( TS_MW, $data->page_touched );
485 $this->mIsRedirect = intval( $data->page_is_redirect );
486 $this->mLatest = intval( $data->page_latest );
489 if ( $this->mLastRevision && $this->mLastRevision->getId() !=
$this->mLatest ) {
490 $this->mLastRevision =
null;
491 $this->mTimestamp =
'';
494 $lc->addBadLinkObj( $this->mTitle );
496 $this->mTitle->loadFromRow(
false );
503 $this->mDataLoaded =
true;
511 if ( !$this->mDataLoaded ) {
521 if ( !$this->mDataLoaded ) {
524 return $this->mId > 0;
536 return $this->mTitle->isKnown();
545 if ( !$this->mDataLoaded ) {
566 return $cache->getWithSetCallback(
567 $cache->makeKey(
'page-content-model', $this->getLatest() ),
573 return $rev->getContentModel();
575 $title = $this->mTitle->getPrefixedDBkey();
576 wfWarn(
"Page $title exists but has no (visible) revisions!" );
577 return $this->mTitle->getContentModel();
584 return $this->mTitle->getContentModel();
592 if ( !$this->mDataLoaded ) {
595 return ( $this->mId && !$this->mIsRedirect );
603 if ( !$this->mDataLoaded ) {
614 if ( !$this->mDataLoaded ) {
625 if ( !$this->mDataLoaded ) {
637 $rev = $this->mTitle->getFirstRevision();
649 if ( $this->mLastRevision !==
null ) {
658 if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
668 } elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
672 $flags = Revision::READ_LATEST;
689 $this->mLastRevision = $revision;
699 if ( $this->mLastRevision ) {
720 if ( $this->mLastRevision ) {
721 return $this->mLastRevision->getContent( $audience,
$user );
731 if ( !$this->mTimestamp ) {
758 if ( $this->mLastRevision ) {
759 return $this->mLastRevision->getUser( $audience,
$user );
778 $userName = $revision->getUserText( $audience,
$user );
796 if ( $this->mLastRevision ) {
797 return $this->mLastRevision->getUserText( $audience,
$user );
814 if ( $this->mLastRevision ) {
815 return $this->mLastRevision->getComment( $audience,
$user );
828 if ( $this->mLastRevision ) {
829 return $this->mLastRevision->isMinor();
846 if ( !$this->mTitle->isContentPage() ) {
851 $content = $editInfo->pstContent;
856 if ( !$content || $content->isRedirect() ) {
870 $hasLinks = (bool)
count( $editInfo->output->getLinks() );
873 [
'pl_from' => $this->
getId() ], __METHOD__ );
877 return $content->isCountable( $hasLinks );
888 if ( !$this->mTitle->isRedirect() ) {
892 if ( $this->mRedirectTarget !==
null ) {
898 $row =
$dbr->selectRow(
'redirect',
899 [
'rd_namespace',
'rd_title',
'rd_fragment',
'rd_interwiki' ],
900 [
'rd_from' => $this->
getId() ],
905 if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) {
907 $row->rd_namespace, $row->rd_title,
908 $row->rd_fragment, $row->rd_interwiki
928 $retval = $content ? $content->getUltimateRedirectTarget() :
null;
953 $dbw->startAtomic( __METHOD__ );
959 'rd_from' => $this->
getId(),
976 $dbw->endAtomic( __METHOD__ );
1000 if ( $rt->isExternal() ) {
1001 if ( $rt->isLocal() ) {
1005 $source = $this->mTitle->getFullURL(
'redirect=no' );
1006 return $rt->getFullURL( [
'rdfrom' =>
$source ] );
1014 if ( $rt->isSpecialPage() ) {
1018 if ( $rt->isValidRedirectTarget() ) {
1019 return $rt->getFullURL();
1039 $actorQuery = $actorMigration->getJoin(
'rev_user' );
1041 $tables = array_merge( [
'revision' ], $actorQuery[
'tables'], [
'user' ] );
1044 'user_id' => $actorQuery[
'fields'][
'rev_user'],
1045 'user_name' => $actorQuery[
'fields'][
'rev_user_text'],
1046 'actor_id' => $actorQuery[
'fields'][
'rev_actor'],
1047 'user_real_name' =>
'MIN(user_real_name)',
1048 'timestamp' =>
'MAX(rev_timestamp)',
1051 $conds = [
'rev_page' => $this->
getId() ];
1058 $conds[] =
'NOT(' . $actorMigration->getWhere(
$dbr,
'rev_user',
$user )[
'conds'] .
')';
1061 $conds[] =
"{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0";
1064 'user' => [
'LEFT JOIN', $actorQuery[
'fields'][
'rev_user'] .
' = user_id' ],
1065 ] + $actorQuery[
'joins'];
1068 'GROUP BY' => [ $fields[
'user_id'], $fields[
'user_name'] ],
1069 'ORDER BY' =>
'timestamp DESC',
1086 && ( $oldId ===
null || $oldId === 0 || $oldId === $this->
getLatest() )
1104 ParserOptions $parserOptions, $oldid =
null, $forceParse =
false
1109 if ( $useParserCache && !$parserOptions->
isSafeToCache() ) {
1110 throw new InvalidArgumentException(
1111 'The supplied ParserOptions are not safe to cache. Fix the options or set $forceParse = true.'
1116 ': using parser cache: ' . ( $useParserCache ?
'yes' :
'no' ) .
"\n" );
1121 if ( $useParserCache ) {
1122 $parserOutput = MediaWikiServices::getInstance()->getParserCache()
1123 ->get( $this, $parserOptions );
1124 if ( $parserOutput !==
false ) {
1125 return $parserOutput;
1129 if ( $oldid ===
null || $oldid === 0 ) {
1136 return $pool->getParserOutput();
1152 function ()
use (
$user, $oldid ) {
1155 $user->clearNotification( $this->mTitle, $oldid );
1171 if ( !
Hooks::run(
'ArticlePurge', [ &$wikiPage ] ) ) {
1175 $this->mTitle->invalidateCache();
1187 $messageCache->updateMessageOverride( $this->mTitle, $this->
getContent() );
1208 $pageIdForInsert = $pageId ? [
'page_id' => $pageId ] : [];
1212 'page_namespace' => $this->mTitle->getNamespace(),
1213 'page_title' => $this->mTitle->getDBkey(),
1214 'page_restrictions' =>
'',
1215 'page_is_redirect' => 0,
1218 'page_touched' => $dbw->timestamp(),
1221 ] + $pageIdForInsert,
1226 if ( $dbw->affectedRows() > 0 ) {
1227 $newid = $pageId ? (int)$pageId : $dbw->insertId();
1228 $this->mId = $newid;
1229 $this->mTitle->resetArticleID( $newid );
1251 $lastRevIsRedirect =
null
1256 if ( (
int)$revision->getId() === 0 ) {
1257 throw new InvalidArgumentException(
1258 __METHOD__ .
': Revision has ID ' . var_export( $revision->getId(), 1 )
1262 $content = $revision->getContent();
1263 $len = $content ? $content->getSize() : 0;
1264 $rt = $content ? $content->getUltimateRedirectTarget() :
null;
1266 $conditions = [
'page_id' => $this->
getId() ];
1268 if ( !is_null( $lastRevision ) ) {
1270 $conditions[
'page_latest'] = $lastRevision;
1273 $revId = $revision->getId();
1274 Assert::parameter( $revId > 0,
'$revision->getId()',
'must be > 0' );
1277 'page_latest' => $revId,
1278 'page_touched' => $dbw->timestamp( $revision->getTimestamp() ),
1279 'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0,
1280 'page_is_redirect' => $rt !==
null ? 1 : 0,
1285 $row[
'page_content_model'] = $revision->getContentModel();
1288 $dbw->update(
'page',
1293 $result = $dbw->affectedRows() > 0;
1297 $this->mLatest = $revision->getId();
1298 $this->mIsRedirect = (bool)$rt;
1306 $revision->getContentModel()
1328 $isRedirect = !is_null( $redirectTitle );
1330 if ( !$isRedirect && $lastRevIsRedirect ===
false ) {
1334 if ( $isRedirect ) {
1338 $where = [
'rd_from' => $this->
getId() ];
1339 $dbw->delete(
'redirect', $where, __METHOD__ );
1346 return ( $dbw->affectedRows() != 0 );
1360 $row = $dbw->selectRow(
1361 [
'revision',
'page' ],
1362 [
'rev_id',
'rev_timestamp',
'page_is_redirect' ],
1364 'page_id' => $this->
getId(),
1365 'page_latest=rev_id' ],
1369 if (
wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) {
1372 $prev = $row->rev_id;
1373 $lastRevIsRedirect = (bool)$row->page_is_redirect;
1377 $lastRevIsRedirect =
null;
1429 $sectionId,
Content $sectionContent, $sectionTitle =
'', $edittime =
null
1432 if ( $edittime && $sectionId !==
'new' ) {
1433 $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
1440 && $lb->getServerCount() > 1
1441 && $lb->hasOrMadeRecentMasterChanges()
1447 $baseRevId =
$rev->getId();
1451 return $this->
replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId );
1468 $sectionTitle =
'', $baseRevId =
null
1470 if ( strval( $sectionId ) ===
'' ) {
1472 $newContent = $sectionContent;
1475 throw new MWException(
"sections not supported for content model " .
1480 if ( is_null( $baseRevId ) || $sectionId ===
'new' ) {
1485 wfDebug( __METHOD__ .
" asked for bogus section (page: " .
1486 $this->
getId() .
"; section: $sectionId)\n" );
1490 $oldContent =
$rev->getContent();
1493 if ( !$oldContent ) {
1494 wfDebug( __METHOD__ .
": no page text\n" );
1498 $newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle );
1580 Content $content, $summary, $flags = 0, $baseRevId =
false,
1581 User $user =
null, $serialFormat =
null, $tags = [], $undidRevId = 0
1586 if ( $tags ===
null ) {
1591 if ( $this->mTitle->getText() ===
'' ) {
1592 throw new MWException(
'Something is trying to edit an article with an empty title' );
1598 $this->mTitle->getPrefixedText()
1615 $hook_args = [ &$wikiPage, &
$user, &$content, &$summary,
1616 $flags &
EDIT_MINOR,
null,
null, &$flags, &$hookStatus ];
1618 if ( !
Hooks::run(
'PageContentSave', $hook_args ) ) {
1619 if ( $hookStatus->isOK() ) {
1621 $hookStatus->fatal(
'edit-hook-aborted' );
1630 $handler = $content->getContentHandler();
1631 $tag =
$handler->getChangeTag( $old_content, $content, $flags );
1639 $tags[] =
'mw-undo';
1644 $summary =
$handler->getAutosummary( $old_content, $content, $flags );
1656 $pstContent = $editInfo->pstContent;
1659 'minor' => ( $flags &
EDIT_MINOR ) &&
$user->isAllowed(
'minoredit' ),
1660 'serialized' => $pstContent->serialize( $serialFormat ),
1661 'serialFormat' => $serialFormat,
1662 'baseRevId' => $baseRevId,
1663 'oldRevision' => $old_revision,
1664 'oldContent' => $old_content,
1668 'tags' => ( $tags !== null ) ? (
array)$tags : [],
1669 'undidRevId' => $undidRevId
1681 $user->addAutopromoteOnceGroups(
'onEdit' );
1682 $user->addAutopromoteOnceGroups(
'onView' );
1710 $oldid = $meta[
'oldId'];
1712 $oldContent = $meta[
'oldContent'];
1713 $newsize = $content->
getSize();
1717 $status->fatal(
'edit-gone-missing' );
1720 } elseif ( !$oldContent ) {
1722 throw new MWException(
"Could not find text for current revision {$oldid}." );
1725 $changed = !$content->
equals( $oldContent );
1732 'page' => $this->
getId(),
1733 'title' => $this->mTitle,
1734 'comment' => $summary,
1735 'minor_edit' => $meta[
'minor'],
1736 'text' => $meta[
'serialized'],
1738 'parent_id' => $oldid,
1739 'user' =>
$user->getId(),
1740 'user_text' =>
$user->getName(),
1741 'timestamp' => $now,
1742 'content_model' => $content->
getModel(),
1743 'content_format' => $meta[
'serialFormat'],
1747 $status->merge( $prepStatus );
1752 $dbw->startAtomic( __METHOD__ );
1757 if ( $latestNow != $oldid ) {
1758 $dbw->endAtomic( __METHOD__ );
1760 $status->fatal(
'edit-conflict' );
1771 $revisionId = $revision->insertOn( $dbw );
1773 if ( !$this->
updateRevisionOn( $dbw, $revision,
null, $meta[
'oldIsRedirect'] ) ) {
1774 throw new MWException(
"Failed to update page row to use new revision." );
1777 $tags = $meta[
'tags'];
1779 [ $this, $revision, $meta[
'baseRevId'],
$user, &$tags ] );
1785 $this->mTitle->getUserPermissionsErrors(
'autopatrol',
$user ) );
1790 $revision->isMinor(),
1797 $oldContent ? $oldContent->getSize() : 0,
1806 $user->incEditCount();
1808 $dbw->endAtomic( __METHOD__ );
1809 $this->mTimestamp = $now;
1815 $revision = $meta[
'oldRevision'];
1820 $status->value[
'revision'] = $revision;
1822 $status->warning(
'edit-no-change' );
1825 $this->mTitle->invalidateCache( $now );
1834 $revision, &
$user, $content, $summary, &$flags,
1842 'changed' => $changed,
1843 'oldcountable' => $meta[
'oldCountable'],
1844 'oldrevision' => $meta[
'oldRevision']
1851 null,
null, &$flags, $revision, &
$status, $meta[
'baseRevId'],
1852 $meta[
'undidRevId'] ];
1882 $newsize = $content->
getSize();
1883 $prepStatus = $content->
prepareSave( $this, $flags, $meta[
'oldId'],
$user );
1884 $status->merge( $prepStatus );
1890 $dbw->startAtomic( __METHOD__ );
1894 if ( $newid ===
false ) {
1895 $dbw->endAtomic( __METHOD__ );
1896 $status->fatal(
'edit-already-exists' );
1909 'title' => $this->mTitle,
1910 'comment' => $summary,
1911 'minor_edit' => $meta[
'minor'],
1912 'text' => $meta[
'serialized'],
1914 'user' =>
$user->getId(),
1915 'user_text' =>
$user->getName(),
1916 'timestamp' => $now,
1917 'content_model' => $content->
getModel(),
1918 'content_format' => $meta[
'serialFormat'],
1922 $revisionId = $revision->
insertOn( $dbw );
1925 throw new MWException(
"Failed to update page row to use new revision." );
1928 Hooks::run(
'NewRevisionFromEditComplete', [ $this, $revision,
false,
$user ] );
1934 !
count( $this->mTitle->getUserPermissionsErrors(
'autopatrol',
$user ) );
1939 $revision->isMinor(),
1951 $user->incEditCount();
1953 $dbw->endAtomic( __METHOD__ );
1954 $this->mTimestamp = $now;
1957 $status->value[
'revision'] = $revision;
1965 $revision, &
$user, $content, $summary, &$flags, $meta, &
$status
1973 $flags &
EDIT_MINOR,
null,
null, &$flags, $revision ];
2003 if ( $this->
getTitle()->isConversionTable() ) {
2006 $options->disableContentConversion();
2031 $serialFormat =
null, $useCache =
true
2035 if ( is_object( $revision ) ) {
2036 $revid = $revision->getId();
2041 if ( $revid !==
null ) {
2042 wfDeprecated( __METHOD__ .
' with $revision = revision ID',
'1.25' );
2053 if ( $serialFormat ===
null ) {
2057 if ( $this->mPreparedEdit
2058 && isset( $this->mPreparedEdit->newContent )
2059 && $this->mPreparedEdit->newContent->equals( $content )
2060 && $this->mPreparedEdit->revid == $revid
2061 && $this->mPreparedEdit->format == $serialFormat
2074 Hooks::run(
'ArticlePrepareTextForEdit', [ $this, $popts ] );
2077 if ( $cachedEdit ) {
2078 $edit->timestamp = $cachedEdit->timestamp;
2083 $edit->revid = $revid;
2085 if ( $cachedEdit ) {
2086 $edit->pstContent = $cachedEdit->pstContent;
2088 $edit->pstContent = $content
2093 $edit->format = $serialFormat;
2095 if ( $cachedEdit ) {
2096 $edit->output = $cachedEdit->output;
2103 $oldCallback = $edit->popts->getCurrentRevisionCallback();
2104 $edit->popts->setCurrentRevisionCallback(
2106 if (
$title->equals( $revision->getTitle() ) ) {
2115 $dbIndex = ( $this->mDataLoadedFrom & self::READ_LATEST ) === self::READ_LATEST
2119 $edit->popts->setSpeculativeRevIdCallback(
function ()
use ( $dbIndex ) {
2120 return 1 + (int)
wfGetDB( $dbIndex )->selectField(
2128 $edit->output = $edit->pstContent
2129 ? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts )
2133 $edit->newContent = $content;
2136 if ( $edit->output ) {
2141 $this->mPreparedEdit = $edit;
2174 'restored' =>
false,
2175 'oldrevision' =>
null,
2176 'oldcountable' => null
2180 $logger = LoggerFactory::getInstance(
'SaveParse' );
2184 if ( !$this->mPreparedEdit ) {
2185 $logger->debug( __METHOD__ .
": No prepared edit...\n" );
2186 } elseif ( $this->mPreparedEdit->output->getFlag(
'vary-revision' ) ) {
2187 $logger->info( __METHOD__ .
": Prepared edit has vary-revision...\n" );
2188 } elseif ( $this->mPreparedEdit->output->getFlag(
'vary-revision-id' )
2189 && $this->mPreparedEdit->output->getSpeculativeRevIdUsed() !== $revision->
getId()
2191 $logger->info( __METHOD__ .
": Prepared edit has vary-revision-id with wrong ID...\n" );
2192 } elseif ( $this->mPreparedEdit->output->getFlag(
'vary-user' ) && !
$options[
'changed'] ) {
2193 $logger->info( __METHOD__ .
": Prepared edit has vary-user and is null...\n" );
2195 wfDebug( __METHOD__ .
": Using prepared edit...\n" );
2208 MediaWikiServices::getInstance()->getParserCache()->save(
2209 $editInfo->output, $this, $editInfo->popts,
2216 $updates = $content->getSecondaryDataUpdates(
2217 $this->
getTitle(),
null, $recursive, $editInfo->output
2219 foreach ( $updates
as $update ) {
2220 $update->setCause(
'edit-page',
$user->getName() );
2222 $update->setRevision( $revision );
2223 $update->setTriggeringUser(
$user );
2238 'pageId' => $this->
getId(),
2250 if (
Hooks::run(
'ArticleEditUpdatesDeleteFromRecentchanges', [ &$wikiPage ] ) ) {
2252 if ( mt_rand( 0, 9 ) == 0 ) {
2257 if ( !$this->
exists() ) {
2261 $id = $this->
getId();
2262 $title = $this->mTitle->getPrefixedDBkey();
2263 $shortTitle = $this->mTitle->getDBkey();
2265 if (
$options[
'oldcountable'] ===
'no-change' ||
2271 } elseif (
$options[
'oldcountable'] !==
null ) {
2276 $edits =
$options[
'changed'] ? 1 : 0;
2277 $pages =
$options[
'created'] ? 1 : 0;
2280 [
'edits' => $edits,
'articles' => $good,
'pages' => $pages ]
2289 && $shortTitle !=
$user->getTitleKey()
2290 && !( $revision->
isMinor() &&
$user->isAllowed(
'nominornewtalk' ) )
2293 if ( !$recipient ) {
2294 wfDebug( __METHOD__ .
": invalid username\n" );
2301 if (
Hooks::run(
'ArticleEditUpdateNewTalk', [ &$wikiPage, $recipient ] ) ) {
2304 $recipient->setNewtalk(
true, $revision );
2305 } elseif ( $recipient->isLoggedIn() ) {
2306 $recipient->setNewtalk(
true, $revision );
2308 wfDebug( __METHOD__ .
": don't need to notify a nonexistent user\n" );
2344 &$cascade, $reason,
User $user, $tags =
null
2353 $restrictionTypes = $this->mTitle->getRestrictionTypes();
2354 $id = $this->
getId();
2365 $isProtected =
false;
2371 foreach ( $restrictionTypes
as $action ) {
2372 if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2373 $expiry[$action] =
'infinity';
2375 if ( !isset( $limit[$action] ) ) {
2376 $limit[$action] =
'';
2377 } elseif ( $limit[$action] !=
'' ) {
2382 $current = implode(
'', $this->mTitle->getRestrictions( $action ) );
2383 if ( $current !=
'' ) {
2384 $isProtected =
true;
2387 if ( $limit[$action] != $current ) {
2389 } elseif ( $limit[$action] !=
'' ) {
2393 if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2399 if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2409 $revCommentMsg =
'unprotectedarticle-comment';
2410 $logAction =
'unprotect';
2411 } elseif ( $isProtected ) {
2412 $revCommentMsg =
'modifiedarticleprotection-comment';
2413 $logAction =
'modify';
2415 $revCommentMsg =
'protectedarticle-comment';
2416 $logAction =
'protect';
2419 $logRelationsValues = [];
2420 $logRelationsField =
null;
2421 $logParamsDetails = [];
2424 $nullRevision =
null;
2430 if ( !
Hooks::run(
'ArticleProtect', [ &$wikiPage, &
$user, $limit, $reason ] ) ) {
2435 $editrestriction = isset( $limit[
'edit'] )
2436 ? [ $limit[
'edit'] ]
2437 : $this->mTitle->getRestrictions(
'edit' );
2438 foreach ( array_keys( $editrestriction,
'sysop' )
as $key ) {
2439 $editrestriction[$key] =
'editprotected';
2441 foreach ( array_keys( $editrestriction,
'autoconfirmed' )
as $key ) {
2442 $editrestriction[$key] =
'editsemiprotected';
2446 foreach ( array_keys( $cascadingRestrictionLevels,
'sysop' )
as $key ) {
2447 $cascadingRestrictionLevels[$key] =
'editprotected';
2449 foreach ( array_keys( $cascadingRestrictionLevels,
'autoconfirmed' )
as $key ) {
2450 $cascadingRestrictionLevels[$key] =
'editsemiprotected';
2454 if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2469 if ( $nullRevision ===
null ) {
2470 return Status::newFatal(
'no-null-revision', $this->mTitle->getPrefixedText() );
2473 $logRelationsField =
'pr_id';
2476 foreach ( $limit
as $action => $restrictions ) {
2478 'page_restrictions',
2481 'pr_type' => $action
2485 if ( $restrictions !=
'' ) {
2486 $cascadeValue = ( $cascade && $action ==
'edit' ) ? 1 : 0;
2488 'page_restrictions',
2491 'pr_type' => $action,
2492 'pr_level' => $restrictions,
2493 'pr_cascade' => $cascadeValue,
2494 'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2498 $logRelationsValues[] = $dbw->insertId();
2499 $logParamsDetails[] = [
2501 'level' => $restrictions,
2502 'expiry' => $expiry[$action],
2503 'cascade' => (bool)$cascadeValue,
2511 [
'page_restrictions' =>
'' ],
2512 [
'page_id' => $id ],
2520 [ $this, $nullRevision, $latest,
$user ] );
2521 Hooks::run(
'ArticleProtectComplete', [ &$wikiPage, &
$user, $limit, $reason ] );
2526 if ( $limit[
'create'] !=
'' ) {
2528 $dbw->replace(
'protected_titles',
2529 [ [
'pt_namespace',
'pt_title' ] ],
2531 'pt_namespace' => $this->mTitle->getNamespace(),
2532 'pt_title' => $this->mTitle->getDBkey(),
2533 'pt_create_perm' => $limit[
'create'],
2534 'pt_timestamp' => $dbw->timestamp(),
2535 'pt_expiry' => $dbw->encodeExpiry( $expiry[
'create'] ),
2536 'pt_user' =>
$user->getId(),
2537 ] + $commentFields, __METHOD__
2539 $logParamsDetails[] = [
2541 'level' => $limit[
'create'],
2542 'expiry' => $expiry[
'create'],
2545 $dbw->delete(
'protected_titles',
2547 'pt_namespace' => $this->mTitle->getNamespace(),
2548 'pt_title' => $this->mTitle->getDBkey()
2554 $this->mTitle->flushRestrictions();
2557 if ( $logAction ==
'unprotect' ) {
2562 '4::description' => $protectDescriptionLog,
2563 '5:bool:cascade' => $cascade,
2564 'details' => $logParamsDetails,
2570 $logEntry->setTarget( $this->mTitle );
2571 $logEntry->setComment( $reason );
2572 $logEntry->setPerformer(
$user );
2573 $logEntry->setParameters(
$params );
2574 if ( !is_null( $nullRevision ) ) {
2575 $logEntry->setAssociatedRevId( $nullRevision->getId() );
2577 $logEntry->setTags( $tags );
2578 if ( $logRelationsField !==
null &&
count( $logRelationsValues ) ) {
2579 $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2581 $logId = $logEntry->insert();
2582 $logEntry->publish( $logId );
2599 array $expiry, $cascade, $reason,
$user =
null
2606 $this->mTitle->getPrefixedText(),
2608 )->inContentLanguage()->text();
2610 $editComment .=
wfMessage(
'colon-separator' )->inContentLanguage()->text() . $reason;
2613 if ( $protectDescription ) {
2614 $editComment .=
wfMessage(
'word-separator' )->inContentLanguage()->text();
2615 $editComment .=
wfMessage(
'parentheses' )->params( $protectDescription )
2616 ->inContentLanguage()->text();
2619 $editComment .=
wfMessage(
'word-separator' )->inContentLanguage()->text();
2620 $editComment .=
wfMessage(
'brackets' )->params(
2621 wfMessage(
'protect-summary-cascade' )->inContentLanguage()->
text()
2622 )->inContentLanguage()->text();
2627 $nullRev->insertOn( $dbw );
2630 $oldLatest = $nullRev->getParentId();
2644 if ( $expiry !=
'infinity' ) {
2647 $wgContLang->timeanddate( $expiry,
false,
false ),
2650 )->inContentLanguage()->text();
2652 return wfMessage(
'protect-expiry-indefinite' )
2653 ->inContentLanguage()->text();
2665 $protectDescription =
'';
2667 foreach ( array_filter( $limit )
as $action => $restrictions ) {
2668 # $action is one of $wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ].
2669 # All possible message keys are listed here for easier grepping:
2670 # * restriction-create
2671 # * restriction-edit
2672 # * restriction-move
2673 # * restriction-upload
2674 $actionText =
wfMessage(
'restriction-' . $action )->inContentLanguage()->text();
2675 # $restrictions is one of $wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ],
2676 # with '' filtered out. All possible message keys are listed below:
2677 # * protect-level-autoconfirmed
2678 # * protect-level-sysop
2679 $restrictionsText =
wfMessage(
'protect-level-' . $restrictions )
2680 ->inContentLanguage()->text();
2684 if ( $protectDescription !==
'' ) {
2685 $protectDescription .=
wfMessage(
'word-separator' )->inContentLanguage()->text();
2687 $protectDescription .=
wfMessage(
'protect-summary-desc' )
2688 ->params( $actionText, $restrictionsText, $expiryText )
2689 ->inContentLanguage()->text();
2692 return $protectDescription;
2709 $protectDescriptionLog =
'';
2711 foreach ( array_filter( $limit )
as $action => $restrictions ) {
2713 $protectDescriptionLog .=
$wgContLang->getDirMark() .
2714 "[$action=$restrictions] ($expiryText)";
2717 return trim( $protectDescriptionLog );
2730 if ( !is_array( $limit ) ) {
2731 throw new MWException( __METHOD__ .
' given non-array restriction set' );
2737 foreach ( array_filter( $limit )
as $action => $restrictions ) {
2738 $bits[] =
"$action=$restrictions";
2741 return implode(
':', $bits );
2761 $reason, $suppress =
false, $u1 =
null, $u2 =
null, &$error =
'',
User $user =
null
2787 $reason, $suppress =
false, $u1 =
null, $u2 =
null, &$error =
'',
User $deleter =
null,
2788 $tags = [], $logsubtype =
'delete'
2797 if ( $this->mTitle->getDBkey() ===
'' ) {
2798 $status->error(
'cannotdelete',
2806 $deleter = is_null( $deleter ) ?
$wgUser : $deleter;
2808 [ &$wikiPage, &$deleter, &$reason, &$error, &
$status, $suppress ]
2812 $status->fatal(
'delete-hook-aborted' );
2818 $dbw->startAtomic( __METHOD__ );
2821 $id = $this->
getId();
2827 if ( $id == 0 || $this->
getLatest() != $lockedLatest ) {
2828 $dbw->endAtomic( __METHOD__ );
2830 $status->error(
'cannotdelete',
2836 $namespace = $this->
getTitle()->getNamespace();
2837 $dbKey = $this->
getTitle()->getDBkey();
2848 }
catch ( Exception $ex ) {
2849 wfLogWarning( __METHOD__ .
': failed to load content during deletion! '
2850 . $ex->getMessage() );
2878 $res = $dbw->select(
2881 [
'revision',
'revision_comment_temp',
'revision_actor_temp' ]
2884 [
'rev_page' => $id ],
2889 foreach (
$res as $row ) {
2894 $res = $dbw->select(
2897 [
'rev_page' => $id ],
2910 foreach (
$res as $row ) {
2911 $comment = $commentStore->getComment(
'rev_comment', $row );
2914 'ar_namespace' => $namespace,
2915 'ar_title' => $dbKey,
2916 'ar_timestamp' => $row->rev_timestamp,
2917 'ar_minor_edit' => $row->rev_minor_edit,
2918 'ar_rev_id' => $row->rev_id,
2919 'ar_parent_id' => $row->rev_parent_id,
2920 'ar_text_id' => $row->rev_text_id,
2921 'ar_len' => $row->rev_len,
2922 'ar_page_id' => $id,
2923 'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
2924 'ar_sha1' => $row->rev_sha1,
2925 ] + $commentStore->insert( $dbw,
'ar_comment', $comment )
2926 + $actorMigration->getInsertValues( $dbw,
'ar_user',
$user );
2928 $rowInsert[
'ar_content_model'] = $row->rev_content_model;
2929 $rowInsert[
'ar_content_format'] = $row->rev_content_format;
2931 $rowsInsert[] = $rowInsert;
2932 $revids[] = $row->rev_id;
2936 if ( (
int)$row->rev_user === 0 &&
IP::isValid( $row->rev_user_text ) ) {
2937 $ipRevIds[] = $row->rev_id;
2941 $dbw->insert(
'archive', $rowsInsert, __METHOD__ );
2943 $archivedRevisionCount = $dbw->affectedRows();
2948 $wikiPageBeforeDelete = clone $this;
2951 $dbw->delete(
'page', [
'page_id' => $id ], __METHOD__ );
2952 $dbw->delete(
'revision', [
'rev_page' => $id ], __METHOD__ );
2954 $dbw->delete(
'revision_comment_temp', [
'revcomment_rev' => $revids ], __METHOD__ );
2957 $dbw->delete(
'revision_actor_temp', [
'revactor_rev' => $revids ], __METHOD__ );
2961 if (
count( $ipRevIds ) > 0 ) {
2962 $dbw->delete(
'ip_changes', [
'ipc_rev_id' => $ipRevIds ], __METHOD__ );
2966 $logtype = $suppress ?
'suppress' :
'delete';
2969 $logEntry->setPerformer( $deleter );
2970 $logEntry->setTarget( $logTitle );
2971 $logEntry->setComment( $reason );
2972 $logEntry->setTags( $tags );
2973 $logid = $logEntry->insert();
2975 $dbw->onTransactionPreCommitOrIdle(
2976 function ()
use ( $dbw, $logEntry, $logid ) {
2978 $logEntry->publish( $logid );
2983 $dbw->endAtomic( __METHOD__ );
2988 &$wikiPageBeforeDelete,
2994 $archivedRevisionCount
2999 $cache = MediaWikiServices::getInstance()->getMainObjectStash();
3000 $key =
$cache->makeKey(
'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
3001 $cache->set( $key, 1, $cache::TTL_DAY );
3017 'page_id' => $this->
getId(),
3020 'page_namespace' => $this->
getTitle()->getNamespace(),
3021 'page_title' => $this->
getTitle()->getDBkey()
3043 }
catch ( Exception $ex ) {
3051 [
'edits' => 1,
'articles' => -$countable,
'pages' => -1 ]
3056 foreach ( $updates
as $update ) {
3060 $causeAgent =
$user ?
$user->getName() :
'unknown';
3063 $this->mTitle,
'templatelinks',
'delete-page', $causeAgent );
3065 if ( $this->mTitle->getNamespace() ==
NS_FILE ) {
3067 $this->mTitle,
'imagelinks',
'delete-page', $causeAgent );
3073 $this->mTitle, $revision,
null,
wfWikiID()
3113 $fromP, $summary, $token, $bot, &$resultDetails,
User $user, $tags =
null
3115 $resultDetails =
null;
3118 $editErrors = $this->mTitle->getUserPermissionsErrors(
'edit',
$user );
3119 $rollbackErrors = $this->mTitle->getUserPermissionsErrors(
'rollback',
$user );
3120 $errors = array_merge( $editErrors,
wfArrayDiff2( $rollbackErrors, $editErrors ) );
3122 if ( !
$user->matchEditToken( $token,
'rollback' ) ) {
3123 $errors[] = [
'sessionfailure' ];
3126 if (
$user->pingLimiter(
'rollback' ) ||
$user->pingLimiter() ) {
3127 $errors[] = [
'actionthrottledtext' ];
3131 if ( !empty( $errors ) ) {
3159 &$resultDetails,
User $guser, $tags =
null
3166 return [ [
'readonlytext' ] ];
3171 if ( is_null( $current ) ) {
3173 return [ [
'notanarticle' ] ];
3176 $from = str_replace(
'_',
' ', $fromP );
3179 if ( $from != $current->getUserText() ) {
3180 $resultDetails = [
'current' => $current ];
3181 return [ [
'alreadyrolled',
3182 htmlspecialchars( $this->mTitle->getPrefixedText() ),
3183 htmlspecialchars( $fromP ),
3184 htmlspecialchars( $current->getUserText() )
3194 $user->setName( $userName );
3201 $s = $dbw->selectRow(
3202 [
'revision' ] + $actorWhere[
'tables'],
3203 [
'rev_id',
'rev_timestamp',
'rev_deleted' ],
3205 'rev_page' => $current->getPage(),
3206 'NOT(' . $actorWhere[
'conds'] .
')',
3210 'USE INDEX' => [
'revision' =>
'page_timestamp' ],
3211 'ORDER BY' =>
'rev_timestamp DESC'
3213 $actorWhere[
'joins']
3215 if (
$s ===
false ) {
3217 return [ [
'cantrollback' ] ];
3222 return [ [
'notvisiblerev' ] ];
3227 if ( empty( $summary ) ) {
3228 if ( $from ==
'' ) {
3229 $summary =
wfMessage(
'revertpage-nouser' );
3237 $target->getUserText(), $from,
$s->rev_id,
3239 $current->getId(),
$wgContLang->timeanddate( $current->getTimestamp() )
3241 if ( $summary instanceof Message ) {
3242 $summary = $summary->params(
$args )->inContentLanguage()->text();
3248 $summary = trim( $summary );
3253 if ( $guser->
isAllowed(
'minoredit' ) ) {
3257 if ( $bot && ( $guser->
isAllowedAny(
'markbotedits',
'bot' ) ) ) {
3261 $targetContent = $target->getContent();
3262 $changingContentModel = $targetContent->getModel() !== $current->getContentModel();
3265 $tags[] =
'mw-rollback';
3282 if ( $bot && $guser->
isAllowed(
'markbotedits' ) ) {
3292 if (
count( $set ) ) {
3294 $dbw->update(
'recentchanges', $set,
3296 'rc_cur_id' => $current->getPage(),
3297 'rc_timestamp > ' . $dbw->addQuotes(
$s->rev_timestamp ),
3298 $actorWhere[
'conds'],
3305 return $status->getErrorsArray();
3309 $statusRev = isset(
$status->value[
'revision'] )
3312 if ( !( $statusRev instanceof
Revision ) ) {
3313 $resultDetails = [
'current' => $current ];
3314 return [ [
'alreadyrolled',
3315 htmlspecialchars( $this->mTitle->getPrefixedText() ),
3316 htmlspecialchars( $fromP ),
3317 htmlspecialchars( $current->getUserText() )
3321 if ( $changingContentModel ) {
3325 $log->setPerformer( $guser );
3326 $log->setTarget( $this->mTitle );
3327 $log->setComment( $summary );
3328 $log->setParameters( [
3329 '4::oldmodel' => $current->getContentModel(),
3330 '5::newmodel' => $targetContent->getModel(),
3333 $logId = $log->insert( $dbw );
3334 $log->publish( $logId );
3337 $revId = $statusRev->getId();
3339 Hooks::run(
'ArticleRollbackComplete', [ $this, $guser, $target, $current ] );
3342 'summary' => $summary,
3343 'current' => $current,
3344 'target' => $target,
3365 $other =
$title->getOtherPage();
3367 $other->purgeSquid();
3371 $title->deleteTitleProtection();
3373 MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle(
$title );
3398 $other =
$title->getOtherPage();
3400 $other->purgeSquid();
3405 MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle(
$title );
3427 $user->setNewtalk(
false );
3452 MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle(
$title );
3459 $revid = $revision ? $revision->getId() :
null;
3474 $id = $this->
getId();
3480 $res =
$dbr->select(
'categorylinks',
3481 [
'cl_to AS page_title, ' .
NS_CATEGORY .
' AS page_namespace' ],
3484 [
'cl_from' => $id ],
3498 $id = $this->
getId();
3505 $res =
$dbr->select( [
'categorylinks',
'page_props',
'page' ],
3507 [
'cl_from' => $id,
'pp_page=page_id',
'pp_propname' =>
'hiddencat',
3508 'page_namespace' =>
NS_CATEGORY,
'page_title=cl_to' ],
3511 if (
$res !==
false ) {
3512 foreach (
$res as $row ) {
3542 $id = $id ?: $this->
getId();
3543 $ns = $this->
getTitle()->getNamespace();
3545 $addFields = [
'cat_pages = cat_pages + 1' ];
3546 $removeFields = [
'cat_pages = cat_pages - 1' ];
3548 $addFields[] =
'cat_subcats = cat_subcats + 1';
3549 $removeFields[] =
'cat_subcats = cat_subcats - 1';
3551 $addFields[] =
'cat_files = cat_files + 1';
3552 $removeFields[] =
'cat_files = cat_files - 1';
3557 if (
count( $added ) ) {
3558 $existingAdded = $dbw->selectFieldValues(
3561 [
'cat_title' => $added ],
3568 if (
count( $existingAdded ) ) {
3572 [
'cat_title' => $existingAdded ],
3577 $missingAdded = array_diff( $added, $existingAdded );
3578 if (
count( $missingAdded ) ) {
3580 foreach ( $missingAdded
as $cat ) {
3582 'cat_title' => $cat,
3585 'cat_files' => ( $ns ==
NS_FILE ) ? 1 : 0,
3598 if (
count( $deleted ) ) {
3602 [
'cat_title' => $deleted ],
3607 foreach ( $added
as $catName ) {
3609 Hooks::run(
'CategoryAfterPageAdded', [ $cat, $this ] );
3612 foreach ( $deleted
as $catName ) {
3614 Hooks::run(
'CategoryAfterPageRemoved', [ $cat, $this, $id ] );
3620 if (
count( $deleted ) ) {
3621 $rows = $dbw->select(
3623 [
'cat_id',
'cat_title',
'cat_pages',
'cat_subcats',
'cat_files' ],
3624 [
'cat_title' => $deleted,
'cat_pages <= 0' ],
3631 $cat->refreshCounts();
3648 if ( !
Hooks::run(
'OpportunisticLinksUpdate',
3649 [ $this, $this->mTitle, $parserOutput ]
3657 'isOpportunistic' =>
true,
3658 'rootJobTimestamp' => $parserOutput->getCacheTime()
3661 if ( $this->mTitle->areRestrictionsCascading() ) {
3666 } elseif ( !$config->get(
'MiserMode' ) && $parserOutput->hasDynamicContent() ) {
3677 $key =
$cache->makeKey(
'dynamic-linksupdate',
'last', $this->
getId() );
3678 $ttl = max( $parserOutput->getCacheExpiry(), 3600 );
3679 if (
$cache->add( $key, time(), $ttl ) ) {
3703 }
catch ( Exception $ex ) {
3714 $updates = $content->getDeletionUpdates( $this );
3717 Hooks::run(
'WikiPageDeletionUpdates', [ $this, $content, &$updates ] );
3755 return $this->
getTitle()->getCanonicalURL();
3764 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3766 return $linkCache->getMutableCacheKeys(
$cache, $this->
getTitle()->getTitleValue() );