MediaWiki REL1_31
Title.php
Go to the documentation of this file.
1<?php
30
39class Title implements LinkTarget, IDBAccessObject {
41 static private $titleCache = null;
42
48 const CACHE_MAX = 1000;
49
54 const GAID_FOR_UPDATE = 1;
55
61 // @{
62
64 public $mTextform = '';
65
67 public $mUrlform = '';
68
70 public $mDbkeyform = '';
71
73 protected $mUserCaseDBKey;
74
77
79 public $mInterwiki = '';
80
82 private $mLocalInterwiki = false;
83
85 public $mFragment = '';
86
88 public $mArticleID = -1;
89
91 protected $mLatestID = false;
92
97 private $mContentModel = false;
98
103 private $mForcedContentModel = false;
104
107
109 public $mRestrictions = [];
110
117 protected $mOldRestrictions = false;
118
121
124
126 protected $mRestrictionsExpiry = [];
127
130
133
135 public $mRestrictionsLoaded = false;
136
138 protected $mPrefixedText = null;
139
142
149
151 protected $mLength = -1;
152
154 public $mRedirect = null;
155
158
161
163 private $mPageLanguage = false;
164
167 private $mDbPageLanguage = false;
168
170 private $mTitleValue = null;
171
173 private $mIsBigDeletion = null;
174 // @}
175
184 private static function getTitleFormatter() {
185 return MediaWikiServices::getInstance()->getTitleFormatter();
186 }
187
196 private static function getInterwikiLookup() {
197 return MediaWikiServices::getInstance()->getInterwikiLookup();
198 }
199
203 function __construct() {
204 }
205
214 public static function newFromDBkey( $key ) {
215 $t = new Title();
216 $t->mDbkeyform = $key;
217
218 try {
219 $t->secureAndSplit();
220 return $t;
221 } catch ( MalformedTitleException $ex ) {
222 return null;
223 }
224 }
225
233 public static function newFromTitleValue( TitleValue $titleValue ) {
234 return self::newFromLinkTarget( $titleValue );
235 }
236
244 public static function newFromLinkTarget( LinkTarget $linkTarget ) {
245 if ( $linkTarget instanceof Title ) {
246 // Special case if it's already a Title object
247 return $linkTarget;
248 }
249 return self::makeTitle(
250 $linkTarget->getNamespace(),
251 $linkTarget->getText(),
252 $linkTarget->getFragment(),
253 $linkTarget->getInterwiki()
254 );
255 }
256
273 public static function newFromText( $text, $defaultNamespace = NS_MAIN ) {
274 // DWIM: Integers can be passed in here when page titles are used as array keys.
275 if ( $text !== null && !is_string( $text ) && !is_int( $text ) ) {
276 throw new InvalidArgumentException( '$text must be a string.' );
277 }
278 if ( $text === null ) {
279 return null;
280 }
281
282 try {
283 return self::newFromTextThrow( strval( $text ), $defaultNamespace );
284 } catch ( MalformedTitleException $ex ) {
285 return null;
286 }
287 }
288
306 public static function newFromTextThrow( $text, $defaultNamespace = NS_MAIN ) {
307 if ( is_object( $text ) ) {
308 throw new MWException( '$text must be a string, given an object' );
309 }
310
311 $titleCache = self::getTitleCache();
312
313 // Wiki pages often contain multiple links to the same page.
314 // Title normalization and parsing can become expensive on pages with many
315 // links, so we can save a little time by caching them.
316 // In theory these are value objects and won't get changed...
317 if ( $defaultNamespace == NS_MAIN ) {
318 $t = $titleCache->get( $text );
319 if ( $t ) {
320 return $t;
321 }
322 }
323
324 // Convert things like &eacute; &#257; or &#x3017; into normalized (T16952) text
325 $filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text );
326
327 $t = new Title();
328 $t->mDbkeyform = strtr( $filteredText, ' ', '_' );
329 $t->mDefaultNamespace = intval( $defaultNamespace );
330
331 $t->secureAndSplit();
332 if ( $defaultNamespace == NS_MAIN ) {
333 $titleCache->set( $text, $t );
334 }
335 return $t;
336 }
337
353 public static function newFromURL( $url ) {
354 $t = new Title();
355
356 # For compatibility with old buggy URLs. "+" is usually not valid in titles,
357 # but some URLs used it as a space replacement and they still come
358 # from some external search tools.
359 if ( strpos( self::legalChars(), '+' ) === false ) {
360 $url = strtr( $url, '+', ' ' );
361 }
362
363 $t->mDbkeyform = strtr( $url, ' ', '_' );
364
365 try {
366 $t->secureAndSplit();
367 return $t;
368 } catch ( MalformedTitleException $ex ) {
369 return null;
370 }
371 }
372
376 private static function getTitleCache() {
377 if ( self::$titleCache == null ) {
378 self::$titleCache = new HashBagOStuff( [ 'maxKeys' => self::CACHE_MAX ] );
379 }
380 return self::$titleCache;
381 }
382
390 protected static function getSelectFields() {
392
393 $fields = [
394 'page_namespace', 'page_title', 'page_id',
395 'page_len', 'page_is_redirect', 'page_latest',
396 ];
397
399 $fields[] = 'page_content_model';
400 }
401
402 if ( $wgPageLanguageUseDB ) {
403 $fields[] = 'page_lang';
404 }
405
406 return $fields;
407 }
408
416 public static function newFromID( $id, $flags = 0 ) {
417 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_REPLICA );
418 $row = $db->selectRow(
419 'page',
420 self::getSelectFields(),
421 [ 'page_id' => $id ],
422 __METHOD__
423 );
424 if ( $row !== false ) {
425 $title = self::newFromRow( $row );
426 } else {
427 $title = null;
428 }
429 return $title;
430 }
431
438 public static function newFromIDs( $ids ) {
439 if ( !count( $ids ) ) {
440 return [];
441 }
443
444 $res = $dbr->select(
445 'page',
446 self::getSelectFields(),
447 [ 'page_id' => $ids ],
448 __METHOD__
449 );
450
451 $titles = [];
452 foreach ( $res as $row ) {
453 $titles[] = self::newFromRow( $row );
454 }
455 return $titles;
456 }
457
464 public static function newFromRow( $row ) {
465 $t = self::makeTitle( $row->page_namespace, $row->page_title );
466 $t->loadFromRow( $row );
467 return $t;
468 }
469
476 public function loadFromRow( $row ) {
477 if ( $row ) { // page found
478 if ( isset( $row->page_id ) ) {
479 $this->mArticleID = (int)$row->page_id;
480 }
481 if ( isset( $row->page_len ) ) {
482 $this->mLength = (int)$row->page_len;
483 }
484 if ( isset( $row->page_is_redirect ) ) {
485 $this->mRedirect = (bool)$row->page_is_redirect;
486 }
487 if ( isset( $row->page_latest ) ) {
488 $this->mLatestID = (int)$row->page_latest;
489 }
490 if ( !$this->mForcedContentModel && isset( $row->page_content_model ) ) {
491 $this->mContentModel = strval( $row->page_content_model );
492 } elseif ( !$this->mForcedContentModel ) {
493 $this->mContentModel = false; # initialized lazily in getContentModel()
494 }
495 if ( isset( $row->page_lang ) ) {
496 $this->mDbPageLanguage = (string)$row->page_lang;
497 }
498 if ( isset( $row->page_restrictions ) ) {
499 $this->mOldRestrictions = $row->page_restrictions;
500 }
501 } else { // page not found
502 $this->mArticleID = 0;
503 $this->mLength = 0;
504 $this->mRedirect = false;
505 $this->mLatestID = 0;
506 if ( !$this->mForcedContentModel ) {
507 $this->mContentModel = false; # initialized lazily in getContentModel()
508 }
509 }
510 }
511
534 public static function makeTitle( $ns, $title, $fragment = '', $interwiki = '' ) {
535 $t = new Title();
536 $t->mInterwiki = $interwiki;
537 $t->mFragment = $fragment;
538 $t->mNamespace = $ns = intval( $ns );
539 $t->mDbkeyform = strtr( $title, ' ', '_' );
540 $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
541 $t->mUrlform = wfUrlencode( $t->mDbkeyform );
542 $t->mTextform = strtr( $title, '_', ' ' );
543 $t->mContentModel = false; # initialized lazily in getContentModel()
544 return $t;
545 }
546
562 public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) {
563 // NOTE: ideally, this would just call makeTitle() and then isValid(),
564 // but presently, that means more overhead on a potential performance hotspot.
565
566 if ( !MWNamespace::exists( $ns ) ) {
567 return null;
568 }
569
570 $t = new Title();
571 $t->mDbkeyform = self::makeName( $ns, $title, $fragment, $interwiki, true );
572
573 try {
574 $t->secureAndSplit();
575 return $t;
576 } catch ( MalformedTitleException $ex ) {
577 return null;
578 }
579 }
580
586 public static function newMainPage() {
587 $title = self::newFromText( wfMessage( 'mainpage' )->inContentLanguage()->text() );
588 // Don't give fatal errors if the message is broken
589 if ( !$title ) {
590 $title = self::newFromText( 'Main Page' );
591 }
592 return $title;
593 }
594
601 public static function nameOf( $id ) {
603
604 $s = $dbr->selectRow(
605 'page',
606 [ 'page_namespace', 'page_title' ],
607 [ 'page_id' => $id ],
608 __METHOD__
609 );
610 if ( $s === false ) {
611 return null;
612 }
613
614 $n = self::makeName( $s->page_namespace, $s->page_title );
615 return $n;
616 }
617
623 public static function legalChars() {
624 global $wgLegalTitleChars;
625 return $wgLegalTitleChars;
626 }
627
637 public static function convertByteClassToUnicodeClass( $byteClass ) {
638 $length = strlen( $byteClass );
639 // Input token queue
640 $x0 = $x1 = $x2 = '';
641 // Decoded queue
642 $d0 = $d1 = $d2 = '';
643 // Decoded integer codepoints
644 $ord0 = $ord1 = $ord2 = 0;
645 // Re-encoded queue
646 $r0 = $r1 = $r2 = '';
647 // Output
648 $out = '';
649 // Flags
650 $allowUnicode = false;
651 for ( $pos = 0; $pos < $length; $pos++ ) {
652 // Shift the queues down
653 $x2 = $x1;
654 $x1 = $x0;
655 $d2 = $d1;
656 $d1 = $d0;
657 $ord2 = $ord1;
658 $ord1 = $ord0;
659 $r2 = $r1;
660 $r1 = $r0;
661 // Load the current input token and decoded values
662 $inChar = $byteClass[$pos];
663 if ( $inChar == '\\' ) {
664 if ( preg_match( '/x([0-9a-fA-F]{2})/A', $byteClass, $m, 0, $pos + 1 ) ) {
665 $x0 = $inChar . $m[0];
666 $d0 = chr( hexdec( $m[1] ) );
667 $pos += strlen( $m[0] );
668 } elseif ( preg_match( '/[0-7]{3}/A', $byteClass, $m, 0, $pos + 1 ) ) {
669 $x0 = $inChar . $m[0];
670 $d0 = chr( octdec( $m[0] ) );
671 $pos += strlen( $m[0] );
672 } elseif ( $pos + 1 >= $length ) {
673 $x0 = $d0 = '\\';
674 } else {
675 $d0 = $byteClass[$pos + 1];
676 $x0 = $inChar . $d0;
677 $pos += 1;
678 }
679 } else {
680 $x0 = $d0 = $inChar;
681 }
682 $ord0 = ord( $d0 );
683 // Load the current re-encoded value
684 if ( $ord0 < 32 || $ord0 == 0x7f ) {
685 $r0 = sprintf( '\x%02x', $ord0 );
686 } elseif ( $ord0 >= 0x80 ) {
687 // Allow unicode if a single high-bit character appears
688 $r0 = sprintf( '\x%02x', $ord0 );
689 $allowUnicode = true;
690 } elseif ( strpos( '-\\[]^', $d0 ) !== false ) {
691 $r0 = '\\' . $d0;
692 } else {
693 $r0 = $d0;
694 }
695 // Do the output
696 if ( $x0 !== '' && $x1 === '-' && $x2 !== '' ) {
697 // Range
698 if ( $ord2 > $ord0 ) {
699 // Empty range
700 } elseif ( $ord0 >= 0x80 ) {
701 // Unicode range
702 $allowUnicode = true;
703 if ( $ord2 < 0x80 ) {
704 // Keep the non-unicode section of the range
705 $out .= "$r2-\\x7F";
706 }
707 } else {
708 // Normal range
709 $out .= "$r2-$r0";
710 }
711 // Reset state to the initial value
712 $x0 = $x1 = $d0 = $d1 = $r0 = $r1 = '';
713 } elseif ( $ord2 < 0x80 ) {
714 // ASCII character
715 $out .= $r2;
716 }
717 }
718 if ( $ord1 < 0x80 ) {
719 $out .= $r1;
720 }
721 if ( $ord0 < 0x80 ) {
722 $out .= $r0;
723 }
724 if ( $allowUnicode ) {
725 $out .= '\u0080-\uFFFF';
726 }
727 return $out;
728 }
729
741 public static function makeName( $ns, $title, $fragment = '', $interwiki = '',
742 $canonicalNamespace = false
743 ) {
744 global $wgContLang;
745
746 if ( $canonicalNamespace ) {
747 $namespace = MWNamespace::getCanonicalName( $ns );
748 } else {
749 $namespace = $wgContLang->getNsText( $ns );
750 }
751 $name = $namespace == '' ? $title : "$namespace:$title";
752 if ( strval( $interwiki ) != '' ) {
753 $name = "$interwiki:$name";
754 }
755 if ( strval( $fragment ) != '' ) {
756 $name .= '#' . $fragment;
757 }
758 return $name;
759 }
760
769 static function escapeFragmentForURL( $fragment ) {
770 wfDeprecated( __METHOD__, '1.30' );
771 # Note that we don't urlencode the fragment. urlencoded Unicode
772 # fragments appear not to work in IE (at least up to 7) or in at least
773 # one version of Opera 9.x. The W3C validator, for one, doesn't seem
774 # to care if they aren't encoded.
775 return Sanitizer::escapeId( $fragment, 'noninitial' );
776 }
777
786 public static function compare( LinkTarget $a, LinkTarget $b ) {
787 if ( $a->getNamespace() == $b->getNamespace() ) {
788 return strcmp( $a->getText(), $b->getText() );
789 } else {
790 return $a->getNamespace() - $b->getNamespace();
791 }
792 }
793
808 public function isValid() {
809 $ns = $this->getNamespace();
810
811 if ( !MWNamespace::exists( $ns ) ) {
812 return false;
813 }
814
815 try {
816 $parser = MediaWikiServices::getInstance()->getTitleParser();
817 $parser->parseTitle( $this->getDBkey(), $ns );
818 return true;
819 } catch ( MalformedTitleException $ex ) {
820 return false;
821 }
822 }
823
831 public function isLocal() {
832 if ( $this->isExternal() ) {
833 $iw = self::getInterwikiLookup()->fetch( $this->mInterwiki );
834 if ( $iw ) {
835 return $iw->isLocal();
836 }
837 }
838 return true;
839 }
840
846 public function isExternal() {
847 return $this->mInterwiki !== '';
848 }
849
857 public function getInterwiki() {
858 return $this->mInterwiki;
859 }
860
866 public function wasLocalInterwiki() {
867 return $this->mLocalInterwiki;
868 }
869
876 public function isTrans() {
877 if ( !$this->isExternal() ) {
878 return false;
879 }
880
881 return self::getInterwikiLookup()->fetch( $this->mInterwiki )->isTranscludable();
882 }
883
889 public function getTransWikiID() {
890 if ( !$this->isExternal() ) {
891 return false;
892 }
893
894 return self::getInterwikiLookup()->fetch( $this->mInterwiki )->getWikiID();
895 }
896
906 public function getTitleValue() {
907 if ( $this->mTitleValue === null ) {
908 try {
909 $this->mTitleValue = new TitleValue(
910 $this->getNamespace(),
911 $this->getDBkey(),
912 $this->getFragment(),
913 $this->getInterwiki()
914 );
915 } catch ( InvalidArgumentException $ex ) {
916 wfDebug( __METHOD__ . ': Can\'t create a TitleValue for [[' .
917 $this->getPrefixedText() . ']]: ' . $ex->getMessage() . "\n" );
918 }
919 }
920
921 return $this->mTitleValue;
922 }
923
929 public function getText() {
930 return $this->mTextform;
931 }
932
938 public function getPartialURL() {
939 return $this->mUrlform;
940 }
941
947 public function getDBkey() {
948 return $this->mDbkeyform;
949 }
950
956 function getUserCaseDBKey() {
957 if ( !is_null( $this->mUserCaseDBKey ) ) {
958 return $this->mUserCaseDBKey;
959 } else {
960 // If created via makeTitle(), $this->mUserCaseDBKey is not set.
961 return $this->mDbkeyform;
962 }
963 }
964
970 public function getNamespace() {
971 return $this->mNamespace;
972 }
973
980 public function getContentModel( $flags = 0 ) {
981 if ( !$this->mForcedContentModel
982 && ( !$this->mContentModel || $flags === self::GAID_FOR_UPDATE )
983 && $this->getArticleID( $flags )
984 ) {
985 $linkCache = LinkCache::singleton();
986 $linkCache->addLinkObj( $this ); # in case we already had an article ID
987 $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' );
988 }
989
990 if ( !$this->mContentModel ) {
991 $this->mContentModel = ContentHandler::getDefaultModelFor( $this );
992 }
993
994 return $this->mContentModel;
995 }
996
1003 public function hasContentModel( $id ) {
1004 return $this->getContentModel() == $id;
1005 }
1006
1018 public function setContentModel( $model ) {
1019 $this->mContentModel = $model;
1020 $this->mForcedContentModel = true;
1021 }
1022
1028 public function getNsText() {
1029 if ( $this->isExternal() ) {
1030 // This probably shouldn't even happen, except for interwiki transclusion.
1031 // If possible, use the canonical name for the foreign namespace.
1032 $nsText = MWNamespace::getCanonicalName( $this->mNamespace );
1033 if ( $nsText !== false ) {
1034 return $nsText;
1035 }
1036 }
1037
1038 try {
1039 $formatter = self::getTitleFormatter();
1040 return $formatter->getNamespaceName( $this->mNamespace, $this->mDbkeyform );
1041 } catch ( InvalidArgumentException $ex ) {
1042 wfDebug( __METHOD__ . ': ' . $ex->getMessage() . "\n" );
1043 return false;
1044 }
1045 }
1046
1052 public function getSubjectNsText() {
1053 global $wgContLang;
1054 return $wgContLang->getNsText( MWNamespace::getSubject( $this->mNamespace ) );
1055 }
1056
1062 public function getTalkNsText() {
1063 global $wgContLang;
1064 return $wgContLang->getNsText( MWNamespace::getTalk( $this->mNamespace ) );
1065 }
1066
1074 public function canTalk() {
1075 return $this->canHaveTalkPage();
1076 }
1077
1086 public function canHaveTalkPage() {
1087 return MWNamespace::hasTalkNamespace( $this->mNamespace );
1088 }
1089
1095 public function canExist() {
1096 return $this->mNamespace >= NS_MAIN;
1097 }
1098
1104 public function isWatchable() {
1105 return !$this->isExternal() && MWNamespace::isWatchable( $this->getNamespace() );
1106 }
1107
1113 public function isSpecialPage() {
1114 return $this->getNamespace() == NS_SPECIAL;
1115 }
1116
1123 public function isSpecial( $name ) {
1124 if ( $this->isSpecialPage() ) {
1125 list( $thisName, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $this->getDBkey() );
1126 if ( $name == $thisName ) {
1127 return true;
1128 }
1129 }
1130 return false;
1131 }
1132
1139 public function fixSpecialName() {
1140 if ( $this->isSpecialPage() ) {
1141 list( $canonicalName, $par ) = SpecialPageFactory::resolveAlias( $this->mDbkeyform );
1142 if ( $canonicalName ) {
1143 $localName = SpecialPageFactory::getLocalNameFor( $canonicalName, $par );
1144 if ( $localName != $this->mDbkeyform ) {
1145 return self::makeTitle( NS_SPECIAL, $localName );
1146 }
1147 }
1148 }
1149 return $this;
1150 }
1151
1162 public function inNamespace( $ns ) {
1163 return MWNamespace::equals( $this->getNamespace(), $ns );
1164 }
1165
1173 public function inNamespaces( /* ... */ ) {
1174 $namespaces = func_get_args();
1175 if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) {
1177 }
1178
1179 foreach ( $namespaces as $ns ) {
1180 if ( $this->inNamespace( $ns ) ) {
1181 return true;
1182 }
1183 }
1184
1185 return false;
1186 }
1187
1201 public function hasSubjectNamespace( $ns ) {
1202 return MWNamespace::subjectEquals( $this->getNamespace(), $ns );
1203 }
1204
1212 public function isContentPage() {
1213 return MWNamespace::isContent( $this->getNamespace() );
1214 }
1215
1222 public function isMovable() {
1223 if ( !MWNamespace::isMovable( $this->getNamespace() ) || $this->isExternal() ) {
1224 // Interwiki title or immovable namespace. Hooks don't get to override here
1225 return false;
1226 }
1227
1228 $result = true;
1229 Hooks::run( 'TitleIsMovable', [ $this, &$result ] );
1230 return $result;
1231 }
1232
1243 public function isMainPage() {
1244 return $this->equals( self::newMainPage() );
1245 }
1246
1252 public function isSubpage() {
1253 return MWNamespace::hasSubpages( $this->mNamespace )
1254 ? strpos( $this->getText(), '/' ) !== false
1255 : false;
1256 }
1257
1263 public function isConversionTable() {
1264 // @todo ConversionTable should become a separate content model.
1265
1266 return $this->getNamespace() == NS_MEDIAWIKI &&
1267 strpos( $this->getText(), 'Conversiontable/' ) === 0;
1268 }
1269
1275 public function isWikitextPage() {
1276 return $this->hasContentModel( CONTENT_MODEL_WIKITEXT );
1277 }
1278
1293 public function isSiteConfigPage() {
1294 return (
1295 NS_MEDIAWIKI == $this->mNamespace
1296 && (
1300 )
1301 );
1302 }
1303
1308 public function isCssOrJsPage() {
1309 wfDeprecated( __METHOD__, '1.31' );
1310 return ( NS_MEDIAWIKI == $this->mNamespace
1311 && ( $this->hasContentModel( CONTENT_MODEL_CSS )
1313 }
1314
1321 public function isUserConfigPage() {
1322 return (
1323 NS_USER == $this->mNamespace
1324 && $this->isSubpage()
1325 && (
1329 )
1330 );
1331 }
1332
1337 public function isCssJsSubpage() {
1338 wfDeprecated( __METHOD__, '1.31' );
1339 return ( NS_USER == $this->mNamespace && $this->isSubpage()
1340 && ( $this->hasContentModel( CONTENT_MODEL_CSS )
1342 }
1343
1350 public function getSkinFromConfigSubpage() {
1351 $subpage = explode( '/', $this->mTextform );
1352 $subpage = $subpage[count( $subpage ) - 1];
1353 $lastdot = strrpos( $subpage, '.' );
1354 if ( $lastdot === false ) {
1355 return $subpage; # Never happens: only called for names ending in '.css'/'.json'/'.js'
1356 }
1357 return substr( $subpage, 0, $lastdot );
1358 }
1359
1364 public function getSkinFromCssJsSubpage() {
1365 wfDeprecated( __METHOD__, '1.31' );
1366 return $this->getSkinFromConfigSubpage();
1367 }
1368
1375 public function isUserCssConfigPage() {
1376 return (
1377 NS_USER == $this->mNamespace
1378 && $this->isSubpage()
1380 );
1381 }
1382
1387 public function isCssSubpage() {
1388 wfDeprecated( __METHOD__, '1.31' );
1389 return $this->isUserCssConfigPage();
1390 }
1391
1398 public function isUserJsonConfigPage() {
1399 return (
1400 NS_USER == $this->mNamespace
1401 && $this->isSubpage()
1403 );
1404 }
1405
1412 public function isUserJsConfigPage() {
1413 return (
1414 NS_USER == $this->mNamespace
1415 && $this->isSubpage()
1417 );
1418 }
1419
1424 public function isJsSubpage() {
1425 wfDeprecated( __METHOD__, '1.31' );
1426 return $this->isUserJsConfigPage();
1427 }
1428
1434 public function isTalkPage() {
1435 return MWNamespace::isTalk( $this->getNamespace() );
1436 }
1437
1443 public function getTalkPage() {
1444 return self::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() );
1445 }
1446
1456 public function getTalkPageIfDefined() {
1457 if ( !$this->canHaveTalkPage() ) {
1458 return null;
1459 }
1460
1461 return $this->getTalkPage();
1462 }
1463
1470 public function getSubjectPage() {
1471 // Is this the same title?
1472 $subjectNS = MWNamespace::getSubject( $this->getNamespace() );
1473 if ( $this->getNamespace() == $subjectNS ) {
1474 return $this;
1475 }
1476 return self::makeTitle( $subjectNS, $this->getDBkey() );
1477 }
1478
1487 public function getOtherPage() {
1488 if ( $this->isSpecialPage() ) {
1489 throw new MWException( 'Special pages cannot have other pages' );
1490 }
1491 if ( $this->isTalkPage() ) {
1492 return $this->getSubjectPage();
1493 } else {
1494 if ( !$this->canHaveTalkPage() ) {
1495 throw new MWException( "{$this->getPrefixedText()} does not have an other page" );
1496 }
1497 return $this->getTalkPage();
1498 }
1499 }
1500
1506 public function getDefaultNamespace() {
1507 return $this->mDefaultNamespace;
1508 }
1509
1517 public function getFragment() {
1518 return $this->mFragment;
1519 }
1520
1527 public function hasFragment() {
1528 return $this->mFragment !== '';
1529 }
1530
1536 public function getFragmentForURL() {
1537 if ( !$this->hasFragment() ) {
1538 return '';
1539 } elseif ( $this->isExternal()
1540 && !self::getInterwikiLookup()->fetch( $this->mInterwiki )->isLocal()
1541 ) {
1542 return '#' . Sanitizer::escapeIdForExternalInterwiki( $this->getFragment() );
1543 }
1544 return '#' . Sanitizer::escapeIdForLink( $this->getFragment() );
1545 }
1546
1559 public function setFragment( $fragment ) {
1560 $this->mFragment = strtr( substr( $fragment, 1 ), '_', ' ' );
1561 }
1562
1570 public function createFragmentTarget( $fragment ) {
1571 return self::makeTitle(
1572 $this->getNamespace(),
1573 $this->getText(),
1574 $fragment,
1575 $this->getInterwiki()
1576 );
1577 }
1578
1586 private function prefix( $name ) {
1587 global $wgContLang;
1588
1589 $p = '';
1590 if ( $this->isExternal() ) {
1591 $p = $this->mInterwiki . ':';
1592 }
1593
1594 if ( 0 != $this->mNamespace ) {
1595 $nsText = $this->getNsText();
1596
1597 if ( $nsText === false ) {
1598 // See T165149. Awkward, but better than erroneously linking to the main namespace.
1599 $nsText = $wgContLang->getNsText( NS_SPECIAL ) . ":Badtitle/NS{$this->mNamespace}";
1600 }
1601
1602 $p .= $nsText . ':';
1603 }
1604 return $p . $name;
1605 }
1606
1613 public function getPrefixedDBkey() {
1614 $s = $this->prefix( $this->mDbkeyform );
1615 $s = strtr( $s, ' ', '_' );
1616 return $s;
1617 }
1618
1625 public function getPrefixedText() {
1626 if ( $this->mPrefixedText === null ) {
1627 $s = $this->prefix( $this->mTextform );
1628 $s = strtr( $s, '_', ' ' );
1629 $this->mPrefixedText = $s;
1630 }
1631 return $this->mPrefixedText;
1632 }
1633
1639 public function __toString() {
1640 return $this->getPrefixedText();
1641 }
1642
1649 public function getFullText() {
1650 $text = $this->getPrefixedText();
1651 if ( $this->hasFragment() ) {
1652 $text .= '#' . $this->getFragment();
1653 }
1654 return $text;
1655 }
1656
1669 public function getRootText() {
1670 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1671 return $this->getText();
1672 }
1673
1674 return strtok( $this->getText(), '/' );
1675 }
1676
1689 public function getRootTitle() {
1690 return self::makeTitle( $this->getNamespace(), $this->getRootText() );
1691 }
1692
1704 public function getBaseText() {
1705 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1706 return $this->getText();
1707 }
1708
1709 $parts = explode( '/', $this->getText() );
1710 # Don't discard the real title if there's no subpage involved
1711 if ( count( $parts ) > 1 ) {
1712 unset( $parts[count( $parts ) - 1] );
1713 }
1714 return implode( '/', $parts );
1715 }
1716
1729 public function getBaseTitle() {
1730 return self::makeTitle( $this->getNamespace(), $this->getBaseText() );
1731 }
1732
1744 public function getSubpageText() {
1745 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1746 return $this->mTextform;
1747 }
1748 $parts = explode( '/', $this->mTextform );
1749 return $parts[count( $parts ) - 1];
1750 }
1751
1765 public function getSubpage( $text ) {
1766 return self::makeTitleSafe( $this->getNamespace(), $this->getText() . '/' . $text );
1767 }
1768
1774 public function getSubpageUrlForm() {
1775 $text = $this->getSubpageText();
1776 $text = wfUrlencode( strtr( $text, ' ', '_' ) );
1777 return $text;
1778 }
1779
1785 public function getPrefixedURL() {
1786 $s = $this->prefix( $this->mDbkeyform );
1787 $s = wfUrlencode( strtr( $s, ' ', '_' ) );
1788 return $s;
1789 }
1790
1804 private static function fixUrlQueryArgs( $query, $query2 = false ) {
1805 if ( $query2 !== false ) {
1806 wfDeprecated( "Title::get{Canonical,Full,Link,Local,Internal}URL " .
1807 "method called with a second parameter is deprecated. Add your " .
1808 "parameter to an array passed as the first parameter.", "1.19" );
1809 }
1810 if ( is_array( $query ) ) {
1812 }
1813 if ( $query2 ) {
1814 if ( is_string( $query2 ) ) {
1815 // $query2 is a string, we will consider this to be
1816 // a deprecated $variant argument and add it to the query
1817 $query2 = wfArrayToCgi( [ 'variant' => $query2 ] );
1818 } else {
1819 $query2 = wfArrayToCgi( $query2 );
1820 }
1821 // If we have $query content add a & to it first
1822 if ( $query ) {
1823 $query .= '&';
1824 }
1825 // Now append the queries together
1826 $query .= $query2;
1827 }
1828 return $query;
1829 }
1830
1842 public function getFullURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) {
1843 $query = self::fixUrlQueryArgs( $query, $query2 );
1844
1845 # Hand off all the decisions on urls to getLocalURL
1846 $url = $this->getLocalURL( $query );
1847
1848 # Expand the url to make it a full url. Note that getLocalURL has the
1849 # potential to output full urls for a variety of reasons, so we use
1850 # wfExpandUrl instead of simply prepending $wgServer
1851 $url = wfExpandUrl( $url, $proto );
1852
1853 # Finally, add the fragment.
1854 $url .= $this->getFragmentForURL();
1855 // Avoid PHP 7.1 warning from passing $this by reference
1856 $titleRef = $this;
1857 Hooks::run( 'GetFullURL', [ &$titleRef, &$url, $query ] );
1858 return $url;
1859 }
1860
1877 public function getFullUrlForRedirect( $query = '', $proto = PROTO_CURRENT ) {
1878 $target = $this;
1879 if ( $this->isExternal() ) {
1880 $target = SpecialPage::getTitleFor(
1881 'GoToInterwiki',
1882 $this->getPrefixedDBkey()
1883 );
1884 }
1885 return $target->getFullURL( $query, false, $proto );
1886 }
1887
1911 public function getLocalURL( $query = '', $query2 = false ) {
1913
1914 $query = self::fixUrlQueryArgs( $query, $query2 );
1915
1916 $interwiki = self::getInterwikiLookup()->fetch( $this->mInterwiki );
1917 if ( $interwiki ) {
1918 $namespace = $this->getNsText();
1919 if ( $namespace != '' ) {
1920 # Can this actually happen? Interwikis shouldn't be parsed.
1921 # Yes! It can in interwiki transclusion. But... it probably shouldn't.
1922 $namespace .= ':';
1923 }
1924 $url = $interwiki->getURL( $namespace . $this->getDBkey() );
1925 $url = wfAppendQuery( $url, $query );
1926 } else {
1927 $dbkey = wfUrlencode( $this->getPrefixedDBkey() );
1928 if ( $query == '' ) {
1929 $url = str_replace( '$1', $dbkey, $wgArticlePath );
1930 // Avoid PHP 7.1 warning from passing $this by reference
1931 $titleRef = $this;
1932 Hooks::run( 'GetLocalURL::Article', [ &$titleRef, &$url ] );
1933 } else {
1935 $url = false;
1936 $matches = [];
1937
1938 if ( !empty( $wgActionPaths )
1939 && preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches )
1940 ) {
1941 $action = urldecode( $matches[2] );
1942 if ( isset( $wgActionPaths[$action] ) ) {
1943 $query = $matches[1];
1944 if ( isset( $matches[4] ) ) {
1945 $query .= $matches[4];
1946 }
1947 $url = str_replace( '$1', $dbkey, $wgActionPaths[$action] );
1948 if ( $query != '' ) {
1949 $url = wfAppendQuery( $url, $query );
1950 }
1951 }
1952 }
1953
1954 if ( $url === false
1956 && preg_match( '/^variant=([^&]*)$/', $query, $matches )
1957 && $this->getPageLanguage()->equals( $wgContLang )
1958 && $this->getPageLanguage()->hasVariants()
1959 ) {
1960 $variant = urldecode( $matches[1] );
1961 if ( $this->getPageLanguage()->hasVariant( $variant ) ) {
1962 // Only do the variant replacement if the given variant is a valid
1963 // variant for the page's language.
1964 $url = str_replace( '$2', urlencode( $variant ), $wgVariantArticlePath );
1965 $url = str_replace( '$1', $dbkey, $url );
1966 }
1967 }
1968
1969 if ( $url === false ) {
1970 if ( $query == '-' ) {
1971 $query = '';
1972 }
1973 $url = "{$wgScript}?title={$dbkey}&{$query}";
1974 }
1975 }
1976 // Avoid PHP 7.1 warning from passing $this by reference
1977 $titleRef = $this;
1978 Hooks::run( 'GetLocalURL::Internal', [ &$titleRef, &$url, $query ] );
1979
1980 // @todo FIXME: This causes breakage in various places when we
1981 // actually expected a local URL and end up with dupe prefixes.
1982 if ( $wgRequest->getVal( 'action' ) == 'render' ) {
1983 $url = $wgServer . $url;
1984 }
1985 }
1986 // Avoid PHP 7.1 warning from passing $this by reference
1987 $titleRef = $this;
1988 Hooks::run( 'GetLocalURL', [ &$titleRef, &$url, $query ] );
1989 return $url;
1990 }
1991
2009 public function getLinkURL( $query = '', $query2 = false, $proto = false ) {
2010 if ( $this->isExternal() || $proto !== false ) {
2011 $ret = $this->getFullURL( $query, $query2, $proto );
2012 } elseif ( $this->getPrefixedText() === '' && $this->hasFragment() ) {
2013 $ret = $this->getFragmentForURL();
2014 } else {
2015 $ret = $this->getLocalURL( $query, $query2 ) . $this->getFragmentForURL();
2016 }
2017 return $ret;
2018 }
2019
2034 public function getInternalURL( $query = '', $query2 = false ) {
2036 $query = self::fixUrlQueryArgs( $query, $query2 );
2037 $server = $wgInternalServer !== false ? $wgInternalServer : $wgServer;
2038 $url = wfExpandUrl( $server . $this->getLocalURL( $query ), PROTO_HTTP );
2039 // Avoid PHP 7.1 warning from passing $this by reference
2040 $titleRef = $this;
2041 Hooks::run( 'GetInternalURL', [ &$titleRef, &$url, $query ] );
2042 return $url;
2043 }
2044
2058 public function getCanonicalURL( $query = '', $query2 = false ) {
2059 $query = self::fixUrlQueryArgs( $query, $query2 );
2060 $url = wfExpandUrl( $this->getLocalURL( $query ) . $this->getFragmentForURL(), PROTO_CANONICAL );
2061 // Avoid PHP 7.1 warning from passing $this by reference
2062 $titleRef = $this;
2063 Hooks::run( 'GetCanonicalURL', [ &$titleRef, &$url, $query ] );
2064 return $url;
2065 }
2066
2072 public function getEditURL() {
2073 if ( $this->isExternal() ) {
2074 return '';
2075 }
2076 $s = $this->getLocalURL( 'action=edit' );
2077
2078 return $s;
2079 }
2080
2095 public function quickUserCan( $action, $user = null ) {
2096 return $this->userCan( $action, $user, false );
2097 }
2098
2108 public function userCan( $action, $user = null, $rigor = 'secure' ) {
2109 if ( !$user instanceof User ) {
2110 global $wgUser;
2111 $user = $wgUser;
2112 }
2113
2114 return !count( $this->getUserPermissionsErrorsInternal( $action, $user, $rigor, true ) );
2115 }
2116
2133 $action, $user, $rigor = 'secure', $ignoreErrors = []
2134 ) {
2135 $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $rigor );
2136
2137 // Remove the errors being ignored.
2138 foreach ( $errors as $index => $error ) {
2139 $errKey = is_array( $error ) ? $error[0] : $error;
2140
2141 if ( in_array( $errKey, $ignoreErrors ) ) {
2142 unset( $errors[$index] );
2143 }
2144 if ( $errKey instanceof MessageSpecifier && in_array( $errKey->getKey(), $ignoreErrors ) ) {
2145 unset( $errors[$index] );
2146 }
2147 }
2148
2149 return $errors;
2150 }
2151
2163 private function checkQuickPermissions( $action, $user, $errors, $rigor, $short ) {
2164 if ( !Hooks::run( 'TitleQuickPermissions',
2165 [ $this, $user, $action, &$errors, ( $rigor !== 'quick' ), $short ] )
2166 ) {
2167 return $errors;
2168 }
2169
2170 if ( $action == 'create' ) {
2171 if (
2172 ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
2173 ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) )
2174 ) {
2175 $errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ 'nocreate-loggedin' ];
2176 }
2177 } elseif ( $action == 'move' ) {
2178 if ( !$user->isAllowed( 'move-rootuserpages' )
2179 && $this->mNamespace == NS_USER && !$this->isSubpage() ) {
2180 // Show user page-specific message only if the user can move other pages
2181 $errors[] = [ 'cant-move-user-page' ];
2182 }
2183
2184 // Check if user is allowed to move files if it's a file
2185 if ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) {
2186 $errors[] = [ 'movenotallowedfile' ];
2187 }
2188
2189 // Check if user is allowed to move category pages if it's a category page
2190 if ( $this->mNamespace == NS_CATEGORY && !$user->isAllowed( 'move-categorypages' ) ) {
2191 $errors[] = [ 'cant-move-category-page' ];
2192 }
2193
2194 if ( !$user->isAllowed( 'move' ) ) {
2195 // User can't move anything
2196 $userCanMove = User::groupHasPermission( 'user', 'move' );
2197 $autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' );
2198 if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
2199 // custom message if logged-in users without any special rights can move
2200 $errors[] = [ 'movenologintext' ];
2201 } else {
2202 $errors[] = [ 'movenotallowed' ];
2203 }
2204 }
2205 } elseif ( $action == 'move-target' ) {
2206 if ( !$user->isAllowed( 'move' ) ) {
2207 // User can't move anything
2208 $errors[] = [ 'movenotallowed' ];
2209 } elseif ( !$user->isAllowed( 'move-rootuserpages' )
2210 && $this->mNamespace == NS_USER && !$this->isSubpage() ) {
2211 // Show user page-specific message only if the user can move other pages
2212 $errors[] = [ 'cant-move-to-user-page' ];
2213 } elseif ( !$user->isAllowed( 'move-categorypages' )
2214 && $this->mNamespace == NS_CATEGORY ) {
2215 // Show category page-specific message only if the user can move other pages
2216 $errors[] = [ 'cant-move-to-category-page' ];
2217 }
2218 } elseif ( !$user->isAllowed( $action ) ) {
2219 $errors[] = $this->missingPermissionError( $action, $short );
2220 }
2221
2222 return $errors;
2223 }
2224
2233 private function resultToError( $errors, $result ) {
2234 if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
2235 // A single array representing an error
2236 $errors[] = $result;
2237 } elseif ( is_array( $result ) && is_array( $result[0] ) ) {
2238 // A nested array representing multiple errors
2239 $errors = array_merge( $errors, $result );
2240 } elseif ( $result !== '' && is_string( $result ) ) {
2241 // A string representing a message-id
2242 $errors[] = [ $result ];
2243 } elseif ( $result instanceof MessageSpecifier ) {
2244 // A message specifier representing an error
2245 $errors[] = [ $result ];
2246 } elseif ( $result === false ) {
2247 // a generic "We don't want them to do that"
2248 $errors[] = [ 'badaccess-group0' ];
2249 }
2250 return $errors;
2251 }
2252
2264 private function checkPermissionHooks( $action, $user, $errors, $rigor, $short ) {
2265 // Use getUserPermissionsErrors instead
2266 $result = '';
2267 // Avoid PHP 7.1 warning from passing $this by reference
2268 $titleRef = $this;
2269 if ( !Hooks::run( 'userCan', [ &$titleRef, &$user, $action, &$result ] ) ) {
2270 return $result ? [] : [ [ 'badaccess-group0' ] ];
2271 }
2272 // Check getUserPermissionsErrors hook
2273 // Avoid PHP 7.1 warning from passing $this by reference
2274 $titleRef = $this;
2275 if ( !Hooks::run( 'getUserPermissionsErrors', [ &$titleRef, &$user, $action, &$result ] ) ) {
2276 $errors = $this->resultToError( $errors, $result );
2277 }
2278 // Check getUserPermissionsErrorsExpensive hook
2279 if (
2280 $rigor !== 'quick'
2281 && !( $short && count( $errors ) > 0 )
2282 && !Hooks::run( 'getUserPermissionsErrorsExpensive', [ &$titleRef, &$user, $action, &$result ] )
2283 ) {
2284 $errors = $this->resultToError( $errors, $result );
2285 }
2286
2287 return $errors;
2288 }
2289
2301 private function checkSpecialsAndNSPermissions( $action, $user, $errors, $rigor, $short ) {
2302 # Only 'createaccount' can be performed on special pages,
2303 # which don't actually exist in the DB.
2304 if ( $this->isSpecialPage() && $action !== 'createaccount' ) {
2305 $errors[] = [ 'ns-specialprotected' ];
2306 }
2307
2308 # Check $wgNamespaceProtection for restricted namespaces
2309 if ( $this->isNamespaceProtected( $user ) ) {
2310 $ns = $this->mNamespace == NS_MAIN ?
2311 wfMessage( 'nstab-main' )->text() : $this->getNsText();
2312 $errors[] = $this->mNamespace == NS_MEDIAWIKI ?
2313 [ 'protectedinterface', $action ] : [ 'namespaceprotected', $ns, $action ];
2314 }
2315
2316 return $errors;
2317 }
2318
2330 private function checkUserConfigPermissions( $action, $user, $errors, $rigor, $short ) {
2331 # Protect css/json/js subpages of user pages
2332 # XXX: this might be better using restrictions
2333
2334 if ( $action != 'patrol' ) {
2335 if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) {
2336 if (
2337 $this->isUserCssConfigPage()
2338 && !$user->isAllowedAny( 'editmyusercss', 'editusercss' )
2339 ) {
2340 $errors[] = [ 'mycustomcssprotected', $action ];
2341 } elseif (
2342 $this->isUserJsonConfigPage()
2343 && !$user->isAllowedAny( 'editmyuserjson', 'edituserjson' )
2344 ) {
2345 $errors[] = [ 'mycustomjsonprotected', $action ];
2346 } elseif (
2347 $this->isUserJsConfigPage()
2348 && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' )
2349 ) {
2350 $errors[] = [ 'mycustomjsprotected', $action ];
2351 }
2352 } else {
2353 if (
2354 $this->isUserCssConfigPage()
2355 && !$user->isAllowed( 'editusercss' )
2356 ) {
2357 $errors[] = [ 'customcssprotected', $action ];
2358 } elseif (
2359 $this->isUserJsonConfigPage()
2360 && !$user->isAllowed( 'edituserjson' )
2361 ) {
2362 $errors[] = [ 'customjsonprotected', $action ];
2363 } elseif (
2364 $this->isUserJsConfigPage()
2365 && !$user->isAllowed( 'edituserjs' )
2366 ) {
2367 $errors[] = [ 'customjsprotected', $action ];
2368 }
2369 }
2370 }
2371
2372 return $errors;
2373 }
2374
2388 private function checkPageRestrictions( $action, $user, $errors, $rigor, $short ) {
2389 foreach ( $this->getRestrictions( $action ) as $right ) {
2390 // Backwards compatibility, rewrite sysop -> editprotected
2391 if ( $right == 'sysop' ) {
2392 $right = 'editprotected';
2393 }
2394 // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
2395 if ( $right == 'autoconfirmed' ) {
2396 $right = 'editsemiprotected';
2397 }
2398 if ( $right == '' ) {
2399 continue;
2400 }
2401 if ( !$user->isAllowed( $right ) ) {
2402 $errors[] = [ 'protectedpagetext', $right, $action ];
2403 } elseif ( $this->mCascadeRestriction && !$user->isAllowed( 'protect' ) ) {
2404 $errors[] = [ 'protectedpagetext', 'protect', $action ];
2405 }
2406 }
2407
2408 return $errors;
2409 }
2410
2422 private function checkCascadingSourcesRestrictions( $action, $user, $errors, $rigor, $short ) {
2423 if ( $rigor !== 'quick' && !$this->isUserConfigPage() ) {
2424 # We /could/ use the protection level on the source page, but it's
2425 # fairly ugly as we have to establish a precedence hierarchy for pages
2426 # included by multiple cascade-protected pages. So just restrict
2427 # it to people with 'protect' permission, as they could remove the
2428 # protection anyway.
2429 list( $cascadingSources, $restrictions ) = $this->getCascadeProtectionSources();
2430 # Cascading protection depends on more than this page...
2431 # Several cascading protected pages may include this page...
2432 # Check each cascading level
2433 # This is only for protection restrictions, not for all actions
2434 if ( isset( $restrictions[$action] ) ) {
2435 foreach ( $restrictions[$action] as $right ) {
2436 // Backwards compatibility, rewrite sysop -> editprotected
2437 if ( $right == 'sysop' ) {
2438 $right = 'editprotected';
2439 }
2440 // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
2441 if ( $right == 'autoconfirmed' ) {
2442 $right = 'editsemiprotected';
2443 }
2444 if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) {
2445 $pages = '';
2446 foreach ( $cascadingSources as $page ) {
2447 $pages .= '* [[:' . $page->getPrefixedText() . "]]\n";
2448 }
2449 $errors[] = [ 'cascadeprotected', count( $cascadingSources ), $pages, $action ];
2450 }
2451 }
2452 }
2453 }
2454
2455 return $errors;
2456 }
2457
2469 private function checkActionPermissions( $action, $user, $errors, $rigor, $short ) {
2471
2472 if ( $action == 'protect' ) {
2473 if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $rigor, true ) ) ) {
2474 // If they can't edit, they shouldn't protect.
2475 $errors[] = [ 'protect-cantedit' ];
2476 }
2477 } elseif ( $action == 'create' ) {
2478 $title_protection = $this->getTitleProtection();
2479 if ( $title_protection ) {
2480 if ( $title_protection['permission'] == ''
2481 || !$user->isAllowed( $title_protection['permission'] )
2482 ) {
2483 $errors[] = [
2484 'titleprotected',
2485 User::whoIs( $title_protection['user'] ),
2486 $title_protection['reason']
2487 ];
2488 }
2489 }
2490 } elseif ( $action == 'move' ) {
2491 // Check for immobile pages
2492 if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
2493 // Specific message for this case
2494 $errors[] = [ 'immobile-source-namespace', $this->getNsText() ];
2495 } elseif ( !$this->isMovable() ) {
2496 // Less specific message for rarer cases
2497 $errors[] = [ 'immobile-source-page' ];
2498 }
2499 } elseif ( $action == 'move-target' ) {
2500 if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
2501 $errors[] = [ 'immobile-target-namespace', $this->getNsText() ];
2502 } elseif ( !$this->isMovable() ) {
2503 $errors[] = [ 'immobile-target-page' ];
2504 }
2505 } elseif ( $action == 'delete' ) {
2506 $tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true );
2507 if ( !$tempErrors ) {
2508 $tempErrors = $this->checkCascadingSourcesRestrictions( 'edit',
2509 $user, $tempErrors, $rigor, true );
2510 }
2511 if ( $tempErrors ) {
2512 // If protection keeps them from editing, they shouldn't be able to delete.
2513 $errors[] = [ 'deleteprotected' ];
2514 }
2515 if ( $rigor !== 'quick' && $wgDeleteRevisionsLimit
2516 && !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion()
2517 ) {
2518 $errors[] = [ 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ];
2519 }
2520 } elseif ( $action === 'undelete' ) {
2521 if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $rigor, true ) ) ) {
2522 // Undeleting implies editing
2523 $errors[] = [ 'undelete-cantedit' ];
2524 }
2525 if ( !$this->exists()
2526 && count( $this->getUserPermissionsErrorsInternal( 'create', $user, $rigor, true ) )
2527 ) {
2528 // Undeleting where nothing currently exists implies creating
2529 $errors[] = [ 'undelete-cantcreate' ];
2530 }
2531 }
2532 return $errors;
2533 }
2534
2546 private function checkUserBlock( $action, $user, $errors, $rigor, $short ) {
2548 // Account creation blocks handled at userlogin.
2549 // Unblocking handled in SpecialUnblock
2550 if ( $rigor === 'quick' || in_array( $action, [ 'createaccount', 'unblock' ] ) ) {
2551 return $errors;
2552 }
2553
2554 // Optimize for a very common case
2555 if ( $action === 'read' && !$wgBlockDisablesLogin ) {
2556 return $errors;
2557 }
2558
2560 && !$user->isEmailConfirmed()
2561 && $action === 'edit'
2562 ) {
2563 $errors[] = [ 'confirmedittext' ];
2564 }
2565
2566 $useSlave = ( $rigor !== 'secure' );
2567 if ( ( $action == 'edit' || $action == 'create' )
2568 && !$user->isBlockedFrom( $this, $useSlave )
2569 ) {
2570 // Don't block the user from editing their own talk page unless they've been
2571 // explicitly blocked from that too.
2572 } elseif ( $user->isBlocked() && $user->getBlock()->prevents( $action ) !== false ) {
2573 // @todo FIXME: Pass the relevant context into this function.
2574 $errors[] = $user->getBlock()->getPermissionsError( RequestContext::getMain() );
2575 }
2576
2577 return $errors;
2578 }
2579
2591 private function checkReadPermissions( $action, $user, $errors, $rigor, $short ) {
2593
2594 $whitelisted = false;
2595 if ( User::isEveryoneAllowed( 'read' ) ) {
2596 # Shortcut for public wikis, allows skipping quite a bit of code
2597 $whitelisted = true;
2598 } elseif ( $user->isAllowed( 'read' ) ) {
2599 # If the user is allowed to read pages, he is allowed to read all pages
2600 $whitelisted = true;
2601 } elseif ( $this->isSpecial( 'Userlogin' )
2602 || $this->isSpecial( 'PasswordReset' )
2603 || $this->isSpecial( 'Userlogout' )
2604 ) {
2605 # Always grant access to the login page.
2606 # Even anons need to be able to log in.
2607 $whitelisted = true;
2608 } elseif ( is_array( $wgWhitelistRead ) && count( $wgWhitelistRead ) ) {
2609 # Time to check the whitelist
2610 # Only do these checks is there's something to check against
2611 $name = $this->getPrefixedText();
2612 $dbName = $this->getPrefixedDBkey();
2613
2614 // Check for explicit whitelisting with and without underscores
2615 if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) {
2616 $whitelisted = true;
2617 } elseif ( $this->getNamespace() == NS_MAIN ) {
2618 # Old settings might have the title prefixed with
2619 # a colon for main-namespace pages
2620 if ( in_array( ':' . $name, $wgWhitelistRead ) ) {
2621 $whitelisted = true;
2622 }
2623 } elseif ( $this->isSpecialPage() ) {
2624 # If it's a special page, ditch the subpage bit and check again
2625 $name = $this->getDBkey();
2626 list( $name, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $name );
2627 if ( $name ) {
2628 $pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
2629 if ( in_array( $pure, $wgWhitelistRead, true ) ) {
2630 $whitelisted = true;
2631 }
2632 }
2633 }
2634 }
2635
2636 if ( !$whitelisted && is_array( $wgWhitelistReadRegexp ) && !empty( $wgWhitelistReadRegexp ) ) {
2637 $name = $this->getPrefixedText();
2638 // Check for regex whitelisting
2639 foreach ( $wgWhitelistReadRegexp as $listItem ) {
2640 if ( preg_match( $listItem, $name ) ) {
2641 $whitelisted = true;
2642 break;
2643 }
2644 }
2645 }
2646
2647 if ( !$whitelisted ) {
2648 # If the title is not whitelisted, give extensions a chance to do so...
2649 Hooks::run( 'TitleReadWhitelist', [ $this, $user, &$whitelisted ] );
2650 if ( !$whitelisted ) {
2651 $errors[] = $this->missingPermissionError( $action, $short );
2652 }
2653 }
2654
2655 return $errors;
2656 }
2657
2666 private function missingPermissionError( $action, $short ) {
2667 // We avoid expensive display logic for quickUserCan's and such
2668 if ( $short ) {
2669 return [ 'badaccess-group0' ];
2670 }
2671
2672 return User::newFatalPermissionDeniedStatus( $action )->getErrorsArray()[0];
2673 }
2674
2690 $action, $user, $rigor = 'secure', $short = false
2691 ) {
2692 if ( $rigor === true ) {
2693 $rigor = 'secure'; // b/c
2694 } elseif ( $rigor === false ) {
2695 $rigor = 'quick'; // b/c
2696 } elseif ( !in_array( $rigor, [ 'quick', 'full', 'secure' ] ) ) {
2697 throw new Exception( "Invalid rigor parameter '$rigor'." );
2698 }
2699
2700 # Read has special handling
2701 if ( $action == 'read' ) {
2702 $checks = [
2703 'checkPermissionHooks',
2704 'checkReadPermissions',
2705 'checkUserBlock', // for wgBlockDisablesLogin
2706 ];
2707 # Don't call checkSpecialsAndNSPermissions or checkUserConfigPermissions
2708 # here as it will lead to duplicate error messages. This is okay to do
2709 # since anywhere that checks for create will also check for edit, and
2710 # those checks are called for edit.
2711 } elseif ( $action == 'create' ) {
2712 $checks = [
2713 'checkQuickPermissions',
2714 'checkPermissionHooks',
2715 'checkPageRestrictions',
2716 'checkCascadingSourcesRestrictions',
2717 'checkActionPermissions',
2718 'checkUserBlock'
2719 ];
2720 } else {
2721 $checks = [
2722 'checkQuickPermissions',
2723 'checkPermissionHooks',
2724 'checkSpecialsAndNSPermissions',
2725 'checkUserConfigPermissions',
2726 'checkPageRestrictions',
2727 'checkCascadingSourcesRestrictions',
2728 'checkActionPermissions',
2729 'checkUserBlock'
2730 ];
2731 }
2732
2733 $errors = [];
2734 while ( count( $checks ) > 0 &&
2735 !( $short && count( $errors ) > 0 ) ) {
2736 $method = array_shift( $checks );
2737 $errors = $this->$method( $action, $user, $errors, $rigor, $short );
2738 }
2739
2740 return $errors;
2741 }
2742
2750 public static function getFilteredRestrictionTypes( $exists = true ) {
2751 global $wgRestrictionTypes;
2752 $types = $wgRestrictionTypes;
2753 if ( $exists ) {
2754 # Remove the create restriction for existing titles
2755 $types = array_diff( $types, [ 'create' ] );
2756 } else {
2757 # Only the create and upload restrictions apply to non-existing titles
2758 $types = array_intersect( $types, [ 'create', 'upload' ] );
2759 }
2760 return $types;
2761 }
2762
2768 public function getRestrictionTypes() {
2769 if ( $this->isSpecialPage() ) {
2770 return [];
2771 }
2772
2773 $types = self::getFilteredRestrictionTypes( $this->exists() );
2774
2775 if ( $this->getNamespace() != NS_FILE ) {
2776 # Remove the upload restriction for non-file titles
2777 $types = array_diff( $types, [ 'upload' ] );
2778 }
2779
2780 Hooks::run( 'TitleGetRestrictionTypes', [ $this, &$types ] );
2781
2782 wfDebug( __METHOD__ . ': applicable restrictions to [[' .
2783 $this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" );
2784
2785 return $types;
2786 }
2787
2795 public function getTitleProtection() {
2796 $protection = $this->getTitleProtectionInternal();
2797 if ( $protection ) {
2798 if ( $protection['permission'] == 'sysop' ) {
2799 $protection['permission'] = 'editprotected'; // B/C
2800 }
2801 if ( $protection['permission'] == 'autoconfirmed' ) {
2802 $protection['permission'] = 'editsemiprotected'; // B/C
2803 }
2804 }
2805 return $protection;
2806 }
2807
2818 protected function getTitleProtectionInternal() {
2819 // Can't protect pages in special namespaces
2820 if ( $this->getNamespace() < 0 ) {
2821 return false;
2822 }
2823
2824 // Can't protect pages that exist.
2825 if ( $this->exists() ) {
2826 return false;
2827 }
2828
2829 if ( $this->mTitleProtection === null ) {
2830 $dbr = wfGetDB( DB_REPLICA );
2831 $commentStore = CommentStore::getStore();
2832 $commentQuery = $commentStore->getJoin( 'pt_reason' );
2833 $res = $dbr->select(
2834 [ 'protected_titles' ] + $commentQuery['tables'],
2835 [
2836 'user' => 'pt_user',
2837 'expiry' => 'pt_expiry',
2838 'permission' => 'pt_create_perm'
2839 ] + $commentQuery['fields'],
2840 [ 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ],
2841 __METHOD__,
2842 [],
2843 $commentQuery['joins']
2844 );
2845
2846 // fetchRow returns false if there are no rows.
2847 $row = $dbr->fetchRow( $res );
2848 if ( $row ) {
2849 $this->mTitleProtection = [
2850 'user' => $row['user'],
2851 'expiry' => $dbr->decodeExpiry( $row['expiry'] ),
2852 'permission' => $row['permission'],
2853 'reason' => $commentStore->getComment( 'pt_reason', $row )->text,
2854 ];
2855 } else {
2856 $this->mTitleProtection = false;
2857 }
2858 }
2859 return $this->mTitleProtection;
2860 }
2861
2865 public function deleteTitleProtection() {
2866 $dbw = wfGetDB( DB_MASTER );
2867
2868 $dbw->delete(
2869 'protected_titles',
2870 [ 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ],
2871 __METHOD__
2872 );
2873 $this->mTitleProtection = false;
2874 }
2875
2883 public function isSemiProtected( $action = 'edit' ) {
2885
2886 $restrictions = $this->getRestrictions( $action );
2888 if ( !$restrictions || !$semi ) {
2889 // Not protected, or all protection is full protection
2890 return false;
2891 }
2892
2893 // Remap autoconfirmed to editsemiprotected for BC
2894 foreach ( array_keys( $semi, 'autoconfirmed' ) as $key ) {
2895 $semi[$key] = 'editsemiprotected';
2896 }
2897 foreach ( array_keys( $restrictions, 'autoconfirmed' ) as $key ) {
2898 $restrictions[$key] = 'editsemiprotected';
2899 }
2900
2901 return !array_diff( $restrictions, $semi );
2902 }
2903
2911 public function isProtected( $action = '' ) {
2912 global $wgRestrictionLevels;
2913
2914 $restrictionTypes = $this->getRestrictionTypes();
2915
2916 # Special pages have inherent protection
2917 if ( $this->isSpecialPage() ) {
2918 return true;
2919 }
2920
2921 # Check regular protection levels
2922 foreach ( $restrictionTypes as $type ) {
2923 if ( $action == $type || $action == '' ) {
2924 $r = $this->getRestrictions( $type );
2925 foreach ( $wgRestrictionLevels as $level ) {
2926 if ( in_array( $level, $r ) && $level != '' ) {
2927 return true;
2928 }
2929 }
2930 }
2931 }
2932
2933 return false;
2934 }
2935
2943 public function isNamespaceProtected( User $user ) {
2945
2946 if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) {
2947 foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) {
2948 if ( $right != '' && !$user->isAllowed( $right ) ) {
2949 return true;
2950 }
2951 }
2952 }
2953 return false;
2954 }
2955
2961 public function isCascadeProtected() {
2962 list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false );
2963 return ( $sources > 0 );
2964 }
2965
2975 public function areCascadeProtectionSourcesLoaded( $getPages = true ) {
2976 return $getPages ? $this->mCascadeSources !== null : $this->mHasCascadingRestrictions !== null;
2977 }
2978
2992 public function getCascadeProtectionSources( $getPages = true ) {
2993 $pagerestrictions = [];
2994
2995 if ( $this->mCascadeSources !== null && $getPages ) {
2996 return [ $this->mCascadeSources, $this->mCascadingRestrictions ];
2997 } elseif ( $this->mHasCascadingRestrictions !== null && !$getPages ) {
2998 return [ $this->mHasCascadingRestrictions, $pagerestrictions ];
2999 }
3000
3001 $dbr = wfGetDB( DB_REPLICA );
3002
3003 if ( $this->getNamespace() == NS_FILE ) {
3004 $tables = [ 'imagelinks', 'page_restrictions' ];
3005 $where_clauses = [
3006 'il_to' => $this->getDBkey(),
3007 'il_from=pr_page',
3008 'pr_cascade' => 1
3009 ];
3010 } else {
3011 $tables = [ 'templatelinks', 'page_restrictions' ];
3012 $where_clauses = [
3013 'tl_namespace' => $this->getNamespace(),
3014 'tl_title' => $this->getDBkey(),
3015 'tl_from=pr_page',
3016 'pr_cascade' => 1
3017 ];
3018 }
3019
3020 if ( $getPages ) {
3021 $cols = [ 'pr_page', 'page_namespace', 'page_title',
3022 'pr_expiry', 'pr_type', 'pr_level' ];
3023 $where_clauses[] = 'page_id=pr_page';
3024 $tables[] = 'page';
3025 } else {
3026 $cols = [ 'pr_expiry' ];
3027 }
3028
3029 $res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ );
3030
3031 $sources = $getPages ? [] : false;
3032 $now = wfTimestampNow();
3033
3034 foreach ( $res as $row ) {
3035 $expiry = $dbr->decodeExpiry( $row->pr_expiry );
3036 if ( $expiry > $now ) {
3037 if ( $getPages ) {
3038 $page_id = $row->pr_page;
3039 $page_ns = $row->page_namespace;
3040 $page_title = $row->page_title;
3041 $sources[$page_id] = self::makeTitle( $page_ns, $page_title );
3042 # Add groups needed for each restriction type if its not already there
3043 # Make sure this restriction type still exists
3044
3045 if ( !isset( $pagerestrictions[$row->pr_type] ) ) {
3046 $pagerestrictions[$row->pr_type] = [];
3047 }
3048
3049 if (
3050 isset( $pagerestrictions[$row->pr_type] )
3051 && !in_array( $row->pr_level, $pagerestrictions[$row->pr_type] )
3052 ) {
3053 $pagerestrictions[$row->pr_type][] = $row->pr_level;
3054 }
3055 } else {
3056 $sources = true;
3057 }
3058 }
3059 }
3060
3061 if ( $getPages ) {
3062 $this->mCascadeSources = $sources;
3063 $this->mCascadingRestrictions = $pagerestrictions;
3064 } else {
3065 $this->mHasCascadingRestrictions = $sources;
3066 }
3067
3068 return [ $sources, $pagerestrictions ];
3069 }
3070
3078 public function areRestrictionsLoaded() {
3079 return $this->mRestrictionsLoaded;
3080 }
3081
3091 public function getRestrictions( $action ) {
3092 if ( !$this->mRestrictionsLoaded ) {
3093 $this->loadRestrictions();
3094 }
3095 return isset( $this->mRestrictions[$action] )
3096 ? $this->mRestrictions[$action]
3097 : [];
3098 }
3099
3107 public function getAllRestrictions() {
3108 if ( !$this->mRestrictionsLoaded ) {
3109 $this->loadRestrictions();
3110 }
3111 return $this->mRestrictions;
3112 }
3113
3121 public function getRestrictionExpiry( $action ) {
3122 if ( !$this->mRestrictionsLoaded ) {
3123 $this->loadRestrictions();
3124 }
3125 return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false;
3126 }
3127
3134 if ( !$this->mRestrictionsLoaded ) {
3135 $this->loadRestrictions();
3136 }
3137
3138 return $this->mCascadeRestriction;
3139 }
3140
3152 public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) {
3153 $dbr = wfGetDB( DB_REPLICA );
3154
3155 $restrictionTypes = $this->getRestrictionTypes();
3156
3157 foreach ( $restrictionTypes as $type ) {
3158 $this->mRestrictions[$type] = [];
3159 $this->mRestrictionsExpiry[$type] = 'infinity';
3160 }
3161
3162 $this->mCascadeRestriction = false;
3163
3164 # Backwards-compatibility: also load the restrictions from the page record (old format).
3165 if ( $oldFashionedRestrictions !== null ) {
3166 $this->mOldRestrictions = $oldFashionedRestrictions;
3167 }
3168
3169 if ( $this->mOldRestrictions === false ) {
3170 $this->mOldRestrictions = $dbr->selectField( 'page', 'page_restrictions',
3171 [ 'page_id' => $this->getArticleID() ], __METHOD__ );
3172 }
3173
3174 if ( $this->mOldRestrictions != '' ) {
3175 foreach ( explode( ':', trim( $this->mOldRestrictions ) ) as $restrict ) {
3176 $temp = explode( '=', trim( $restrict ) );
3177 if ( count( $temp ) == 1 ) {
3178 // old old format should be treated as edit/move restriction
3179 $this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) );
3180 $this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) );
3181 } else {
3182 $restriction = trim( $temp[1] );
3183 if ( $restriction != '' ) { // some old entries are empty
3184 $this->mRestrictions[$temp[0]] = explode( ',', $restriction );
3185 }
3186 }
3187 }
3188 }
3189
3190 if ( count( $rows ) ) {
3191 # Current system - load second to make them override.
3192 $now = wfTimestampNow();
3193
3194 # Cycle through all the restrictions.
3195 foreach ( $rows as $row ) {
3196 // Don't take care of restrictions types that aren't allowed
3197 if ( !in_array( $row->pr_type, $restrictionTypes ) ) {
3198 continue;
3199 }
3200
3201 $expiry = $dbr->decodeExpiry( $row->pr_expiry );
3202
3203 // Only apply the restrictions if they haven't expired!
3204 if ( !$expiry || $expiry > $now ) {
3205 $this->mRestrictionsExpiry[$row->pr_type] = $expiry;
3206 $this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) );
3207
3208 $this->mCascadeRestriction |= $row->pr_cascade;
3209 }
3210 }
3211 }
3212
3213 $this->mRestrictionsLoaded = true;
3214 }
3215
3224 public function loadRestrictions( $oldFashionedRestrictions = null ) {
3225 if ( $this->mRestrictionsLoaded ) {
3226 return;
3227 }
3228
3229 $id = $this->getArticleID();
3230 if ( $id ) {
3231 $cache = ObjectCache::getMainWANInstance();
3232 $rows = $cache->getWithSetCallback(
3233 // Page protections always leave a new null revision
3234 $cache->makeKey( 'page-restrictions', $id, $this->getLatestRevID() ),
3235 $cache::TTL_DAY,
3236 function ( $curValue, &$ttl, array &$setOpts ) {
3237 $dbr = wfGetDB( DB_REPLICA );
3238
3239 $setOpts += Database::getCacheSetOptions( $dbr );
3240
3241 return iterator_to_array(
3242 $dbr->select(
3243 'page_restrictions',
3244 [ 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ],
3245 [ 'pr_page' => $this->getArticleID() ],
3246 __METHOD__
3247 )
3248 );
3249 }
3250 );
3251
3252 $this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions );
3253 } else {
3254 $title_protection = $this->getTitleProtectionInternal();
3255
3256 if ( $title_protection ) {
3257 $now = wfTimestampNow();
3258 $expiry = wfGetDB( DB_REPLICA )->decodeExpiry( $title_protection['expiry'] );
3259
3260 if ( !$expiry || $expiry > $now ) {
3261 // Apply the restrictions
3262 $this->mRestrictionsExpiry['create'] = $expiry;
3263 $this->mRestrictions['create'] =
3264 explode( ',', trim( $title_protection['permission'] ) );
3265 } else { // Get rid of the old restrictions
3266 $this->mTitleProtection = false;
3267 }
3268 } else {
3269 $this->mRestrictionsExpiry['create'] = 'infinity';
3270 }
3271 $this->mRestrictionsLoaded = true;
3272 }
3273 }
3274
3279 public function flushRestrictions() {
3280 $this->mRestrictionsLoaded = false;
3281 $this->mTitleProtection = null;
3282 }
3283
3289 static function purgeExpiredRestrictions() {
3290 if ( wfReadOnly() ) {
3291 return;
3292 }
3293
3294 DeferredUpdates::addUpdate( new AtomicSectionUpdate(
3295 wfGetDB( DB_MASTER ),
3296 __METHOD__,
3297 function ( IDatabase $dbw, $fname ) {
3298 $config = MediaWikiServices::getInstance()->getMainConfig();
3299 $ids = $dbw->selectFieldValues(
3300 'page_restrictions',
3301 'pr_id',
3302 [ 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
3303 $fname,
3304 [ 'LIMIT' => $config->get( 'UpdateRowsPerQuery' ) ] // T135470
3305 );
3306 if ( $ids ) {
3307 $dbw->delete( 'page_restrictions', [ 'pr_id' => $ids ], $fname );
3308 }
3309 }
3310 ) );
3311
3312 DeferredUpdates::addUpdate( new AtomicSectionUpdate(
3313 wfGetDB( DB_MASTER ),
3314 __METHOD__,
3315 function ( IDatabase $dbw, $fname ) {
3316 $dbw->delete(
3317 'protected_titles',
3318 [ 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
3319 $fname
3320 );
3321 }
3322 ) );
3323 }
3324
3330 public function hasSubpages() {
3331 if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
3332 # Duh
3333 return false;
3334 }
3335
3336 # We dynamically add a member variable for the purpose of this method
3337 # alone to cache the result. There's no point in having it hanging
3338 # around uninitialized in every Title object; therefore we only add it
3339 # if needed and don't declare it statically.
3340 if ( $this->mHasSubpages === null ) {
3341 $this->mHasSubpages = false;
3342 $subpages = $this->getSubpages( 1 );
3343 if ( $subpages instanceof TitleArray ) {
3344 $this->mHasSubpages = (bool)$subpages->count();
3345 }
3346 }
3347
3348 return $this->mHasSubpages;
3349 }
3350
3358 public function getSubpages( $limit = -1 ) {
3359 if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) {
3360 return [];
3361 }
3362
3363 $dbr = wfGetDB( DB_REPLICA );
3364 $conds['page_namespace'] = $this->getNamespace();
3365 $conds[] = 'page_title ' . $dbr->buildLike( $this->getDBkey() . '/', $dbr->anyString() );
3366 $options = [];
3367 if ( $limit > -1 ) {
3368 $options['LIMIT'] = $limit;
3369 }
3371 $dbr->select( 'page',
3372 [ 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ],
3373 $conds,
3374 __METHOD__,
3375 $options
3376 )
3377 );
3378 }
3379
3385 public function isDeleted() {
3386 if ( $this->getNamespace() < 0 ) {
3387 $n = 0;
3388 } else {
3389 $dbr = wfGetDB( DB_REPLICA );
3390
3391 $n = $dbr->selectField( 'archive', 'COUNT(*)',
3392 [ 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ],
3393 __METHOD__
3394 );
3395 if ( $this->getNamespace() == NS_FILE ) {
3396 $n += $dbr->selectField( 'filearchive', 'COUNT(*)',
3397 [ 'fa_name' => $this->getDBkey() ],
3398 __METHOD__
3399 );
3400 }
3401 }
3402 return (int)$n;
3403 }
3404
3410 public function isDeletedQuick() {
3411 if ( $this->getNamespace() < 0 ) {
3412 return false;
3413 }
3414 $dbr = wfGetDB( DB_REPLICA );
3415 $deleted = (bool)$dbr->selectField( 'archive', '1',
3416 [ 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ],
3417 __METHOD__
3418 );
3419 if ( !$deleted && $this->getNamespace() == NS_FILE ) {
3420 $deleted = (bool)$dbr->selectField( 'filearchive', '1',
3421 [ 'fa_name' => $this->getDBkey() ],
3422 __METHOD__
3423 );
3424 }
3425 return $deleted;
3426 }
3427
3436 public function getArticleID( $flags = 0 ) {
3437 if ( $this->getNamespace() < 0 ) {
3438 $this->mArticleID = 0;
3439 return $this->mArticleID;
3440 }
3441 $linkCache = LinkCache::singleton();
3442 if ( $flags & self::GAID_FOR_UPDATE ) {
3443 $oldUpdate = $linkCache->forUpdate( true );
3444 $linkCache->clearLink( $this );
3445 $this->mArticleID = $linkCache->addLinkObj( $this );
3446 $linkCache->forUpdate( $oldUpdate );
3447 } else {
3448 if ( -1 == $this->mArticleID ) {
3449 $this->mArticleID = $linkCache->addLinkObj( $this );
3450 }
3451 }
3452 return $this->mArticleID;
3453 }
3454
3462 public function isRedirect( $flags = 0 ) {
3463 if ( !is_null( $this->mRedirect ) ) {
3464 return $this->mRedirect;
3465 }
3466 if ( !$this->getArticleID( $flags ) ) {
3467 $this->mRedirect = false;
3468 return $this->mRedirect;
3469 }
3470
3471 $linkCache = LinkCache::singleton();
3472 $linkCache->addLinkObj( $this ); # in case we already had an article ID
3473 $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' );
3474 if ( $cached === null ) {
3475 # Trust LinkCache's state over our own
3476 # LinkCache is telling us that the page doesn't exist, despite there being cached
3477 # data relating to an existing page in $this->mArticleID. Updaters should clear
3478 # LinkCache as appropriate, or use $flags = Title::GAID_FOR_UPDATE. If that flag is
3479 # set, then LinkCache will definitely be up to date here, since getArticleID() forces
3480 # LinkCache to refresh its data from the master.
3481 $this->mRedirect = false;
3482 return $this->mRedirect;
3483 }
3484
3485 $this->mRedirect = (bool)$cached;
3486
3487 return $this->mRedirect;
3488 }
3489
3497 public function getLength( $flags = 0 ) {
3498 if ( $this->mLength != -1 ) {
3499 return $this->mLength;
3500 }
3501 if ( !$this->getArticleID( $flags ) ) {
3502 $this->mLength = 0;
3503 return $this->mLength;
3504 }
3505 $linkCache = LinkCache::singleton();
3506 $linkCache->addLinkObj( $this ); # in case we already had an article ID
3507 $cached = $linkCache->getGoodLinkFieldObj( $this, 'length' );
3508 if ( $cached === null ) {
3509 # Trust LinkCache's state over our own, as for isRedirect()
3510 $this->mLength = 0;
3511 return $this->mLength;
3512 }
3513
3514 $this->mLength = intval( $cached );
3515
3516 return $this->mLength;
3517 }
3518
3525 public function getLatestRevID( $flags = 0 ) {
3526 if ( !( $flags & self::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) {
3527 return intval( $this->mLatestID );
3528 }
3529 if ( !$this->getArticleID( $flags ) ) {
3530 $this->mLatestID = 0;
3531 return $this->mLatestID;
3532 }
3533 $linkCache = LinkCache::singleton();
3534 $linkCache->addLinkObj( $this ); # in case we already had an article ID
3535 $cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' );
3536 if ( $cached === null ) {
3537 # Trust LinkCache's state over our own, as for isRedirect()
3538 $this->mLatestID = 0;
3539 return $this->mLatestID;
3540 }
3541
3542 $this->mLatestID = intval( $cached );
3543
3544 return $this->mLatestID;
3545 }
3546
3557 public function resetArticleID( $newid ) {
3558 $linkCache = LinkCache::singleton();
3559 $linkCache->clearLink( $this );
3560
3561 if ( $newid === false ) {
3562 $this->mArticleID = -1;
3563 } else {
3564 $this->mArticleID = intval( $newid );
3565 }
3566 $this->mRestrictionsLoaded = false;
3567 $this->mRestrictions = [];
3568 $this->mOldRestrictions = false;
3569 $this->mRedirect = null;
3570 $this->mLength = -1;
3571 $this->mLatestID = false;
3572 $this->mContentModel = false;
3573 $this->mEstimateRevisions = null;
3574 $this->mPageLanguage = false;
3575 $this->mDbPageLanguage = false;
3576 $this->mIsBigDeletion = null;
3577 }
3578
3579 public static function clearCaches() {
3580 $linkCache = LinkCache::singleton();
3581 $linkCache->clear();
3582
3583 $titleCache = self::getTitleCache();
3584 $titleCache->clear();
3585 }
3586
3594 public static function capitalize( $text, $ns = NS_MAIN ) {
3595 global $wgContLang;
3596
3597 if ( MWNamespace::isCapitalized( $ns ) ) {
3598 return $wgContLang->ucfirst( $text );
3599 } else {
3600 return $text;
3601 }
3602 }
3603
3616 private function secureAndSplit() {
3617 # Initialisation
3618 $this->mInterwiki = '';
3619 $this->mFragment = '';
3620 $this->mNamespace = $this->mDefaultNamespace; # Usually NS_MAIN
3621
3622 $dbkey = $this->mDbkeyform;
3623
3624 // @note: splitTitleString() is a temporary hack to allow MediaWikiTitleCodec to share
3625 // the parsing code with Title, while avoiding massive refactoring.
3626 // @todo: get rid of secureAndSplit, refactor parsing code.
3627 // @note: getTitleParser() returns a TitleParser implementation which does not have a
3628 // splitTitleString method, but the only implementation (MediaWikiTitleCodec) does
3629 $titleCodec = MediaWikiServices::getInstance()->getTitleParser();
3630 // MalformedTitleException can be thrown here
3631 $parts = $titleCodec->splitTitleString( $dbkey, $this->getDefaultNamespace() );
3632
3633 # Fill fields
3634 $this->setFragment( '#' . $parts['fragment'] );
3635 $this->mInterwiki = $parts['interwiki'];
3636 $this->mLocalInterwiki = $parts['local_interwiki'];
3637 $this->mNamespace = $parts['namespace'];
3638 $this->mUserCaseDBKey = $parts['user_case_dbkey'];
3639
3640 $this->mDbkeyform = $parts['dbkey'];
3641 $this->mUrlform = wfUrlencode( $this->mDbkeyform );
3642 $this->mTextform = strtr( $this->mDbkeyform, '_', ' ' );
3643
3644 # We already know that some pages won't be in the database!
3645 if ( $this->isExternal() || $this->isSpecialPage() ) {
3646 $this->mArticleID = 0;
3647 }
3648
3649 return true;
3650 }
3651
3664 public function getLinksTo( $options = [], $table = 'pagelinks', $prefix = 'pl' ) {
3665 if ( count( $options ) > 0 ) {
3666 $db = wfGetDB( DB_MASTER );
3667 } else {
3668 $db = wfGetDB( DB_REPLICA );
3669 }
3670
3671 $res = $db->select(
3672 [ 'page', $table ],
3673 self::getSelectFields(),
3674 [
3675 "{$prefix}_from=page_id",
3676 "{$prefix}_namespace" => $this->getNamespace(),
3677 "{$prefix}_title" => $this->getDBkey() ],
3678 __METHOD__,
3679 $options
3680 );
3681
3682 $retVal = [];
3683 if ( $res->numRows() ) {
3684 $linkCache = LinkCache::singleton();
3685 foreach ( $res as $row ) {
3686 $titleObj = self::makeTitle( $row->page_namespace, $row->page_title );
3687 if ( $titleObj ) {
3688 $linkCache->addGoodLinkObjFromRow( $titleObj, $row );
3689 $retVal[] = $titleObj;
3690 }
3691 }
3692 }
3693 return $retVal;
3694 }
3695
3706 public function getTemplateLinksTo( $options = [] ) {
3707 return $this->getLinksTo( $options, 'templatelinks', 'tl' );
3708 }
3709
3722 public function getLinksFrom( $options = [], $table = 'pagelinks', $prefix = 'pl' ) {
3723 $id = $this->getArticleID();
3724
3725 # If the page doesn't exist; there can't be any link from this page
3726 if ( !$id ) {
3727 return [];
3728 }
3729
3730 $db = wfGetDB( DB_REPLICA );
3731
3732 $blNamespace = "{$prefix}_namespace";
3733 $blTitle = "{$prefix}_title";
3734
3735 $pageQuery = WikiPage::getQueryInfo();
3736 $res = $db->select(
3737 [ $table, 'nestpage' => $pageQuery['tables'] ],
3738 array_merge(
3739 [ $blNamespace, $blTitle ],
3740 $pageQuery['fields']
3741 ),
3742 [ "{$prefix}_from" => $id ],
3743 __METHOD__,
3744 $options,
3745 [ 'nestpage' => [
3746 'LEFT JOIN',
3747 [ "page_namespace=$blNamespace", "page_title=$blTitle" ]
3748 ] ] + $pageQuery['joins']
3749 );
3750
3751 $retVal = [];
3752 $linkCache = LinkCache::singleton();
3753 foreach ( $res as $row ) {
3754 if ( $row->page_id ) {
3755 $titleObj = self::newFromRow( $row );
3756 } else {
3757 $titleObj = self::makeTitle( $row->$blNamespace, $row->$blTitle );
3758 $linkCache->addBadLinkObj( $titleObj );
3759 }
3760 $retVal[] = $titleObj;
3761 }
3762
3763 return $retVal;
3764 }
3765
3776 public function getTemplateLinksFrom( $options = [] ) {
3777 return $this->getLinksFrom( $options, 'templatelinks', 'tl' );
3778 }
3779
3788 public function getBrokenLinksFrom() {
3789 if ( $this->getArticleID() == 0 ) {
3790 # All links from article ID 0 are false positives
3791 return [];
3792 }
3793
3794 $dbr = wfGetDB( DB_REPLICA );
3795 $res = $dbr->select(
3796 [ 'page', 'pagelinks' ],
3797 [ 'pl_namespace', 'pl_title' ],
3798 [
3799 'pl_from' => $this->getArticleID(),
3800 'page_namespace IS NULL'
3801 ],
3802 __METHOD__, [],
3803 [
3804 'page' => [
3805 'LEFT JOIN',
3806 [ 'pl_namespace=page_namespace', 'pl_title=page_title' ]
3807 ]
3808 ]
3809 );
3810
3811 $retVal = [];
3812 foreach ( $res as $row ) {
3813 $retVal[] = self::makeTitle( $row->pl_namespace, $row->pl_title );
3814 }
3815 return $retVal;
3816 }
3817
3824 public function getCdnUrls() {
3825 $urls = [
3826 $this->getInternalURL(),
3827 $this->getInternalURL( 'action=history' )
3828 ];
3829
3830 $pageLang = $this->getPageLanguage();
3831 if ( $pageLang->hasVariants() ) {
3832 $variants = $pageLang->getVariants();
3833 foreach ( $variants as $vCode ) {
3834 $urls[] = $this->getInternalURL( $vCode );
3835 }
3836 }
3837
3838 // If we are looking at a css/js user subpage, purge the action=raw.
3839 if ( $this->isUserJsConfigPage() ) {
3840 $urls[] = $this->getInternalURL( 'action=raw&ctype=text/javascript' );
3841 } elseif ( $this->isUserJsonConfigPage() ) {
3842 $urls[] = $this->getInternalURL( 'action=raw&ctype=application/json' );
3843 } elseif ( $this->isUserCssConfigPage() ) {
3844 $urls[] = $this->getInternalURL( 'action=raw&ctype=text/css' );
3845 }
3846
3847 Hooks::run( 'TitleSquidURLs', [ $this, &$urls ] );
3848 return $urls;
3849 }
3850
3854 public function getSquidURLs() {
3855 return $this->getCdnUrls();
3856 }
3857
3861 public function purgeSquid() {
3862 DeferredUpdates::addUpdate(
3863 new CdnCacheUpdate( $this->getCdnUrls() ),
3864 DeferredUpdates::PRESEND
3865 );
3866 }
3867
3878 public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) {
3879 global $wgUser;
3880
3881 if ( !( $nt instanceof Title ) ) {
3882 // Normally we'd add this to $errors, but we'll get
3883 // lots of syntax errors if $nt is not an object
3884 return [ [ 'badtitletext' ] ];
3885 }
3886
3887 $mp = new MovePage( $this, $nt );
3888 $errors = $mp->isValidMove()->getErrorsArray();
3889 if ( $auth ) {
3890 $errors = wfMergeErrorArrays(
3891 $errors,
3892 $mp->checkPermissions( $wgUser, $reason )->getErrorsArray()
3893 );
3894 }
3895
3896 return $errors ?: true;
3897 }
3898
3905 protected function validateFileMoveOperation( $nt ) {
3906 global $wgUser;
3907
3908 $errors = [];
3909
3910 $destFile = wfLocalFile( $nt );
3911 $destFile->load( File::READ_LATEST );
3912 if ( !$wgUser->isAllowed( 'reupload-shared' )
3913 && !$destFile->exists() && wfFindFile( $nt )
3914 ) {
3915 $errors[] = [ 'file-exists-sharedrepo' ];
3916 }
3917
3918 return $errors;
3919 }
3920
3934 public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true,
3935 array $changeTags = []
3936 ) {
3937 global $wgUser;
3938 $err = $this->isValidMoveOperation( $nt, $auth, $reason );
3939 if ( is_array( $err ) ) {
3940 // Auto-block user's IP if the account was "hard" blocked
3941 $wgUser->spreadAnyEditBlock();
3942 return $err;
3943 }
3944 // Check suppressredirect permission
3945 if ( $auth && !$wgUser->isAllowed( 'suppressredirect' ) ) {
3946 $createRedirect = true;
3947 }
3948
3949 $mp = new MovePage( $this, $nt );
3950 $status = $mp->move( $wgUser, $reason, $createRedirect, $changeTags );
3951 if ( $status->isOK() ) {
3952 return true;
3953 } else {
3954 return $status->getErrorsArray();
3955 }
3956 }
3957
3972 public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true,
3973 array $changeTags = []
3974 ) {
3975 global $wgMaximumMovedPages;
3976 // Check permissions
3977 if ( !$this->userCan( 'move-subpages' ) ) {
3978 return [
3979 [ 'cant-move-subpages' ],
3980 ];
3981 }
3982 // Do the source and target namespaces support subpages?
3983 if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) {
3984 return [
3985 [ 'namespace-nosubpages', MWNamespace::getCanonicalName( $this->getNamespace() ) ],
3986 ];
3987 }
3988 if ( !MWNamespace::hasSubpages( $nt->getNamespace() ) ) {
3989 return [
3990 [ 'namespace-nosubpages', MWNamespace::getCanonicalName( $nt->getNamespace() ) ],
3991 ];
3992 }
3993
3994 $subpages = $this->getSubpages( $wgMaximumMovedPages + 1 );
3995 $retval = [];
3996 $count = 0;
3997 foreach ( $subpages as $oldSubpage ) {
3998 $count++;
3999 if ( $count > $wgMaximumMovedPages ) {
4000 $retval[$oldSubpage->getPrefixedText()] = [
4001 [ 'movepage-max-pages', $wgMaximumMovedPages ],
4002 ];
4003 break;
4004 }
4005
4006 // We don't know whether this function was called before
4007 // or after moving the root page, so check both
4008 // $this and $nt
4009 if ( $oldSubpage->getArticleID() == $this->getArticleID()
4010 || $oldSubpage->getArticleID() == $nt->getArticleID()
4011 ) {
4012 // When moving a page to a subpage of itself,
4013 // don't move it twice
4014 continue;
4015 }
4016 $newPageName = preg_replace(
4017 '#^' . preg_quote( $this->getDBkey(), '#' ) . '#',
4018 StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # T23234
4019 $oldSubpage->getDBkey() );
4020 if ( $oldSubpage->isTalkPage() ) {
4021 $newNs = $nt->getTalkPage()->getNamespace();
4022 } else {
4023 $newNs = $nt->getSubjectPage()->getNamespace();
4024 }
4025 # T16385: we need makeTitleSafe because the new page names may
4026 # be longer than 255 characters.
4027 $newSubpage = self::makeTitleSafe( $newNs, $newPageName );
4028
4029 $success = $oldSubpage->moveTo( $newSubpage, $auth, $reason, $createRedirect, $changeTags );
4030 if ( $success === true ) {
4031 $retval[$oldSubpage->getPrefixedText()] = $newSubpage->getPrefixedText();
4032 } else {
4033 $retval[$oldSubpage->getPrefixedText()] = $success;
4034 }
4035 }
4036 return $retval;
4037 }
4038
4045 public function isSingleRevRedirect() {
4047
4048 $dbw = wfGetDB( DB_MASTER );
4049
4050 # Is it a redirect?
4051 $fields = [ 'page_is_redirect', 'page_latest', 'page_id' ];
4052 if ( $wgContentHandlerUseDB ) {
4053 $fields[] = 'page_content_model';
4054 }
4055
4056 $row = $dbw->selectRow( 'page',
4057 $fields,
4058 $this->pageCond(),
4059 __METHOD__,
4060 [ 'FOR UPDATE' ]
4061 );
4062 # Cache some fields we may want
4063 $this->mArticleID = $row ? intval( $row->page_id ) : 0;
4064 $this->mRedirect = $row ? (bool)$row->page_is_redirect : false;
4065 $this->mLatestID = $row ? intval( $row->page_latest ) : false;
4066 $this->mContentModel = $row && isset( $row->page_content_model )
4067 ? strval( $row->page_content_model )
4068 : false;
4069
4070 if ( !$this->mRedirect ) {
4071 return false;
4072 }
4073 # Does the article have a history?
4074 $row = $dbw->selectField( [ 'page', 'revision' ],
4075 'rev_id',
4076 [ 'page_namespace' => $this->getNamespace(),
4077 'page_title' => $this->getDBkey(),
4078 'page_id=rev_page',
4079 'page_latest != rev_id'
4080 ],
4081 __METHOD__,
4082 [ 'FOR UPDATE' ]
4083 );
4084 # Return true if there was no history
4085 return ( $row === false );
4086 }
4087
4096 public function isValidMoveTarget( $nt ) {
4097 # Is it an existing file?
4098 if ( $nt->getNamespace() == NS_FILE ) {
4099 $file = wfLocalFile( $nt );
4100 $file->load( File::READ_LATEST );
4101 if ( $file->exists() ) {
4102 wfDebug( __METHOD__ . ": file exists\n" );
4103 return false;
4104 }
4105 }
4106 # Is it a redirect with no history?
4107 if ( !$nt->isSingleRevRedirect() ) {
4108 wfDebug( __METHOD__ . ": not a one-rev redirect\n" );
4109 return false;
4110 }
4111 # Get the article text
4112 $rev = Revision::newFromTitle( $nt, false, Revision::READ_LATEST );
4113 if ( !is_object( $rev ) ) {
4114 return false;
4115 }
4116 $content = $rev->getContent();
4117 # Does the redirect point to the source?
4118 # Or is it a broken self-redirect, usually caused by namespace collisions?
4119 $redirTitle = $content ? $content->getRedirectTarget() : null;
4120
4121 if ( $redirTitle ) {
4122 if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
4123 $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) {
4124 wfDebug( __METHOD__ . ": redirect points to other page\n" );
4125 return false;
4126 } else {
4127 return true;
4128 }
4129 } else {
4130 # Fail safe (not a redirect after all. strange.)
4131 wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() .
4132 " is a redirect, but it doesn't contain a valid redirect.\n" );
4133 return false;
4134 }
4135 }
4136
4144 public function getParentCategories() {
4145 global $wgContLang;
4146
4147 $data = [];
4148
4149 $titleKey = $this->getArticleID();
4150
4151 if ( $titleKey === 0 ) {
4152 return $data;
4153 }
4154
4155 $dbr = wfGetDB( DB_REPLICA );
4156
4157 $res = $dbr->select(
4158 'categorylinks',
4159 'cl_to',
4160 [ 'cl_from' => $titleKey ],
4161 __METHOD__
4162 );
4163
4164 if ( $res->numRows() > 0 ) {
4165 foreach ( $res as $row ) {
4166 // $data[] = Title::newFromText($wgContLang->getNsText ( NS_CATEGORY ).':'.$row->cl_to);
4167 $data[$wgContLang->getNsText( NS_CATEGORY ) . ':' . $row->cl_to] = $this->getFullText();
4168 }
4169 }
4170 return $data;
4171 }
4172
4179 public function getParentCategoryTree( $children = [] ) {
4180 $stack = [];
4181 $parents = $this->getParentCategories();
4182
4183 if ( $parents ) {
4184 foreach ( $parents as $parent => $current ) {
4185 if ( array_key_exists( $parent, $children ) ) {
4186 # Circular reference
4187 $stack[$parent] = [];
4188 } else {
4189 $nt = self::newFromText( $parent );
4190 if ( $nt ) {
4191 $stack[$parent] = $nt->getParentCategoryTree( $children + [ $parent => 1 ] );
4192 }
4193 }
4194 }
4195 }
4196
4197 return $stack;
4198 }
4199
4206 public function pageCond() {
4207 if ( $this->mArticleID > 0 ) {
4208 // PK avoids secondary lookups in InnoDB, shouldn't hurt other DBs
4209 return [ 'page_id' => $this->mArticleID ];
4210 } else {
4211 return [ 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform ];
4212 }
4213 }
4214
4222 private function getRelativeRevisionID( $revId, $flags, $dir ) {
4223 $revId = (int)$revId;
4224 if ( $dir === 'next' ) {
4225 $op = '>';
4226 $sort = 'ASC';
4227 } elseif ( $dir === 'prev' ) {
4228 $op = '<';
4229 $sort = 'DESC';
4230 } else {
4231 throw new InvalidArgumentException( '$dir must be "next" or "prev"' );
4232 }
4233
4234 if ( $flags & self::GAID_FOR_UPDATE ) {
4235 $db = wfGetDB( DB_MASTER );
4236 } else {
4237 $db = wfGetDB( DB_REPLICA, 'contributions' );
4238 }
4239
4240 // Intentionally not caring if the specified revision belongs to this
4241 // page. We only care about the timestamp.
4242 $ts = $db->selectField( 'revision', 'rev_timestamp', [ 'rev_id' => $revId ], __METHOD__ );
4243 if ( $ts === false ) {
4244 $ts = $db->selectField( 'archive', 'ar_timestamp', [ 'ar_rev_id' => $revId ], __METHOD__ );
4245 if ( $ts === false ) {
4246 // Or should this throw an InvalidArgumentException or something?
4247 return false;
4248 }
4249 }
4250 $ts = $db->addQuotes( $ts );
4251
4252 $revId = $db->selectField( 'revision', 'rev_id',
4253 [
4254 'rev_page' => $this->getArticleID( $flags ),
4255 "rev_timestamp $op $ts OR (rev_timestamp = $ts AND rev_id $op $revId)"
4256 ],
4257 __METHOD__,
4258 [
4259 'ORDER BY' => "rev_timestamp $sort, rev_id $sort",
4260 'IGNORE INDEX' => 'rev_timestamp', // Probably needed for T159319
4261 ]
4262 );
4263
4264 if ( $revId === false ) {
4265 return false;
4266 } else {
4267 return intval( $revId );
4268 }
4269 }
4270
4278 public function getPreviousRevisionID( $revId, $flags = 0 ) {
4279 return $this->getRelativeRevisionID( $revId, $flags, 'prev' );
4280 }
4281
4289 public function getNextRevisionID( $revId, $flags = 0 ) {
4290 return $this->getRelativeRevisionID( $revId, $flags, 'next' );
4291 }
4292
4299 public function getFirstRevision( $flags = 0 ) {
4300 $pageId = $this->getArticleID( $flags );
4301 if ( $pageId ) {
4302 $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_REPLICA );
4303 $revQuery = Revision::getQueryInfo();
4304 $row = $db->selectRow( $revQuery['tables'], $revQuery['fields'],
4305 [ 'rev_page' => $pageId ],
4306 __METHOD__,
4307 [
4308 'ORDER BY' => 'rev_timestamp ASC, rev_id ASC',
4309 'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319
4310 ],
4311 $revQuery['joins']
4312 );
4313 if ( $row ) {
4314 return new Revision( $row );
4315 }
4316 }
4317 return null;
4318 }
4319
4326 public function getEarliestRevTime( $flags = 0 ) {
4327 $rev = $this->getFirstRevision( $flags );
4328 return $rev ? $rev->getTimestamp() : null;
4329 }
4330
4336 public function isNewPage() {
4337 $dbr = wfGetDB( DB_REPLICA );
4338 return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ );
4339 }
4340
4346 public function isBigDeletion() {
4348
4349 if ( !$wgDeleteRevisionsLimit ) {
4350 return false;
4351 }
4352
4353 if ( $this->mIsBigDeletion === null ) {
4354 $dbr = wfGetDB( DB_REPLICA );
4355
4356 $revCount = $dbr->selectRowCount(
4357 'revision',
4358 '1',
4359 [ 'rev_page' => $this->getArticleID() ],
4360 __METHOD__,
4361 [ 'LIMIT' => $wgDeleteRevisionsLimit + 1 ]
4362 );
4363
4364 $this->mIsBigDeletion = $revCount > $wgDeleteRevisionsLimit;
4365 }
4366
4367 return $this->mIsBigDeletion;
4368 }
4369
4375 public function estimateRevisionCount() {
4376 if ( !$this->exists() ) {
4377 return 0;
4378 }
4379
4380 if ( $this->mEstimateRevisions === null ) {
4381 $dbr = wfGetDB( DB_REPLICA );
4382 $this->mEstimateRevisions = $dbr->estimateRowCount( 'revision', '*',
4383 [ 'rev_page' => $this->getArticleID() ], __METHOD__ );
4384 }
4385
4386 return $this->mEstimateRevisions;
4387 }
4388
4398 public function countRevisionsBetween( $old, $new, $max = null ) {
4399 if ( !( $old instanceof Revision ) ) {
4400 $old = Revision::newFromTitle( $this, (int)$old );
4401 }
4402 if ( !( $new instanceof Revision ) ) {
4403 $new = Revision::newFromTitle( $this, (int)$new );
4404 }
4405 if ( !$old || !$new ) {
4406 return 0; // nothing to compare
4407 }
4408 $dbr = wfGetDB( DB_REPLICA );
4409 $conds = [
4410 'rev_page' => $this->getArticleID(),
4411 'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
4412 'rev_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
4413 ];
4414 if ( $max !== null ) {
4415 return $dbr->selectRowCount( 'revision', '1',
4416 $conds,
4417 __METHOD__,
4418 [ 'LIMIT' => $max + 1 ] // extra to detect truncation
4419 );
4420 } else {
4421 return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ );
4422 }
4423 }
4424
4441 public function getAuthorsBetween( $old, $new, $limit, $options = [] ) {
4442 if ( !( $old instanceof Revision ) ) {
4443 $old = Revision::newFromTitle( $this, (int)$old );
4444 }
4445 if ( !( $new instanceof Revision ) ) {
4446 $new = Revision::newFromTitle( $this, (int)$new );
4447 }
4448 // XXX: what if Revision objects are passed in, but they don't refer to this title?
4449 // Add $old->getPage() != $new->getPage() || $old->getPage() != $this->getArticleID()
4450 // in the sanity check below?
4451 if ( !$old || !$new ) {
4452 return null; // nothing to compare
4453 }
4454 $authors = [];
4455 $old_cmp = '>';
4456 $new_cmp = '<';
4458 if ( in_array( 'include_old', $options ) ) {
4459 $old_cmp = '>=';
4460 }
4461 if ( in_array( 'include_new', $options ) ) {
4462 $new_cmp = '<=';
4463 }
4464 if ( in_array( 'include_both', $options ) ) {
4465 $old_cmp = '>=';
4466 $new_cmp = '<=';
4467 }
4468 // No DB query needed if $old and $new are the same or successive revisions:
4469 if ( $old->getId() === $new->getId() ) {
4470 return ( $old_cmp === '>' && $new_cmp === '<' ) ?
4471 [] :
4472 [ $old->getUserText( Revision::RAW ) ];
4473 } elseif ( $old->getId() === $new->getParentId() ) {
4474 if ( $old_cmp === '>=' && $new_cmp === '<=' ) {
4475 $authors[] = $old->getUserText( Revision::RAW );
4476 if ( $old->getUserText( Revision::RAW ) != $new->getUserText( Revision::RAW ) ) {
4477 $authors[] = $new->getUserText( Revision::RAW );
4478 }
4479 } elseif ( $old_cmp === '>=' ) {
4480 $authors[] = $old->getUserText( Revision::RAW );
4481 } elseif ( $new_cmp === '<=' ) {
4482 $authors[] = $new->getUserText( Revision::RAW );
4483 }
4484 return $authors;
4485 }
4486 $dbr = wfGetDB( DB_REPLICA );
4487 $revQuery = Revision::getQueryInfo();
4488 $authors = $dbr->selectFieldValues(
4489 $revQuery['tables'],
4490 $revQuery['fields']['rev_user_text'],
4491 [
4492 'rev_page' => $this->getArticleID(),
4493 "rev_timestamp $old_cmp " . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
4494 "rev_timestamp $new_cmp " . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
4495 ], __METHOD__,
4496 [ 'DISTINCT', 'LIMIT' => $limit + 1 ], // add one so caller knows it was truncated
4497 $revQuery['joins']
4498 );
4499 return $authors;
4500 }
4501
4516 public function countAuthorsBetween( $old, $new, $limit, $options = [] ) {
4517 $authors = $this->getAuthorsBetween( $old, $new, $limit, $options );
4518 return $authors ? count( $authors ) : 0;
4519 }
4520
4527 public function equals( Title $title ) {
4528 // Note: === is necessary for proper matching of number-like titles.
4529 return $this->getInterwiki() === $title->getInterwiki()
4530 && $this->getNamespace() == $title->getNamespace()
4531 && $this->getDBkey() === $title->getDBkey();
4532 }
4533
4540 public function isSubpageOf( Title $title ) {
4541 return $this->getInterwiki() === $title->getInterwiki()
4542 && $this->getNamespace() == $title->getNamespace()
4543 && strpos( $this->getDBkey(), $title->getDBkey() . '/' ) === 0;
4544 }
4545
4557 public function exists( $flags = 0 ) {
4558 $exists = $this->getArticleID( $flags ) != 0;
4559 Hooks::run( 'TitleExists', [ $this, &$exists ] );
4560 return $exists;
4561 }
4562
4579 public function isAlwaysKnown() {
4580 $isKnown = null;
4581
4592 Hooks::run( 'TitleIsAlwaysKnown', [ $this, &$isKnown ] );
4593
4594 if ( !is_null( $isKnown ) ) {
4595 return $isKnown;
4596 }
4597
4598 if ( $this->isExternal() ) {
4599 return true; // any interwiki link might be viewable, for all we know
4600 }
4601
4602 switch ( $this->mNamespace ) {
4603 case NS_MEDIA:
4604 case NS_FILE:
4605 // file exists, possibly in a foreign repo
4606 return (bool)wfFindFile( $this );
4607 case NS_SPECIAL:
4608 // valid special page
4609 return SpecialPageFactory::exists( $this->getDBkey() );
4610 case NS_MAIN:
4611 // selflink, possibly with fragment
4612 return $this->mDbkeyform == '';
4613 case NS_MEDIAWIKI:
4614 // known system message
4615 return $this->hasSourceText() !== false;
4616 default:
4617 return false;
4618 }
4619 }
4620
4632 public function isKnown() {
4633 return $this->isAlwaysKnown() || $this->exists();
4634 }
4635
4641 public function hasSourceText() {
4642 if ( $this->exists() ) {
4643 return true;
4644 }
4645
4646 if ( $this->mNamespace == NS_MEDIAWIKI ) {
4647 // If the page doesn't exist but is a known system message, default
4648 // message content will be displayed, same for language subpages-
4649 // Use always content language to avoid loading hundreds of languages
4650 // to get the link color.
4651 global $wgContLang;
4652 list( $name, ) = MessageCache::singleton()->figureMessage(
4653 $wgContLang->lcfirst( $this->getText() )
4654 );
4655 $message = wfMessage( $name )->inLanguage( $wgContLang )->useDatabase( false );
4656 return $message->exists();
4657 }
4658
4659 return false;
4660 }
4661
4667 public function getDefaultMessageText() {
4668 global $wgContLang;
4669
4670 if ( $this->getNamespace() != NS_MEDIAWIKI ) { // Just in case
4671 return false;
4672 }
4673
4674 list( $name, $lang ) = MessageCache::singleton()->figureMessage(
4675 $wgContLang->lcfirst( $this->getText() )
4676 );
4677 $message = wfMessage( $name )->inLanguage( $lang )->useDatabase( false );
4678
4679 if ( $message->exists() ) {
4680 return $message->plain();
4681 } else {
4682 return false;
4683 }
4684 }
4685
4692 public function invalidateCache( $purgeTime = null ) {
4693 if ( wfReadOnly() ) {
4694 return false;
4695 } elseif ( $this->mArticleID === 0 ) {
4696 return true; // avoid gap locking if we know it's not there
4697 }
4698
4699 $dbw = wfGetDB( DB_MASTER );
4700 $dbw->onTransactionPreCommitOrIdle( function () {
4702 } );
4703
4704 $conds = $this->pageCond();
4705 DeferredUpdates::addUpdate(
4706 new AutoCommitUpdate(
4707 $dbw,
4708 __METHOD__,
4709 function ( IDatabase $dbw, $fname ) use ( $conds, $purgeTime ) {
4710 $dbTimestamp = $dbw->timestamp( $purgeTime ?: time() );
4711 $dbw->update(
4712 'page',
4713 [ 'page_touched' => $dbTimestamp ],
4714 $conds + [ 'page_touched < ' . $dbw->addQuotes( $dbTimestamp ) ],
4715 $fname
4716 );
4717 MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $this );
4718 }
4719 ),
4720 DeferredUpdates::PRESEND
4721 );
4722
4723 return true;
4724 }
4725
4731 public function touchLinks() {
4732 DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'pagelinks', 'page-touch' ) );
4733 if ( $this->getNamespace() == NS_CATEGORY ) {
4734 DeferredUpdates::addUpdate(
4735 new HTMLCacheUpdate( $this, 'categorylinks', 'category-touch' )
4736 );
4737 }
4738 }
4739
4746 public function getTouched( $db = null ) {
4747 if ( $db === null ) {
4748 $db = wfGetDB( DB_REPLICA );
4749 }
4750 $touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ );
4751 return $touched;
4752 }
4753
4760 public function getNotificationTimestamp( $user = null ) {
4761 global $wgUser;
4762
4763 // Assume current user if none given
4764 if ( !$user ) {
4765 $user = $wgUser;
4766 }
4767 // Check cache first
4768 $uid = $user->getId();
4769 if ( !$uid ) {
4770 return false;
4771 }
4772 // avoid isset here, as it'll return false for null entries
4773 if ( array_key_exists( $uid, $this->mNotificationTimestamp ) ) {
4774 return $this->mNotificationTimestamp[$uid];
4775 }
4776 // Don't cache too much!
4777 if ( count( $this->mNotificationTimestamp ) >= self::CACHE_MAX ) {
4778 $this->mNotificationTimestamp = [];
4779 }
4780
4781 $store = MediaWikiServices::getInstance()->getWatchedItemStore();
4782 $watchedItem = $store->getWatchedItem( $user, $this );
4783 if ( $watchedItem ) {
4784 $this->mNotificationTimestamp[$uid] = $watchedItem->getNotificationTimestamp();
4785 } else {
4786 $this->mNotificationTimestamp[$uid] = false;
4787 }
4788
4789 return $this->mNotificationTimestamp[$uid];
4790 }
4791
4798 public function getNamespaceKey( $prepend = 'nstab-' ) {
4799 global $wgContLang;
4800 // Gets the subject namespace of this title
4801 $subjectNS = MWNamespace::getSubject( $this->getNamespace() );
4802 // Prefer canonical namespace name for HTML IDs
4803 $namespaceKey = MWNamespace::getCanonicalName( $subjectNS );
4804 if ( $namespaceKey === false ) {
4805 // Fallback to localised text
4806 $namespaceKey = $this->getSubjectNsText();
4807 }
4808 // Makes namespace key lowercase
4809 $namespaceKey = $wgContLang->lc( $namespaceKey );
4810 // Uses main
4811 if ( $namespaceKey == '' ) {
4812 $namespaceKey = 'main';
4813 }
4814 // Changes file to image for backwards compatibility
4815 if ( $namespaceKey == 'file' ) {
4816 $namespaceKey = 'image';
4817 }
4818 return $prepend . $namespaceKey;
4819 }
4820
4827 public function getRedirectsHere( $ns = null ) {
4828 $redirs = [];
4829
4830 $dbr = wfGetDB( DB_REPLICA );
4831 $where = [
4832 'rd_namespace' => $this->getNamespace(),
4833 'rd_title' => $this->getDBkey(),
4834 'rd_from = page_id'
4835 ];
4836 if ( $this->isExternal() ) {
4837 $where['rd_interwiki'] = $this->getInterwiki();
4838 } else {
4839 $where[] = 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL';
4840 }
4841 if ( !is_null( $ns ) ) {
4842 $where['page_namespace'] = $ns;
4843 }
4844
4845 $res = $dbr->select(
4846 [ 'redirect', 'page' ],
4847 [ 'page_namespace', 'page_title' ],
4848 $where,
4849 __METHOD__
4850 );
4851
4852 foreach ( $res as $row ) {
4853 $redirs[] = self::newFromRow( $row );
4854 }
4855 return $redirs;
4856 }
4857
4863 public function isValidRedirectTarget() {
4865
4866 if ( $this->isSpecialPage() ) {
4867 // invalid redirect targets are stored in a global array, but explicitly disallow Userlogout here
4868 if ( $this->isSpecial( 'Userlogout' ) ) {
4869 return false;
4870 }
4871
4872 foreach ( $wgInvalidRedirectTargets as $target ) {
4873 if ( $this->isSpecial( $target ) ) {
4874 return false;
4875 }
4876 }
4877 }
4878
4879 return true;
4880 }
4881
4887 public function getBacklinkCache() {
4888 return BacklinkCache::get( $this );
4889 }
4890
4896 public function canUseNoindex() {
4898
4899 $bannedNamespaces = is_null( $wgExemptFromUserRobotsControl )
4900 ? MWNamespace::getContentNamespaces()
4902
4903 return !in_array( $this->mNamespace, $bannedNamespaces );
4904 }
4905
4916 public function getCategorySortkey( $prefix = '' ) {
4917 $unprefixed = $this->getText();
4918
4919 // Anything that uses this hook should only depend
4920 // on the Title object passed in, and should probably
4921 // tell the users to run updateCollations.php --force
4922 // in order to re-sort existing category relations.
4923 Hooks::run( 'GetDefaultSortkey', [ $this, &$unprefixed ] );
4924 if ( $prefix !== '' ) {
4925 # Separate with a line feed, so the unprefixed part is only used as
4926 # a tiebreaker when two pages have the exact same prefix.
4927 # In UCA, tab is the only character that can sort above LF
4928 # so we strip both of them from the original prefix.
4929 $prefix = strtr( $prefix, "\n\t", ' ' );
4930 return "$prefix\n$unprefixed";
4931 }
4932 return $unprefixed;
4933 }
4934
4942 private function getDbPageLanguageCode() {
4943 global $wgPageLanguageUseDB;
4944
4945 // check, if the page language could be saved in the database, and if so and
4946 // the value is not requested already, lookup the page language using LinkCache
4947 if ( $wgPageLanguageUseDB && $this->mDbPageLanguage === false ) {
4948 $linkCache = LinkCache::singleton();
4949 $linkCache->addLinkObj( $this );
4950 $this->mDbPageLanguage = $linkCache->getGoodLinkFieldObj( $this, 'lang' );
4951 }
4952
4953 return $this->mDbPageLanguage;
4954 }
4955
4964 public function getPageLanguage() {
4965 global $wgLang, $wgLanguageCode;
4966 if ( $this->isSpecialPage() ) {
4967 // special pages are in the user language
4968 return $wgLang;
4969 }
4970
4971 // Checking if DB language is set
4972 $dbPageLanguage = $this->getDbPageLanguageCode();
4973 if ( $dbPageLanguage ) {
4974 return wfGetLangObj( $dbPageLanguage );
4975 }
4976
4977 if ( !$this->mPageLanguage || $this->mPageLanguage[1] !== $wgLanguageCode ) {
4978 // Note that this may depend on user settings, so the cache should
4979 // be only per-request.
4980 // NOTE: ContentHandler::getPageLanguage() may need to load the
4981 // content to determine the page language!
4982 // Checking $wgLanguageCode hasn't changed for the benefit of unit
4983 // tests.
4984 $contentHandler = ContentHandler::getForTitle( $this );
4985 $langObj = $contentHandler->getPageLanguage( $this );
4986 $this->mPageLanguage = [ $langObj->getCode(), $wgLanguageCode ];
4987 } else {
4988 $langObj = wfGetLangObj( $this->mPageLanguage[0] );
4989 }
4990
4991 return $langObj;
4992 }
4993
5002 public function getPageViewLanguage() {
5003 global $wgLang;
5004
5005 if ( $this->isSpecialPage() ) {
5006 // If the user chooses a variant, the content is actually
5007 // in a language whose code is the variant code.
5008 $variant = $wgLang->getPreferredVariant();
5009 if ( $wgLang->getCode() !== $variant ) {
5010 return Language::factory( $variant );
5011 }
5012
5013 return $wgLang;
5014 }
5015
5016 // Checking if DB language is set
5017 $dbPageLanguage = $this->getDbPageLanguageCode();
5018 if ( $dbPageLanguage ) {
5019 $pageLang = wfGetLangObj( $dbPageLanguage );
5020 $variant = $pageLang->getPreferredVariant();
5021 if ( $pageLang->getCode() !== $variant ) {
5022 $pageLang = Language::factory( $variant );
5023 }
5024
5025 return $pageLang;
5026 }
5027
5028 // @note Can't be cached persistently, depends on user settings.
5029 // @note ContentHandler::getPageViewLanguage() may need to load the
5030 // content to determine the page language!
5031 $contentHandler = ContentHandler::getForTitle( $this );
5032 $pageLang = $contentHandler->getPageViewLanguage( $this );
5033 return $pageLang;
5034 }
5035
5046 public function getEditNotices( $oldid = 0 ) {
5047 $notices = [];
5048
5049 // Optional notice for the entire namespace
5050 $editnotice_ns = 'editnotice-' . $this->getNamespace();
5051 $msg = wfMessage( $editnotice_ns );
5052 if ( $msg->exists() ) {
5053 $html = $msg->parseAsBlock();
5054 // Edit notices may have complex logic, but output nothing (T91715)
5055 if ( trim( $html ) !== '' ) {
5056 $notices[$editnotice_ns] = Html::rawElement(
5057 'div',
5058 [ 'class' => [
5059 'mw-editnotice',
5060 'mw-editnotice-namespace',
5061 Sanitizer::escapeClass( "mw-$editnotice_ns" )
5062 ] ],
5063 $html
5064 );
5065 }
5066 }
5067
5068 if ( MWNamespace::hasSubpages( $this->getNamespace() ) ) {
5069 // Optional notice for page itself and any parent page
5070 $parts = explode( '/', $this->getDBkey() );
5071 $editnotice_base = $editnotice_ns;
5072 while ( count( $parts ) > 0 ) {
5073 $editnotice_base .= '-' . array_shift( $parts );
5074 $msg = wfMessage( $editnotice_base );
5075 if ( $msg->exists() ) {
5076 $html = $msg->parseAsBlock();
5077 if ( trim( $html ) !== '' ) {
5078 $notices[$editnotice_base] = Html::rawElement(
5079 'div',
5080 [ 'class' => [
5081 'mw-editnotice',
5082 'mw-editnotice-base',
5083 Sanitizer::escapeClass( "mw-$editnotice_base" )
5084 ] ],
5085 $html
5086 );
5087 }
5088 }
5089 }
5090 } else {
5091 // Even if there are no subpages in namespace, we still don't want "/" in MediaWiki message keys
5092 $editnoticeText = $editnotice_ns . '-' . strtr( $this->getDBkey(), '/', '-' );
5093 $msg = wfMessage( $editnoticeText );
5094 if ( $msg->exists() ) {
5095 $html = $msg->parseAsBlock();
5096 if ( trim( $html ) !== '' ) {
5097 $notices[$editnoticeText] = Html::rawElement(
5098 'div',
5099 [ 'class' => [
5100 'mw-editnotice',
5101 'mw-editnotice-page',
5102 Sanitizer::escapeClass( "mw-$editnoticeText" )
5103 ] ],
5104 $html
5105 );
5106 }
5107 }
5108 }
5109
5110 Hooks::run( 'TitleGetEditNotices', [ $this, $oldid, &$notices ] );
5111 return $notices;
5112 }
5113
5117 public function __sleep() {
5118 return [
5119 'mNamespace',
5120 'mDbkeyform',
5121 'mFragment',
5122 'mInterwiki',
5123 'mLocalInterwiki',
5124 'mUserCaseDBKey',
5125 'mDefaultNamespace',
5126 ];
5127 }
5128
5129 public function __wakeup() {
5130 $this->mArticleID = ( $this->mNamespace >= 0 ) ? -1 : 0;
5131 $this->mUrlform = wfUrlencode( $this->mDbkeyform );
5132 $this->mTextform = strtr( $this->mDbkeyform, '_', ' ' );
5133 }
5134
5135}
$wgRestrictionLevels
Rights which can be required for each protection level (via action=protect)
$wgExemptFromUserRobotsControl
An array of namespace keys in which the INDEX/__NOINDEX__ magic words will not function,...
$wgLegalTitleChars
Allowed title characters – regex character class Don't change this unless you know what you're doing.
bool $wgPageLanguageUseDB
Enable page language feature Allows setting page language in database.
$wgSemiprotectedRestrictionLevels
Restriction levels that should be considered "semiprotected".
$wgLanguageCode
Site language code.
$wgMaximumMovedPages
Maximum number of pages to move at once when moving subpages with a page.
$wgScript
The URL path to index.php.
$wgInternalServer
Internal server name as known to CDN, if different.
$wgInvalidRedirectTargets
Array of invalid page redirect targets.
$wgNamespaceProtection
Set the minimum permissions required to edit pages in each namespace.
$wgRestrictionTypes
Set of available actions that can be restricted via action=protect You probably shouldn't change this...
$wgWhitelistRead
Pages anonymous user may see, set as an array of pages titles.
$wgWhitelistReadRegexp
Pages anonymous user may see, set as an array of regular expressions.
$wgDeleteRevisionsLimit
Optional to restrict deletion of pages with higher revision counts to users with the 'bigdelete' perm...
$wgBlockDisablesLogin
If true, blocked users will not be allowed to login.
$wgEmailConfirmToEdit
Should editors be required to have a validated e-mail address before being allowed to edit?
$wgVariantArticlePath
Like $wgArticlePath, but on multi-variant wikis, this provides a path format that describes which par...
$wgServer
URL of the server.
$wgContentHandlerUseDB
Set to false to disable use of the database fields introduced by the ContentHandler facility.
wfGetLangObj( $langcode=false)
Return a Language object from $langcode.
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
wfUrlencode( $s)
We want some things to be included as literal characters in our title URLs for prettiness,...
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
wfReadOnly()
Check whether the wiki is in read-only mode.
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfMergeErrorArrays()
Merge arrays in the style of getUserPermissionsErrors, with duplicate removal e.g.
wfLocalFile( $title)
Get an object referring to a locally registered file.
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
wfFindFile( $title, $options=[])
Find a file.
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
wfArrayToCgi( $array1, $array2=null, $prefix='')
This function takes one or two arrays as input, and returns a CGI-style string, e....
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
wfWikiID()
Get an ASCII string identifying this wiki This is used as a prefix in memcached keys.
$wgUser
Definition Setup.php:902
if(defined( 'MW_SETUP_CALLBACK')) $fname
Customization point after all loading (constants, functions, classes, DefaultSettings,...
Definition Setup.php:112
if(! $wgDBerrorLogTZ) $wgRequest
Definition Setup.php:737
Deferrable Update for closure/callback updates via IDatabase::doAtomicSection()
Deferrable Update for closure/callback updates that should use auto-commit mode.
static get(Title $title)
Create a new BacklinkCache or reuse any existing one.
get( $key, $flags=0, $oldFlags=null)
Get an item with the given key.
Handles purging appropriate CDN URLs given a title (or titles)
Class to invalidate the HTML cache of all the pages linking to a given title.
Simple store for keeping values in an associative array for the current process.
set( $key, $value, $exptime=0, $flags=0)
Set an item.
MediaWiki exception.
MalformedTitleException is thrown when a TitleParser is unable to parse a title string.
MediaWikiServices is the service locator for the application scope of MediaWiki.
static singleton()
Get the signleton instance of this class.
Handles the backend logic of moving a page from one title to another.
Definition MovePage.php:30
static getMain()
Get the RequestContext object associated with the main request.
static invalidateModuleCache(Title $title, Revision $old=null, Revision $new=null, $wikiId)
Clear the preloadTitleInfo() cache for all wiki modules on this wiki on page change if it was a JS or...
static getLocalNameFor( $name, $subpage=false)
Get the local name for a specified canonical name.
static exists( $name)
Check if a given name exist as a special page or as a special page alias.
static resolveAlias( $alias)
Given a special page name with a possible subpage, return an array where the first element is the spe...
static escapeRegexReplacement( $string)
Escape a string to make it suitable for inclusion in a preg_replace() replacement parameter.
The TitleArray class only exists to provide the newFromResult method at pre- sent.
static newFromResult( $res)
Represents a page (or page fragment) title within MediaWiki.
Represents a title within MediaWiki.
Definition Title.php:39
string $mInterwiki
Interwiki prefix.
Definition Title.php:79
static newFromID( $id, $flags=0)
Create a new Title from an article ID.
Definition Title.php:416
inNamespaces()
Returns true if the title is inside one of the specified namespaces.
Definition Title.php:1173
checkUserConfigPermissions( $action, $user, $errors, $rigor, $short)
Check CSS/JSON/JS sub-page permissions.
Definition Title.php:2330
getSubpages( $limit=-1)
Get all subpages of this page.
Definition Title.php:3358
isWatchable()
Can this title be added to a user's watchlist?
Definition Title.php:1104
getTalkPageIfDefined()
Get a Title object associated with the talk page of this article, if such a talk page can exist.
Definition Title.php:1456
getNamespace()
Get the namespace index, i.e.
Definition Title.php:970
estimateRevisionCount()
Get the approximate revision count of this page.
Definition Title.php:4375
__wakeup()
Text form (spaces not underscores) of the main part.
Definition Title.php:5129
static newFromDBkey( $key)
Create a new Title from a prefixed DB key.
Definition Title.php:214
isProtected( $action='')
Does the title correspond to a protected article?
Definition Title.php:2911
getTitleProtectionInternal()
Fetch title protection settings.
Definition Title.php:2818
getLinkURL( $query='', $query2=false, $proto=false)
Get a URL that's the simplest URL that will be valid to link, locally, to the current Title.
Definition Title.php:2009
bool $mPageLanguage
The (string) language code of the page's language and content code.
Definition Title.php:163
array $mCascadeSources
Where are the cascading restrictions coming from on this page?
Definition Title.php:132
isSingleRevRedirect()
Checks if this page is just a one-rev redirect.
Definition Title.php:4045
wasLocalInterwiki()
Was this a local interwiki link?
Definition Title.php:866
getInternalURL( $query='', $query2=false)
Get the URL form for an internal link.
Definition Title.php:2034
purgeSquid()
Purge all applicable CDN URLs.
Definition Title.php:3861
getFullURL( $query='', $query2=false, $proto=PROTO_RELATIVE)
Get a real URL referring to this title, with interwiki link and fragment.
Definition Title.php:1842
getRestrictions( $action)
Accessor/initialisation for mRestrictions.
Definition Title.php:3091
isKnown()
Does this title refer to a page that can (or might) be meaningfully viewed? In particular,...
Definition Title.php:4632
int $mEstimateRevisions
Estimated number of revisions; null of not loaded.
Definition Title.php:106
getBacklinkCache()
Get a backlink cache object.
Definition Title.php:4887
static getInterwikiLookup()
B/C kludge: provide an InterwikiLookup for use by Title.
Definition Title.php:196
static getTitleFormatter()
B/C kludge: provide a TitleParser for use by Title.
Definition Title.php:184
inNamespace( $ns)
Returns true if the title is inside the specified namespace.
Definition Title.php:1162
equals(Title $title)
Compare with another title.
Definition Title.php:4527
isDeletedQuick()
Is there a version of this page in the deletion archive?
Definition Title.php:3410
static capitalize( $text, $ns=NS_MAIN)
Capitalize a text string for a title if it belongs to a namespace that capitalizes.
Definition Title.php:3594
checkQuickPermissions( $action, $user, $errors, $rigor, $short)
Permissions checks that fail most often, and which are easiest to test.
Definition Title.php:2163
getTalkPage()
Get a Title object associated with the talk page of this article.
Definition Title.php:1443
secureAndSplit()
Secure and split - main initialisation function for this object.
Definition Title.php:3616
getAllRestrictions()
Accessor/initialisation for mRestrictions.
Definition Title.php:3107
hasContentModel( $id)
Convenience method for checking a title's content model name.
Definition Title.php:1003
getSkinFromCssJsSubpage()
Definition Title.php:1364
static clearCaches()
Text form (spaces not underscores) of the main part.
Definition Title.php:3579
createFragmentTarget( $fragment)
Creates a new Title for a different fragment of the same page.
Definition Title.php:1570
getDefaultNamespace()
Get the default namespace index, for when there is no namespace.
Definition Title.php:1506
moveTo(&$nt, $auth=true, $reason='', $createRedirect=true, array $changeTags=[])
Move a title to a new location.
Definition Title.php:3934
isConversionTable()
Is this a conversion table for the LanguageConverter?
Definition Title.php:1263
getFragment()
Get the Title fragment (i.e.
Definition Title.php:1517
isCascadeProtected()
Cascading protection: Return true if cascading restrictions apply to this page, false if not.
Definition Title.php:2961
static getFilteredRestrictionTypes( $exists=true)
Get a filtered list of all restriction types supported by this wiki.
Definition Title.php:2750
getPrefixedURL()
Get a URL-encoded title (not an actual URL) including interwiki.
Definition Title.php:1785
TitleValue $mTitleValue
A corresponding TitleValue object.
Definition Title.php:170
checkUserBlock( $action, $user, $errors, $rigor, $short)
Check that the user isn't blocked from editing.
Definition Title.php:2546
isWikitextPage()
Does that page contain wikitext, or it is JS, CSS or whatever?
Definition Title.php:1275
validateFileMoveOperation( $nt)
Check if the requested move target is a valid file move target.
Definition Title.php:3905
getTalkNsText()
Get the namespace text of the talk page.
Definition Title.php:1062
areRestrictionsCascading()
Returns cascading restrictions for the current article.
Definition Title.php:3133
hasFragment()
Check if a Title fragment is set.
Definition Title.php:1527
static nameOf( $id)
Get the prefixed DB key associated with an ID.
Definition Title.php:601
isSpecial( $name)
Returns true if this title resolves to the named special page.
Definition Title.php:1123
getRedirectsHere( $ns=null)
Get all extant redirects to this Title.
Definition Title.php:4827
getLength( $flags=0)
What is the length of this page? Uses link cache, adding it if necessary.
Definition Title.php:3497
array $mNotificationTimestamp
Associative array of user ID -> timestamp/false.
Definition Title.php:157
isValidMoveOperation(&$nt, $auth=true, $reason='')
Check whether a given move operation would be valid.
Definition Title.php:3878
getFullText()
Get the prefixed title with spaces, plus any fragment (part beginning with '#')
Definition Title.php:1649
areRestrictionsLoaded()
Accessor for mRestrictionsLoaded.
Definition Title.php:3078
canUseNoindex()
Whether the magic words INDEX and NOINDEX function for this page.
Definition Title.php:4896
exists( $flags=0)
Check if page exists.
Definition Title.php:4557
static newFromURL( $url)
THIS IS NOT THE FUNCTION YOU WANT.
Definition Title.php:353
static newFromTextThrow( $text, $defaultNamespace=NS_MAIN)
Like Title::newFromText(), but throws MalformedTitleException when the title is invalid,...
Definition Title.php:306
isLocal()
Determine whether the object refers to a page within this project (either this wiki or a wiki with a ...
Definition Title.php:831
int $mLength
The page length, 0 for special pages.
Definition Title.php:151
loadFromRow( $row)
Load Title object fields from a DB row.
Definition Title.php:476
getPageLanguage()
Get the language in which the content of this page is written in wikitext.
Definition Title.php:4964
bool $mLocalInterwiki
Was this Title created from a string with a local interwiki prefix?
Definition Title.php:82
getUserCaseDBKey()
Get the DB key with the initial letter case as specified by the user.
Definition Title.php:956
isMovable()
Would anybody with sufficient privileges be able to move this page? Some pages just aren't movable.
Definition Title.php:1222
const CACHE_MAX
Title::newFromText maintains a cache to avoid expensive re-normalization of commonly used titles.
Definition Title.php:48
getRestrictionExpiry( $action)
Get the expiry time for the restriction against a given action.
Definition Title.php:3121
getUserPermissionsErrors( $action, $user, $rigor='secure', $ignoreErrors=[])
Can $user perform $action on this page?
Definition Title.php:2132
getSubjectPage()
Get a title object associated with the subject page of this talk page.
Definition Title.php:1470
getTemplateLinksTo( $options=[])
Get an array of Title objects using this Title as a template Also stores the IDs in the link cache.
Definition Title.php:3706
fixSpecialName()
If the Title refers to a special page alias which is not the local default, resolve the alias,...
Definition Title.php:1139
getRestrictionTypes()
Returns restriction types for the current Title.
Definition Title.php:2768
static legalChars()
Get a regex character class describing the legal characters in a link.
Definition Title.php:623
__toString()
Return a string representation of this title.
Definition Title.php:1639
hasSubjectNamespace( $ns)
Returns true if the title has the same subject namespace as the namespace specified.
Definition Title.php:1201
isSemiProtected( $action='edit')
Is this page "semi-protected" - the only protection levels are listed in $wgSemiprotectedRestrictionL...
Definition Title.php:2883
isCssJsSubpage()
Definition Title.php:1337
getPrefixedDBkey()
Get the prefixed database key form.
Definition Title.php:1613
areCascadeProtectionSourcesLoaded( $getPages=true)
Determines whether cascading protection sources have already been loaded from the database.
Definition Title.php:2975
getPreviousRevisionID( $revId, $flags=0)
Get the revision ID of the previous revision.
Definition Title.php:4278
getNsText()
Get the namespace text.
Definition Title.php:1028
canExist()
Is this in a namespace that allows actual pages?
Definition Title.php:1095
static purgeExpiredRestrictions()
Purge expired restrictions from the page_restrictions table.
Definition Title.php:3289
getDefaultMessageText()
Get the default message text or false if the message doesn't exist.
Definition Title.php:4667
getDbPageLanguageCode()
Returns the page language code saved in the database, if $wgPageLanguageUseDB is set to true in Local...
Definition Title.php:4942
countRevisionsBetween( $old, $new, $max=null)
Get the number of revisions between the given revision.
Definition Title.php:4398
checkPermissionHooks( $action, $user, $errors, $rigor, $short)
Check various permission hooks.
Definition Title.php:2264
bool $mForcedContentModel
If a content model was forced via setContentModel() this will be true to avoid having other code path...
Definition Title.php:103
getNotificationTimestamp( $user=null)
Get the timestamp when this page was updated since the user last saw it.
Definition Title.php:4760
isTrans()
Determine whether the object refers to a page within this project and is transcludable.
Definition Title.php:876
resetArticleID( $newid)
This clears some fields in this object, and clears any associated keys in the "bad links" section of ...
Definition Title.php:3557
isNewPage()
Check if this is a new page.
Definition Title.php:4336
touchLinks()
Update page_touched timestamps and send CDN purge messages for pages linking to this title.
Definition Title.php:4731
isExternal()
Is this Title interwiki?
Definition Title.php:846
bool $mRestrictionsLoaded
Boolean for initialisation on demand.
Definition Title.php:135
isMainPage()
Is this the mainpage?
Definition Title.php:1243
isUserConfigPage()
Is this a "config" (.css, .json, or .js) sub-page of a user page?
Definition Title.php:1321
getFragmentForURL()
Get the fragment in URL form, including the "#" character if there is one.
Definition Title.php:1536
string bool null $mDbPageLanguage
The page language code from the database, null if not saved in the database or false if not loaded,...
Definition Title.php:167
getAuthorsBetween( $old, $new, $limit, $options=[])
Get the authors between the given revisions or revision IDs.
Definition Title.php:4441
isSpecialPage()
Returns true if this is a special page.
Definition Title.php:1113
isNamespaceProtected(User $user)
Determines if $user is unable to edit this page because it has been protected by $wgNamespaceProtecti...
Definition Title.php:2943
isUserJsConfigPage()
Is this a JS "config" sub-page of a user page?
Definition Title.php:1412
static newFromLinkTarget(LinkTarget $linkTarget)
Create a new Title from a LinkTarget.
Definition Title.php:244
getSubpageUrlForm()
Get a URL-encoded form of the subpage text.
Definition Title.php:1774
canHaveTalkPage()
Can this title have a corresponding talk page?
Definition Title.php:1086
isTalkPage()
Is this a talk page of some sort?
Definition Title.php:1434
getRootTitle()
Get the root page name title, i.e.
Definition Title.php:1689
bool int $mLatestID
ID of most recent revision.
Definition Title.php:91
getBrokenLinksFrom()
Get an array of Title objects referring to non-existent articles linked from this page.
Definition Title.php:3788
getDBkey()
Get the main part with underscores.
Definition Title.php:947
missingPermissionError( $action, $short)
Get a description array when the user doesn't have the right to perform $action (i....
Definition Title.php:2666
prefix( $name)
Prefix some arbitrary text with the namespace or interwiki prefix of this object.
Definition Title.php:1586
getEarliestRevTime( $flags=0)
Get the oldest revision timestamp of this page.
Definition Title.php:4326
checkActionPermissions( $action, $user, $errors, $rigor, $short)
Check action permissions not already checked in checkQuickPermissions.
Definition Title.php:2469
string $mFragment
Title fragment (i.e.
Definition Title.php:85
getRootText()
Get the root page name text without a namespace, i.e.
Definition Title.php:1669
getFullUrlForRedirect( $query='', $proto=PROTO_CURRENT)
Get a url appropriate for making redirects based on an untrusted url arg.
Definition Title.php:1877
static newFromTitleValue(TitleValue $titleValue)
Create a new Title from a TitleValue.
Definition Title.php:233
string $mPrefixedText
Text form including namespace/interwiki, initialised on demand.
Definition Title.php:138
bool string $mContentModel
ID of the page's content model, i.e.
Definition Title.php:97
getLatestRevID( $flags=0)
What is the page_latest field for this page?
Definition Title.php:3525
static convertByteClassToUnicodeClass( $byteClass)
Utility method for converting a character sequence from bytes to Unicode.
Definition Title.php:637
isValidRedirectTarget()
Check if this Title is a valid redirect target.
Definition Title.php:4863
static HashBagOStuff $titleCache
Definition Title.php:41
getLinksFrom( $options=[], $table='pagelinks', $prefix='pl')
Get an array of Title objects linked from this Title Also stores the IDs in the link cache.
Definition Title.php:3722
static makeName( $ns, $title, $fragment='', $interwiki='', $canonicalNamespace=false)
Make a prefixed DB key from a DB key and a namespace index.
Definition Title.php:741
getLinksTo( $options=[], $table='pagelinks', $prefix='pl')
Get an array of Title objects linking to this Title Also stores the IDs in the link cache.
Definition Title.php:3664
bool $mHasCascadingRestrictions
Are cascading restrictions in effect on this page?
Definition Title.php:129
getPartialURL()
Get the URL-encoded form of the main part.
Definition Title.php:938
getBaseText()
Get the base page name without a namespace, i.e.
Definition Title.php:1704
isContentPage()
Is this Title in a namespace which contains content? In other words, is this a content page,...
Definition Title.php:1212
getText()
Get the text form (spaces not underscores) of the main part.
Definition Title.php:929
getTouched( $db=null)
Get the last touched timestamp.
Definition Title.php:4746
getTitleValue()
Get a TitleValue object representing this Title.
Definition Title.php:906
pageCond()
Get an associative array for selecting this title from the "page" table.
Definition Title.php:4206
bool $mCascadeRestriction
Cascade restrictions on this page to included templates and images?
Definition Title.php:120
string $mUrlform
URL-encoded form of the main part.
Definition Title.php:67
isJsSubpage()
Definition Title.php:1424
getFirstRevision( $flags=0)
Get the first revision of the page.
Definition Title.php:4299
string $mTextform
Text form (spaces not underscores) of the main part.
Definition Title.php:64
getOtherPage()
Get the other title for this page, if this is a subject page get the talk page, if it is a subject pa...
Definition Title.php:1487
static newFromIDs( $ids)
Make an array of titles from an array of IDs.
Definition Title.php:438
getUserPermissionsErrorsInternal( $action, $user, $rigor='secure', $short=false)
Can $user perform $action on this page? This is an internal function, with multiple levels of checks ...
Definition Title.php:2689
quickUserCan( $action, $user=null)
Can $user perform $action on this page? This skips potentially expensive cascading permission checks ...
Definition Title.php:2095
static getSelectFields()
Returns a list of fields that are to be selected for initializing Title objects or LinkCache entries.
Definition Title.php:390
__construct()
Definition Title.php:203
isSubpageOf(Title $title)
Check if this title is a subpage of another title.
Definition Title.php:4540
getBaseTitle()
Get the base page name title, i.e.
Definition Title.php:1729
static newMainPage()
Create a new Title for the Main Page.
Definition Title.php:586
getParentCategoryTree( $children=[])
Get a tree of parent categories.
Definition Title.php:4179
checkSpecialsAndNSPermissions( $action, $user, $errors, $rigor, $short)
Check permissions on special pages & namespaces.
Definition Title.php:2301
bool $mHasSubpages
Whether a page has any subpages.
Definition Title.php:160
isCssSubpage()
Definition Title.php:1387
getNextRevisionID( $revId, $flags=0)
Get the revision ID of the next revision.
Definition Title.php:4289
array $mRestrictionsExpiry
When do the restrictions on this page expire?
Definition Title.php:126
loadRestrictionsFromRows( $rows, $oldFashionedRestrictions=null)
Compiles list of active page restrictions from both page table (pre 1.10) and page_restrictions table...
Definition Title.php:3152
static fixUrlQueryArgs( $query, $query2=false)
Helper to fix up the get{Canonical,Full,Link,Local,Internal}URL args get{Canonical,...
Definition Title.php:1804
isUserJsonConfigPage()
Is this a JSON "config" sub-page of a user page?
Definition Title.php:1398
isValidMoveTarget( $nt)
Checks if $this can be moved to a given Title.
Definition Title.php:4096
loadRestrictions( $oldFashionedRestrictions=null)
Load restrictions from the page_restrictions table.
Definition Title.php:3224
getSquidURLs()
Definition Title.php:3854
isRedirect( $flags=0)
Is this an article that is a redirect page? Uses link cache, adding it if necessary.
Definition Title.php:3462
checkPageRestrictions( $action, $user, $errors, $rigor, $short)
Check against page_restrictions table requirements on this page.
Definition Title.php:2388
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition Title.php:273
invalidateCache( $purgeTime=null)
Updates page_touched for this page; called from LinksUpdate.php.
Definition Title.php:4692
$mCascadingRestrictions
Caching the results of getCascadeProtectionSources.
Definition Title.php:123
static escapeFragmentForURL( $fragment)
Escape a text fragment, say from a link, for a URL.
Definition Title.php:769
getArticleID( $flags=0)
Get the article ID for this Title from the link cache, adding it if necessary.
Definition Title.php:3436
getSubjectNsText()
Get the namespace text of the subject (rather than talk) page.
Definition Title.php:1052
bool $mIsBigDeletion
Would deleting this page be a big deletion?
Definition Title.php:173
int $mNamespace
Namespace index, i.e.
Definition Title.php:76
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition Title.php:562
null $mRedirect
Is the article at this title a redirect?
Definition Title.php:154
countAuthorsBetween( $old, $new, $limit, $options=[])
Get the number of authors between the given revisions or revision IDs.
Definition Title.php:4516
static compare(LinkTarget $a, LinkTarget $b)
Callback for usort() to do title sorts by (namespace, title)
Definition Title.php:786
getCanonicalURL( $query='', $query2=false)
Get the URL for a canonical link, for use in things like IRC and e-mail notifications.
Definition Title.php:2058
checkCascadingSourcesRestrictions( $action, $user, $errors, $rigor, $short)
Check restrictions on cascading pages.
Definition Title.php:2422
isDeleted()
Is there a version of this page in the deletion archive?
Definition Title.php:3385
getPageViewLanguage()
Get the language in which the content of this page is written when viewed by user.
Definition Title.php:5002
const GAID_FOR_UPDATE
Used to be GAID_FOR_UPDATE define.
Definition Title.php:54
getSkinFromConfigSubpage()
Trim down a .css, .json, or .js subpage title to get the corresponding skin name.
Definition Title.php:1350
checkReadPermissions( $action, $user, $errors, $rigor, $short)
Check that the user is allowed to read this page.
Definition Title.php:2591
userCan( $action, $user=null, $rigor='secure')
Can $user perform $action on this page?
Definition Title.php:2108
array $mRestrictions
Array of groups allowed to edit this article.
Definition Title.php:109
isSiteConfigPage()
Could this MediaWiki namespace page contain custom CSS, JSON, or JavaScript for the global UI.
Definition Title.php:1293
int $mDefaultNamespace
Namespace index when there is no namespace.
Definition Title.php:148
moveSubpages( $nt, $auth=true, $reason='', $createRedirect=true, array $changeTags=[])
Move this page's subpages to be subpages of $nt.
Definition Title.php:3972
getRelativeRevisionID( $revId, $flags, $dir)
Get next/previous revision ID relative to another revision ID.
Definition Title.php:4222
deleteTitleProtection()
Remove any title protection due to page existing.
Definition Title.php:2865
getSubpage( $text)
Get the title for a subpage of the current page.
Definition Title.php:1765
getTitleProtection()
Is this title subject to title protection? Title protection is the one applied against creation of su...
Definition Title.php:2795
getEditURL()
Get the edit URL for this Title.
Definition Title.php:2072
getParentCategories()
Get categories to which this Title belongs and return an array of categories' names.
Definition Title.php:4144
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition Title.php:534
int $mArticleID
Article ID, fetched from the link cache on demand.
Definition Title.php:88
static getTitleCache()
Definition Title.php:376
getTemplateLinksFrom( $options=[])
Get an array of Title objects used on this Title as a template Also stores the IDs in the link cache.
Definition Title.php:3776
getTransWikiID()
Returns the DB name of the distant wiki which owns the object.
Definition Title.php:889
isSubpage()
Is this a subpage?
Definition Title.php:1252
isValid()
Returns true if the title is valid, false if it is invalid.
Definition Title.php:808
setFragment( $fragment)
Set the fragment for this title.
Definition Title.php:1559
getLocalURL( $query='', $query2=false)
Get a URL with no fragment or server name (relative URL) from a Title object.
Definition Title.php:1911
getContentModel( $flags=0)
Get the page's content model id, see the CONTENT_MODEL_XXX constants.
Definition Title.php:980
isCssOrJsPage()
Definition Title.php:1308
isBigDeletion()
Check whether the number of revisions of this page surpasses $wgDeleteRevisionsLimit.
Definition Title.php:4346
getCdnUrls()
Get a list of URLs to purge from the CDN cache when this page changes.
Definition Title.php:3824
string $mUserCaseDBKey
Database key with the initial letter in the case specified by the user.
Definition Title.php:73
getInterwiki()
Get the interwiki prefix.
Definition Title.php:857
getEditNotices( $oldid=0)
Get a list of rendered edit notices for this page.
Definition Title.php:5046
__sleep()
Definition Title.php:5117
setContentModel( $model)
Set a proposed content model for the page for permissions checking.
Definition Title.php:1018
getCascadeProtectionSources( $getPages=true)
Cascading protection: Get the source of any cascading restrictions on this page.
Definition Title.php:2992
mixed $mTitleProtection
Cached value for getTitleProtection (create protection)
Definition Title.php:141
getSubpageText()
Get the lowest-level subpage name, i.e.
Definition Title.php:1744
string $mDbkeyform
Main part with underscores.
Definition Title.php:70
hasSourceText()
Does this page have source text?
Definition Title.php:4641
flushRestrictions()
Flush the protection cache in this object and force reload from the database.
Definition Title.php:3279
getPrefixedText()
Get the prefixed title with spaces.
Definition Title.php:1625
hasSubpages()
Does this have subpages? (Warning, usually requires an extra DB query.)
Definition Title.php:3330
string bool $mOldRestrictions
Comma-separated set of permission keys indicating who can move or edit the page from the page table,...
Definition Title.php:117
canTalk()
Can this title have a corresponding talk page?
Definition Title.php:1074
resultToError( $errors, $result)
Add the resulting error code to the errors array.
Definition Title.php:2233
isAlwaysKnown()
Should links to this title be shown as potentially viewable (i.e.
Definition Title.php:4579
getNamespaceKey( $prepend='nstab-')
Generate strings used for xml 'id' names in monobook tabs.
Definition Title.php:4798
getCategorySortkey( $prefix='')
Returns the raw sort key to be used for categories, with the specified prefix.
Definition Title.php:4916
static newFromRow( $row)
Make a Title object from a DB row.
Definition Title.php:464
isUserCssConfigPage()
Is this a CSS "config" sub-page of a user page?
Definition Title.php:1375
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:53
static isEveryoneAllowed( $right)
Check if all users may be assumed to have the given permission.
Definition User.php:5025
static groupHasPermission( $group, $role)
Check, if the given group has the given permission.
Definition User.php:5005
static whoIs( $id)
Get the username corresponding to a given user ID.
Definition User.php:863
static newFatalPermissionDeniedStatus( $permission)
Factory function for fatal permission-denied errors.
Definition User.php:5710
Relational database abstraction object.
Definition Database.php:48
$res
Definition database.txt:21
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add in any and then calling but I prefer the flexibility This should also do the output encoding The system allocates a global one in $wgOut Title Represents the title of an article
Definition design.txt:25
this class mediates it Skin Encapsulates a look and feel for the wiki All of the functions that render HTML and make choices about how to render it are here and are called from various other places when and is meant to be subclassed with other skins that may override some of its functions The User object contains a reference to a and so rather than having a global skin object we just rely on the global User and get the skin with $wgUser and also has some character encoding functions and other locale stuff The current user interface language is instantiated as and the local content language as $wgContLang
Definition design.txt:57
when a variable name is used in a function
Definition design.txt:94
design txt This is a brief overview of the new design More thorough and up to date information is available on the documentation wiki at etc Handles the details of getting and saving to the user table of the and dealing with sessions and cookies OutputPage Encapsulates the entire HTML page that will be sent in response to any server request It is used by calling its functions to add text
Definition design.txt:18
this class mediates it Skin Encapsulates a look and feel for the wiki All of the functions that render HTML and make choices about how to render it are here and are called from various other places when and is meant to be subclassed with other skins that may override some of its functions The User object contains a reference to a and so rather than having a global skin object we just rely on the global User and get the skin with $wgUser and also has some character encoding functions and other locale stuff The current user interface language is instantiated as $wgLang
Definition design.txt:56
the array() calling protocol came about after MediaWiki 1.4rc1.
namespace being checked & $result
Definition hooks.txt:2323
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction $rows
Definition hooks.txt:2783
do that in ParserLimitReportFormat instead $parser
Definition hooks.txt:2603
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account incomplete not yet checked for validity & $retval
Definition hooks.txt:266
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action or null $user:User who performed the tagging when the tagging is subsequent to the action or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy: boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition hooks.txt:1051
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist & $tables
Definition hooks.txt:1015
This code would result in ircNotify being run twice when an article is and once for brion Hooks can return three possible true was required This is the default since MediaWiki *some string
Definition hooks.txt:181
namespace and then decline to actually register it & $namespaces
Definition hooks.txt:934
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition hooks.txt:2001
namespace and then decline to actually register it file or subcat img or subcat $title
Definition hooks.txt:964
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt;div ...>$1&lt;/div>"). - flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException':Called before an exception(or PHP error) is logged. This is meant for integration with external error aggregation services
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition hooks.txt:2005
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition hooks.txt:864
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses & $html
Definition hooks.txt:2013
Allows to change the fields on the form that will be generated $name
Definition hooks.txt:302
null for the local wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition hooks.txt:1620
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition hooks.txt:1777
processing should stop and the error should be shown to the user * false
Definition hooks.txt:187
const PROTO_CANONICAL
Definition Defines.php:233
const CONTENT_MODEL_CSS
Definition Defines.php:247
const NS_FILE
Definition Defines.php:80
const PROTO_CURRENT
Definition Defines.php:232
const NS_MAIN
Definition Defines.php:74
const NS_MEDIAWIKI
Definition Defines.php:82
const NS_SPECIAL
Definition Defines.php:63
const CONTENT_MODEL_WIKITEXT
Definition Defines.php:245
const CONTENT_MODEL_JSON
Definition Defines.php:249
const PROTO_HTTP
Definition Defines.php:229
const NS_MEDIA
Definition Defines.php:62
const PROTO_RELATIVE
Definition Defines.php:231
const NS_CATEGORY
Definition Defines.php:88
const CONTENT_MODEL_JAVASCRIPT
Definition Defines.php:246
$wgActionPaths
Definition img_auth.php:46
$wgArticlePath
Definition img_auth.php:45
Interface for database access objects.
Service interface for looking up Interwiki records.
getInterwiki()
The interwiki component of this LinkTarget.
getFragment()
Get the link fragment (i.e.
getNamespace()
Get the namespace index.
getText()
Returns the link in text form, without namespace prefix or fragment.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
delete( $table, $conds, $fname=__METHOD__)
DELETE query wrapper.
addQuotes( $s)
Adds quotes and backslashes.
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by wfTimestamp() to the format used for inserting ...
update( $table, $values, $conds, $fname=__METHOD__, $options=[])
UPDATE wrapper.
selectFieldValues( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a list of single field values from result rows.
linkcache txt The LinkCache class maintains a list of article titles and the information about whether or not the article exists in the database This is used to mark up links when displaying a page If the same link appears more than once on any page then it only has to be looked up once In most cases link lookups are done in batches with the LinkBatch class or the equivalent in so the link cache is mostly useful for short snippets of parsed and for links in the navigation areas of the skin The link cache was formerly used to track links used in a document for the purposes of updating the link tables This application is now deprecated To create a you can use the following $titles
Definition linkcache.txt:17
$cache
Definition mcc.php:33
$sort
const DB_REPLICA
Definition defines.php:25
const DB_MASTER
Definition defines.php:29
if(!isset( $args[0])) $lang