MediaWiki  master
Title.php
Go to the documentation of this file.
1 <?php
33 
42 class Title implements LinkTarget, IDBAccessObject {
44  private static $titleCache = null;
45 
51  const CACHE_MAX = 1000;
52 
58  const GAID_FOR_UPDATE = 512;
59 
67  const NEW_CLONE = 'clone';
68 
74  // @{
75 
77  public $mTextform = '';
79  public $mUrlform = '';
81  public $mDbkeyform = '';
83  protected $mUserCaseDBKey;
85  public $mNamespace = NS_MAIN;
87  public $mInterwiki = '';
89  private $mLocalInterwiki = false;
91  public $mFragment = '';
92 
94  public $mArticleID = -1;
95 
97  protected $mLatestID = false;
98 
103  private $mContentModel = false;
104 
109  private $mForcedContentModel = false;
110 
113 
115  public $mRestrictions = [];
116 
123  protected $mOldRestrictions = false;
124 
127 
130 
132  protected $mRestrictionsExpiry = [];
133 
136 
139 
141  public $mRestrictionsLoaded = false;
142 
151  public $prefixedText = null;
152 
155 
163 
165  protected $mLength = -1;
166 
168  public $mRedirect = null;
169 
172 
174  private $mHasSubpages;
175 
177  private $mPageLanguage;
178 
182  private $mDbPageLanguage = false;
183 
185  private $mTitleValue = null;
186 
188  private $mIsBigDeletion = null;
189 
191  private $mIsValid = null;
192  // @}
193 
202  private static function getTitleFormatter() {
203  return MediaWikiServices::getInstance()->getTitleFormatter();
204  }
205 
214  private static function getInterwikiLookup() {
215  return MediaWikiServices::getInstance()->getInterwikiLookup();
216  }
217 
221  function __construct() {
222  }
223 
232  public static function newFromDBkey( $key ) {
233  $t = new self();
234 
235  try {
236  $t->secureAndSplit( $key );
237  return $t;
238  } catch ( MalformedTitleException $ex ) {
239  return null;
240  }
241  }
242 
256  public static function newFromTitleValue( TitleValue $titleValue, $forceClone = '' ) {
257  return self::newFromLinkTarget( $titleValue, $forceClone );
258  }
259 
271  public static function newFromLinkTarget( LinkTarget $linkTarget, $forceClone = '' ) {
272  if ( $linkTarget instanceof Title ) {
273  // Special case if it's already a Title object
274  if ( $forceClone === self::NEW_CLONE ) {
275  return clone $linkTarget;
276  } else {
277  return $linkTarget;
278  }
279  }
280  return self::makeTitle(
281  $linkTarget->getNamespace(),
282  $linkTarget->getText(),
283  $linkTarget->getFragment(),
284  $linkTarget->getInterwiki()
285  );
286  }
287 
295  public static function castFromLinkTarget( $linkTarget ) {
296  return $linkTarget ? self::newFromLinkTarget( $linkTarget ) : null;
297  }
298 
319  public static function newFromText( $text, $defaultNamespace = NS_MAIN ) {
320  // DWIM: Integers can be passed in here when page titles are used as array keys.
321  if ( $text !== null && !is_string( $text ) && !is_int( $text ) ) {
322  throw new InvalidArgumentException( '$text must be a string.' );
323  }
324  if ( $text === null ) {
325  return null;
326  }
327 
328  try {
329  return self::newFromTextThrow( (string)$text, (int)$defaultNamespace );
330  } catch ( MalformedTitleException $ex ) {
331  return null;
332  }
333  }
334 
354  public static function newFromTextThrow( $text, $defaultNamespace = NS_MAIN ) {
355  if ( is_object( $text ) ) {
356  throw new MWException( '$text must be a string, given an object' );
357  } elseif ( $text === null ) {
358  // Legacy code relies on MalformedTitleException being thrown in this case
359  // (happens when URL with no title in it is parsed). TODO fix
360  throw new MalformedTitleException( 'title-invalid-empty' );
361  }
362 
363  $titleCache = self::getTitleCache();
364 
365  // Wiki pages often contain multiple links to the same page.
366  // Title normalization and parsing can become expensive on pages with many
367  // links, so we can save a little time by caching them.
368  // In theory these are value objects and won't get changed...
369  if ( $defaultNamespace == NS_MAIN ) {
370  $t = $titleCache->get( $text );
371  if ( $t ) {
372  return $t;
373  }
374  }
375 
376  // Convert things like &eacute; &#257; or &#x3017; into normalized (T16952) text
377  $filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text );
378 
379  $t = new Title();
380  $dbKeyForm = strtr( $filteredText, ' ', '_' );
381 
382  $t->secureAndSplit( $dbKeyForm, (int)$defaultNamespace );
383  if ( $defaultNamespace == NS_MAIN ) {
384  $titleCache->set( $text, $t );
385  }
386  return $t;
387  }
388 
404  public static function newFromURL( $url ) {
405  $t = new Title();
406 
407  # For compatibility with old buggy URLs. "+" is usually not valid in titles,
408  # but some URLs used it as a space replacement and they still come
409  # from some external search tools.
410  if ( strpos( self::legalChars(), '+' ) === false ) {
411  $url = strtr( $url, '+', ' ' );
412  }
413 
414  $dbKeyForm = strtr( $url, ' ', '_' );
415 
416  try {
417  $t->secureAndSplit( $dbKeyForm );
418  return $t;
419  } catch ( MalformedTitleException $ex ) {
420  return null;
421  }
422  }
423 
427  private static function getTitleCache() {
428  if ( self::$titleCache === null ) {
429  self::$titleCache = new MapCacheLRU( self::CACHE_MAX );
430  }
431  return self::$titleCache;
432  }
433 
441  protected static function getSelectFields() {
443 
444  $fields = [
445  'page_namespace', 'page_title', 'page_id',
446  'page_len', 'page_is_redirect', 'page_latest',
447  ];
448 
449  if ( $wgContentHandlerUseDB ) {
450  $fields[] = 'page_content_model';
451  }
452 
453  if ( $wgPageLanguageUseDB ) {
454  $fields[] = 'page_lang';
455  }
456 
457  return $fields;
458  }
459 
467  public static function newFromID( $id, $flags = 0 ) {
468  $flags |= ( $flags & self::GAID_FOR_UPDATE ) ? self::READ_LATEST : 0; // b/c
469  list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
470  $row = wfGetDB( $index )->selectRow(
471  'page',
472  self::getSelectFields(),
473  [ 'page_id' => $id ],
474  __METHOD__,
475  $options
476  );
477  if ( $row !== false ) {
478  $title = self::newFromRow( $row );
479  } else {
480  $title = null;
481  }
482 
483  return $title;
484  }
485 
492  public static function newFromIDs( $ids ) {
493  if ( !count( $ids ) ) {
494  return [];
495  }
496  $dbr = wfGetDB( DB_REPLICA );
497 
498  $res = $dbr->select(
499  'page',
500  self::getSelectFields(),
501  [ 'page_id' => $ids ],
502  __METHOD__
503  );
504 
505  $titles = [];
506  foreach ( $res as $row ) {
507  $titles[] = self::newFromRow( $row );
508  }
509  return $titles;
510  }
511 
518  public static function newFromRow( $row ) {
519  $t = self::makeTitle( $row->page_namespace, $row->page_title );
520  $t->loadFromRow( $row );
521  return $t;
522  }
523 
530  public function loadFromRow( $row ) {
531  if ( $row ) { // page found
532  if ( isset( $row->page_id ) ) {
533  $this->mArticleID = (int)$row->page_id;
534  }
535  if ( isset( $row->page_len ) ) {
536  $this->mLength = (int)$row->page_len;
537  }
538  if ( isset( $row->page_is_redirect ) ) {
539  $this->mRedirect = (bool)$row->page_is_redirect;
540  }
541  if ( isset( $row->page_latest ) ) {
542  $this->mLatestID = (int)$row->page_latest;
543  }
544  if ( isset( $row->page_content_model ) ) {
545  $this->lazyFillContentModel( $row->page_content_model );
546  } else {
547  $this->lazyFillContentModel( false ); // lazily-load getContentModel()
548  }
549  if ( isset( $row->page_lang ) ) {
550  $this->mDbPageLanguage = (string)$row->page_lang;
551  }
552  if ( isset( $row->page_restrictions ) ) {
553  $this->mOldRestrictions = $row->page_restrictions;
554  }
555  } else { // page not found
556  $this->mArticleID = 0;
557  $this->mLength = 0;
558  $this->mRedirect = false;
559  $this->mLatestID = 0;
560  $this->lazyFillContentModel( false ); // lazily-load getContentModel()
561  }
562  }
563 
586  public static function makeTitle( $ns, $title, $fragment = '', $interwiki = '' ) {
587  $t = new Title();
588  $t->mInterwiki = $interwiki;
589  $t->mFragment = $fragment;
590  $t->mNamespace = $ns = (int)$ns;
591  $t->mDbkeyform = strtr( $title, ' ', '_' );
592  $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
593  $t->mUrlform = wfUrlencode( $t->mDbkeyform );
594  $t->mTextform = strtr( $title, '_', ' ' );
595  return $t;
596  }
597 
612  public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) {
613  // NOTE: ideally, this would just call makeTitle() and then isValid(),
614  // but presently, that means more overhead on a potential performance hotspot.
615 
616  if ( !MediaWikiServices::getInstance()->getNamespaceInfo()->exists( $ns ) ) {
617  return null;
618  }
619 
620  $t = new Title();
621  $dbKeyForm = self::makeName( $ns, $title, $fragment, $interwiki, true );
622 
623  try {
624  $t->secureAndSplit( $dbKeyForm );
625  return $t;
626  } catch ( MalformedTitleException $ex ) {
627  return null;
628  }
629  }
630 
648  public static function newMainPage( MessageLocalizer $localizer = null ) {
649  if ( $localizer ) {
650  $msg = $localizer->msg( 'mainpage' );
651  } else {
652  $msg = wfMessage( 'mainpage' );
653  }
654 
655  $title = self::newFromText( $msg->inContentLanguage()->text() );
656 
657  // Every page renders at least one link to the Main Page (e.g. sidebar).
658  // If the localised value is invalid, don't produce fatal errors that
659  // would make the wiki inaccessible (and hard to fix the invalid message).
660  // Gracefully fallback...
661  if ( !$title ) {
662  $title = self::newFromText( 'Main Page' );
663  }
664  return $title;
665  }
666 
673  public static function nameOf( $id ) {
674  $dbr = wfGetDB( DB_REPLICA );
675 
676  $s = $dbr->selectRow(
677  'page',
678  [ 'page_namespace', 'page_title' ],
679  [ 'page_id' => $id ],
680  __METHOD__
681  );
682  if ( $s === false ) {
683  return null;
684  }
685 
686  return self::makeName( $s->page_namespace, $s->page_title );
687  }
688 
694  public static function legalChars() {
695  global $wgLegalTitleChars;
696  return $wgLegalTitleChars;
697  }
698 
708  public static function convertByteClassToUnicodeClass( $byteClass ) {
709  $length = strlen( $byteClass );
710  // Input token queue
711  $x0 = $x1 = $x2 = '';
712  // Decoded queue
713  $d0 = $d1 = $d2 = '';
714  // Decoded integer codepoints
715  $ord0 = $ord1 = $ord2 = 0;
716  // Re-encoded queue
717  $r0 = $r1 = $r2 = '';
718  // Output
719  $out = '';
720  // Flags
721  $allowUnicode = false;
722  for ( $pos = 0; $pos < $length; $pos++ ) {
723  // Shift the queues down
724  $x2 = $x1;
725  $x1 = $x0;
726  $d2 = $d1;
727  $d1 = $d0;
728  $ord2 = $ord1;
729  $ord1 = $ord0;
730  $r2 = $r1;
731  $r1 = $r0;
732  // Load the current input token and decoded values
733  $inChar = $byteClass[$pos];
734  if ( $inChar == '\\' ) {
735  if ( preg_match( '/x([0-9a-fA-F]{2})/A', $byteClass, $m, 0, $pos + 1 ) ) {
736  $x0 = $inChar . $m[0];
737  $d0 = chr( hexdec( $m[1] ) );
738  $pos += strlen( $m[0] );
739  } elseif ( preg_match( '/[0-7]{3}/A', $byteClass, $m, 0, $pos + 1 ) ) {
740  $x0 = $inChar . $m[0];
741  $d0 = chr( octdec( $m[0] ) );
742  $pos += strlen( $m[0] );
743  } elseif ( $pos + 1 >= $length ) {
744  $x0 = $d0 = '\\';
745  } else {
746  $d0 = $byteClass[$pos + 1];
747  $x0 = $inChar . $d0;
748  $pos += 1;
749  }
750  } else {
751  $x0 = $d0 = $inChar;
752  }
753  $ord0 = ord( $d0 );
754  // Load the current re-encoded value
755  if ( $ord0 < 32 || $ord0 == 0x7f ) {
756  $r0 = sprintf( '\x%02x', $ord0 );
757  } elseif ( $ord0 >= 0x80 ) {
758  // Allow unicode if a single high-bit character appears
759  $r0 = sprintf( '\x%02x', $ord0 );
760  $allowUnicode = true;
761  // @phan-suppress-next-line PhanParamSuspiciousOrder false positive
762  } elseif ( strpos( '-\\[]^', $d0 ) !== false ) {
763  $r0 = '\\' . $d0;
764  } else {
765  $r0 = $d0;
766  }
767  // Do the output
768  if ( $x0 !== '' && $x1 === '-' && $x2 !== '' ) {
769  // Range
770  if ( $ord2 > $ord0 ) {
771  // Empty range
772  } elseif ( $ord0 >= 0x80 ) {
773  // Unicode range
774  $allowUnicode = true;
775  if ( $ord2 < 0x80 ) {
776  // Keep the non-unicode section of the range
777  $out .= "$r2-\\x7F";
778  }
779  } else {
780  // Normal range
781  $out .= "$r2-$r0";
782  }
783  // Reset state to the initial value
784  $x0 = $x1 = $d0 = $d1 = $r0 = $r1 = '';
785  } elseif ( $ord2 < 0x80 ) {
786  // ASCII character
787  $out .= $r2;
788  }
789  }
790  if ( $ord1 < 0x80 ) {
791  $out .= $r1;
792  }
793  if ( $ord0 < 0x80 ) {
794  $out .= $r0;
795  }
796  if ( $allowUnicode ) {
797  $out .= '\u0080-\uFFFF';
798  }
799  return $out;
800  }
801 
813  public static function makeName( $ns, $title, $fragment = '', $interwiki = '',
814  $canonicalNamespace = false
815  ) {
816  if ( $canonicalNamespace ) {
817  $namespace = MediaWikiServices::getInstance()->getNamespaceInfo()->
818  getCanonicalName( $ns );
819  } else {
820  $namespace = MediaWikiServices::getInstance()->getContentLanguage()->getNsText( $ns );
821  }
822  $name = $namespace == '' ? $title : "$namespace:$title";
823  if ( strval( $interwiki ) != '' ) {
824  $name = "$interwiki:$name";
825  }
826  if ( strval( $fragment ) != '' ) {
827  $name .= '#' . $fragment;
828  }
829  return $name;
830  }
831 
840  public static function compare( LinkTarget $a, LinkTarget $b ) {
841  return $a->getNamespace() <=> $b->getNamespace()
842  ?: strcmp( $a->getText(), $b->getText() );
843  }
844 
861  public function isValid() {
862  if ( $this->mIsValid !== null ) {
863  return $this->mIsValid;
864  }
865 
866  try {
867  $text = $this->getFullText();
868  $titleCodec = MediaWikiServices::getInstance()->getTitleParser();
869 
870  '@phan-var MediaWikiTitleCodec $titleCodec';
871  $parts = $titleCodec->splitTitleString( $text, $this->mNamespace );
872 
873  // Check that nothing changed!
874  // This ensures that $text was already perperly normalized.
875  if ( $parts['fragment'] !== $this->mFragment
876  || $parts['interwiki'] !== $this->mInterwiki
877  || $parts['local_interwiki'] !== $this->mLocalInterwiki
878  || $parts['namespace'] !== $this->mNamespace
879  || $parts['dbkey'] !== $this->mDbkeyform
880  ) {
881  $this->mIsValid = false;
882  return $this->mIsValid;
883  }
884  } catch ( MalformedTitleException $ex ) {
885  $this->mIsValid = false;
886  return $this->mIsValid;
887  }
888 
889  $this->mIsValid = true;
890  return $this->mIsValid;
891  }
892 
900  public function isLocal() {
901  if ( $this->isExternal() ) {
902  $iw = self::getInterwikiLookup()->fetch( $this->mInterwiki );
903  if ( $iw ) {
904  return $iw->isLocal();
905  }
906  }
907  return true;
908  }
909 
915  public function isExternal() {
916  return $this->mInterwiki !== '';
917  }
918 
926  public function getInterwiki() {
927  return $this->mInterwiki;
928  }
929 
935  public function wasLocalInterwiki() {
936  return $this->mLocalInterwiki;
937  }
938 
945  public function isTrans() {
946  if ( !$this->isExternal() ) {
947  return false;
948  }
949 
950  return self::getInterwikiLookup()->fetch( $this->mInterwiki )->isTranscludable();
951  }
952 
958  public function getTransWikiID() {
959  if ( !$this->isExternal() ) {
960  return false;
961  }
962 
963  return self::getInterwikiLookup()->fetch( $this->mInterwiki )->getWikiID();
964  }
965 
975  public function getTitleValue() {
976  if ( $this->mTitleValue === null ) {
977  try {
978  $this->mTitleValue = new TitleValue(
979  $this->mNamespace,
980  $this->mDbkeyform,
981  $this->mFragment,
982  $this->mInterwiki
983  );
984  } catch ( InvalidArgumentException $ex ) {
985  wfDebug( __METHOD__ . ': Can\'t create a TitleValue for [[' .
986  $this->getPrefixedText() . ']]: ' . $ex->getMessage() . "\n" );
987  }
988  }
989 
990  return $this->mTitleValue;
991  }
992 
998  public function getText() {
999  return $this->mTextform;
1000  }
1001 
1007  public function getPartialURL() {
1008  return $this->mUrlform;
1009  }
1010 
1016  public function getDBkey() {
1017  return $this->mDbkeyform;
1018  }
1019 
1026  function getUserCaseDBKey() {
1027  if ( !is_null( $this->mUserCaseDBKey ) ) {
1028  return $this->mUserCaseDBKey;
1029  } else {
1030  // If created via makeTitle(), $this->mUserCaseDBKey is not set.
1031  return $this->mDbkeyform;
1032  }
1033  }
1034 
1040  public function getNamespace() {
1041  return $this->mNamespace;
1042  }
1043 
1052  public function getContentModel( $flags = 0 ) {
1053  if ( $this->mForcedContentModel ) {
1054  if ( !$this->mContentModel ) {
1055  throw new RuntimeException( 'Got out of sync; an empty model is being forced' );
1056  }
1057  // Content model is locked to the currently loaded one
1058  return $this->mContentModel;
1059  }
1060 
1061  if ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) {
1062  $this->lazyFillContentModel( $this->loadFieldFromDB( 'page_content_model', $flags ) );
1063  } elseif (
1064  ( !$this->mContentModel || $flags & self::GAID_FOR_UPDATE ) &&
1065  $this->getArticleID( $flags )
1066  ) {
1067  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
1068  $linkCache->addLinkObj( $this ); # in case we already had an article ID
1069  $this->lazyFillContentModel( $linkCache->getGoodLinkFieldObj( $this, 'model' ) );
1070  }
1071 
1072  if ( !$this->mContentModel ) {
1074  }
1075 
1076  return $this->mContentModel;
1077  }
1078 
1085  public function hasContentModel( $id ) {
1086  return $this->getContentModel() == $id;
1087  }
1088 
1102  public function setContentModel( $model ) {
1103  if ( (string)$model === '' ) {
1104  throw new InvalidArgumentException( "Missing CONTENT_MODEL_* constant" );
1105  }
1106 
1107  $this->mContentModel = $model;
1108  $this->mForcedContentModel = true;
1109  }
1110 
1116  private function lazyFillContentModel( $model ) {
1117  if ( !$this->mForcedContentModel ) {
1118  $this->mContentModel = ( $model === false ) ? false : (string)$model;
1119  }
1120  }
1121 
1127  public function getNsText() {
1128  if ( $this->isExternal() ) {
1129  // This probably shouldn't even happen, except for interwiki transclusion.
1130  // If possible, use the canonical name for the foreign namespace.
1131  $nsText = MediaWikiServices::getInstance()->getNamespaceInfo()->
1132  getCanonicalName( $this->mNamespace );
1133  if ( $nsText !== false ) {
1134  return $nsText;
1135  }
1136  }
1137 
1138  try {
1139  $formatter = self::getTitleFormatter();
1140  return $formatter->getNamespaceName( $this->mNamespace, $this->mDbkeyform );
1141  } catch ( InvalidArgumentException $ex ) {
1142  wfDebug( __METHOD__ . ': ' . $ex->getMessage() . "\n" );
1143  return false;
1144  }
1145  }
1146 
1152  public function getSubjectNsText() {
1153  $services = MediaWikiServices::getInstance();
1154  return $services->getContentLanguage()->
1155  getNsText( $services->getNamespaceInfo()->getSubject( $this->mNamespace ) );
1156  }
1157 
1163  public function getTalkNsText() {
1164  $services = MediaWikiServices::getInstance();
1165  return $services->getContentLanguage()->
1166  getNsText( $services->getNamespaceInfo()->getTalk( $this->mNamespace ) );
1167  }
1168 
1180  public function canHaveTalkPage() {
1181  return MediaWikiServices::getInstance()->getNamespaceInfo()->canHaveTalkPage( $this );
1182  }
1183 
1194  public function canExist() {
1195  // NOTE: Don't use getArticleID(), we don't want to
1196  // trigger a database query here. This check is supposed to
1197  // act as an optimization, not add extra cost.
1198  if ( $this->mArticleID > 0 ) {
1199  // It exists, so it can exist.
1200  return true;
1201  }
1202 
1203  // NOTE: we call the relatively expensive isValid() method further down,
1204  // but we can bail out early if we already know the title is invalid.
1205  if ( $this->mIsValid === false ) {
1206  // It's invalid, so it can't exist.
1207  return false;
1208  }
1209 
1210  if ( $this->getNamespace() < NS_MAIN ) {
1211  // It's a special page, so it can't exist in the database.
1212  return false;
1213  }
1214 
1215  if ( $this->isExternal() ) {
1216  // If it's external, it's not local, so it can't exist.
1217  return false;
1218  }
1219 
1220  if ( $this->getText() === '' ) {
1221  // The title has no text, so it can't exist in the database.
1222  // It's probably an on-page section link, like "#something".
1223  return false;
1224  }
1225 
1226  // Double check that the title is valid.
1227  return $this->isValid();
1228  }
1229 
1238  public function isWatchable() {
1239  $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
1240  return $this->getText() !== '' && !$this->isExternal() &&
1241  $nsInfo->isWatchable( $this->mNamespace );
1242  }
1243 
1249  public function isSpecialPage() {
1250  return $this->mNamespace == NS_SPECIAL;
1251  }
1252 
1259  public function isSpecial( $name ) {
1260  if ( $this->isSpecialPage() ) {
1261  list( $thisName, /* $subpage */ ) =
1262  MediaWikiServices::getInstance()->getSpecialPageFactory()->
1263  resolveAlias( $this->mDbkeyform );
1264  if ( $name == $thisName ) {
1265  return true;
1266  }
1267  }
1268  return false;
1269  }
1270 
1277  public function fixSpecialName() {
1278  if ( $this->isSpecialPage() ) {
1279  $spFactory = MediaWikiServices::getInstance()->getSpecialPageFactory();
1280  list( $canonicalName, $par ) = $spFactory->resolveAlias( $this->mDbkeyform );
1281  if ( $canonicalName ) {
1282  $localName = $spFactory->getLocalNameFor( $canonicalName, $par );
1283  if ( $localName != $this->mDbkeyform ) {
1284  return self::makeTitle( NS_SPECIAL, $localName );
1285  }
1286  }
1287  }
1288  return $this;
1289  }
1290 
1301  public function inNamespace( $ns ) {
1302  return MediaWikiServices::getInstance()->getNamespaceInfo()->
1303  equals( $this->mNamespace, $ns );
1304  }
1305 
1313  public function inNamespaces( ...$namespaces ) {
1314  if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) {
1315  $namespaces = $namespaces[0];
1316  }
1317 
1318  foreach ( $namespaces as $ns ) {
1319  if ( $this->inNamespace( $ns ) ) {
1320  return true;
1321  }
1322  }
1323 
1324  return false;
1325  }
1326 
1340  public function hasSubjectNamespace( $ns ) {
1341  return MediaWikiServices::getInstance()->getNamespaceInfo()->
1342  subjectEquals( $this->mNamespace, $ns );
1343  }
1344 
1352  public function isContentPage() {
1353  return MediaWikiServices::getInstance()->getNamespaceInfo()->
1354  isContent( $this->mNamespace );
1355  }
1356 
1363  public function isMovable() {
1364  if (
1365  !MediaWikiServices::getInstance()->getNamespaceInfo()->
1366  isMovable( $this->mNamespace ) || $this->isExternal()
1367  ) {
1368  // Interwiki title or immovable namespace. Hooks don't get to override here
1369  return false;
1370  }
1371 
1372  $result = true;
1373  Hooks::run( 'TitleIsMovable', [ $this, &$result ] );
1374  return $result;
1375  }
1376 
1387  public function isMainPage() {
1388  return $this->equals( self::newMainPage() );
1389  }
1390 
1396  public function isSubpage() {
1397  return MediaWikiServices::getInstance()->getNamespaceInfo()->
1398  hasSubpages( $this->mNamespace )
1399  ? strpos( $this->getText(), '/' ) !== false
1400  : false;
1401  }
1402 
1408  public function isConversionTable() {
1409  // @todo ConversionTable should become a separate content model.
1410 
1411  return $this->mNamespace == NS_MEDIAWIKI &&
1412  strpos( $this->getText(), 'Conversiontable/' ) === 0;
1413  }
1414 
1420  public function isWikitextPage() {
1421  return $this->hasContentModel( CONTENT_MODEL_WIKITEXT );
1422  }
1423 
1438  public function isSiteConfigPage() {
1439  return (
1440  $this->isSiteCssConfigPage()
1441  || $this->isSiteJsonConfigPage()
1442  || $this->isSiteJsConfigPage()
1443  );
1444  }
1445 
1452  public function isUserConfigPage() {
1453  return (
1454  $this->isUserCssConfigPage()
1455  || $this->isUserJsonConfigPage()
1456  || $this->isUserJsConfigPage()
1457  );
1458  }
1459 
1466  public function getSkinFromConfigSubpage() {
1467  $subpage = explode( '/', $this->mTextform );
1468  $subpage = $subpage[count( $subpage ) - 1];
1469  $lastdot = strrpos( $subpage, '.' );
1470  if ( $lastdot === false ) {
1471  return $subpage; # Never happens: only called for names ending in '.css'/'.json'/'.js'
1472  }
1473  return substr( $subpage, 0, $lastdot );
1474  }
1475 
1482  public function isUserCssConfigPage() {
1483  return (
1484  NS_USER == $this->mNamespace
1485  && $this->isSubpage()
1486  && $this->hasContentModel( CONTENT_MODEL_CSS )
1487  );
1488  }
1489 
1496  public function isUserJsonConfigPage() {
1497  return (
1498  NS_USER == $this->mNamespace
1499  && $this->isSubpage()
1500  && $this->hasContentModel( CONTENT_MODEL_JSON )
1501  );
1502  }
1503 
1510  public function isUserJsConfigPage() {
1511  return (
1512  NS_USER == $this->mNamespace
1513  && $this->isSubpage()
1515  );
1516  }
1517 
1524  public function isSiteCssConfigPage() {
1525  return (
1526  NS_MEDIAWIKI == $this->mNamespace
1527  && (
1529  // paranoia - a MediaWiki: namespace page with mismatching extension and content
1530  // model is probably by mistake and might get handled incorrectly (see e.g. T112937)
1531  || substr( $this->mDbkeyform, -4 ) === '.css'
1532  )
1533  );
1534  }
1535 
1542  public function isSiteJsonConfigPage() {
1543  return (
1544  NS_MEDIAWIKI == $this->mNamespace
1545  && (
1547  // paranoia - a MediaWiki: namespace page with mismatching extension and content
1548  // model is probably by mistake and might get handled incorrectly (see e.g. T112937)
1549  || substr( $this->mDbkeyform, -5 ) === '.json'
1550  )
1551  );
1552  }
1553 
1560  public function isSiteJsConfigPage() {
1561  return (
1562  NS_MEDIAWIKI == $this->mNamespace
1563  && (
1565  // paranoia - a MediaWiki: namespace page with mismatching extension and content
1566  // model is probably by mistake and might get handled incorrectly (see e.g. T112937)
1567  || substr( $this->mDbkeyform, -3 ) === '.js'
1568  )
1569  );
1570  }
1571 
1578  public function isRawHtmlMessage() {
1579  global $wgRawHtmlMessages;
1580 
1581  if ( !$this->inNamespace( NS_MEDIAWIKI ) ) {
1582  return false;
1583  }
1584  $message = lcfirst( $this->getRootTitle()->getDBkey() );
1585  return in_array( $message, $wgRawHtmlMessages, true );
1586  }
1587 
1593  public function isTalkPage() {
1594  return MediaWikiServices::getInstance()->getNamespaceInfo()->
1595  isTalk( $this->mNamespace );
1596  }
1597 
1609  public function getTalkPage() {
1610  // NOTE: The equivalent code in NamespaceInfo is less lenient about producing invalid titles.
1611  // Instead of failing on invalid titles, let's just log the issue for now.
1612  // See the discussion on T227817.
1613 
1614  // Is this the same title?
1615  $talkNS = MediaWikiServices::getInstance()->getNamespaceInfo()->getTalk( $this->mNamespace );
1616  if ( $this->mNamespace == $talkNS ) {
1617  return $this;
1618  }
1619 
1620  $title = self::makeTitle( $talkNS, $this->mDbkeyform );
1621 
1622  $this->warnIfPageCannotExist( $title, __METHOD__ );
1623 
1624  return $title;
1625  // TODO: replace the above with the code below:
1626  // return self::castFromLinkTarget(
1627  // MediaWikiServices::getInstance()->getNamespaceInfo()->getTalkPage( $this ) );
1628  }
1629 
1639  public function getTalkPageIfDefined() {
1640  if ( !$this->canHaveTalkPage() ) {
1641  return null;
1642  }
1643 
1644  return $this->getTalkPage();
1645  }
1646 
1654  public function getSubjectPage() {
1655  // Is this the same title?
1656  $subjectNS = MediaWikiServices::getInstance()->getNamespaceInfo()
1657  ->getSubject( $this->mNamespace );
1658  if ( $this->mNamespace == $subjectNS ) {
1659  return $this;
1660  }
1661  // NOTE: The equivalent code in NamespaceInfo is less lenient about producing invalid titles.
1662  // Instead of failing on invalid titles, let's just log the issue for now.
1663  // See the discussion on T227817.
1664  $title = self::makeTitle( $subjectNS, $this->mDbkeyform );
1665 
1666  $this->warnIfPageCannotExist( $title, __METHOD__ );
1667 
1668  return $title;
1669  // TODO: replace the above with the code below:
1670  // return self::castFromLinkTarget(
1671  // MediaWikiServices::getInstance()->getNamespaceInfo()->getSubjectPage( $this ) );
1672  }
1673 
1680  private function warnIfPageCannotExist( Title $title, $method ) {
1681  if ( $this->getText() == '' ) {
1682  wfLogWarning(
1683  $method . ': called on empty title ' . $this->getFullText() . ', returning '
1684  . $title->getFullText()
1685  );
1686 
1687  return true;
1688  }
1689 
1690  if ( $this->getInterwiki() !== '' ) {
1691  wfLogWarning(
1692  $method . ': called on interwiki title ' . $this->getFullText() . ', returning '
1693  . $title->getFullText()
1694  );
1695 
1696  return true;
1697  }
1698 
1699  return false;
1700  }
1701 
1711  public function getOtherPage() {
1712  // NOTE: Depend on the methods in this class instead of their equivalent in NamespaceInfo,
1713  // until their semantics has become exactly the same.
1714  // See the discussion on T227817.
1715  if ( $this->isSpecialPage() ) {
1716  throw new MWException( 'Special pages cannot have other pages' );
1717  }
1718  if ( $this->isTalkPage() ) {
1719  return $this->getSubjectPage();
1720  } else {
1721  if ( !$this->canHaveTalkPage() ) {
1722  throw new MWException( "{$this->getPrefixedText()} does not have an other page" );
1723  }
1724  return $this->getTalkPage();
1725  }
1726  // TODO: replace the above with the code below:
1727  // return self::castFromLinkTarget(
1728  // MediaWikiServices::getInstance()->getNamespaceInfo()->getAssociatedPage( $this ) );
1729  }
1730 
1736  public function getDefaultNamespace() {
1737  return $this->mDefaultNamespace;
1738  }
1739 
1747  public function getFragment() {
1748  return $this->mFragment;
1749  }
1750 
1757  public function hasFragment() {
1758  return $this->mFragment !== '';
1759  }
1760 
1766  public function getFragmentForURL() {
1767  if ( !$this->hasFragment() ) {
1768  return '';
1769  } elseif ( $this->isExternal() ) {
1770  // Note: If the interwiki is unknown, it's treated as a namespace on the local wiki,
1771  // so we treat it like a local interwiki.
1772  $interwiki = self::getInterwikiLookup()->fetch( $this->mInterwiki );
1773  if ( $interwiki && !$interwiki->isLocal() ) {
1774  return '#' . Sanitizer::escapeIdForExternalInterwiki( $this->mFragment );
1775  }
1776  }
1777 
1778  return '#' . Sanitizer::escapeIdForLink( $this->mFragment );
1779  }
1780 
1793  public function setFragment( $fragment ) {
1794  $this->mFragment = strtr( substr( $fragment, 1 ), '_', ' ' );
1795  }
1796 
1804  public function createFragmentTarget( $fragment ) {
1805  return self::makeTitle(
1806  $this->mNamespace,
1807  $this->getText(),
1808  $fragment,
1809  $this->mInterwiki
1810  );
1811  }
1812 
1820  private function prefix( $name ) {
1821  $p = '';
1822  if ( $this->isExternal() ) {
1823  $p = $this->mInterwiki . ':';
1824  }
1825 
1826  if ( $this->mNamespace != 0 ) {
1827  $nsText = $this->getNsText();
1828 
1829  if ( $nsText === false ) {
1830  // See T165149. Awkward, but better than erroneously linking to the main namespace.
1831  $nsText = MediaWikiServices::getInstance()->getContentLanguage()->
1832  getNsText( NS_SPECIAL ) . ":Badtitle/NS{$this->mNamespace}";
1833  }
1834 
1835  $p .= $nsText . ':';
1836  }
1837  return $p . $name;
1838  }
1839 
1846  public function getPrefixedDBkey() {
1847  $s = $this->prefix( $this->mDbkeyform );
1848  $s = strtr( $s, ' ', '_' );
1849  return $s;
1850  }
1851 
1858  public function getPrefixedText() {
1859  if ( $this->prefixedText === null ) {
1860  $s = $this->prefix( $this->mTextform );
1861  $s = strtr( $s, '_', ' ' );
1862  $this->prefixedText = $s;
1863  }
1864  return $this->prefixedText;
1865  }
1866 
1872  public function __toString() {
1873  return $this->getPrefixedText();
1874  }
1875 
1882  public function getFullText() {
1883  $text = $this->getPrefixedText();
1884  if ( $this->hasFragment() ) {
1885  $text .= '#' . $this->mFragment;
1886  }
1887  return $text;
1888  }
1889 
1905  public function getRootText() {
1906  if (
1907  !MediaWikiServices::getInstance()->getNamespaceInfo()->
1908  hasSubpages( $this->mNamespace )
1909  || strtok( $this->getText(), '/' ) === false
1910  ) {
1911  return $this->getText();
1912  }
1913 
1914  return strtok( $this->getText(), '/' );
1915  }
1916 
1929  public function getRootTitle() {
1930  $title = self::makeTitleSafe( $this->mNamespace, $this->getRootText() );
1931  Assert::postcondition(
1932  $title !== null,
1933  'makeTitleSafe() should always return a Title for the text returned by getRootText().'
1934  );
1935  return $title;
1936  }
1937 
1952  public function getBaseText() {
1953  $text = $this->getText();
1954  if (
1955  !MediaWikiServices::getInstance()->getNamespaceInfo()->
1956  hasSubpages( $this->mNamespace )
1957  ) {
1958  return $text;
1959  }
1960 
1961  $lastSlashPos = strrpos( $text, '/' );
1962  // Don't discard the real title if there's no subpage involved
1963  if ( $lastSlashPos === false ) {
1964  return $text;
1965  }
1966 
1967  return substr( $text, 0, $lastSlashPos );
1968  }
1969 
1982  public function getBaseTitle() {
1983  $title = self::makeTitleSafe( $this->mNamespace, $this->getBaseText() );
1984  Assert::postcondition(
1985  $title !== null,
1986  'makeTitleSafe() should always return a Title for the text returned by getBaseText().'
1987  );
1988  return $title;
1989  }
1990 
2002  public function getSubpageText() {
2003  if (
2004  !MediaWikiServices::getInstance()->getNamespaceInfo()->
2005  hasSubpages( $this->mNamespace )
2006  ) {
2007  return $this->mTextform;
2008  }
2009  $parts = explode( '/', $this->mTextform );
2010  return $parts[count( $parts ) - 1];
2011  }
2012 
2026  public function getSubpage( $text ) {
2027  return self::makeTitleSafe(
2028  $this->mNamespace,
2029  $this->getText() . '/' . $text,
2030  '',
2031  $this->mInterwiki
2032  );
2033  }
2034 
2040  public function getSubpageUrlForm() {
2041  $text = $this->getSubpageText();
2042  $text = wfUrlencode( strtr( $text, ' ', '_' ) );
2043  return $text;
2044  }
2045 
2051  public function getPrefixedURL() {
2052  $s = $this->prefix( $this->mDbkeyform );
2053  $s = wfUrlencode( strtr( $s, ' ', '_' ) );
2054  return $s;
2055  }
2056 
2070  private static function fixUrlQueryArgs( $query, $query2 = false ) {
2071  if ( $query2 !== false ) {
2072  wfDeprecated( "Title::get{Canonical,Full,Link,Local,Internal}URL " .
2073  "method called with a second parameter is deprecated. Add your " .
2074  "parameter to an array passed as the first parameter.", "1.19" );
2075  }
2076  if ( is_array( $query ) ) {
2077  $query = wfArrayToCgi( $query );
2078  }
2079  if ( $query2 ) {
2080  if ( is_string( $query2 ) ) {
2081  // $query2 is a string, we will consider this to be
2082  // a deprecated $variant argument and add it to the query
2083  $query2 = wfArrayToCgi( [ 'variant' => $query2 ] );
2084  } else {
2085  $query2 = wfArrayToCgi( $query2 );
2086  }
2087  // If we have $query content add a & to it first
2088  if ( $query ) {
2089  $query .= '&';
2090  }
2091  // Now append the queries together
2092  $query .= $query2;
2093  }
2094  return $query;
2095  }
2096 
2108  public function getFullURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) {
2109  $query = self::fixUrlQueryArgs( $query, $query2 );
2110 
2111  # Hand off all the decisions on urls to getLocalURL
2112  $url = $this->getLocalURL( $query );
2113 
2114  # Expand the url to make it a full url. Note that getLocalURL has the
2115  # potential to output full urls for a variety of reasons, so we use
2116  # wfExpandUrl instead of simply prepending $wgServer
2117  $url = wfExpandUrl( $url, $proto );
2118 
2119  # Finally, add the fragment.
2120  $url .= $this->getFragmentForURL();
2121  // Avoid PHP 7.1 warning from passing $this by reference
2122  $titleRef = $this;
2123  Hooks::run( 'GetFullURL', [ &$titleRef, &$url, $query ] );
2124  return $url;
2125  }
2126 
2143  public function getFullUrlForRedirect( $query = '', $proto = PROTO_CURRENT ) {
2144  $target = $this;
2145  if ( $this->isExternal() ) {
2146  $target = SpecialPage::getTitleFor(
2147  'GoToInterwiki',
2148  $this->getPrefixedDBkey()
2149  );
2150  }
2151  return $target->getFullURL( $query, false, $proto );
2152  }
2153 
2177  public function getLocalURL( $query = '', $query2 = false ) {
2179 
2180  $query = self::fixUrlQueryArgs( $query, $query2 );
2181 
2182  $interwiki = self::getInterwikiLookup()->fetch( $this->mInterwiki );
2183  if ( $interwiki ) {
2184  $namespace = $this->getNsText();
2185  if ( $namespace != '' ) {
2186  # Can this actually happen? Interwikis shouldn't be parsed.
2187  # Yes! It can in interwiki transclusion. But... it probably shouldn't.
2188  $namespace .= ':';
2189  }
2190  $url = $interwiki->getURL( $namespace . $this->mDbkeyform );
2191  $url = wfAppendQuery( $url, $query );
2192  } else {
2193  $dbkey = wfUrlencode( $this->getPrefixedDBkey() );
2194  if ( $query == '' ) {
2195  $url = str_replace( '$1', $dbkey, $wgArticlePath );
2196  // Avoid PHP 7.1 warning from passing $this by reference
2197  $titleRef = $this;
2198  Hooks::run( 'GetLocalURL::Article', [ &$titleRef, &$url ] );
2199  } else {
2201  $url = false;
2202  $matches = [];
2203 
2204  $articlePaths = PathRouter::getActionPaths( $wgActionPaths, $wgArticlePath );
2205 
2206  if ( $articlePaths
2207  && preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches )
2208  ) {
2209  $action = urldecode( $matches[2] );
2210  if ( isset( $articlePaths[$action] ) ) {
2211  $query = $matches[1];
2212  if ( isset( $matches[4] ) ) {
2213  $query .= $matches[4];
2214  }
2215  $url = str_replace( '$1', $dbkey, $articlePaths[$action] );
2216  if ( $query != '' ) {
2217  $url = wfAppendQuery( $url, $query );
2218  }
2219  }
2220  }
2221 
2222  if ( $url === false
2223  && $wgVariantArticlePath
2224  && preg_match( '/^variant=([^&]*)$/', $query, $matches )
2225  && $this->getPageLanguage()->equals(
2226  MediaWikiServices::getInstance()->getContentLanguage() )
2227  && $this->getPageLanguage()->hasVariants()
2228  ) {
2229  $variant = urldecode( $matches[1] );
2230  if ( $this->getPageLanguage()->hasVariant( $variant ) ) {
2231  // Only do the variant replacement if the given variant is a valid
2232  // variant for the page's language.
2233  $url = str_replace( '$2', urlencode( $variant ), $wgVariantArticlePath );
2234  $url = str_replace( '$1', $dbkey, $url );
2235  }
2236  }
2237 
2238  if ( $url === false ) {
2239  if ( $query == '-' ) {
2240  $query = '';
2241  }
2242  $url = "{$wgScript}?title={$dbkey}&{$query}";
2243  }
2244  }
2245  // Avoid PHP 7.1 warning from passing $this by reference
2246  $titleRef = $this;
2247  Hooks::run( 'GetLocalURL::Internal', [ &$titleRef, &$url, $query ] );
2248 
2249  // @todo FIXME: This causes breakage in various places when we
2250  // actually expected a local URL and end up with dupe prefixes.
2251  if ( $wgRequest->getVal( 'action' ) == 'render' ) {
2252  $url = $wgServer . $url;
2253  }
2254  }
2255 
2256  if ( $wgMainPageIsDomainRoot && $this->isMainPage() && $query === '' ) {
2257  return '/';
2258  }
2259 
2260  // Avoid PHP 7.1 warning from passing $this by reference
2261  $titleRef = $this;
2262  Hooks::run( 'GetLocalURL', [ &$titleRef, &$url, $query ] );
2263  return $url;
2264  }
2265 
2283  public function getLinkURL( $query = '', $query2 = false, $proto = false ) {
2284  if ( $this->isExternal() || $proto !== false ) {
2285  $ret = $this->getFullURL( $query, $query2, $proto );
2286  } elseif ( $this->getPrefixedText() === '' && $this->hasFragment() ) {
2287  $ret = $this->getFragmentForURL();
2288  } else {
2289  $ret = $this->getLocalURL( $query, $query2 ) . $this->getFragmentForURL();
2290  }
2291  return $ret;
2292  }
2293 
2308  public function getInternalURL( $query = '', $query2 = false ) {
2309  global $wgInternalServer, $wgServer;
2310  $query = self::fixUrlQueryArgs( $query, $query2 );
2311  $server = $wgInternalServer !== false ? $wgInternalServer : $wgServer;
2312  $url = wfExpandUrl( $server . $this->getLocalURL( $query ), PROTO_HTTP );
2313  // Avoid PHP 7.1 warning from passing $this by reference
2314  $titleRef = $this;
2315  Hooks::run( 'GetInternalURL', [ &$titleRef, &$url, $query ] );
2316  return $url;
2317  }
2318 
2332  public function getCanonicalURL( $query = '', $query2 = false ) {
2333  $query = self::fixUrlQueryArgs( $query, $query2 );
2334  $url = wfExpandUrl( $this->getLocalURL( $query ) . $this->getFragmentForURL(), PROTO_CANONICAL );
2335  // Avoid PHP 7.1 warning from passing $this by reference
2336  $titleRef = $this;
2337  Hooks::run( 'GetCanonicalURL', [ &$titleRef, &$url, $query ] );
2338  return $url;
2339  }
2340 
2346  public function getEditURL() {
2347  if ( $this->isExternal() ) {
2348  return '';
2349  }
2350  $s = $this->getLocalURL( 'action=edit' );
2351 
2352  return $s;
2353  }
2354 
2375  public function quickUserCan( $action, $user = null ) {
2376  return $this->userCan( $action, $user, false );
2377  }
2378 
2394  public function userCan( $action, $user = null, $rigor = PermissionManager::RIGOR_SECURE ) {
2395  if ( !$user instanceof User ) {
2396  global $wgUser;
2397  $user = $wgUser;
2398  }
2399 
2400  // TODO: this is for b/c, eventually will be removed
2401  if ( $rigor === true ) {
2402  $rigor = PermissionManager::RIGOR_SECURE; // b/c
2403  } elseif ( $rigor === false ) {
2404  $rigor = PermissionManager::RIGOR_QUICK; // b/c
2405  }
2406 
2407  return MediaWikiServices::getInstance()->getPermissionManager()
2408  ->userCan( $action, $user, $this, $rigor );
2409  }
2410 
2432  public function getUserPermissionsErrors(
2433  $action, $user, $rigor = PermissionManager::RIGOR_SECURE, $ignoreErrors = []
2434  ) {
2435  // TODO: this is for b/c, eventually will be removed
2436  if ( $rigor === true ) {
2437  $rigor = PermissionManager::RIGOR_SECURE; // b/c
2438  } elseif ( $rigor === false ) {
2439  $rigor = PermissionManager::RIGOR_QUICK; // b/c
2440  }
2441 
2442  return MediaWikiServices::getInstance()->getPermissionManager()
2443  ->getPermissionErrors( $action, $user, $this, $rigor, $ignoreErrors );
2444  }
2445 
2453  public static function getFilteredRestrictionTypes( $exists = true ) {
2454  global $wgRestrictionTypes;
2455  $types = $wgRestrictionTypes;
2456  if ( $exists ) {
2457  # Remove the create restriction for existing titles
2458  $types = array_diff( $types, [ 'create' ] );
2459  } else {
2460  # Only the create and upload restrictions apply to non-existing titles
2461  $types = array_intersect( $types, [ 'create', 'upload' ] );
2462  }
2463  return $types;
2464  }
2465 
2471  public function getRestrictionTypes() {
2472  if ( $this->isSpecialPage() ) {
2473  return [];
2474  }
2475 
2476  $types = self::getFilteredRestrictionTypes( $this->exists() );
2477 
2478  if ( $this->mNamespace != NS_FILE ) {
2479  # Remove the upload restriction for non-file titles
2480  $types = array_diff( $types, [ 'upload' ] );
2481  }
2482 
2483  Hooks::run( 'TitleGetRestrictionTypes', [ $this, &$types ] );
2484 
2485  wfDebug( __METHOD__ . ': applicable restrictions to [[' .
2486  $this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" );
2487 
2488  return $types;
2489  }
2490 
2498  public function getTitleProtection() {
2499  $protection = $this->getTitleProtectionInternal();
2500  if ( $protection ) {
2501  if ( $protection['permission'] == 'sysop' ) {
2502  $protection['permission'] = 'editprotected'; // B/C
2503  }
2504  if ( $protection['permission'] == 'autoconfirmed' ) {
2505  $protection['permission'] = 'editsemiprotected'; // B/C
2506  }
2507  }
2508  return $protection;
2509  }
2510 
2521  protected function getTitleProtectionInternal() {
2522  // Can't protect pages in special namespaces
2523  if ( $this->mNamespace < 0 ) {
2524  return false;
2525  }
2526 
2527  // Can't protect pages that exist.
2528  if ( $this->exists() ) {
2529  return false;
2530  }
2531 
2532  if ( $this->mTitleProtection === null ) {
2533  $dbr = wfGetDB( DB_REPLICA );
2534  $commentStore = CommentStore::getStore();
2535  $commentQuery = $commentStore->getJoin( 'pt_reason' );
2536  $res = $dbr->select(
2537  [ 'protected_titles' ] + $commentQuery['tables'],
2538  [
2539  'user' => 'pt_user',
2540  'expiry' => 'pt_expiry',
2541  'permission' => 'pt_create_perm'
2542  ] + $commentQuery['fields'],
2543  [ 'pt_namespace' => $this->mNamespace, 'pt_title' => $this->mDbkeyform ],
2544  __METHOD__,
2545  [],
2546  $commentQuery['joins']
2547  );
2548 
2549  // fetchRow returns false if there are no rows.
2550  $row = $dbr->fetchRow( $res );
2551  if ( $row ) {
2552  $this->mTitleProtection = [
2553  'user' => $row['user'],
2554  'expiry' => $dbr->decodeExpiry( $row['expiry'] ),
2555  'permission' => $row['permission'],
2556  'reason' => $commentStore->getComment( 'pt_reason', $row )->text,
2557  ];
2558  } else {
2559  $this->mTitleProtection = false;
2560  }
2561  }
2562  return $this->mTitleProtection;
2563  }
2564 
2568  public function deleteTitleProtection() {
2569  $dbw = wfGetDB( DB_MASTER );
2570 
2571  $dbw->delete(
2572  'protected_titles',
2573  [ 'pt_namespace' => $this->mNamespace, 'pt_title' => $this->mDbkeyform ],
2574  __METHOD__
2575  );
2576  $this->mTitleProtection = false;
2577  }
2578 
2586  public function isSemiProtected( $action = 'edit' ) {
2588 
2589  $restrictions = $this->getRestrictions( $action );
2591  if ( !$restrictions || !$semi ) {
2592  // Not protected, or all protection is full protection
2593  return false;
2594  }
2595 
2596  // Remap autoconfirmed to editsemiprotected for BC
2597  foreach ( array_keys( $semi, 'autoconfirmed' ) as $key ) {
2598  $semi[$key] = 'editsemiprotected';
2599  }
2600  foreach ( array_keys( $restrictions, 'autoconfirmed' ) as $key ) {
2601  $restrictions[$key] = 'editsemiprotected';
2602  }
2603 
2604  return !array_diff( $restrictions, $semi );
2605  }
2606 
2614  public function isProtected( $action = '' ) {
2615  global $wgRestrictionLevels;
2616 
2617  $restrictionTypes = $this->getRestrictionTypes();
2618 
2619  # Special pages have inherent protection
2620  if ( $this->isSpecialPage() ) {
2621  return true;
2622  }
2623 
2624  # Check regular protection levels
2625  foreach ( $restrictionTypes as $type ) {
2626  if ( $action == $type || $action == '' ) {
2627  $r = $this->getRestrictions( $type );
2628  foreach ( $wgRestrictionLevels as $level ) {
2629  if ( in_array( $level, $r ) && $level != '' ) {
2630  return true;
2631  }
2632  }
2633  }
2634  }
2635 
2636  return false;
2637  }
2638 
2647  public function isNamespaceProtected( User $user ) {
2648  global $wgNamespaceProtection;
2649 
2650  if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) {
2651  $permissionManager = MediaWikiServices::getInstance()->getPermissionManager();
2652  foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) {
2653  if ( !$permissionManager->userHasRight( $user, $right ) ) {
2654  return true;
2655  }
2656  }
2657  }
2658  return false;
2659  }
2660 
2666  public function isCascadeProtected() {
2667  list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false );
2668  return ( $sources > 0 );
2669  }
2670 
2680  public function areCascadeProtectionSourcesLoaded( $getPages = true ) {
2681  return $getPages ? $this->mCascadeSources !== null : $this->mHasCascadingRestrictions !== null;
2682  }
2683 
2697  public function getCascadeProtectionSources( $getPages = true ) {
2698  $pagerestrictions = [];
2699 
2700  if ( $this->mCascadeSources !== null && $getPages ) {
2702  } elseif ( $this->mHasCascadingRestrictions !== null && !$getPages ) {
2703  return [ $this->mHasCascadingRestrictions, $pagerestrictions ];
2704  }
2705 
2706  $dbr = wfGetDB( DB_REPLICA );
2707 
2708  if ( $this->mNamespace == NS_FILE ) {
2709  $tables = [ 'imagelinks', 'page_restrictions' ];
2710  $where_clauses = [
2711  'il_to' => $this->mDbkeyform,
2712  'il_from=pr_page',
2713  'pr_cascade' => 1
2714  ];
2715  } else {
2716  $tables = [ 'templatelinks', 'page_restrictions' ];
2717  $where_clauses = [
2718  'tl_namespace' => $this->mNamespace,
2719  'tl_title' => $this->mDbkeyform,
2720  'tl_from=pr_page',
2721  'pr_cascade' => 1
2722  ];
2723  }
2724 
2725  if ( $getPages ) {
2726  $cols = [ 'pr_page', 'page_namespace', 'page_title',
2727  'pr_expiry', 'pr_type', 'pr_level' ];
2728  $where_clauses[] = 'page_id=pr_page';
2729  $tables[] = 'page';
2730  } else {
2731  $cols = [ 'pr_expiry' ];
2732  }
2733 
2734  $res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ );
2735 
2736  $sources = $getPages ? [] : false;
2737  $now = wfTimestampNow();
2738 
2739  foreach ( $res as $row ) {
2740  $expiry = $dbr->decodeExpiry( $row->pr_expiry );
2741  if ( $expiry > $now ) {
2742  if ( $getPages ) {
2743  $page_id = $row->pr_page;
2744  $page_ns = $row->page_namespace;
2745  $page_title = $row->page_title;
2746  $sources[$page_id] = self::makeTitle( $page_ns, $page_title );
2747  # Add groups needed for each restriction type if its not already there
2748  # Make sure this restriction type still exists
2749 
2750  if ( !isset( $pagerestrictions[$row->pr_type] ) ) {
2751  $pagerestrictions[$row->pr_type] = [];
2752  }
2753 
2754  if (
2755  isset( $pagerestrictions[$row->pr_type] )
2756  && !in_array( $row->pr_level, $pagerestrictions[$row->pr_type] )
2757  ) {
2758  $pagerestrictions[$row->pr_type][] = $row->pr_level;
2759  }
2760  } else {
2761  $sources = true;
2762  }
2763  }
2764  }
2765 
2766  if ( $getPages ) {
2767  $this->mCascadeSources = $sources;
2768  $this->mCascadingRestrictions = $pagerestrictions;
2769  } else {
2770  $this->mHasCascadingRestrictions = $sources;
2771  }
2772 
2773  return [ $sources, $pagerestrictions ];
2774  }
2775 
2783  public function areRestrictionsLoaded() {
2785  }
2786 
2796  public function getRestrictions( $action ) {
2797  if ( !$this->mRestrictionsLoaded ) {
2798  $this->loadRestrictions();
2799  }
2800  return $this->mRestrictions[$action] ?? [];
2801  }
2802 
2810  public function getAllRestrictions() {
2811  if ( !$this->mRestrictionsLoaded ) {
2812  $this->loadRestrictions();
2813  }
2814  return $this->mRestrictions;
2815  }
2816 
2824  public function getRestrictionExpiry( $action ) {
2825  if ( !$this->mRestrictionsLoaded ) {
2826  $this->loadRestrictions();
2827  }
2828  return $this->mRestrictionsExpiry[$action] ?? false;
2829  }
2830 
2837  if ( !$this->mRestrictionsLoaded ) {
2838  $this->loadRestrictions();
2839  }
2840 
2842  }
2843 
2855  public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) {
2856  // This function will only read rows from a table that we migrated away
2857  // from before adding READ_LATEST support to loadRestrictions, so we
2858  // don't need to support reading from DB_MASTER here.
2859  $dbr = wfGetDB( DB_REPLICA );
2860 
2861  $restrictionTypes = $this->getRestrictionTypes();
2862 
2863  foreach ( $restrictionTypes as $type ) {
2864  $this->mRestrictions[$type] = [];
2865  $this->mRestrictionsExpiry[$type] = 'infinity';
2866  }
2867 
2868  $this->mCascadeRestriction = false;
2869 
2870  # Backwards-compatibility: also load the restrictions from the page record (old format).
2871  if ( $oldFashionedRestrictions !== null ) {
2872  $this->mOldRestrictions = $oldFashionedRestrictions;
2873  }
2874 
2875  if ( $this->mOldRestrictions === false ) {
2876  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
2877  $linkCache->addLinkObj( $this ); # in case we already had an article ID
2878  $this->mOldRestrictions = $linkCache->getGoodLinkFieldObj( $this, 'restrictions' );
2879  }
2880 
2881  if ( $this->mOldRestrictions != '' ) {
2882  foreach ( explode( ':', trim( $this->mOldRestrictions ) ) as $restrict ) {
2883  $temp = explode( '=', trim( $restrict ) );
2884  if ( count( $temp ) == 1 ) {
2885  // old old format should be treated as edit/move restriction
2886  $this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) );
2887  $this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) );
2888  } else {
2889  $restriction = trim( $temp[1] );
2890  if ( $restriction != '' ) { // some old entries are empty
2891  $this->mRestrictions[$temp[0]] = explode( ',', $restriction );
2892  }
2893  }
2894  }
2895  }
2896 
2897  if ( count( $rows ) ) {
2898  # Current system - load second to make them override.
2899  $now = wfTimestampNow();
2900 
2901  # Cycle through all the restrictions.
2902  foreach ( $rows as $row ) {
2903  // Don't take care of restrictions types that aren't allowed
2904  if ( !in_array( $row->pr_type, $restrictionTypes ) ) {
2905  continue;
2906  }
2907 
2908  $expiry = $dbr->decodeExpiry( $row->pr_expiry );
2909 
2910  // Only apply the restrictions if they haven't expired!
2911  if ( !$expiry || $expiry > $now ) {
2912  $this->mRestrictionsExpiry[$row->pr_type] = $expiry;
2913  $this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) );
2914 
2915  $this->mCascadeRestriction |= $row->pr_cascade;
2916  }
2917  }
2918  }
2919 
2920  $this->mRestrictionsLoaded = true;
2921  }
2922 
2933  public function loadRestrictions( $oldFashionedRestrictions = null, $flags = 0 ) {
2934  $readLatest = DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST );
2935  if ( $this->mRestrictionsLoaded && !$readLatest ) {
2936  return;
2937  }
2938 
2939  $id = $this->getArticleID( $flags );
2940  if ( $id ) {
2941  $fname = __METHOD__;
2942  $loadRestrictionsFromDb = function ( IDatabase $dbr ) use ( $fname, $id ) {
2943  return iterator_to_array(
2944  $dbr->select(
2945  'page_restrictions',
2946  [ 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ],
2947  [ 'pr_page' => $id ],
2948  $fname
2949  )
2950  );
2951  };
2952 
2953  if ( $readLatest ) {
2954  $dbr = wfGetDB( DB_MASTER );
2955  $rows = $loadRestrictionsFromDb( $dbr );
2956  } else {
2957  $cache = MediaWikiServices::getInstance()->getMainWANObjectCache();
2958  $rows = $cache->getWithSetCallback(
2959  // Page protections always leave a new null revision
2960  $cache->makeKey( 'page-restrictions', 'v1', $id, $this->getLatestRevID() ),
2961  $cache::TTL_DAY,
2962  function ( $curValue, &$ttl, array &$setOpts ) use ( $loadRestrictionsFromDb ) {
2963  $dbr = wfGetDB( DB_REPLICA );
2964 
2965  $setOpts += Database::getCacheSetOptions( $dbr );
2966  $lb = MediaWikiServices::getInstance()->getDBLoadBalancer();
2967  if ( $lb->hasOrMadeRecentMasterChanges() ) {
2968  // @TODO: cleanup Title cache and caller assumption mess in general
2969  $ttl = WANObjectCache::TTL_UNCACHEABLE;
2970  }
2971 
2972  return $loadRestrictionsFromDb( $dbr );
2973  }
2974  );
2975  }
2976 
2977  $this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions );
2978  } else {
2979  $title_protection = $this->getTitleProtectionInternal();
2980 
2981  if ( $title_protection ) {
2982  $now = wfTimestampNow();
2983  $expiry = wfGetDB( DB_REPLICA )->decodeExpiry( $title_protection['expiry'] );
2984 
2985  if ( !$expiry || $expiry > $now ) {
2986  // Apply the restrictions
2987  $this->mRestrictionsExpiry['create'] = $expiry;
2988  $this->mRestrictions['create'] =
2989  explode( ',', trim( $title_protection['permission'] ) );
2990  } else { // Get rid of the old restrictions
2991  $this->mTitleProtection = false;
2992  }
2993  } else {
2994  $this->mRestrictionsExpiry['create'] = 'infinity';
2995  }
2996  $this->mRestrictionsLoaded = true;
2997  }
2998  }
2999 
3004  public function flushRestrictions() {
3005  $this->mRestrictionsLoaded = false;
3006  $this->mTitleProtection = null;
3007  }
3008 
3014  static function purgeExpiredRestrictions() {
3015  if ( wfReadOnly() ) {
3016  return;
3017  }
3018 
3020  wfGetDB( DB_MASTER ),
3021  __METHOD__,
3022  function ( IDatabase $dbw, $fname ) {
3023  $config = MediaWikiServices::getInstance()->getMainConfig();
3024  $ids = $dbw->selectFieldValues(
3025  'page_restrictions',
3026  'pr_id',
3027  [ 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
3028  $fname,
3029  [ 'LIMIT' => $config->get( 'UpdateRowsPerQuery' ) ] // T135470
3030  );
3031  if ( $ids ) {
3032  $dbw->delete( 'page_restrictions', [ 'pr_id' => $ids ], $fname );
3033  }
3034  }
3035  ) );
3036 
3038  wfGetDB( DB_MASTER ),
3039  __METHOD__,
3040  function ( IDatabase $dbw, $fname ) {
3041  $dbw->delete(
3042  'protected_titles',
3043  [ 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
3044  $fname
3045  );
3046  }
3047  ) );
3048  }
3049 
3055  public function hasSubpages() {
3056  if (
3057  !MediaWikiServices::getInstance()->getNamespaceInfo()->
3058  hasSubpages( $this->mNamespace )
3059  ) {
3060  # Duh
3061  return false;
3062  }
3063 
3064  # We dynamically add a member variable for the purpose of this method
3065  # alone to cache the result. There's no point in having it hanging
3066  # around uninitialized in every Title object; therefore we only add it
3067  # if needed and don't declare it statically.
3068  if ( $this->mHasSubpages === null ) {
3069  $this->mHasSubpages = false;
3070  $subpages = $this->getSubpages( 1 );
3071  if ( $subpages instanceof TitleArray ) {
3072  $this->mHasSubpages = (bool)$subpages->current();
3073  }
3074  }
3075 
3076  return $this->mHasSubpages;
3077  }
3078 
3086  public function getSubpages( $limit = -1 ) {
3087  if (
3088  !MediaWikiServices::getInstance()->getNamespaceInfo()->
3089  hasSubpages( $this->mNamespace )
3090  ) {
3091  return [];
3092  }
3093 
3094  $dbr = wfGetDB( DB_REPLICA );
3095  $conds = [ 'page_namespace' => $this->mNamespace ];
3096  $conds[] = 'page_title ' . $dbr->buildLike( $this->mDbkeyform . '/', $dbr->anyString() );
3097  $options = [];
3098  if ( $limit > -1 ) {
3099  $options['LIMIT'] = $limit;
3100  }
3102  $dbr->select( 'page',
3103  [ 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ],
3104  $conds,
3105  __METHOD__,
3106  $options
3107  )
3108  );
3109  }
3110 
3116  public function isDeleted() {
3117  if ( $this->mNamespace < 0 ) {
3118  $n = 0;
3119  } else {
3120  $dbr = wfGetDB( DB_REPLICA );
3121 
3122  $n = $dbr->selectField( 'archive', 'COUNT(*)',
3123  [ 'ar_namespace' => $this->mNamespace, 'ar_title' => $this->mDbkeyform ],
3124  __METHOD__
3125  );
3126  if ( $this->mNamespace == NS_FILE ) {
3127  $n += $dbr->selectField( 'filearchive', 'COUNT(*)',
3128  [ 'fa_name' => $this->mDbkeyform ],
3129  __METHOD__
3130  );
3131  }
3132  }
3133  return (int)$n;
3134  }
3135 
3141  public function isDeletedQuick() {
3142  if ( $this->mNamespace < 0 ) {
3143  return false;
3144  }
3145  $dbr = wfGetDB( DB_REPLICA );
3146  $deleted = (bool)$dbr->selectField( 'archive', '1',
3147  [ 'ar_namespace' => $this->mNamespace, 'ar_title' => $this->mDbkeyform ],
3148  __METHOD__
3149  );
3150  if ( !$deleted && $this->mNamespace == NS_FILE ) {
3151  $deleted = (bool)$dbr->selectField( 'filearchive', '1',
3152  [ 'fa_name' => $this->mDbkeyform ],
3153  __METHOD__
3154  );
3155  }
3156  return $deleted;
3157  }
3158 
3166  public function getArticleID( $flags = 0 ) {
3167  if ( $this->mNamespace < 0 ) {
3168  $this->mArticleID = 0;
3169 
3170  return $this->mArticleID;
3171  }
3172 
3173  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3174  if ( $flags & self::GAID_FOR_UPDATE ) {
3175  $oldUpdate = $linkCache->forUpdate( true );
3176  $linkCache->clearLink( $this );
3177  $this->mArticleID = $linkCache->addLinkObj( $this );
3178  $linkCache->forUpdate( $oldUpdate );
3179  } elseif ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) {
3180  $this->mArticleID = (int)$this->loadFieldFromDB( 'page_id', $flags );
3181  } elseif ( $this->mArticleID == -1 ) {
3182  $this->mArticleID = $linkCache->addLinkObj( $this );
3183  }
3184 
3185  return $this->mArticleID;
3186  }
3187 
3195  public function isRedirect( $flags = 0 ) {
3196  if ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) {
3197  $this->mRedirect = (bool)$this->loadFieldFromDB( 'page_is_redirect', $flags );
3198  } elseif ( $this->mRedirect === null ) {
3199  if ( $this->getArticleID( $flags ) ) {
3200  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3201  $linkCache->addLinkObj( $this ); // in case we already had an article ID
3202  // Note that LinkCache returns null if it thinks the page does not exist;
3203  // always trust the state of LinkCache over that of this Title instance.
3204  $this->mRedirect = (bool)$linkCache->getGoodLinkFieldObj( $this, 'redirect' );
3205  } else {
3206  $this->mRedirect = false;
3207  }
3208  }
3209 
3210  return $this->mRedirect;
3211  }
3212 
3220  public function getLength( $flags = 0 ) {
3221  if ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) {
3222  $this->mLength = (int)$this->loadFieldFromDB( 'page_len', $flags );
3223  } else {
3224  if ( $this->mLength != -1 ) {
3225  return $this->mLength;
3226  } elseif ( !$this->getArticleID( $flags ) ) {
3227  $this->mLength = 0;
3228  return $this->mLength;
3229  }
3230 
3231  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3232  $linkCache->addLinkObj( $this ); // in case we already had an article ID
3233  // Note that LinkCache returns null if it thinks the page does not exist;
3234  // always trust the state of LinkCache over that of this Title instance.
3235  $this->mLength = (int)$linkCache->getGoodLinkFieldObj( $this, 'length' );
3236  }
3237 
3238  return $this->mLength;
3239  }
3240 
3247  public function getLatestRevID( $flags = 0 ) {
3248  if ( DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST ) ) {
3249  $this->mLatestID = (int)$this->loadFieldFromDB( 'page_latest', $flags );
3250  } else {
3251  if ( $this->mLatestID !== false ) {
3252  return (int)$this->mLatestID;
3253  } elseif ( !$this->getArticleID( $flags ) ) {
3254  $this->mLatestID = 0;
3255 
3256  return $this->mLatestID;
3257  }
3258 
3259  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3260  $linkCache->addLinkObj( $this ); // in case we already had an article ID
3261  // Note that LinkCache returns null if it thinks the page does not exist;
3262  // always trust the state of LinkCache over that of this Title instance.
3263  $this->mLatestID = (int)$linkCache->getGoodLinkFieldObj( $this, 'revision' );
3264  }
3265 
3266  return $this->mLatestID;
3267  }
3268 
3279  public function resetArticleID( $id ) {
3280  if ( $id === false ) {
3281  $this->mArticleID = -1;
3282  } else {
3283  $this->mArticleID = (int)$id;
3284  }
3285  $this->mRestrictionsLoaded = false;
3286  $this->mRestrictions = [];
3287  $this->mOldRestrictions = false;
3288  $this->mRedirect = null;
3289  $this->mLength = -1;
3290  $this->mLatestID = false;
3291  $this->mContentModel = false;
3292  $this->mForcedContentModel = false;
3293  $this->mEstimateRevisions = null;
3294  $this->mPageLanguage = null;
3295  $this->mDbPageLanguage = false;
3296  $this->mIsBigDeletion = null;
3297 
3298  MediaWikiServices::getInstance()->getLinkCache()->clearLink( $this );
3299  }
3300 
3301  public static function clearCaches() {
3302  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3303  $linkCache->clear();
3304 
3305  $titleCache = self::getTitleCache();
3306  $titleCache->clear();
3307  }
3308 
3316  public static function capitalize( $text, $ns = NS_MAIN ) {
3317  $services = MediaWikiServices::getInstance();
3318  if ( $services->getNamespaceInfo()->isCapitalized( $ns ) ) {
3319  return $services->getContentLanguage()->ucfirst( $text );
3320  } else {
3321  return $text;
3322  }
3323  }
3324 
3341  private function secureAndSplit( $text, $defaultNamespace = null ) {
3342  if ( $defaultNamespace === null ) {
3343  $defaultNamespace = $this->mDefaultNamespace;
3344  }
3345 
3346  // @note: splitTitleString() is a temporary hack to allow MediaWikiTitleCodec to share
3347  // the parsing code with Title, while avoiding massive refactoring.
3348  // @todo: get rid of secureAndSplit, refactor parsing code.
3349  // @note: getTitleParser() returns a TitleParser implementation which does not have a
3350  // splitTitleString method, but the only implementation (MediaWikiTitleCodec) does
3352  $titleCodec = MediaWikiServices::getInstance()->getTitleParser();
3353  '@phan-var MediaWikiTitleCodec $titleCodec';
3354  // MalformedTitleException can be thrown here
3355  $parts = $titleCodec->splitTitleString( $text, $defaultNamespace );
3356 
3357  # Fill fields
3358  $this->setFragment( '#' . $parts['fragment'] );
3359  $this->mInterwiki = $parts['interwiki'];
3360  $this->mLocalInterwiki = $parts['local_interwiki'];
3361  $this->mNamespace = $parts['namespace'];
3362  $this->mDefaultNamespace = $defaultNamespace;
3363  $this->mUserCaseDBKey = $parts['user_case_dbkey'];
3364 
3365  $this->mDbkeyform = $parts['dbkey'];
3366  $this->mUrlform = wfUrlencode( $this->mDbkeyform );
3367  $this->mTextform = strtr( $this->mDbkeyform, '_', ' ' );
3368 
3369  // splitTitleString() guarantees that this title is valid.
3370  $this->mIsValid = true;
3371 
3372  # We already know that some pages won't be in the database!
3373  if ( $this->isExternal() || $this->isSpecialPage() || $this->mTextform === '' ) {
3374  $this->mArticleID = 0;
3375  }
3376  }
3377 
3390  public function getLinksTo( $options = [], $table = 'pagelinks', $prefix = 'pl' ) {
3391  if ( count( $options ) > 0 ) {
3392  $db = wfGetDB( DB_MASTER );
3393  } else {
3394  $db = wfGetDB( DB_REPLICA );
3395  }
3396 
3397  $res = $db->select(
3398  [ 'page', $table ],
3399  self::getSelectFields(),
3400  [
3401  "{$prefix}_from=page_id",
3402  "{$prefix}_namespace" => $this->mNamespace,
3403  "{$prefix}_title" => $this->mDbkeyform ],
3404  __METHOD__,
3405  $options
3406  );
3407 
3408  $retVal = [];
3409  if ( $res->numRows() ) {
3410  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3411  foreach ( $res as $row ) {
3412  $titleObj = self::makeTitle( $row->page_namespace, $row->page_title );
3413  if ( $titleObj ) {
3414  $linkCache->addGoodLinkObjFromRow( $titleObj, $row );
3415  $retVal[] = $titleObj;
3416  }
3417  }
3418  }
3419  return $retVal;
3420  }
3421 
3432  public function getTemplateLinksTo( $options = [] ) {
3433  return $this->getLinksTo( $options, 'templatelinks', 'tl' );
3434  }
3435 
3448  public function getLinksFrom( $options = [], $table = 'pagelinks', $prefix = 'pl' ) {
3449  $id = $this->getArticleID();
3450 
3451  # If the page doesn't exist; there can't be any link from this page
3452  if ( !$id ) {
3453  return [];
3454  }
3455 
3456  $db = wfGetDB( DB_REPLICA );
3457 
3458  $blNamespace = "{$prefix}_namespace";
3459  $blTitle = "{$prefix}_title";
3460 
3461  $pageQuery = WikiPage::getQueryInfo();
3462  $res = $db->select(
3463  [ $table, 'nestpage' => $pageQuery['tables'] ],
3464  array_merge(
3465  [ $blNamespace, $blTitle ],
3466  $pageQuery['fields']
3467  ),
3468  [ "{$prefix}_from" => $id ],
3469  __METHOD__,
3470  $options,
3471  [ 'nestpage' => [
3472  'LEFT JOIN',
3473  [ "page_namespace=$blNamespace", "page_title=$blTitle" ]
3474  ] ] + $pageQuery['joins']
3475  );
3476 
3477  $retVal = [];
3478  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3479  foreach ( $res as $row ) {
3480  if ( $row->page_id ) {
3481  $titleObj = self::newFromRow( $row );
3482  } else {
3483  $titleObj = self::makeTitle( $row->$blNamespace, $row->$blTitle );
3484  $linkCache->addBadLinkObj( $titleObj );
3485  }
3486  $retVal[] = $titleObj;
3487  }
3488 
3489  return $retVal;
3490  }
3491 
3502  public function getTemplateLinksFrom( $options = [] ) {
3503  return $this->getLinksFrom( $options, 'templatelinks', 'tl' );
3504  }
3505 
3514  public function getBrokenLinksFrom() {
3515  if ( $this->getArticleID() == 0 ) {
3516  # All links from article ID 0 are false positives
3517  return [];
3518  }
3519 
3520  $dbr = wfGetDB( DB_REPLICA );
3521  $res = $dbr->select(
3522  [ 'page', 'pagelinks' ],
3523  [ 'pl_namespace', 'pl_title' ],
3524  [
3525  'pl_from' => $this->getArticleID(),
3526  'page_namespace IS NULL'
3527  ],
3528  __METHOD__, [],
3529  [
3530  'page' => [
3531  'LEFT JOIN',
3532  [ 'pl_namespace=page_namespace', 'pl_title=page_title' ]
3533  ]
3534  ]
3535  );
3536 
3537  $retVal = [];
3538  foreach ( $res as $row ) {
3539  $retVal[] = self::makeTitle( $row->pl_namespace, $row->pl_title );
3540  }
3541  return $retVal;
3542  }
3543 
3550  public function getCdnUrls() {
3551  $urls = [
3552  $this->getInternalURL(),
3553  $this->getInternalURL( 'action=history' )
3554  ];
3555 
3556  $pageLang = $this->getPageLanguage();
3557  if ( $pageLang->hasVariants() ) {
3558  $variants = $pageLang->getVariants();
3559  foreach ( $variants as $vCode ) {
3560  $urls[] = $this->getInternalURL( $vCode );
3561  }
3562  }
3563 
3564  // If we are looking at a css/js user subpage, purge the action=raw.
3565  if ( $this->isUserJsConfigPage() ) {
3566  $urls[] = $this->getInternalURL( 'action=raw&ctype=text/javascript' );
3567  } elseif ( $this->isUserJsonConfigPage() ) {
3568  $urls[] = $this->getInternalURL( 'action=raw&ctype=application/json' );
3569  } elseif ( $this->isUserCssConfigPage() ) {
3570  $urls[] = $this->getInternalURL( 'action=raw&ctype=text/css' );
3571  }
3572 
3573  Hooks::run( 'TitleSquidURLs', [ $this, &$urls ] );
3574  return $urls;
3575  }
3576 
3580  public function purgeSquid() {
3582  new CdnCacheUpdate( $this->getCdnUrls() ),
3584  );
3585  }
3586 
3597  public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) {
3598  wfDeprecated( __METHOD__, '1.25' );
3599 
3600  global $wgUser;
3601 
3602  if ( !( $nt instanceof Title ) ) {
3603  // Normally we'd add this to $errors, but we'll get
3604  // lots of syntax errors if $nt is not an object
3605  return [ [ 'badtitletext' ] ];
3606  }
3607 
3608  $mp = MediaWikiServices::getInstance()->getMovePageFactory()->newMovePage( $this, $nt );
3609  $errors = $mp->isValidMove()->getErrorsArray();
3610  if ( $auth ) {
3611  $errors = wfMergeErrorArrays(
3612  $errors,
3613  $mp->checkPermissions( $wgUser, $reason )->getErrorsArray()
3614  );
3615  }
3616 
3617  return $errors ?: true;
3618  }
3619 
3633  public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true,
3634  array $changeTags = []
3635  ) {
3636  wfDeprecated( __METHOD__, '1.25' );
3637 
3638  global $wgUser;
3639 
3640  $mp = MediaWikiServices::getInstance()->getMovePageFactory()->newMovePage( $this, $nt );
3641  $method = $auth ? 'moveIfAllowed' : 'move';
3643  $status = $mp->$method( $wgUser, $reason, $createRedirect, $changeTags );
3644  if ( $status->isOK() ) {
3645  return true;
3646  } else {
3647  return $status->getErrorsArray();
3648  }
3649  }
3650 
3666  public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true,
3667  array $changeTags = []
3668  ) {
3669  wfDeprecated( __METHOD__, '1.34' );
3670 
3671  global $wgUser;
3672 
3673  $mp = new MovePage( $this, $nt );
3674  $method = $auth ? 'moveSubpagesIfAllowed' : 'moveSubpages';
3676  $result = $mp->$method( $wgUser, $reason, $createRedirect, $changeTags );
3677 
3678  if ( !$result->isOK() ) {
3679  return $result->getErrorsArray();
3680  }
3681 
3682  $retval = [];
3683  foreach ( $result->getValue() as $key => $status ) {
3685  if ( $status->isOK() ) {
3686  $retval[$key] = $status->getValue();
3687  } else {
3688  $retval[$key] = $status->getErrorsArray();
3689  }
3690  }
3691  return $retval;
3692  }
3693 
3701  public function isSingleRevRedirect() {
3702  global $wgContentHandlerUseDB;
3703 
3704  $dbw = wfGetDB( DB_MASTER );
3705 
3706  # Is it a redirect?
3707  $fields = [ 'page_is_redirect', 'page_latest', 'page_id' ];
3708  if ( $wgContentHandlerUseDB ) {
3709  $fields[] = 'page_content_model';
3710  }
3711 
3712  $row = $dbw->selectRow( 'page',
3713  $fields,
3714  $this->pageCond(),
3715  __METHOD__,
3716  [ 'FOR UPDATE' ]
3717  );
3718  # Cache some fields we may want
3719  $this->mArticleID = $row ? intval( $row->page_id ) : 0;
3720  $this->mRedirect = $row ? (bool)$row->page_is_redirect : false;
3721  $this->mLatestID = $row ? intval( $row->page_latest ) : false;
3722  $this->mContentModel = $row && isset( $row->page_content_model )
3723  ? strval( $row->page_content_model )
3724  : false;
3725 
3726  if ( !$this->mRedirect ) {
3727  return false;
3728  }
3729  # Does the article have a history?
3730  $row = $dbw->selectField( [ 'page', 'revision' ],
3731  'rev_id',
3732  [ 'page_namespace' => $this->mNamespace,
3733  'page_title' => $this->mDbkeyform,
3734  'page_id=rev_page',
3735  'page_latest != rev_id'
3736  ],
3737  __METHOD__,
3738  [ 'FOR UPDATE' ]
3739  );
3740  # Return true if there was no history
3741  return ( $row === false );
3742  }
3743 
3752  public function isValidMoveTarget( $nt ) {
3753  wfDeprecated( __METHOD__, '1.25' );
3754 
3755  # Is it an existing file?
3756  if ( $nt->getNamespace() == NS_FILE ) {
3757  $file = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()
3758  ->newFile( $nt );
3759  $file->load( File::READ_LATEST );
3760  if ( $file->exists() ) {
3761  wfDebug( __METHOD__ . ": file exists\n" );
3762  return false;
3763  }
3764  }
3765  # Is it a redirect with no history?
3766  if ( !$nt->isSingleRevRedirect() ) {
3767  wfDebug( __METHOD__ . ": not a one-rev redirect\n" );
3768  return false;
3769  }
3770  # Get the article text
3771  $rev = Revision::newFromTitle( $nt, false, Revision::READ_LATEST );
3772  if ( !is_object( $rev ) ) {
3773  return false;
3774  }
3775  $content = $rev->getContent();
3776  # Does the redirect point to the source?
3777  # Or is it a broken self-redirect, usually caused by namespace collisions?
3778  $redirTitle = $content ? $content->getRedirectTarget() : null;
3779 
3780  if ( $redirTitle ) {
3781  if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
3782  $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) {
3783  wfDebug( __METHOD__ . ": redirect points to other page\n" );
3784  return false;
3785  } else {
3786  return true;
3787  }
3788  } else {
3789  # Fail safe (not a redirect after all. strange.)
3790  wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() .
3791  " is a redirect, but it doesn't contain a valid redirect.\n" );
3792  return false;
3793  }
3794  }
3795 
3803  public function getParentCategories() {
3804  $data = [];
3805 
3806  $titleKey = $this->getArticleID();
3807 
3808  if ( $titleKey === 0 ) {
3809  return $data;
3810  }
3811 
3812  $dbr = wfGetDB( DB_REPLICA );
3813 
3814  $res = $dbr->select(
3815  'categorylinks',
3816  'cl_to',
3817  [ 'cl_from' => $titleKey ],
3818  __METHOD__
3819  );
3820 
3821  if ( $res->numRows() > 0 ) {
3822  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
3823  foreach ( $res as $row ) {
3824  // $data[] = Title::newFromText( $contLang->getNsText ( NS_CATEGORY ).':'.$row->cl_to);
3825  $data[$contLang->getNsText( NS_CATEGORY ) . ':' . $row->cl_to] =
3826  $this->getFullText();
3827  }
3828  }
3829  return $data;
3830  }
3831 
3838  public function getParentCategoryTree( $children = [] ) {
3839  $stack = [];
3840  $parents = $this->getParentCategories();
3841 
3842  if ( $parents ) {
3843  foreach ( $parents as $parent => $current ) {
3844  if ( array_key_exists( $parent, $children ) ) {
3845  # Circular reference
3846  $stack[$parent] = [];
3847  } else {
3848  $nt = self::newFromText( $parent );
3849  if ( $nt ) {
3850  $stack[$parent] = $nt->getParentCategoryTree( $children + [ $parent => 1 ] );
3851  }
3852  }
3853  }
3854  }
3855 
3856  return $stack;
3857  }
3858 
3865  public function pageCond() {
3866  if ( $this->mArticleID > 0 ) {
3867  // PK avoids secondary lookups in InnoDB, shouldn't hurt other DBs
3868  return [ 'page_id' => $this->mArticleID ];
3869  } else {
3870  return [ 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform ];
3871  }
3872  }
3873 
3881  private function getRelativeRevisionID( $revId, $flags, $dir ) {
3882  $rl = MediaWikiServices::getInstance()->getRevisionLookup();
3883  $rev = $rl->getRevisionById( $revId, $flags );
3884  if ( !$rev ) {
3885  return false;
3886  }
3887 
3888  $oldRev = ( $dir === 'next' )
3889  ? $rl->getNextRevision( $rev, $flags )
3890  : $rl->getPreviousRevision( $rev, $flags );
3891 
3892  return $oldRev ? $oldRev->getId() : false;
3893  }
3894 
3903  public function getPreviousRevisionID( $revId, $flags = 0 ) {
3904  return $this->getRelativeRevisionID( $revId, $flags, 'prev' );
3905  }
3906 
3915  public function getNextRevisionID( $revId, $flags = 0 ) {
3916  return $this->getRelativeRevisionID( $revId, $flags, 'next' );
3917  }
3918 
3925  public function getFirstRevision( $flags = 0 ) {
3926  $pageId = $this->getArticleID( $flags );
3927  if ( $pageId ) {
3928  $flags |= ( $flags & self::GAID_FOR_UPDATE ) ? self::READ_LATEST : 0; // b/c
3929  list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
3931  $row = wfGetDB( $index )->selectRow(
3932  $revQuery['tables'], $revQuery['fields'],
3933  [ 'rev_page' => $pageId ],
3934  __METHOD__,
3935  array_merge(
3936  [
3937  'ORDER BY' => 'rev_timestamp ASC, rev_id ASC',
3938  'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319
3939  ],
3940  $options
3941  ),
3942  $revQuery['joins']
3943  );
3944  if ( $row ) {
3945  return new Revision( $row, 0, $this );
3946  }
3947  }
3948  return null;
3949  }
3950 
3957  public function getEarliestRevTime( $flags = 0 ) {
3958  $rev = $this->getFirstRevision( $flags );
3959  return $rev ? $rev->getTimestamp() : null;
3960  }
3961 
3967  public function isNewPage() {
3968  $dbr = wfGetDB( DB_REPLICA );
3969  return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ );
3970  }
3971 
3977  public function isBigDeletion() {
3978  global $wgDeleteRevisionsLimit;
3979 
3980  if ( !$wgDeleteRevisionsLimit ) {
3981  return false;
3982  }
3983 
3984  if ( $this->mIsBigDeletion === null ) {
3985  $dbr = wfGetDB( DB_REPLICA );
3986 
3987  $revCount = $dbr->selectRowCount(
3988  'revision',
3989  '1',
3990  [ 'rev_page' => $this->getArticleID() ],
3991  __METHOD__,
3992  [ 'LIMIT' => $wgDeleteRevisionsLimit + 1 ]
3993  );
3994 
3995  $this->mIsBigDeletion = $revCount > $wgDeleteRevisionsLimit;
3996  }
3997 
3998  return $this->mIsBigDeletion;
3999  }
4000 
4006  public function estimateRevisionCount() {
4007  if ( !$this->exists() ) {
4008  return 0;
4009  }
4010 
4011  if ( $this->mEstimateRevisions === null ) {
4012  $dbr = wfGetDB( DB_REPLICA );
4013  $this->mEstimateRevisions = $dbr->estimateRowCount( 'revision', '*',
4014  [ 'rev_page' => $this->getArticleID() ], __METHOD__ );
4015  }
4016 
4018  }
4019 
4031  public function countRevisionsBetween( $old, $new, $max = null ) {
4032  if ( !( $old instanceof Revision ) ) {
4033  $old = Revision::newFromTitle( $this, (int)$old );
4034  }
4035  if ( !( $new instanceof Revision ) ) {
4036  $new = Revision::newFromTitle( $this, (int)$new );
4037  }
4038  if ( !$old || !$new ) {
4039  return 0; // nothing to compare
4040  }
4041  return MediaWikiServices::getInstance()
4042  ->getRevisionStore()
4043  ->countRevisionsBetween( $old->getRevisionRecord(), $new->getRevisionRecord(), $max );
4044  }
4045 
4062  public function getAuthorsBetween( $old, $new, $limit, $options = [] ) {
4063  if ( !( $old instanceof Revision ) ) {
4064  $old = Revision::newFromTitle( $this, (int)$old );
4065  }
4066  if ( !( $new instanceof Revision ) ) {
4067  $new = Revision::newFromTitle( $this, (int)$new );
4068  }
4069  // XXX: what if Revision objects are passed in, but they don't refer to this title?
4070  // Add $old->getPage() != $new->getPage() || $old->getPage() != $this->getArticleID()
4071  // in the sanity check below?
4072  if ( !$old || !$new ) {
4073  return null; // nothing to compare
4074  }
4075  $authors = [];
4076  $old_cmp = '>';
4077  $new_cmp = '<';
4078  $options = (array)$options;
4079  if ( in_array( 'include_old', $options ) ) {
4080  $old_cmp = '>=';
4081  }
4082  if ( in_array( 'include_new', $options ) ) {
4083  $new_cmp = '<=';
4084  }
4085  if ( in_array( 'include_both', $options ) ) {
4086  $old_cmp = '>=';
4087  $new_cmp = '<=';
4088  }
4089  // No DB query needed if $old and $new are the same or successive revisions:
4090  if ( $old->getId() === $new->getId() ) {
4091  return ( $old_cmp === '>' && $new_cmp === '<' ) ?
4092  [] :
4093  [ $old->getUserText( RevisionRecord::RAW ) ];
4094  } elseif ( $old->getId() === $new->getParentId() ) {
4095  if ( $old_cmp === '>=' && $new_cmp === '<=' ) {
4096  $authors[] = $oldUserText = $old->getUserText( RevisionRecord::RAW );
4097  $newUserText = $new->getUserText( RevisionRecord::RAW );
4098  if ( $oldUserText != $newUserText ) {
4099  $authors[] = $newUserText;
4100  }
4101  } elseif ( $old_cmp === '>=' ) {
4102  $authors[] = $old->getUserText( RevisionRecord::RAW );
4103  } elseif ( $new_cmp === '<=' ) {
4104  $authors[] = $new->getUserText( RevisionRecord::RAW );
4105  }
4106  return $authors;
4107  }
4108  $dbr = wfGetDB( DB_REPLICA );
4110  $authors = $dbr->selectFieldValues(
4111  $revQuery['tables'],
4112  $revQuery['fields']['rev_user_text'],
4113  [
4114  'rev_page' => $this->getArticleID(),
4115  "rev_timestamp $old_cmp " . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
4116  "rev_timestamp $new_cmp " . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
4117  ], __METHOD__,
4118  [ 'DISTINCT', 'LIMIT' => $limit + 1 ], // add one so caller knows it was truncated
4119  $revQuery['joins']
4120  );
4121  return $authors;
4122  }
4123 
4138  public function countAuthorsBetween( $old, $new, $limit, $options = [] ) {
4139  $authors = $this->getAuthorsBetween( $old, $new, $limit, $options );
4140  return $authors ? count( $authors ) : 0;
4141  }
4142 
4149  public function equals( LinkTarget $title ) {
4150  // Note: === is necessary for proper matching of number-like titles.
4151  return $this->mInterwiki === $title->getInterwiki()
4152  && $this->mNamespace == $title->getNamespace()
4153  && $this->mDbkeyform === $title->getDBkey();
4154  }
4155 
4162  public function isSubpageOf( Title $title ) {
4163  return $this->mInterwiki === $title->mInterwiki
4164  && $this->mNamespace == $title->mNamespace
4165  && strpos( $this->mDbkeyform, $title->mDbkeyform . '/' ) === 0;
4166  }
4167 
4178  public function exists( $flags = 0 ) {
4179  $exists = $this->getArticleID( $flags ) != 0;
4180  Hooks::run( 'TitleExists', [ $this, &$exists ] );
4181  return $exists;
4182  }
4183 
4200  public function isAlwaysKnown() {
4201  $isKnown = null;
4202 
4213  Hooks::run( 'TitleIsAlwaysKnown', [ $this, &$isKnown ] );
4214 
4215  if ( !is_null( $isKnown ) ) {
4216  return $isKnown;
4217  }
4218 
4219  if ( $this->isExternal() ) {
4220  return true; // any interwiki link might be viewable, for all we know
4221  }
4222 
4223  $services = MediaWikiServices::getInstance();
4224  switch ( $this->mNamespace ) {
4225  case NS_MEDIA:
4226  case NS_FILE:
4227  // file exists, possibly in a foreign repo
4228  return (bool)$services->getRepoGroup()->findFile( $this );
4229  case NS_SPECIAL:
4230  // valid special page
4231  return $services->getSpecialPageFactory()->exists( $this->mDbkeyform );
4232  case NS_MAIN:
4233  // selflink, possibly with fragment
4234  return $this->mDbkeyform == '';
4235  case NS_MEDIAWIKI:
4236  // known system message
4237  return $this->hasSourceText() !== false;
4238  default:
4239  return false;
4240  }
4241  }
4242 
4254  public function isKnown() {
4255  return $this->isAlwaysKnown() || $this->exists();
4256  }
4257 
4263  public function hasSourceText() {
4264  if ( $this->exists() ) {
4265  return true;
4266  }
4267 
4268  if ( $this->mNamespace == NS_MEDIAWIKI ) {
4269  // If the page doesn't exist but is a known system message, default
4270  // message content will be displayed, same for language subpages-
4271  // Use always content language to avoid loading hundreds of languages
4272  // to get the link color.
4273  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
4274  list( $name, ) = MessageCache::singleton()->figureMessage(
4275  $contLang->lcfirst( $this->getText() )
4276  );
4277  $message = wfMessage( $name )->inLanguage( $contLang )->useDatabase( false );
4278  return $message->exists();
4279  }
4280 
4281  return false;
4282  }
4283 
4321  public function getDefaultMessageText() {
4322  if ( $this->mNamespace != NS_MEDIAWIKI ) { // Just in case
4323  return false;
4324  }
4325 
4326  list( $name, $lang ) = MessageCache::singleton()->figureMessage(
4327  MediaWikiServices::getInstance()->getContentLanguage()->lcfirst( $this->getText() )
4328  );
4329  $message = wfMessage( $name )->inLanguage( $lang )->useDatabase( false );
4330 
4331  if ( $message->exists() ) {
4332  return $message->plain();
4333  } else {
4334  return false;
4335  }
4336  }
4337 
4344  public function invalidateCache( $purgeTime = null ) {
4345  if ( wfReadOnly() ) {
4346  return false;
4347  } elseif ( $this->mArticleID === 0 ) {
4348  return true; // avoid gap locking if we know it's not there
4349  }
4350 
4351  $dbw = wfGetDB( DB_MASTER );
4352  $dbw->onTransactionPreCommitOrIdle(
4353  function () use ( $dbw ) {
4355  $this, null, null, $dbw->getDomainID() );
4356  },
4357  __METHOD__
4358  );
4359 
4360  $conds = $this->pageCond();
4362  new AutoCommitUpdate(
4363  $dbw,
4364  __METHOD__,
4365  function ( IDatabase $dbw, $fname ) use ( $conds, $purgeTime ) {
4366  $dbTimestamp = $dbw->timestamp( $purgeTime ?: time() );
4367  $dbw->update(
4368  'page',
4369  [ 'page_touched' => $dbTimestamp ],
4370  $conds + [ 'page_touched < ' . $dbw->addQuotes( $dbTimestamp ) ],
4371  $fname
4372  );
4373  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $this );
4374  }
4375  ),
4377  );
4378 
4379  return true;
4380  }
4381 
4387  public function touchLinks() {
4388  $jobs = [];
4390  $this,
4391  'pagelinks',
4392  [ 'causeAction' => 'page-touch' ]
4393  );
4394  if ( $this->mNamespace == NS_CATEGORY ) {
4396  $this,
4397  'categorylinks',
4398  [ 'causeAction' => 'category-touch' ]
4399  );
4400  }
4401 
4402  JobQueueGroup::singleton()->lazyPush( $jobs );
4403  }
4404 
4411  public function getTouched( $db = null ) {
4412  if ( $db === null ) {
4413  $db = wfGetDB( DB_REPLICA );
4414  }
4415  $touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ );
4416  return $touched;
4417  }
4418 
4425  public function getNotificationTimestamp( $user = null ) {
4426  global $wgUser;
4427 
4428  // Assume current user if none given
4429  if ( !$user ) {
4430  $user = $wgUser;
4431  }
4432  // Check cache first
4433  $uid = $user->getId();
4434  if ( !$uid ) {
4435  return false;
4436  }
4437  // avoid isset here, as it'll return false for null entries
4438  if ( array_key_exists( $uid, $this->mNotificationTimestamp ) ) {
4439  return $this->mNotificationTimestamp[$uid];
4440  }
4441  // Don't cache too much!
4442  if ( count( $this->mNotificationTimestamp ) >= self::CACHE_MAX ) {
4443  $this->mNotificationTimestamp = [];
4444  }
4445 
4446  $store = MediaWikiServices::getInstance()->getWatchedItemStore();
4447  $watchedItem = $store->getWatchedItem( $user, $this );
4448  if ( $watchedItem ) {
4449  $this->mNotificationTimestamp[$uid] = $watchedItem->getNotificationTimestamp();
4450  } else {
4451  $this->mNotificationTimestamp[$uid] = false;
4452  }
4453 
4454  return $this->mNotificationTimestamp[$uid];
4455  }
4456 
4463  public function getNamespaceKey( $prepend = 'nstab-' ) {
4464  // Gets the subject namespace of this title
4465  $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo();
4466  $subjectNS = $nsInfo->getSubject( $this->mNamespace );
4467  // Prefer canonical namespace name for HTML IDs
4468  $namespaceKey = $nsInfo->getCanonicalName( $subjectNS );
4469  if ( $namespaceKey === false ) {
4470  // Fallback to localised text
4471  $namespaceKey = $this->getSubjectNsText();
4472  }
4473  // Makes namespace key lowercase
4474  $namespaceKey = MediaWikiServices::getInstance()->getContentLanguage()->lc( $namespaceKey );
4475  // Uses main
4476  if ( $namespaceKey == '' ) {
4477  $namespaceKey = 'main';
4478  }
4479  // Changes file to image for backwards compatibility
4480  if ( $namespaceKey == 'file' ) {
4481  $namespaceKey = 'image';
4482  }
4483  return $prepend . $namespaceKey;
4484  }
4485 
4492  public function getRedirectsHere( $ns = null ) {
4493  $redirs = [];
4494 
4495  $dbr = wfGetDB( DB_REPLICA );
4496  $where = [
4497  'rd_namespace' => $this->mNamespace,
4498  'rd_title' => $this->mDbkeyform,
4499  'rd_from = page_id'
4500  ];
4501  if ( $this->isExternal() ) {
4502  $where['rd_interwiki'] = $this->mInterwiki;
4503  } else {
4504  $where[] = 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL';
4505  }
4506  if ( !is_null( $ns ) ) {
4507  $where['page_namespace'] = $ns;
4508  }
4509 
4510  $res = $dbr->select(
4511  [ 'redirect', 'page' ],
4512  [ 'page_namespace', 'page_title' ],
4513  $where,
4514  __METHOD__
4515  );
4516 
4517  foreach ( $res as $row ) {
4518  $redirs[] = self::newFromRow( $row );
4519  }
4520  return $redirs;
4521  }
4522 
4528  public function isValidRedirectTarget() {
4530 
4531  if ( $this->isSpecialPage() ) {
4532  // invalid redirect targets are stored in a global array, but explicitly disallow Userlogout here
4533  if ( $this->isSpecial( 'Userlogout' ) ) {
4534  return false;
4535  }
4536 
4537  foreach ( $wgInvalidRedirectTargets as $target ) {
4538  if ( $this->isSpecial( $target ) ) {
4539  return false;
4540  }
4541  }
4542  }
4543 
4544  return true;
4545  }
4546 
4552  public function getBacklinkCache() {
4553  return BacklinkCache::get( $this );
4554  }
4555 
4561  public function canUseNoindex() {
4563 
4564  $bannedNamespaces = $wgExemptFromUserRobotsControl ??
4565  MediaWikiServices::getInstance()->getNamespaceInfo()->getContentNamespaces();
4566 
4567  return !in_array( $this->mNamespace, $bannedNamespaces );
4568  }
4569 
4580  public function getCategorySortkey( $prefix = '' ) {
4581  $unprefixed = $this->getText();
4582 
4583  // Anything that uses this hook should only depend
4584  // on the Title object passed in, and should probably
4585  // tell the users to run updateCollations.php --force
4586  // in order to re-sort existing category relations.
4587  Hooks::run( 'GetDefaultSortkey', [ $this, &$unprefixed ] );
4588  if ( $prefix !== '' ) {
4589  # Separate with a line feed, so the unprefixed part is only used as
4590  # a tiebreaker when two pages have the exact same prefix.
4591  # In UCA, tab is the only character that can sort above LF
4592  # so we strip both of them from the original prefix.
4593  $prefix = strtr( $prefix, "\n\t", ' ' );
4594  return "$prefix\n$unprefixed";
4595  }
4596  return $unprefixed;
4597  }
4598 
4606  private function getDbPageLanguageCode() {
4607  global $wgPageLanguageUseDB;
4608 
4609  // check, if the page language could be saved in the database, and if so and
4610  // the value is not requested already, lookup the page language using LinkCache
4611  if ( $wgPageLanguageUseDB && $this->mDbPageLanguage === false ) {
4612  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
4613  $linkCache->addLinkObj( $this );
4614  $this->mDbPageLanguage = $linkCache->getGoodLinkFieldObj( $this, 'lang' );
4615  }
4616 
4617  return $this->mDbPageLanguage;
4618  }
4619 
4628  public function getPageLanguage() {
4629  global $wgLang, $wgLanguageCode;
4630  if ( $this->isSpecialPage() ) {
4631  // special pages are in the user language
4632  return $wgLang;
4633  }
4634 
4635  // Checking if DB language is set
4636  $dbPageLanguage = $this->getDbPageLanguageCode();
4637  if ( $dbPageLanguage ) {
4638  return wfGetLangObj( $dbPageLanguage );
4639  }
4640 
4641  if ( !$this->mPageLanguage || $this->mPageLanguage[1] !== $wgLanguageCode ) {
4642  // Note that this may depend on user settings, so the cache should
4643  // be only per-request.
4644  // NOTE: ContentHandler::getPageLanguage() may need to load the
4645  // content to determine the page language!
4646  // Checking $wgLanguageCode hasn't changed for the benefit of unit
4647  // tests.
4648  $contentHandler = ContentHandler::getForTitle( $this );
4649  $langObj = $contentHandler->getPageLanguage( $this );
4650  $this->mPageLanguage = [ $langObj->getCode(), $wgLanguageCode ];
4651  } else {
4652  $langObj = Language::factory( $this->mPageLanguage[0] );
4653  }
4654 
4655  return $langObj;
4656  }
4657 
4666  public function getPageViewLanguage() {
4667  global $wgLang;
4668 
4669  if ( $this->isSpecialPage() ) {
4670  // If the user chooses a variant, the content is actually
4671  // in a language whose code is the variant code.
4672  $variant = $wgLang->getPreferredVariant();
4673  if ( $wgLang->getCode() !== $variant ) {
4674  return Language::factory( $variant );
4675  }
4676 
4677  return $wgLang;
4678  }
4679 
4680  // Checking if DB language is set
4681  $dbPageLanguage = $this->getDbPageLanguageCode();
4682  if ( $dbPageLanguage ) {
4683  $pageLang = wfGetLangObj( $dbPageLanguage );
4684  $variant = $pageLang->getPreferredVariant();
4685  if ( $pageLang->getCode() !== $variant ) {
4686  $pageLang = Language::factory( $variant );
4687  }
4688 
4689  return $pageLang;
4690  }
4691 
4692  // @note Can't be cached persistently, depends on user settings.
4693  // @note ContentHandler::getPageViewLanguage() may need to load the
4694  // content to determine the page language!
4695  $contentHandler = ContentHandler::getForTitle( $this );
4696  $pageLang = $contentHandler->getPageViewLanguage( $this );
4697  return $pageLang;
4698  }
4699 
4710  public function getEditNotices( $oldid = 0 ) {
4711  $notices = [];
4712 
4713  // Optional notice for the entire namespace
4714  $editnotice_ns = 'editnotice-' . $this->mNamespace;
4715  $msg = wfMessage( $editnotice_ns );
4716  if ( $msg->exists() ) {
4717  $html = $msg->parseAsBlock();
4718  // Edit notices may have complex logic, but output nothing (T91715)
4719  if ( trim( $html ) !== '' ) {
4720  $notices[$editnotice_ns] = Html::rawElement(
4721  'div',
4722  [ 'class' => [
4723  'mw-editnotice',
4724  'mw-editnotice-namespace',
4725  Sanitizer::escapeClass( "mw-$editnotice_ns" )
4726  ] ],
4727  $html
4728  );
4729  }
4730  }
4731 
4732  if (
4733  MediaWikiServices::getInstance()->getNamespaceInfo()->
4734  hasSubpages( $this->mNamespace )
4735  ) {
4736  // Optional notice for page itself and any parent page
4737  $editnotice_base = $editnotice_ns;
4738  foreach ( explode( '/', $this->mDbkeyform ) as $part ) {
4739  $editnotice_base .= '-' . $part;
4740  $msg = wfMessage( $editnotice_base );
4741  if ( $msg->exists() ) {
4742  $html = $msg->parseAsBlock();
4743  if ( trim( $html ) !== '' ) {
4744  $notices[$editnotice_base] = Html::rawElement(
4745  'div',
4746  [ 'class' => [
4747  'mw-editnotice',
4748  'mw-editnotice-base',
4749  Sanitizer::escapeClass( "mw-$editnotice_base" )
4750  ] ],
4751  $html
4752  );
4753  }
4754  }
4755  }
4756  } else {
4757  // Even if there are no subpages in namespace, we still don't want "/" in MediaWiki message keys
4758  $editnoticeText = $editnotice_ns . '-' . strtr( $this->mDbkeyform, '/', '-' );
4759  $msg = wfMessage( $editnoticeText );
4760  if ( $msg->exists() ) {
4761  $html = $msg->parseAsBlock();
4762  if ( trim( $html ) !== '' ) {
4763  $notices[$editnoticeText] = Html::rawElement(
4764  'div',
4765  [ 'class' => [
4766  'mw-editnotice',
4767  'mw-editnotice-page',
4768  Sanitizer::escapeClass( "mw-$editnoticeText" )
4769  ] ],
4770  $html
4771  );
4772  }
4773  }
4774  }
4775 
4776  Hooks::run( 'TitleGetEditNotices', [ $this, $oldid, &$notices ] );
4777  return $notices;
4778  }
4779 
4784  private function loadFieldFromDB( $field, $flags ) {
4785  if ( !in_array( $field, self::getSelectFields(), true ) ) {
4786  return false; // field does not exist
4787  }
4788 
4789  $flags |= ( $flags & self::GAID_FOR_UPDATE ) ? self::READ_LATEST : 0; // b/c
4790  list( $index, $options ) = DBAccessObjectUtils::getDBOptions( $flags );
4791 
4792  return wfGetDB( $index )->selectField(
4793  'page',
4794  $field,
4795  $this->pageCond(),
4796  __METHOD__,
4797  $options
4798  );
4799  }
4800 
4804  public function __sleep() {
4805  return [
4806  'mNamespace',
4807  'mDbkeyform',
4808  'mFragment',
4809  'mInterwiki',
4810  'mLocalInterwiki',
4811  'mUserCaseDBKey',
4812  'mDefaultNamespace',
4813  ];
4814  }
4815 
4816  public function __wakeup() {
4817  $this->mArticleID = ( $this->mNamespace >= 0 ) ? -1 : 0;
4818  $this->mUrlform = wfUrlencode( $this->mDbkeyform );
4819  $this->mTextform = strtr( $this->mDbkeyform, '_', ' ' );
4820  }
4821 
4822 }
bool $mHasSubpages
Whether a page has any subpages.
Definition: Title.php:174
static getFilteredRestrictionTypes( $exists=true)
Get a filtered list of all restriction types supported by this wiki.
Definition: Title.php:2453
isAlwaysKnown()
Should links to this title be shown as potentially viewable (i.e.
Definition: Title.php:4200
areCascadeProtectionSourcesLoaded( $getPages=true)
Determines whether cascading protection sources have already been loaded from the database...
Definition: Title.php:2680
static purgeExpiredRestrictions()
Purge expired restrictions from the page_restrictions table.
Definition: Title.php:3014
clear( $keys=null)
Clear one or several cache entries, or all cache entries.
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition: router.php:42
static castFromLinkTarget( $linkTarget)
Same as newFromLinkTarget, but if passed null, returns null.
Definition: Title.php:295
lazyFillContentModel( $model)
If the content model field is not frozen then update it with a retreived value.
Definition: Title.php:1116
touchLinks()
Update page_touched timestamps and send CDN purge messages for pages linking to this title...
Definition: Title.php:4387
getFragment()
Get the Title fragment (i.e.
Definition: Title.php:1747
mixed $mTitleProtection
Cached value for getTitleProtection (create protection)
Definition: Title.php:154
invalidateCache( $purgeTime=null)
Updates page_touched for this page; called from LinksUpdate.php.
Definition: Title.php:4344
getRestrictions( $action)
Accessor/initialisation for mRestrictions.
Definition: Title.php:2796
isMovable()
Would anybody with sufficient privileges be able to move this page? Some pages just aren&#39;t movable...
Definition: Title.php:1363
getRootTitle()
Get the root page name title, i.e.
Definition: Title.php:1929
bool $wgMainPageIsDomainRoot
Option to whether serve the main page as the domain root.
getArticleID( $flags=0)
Get the article ID for this Title from the link cache, adding it if necessary.
Definition: Title.php:3166
getAuthorsBetween( $old, $new, $limit, $options=[])
Get the authors between the given revisions or revision IDs.
Definition: Title.php:4062
canHaveTalkPage()
Can this title have a corresponding talk page?
Definition: Title.php:1180
canUseNoindex()
Whether the magic words INDEX and NOINDEX function for this page.
Definition: Title.php:4561
const CONTENT_MODEL_WIKITEXT
Definition: Defines.php:215
wasLocalInterwiki()
Was this a local interwiki link?
Definition: Title.php:935
get( $key, $maxAge=INF, $default=null)
Get the value for a key.
$wgScript
The URL path to index.php.
isContentPage()
Is this Title in a namespace which contains content? In other words, is this a content page...
Definition: Title.php:1352
getUserCaseDBKey()
Get the DB key with the initial letter case as specified by the user.
Definition: Title.php:1026
isSemiProtected( $action='edit')
Is this page "semi-protected" - the only protection levels are listed in $wgSemiprotectedRestrictionL...
Definition: Title.php:2586
static newFromID( $id, $flags=0)
Create a new Title from an article ID.
Definition: Title.php:467
isNamespaceProtected(User $user)
Determines if $user is unable to edit this page because it has been protected by $wgNamespaceProtecti...
Definition: Title.php:2647
static clearCaches()
Definition: Title.php:3301
countRevisionsBetween( $old, $new, $max=null)
Get the number of revisions between the given revision.
Definition: Title.php:4031
hasSubpages()
Does this have subpages? (Warning, usually requires an extra DB query.)
Definition: Title.php:3055
const NS_MAIN
Definition: Defines.php:60
getSkinFromConfigSubpage()
Trim down a .css, .json, or .js subpage title to get the corresponding skin name. ...
Definition: Title.php:1466
getText()
Get the text form (spaces not underscores) of the main part.
Definition: Title.php:998
getSubpageText()
Get the lowest-level subpage name, i.e.
Definition: Title.php:2002
getBaseText()
Get the base page name without a namespace, i.e.
Definition: Title.php:1952
static newMainPage(MessageLocalizer $localizer=null)
Create a new Title for the Main Page.
Definition: Title.php:648
getParentCategoryTree( $children=[])
Get a tree of parent categories.
Definition: Title.php:3838
getDefaultMessageText()
Get the default (plain) message contents for an page that overrides an interface message key...
Definition: Title.php:4321
loadRestrictionsFromRows( $rows, $oldFashionedRestrictions=null)
Compiles list of active page restrictions from both page table (pre 1.10) and page_restrictions table...
Definition: Title.php:2855
static fixUrlQueryArgs( $query, $query2=false)
Helper to fix up the get{Canonical,Full,Link,Local,Internal}URL args get{Canonical,Full,Link,Local,Internal}URL methods accepted an optional second argument named variant.
Definition: Title.php:2070
loadFromRow( $row)
Load Title object fields from a DB row.
Definition: Title.php:530
setFragment( $fragment)
Set the fragment for this title.
Definition: Title.php:1793
equals(LinkTarget $title)
Compare with another title.
Definition: Title.php:4149
getSubjectNsText()
Get the namespace text of the subject (rather than talk) page.
Definition: Title.php:1152
bool null $mIsBigDeletion
Would deleting this page be a big deletion?
Definition: Title.php:188
isUserJsConfigPage()
Is this a JS "config" sub-page of a user page?
Definition: Title.php:1510
__wakeup()
Definition: Title.php:4816
getTalkPageIfDefined()
Get a Title object associated with the talk page of this article, if such a talk page can exist...
Definition: Title.php:1639
getTransWikiID()
Returns the DB name of the distant wiki which owns the object.
Definition: Title.php:958
bool $mForcedContentModel
If a content model was forced via setContentModel() this will be true to avoid having other code path...
Definition: Title.php:109
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
$wgActionPaths
Definition: img_auth.php:48
getFullUrlForRedirect( $query='', $proto=PROTO_CURRENT)
Get a url appropriate for making redirects based on an untrusted url arg.
Definition: Title.php:2143
$wgInternalServer
Internal server name as known to CDN, if different.
isWatchable()
Can this title be added to a user&#39;s watchlist?
Definition: Title.php:1238
if(!isset( $args[0])) $lang
isUserCssConfigPage()
Is this a CSS "config" sub-page of a user page?
Definition: Title.php:1482
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:209
$wgRestrictionLevels
Rights which can be required for each protection level (via action=protect)
inNamespaces(... $namespaces)
Returns true if the title is inside one of the specified namespaces.
Definition: Title.php:1313
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
getLocalURL( $query='', $query2=false)
Get a URL with no fragment or server name (relative URL) from a Title object.
Definition: Title.php:2177
isTalkPage()
Is this a talk page of some sort?
Definition: Title.php:1593
Handles purging the appropriate CDN objects given a list of URLs or Title instances.
string $mUrlform
URL-encoded form of the main part.
Definition: Title.php:79
static getDefaultModelFor(Title $title)
Returns the name of the default content model to be used for the page with the given title...
getDbPageLanguageCode()
Returns the page language code saved in the database, if $wgPageLanguageUseDB is set to true in Local...
Definition: Title.php:4606
Represents a page (or page fragment) title within MediaWiki.
Definition: TitleValue.php:36
isBigDeletion()
Check whether the number of revisions of this page surpasses $wgDeleteRevisionsLimit.
Definition: Title.php:3977
const NS_SPECIAL
Definition: Defines.php:49
const PROTO_CURRENT
Definition: Defines.php:202
getNotificationTimestamp( $user=null)
Get the timestamp when this page was updated since the user last saw it.
Definition: Title.php:4425
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:1711
static getInterwikiLookup()
B/C kludge: provide an InterwikiLookup for use by Title.
Definition: Title.php:214
$wgSemiprotectedRestrictionLevels
Restriction levels that should be considered "semiprotected".
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
inNamespace( $ns)
Returns true if the title is inside the specified namespace.
Definition: Title.php:1301
getPrefixedText()
Get the prefixed title with spaces.
Definition: Title.php:1858
moveTo(&$nt, $auth=true, $reason='', $createRedirect=true, array $changeTags=[])
Move a title to a new location.
Definition: Title.php:3633
getBaseTitle()
Get the base page name title, i.e.
Definition: Title.php:1982
getParentCategories()
Get categories to which this Title belongs and return an array of categories&#39; names.
Definition: Title.php:3803
static newFromRow( $row)
Make a Title object from a DB row.
Definition: Title.php:518
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:3390
getBacklinkCache()
Get a backlink cache object.
Definition: Title.php:4552
$wgArticlePath
Definition: img_auth.php:47
TitleValue null $mTitleValue
A corresponding TitleValue object.
Definition: Title.php:185
getCategorySortkey( $prefix='')
Returns the raw sort key to be used for categories, with the specified prefix.
Definition: Title.php:4580
static newFromTitle(LinkTarget $linkTarget, $id=0, $flags=0)
Load either the current, or a specified, revision that&#39;s attached to a given link target...
Definition: Revision.php:138
const CONTENT_MODEL_JSON
Definition: Defines.php:219
The TitleArray class only exists to provide the newFromResult method at pre- sent.
Definition: TitleArray.php:35
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:3448
const DB_MASTER
Definition: defines.php:26
static getDBOptions( $bitfield)
Get an appropriate DB index, options, and fallback DB index for a query.
$wgRestrictionTypes
Set of available actions that can be restricted via action=protect You probably shouldn&#39;t change this...
getTalkPage()
Get a Title object associated with the talk page of this article.
Definition: Title.php:1609
userCan( $action, $user=null, $rigor=PermissionManager::RIGOR_SECURE)
Can $user perform $action on this page?
Definition: Title.php:2394
getNamespace()
Get the namespace index.
wfGetLangObj( $langcode=false)
Return a Language object from $langcode.
const NEW_CLONE
Flag for use with factory methods like newFromLinkTarget() that have a $forceClone parameter...
Definition: Title.php:67
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by ConvertibleTimestamp to the format used for ins...
array $mCascadeSources
Where are the cascading restrictions coming from on this page?
Definition: Title.php:138
isSiteConfigPage()
Could this MediaWiki namespace page contain custom CSS, JSON, or JavaScript for the global UI...
Definition: Title.php:1438
fixSpecialName()
If the Title refers to a special page alias which is not the local default, resolve the alias...
Definition: Title.php:1277
static convertByteClassToUnicodeClass( $byteClass)
Utility method for converting a character sequence from bytes to Unicode.
Definition: Title.php:708
getFragment()
Get the link fragment (i.e.
static nameOf( $id)
Get the prefixed DB key associated with an ID.
Definition: Title.php:673
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:51
$wgContentHandlerUseDB
Set to false to disable use of the database fields introduced by the ContentHandler facility...
wfArrayToCgi( $array1, $array2=null, $prefix='')
This function takes one or two arrays as input, and returns a CGI-style string, e.g.
static get(Title $title)
Create a new BacklinkCache or reuse any existing one.
warnIfPageCannotExist(Title $title, $method)
Definition: Title.php:1680
getSubpage( $text)
Get the title for a subpage of the current page.
Definition: Title.php:2026
$wgLanguageCode
Site language code.
wfAppendQuery( $url, $query)
Append a query string to an existing URL, which may or may not already have query string parameters a...
bool $mCascadeRestriction
Cascade restrictions on this page to included templates and images?
Definition: Title.php:126
getLinkURL( $query='', $query2=false, $proto=false)
Get a URL that&#39;s the simplest URL that will be valid to link, locally, to the current Title...
Definition: Title.php:2283
quickUserCan( $action, $user=null)
Can $user perform $action on this page? This skips potentially expensive cascading permission checks ...
Definition: Title.php:2375
isSiteCssConfigPage()
Is this a sitewide CSS "config" page?
Definition: Title.php:1524
flushRestrictions()
Flush the protection cache in this object and force reload from the database.
Definition: Title.php:3004
string null $prefixedText
Text form including namespace/interwiki, initialised on demand.
Definition: Title.php:151
string $mDbkeyform
Main part with underscores.
Definition: Title.php:81
getNsText()
Get the namespace text.
Definition: Title.php:1127
getCanonicalURL( $query='', $query2=false)
Get the URL for a canonical link, for use in things like IRC and e-mail notifications.
Definition: Title.php:2332
__sleep()
Definition: Title.php:4804
string bool $mOldRestrictions
Comma-separated set of permission keys indicating who can move or edit the page from the page table...
Definition: Title.php:123
__construct()
Definition: Title.php:221
wfMergeErrorArrays(... $args)
Merge arrays in the style of getUserPermissionsErrors, with duplicate removal e.g.
wfReadOnly()
Check whether the wiki is in read-only mode.
isExternal()
Is this Title interwiki?
Definition: Title.php:915
wfUrlencode( $s)
We want some things to be included as literal characters in our title URLs for prettiness, which urlencode encodes by default.
$wgLang
Definition: Setup.php:856
isUserConfigPage()
Is this a "config" (.css, .json, or .js) sub-page of a user page?
Definition: Title.php:1452
deleteTitleProtection()
Remove any title protection due to page existing.
Definition: Title.php:2568
int $mNamespace
Namespace index, i.e.
Definition: Title.php:85
set( $key, $value, $rank=self::RANK_TOP)
Set a key/value pair.
hasSourceText()
Does this page have source text?
Definition: Title.php:4263
isUserJsonConfigPage()
Is this a JSON "config" sub-page of a user page?
Definition: Title.php:1496
static getTitleCache()
Definition: Title.php:427
static getTitleFormatter()
B/C kludge: provide a TitleParser for use by Title.
Definition: Title.php:202
getDBkey()
Get the main part with underscores.
Definition: Title.php:1016
__toString()
Return a string representation of this title.
Definition: Title.php:1872
getPrefixedURL()
Get a URL-encoded title (not an actual URL) including interwiki.
Definition: Title.php:2051
isSpecial( $name)
Returns true if this title resolves to the named special page.
Definition: Title.php:1259
hasFragment()
Check if a Title fragment is set.
Definition: Title.php:1757
getRedirectsHere( $ns=null)
Get all extant redirects to this Title.
Definition: Title.php:4492
$wgExemptFromUserRobotsControl
An array of namespace keys in which the INDEX/__NOINDEX__ magic words will not function, so users can&#39;t decide whether pages in that namespace are indexed by search engines.
$wgInvalidRedirectTargets
Array of invalid page redirect targets.
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:3432
areRestrictionsCascading()
Returns cascading restrictions for the current article.
Definition: Title.php:2836
static newForBacklinks(Title $title, $table, $params=[])
isConversionTable()
Is this a conversion table for the LanguageConverter?
Definition: Title.php:1408
const NS_MEDIA
Definition: Defines.php:48
getRootText()
Get the root page name text without a namespace, i.e.
Definition: Title.php:1905
bool string $mContentModel
ID of the page&#39;s content model, i.e.
Definition: Title.php:103
canExist()
Can this title represent a page in the wiki&#39;s database?
Definition: Title.php:1194
static getActionPaths(array $actionPaths, $articlePath)
Definition: PathRouter.php:430
wfDebug( $text, $dest='all', array $context=[])
Sends a line to the debug log if enabled or, optionally, to a comment in output.
getSubpages( $limit=-1)
Get all subpages of this page.
Definition: Title.php:3086
const GAID_FOR_UPDATE
Used to be GAID_FOR_UPDATE define().
Definition: Title.php:58
loadFieldFromDB( $field, $flags)
Definition: Title.php:4784
null $mRedirect
Is the article at this title a redirect?
Definition: Title.php:168
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
hasContentModel( $id)
Convenience method for checking a title&#39;s content model name.
Definition: Title.php:1085
isValidRedirectTarget()
Check if this Title is a valid redirect target.
Definition: Title.php:4528
isCascadeProtected()
Cascading protection: Return true if cascading restrictions apply to this page, false if not...
Definition: Title.php:2666
isValidMoveTarget( $nt)
Checks if $this can be moved to a given Title.
Definition: Title.php:3752
isValidMoveOperation(&$nt, $auth=true, $reason='')
Check whether a given move operation would be valid.
Definition: Title.php:3597
getCdnUrls()
Get a list of URLs to purge from the CDN cache when this page changes.
Definition: Title.php:3550
getDBkey()
Get the main part with underscores.
$cache
Definition: mcc.php:33
getDefaultNamespace()
Get the default namespace index, for when there is no namespace.
Definition: Title.php:1736
const NS_CATEGORY
Definition: Defines.php:74
resetArticleID( $id)
Inject a page ID, reset DB-loaded fields, and clear the link cache for this title.
Definition: Title.php:3279
isSubpageOf(Title $title)
Check if this title is a subpage of another title.
Definition: Title.php:4162
array $mNotificationTimestamp
Associative array of user ID -> timestamp/false.
Definition: Title.php:171
loadRestrictions( $oldFashionedRestrictions=null, $flags=0)
Load restrictions from the page_restrictions table.
Definition: Title.php:2933
getFullText()
Get the prefixed title with spaces, plus any fragment (part beginning with &#39;#&#39;)
Definition: Title.php:1882
static newFromIDs( $ids)
Make an array of titles from an array of IDs.
Definition: Title.php:492
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:182
static newFromResult( $res)
Definition: TitleArray.php:42
isMainPage()
Is this the mainpage?
Definition: Title.php:1387
prefix( $name)
Prefix some arbitrary text with the namespace or interwiki prefix of this object. ...
Definition: Title.php:1820
static escapeClass( $class)
Given a value, escape it so that it can be used as a CSS class and return it.
Definition: Sanitizer.php:1418
static getQueryInfo( $options=[])
Return the tables, fields, and join conditions to be selected to create a new revision object...
Definition: Revision.php:315
getNamespaceKey( $prepend='nstab-')
Generate strings used for xml &#39;id&#39; names in monobook tabs.
Definition: Title.php:4463
static factory( $code)
Get a cached or new language object for a given language code.
Definition: Language.php:212
getFullURL( $query='', $query2=false, $proto=PROTO_RELATIVE)
Get a real URL referring to this title, with interwiki link and fragment.
Definition: Title.php:2108
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:1040
const PROTO_RELATIVE
Definition: Defines.php:201
string $mInterwiki
Interwiki prefix.
Definition: Title.php:87
static hasFlags( $bitfield, $flags)
isKnown()
Does this title refer to a page that can (or might) be meaningfully viewed? In particular, this function may be used to determine if links to the title should be rendered as "bluelinks" (as opposed to "redlinks" to non-existent pages).
Definition: Title.php:4254
isProtected( $action='')
Does the title correspond to a protected article?
Definition: Title.php:2614
const NS_FILE
Definition: Defines.php:66
areRestrictionsLoaded()
Accessor for mRestrictionsLoaded.
Definition: Title.php:2783
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don&#39;t need a full Title object...
Definition: SpecialPage.php:83
isSiteJsonConfigPage()
Is this a sitewide JSON "config" page?
Definition: Title.php:1542
isSubpage()
Is this a subpage?
Definition: Title.php:1396
getInterwiki()
Get the interwiki prefix.
Definition: Title.php:926
static getForTitle(Title $title)
Returns the appropriate ContentHandler singleton for the given title.
getBrokenLinksFrom()
Get an array of Title objects referring to non-existent articles linked from this page...
Definition: Title.php:3514
const NS_MEDIAWIKI
Definition: Defines.php:68
const PROTO_HTTP
Definition: Defines.php:199
array $mRestrictionsExpiry
When do the restrictions on this page expire?
Definition: Title.php:132
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:3502
array null $mPageLanguage
The (string) language code of the page&#39;s language and content code.
Definition: Title.php:177
static newFromTextThrow( $text, $defaultNamespace=NS_MAIN)
Like Title::newFromText(), but throws MalformedTitleException when the title is invalid, rather than returning null.
Definition: Title.php:354
isValid()
Returns true if the title is a valid link target, and that it has been properly normalized.
Definition: Title.php:861
CONTENT_MODEL_JAVASCRIPT
Allow users to upload files.
bool $wgPageLanguageUseDB
Enable page language feature Allows setting page language in database.
getSubjectPage()
Get a title object associated with the subject page of this talk page.
Definition: Title.php:1654
static invalidateModuleCache(Title $title, ?Revision $old, ?Revision $new, $domain)
Clear the preloadTitleInfo() cache for all wiki modules on this wiki on page change if it was a JS or...
int $mLength
The page length, 0 for special pages.
Definition: Title.php:165
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:612
hasSubjectNamespace( $ns)
Returns true if the title has the same subject namespace as the namespace specified.
Definition: Title.php:1340
static newFromLinkTarget(LinkTarget $linkTarget, $forceClone='')
Returns a Title given a LinkTarget.
Definition: Title.php:271
isSpecialPage()
Returns true if this is a special page.
Definition: Title.php:1249
getEditURL()
Get the edit URL for this Title.
Definition: Title.php:2346
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:586
$wgLegalTitleChars
Allowed title characters – regex character class Don&#39;t change this unless you know what you&#39;re doing...
$wgNamespaceProtection
Set the minimum permissions required to edit pages in each namespace.
selectFieldValues( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a list of single field values from result rows.
isSingleRevRedirect()
Locks the page row and check if this page is single revision redirect.
Definition: Title.php:3701
const PROTO_CANONICAL
Definition: Defines.php:203
static addUpdate(DeferrableUpdate $update, $stage=self::POSTSEND)
Add an update to the deferred list to be run later by execute()
getFirstRevision( $flags=0)
Get the first revision of the page.
Definition: Title.php:3925
static newFromDBkey( $key)
Create a new Title from a prefixed DB key.
Definition: Title.php:232
getPageViewLanguage()
Get the language in which the content of this page is written when viewed by user.
Definition: Title.php:4666
bool null $mIsValid
Is the title known to be valid?
Definition: Title.php:191
getSubpageUrlForm()
Get a URL-encoded form of the subpage text.
Definition: Title.php:2040
$mCascadingRestrictions
Caching the results of getCascadeProtectionSources.
Definition: Title.php:129
static getStore()
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
createFragmentTarget( $fragment)
Creates a new Title for a different fragment of the same page.
Definition: Title.php:1804
static newFromURL( $url)
THIS IS NOT THE FUNCTION YOU WANT.
Definition: Title.php:404
countAuthorsBetween( $old, $new, $limit, $options=[])
Get the number of authors between the given revisions or revision IDs.
Definition: Title.php:4138
getTitleProtectionInternal()
Fetch title protection settings.
Definition: Title.php:2521
getRestrictionTypes()
Returns restriction types for the current Title.
Definition: Title.php:2471
static escapeIdForLink( $id)
Given a section name or other user-generated or otherwise unsafe string, escapes it to be a valid URL...
Definition: Sanitizer.php:1322
getContentModel( $flags=0)
Get the page&#39;s content model id, see the CONTENT_MODEL_XXX constants.
Definition: Title.php:1052
isTrans()
Determine whether the object refers to a page within this project and is transcludable.
Definition: Title.php:945
string $mFragment
Title fragment (i.e.
Definition: Title.php:91
int $mArticleID
Article ID, fetched from the link cache on demand.
Definition: Title.php:94
int $mDefaultNamespace
Namespace index when there is no namespace.
Definition: Title.php:162
static capitalize( $text, $ns=NS_MAIN)
Capitalize a text string for a title if it belongs to a namespace that capitalizes.
Definition: Title.php:3316
string [] $wgRawHtmlMessages
List of messages which might contain raw HTML.
getPreviousRevisionID( $revId, $flags=0)
Get the revision ID of the previous revision.
Definition: Title.php:3903
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
getPageLanguage()
Get the language in which the content of this page is written in wikitext.
Definition: Title.php:4628
const CACHE_MAX
Title::newFromText maintains a cache to avoid expensive re-normalization of commonly used titles...
Definition: Title.php:51
$revQuery
static escapeIdForExternalInterwiki( $id)
Given a section name or other user-generated or otherwise unsafe string, escapes it to be a valid URL...
Definition: Sanitizer.php:1345
isRawHtmlMessage()
Is this a message which can contain raw HTML?
Definition: Title.php:1578
getFragmentForURL()
Get the fragment in URL form, including the "#" character if there is one.
Definition: Title.php:1766
$wgDeleteRevisionsLimit
Optional to restrict deletion of pages with higher revision counts to users with the &#39;bigdelete&#39; perm...
exists( $flags=0)
Check if page exists.
Definition: Title.php:4178
getLatestRevID( $flags=0)
What is the page_latest field for this page?
Definition: Title.php:3247
pageCond()
Get an associative array for selecting this title from the "page" table.
Definition: Title.php:3865
getInterwiki()
The interwiki component of this LinkTarget.
const CONTENT_MODEL_CSS
Definition: Defines.php:217
getEarliestRevTime( $flags=0)
Get the oldest revision timestamp of this page.
Definition: Title.php:3957
isDeletedQuick()
Is there a version of this page in the deletion archive?
Definition: Title.php:3141
isNewPage()
Check if this is a new page.
Definition: Title.php:3967
getText()
Returns the link in text form, without namespace prefix or fragment.
getRestrictionExpiry( $action)
Get the expiry time for the restriction against a given action.
Definition: Title.php:2824
static compare(LinkTarget $a, LinkTarget $b)
Callback for usort() to do title sorts by (namespace, title)
Definition: Title.php:840
static singleton( $domain=false)
bool $mRestrictionsLoaded
Boolean for initialisation on demand.
Definition: Title.php:141
static legalChars()
Get a regex character class describing the legal characters in a link.
Definition: Title.php:694
getCascadeProtectionSources( $getPages=true)
Cascading protection: Get the source of any cascading restrictions on this page.
Definition: Title.php:2697
isRedirect( $flags=0)
Is this an article that is a redirect page? Uses link cache, adding it if necessary.
Definition: Title.php:3195
isSiteJsConfigPage()
Is this a sitewide JS "config" page?
Definition: Title.php:1560
if(! $wgDBerrorLogTZ) $wgRequest
Definition: Setup.php:727
$wgServer
URL of the server.
isDeleted()
Is there a version of this page in the deletion archive?
Definition: Title.php:3116
isLocal()
Determine whether the object refers to a page within this project (either this wiki or a wiki with a ...
Definition: Title.php:900
array $mRestrictions
Array of groups allowed to edit this article.
Definition: Title.php:115
const DB_REPLICA
Definition: defines.php:25
getTitleProtection()
Is this title subject to title protection? Title protection is the one applied against creation of su...
Definition: Title.php:2498
bool $mLocalInterwiki
Was this Title created from a string with a local interwiki prefix?
Definition: Title.php:89
$content
Definition: router.php:78
int $mEstimateRevisions
Estimated number of revisions; null of not loaded.
Definition: Title.php:112
getEditNotices( $oldid=0)
Get a list of rendered edit notices for this page.
Definition: Title.php:4710
update( $table, $values, $conds, $fname=__METHOD__, $options=[])
UPDATE wrapper.
getTouched( $db=null)
Get the last touched timestamp.
Definition: Title.php:4411
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
static MapCacheLRU null $titleCache
Definition: Title.php:44
getTalkNsText()
Get the namespace text of the talk page.
Definition: Title.php:1163
static getSelectFields()
Returns a list of fields that are to be selected for initializing Title objects or LinkCache entries...
Definition: Title.php:441
delete( $table, $conds, $fname=__METHOD__)
DELETE query wrapper.
string $mTextform
Text form (spaces not underscores) of the main part.
Definition: Title.php:77
getAllRestrictions()
Accessor/initialisation for mRestrictions.
Definition: Title.php:2810
secureAndSplit( $text, $defaultNamespace=null)
Secure and split - main initialisation function for this object.
Definition: Title.php:3341
getInternalURL( $query='', $query2=false)
Get the URL form for an internal link.
Definition: Title.php:2308
static makeName( $ns, $title, $fragment='', $interwiki='', $canonicalNamespace=false)
Make a prefixed DB key from a DB key and a namespace index.
Definition: Title.php:813
addQuotes( $s)
Escape and quote a raw value string for use in a SQL query.
bool int $mLatestID
ID of most recent revision.
Definition: Title.php:97
setContentModel( $model)
Set a proposed content model for the page for permissions checking.
Definition: Title.php:1102
getUserPermissionsErrors( $action, $user, $rigor=PermissionManager::RIGOR_SECURE, $ignoreErrors=[])
Can $user perform $action on this page?
Definition: Title.php:2432
$wgVariantArticlePath
Like $wgArticlePath, but on multi-variant wikis, this provides a path format that describes which par...
static newFromTitleValue(TitleValue $titleValue, $forceClone='')
Returns a Title given a TitleValue.
Definition: Title.php:256
getRelativeRevisionID( $revId, $flags, $dir)
Get next/previous revision ID relative to another revision ID.
Definition: Title.php:3881
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new page object...
Definition: WikiPage.php:387
getLength( $flags=0)
What is the length of this page? Uses link cache, adding it if necessary.
Definition: Title.php:3220
isWikitextPage()
Does that page contain wikitext, or it is JS, CSS or whatever?
Definition: Title.php:1420
static singleton()
Get the singleton instance of this class.
purgeSquid()
Purge all applicable CDN URLs.
Definition: Title.php:3580
static run( $event, array $args=[], $deprecatedVersion=null)
Call hook functions defined in Hooks::register and $wgHooks.
Definition: Hooks.php:200
getTitleValue()
Get a TitleValue object representing this Title.
Definition: Title.php:975
bool $mHasCascadingRestrictions
Are cascading restrictions in effect on this page?
Definition: Title.php:135
getPartialURL()
Get the URL-encoded form of the main part.
Definition: Title.php:1007
getPrefixedDBkey()
Get the prefixed database key form.
Definition: Title.php:1846
static decodeCharReferencesAndNormalize( $text)
Decode any character references, numeric or named entities, in the next and normalize the resulting s...
Definition: Sanitizer.php:1686
moveSubpages( $nt, $auth=true, $reason='', $createRedirect=true, array $changeTags=[])
Move this page&#39;s subpages to be subpages of $nt.
Definition: Title.php:3666
estimateRevisionCount()
Get the approximate revision count of this page.
Definition: Title.php:4006
$matches
getNextRevisionID( $revId, $flags=0)
Get the revision ID of the next revision.
Definition: Title.php:3915
string $mUserCaseDBKey
Database key with the initial letter in the case specified by the user.
Definition: Title.php:83
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:319