122 $ns =
$title->getNamespace();
125 throw new MWException(
"NS_MEDIA is a virtual namespace; use NS_FILE." );
126 } elseif ( $ns < 0 ) {
127 throw new MWException(
"Invalid or virtual namespace $ns given." );
159 public static function newFromID( $id, $from =
'fromdb' ) {
167 $row = $db->selectRow(
168 'page', self::selectFields(), [
'page_id' => $id ], __METHOD__ );
186 public static function newFromRow( $row, $from =
'fromdb' ) {
188 $page->loadFromRow( $row, $from );
201 return self::READ_NORMAL;
203 return self::READ_LATEST;
248 $this->mDataLoaded =
false;
260 $this->mRedirectTarget =
null;
261 $this->mLastRevision =
null;
262 $this->mTouched =
'19700101000000';
263 $this->mLinksUpdated =
'19700101000000';
264 $this->mTimestamp =
'';
265 $this->mIsRedirect =
false;
266 $this->mLatest =
false;
279 $this->mPreparedEdit =
false;
300 'page_links_updated',
306 $fields[] =
'page_content_model';
310 $fields[] =
'page_lang';
329 Hooks::run(
'ArticlePageDataBefore', [ &$wikiPage, &$fields ] );
331 $row =
$dbr->selectRow(
'page', $fields, $conditions, __METHOD__,
$options );
333 Hooks::run(
'ArticlePageDataAfter', [ &$wikiPage, &$row ] );
349 'page_namespace' =>
$title->getNamespace(),
379 if ( is_int( $from ) && $from <= $this->mDataLoadedFrom ) {
384 if ( is_int( $from ) ) {
387 $loadBalancer = MediaWikiServices::getInstance()->getDBLoadBalancer();
391 && $loadBalancer->getServerCount() > 1
392 && $loadBalancer->hasOrMadeRecentMasterChanges()
394 $from = self::READ_LATEST;
401 $from = self::READ_NORMAL;
420 $lc->clearLink( $this->mTitle );
423 $lc->addGoodLinkObjFromRow( $this->mTitle, $data );
425 $this->mTitle->loadFromRow( $data );
428 $this->mTitle->loadRestrictions( $data->page_restrictions );
430 $this->mId = intval( $data->page_id );
431 $this->mTouched =
wfTimestamp( TS_MW, $data->page_touched );
433 $this->mIsRedirect = intval( $data->page_is_redirect );
434 $this->mLatest = intval( $data->page_latest );
437 if ( $this->mLastRevision && $this->mLastRevision->getId() !=
$this->mLatest ) {
438 $this->mLastRevision =
null;
439 $this->mTimestamp =
'';
442 $lc->addBadLinkObj( $this->mTitle );
444 $this->mTitle->loadFromRow(
false );
451 $this->mDataLoaded =
true;
459 if ( !$this->mDataLoaded ) {
469 if ( !$this->mDataLoaded ) {
472 return $this->mId > 0;
484 return $this->mTitle->isKnown();
493 if ( !$this->mDataLoaded ) {
514 return $cache->getWithSetCallback(
515 $cache->makeKey(
'page',
'content-model', $this->getLatest() ),
521 return $rev->getContentModel();
523 $title = $this->mTitle->getPrefixedDBkey();
524 wfWarn(
"Page $title exists but has no (visible) revisions!" );
525 return $this->mTitle->getContentModel();
532 return $this->mTitle->getContentModel();
540 if ( !$this->mDataLoaded ) {
543 return ( $this->mId && !$this->mIsRedirect );
551 if ( !$this->mDataLoaded ) {
562 if ( !$this->mDataLoaded ) {
573 if ( !$this->mDataLoaded ) {
585 $rev = $this->mTitle->getFirstRevision();
597 if ( $this->mLastRevision !==
null ) {
606 if ( $this->mDataLoadedFrom == self::READ_LOCKING ) {
616 } elseif ( $this->mDataLoadedFrom == self::READ_LATEST ) {
620 $flags = Revision::READ_LATEST;
637 $this->mLastRevision = $revision;
647 if ( $this->mLastRevision ) {
668 if ( $this->mLastRevision ) {
669 return $this->mLastRevision->getContent( $audience,
$user );
679 if ( !$this->mTimestamp ) {
706 if ( $this->mLastRevision ) {
707 return $this->mLastRevision->getUser( $audience,
$user );
726 $userName = $revision->getUserText( $audience,
$user );
744 if ( $this->mLastRevision ) {
745 return $this->mLastRevision->getUserText( $audience,
$user );
762 if ( $this->mLastRevision ) {
763 return $this->mLastRevision->getComment( $audience,
$user );
776 if ( $this->mLastRevision ) {
777 return $this->mLastRevision->isMinor();
794 if ( !$this->mTitle->isContentPage() ) {
799 $content = $editInfo->pstContent;
804 if ( !$content || $content->isRedirect() ) {
818 $hasLinks = (bool)
count( $editInfo->output->getLinks() );
821 [
'pl_from' => $this->
getId() ], __METHOD__ );
825 return $content->isCountable( $hasLinks );
836 if ( !$this->mTitle->isRedirect() ) {
840 if ( $this->mRedirectTarget !==
null ) {
846 $row =
$dbr->selectRow(
'redirect',
847 [
'rd_namespace',
'rd_title',
'rd_fragment',
'rd_interwiki' ],
848 [
'rd_from' => $this->
getId() ],
853 if ( $row && !is_null( $row->rd_fragment ) && !is_null( $row->rd_interwiki ) ) {
855 $row->rd_namespace, $row->rd_title,
856 $row->rd_fragment, $row->rd_interwiki
876 $retval = $content ? $content->getUltimateRedirectTarget() :
null;
901 $dbw->startAtomic( __METHOD__ );
907 'rd_from' => $this->
getId(),
924 $dbw->endAtomic( __METHOD__ );
948 if ( $rt->isExternal() ) {
949 if ( $rt->isLocal() ) {
953 $source = $this->mTitle->getFullURL(
'redirect=no' );
954 return $rt->getFullURL( [
'rdfrom' =>
$source ] );
962 if ( $rt->isSpecialPage() ) {
966 if ( $rt->isValidRedirectTarget() ) {
967 return $rt->getFullURL();
986 if (
$dbr->implicitGroupby() ) {
987 $realNameField =
'user_real_name';
989 $realNameField =
'MIN(user_real_name) AS user_real_name';
992 $tables = [
'revision',
'user' ];
995 'user_id' =>
'rev_user',
996 'user_name' =>
'rev_user_text',
998 'timestamp' =>
'MAX(rev_timestamp)',
1001 $conds = [
'rev_page' => $this->
getId() ];
1007 $conds[] =
"rev_user != $user";
1009 $conds[] =
"rev_user_text != {$dbr->addQuotes( $this->getUserText() )}";
1013 $conds[] =
"{$dbr->bitAnd( 'rev_deleted', Revision::DELETED_USER )} = 0";
1016 'user' => [
'LEFT JOIN',
'rev_user = user_id' ],
1020 'GROUP BY' => [
'rev_user',
'rev_user_text' ],
1021 'ORDER BY' =>
'timestamp DESC',
1038 && ( $oldId ===
null || $oldId === 0 || $oldId === $this->
getLatest() )
1056 ParserOptions $parserOptions, $oldid =
null, $forceParse =
false
1061 if ( $useParserCache && !$parserOptions->
isSafeToCache() ) {
1062 throw new InvalidArgumentException(
1063 'The supplied ParserOptions are not safe to cache. Fix the options or set $forceParse = true.'
1068 ': using parser cache: ' . ( $useParserCache ?
'yes' :
'no' ) .
"\n" );
1073 if ( $useParserCache ) {
1074 $parserOutput = MediaWikiServices::getInstance()->getParserCache()
1075 ->get( $this, $parserOptions );
1076 if ( $parserOutput !==
false ) {
1077 return $parserOutput;
1081 if ( $oldid ===
null || $oldid === 0 ) {
1088 return $pool->getParserOutput();
1104 $user->clearNotification( $this->mTitle, $oldid );
1121 if ( !
Hooks::run(
'ArticlePurge', [ &$wikiPage ] ) ) {
1125 $this->mTitle->invalidateCache();
1137 $messageCache->updateMessageOverride( $this->mTitle, $this->
getContent() );
1170 $pageIdForInsert = $pageId ? [
'page_id' => $pageId ] : [];
1174 'page_namespace' => $this->mTitle->getNamespace(),
1175 'page_title' => $this->mTitle->getDBkey(),
1176 'page_restrictions' =>
'',
1177 'page_is_redirect' => 0,
1180 'page_touched' => $dbw->timestamp(),
1183 ] + $pageIdForInsert,
1188 if ( $dbw->affectedRows() > 0 ) {
1189 $newid = $pageId ? (int)$pageId : $dbw->insertId();
1190 $this->mId = $newid;
1191 $this->mTitle->resetArticleID( $newid );
1213 $lastRevIsRedirect =
null
1218 if ( (
int)$revision->getId() === 0 ) {
1219 throw new InvalidArgumentException(
1220 __METHOD__ .
': Revision has ID ' . var_export( $revision->getId(), 1 )
1224 $content = $revision->getContent();
1225 $len = $content ? $content->getSize() : 0;
1226 $rt = $content ? $content->getUltimateRedirectTarget() :
null;
1228 $conditions = [
'page_id' => $this->
getId() ];
1230 if ( !is_null( $lastRevision ) ) {
1232 $conditions[
'page_latest'] = $lastRevision;
1236 'page_latest' => $revision->getId(),
1237 'page_touched' => $dbw->timestamp( $revision->getTimestamp() ),
1238 'page_is_new' => ( $lastRevision === 0 ) ? 1 : 0,
1239 'page_is_redirect' => $rt !==
null ? 1 : 0,
1244 $row[
'page_content_model'] = $revision->getContentModel();
1247 $dbw->update(
'page',
1252 $result = $dbw->affectedRows() > 0;
1256 $this->mLatest = $revision->getId();
1257 $this->mIsRedirect = (bool)$rt;
1265 $revision->getContentModel()
1287 $isRedirect = !is_null( $redirectTitle );
1289 if ( !$isRedirect && $lastRevIsRedirect ===
false ) {
1293 if ( $isRedirect ) {
1297 $where = [
'rd_from' => $this->
getId() ];
1298 $dbw->delete(
'redirect', $where, __METHOD__ );
1305 return ( $dbw->affectedRows() != 0 );
1319 $row = $dbw->selectRow(
1320 [
'revision',
'page' ],
1321 [
'rev_id',
'rev_timestamp',
'page_is_redirect' ],
1323 'page_id' => $this->
getId(),
1324 'page_latest=rev_id' ],
1328 if (
wfTimestamp( TS_MW, $row->rev_timestamp ) >= $revision->getTimestamp() ) {
1331 $prev = $row->rev_id;
1332 $lastRevIsRedirect = (bool)$row->page_is_redirect;
1336 $lastRevIsRedirect =
null;
1388 $sectionId,
Content $sectionContent, $sectionTitle =
'', $edittime =
null
1391 if ( $edittime && $sectionId !==
'new' ) {
1398 &&
wfGetLB()->getServerCount() > 1
1399 &&
wfGetLB()->hasOrMadeRecentMasterChanges()
1405 $baseRevId =
$rev->getId();
1409 return $this->
replaceSectionAtRev( $sectionId, $sectionContent, $sectionTitle, $baseRevId );
1426 $sectionTitle =
'', $baseRevId =
null
1428 if ( strval( $sectionId ) ===
'' ) {
1430 $newContent = $sectionContent;
1433 throw new MWException(
"sections not supported for content model " .
1438 if ( is_null( $baseRevId ) || $sectionId ===
'new' ) {
1443 wfDebug( __METHOD__ .
" asked for bogus section (page: " .
1444 $this->
getId() .
"; section: $sectionId)\n" );
1448 $oldContent =
$rev->getContent();
1451 if ( !$oldContent ) {
1452 wfDebug( __METHOD__ .
": no page text\n" );
1456 $newContent = $oldContent->replaceSection( $sectionId, $sectionContent, $sectionTitle );
1539 User $user =
null, $serialFormat =
null, $tags = [], $undidRevId = 0
1544 if ( $tags ===
null ) {
1549 if ( $this->mTitle->getText() ===
'' ) {
1550 throw new MWException(
'Something is trying to edit an article with an empty title' );
1556 $this->mTitle->getPrefixedText()
1573 $hook_args = [ &$wikiPage, &
$user, &$content, &$summary,
1576 if ( !
Hooks::run(
'PageContentSave', $hook_args ) ) {
1577 if ( $hookStatus->isOK() ) {
1579 $hookStatus->fatal(
'edit-hook-aborted' );
1588 if ( $old_content && $old_content->getModel() !== $content->getModel() ) {
1589 $tags[] =
'mw-contentmodelchange';
1594 $handler = $content->getContentHandler();
1595 $summary =
$handler->getAutosummary( $old_content, $content,
$flags );
1607 $pstContent = $editInfo->pstContent;
1611 'serialized' => $pstContent->serialize( $serialFormat ),
1612 'serialFormat' => $serialFormat,
1613 'baseRevId' => $baseRevId,
1614 'oldRevision' => $old_revision,
1615 'oldContent' => $old_content,
1619 'tags' => ( $tags !== null ) ? (
array)$tags : [],
1620 'undidRevId' => $undidRevId
1632 $user->addAutopromoteOnceGroups(
'onEdit' );
1633 $user->addAutopromoteOnceGroups(
'onView' );
1661 $oldid = $meta[
'oldId'];
1663 $oldContent = $meta[
'oldContent'];
1664 $newsize = $content->
getSize();
1668 $status->fatal(
'edit-gone-missing' );
1671 } elseif ( !$oldContent ) {
1673 throw new MWException(
"Could not find text for current revision {$oldid}." );
1678 'page' => $this->
getId(),
1679 'title' => $this->mTitle,
1680 'comment' => $summary,
1681 'minor_edit' => $meta[
'minor'],
1682 'text' => $meta[
'serialized'],
1684 'parent_id' => $oldid,
1685 'user' =>
$user->getId(),
1686 'user_text' =>
$user->getName(),
1687 'timestamp' => $now,
1688 'content_model' => $content->
getModel(),
1689 'content_format' => $meta[
'serialFormat'],
1692 $changed = !$content->
equals( $oldContent );
1698 $status->merge( $prepStatus );
1703 $dbw->startAtomic( __METHOD__ );
1708 if ( $latestNow != $oldid ) {
1709 $dbw->endAtomic( __METHOD__ );
1711 $status->fatal(
'edit-conflict' );
1722 $revisionId = $revision->insertOn( $dbw );
1724 if ( !$this->
updateRevisionOn( $dbw, $revision,
null, $meta[
'oldIsRedirect'] ) ) {
1725 throw new MWException(
"Failed to update page row to use new revision." );
1729 [ $this, $revision, $meta[
'baseRevId'],
$user ] );
1735 $this->mTitle->getUserPermissionsErrors(
'autopatrol',
$user ) );
1740 $revision->isMinor(),
1747 $oldContent ? $oldContent->getSize() : 0,
1755 $user->incEditCount();
1757 $dbw->endAtomic( __METHOD__ );
1758 $this->mTimestamp = $now;
1763 $revision->setUserIdAndName(
1771 $status->value[
'revision'] = $revision;
1773 $status->warning(
'edit-no-change' );
1776 $this->mTitle->invalidateCache( $now );
1793 'changed' => $changed,
1794 'oldcountable' => $meta[
'oldCountable'],
1795 'oldrevision' => $meta[
'oldRevision']
1802 null,
null, &
$flags, $revision, &
$status, $meta[
'baseRevId'],
1803 $meta[
'undidRevId'] ];
1833 $newsize = $content->
getSize();
1835 $status->merge( $prepStatus );
1841 $dbw->startAtomic( __METHOD__ );
1845 if ( $newid ===
false ) {
1846 $dbw->endAtomic( __METHOD__ );
1847 $status->fatal(
'edit-already-exists' );
1860 'title' => $this->mTitle,
1861 'comment' => $summary,
1862 'minor_edit' => $meta[
'minor'],
1863 'text' => $meta[
'serialized'],
1865 'user' =>
$user->getId(),
1866 'user_text' =>
$user->getName(),
1867 'timestamp' => $now,
1868 'content_model' => $content->
getModel(),
1869 'content_format' => $meta[
'serialFormat'],
1873 $revisionId = $revision->
insertOn( $dbw );
1876 throw new MWException(
"Failed to update page row to use new revision." );
1879 Hooks::run(
'NewRevisionFromEditComplete', [ $this, $revision,
false,
$user ] );
1885 !
count( $this->mTitle->getUserPermissionsErrors(
'autopatrol',
$user ) );
1890 $revision->isMinor(),
1902 $user->incEditCount();
1904 $dbw->endAtomic( __METHOD__ );
1905 $this->mTimestamp = $now;
1908 $status->value[
'revision'] = $revision;
1954 if ( $this->
getTitle()->isConversionTable() ) {
1957 $options->disableContentConversion();
1982 $serialFormat =
null, $useCache =
true
1986 if ( is_object( $revision ) ) {
1987 $revid = $revision->getId();
1992 if ( $revid !==
null ) {
1993 wfDeprecated( __METHOD__ .
' with $revision = revision ID',
'1.25' );
2004 if ( $serialFormat ===
null ) {
2008 if ( $this->mPreparedEdit
2009 && isset( $this->mPreparedEdit->newContent )
2010 && $this->mPreparedEdit->newContent->equals( $content )
2011 && $this->mPreparedEdit->revid == $revid
2012 && $this->mPreparedEdit->format == $serialFormat
2025 Hooks::run(
'ArticlePrepareTextForEdit', [ $this, $popts ] );
2028 if ( $cachedEdit ) {
2029 $edit->timestamp = $cachedEdit->timestamp;
2034 $edit->revid = $revid;
2036 if ( $cachedEdit ) {
2037 $edit->pstContent = $cachedEdit->pstContent;
2039 $edit->pstContent = $content
2044 $edit->format = $serialFormat;
2046 if ( $cachedEdit ) {
2047 $edit->output = $cachedEdit->output;
2054 $oldCallback = $edit->popts->getCurrentRevisionCallback();
2055 $edit->popts->setCurrentRevisionCallback(
2057 if (
$title->equals( $revision->getTitle() ) ) {
2066 $dbIndex = ( $this->mDataLoadedFrom & self::READ_LATEST ) === self::READ_LATEST
2070 $edit->popts->setSpeculativeRevIdCallback(
function ()
use ( $dbIndex ) {
2071 return 1 + (int)
wfGetDB( $dbIndex )->selectField(
2079 $edit->output = $edit->pstContent
2080 ? $edit->pstContent->getParserOutput( $this->mTitle, $revid, $edit->popts )
2084 $edit->newContent = $content;
2088 $edit->newText = $edit->newContent
2091 $edit->oldText = $edit->oldContent
2094 $edit->pst = $edit->pstContent ? $edit->pstContent->serialize( $serialFormat ) :
'';
2096 if ( $edit->output ) {
2101 $this->mPreparedEdit = $edit;
2134 'restored' =>
false,
2135 'oldrevision' =>
null,
2136 'oldcountable' => null
2140 $logger = LoggerFactory::getInstance(
'SaveParse' );
2144 if ( !$this->mPreparedEdit ) {
2145 $logger->debug( __METHOD__ .
": No prepared edit...\n" );
2146 } elseif ( $this->mPreparedEdit->output->getFlag(
'vary-revision' ) ) {
2147 $logger->info( __METHOD__ .
": Prepared edit has vary-revision...\n" );
2148 } elseif ( $this->mPreparedEdit->output->getFlag(
'vary-revision-id' )
2149 && $this->mPreparedEdit->output->getSpeculativeRevIdUsed() !== $revision->
getId()
2151 $logger->info( __METHOD__ .
": Prepared edit has vary-revision-id with wrong ID...\n" );
2152 } elseif ( $this->mPreparedEdit->output->getFlag(
'vary-user' ) && !
$options[
'changed'] ) {
2153 $logger->info( __METHOD__ .
": Prepared edit has vary-user and is null...\n" );
2155 wfDebug( __METHOD__ .
": Using prepared edit...\n" );
2168 MediaWikiServices::getInstance()->getParserCache()->save(
2169 $editInfo->output, $this, $editInfo->popts,
2176 $updates = $content->getSecondaryDataUpdates(
2177 $this->
getTitle(),
null, $recursive, $editInfo->output
2179 foreach ( $updates
as $update ) {
2181 $update->setRevision( $revision );
2182 $update->setTriggeringUser(
$user );
2197 'pageId' => $this->
getId(),
2209 if (
Hooks::run(
'ArticleEditUpdatesDeleteFromRecentchanges', [ &$wikiPage ] ) ) {
2211 if ( mt_rand( 0, 9 ) == 0 ) {
2216 if ( !$this->
exists() ) {
2220 $id = $this->
getId();
2221 $title = $this->mTitle->getPrefixedDBkey();
2222 $shortTitle = $this->mTitle->getDBkey();
2224 if (
$options[
'oldcountable'] ===
'no-change' ||
2230 } elseif (
$options[
'oldcountable'] !==
null ) {
2235 $edits =
$options[
'changed'] ? 1 : 0;
2236 $total =
$options[
'created'] ? 1 : 0;
2246 && $shortTitle !=
$user->getTitleKey()
2247 && !( $revision->
isMinor() &&
$user->isAllowed(
'nominornewtalk' ) )
2250 if ( !$recipient ) {
2251 wfDebug( __METHOD__ .
": invalid username\n" );
2258 if (
Hooks::run(
'ArticleEditUpdateNewTalk', [ &$wikiPage, $recipient ] ) ) {
2261 $recipient->setNewtalk(
true, $revision );
2262 } elseif ( $recipient->isLoggedIn() ) {
2263 $recipient->setNewtalk(
true, $revision );
2265 wfDebug( __METHOD__ .
": don't need to notify a nonexistent user\n" );
2301 &$cascade, $reason,
User $user, $tags =
null
2310 $restrictionTypes = $this->mTitle->getRestrictionTypes();
2311 $id = $this->
getId();
2322 $isProtected =
false;
2328 foreach ( $restrictionTypes
as $action ) {
2329 if ( !isset( $expiry[$action] ) || $expiry[$action] === $dbw->getInfinity() ) {
2330 $expiry[$action] =
'infinity';
2332 if ( !isset( $limit[$action] ) ) {
2333 $limit[$action] =
'';
2334 } elseif ( $limit[$action] !=
'' ) {
2339 $current = implode(
'', $this->mTitle->getRestrictions( $action ) );
2340 if ( $current !=
'' ) {
2341 $isProtected =
true;
2344 if ( $limit[$action] != $current ) {
2346 } elseif ( $limit[$action] !=
'' ) {
2350 if ( $this->mTitle->getRestrictionExpiry( $action ) != $expiry[$action] ) {
2356 if ( !$changed && $protect && $this->mTitle->areRestrictionsCascading() != $cascade ) {
2366 $revCommentMsg =
'unprotectedarticle-comment';
2367 $logAction =
'unprotect';
2368 } elseif ( $isProtected ) {
2369 $revCommentMsg =
'modifiedarticleprotection-comment';
2370 $logAction =
'modify';
2372 $revCommentMsg =
'protectedarticle-comment';
2373 $logAction =
'protect';
2376 $logRelationsValues = [];
2377 $logRelationsField =
null;
2378 $logParamsDetails = [];
2381 $nullRevision =
null;
2387 if ( !
Hooks::run(
'ArticleProtect', [ &$wikiPage, &
$user, $limit, $reason ] ) ) {
2392 $editrestriction = isset( $limit[
'edit'] )
2393 ? [ $limit[
'edit'] ]
2394 : $this->mTitle->getRestrictions(
'edit' );
2395 foreach ( array_keys( $editrestriction,
'sysop' )
as $key ) {
2396 $editrestriction[$key] =
'editprotected';
2398 foreach ( array_keys( $editrestriction,
'autoconfirmed' )
as $key ) {
2399 $editrestriction[$key] =
'editsemiprotected';
2403 foreach ( array_keys( $cascadingRestrictionLevels,
'sysop' )
as $key ) {
2404 $cascadingRestrictionLevels[$key] =
'editprotected';
2406 foreach ( array_keys( $cascadingRestrictionLevels,
'autoconfirmed' )
as $key ) {
2407 $cascadingRestrictionLevels[$key] =
'editsemiprotected';
2411 if ( !array_intersect( $editrestriction, $cascadingRestrictionLevels ) ) {
2426 if ( $nullRevision ===
null ) {
2427 return Status::newFatal(
'no-null-revision', $this->mTitle->getPrefixedText() );
2430 $logRelationsField =
'pr_id';
2433 foreach ( $limit
as $action => $restrictions ) {
2435 'page_restrictions',
2438 'pr_type' => $action
2442 if ( $restrictions !=
'' ) {
2443 $cascadeValue = ( $cascade && $action ==
'edit' ) ? 1 : 0;
2445 'page_restrictions',
2448 'pr_type' => $action,
2449 'pr_level' => $restrictions,
2450 'pr_cascade' => $cascadeValue,
2451 'pr_expiry' => $dbw->encodeExpiry( $expiry[$action] )
2455 $logRelationsValues[] = $dbw->insertId();
2456 $logParamsDetails[] = [
2458 'level' => $restrictions,
2459 'expiry' => $expiry[$action],
2460 'cascade' => (bool)$cascadeValue,
2468 [
'page_restrictions' =>
'' ],
2469 [
'page_id' => $id ],
2477 [ $this, $nullRevision, $latest,
$user ] );
2478 Hooks::run(
'ArticleProtectComplete', [ &$wikiPage, &
$user, $limit, $reason ] );
2483 if ( $limit[
'create'] !=
'' ) {
2485 $dbw->replace(
'protected_titles',
2486 [ [
'pt_namespace',
'pt_title' ] ],
2488 'pt_namespace' => $this->mTitle->getNamespace(),
2489 'pt_title' => $this->mTitle->getDBkey(),
2490 'pt_create_perm' => $limit[
'create'],
2491 'pt_timestamp' => $dbw->timestamp(),
2492 'pt_expiry' => $dbw->encodeExpiry( $expiry[
'create'] ),
2493 'pt_user' =>
$user->getId(),
2494 ] + $commentFields, __METHOD__
2496 $logParamsDetails[] = [
2498 'level' => $limit[
'create'],
2499 'expiry' => $expiry[
'create'],
2502 $dbw->delete(
'protected_titles',
2504 'pt_namespace' => $this->mTitle->getNamespace(),
2505 'pt_title' => $this->mTitle->getDBkey()
2511 $this->mTitle->flushRestrictions();
2514 if ( $logAction ==
'unprotect' ) {
2519 '4::description' => $protectDescriptionLog,
2520 '5:bool:cascade' => $cascade,
2521 'details' => $logParamsDetails,
2527 $logEntry->setTarget( $this->mTitle );
2528 $logEntry->setComment( $reason );
2529 $logEntry->setPerformer(
$user );
2530 $logEntry->setParameters(
$params );
2531 if ( !is_null( $nullRevision ) ) {
2532 $logEntry->setAssociatedRevId( $nullRevision->getId() );
2534 $logEntry->setTags( $tags );
2535 if ( $logRelationsField !==
null &&
count( $logRelationsValues ) ) {
2536 $logEntry->setRelations( [ $logRelationsField => $logRelationsValues ] );
2538 $logId = $logEntry->insert();
2539 $logEntry->publish( $logId );
2556 array $expiry, $cascade, $reason,
$user =
null
2563 $this->mTitle->getPrefixedText(),
2565 )->inContentLanguage()->text();
2567 $editComment .=
wfMessage(
'colon-separator' )->inContentLanguage()->text() . $reason;
2570 if ( $protectDescription ) {
2571 $editComment .=
wfMessage(
'word-separator' )->inContentLanguage()->text();
2572 $editComment .=
wfMessage(
'parentheses' )->params( $protectDescription )
2573 ->inContentLanguage()->text();
2576 $editComment .=
wfMessage(
'word-separator' )->inContentLanguage()->text();
2577 $editComment .=
wfMessage(
'brackets' )->params(
2578 wfMessage(
'protect-summary-cascade' )->inContentLanguage()->
text()
2579 )->inContentLanguage()->text();
2584 $nullRev->insertOn( $dbw );
2587 $oldLatest = $nullRev->getParentId();
2601 if ( $expiry !=
'infinity' ) {
2604 $wgContLang->timeanddate( $expiry,
false,
false ),
2607 )->inContentLanguage()->text();
2609 return wfMessage(
'protect-expiry-indefinite' )
2610 ->inContentLanguage()->text();
2622 $protectDescription =
'';
2624 foreach ( array_filter( $limit )
as $action => $restrictions ) {
2625 # $action is one of $wgRestrictionTypes = [ 'create', 'edit', 'move', 'upload' ].
2626 # All possible message keys are listed here for easier grepping:
2627 # * restriction-create
2628 # * restriction-edit
2629 # * restriction-move
2630 # * restriction-upload
2631 $actionText =
wfMessage(
'restriction-' . $action )->inContentLanguage()->text();
2632 # $restrictions is one of $wgRestrictionLevels = [ '', 'autoconfirmed', 'sysop' ],
2633 # with '' filtered out. All possible message keys are listed below:
2634 # * protect-level-autoconfirmed
2635 # * protect-level-sysop
2636 $restrictionsText =
wfMessage(
'protect-level-' . $restrictions )
2637 ->inContentLanguage()->text();
2641 if ( $protectDescription !==
'' ) {
2642 $protectDescription .=
wfMessage(
'word-separator' )->inContentLanguage()->text();
2644 $protectDescription .=
wfMessage(
'protect-summary-desc' )
2645 ->params( $actionText, $restrictionsText, $expiryText )
2646 ->inContentLanguage()->text();
2649 return $protectDescription;
2666 $protectDescriptionLog =
'';
2668 foreach ( array_filter( $limit )
as $action => $restrictions ) {
2670 $protectDescriptionLog .=
$wgContLang->getDirMark() .
2671 "[$action=$restrictions] ($expiryText)";
2674 return trim( $protectDescriptionLog );
2687 if ( !is_array( $limit ) ) {
2688 throw new MWException( __METHOD__ .
' given non-array restriction set' );
2694 foreach ( array_filter( $limit )
as $action => $restrictions ) {
2695 $bits[] =
"$action=$restrictions";
2698 return implode(
':', $bits );
2718 $reason, $suppress =
false, $u1 =
null, $u2 =
null, &$error =
'',
User $user =
null
2744 $reason, $suppress =
false, $u1 =
null, $u2 =
null, &$error =
'',
User $user =
null,
2745 $tags = [], $logsubtype =
'delete'
2753 if ( $this->mTitle->getDBkey() ===
'' ) {
2754 $status->error(
'cannotdelete',
2764 [ &$wikiPage, &
$user, &$reason, &$error, &
$status, $suppress ]
2768 $status->fatal(
'delete-hook-aborted' );
2774 $dbw->startAtomic( __METHOD__ );
2777 $id = $this->
getId();
2783 if ( $id == 0 || $this->
getLatest() != $lockedLatest ) {
2784 $dbw->endAtomic( __METHOD__ );
2786 $status->error(
'cannotdelete',
2792 $namespace = $this->
getTitle()->getNamespace();
2793 $dbKey = $this->
getTitle()->getDBkey();
2804 }
catch ( Exception $ex ) {
2805 wfLogWarning( __METHOD__ .
': failed to load content during deletion! '
2806 . $ex->getMessage() );
2820 $fields = array_diff( $fields, [
'rev_deleted' ] );
2831 $commentQuery = $revCommentStore->getJoin();
2832 $res = $dbw->select(
2833 [
'revision' ] + $commentQuery[
'tables'],
2834 $fields + $commentQuery[
'fields'],
2835 [
'rev_page' => $id ],
2838 $commentQuery[
'joins']
2848 foreach (
$res as $row ) {
2849 $comment = $revCommentStore->getComment( $row );
2851 'ar_namespace' => $namespace,
2852 'ar_title' => $dbKey,
2853 'ar_user' => $row->rev_user,
2854 'ar_user_text' => $row->rev_user_text,
2855 'ar_timestamp' => $row->rev_timestamp,
2856 'ar_minor_edit' => $row->rev_minor_edit,
2857 'ar_rev_id' => $row->rev_id,
2858 'ar_parent_id' => $row->rev_parent_id,
2859 'ar_text_id' => $row->rev_text_id,
2862 'ar_len' => $row->rev_len,
2863 'ar_page_id' => $id,
2864 'ar_deleted' => $suppress ? $bitfield : $row->rev_deleted,
2865 'ar_sha1' => $row->rev_sha1,
2866 ] + $arCommentStore->insert( $dbw, $comment );
2868 $rowInsert[
'ar_content_model'] = $row->rev_content_model;
2869 $rowInsert[
'ar_content_format'] = $row->rev_content_format;
2871 $rowsInsert[] = $rowInsert;
2872 $revids[] = $row->rev_id;
2876 if ( (
int)$row->rev_user === 0 &&
IP::isValid( $row->rev_user_text ) ) {
2877 $ipRevIds[] = $row->rev_id;
2881 $dbw->insert(
'archive', $rowsInsert, __METHOD__ );
2883 $archivedRevisionCount = $dbw->affectedRows();
2888 $wikiPageBeforeDelete = clone $this;
2891 $dbw->delete(
'page', [
'page_id' => $id ], __METHOD__ );
2892 $dbw->delete(
'revision', [
'rev_page' => $id ], __METHOD__ );
2894 $dbw->delete(
'revision_comment_temp', [
'revcomment_rev' => $revids ], __METHOD__ );
2898 if (
count( $ipRevIds ) > 0 ) {
2899 $dbw->delete(
'ip_changes', [
'ipc_rev_id' => $ipRevIds ], __METHOD__ );
2903 $logtype = $suppress ?
'suppress' :
'delete';
2906 $logEntry->setPerformer(
$user );
2907 $logEntry->setTarget( $logTitle );
2908 $logEntry->setComment( $reason );
2909 $logEntry->setTags( $tags );
2910 $logid = $logEntry->insert();
2912 $dbw->onTransactionPreCommitOrIdle(
2913 function ()
use ( $dbw, $logEntry, $logid ) {
2915 $logEntry->publish( $logid );
2920 $dbw->endAtomic( __METHOD__ );
2925 &$wikiPageBeforeDelete,
2931 $archivedRevisionCount
2936 $cache = MediaWikiServices::getInstance()->getMainObjectStash();
2937 $key =
$cache->makeKey(
'page-recent-delete', md5( $logTitle->getPrefixedText() ) );
2938 $cache->set( $key, 1, $cache::TTL_DAY );
2954 'page_id' => $this->
getId(),
2957 'page_namespace' => $this->
getTitle()->getNamespace(),
2958 'page_title' => $this->
getTitle()->getDBkey()
2977 }
catch ( Exception $ex ) {
2988 foreach ( $updates
as $update ) {
2996 if ( $this->mTitle->getNamespace() ==
NS_FILE ) {
3003 $this->mTitle, $revision,
null,
wfWikiID()
3043 $fromP, $summary, $token, $bot, &$resultDetails,
User $user, $tags =
null
3045 $resultDetails =
null;
3048 $editErrors = $this->mTitle->getUserPermissionsErrors(
'edit',
$user );
3049 $rollbackErrors = $this->mTitle->getUserPermissionsErrors(
'rollback',
$user );
3050 $errors = array_merge( $editErrors,
wfArrayDiff2( $rollbackErrors, $editErrors ) );
3052 if ( !
$user->matchEditToken( $token,
'rollback' ) ) {
3053 $errors[] = [
'sessionfailure' ];
3056 if (
$user->pingLimiter(
'rollback' ) ||
$user->pingLimiter() ) {
3057 $errors[] = [
'actionthrottledtext' ];
3061 if ( !empty( $errors ) ) {
3089 &$resultDetails,
User $guser, $tags =
null
3096 return [ [
'readonlytext' ] ];
3101 if ( is_null( $current ) ) {
3103 return [ [
'notanarticle' ] ];
3106 $from = str_replace(
'_',
' ', $fromP );
3109 if ( $from != $current->getUserText() ) {
3110 $resultDetails = [
'current' => $current ];
3111 return [ [
'alreadyrolled',
3112 htmlspecialchars( $this->mTitle->getPrefixedText() ),
3113 htmlspecialchars( $fromP ),
3114 htmlspecialchars( $current->getUserText() )
3121 $user_text = $dbw->addQuotes( $current->getUserText(
Revision::RAW ) );
3122 $s = $dbw->selectRow(
'revision',
3123 [
'rev_id',
'rev_timestamp',
'rev_deleted' ],
3124 [
'rev_page' => $current->getPage(),
3125 "rev_user != {$user} OR rev_user_text != {$user_text}"
3127 [
'USE INDEX' =>
'page_timestamp',
3128 'ORDER BY' =>
'rev_timestamp DESC' ]
3130 if (
$s ===
false ) {
3132 return [ [
'cantrollback' ] ];
3137 return [ [
'notvisiblerev' ] ];
3142 if ( empty( $summary ) ) {
3143 if ( $from ==
'' ) {
3144 $summary =
wfMessage(
'revertpage-nouser' );
3152 $target->getUserText(), $from,
$s->rev_id,
3154 $current->getId(),
$wgContLang->timeanddate( $current->getTimestamp() )
3156 if ( $summary instanceof Message ) {
3157 $summary = $summary->params(
$args )->inContentLanguage()->text();
3163 $summary = trim( $summary );
3168 if ( $guser->
isAllowed(
'minoredit' ) ) {
3172 if ( $bot && ( $guser->
isAllowedAny(
'markbotedits',
'bot' ) ) ) {
3176 $targetContent = $target->getContent();
3177 $changingContentModel = $targetContent->getModel() !== $current->getContentModel();
3193 if ( $bot && $guser->
isAllowed(
'markbotedits' ) ) {
3200 $set[
'rc_patrolled'] = 1;
3203 if (
count( $set ) ) {
3204 $dbw->update(
'recentchanges', $set,
3206 'rc_cur_id' => $current->getPage(),
3207 'rc_user_text' => $current->getUserText(),
3208 'rc_timestamp > ' . $dbw->addQuotes(
$s->rev_timestamp ),
3215 return $status->getErrorsArray();
3219 $statusRev = isset(
$status->value[
'revision'] )
3222 if ( !( $statusRev instanceof
Revision ) ) {
3223 $resultDetails = [
'current' => $current ];
3224 return [ [
'alreadyrolled',
3225 htmlspecialchars( $this->mTitle->getPrefixedText() ),
3226 htmlspecialchars( $fromP ),
3227 htmlspecialchars( $current->getUserText() )
3231 if ( $changingContentModel ) {
3235 $log->setPerformer( $guser );
3236 $log->setTarget( $this->mTitle );
3237 $log->setComment( $summary );
3238 $log->setParameters( [
3239 '4::oldmodel' => $current->getContentModel(),
3240 '5::newmodel' => $targetContent->getModel(),
3243 $logId = $log->insert( $dbw );
3244 $log->publish( $logId );
3247 $revId = $statusRev->getId();
3249 Hooks::run(
'ArticleRollbackComplete', [ $this, $guser, $target, $current ] );
3252 'summary' => $summary,
3253 'current' => $current,
3254 'target' => $target,
3274 $other =
$title->getOtherPage();
3276 $other->purgeSquid();
3280 $title->deleteTitleProtection();
3282 MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle(
$title );
3303 $other =
$title->getOtherPage();
3305 $other->purgeSquid();
3310 MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle(
$title );
3330 $user->setNewtalk(
false );
3351 MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle(
$title );
3358 $revid = $revision ? $revision->getId() :
null;
3373 $id = $this->
getId();
3379 $res =
$dbr->select(
'categorylinks',
3380 [
'cl_to AS page_title, ' .
NS_CATEGORY .
' AS page_namespace' ],
3383 [
'cl_from' => $id ],
3397 $id = $this->
getId();
3404 $res =
$dbr->select( [
'categorylinks',
'page_props',
'page' ],
3406 [
'cl_from' => $id,
'pp_page=page_id',
'pp_propname' =>
'hiddencat',
3407 'page_namespace' =>
NS_CATEGORY,
'page_title=cl_to' ],
3410 if (
$res !==
false ) {
3411 foreach (
$res as $row ) {
3441 $id = $id ?: $this->
getId();
3442 $ns = $this->
getTitle()->getNamespace();
3444 $addFields = [
'cat_pages = cat_pages + 1' ];
3445 $removeFields = [
'cat_pages = cat_pages - 1' ];
3447 $addFields[] =
'cat_subcats = cat_subcats + 1';
3448 $removeFields[] =
'cat_subcats = cat_subcats - 1';
3450 $addFields[] =
'cat_files = cat_files + 1';
3451 $removeFields[] =
'cat_files = cat_files - 1';
3456 if (
count( $added ) ) {
3457 $existingAdded = $dbw->selectFieldValues(
3460 [
'cat_title' => $added ],
3467 if (
count( $existingAdded ) ) {
3471 [
'cat_title' => $existingAdded ],
3476 $missingAdded = array_diff( $added, $existingAdded );
3477 if (
count( $missingAdded ) ) {
3479 foreach ( $missingAdded
as $cat ) {
3481 'cat_title' => $cat,
3484 'cat_files' => ( $ns ==
NS_FILE ) ? 1 : 0,
3497 if (
count( $deleted ) ) {
3501 [
'cat_title' => $deleted ],
3506 foreach ( $added
as $catName ) {
3508 Hooks::run(
'CategoryAfterPageAdded', [ $cat, $this ] );
3511 foreach ( $deleted
as $catName ) {
3513 Hooks::run(
'CategoryAfterPageRemoved', [ $cat, $this, $id ] );
3519 if (
count( $deleted ) ) {
3520 $rows = $dbw->select(
3522 [
'cat_id',
'cat_title',
'cat_pages',
'cat_subcats',
'cat_files' ],
3523 [
'cat_title' => $deleted,
'cat_pages <= 0' ],
3530 $cat->refreshCounts();
3547 if ( !
Hooks::run(
'OpportunisticLinksUpdate',
3548 [ $this, $this->mTitle, $parserOutput ]
3556 'isOpportunistic' =>
true,
3560 if ( $this->mTitle->areRestrictionsCascading() ) {
3565 } elseif ( !$config->get(
'MiserMode' ) && $parserOutput->
hasDynamicContent() ) {
3576 $key =
$cache->makeKey(
'dynamic-linksupdate',
'last', $this->
getId() );
3578 if (
$cache->add( $key, time(), $ttl ) ) {
3602 }
catch ( Exception $ex ) {
3613 $updates = $content->getDeletionUpdates( $this );
3616 Hooks::run(
'WikiPageDeletionUpdates', [ $this, $content, &$updates ] );
3654 return $this->
getTitle()->getCanonicalURL();
3663 $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3665 return $linkCache->getMutableCacheKeys(
$cache, $this->
getTitle()->getTitleValue() );