MediaWiki  master
Title.php
Go to the documentation of this file.
1 <?php
30 
39 class Title implements LinkTarget {
41  static private $titleCache = null;
42 
48  const CACHE_MAX = 1000;
49 
54  const GAID_FOR_UPDATE = 1;
55 
61  // @{
62 
64  public $mTextform = '';
65 
67  public $mUrlform = '';
68 
70  public $mDbkeyform = '';
71 
73  protected $mUserCaseDBKey;
74 
76  public $mNamespace = NS_MAIN;
77 
79  public $mInterwiki = '';
80 
82  private $mLocalInterwiki = false;
83 
85  public $mFragment = '';
86 
88  public $mArticleID = -1;
89 
91  protected $mLatestID = false;
92 
97  private $mContentModel = false;
98 
103  private $mForcedContentModel = false;
104 
107 
109  public $mRestrictions = [];
110 
117  protected $mOldRestrictions = false;
118 
121 
124 
126  protected $mRestrictionsExpiry = [];
127 
130 
133 
135  public $mRestrictionsLoaded = false;
136 
145  public $prefixedText = null;
146 
149 
156 
158  protected $mLength = -1;
159 
161  public $mRedirect = null;
162 
165 
167  private $mHasSubpages;
168 
170  private $mPageLanguage = false;
171 
174  private $mDbPageLanguage = false;
175 
177  private $mTitleValue = null;
178 
180  private $mIsBigDeletion = null;
181  // @}
182 
191  private static function getTitleFormatter() {
192  return MediaWikiServices::getInstance()->getTitleFormatter();
193  }
194 
203  private static function getInterwikiLookup() {
204  return MediaWikiServices::getInstance()->getInterwikiLookup();
205  }
206 
210  function __construct() {
211  }
212 
221  public static function newFromDBkey( $key ) {
222  $t = new Title();
223  $t->mDbkeyform = $key;
224 
225  try {
226  $t->secureAndSplit();
227  return $t;
228  } catch ( MalformedTitleException $ex ) {
229  return null;
230  }
231  }
232 
240  public static function newFromTitleValue( TitleValue $titleValue ) {
241  return self::newFromLinkTarget( $titleValue );
242  }
243 
251  public static function newFromLinkTarget( LinkTarget $linkTarget ) {
252  if ( $linkTarget instanceof Title ) {
253  // Special case if it's already a Title object
254  return $linkTarget;
255  }
256  return self::makeTitle(
257  $linkTarget->getNamespace(),
258  $linkTarget->getText(),
259  $linkTarget->getFragment(),
260  $linkTarget->getInterwiki()
261  );
262  }
263 
280  public static function newFromText( $text, $defaultNamespace = NS_MAIN ) {
281  // DWIM: Integers can be passed in here when page titles are used as array keys.
282  if ( $text !== null && !is_string( $text ) && !is_int( $text ) ) {
283  throw new InvalidArgumentException( '$text must be a string.' );
284  }
285  if ( $text === null ) {
286  return null;
287  }
288 
289  try {
290  return self::newFromTextThrow( strval( $text ), $defaultNamespace );
291  } catch ( MalformedTitleException $ex ) {
292  return null;
293  }
294  }
295 
313  public static function newFromTextThrow( $text, $defaultNamespace = NS_MAIN ) {
314  if ( is_object( $text ) ) {
315  throw new MWException( '$text must be a string, given an object' );
316  } elseif ( $text === null ) {
317  // Legacy code relies on MalformedTitleException being thrown in this case
318  // (happens when URL with no title in it is parsed). TODO fix
319  throw new MalformedTitleException( 'title-invalid-empty' );
320  }
321 
322  $titleCache = self::getTitleCache();
323 
324  // Wiki pages often contain multiple links to the same page.
325  // Title normalization and parsing can become expensive on pages with many
326  // links, so we can save a little time by caching them.
327  // In theory these are value objects and won't get changed...
328  if ( $defaultNamespace == NS_MAIN ) {
329  $t = $titleCache->get( $text );
330  if ( $t ) {
331  return $t;
332  }
333  }
334 
335  // Convert things like &eacute; &#257; or &#x3017; into normalized (T16952) text
336  $filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text );
337 
338  $t = new Title();
339  $t->mDbkeyform = strtr( $filteredText, ' ', '_' );
340  $t->mDefaultNamespace = intval( $defaultNamespace );
341 
342  $t->secureAndSplit();
343  if ( $defaultNamespace == NS_MAIN ) {
344  $titleCache->set( $text, $t );
345  }
346  return $t;
347  }
348 
364  public static function newFromURL( $url ) {
365  $t = new Title();
366 
367  # For compatibility with old buggy URLs. "+" is usually not valid in titles,
368  # but some URLs used it as a space replacement and they still come
369  # from some external search tools.
370  if ( strpos( self::legalChars(), '+' ) === false ) {
371  $url = strtr( $url, '+', ' ' );
372  }
373 
374  $t->mDbkeyform = strtr( $url, ' ', '_' );
375 
376  try {
377  $t->secureAndSplit();
378  return $t;
379  } catch ( MalformedTitleException $ex ) {
380  return null;
381  }
382  }
383 
387  private static function getTitleCache() {
388  if ( self::$titleCache == null ) {
389  self::$titleCache = new MapCacheLRU( self::CACHE_MAX );
390  }
391  return self::$titleCache;
392  }
393 
401  protected static function getSelectFields() {
403 
404  $fields = [
405  'page_namespace', 'page_title', 'page_id',
406  'page_len', 'page_is_redirect', 'page_latest',
407  ];
408 
409  if ( $wgContentHandlerUseDB ) {
410  $fields[] = 'page_content_model';
411  }
412 
413  if ( $wgPageLanguageUseDB ) {
414  $fields[] = 'page_lang';
415  }
416 
417  return $fields;
418  }
419 
427  public static function newFromID( $id, $flags = 0 ) {
428  $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_REPLICA );
429  $row = $db->selectRow(
430  'page',
431  self::getSelectFields(),
432  [ 'page_id' => $id ],
433  __METHOD__
434  );
435  if ( $row !== false ) {
436  $title = self::newFromRow( $row );
437  } else {
438  $title = null;
439  }
440  return $title;
441  }
442 
449  public static function newFromIDs( $ids ) {
450  if ( !count( $ids ) ) {
451  return [];
452  }
453  $dbr = wfGetDB( DB_REPLICA );
454 
455  $res = $dbr->select(
456  'page',
457  self::getSelectFields(),
458  [ 'page_id' => $ids ],
459  __METHOD__
460  );
461 
462  $titles = [];
463  foreach ( $res as $row ) {
464  $titles[] = self::newFromRow( $row );
465  }
466  return $titles;
467  }
468 
475  public static function newFromRow( $row ) {
476  $t = self::makeTitle( $row->page_namespace, $row->page_title );
477  $t->loadFromRow( $row );
478  return $t;
479  }
480 
487  public function loadFromRow( $row ) {
488  if ( $row ) { // page found
489  if ( isset( $row->page_id ) ) {
490  $this->mArticleID = (int)$row->page_id;
491  }
492  if ( isset( $row->page_len ) ) {
493  $this->mLength = (int)$row->page_len;
494  }
495  if ( isset( $row->page_is_redirect ) ) {
496  $this->mRedirect = (bool)$row->page_is_redirect;
497  }
498  if ( isset( $row->page_latest ) ) {
499  $this->mLatestID = (int)$row->page_latest;
500  }
501  if ( !$this->mForcedContentModel && isset( $row->page_content_model ) ) {
502  $this->mContentModel = strval( $row->page_content_model );
503  } elseif ( !$this->mForcedContentModel ) {
504  $this->mContentModel = false; # initialized lazily in getContentModel()
505  }
506  if ( isset( $row->page_lang ) ) {
507  $this->mDbPageLanguage = (string)$row->page_lang;
508  }
509  if ( isset( $row->page_restrictions ) ) {
510  $this->mOldRestrictions = $row->page_restrictions;
511  }
512  } else { // page not found
513  $this->mArticleID = 0;
514  $this->mLength = 0;
515  $this->mRedirect = false;
516  $this->mLatestID = 0;
517  if ( !$this->mForcedContentModel ) {
518  $this->mContentModel = false; # initialized lazily in getContentModel()
519  }
520  }
521  }
522 
545  public static function makeTitle( $ns, $title, $fragment = '', $interwiki = '' ) {
546  $t = new Title();
547  $t->mInterwiki = $interwiki;
548  $t->mFragment = $fragment;
549  $t->mNamespace = $ns = intval( $ns );
550  $t->mDbkeyform = strtr( $title, ' ', '_' );
551  $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
552  $t->mUrlform = wfUrlencode( $t->mDbkeyform );
553  $t->mTextform = strtr( $title, '_', ' ' );
554  $t->mContentModel = false; # initialized lazily in getContentModel()
555  return $t;
556  }
557 
573  public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) {
574  // NOTE: ideally, this would just call makeTitle() and then isValid(),
575  // but presently, that means more overhead on a potential performance hotspot.
576 
577  if ( !MWNamespace::exists( $ns ) ) {
578  return null;
579  }
580 
581  $t = new Title();
582  $t->mDbkeyform = self::makeName( $ns, $title, $fragment, $interwiki, true );
583 
584  try {
585  $t->secureAndSplit();
586  return $t;
587  } catch ( MalformedTitleException $ex ) {
588  return null;
589  }
590  }
591 
597  public static function newMainPage() {
598  $title = self::newFromText( wfMessage( 'mainpage' )->inContentLanguage()->text() );
599  // Don't give fatal errors if the message is broken
600  if ( !$title ) {
601  $title = self::newFromText( 'Main Page' );
602  }
603  return $title;
604  }
605 
612  public static function nameOf( $id ) {
613  $dbr = wfGetDB( DB_REPLICA );
614 
615  $s = $dbr->selectRow(
616  'page',
617  [ 'page_namespace', 'page_title' ],
618  [ 'page_id' => $id ],
619  __METHOD__
620  );
621  if ( $s === false ) {
622  return null;
623  }
624 
625  $n = self::makeName( $s->page_namespace, $s->page_title );
626  return $n;
627  }
628 
634  public static function legalChars() {
635  global $wgLegalTitleChars;
636  return $wgLegalTitleChars;
637  }
638 
648  public static function convertByteClassToUnicodeClass( $byteClass ) {
649  $length = strlen( $byteClass );
650  // Input token queue
651  $x0 = $x1 = $x2 = '';
652  // Decoded queue
653  $d0 = $d1 = $d2 = '';
654  // Decoded integer codepoints
655  $ord0 = $ord1 = $ord2 = 0;
656  // Re-encoded queue
657  $r0 = $r1 = $r2 = '';
658  // Output
659  $out = '';
660  // Flags
661  $allowUnicode = false;
662  for ( $pos = 0; $pos < $length; $pos++ ) {
663  // Shift the queues down
664  $x2 = $x1;
665  $x1 = $x0;
666  $d2 = $d1;
667  $d1 = $d0;
668  $ord2 = $ord1;
669  $ord1 = $ord0;
670  $r2 = $r1;
671  $r1 = $r0;
672  // Load the current input token and decoded values
673  $inChar = $byteClass[$pos];
674  if ( $inChar == '\\' ) {
675  if ( preg_match( '/x([0-9a-fA-F]{2})/A', $byteClass, $m, 0, $pos + 1 ) ) {
676  $x0 = $inChar . $m[0];
677  $d0 = chr( hexdec( $m[1] ) );
678  $pos += strlen( $m[0] );
679  } elseif ( preg_match( '/[0-7]{3}/A', $byteClass, $m, 0, $pos + 1 ) ) {
680  $x0 = $inChar . $m[0];
681  $d0 = chr( octdec( $m[0] ) );
682  $pos += strlen( $m[0] );
683  } elseif ( $pos + 1 >= $length ) {
684  $x0 = $d0 = '\\';
685  } else {
686  $d0 = $byteClass[$pos + 1];
687  $x0 = $inChar . $d0;
688  $pos += 1;
689  }
690  } else {
691  $x0 = $d0 = $inChar;
692  }
693  $ord0 = ord( $d0 );
694  // Load the current re-encoded value
695  if ( $ord0 < 32 || $ord0 == 0x7f ) {
696  $r0 = sprintf( '\x%02x', $ord0 );
697  } elseif ( $ord0 >= 0x80 ) {
698  // Allow unicode if a single high-bit character appears
699  $r0 = sprintf( '\x%02x', $ord0 );
700  $allowUnicode = true;
701  } elseif ( strpos( '-\\[]^', $d0 ) !== false ) {
702  $r0 = '\\' . $d0;
703  } else {
704  $r0 = $d0;
705  }
706  // Do the output
707  if ( $x0 !== '' && $x1 === '-' && $x2 !== '' ) {
708  // Range
709  if ( $ord2 > $ord0 ) {
710  // Empty range
711  } elseif ( $ord0 >= 0x80 ) {
712  // Unicode range
713  $allowUnicode = true;
714  if ( $ord2 < 0x80 ) {
715  // Keep the non-unicode section of the range
716  $out .= "$r2-\\x7F";
717  }
718  } else {
719  // Normal range
720  $out .= "$r2-$r0";
721  }
722  // Reset state to the initial value
723  $x0 = $x1 = $d0 = $d1 = $r0 = $r1 = '';
724  } elseif ( $ord2 < 0x80 ) {
725  // ASCII character
726  $out .= $r2;
727  }
728  }
729  if ( $ord1 < 0x80 ) {
730  $out .= $r1;
731  }
732  if ( $ord0 < 0x80 ) {
733  $out .= $r0;
734  }
735  if ( $allowUnicode ) {
736  $out .= '\u0080-\uFFFF';
737  }
738  return $out;
739  }
740 
752  public static function makeName( $ns, $title, $fragment = '', $interwiki = '',
753  $canonicalNamespace = false
754  ) {
755  if ( $canonicalNamespace ) {
756  $namespace = MWNamespace::getCanonicalName( $ns );
757  } else {
758  $namespace = MediaWikiServices::getInstance()->getContentLanguage()->getNsText( $ns );
759  }
760  $name = $namespace == '' ? $title : "$namespace:$title";
761  if ( strval( $interwiki ) != '' ) {
762  $name = "$interwiki:$name";
763  }
764  if ( strval( $fragment ) != '' ) {
765  $name .= '#' . $fragment;
766  }
767  return $name;
768  }
769 
778  static function escapeFragmentForURL( $fragment ) {
779  wfDeprecated( __METHOD__, '1.30' );
780  # Note that we don't urlencode the fragment. urlencoded Unicode
781  # fragments appear not to work in IE (at least up to 7) or in at least
782  # one version of Opera 9.x. The W3C validator, for one, doesn't seem
783  # to care if they aren't encoded.
784  return Sanitizer::escapeId( $fragment, 'noninitial' );
785  }
786 
795  public static function compare( LinkTarget $a, LinkTarget $b ) {
796  return $a->getNamespace() <=> $b->getNamespace()
797  ?: strcmp( $a->getText(), $b->getText() );
798  }
799 
814  public function isValid() {
815  if ( !MWNamespace::exists( $this->mNamespace ) ) {
816  return false;
817  }
818 
819  try {
820  $parser = MediaWikiServices::getInstance()->getTitleParser();
821  $parser->parseTitle( $this->mDbkeyform, $this->mNamespace );
822  return true;
823  } catch ( MalformedTitleException $ex ) {
824  return false;
825  }
826  }
827 
835  public function isLocal() {
836  if ( $this->isExternal() ) {
837  $iw = self::getInterwikiLookup()->fetch( $this->mInterwiki );
838  if ( $iw ) {
839  return $iw->isLocal();
840  }
841  }
842  return true;
843  }
844 
850  public function isExternal() {
851  return $this->mInterwiki !== '';
852  }
853 
861  public function getInterwiki() {
862  return $this->mInterwiki;
863  }
864 
870  public function wasLocalInterwiki() {
871  return $this->mLocalInterwiki;
872  }
873 
880  public function isTrans() {
881  if ( !$this->isExternal() ) {
882  return false;
883  }
884 
885  return self::getInterwikiLookup()->fetch( $this->mInterwiki )->isTranscludable();
886  }
887 
893  public function getTransWikiID() {
894  if ( !$this->isExternal() ) {
895  return false;
896  }
897 
898  return self::getInterwikiLookup()->fetch( $this->mInterwiki )->getWikiID();
899  }
900 
910  public function getTitleValue() {
911  if ( $this->mTitleValue === null ) {
912  try {
913  $this->mTitleValue = new TitleValue(
914  $this->mNamespace,
915  $this->mDbkeyform,
916  $this->mFragment,
917  $this->mInterwiki
918  );
919  } catch ( InvalidArgumentException $ex ) {
920  wfDebug( __METHOD__ . ': Can\'t create a TitleValue for [[' .
921  $this->getPrefixedText() . ']]: ' . $ex->getMessage() . "\n" );
922  }
923  }
924 
925  return $this->mTitleValue;
926  }
927 
933  public function getText() {
934  return $this->mTextform;
935  }
936 
942  public function getPartialURL() {
943  return $this->mUrlform;
944  }
945 
951  public function getDBkey() {
952  return $this->mDbkeyform;
953  }
954 
960  function getUserCaseDBKey() {
961  if ( !is_null( $this->mUserCaseDBKey ) ) {
962  return $this->mUserCaseDBKey;
963  } else {
964  // If created via makeTitle(), $this->mUserCaseDBKey is not set.
965  return $this->mDbkeyform;
966  }
967  }
968 
974  public function getNamespace() {
975  return $this->mNamespace;
976  }
977 
986  public function getContentModel( $flags = 0 ) {
987  if ( !$this->mForcedContentModel
988  && ( !$this->mContentModel || $flags === self::GAID_FOR_UPDATE )
989  && $this->getArticleID( $flags )
990  ) {
991  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
992  $linkCache->addLinkObj( $this ); # in case we already had an article ID
993  $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' );
994  }
995 
996  if ( !$this->mContentModel ) {
997  $this->mContentModel = ContentHandler::getDefaultModelFor( $this );
998  }
999 
1000  return $this->mContentModel;
1001  }
1002 
1009  public function hasContentModel( $id ) {
1010  return $this->getContentModel() == $id;
1011  }
1012 
1024  public function setContentModel( $model ) {
1025  $this->mContentModel = $model;
1026  $this->mForcedContentModel = true;
1027  }
1028 
1034  public function getNsText() {
1035  if ( $this->isExternal() ) {
1036  // This probably shouldn't even happen, except for interwiki transclusion.
1037  // If possible, use the canonical name for the foreign namespace.
1038  $nsText = MWNamespace::getCanonicalName( $this->mNamespace );
1039  if ( $nsText !== false ) {
1040  return $nsText;
1041  }
1042  }
1043 
1044  try {
1045  $formatter = self::getTitleFormatter();
1046  return $formatter->getNamespaceName( $this->mNamespace, $this->mDbkeyform );
1047  } catch ( InvalidArgumentException $ex ) {
1048  wfDebug( __METHOD__ . ': ' . $ex->getMessage() . "\n" );
1049  return false;
1050  }
1051  }
1052 
1058  public function getSubjectNsText() {
1059  return MediaWikiServices::getInstance()->getContentLanguage()->
1060  getNsText( MWNamespace::getSubject( $this->mNamespace ) );
1061  }
1062 
1068  public function getTalkNsText() {
1069  return MediaWikiServices::getInstance()->getContentLanguage()->
1070  getNsText( MWNamespace::getTalk( $this->mNamespace ) );
1071  }
1072 
1080  public function canTalk() {
1081  return $this->canHaveTalkPage();
1082  }
1083 
1092  public function canHaveTalkPage() {
1093  return MWNamespace::hasTalkNamespace( $this->mNamespace );
1094  }
1095 
1101  public function canExist() {
1102  return $this->mNamespace >= NS_MAIN;
1103  }
1104 
1110  public function isWatchable() {
1111  return !$this->isExternal() && MWNamespace::isWatchable( $this->mNamespace );
1112  }
1113 
1119  public function isSpecialPage() {
1120  return $this->mNamespace == NS_SPECIAL;
1121  }
1122 
1129  public function isSpecial( $name ) {
1130  if ( $this->isSpecialPage() ) {
1131  list( $thisName, /* $subpage */ ) =
1132  MediaWikiServices::getInstance()->getSpecialPageFactory()->
1133  resolveAlias( $this->mDbkeyform );
1134  if ( $name == $thisName ) {
1135  return true;
1136  }
1137  }
1138  return false;
1139  }
1140 
1147  public function fixSpecialName() {
1148  if ( $this->isSpecialPage() ) {
1149  $spFactory = MediaWikiServices::getInstance()->getSpecialPageFactory();
1150  list( $canonicalName, $par ) = $spFactory->resolveAlias( $this->mDbkeyform );
1151  if ( $canonicalName ) {
1152  $localName = $spFactory->getLocalNameFor( $canonicalName, $par );
1153  if ( $localName != $this->mDbkeyform ) {
1154  return self::makeTitle( NS_SPECIAL, $localName );
1155  }
1156  }
1157  }
1158  return $this;
1159  }
1160 
1171  public function inNamespace( $ns ) {
1172  return MWNamespace::equals( $this->mNamespace, $ns );
1173  }
1174 
1182  public function inNamespaces( /* ... */ ) {
1183  $namespaces = func_get_args();
1184  if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) {
1185  $namespaces = $namespaces[0];
1186  }
1187 
1188  foreach ( $namespaces as $ns ) {
1189  if ( $this->inNamespace( $ns ) ) {
1190  return true;
1191  }
1192  }
1193 
1194  return false;
1195  }
1196 
1210  public function hasSubjectNamespace( $ns ) {
1211  return MWNamespace::subjectEquals( $this->mNamespace, $ns );
1212  }
1213 
1221  public function isContentPage() {
1222  return MWNamespace::isContent( $this->mNamespace );
1223  }
1224 
1231  public function isMovable() {
1232  if ( !MWNamespace::isMovable( $this->mNamespace ) || $this->isExternal() ) {
1233  // Interwiki title or immovable namespace. Hooks don't get to override here
1234  return false;
1235  }
1236 
1237  $result = true;
1238  Hooks::run( 'TitleIsMovable', [ $this, &$result ] );
1239  return $result;
1240  }
1241 
1252  public function isMainPage() {
1253  return $this->equals( self::newMainPage() );
1254  }
1255 
1261  public function isSubpage() {
1262  return MWNamespace::hasSubpages( $this->mNamespace )
1263  ? strpos( $this->getText(), '/' ) !== false
1264  : false;
1265  }
1266 
1272  public function isConversionTable() {
1273  // @todo ConversionTable should become a separate content model.
1274 
1275  return $this->mNamespace == NS_MEDIAWIKI &&
1276  strpos( $this->getText(), 'Conversiontable/' ) === 0;
1277  }
1278 
1284  public function isWikitextPage() {
1285  return $this->hasContentModel( CONTENT_MODEL_WIKITEXT );
1286  }
1287 
1302  public function isSiteConfigPage() {
1303  return (
1304  $this->isSiteCssConfigPage()
1305  || $this->isSiteJsonConfigPage()
1306  || $this->isSiteJsConfigPage()
1307  );
1308  }
1309 
1314  public function isCssOrJsPage() {
1315  wfDeprecated( __METHOD__, '1.31' );
1316  return ( NS_MEDIAWIKI == $this->mNamespace
1317  && ( $this->hasContentModel( CONTENT_MODEL_CSS )
1318  || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
1319  }
1320 
1327  public function isUserConfigPage() {
1328  return (
1329  $this->isUserCssConfigPage()
1330  || $this->isUserJsonConfigPage()
1331  || $this->isUserJsConfigPage()
1332  );
1333  }
1334 
1339  public function isCssJsSubpage() {
1340  wfDeprecated( __METHOD__, '1.31' );
1341  return ( NS_USER == $this->mNamespace && $this->isSubpage()
1342  && ( $this->hasContentModel( CONTENT_MODEL_CSS )
1343  || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
1344  }
1345 
1352  public function getSkinFromConfigSubpage() {
1353  $subpage = explode( '/', $this->mTextform );
1354  $subpage = $subpage[count( $subpage ) - 1];
1355  $lastdot = strrpos( $subpage, '.' );
1356  if ( $lastdot === false ) {
1357  return $subpage; # Never happens: only called for names ending in '.css'/'.json'/'.js'
1358  }
1359  return substr( $subpage, 0, $lastdot );
1360  }
1361 
1366  public function getSkinFromCssJsSubpage() {
1367  wfDeprecated( __METHOD__, '1.31' );
1368  return $this->getSkinFromConfigSubpage();
1369  }
1370 
1377  public function isUserCssConfigPage() {
1378  return (
1379  NS_USER == $this->mNamespace
1380  && $this->isSubpage()
1381  && $this->hasContentModel( CONTENT_MODEL_CSS )
1382  );
1383  }
1384 
1389  public function isCssSubpage() {
1390  wfDeprecated( __METHOD__, '1.31' );
1391  return $this->isUserCssConfigPage();
1392  }
1393 
1400  public function isUserJsonConfigPage() {
1401  return (
1402  NS_USER == $this->mNamespace
1403  && $this->isSubpage()
1404  && $this->hasContentModel( CONTENT_MODEL_JSON )
1405  );
1406  }
1407 
1414  public function isUserJsConfigPage() {
1415  return (
1416  NS_USER == $this->mNamespace
1417  && $this->isSubpage()
1419  );
1420  }
1421 
1426  public function isJsSubpage() {
1427  wfDeprecated( __METHOD__, '1.31' );
1428  return $this->isUserJsConfigPage();
1429  }
1430 
1437  public function isSiteCssConfigPage() {
1438  return (
1439  NS_MEDIAWIKI == $this->mNamespace
1440  && (
1442  // paranoia - a MediaWiki: namespace page with mismatching extension and content
1443  // model is probably by mistake and might get handled incorrectly (see e.g. T112937)
1444  || substr( $this->mDbkeyform, -4 ) === '.css'
1445  )
1446  );
1447  }
1448 
1455  public function isSiteJsonConfigPage() {
1456  return (
1457  NS_MEDIAWIKI == $this->mNamespace
1458  && (
1460  // paranoia - a MediaWiki: namespace page with mismatching extension and content
1461  // model is probably by mistake and might get handled incorrectly (see e.g. T112937)
1462  || substr( $this->mDbkeyform, -5 ) === '.json'
1463  )
1464  );
1465  }
1466 
1473  public function isSiteJsConfigPage() {
1474  return (
1475  NS_MEDIAWIKI == $this->mNamespace
1476  && (
1478  // paranoia - a MediaWiki: namespace page with mismatching extension and content
1479  // model is probably by mistake and might get handled incorrectly (see e.g. T112937)
1480  || substr( $this->mDbkeyform, -3 ) === '.js'
1481  )
1482  );
1483  }
1484 
1491  public function isRawHtmlMessage() {
1492  global $wgRawHtmlMessages;
1493 
1494  if ( !$this->inNamespace( NS_MEDIAWIKI ) ) {
1495  return false;
1496  }
1497  $message = lcfirst( $this->getRootTitle()->getDBkey() );
1498  return in_array( $message, $wgRawHtmlMessages, true );
1499  }
1500 
1506  public function isTalkPage() {
1507  return MWNamespace::isTalk( $this->mNamespace );
1508  }
1509 
1515  public function getTalkPage() {
1516  return self::makeTitle( MWNamespace::getTalk( $this->mNamespace ), $this->mDbkeyform );
1517  }
1518 
1528  public function getTalkPageIfDefined() {
1529  if ( !$this->canHaveTalkPage() ) {
1530  return null;
1531  }
1532 
1533  return $this->getTalkPage();
1534  }
1535 
1542  public function getSubjectPage() {
1543  // Is this the same title?
1544  $subjectNS = MWNamespace::getSubject( $this->mNamespace );
1545  if ( $this->mNamespace == $subjectNS ) {
1546  return $this;
1547  }
1548  return self::makeTitle( $subjectNS, $this->mDbkeyform );
1549  }
1550 
1559  public function getOtherPage() {
1560  if ( $this->isSpecialPage() ) {
1561  throw new MWException( 'Special pages cannot have other pages' );
1562  }
1563  if ( $this->isTalkPage() ) {
1564  return $this->getSubjectPage();
1565  } else {
1566  if ( !$this->canHaveTalkPage() ) {
1567  throw new MWException( "{$this->getPrefixedText()} does not have an other page" );
1568  }
1569  return $this->getTalkPage();
1570  }
1571  }
1572 
1578  public function getDefaultNamespace() {
1579  return $this->mDefaultNamespace;
1580  }
1581 
1589  public function getFragment() {
1590  return $this->mFragment;
1591  }
1592 
1599  public function hasFragment() {
1600  return $this->mFragment !== '';
1601  }
1602 
1608  public function getFragmentForURL() {
1609  if ( !$this->hasFragment() ) {
1610  return '';
1611  } elseif ( $this->isExternal()
1612  && !self::getInterwikiLookup()->fetch( $this->mInterwiki )->isLocal()
1613  ) {
1614  return '#' . Sanitizer::escapeIdForExternalInterwiki( $this->mFragment );
1615  }
1616  return '#' . Sanitizer::escapeIdForLink( $this->mFragment );
1617  }
1618 
1631  public function setFragment( $fragment ) {
1632  $this->mFragment = strtr( substr( $fragment, 1 ), '_', ' ' );
1633  }
1634 
1642  public function createFragmentTarget( $fragment ) {
1643  return self::makeTitle(
1644  $this->mNamespace,
1645  $this->getText(),
1646  $fragment,
1647  $this->mInterwiki
1648  );
1649  }
1650 
1658  private function prefix( $name ) {
1659  $p = '';
1660  if ( $this->isExternal() ) {
1661  $p = $this->mInterwiki . ':';
1662  }
1663 
1664  if ( $this->mNamespace != 0 ) {
1665  $nsText = $this->getNsText();
1666 
1667  if ( $nsText === false ) {
1668  // See T165149. Awkward, but better than erroneously linking to the main namespace.
1669  $nsText = MediaWikiServices::getInstance()->getContentLanguage()->
1670  getNsText( NS_SPECIAL ) . ":Badtitle/NS{$this->mNamespace}";
1671  }
1672 
1673  $p .= $nsText . ':';
1674  }
1675  return $p . $name;
1676  }
1677 
1684  public function getPrefixedDBkey() {
1685  $s = $this->prefix( $this->mDbkeyform );
1686  $s = strtr( $s, ' ', '_' );
1687  return $s;
1688  }
1689 
1696  public function getPrefixedText() {
1697  if ( $this->prefixedText === null ) {
1698  $s = $this->prefix( $this->mTextform );
1699  $s = strtr( $s, '_', ' ' );
1700  $this->prefixedText = $s;
1701  }
1702  return $this->prefixedText;
1703  }
1704 
1710  public function __toString() {
1711  return $this->getPrefixedText();
1712  }
1713 
1720  public function getFullText() {
1721  $text = $this->getPrefixedText();
1722  if ( $this->hasFragment() ) {
1723  $text .= '#' . $this->mFragment;
1724  }
1725  return $text;
1726  }
1727 
1740  public function getRootText() {
1741  if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1742  return $this->getText();
1743  }
1744 
1745  return strtok( $this->getText(), '/' );
1746  }
1747 
1760  public function getRootTitle() {
1761  return self::makeTitle( $this->mNamespace, $this->getRootText() );
1762  }
1763 
1775  public function getBaseText() {
1776  if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1777  return $this->getText();
1778  }
1779 
1780  $parts = explode( '/', $this->getText() );
1781  # Don't discard the real title if there's no subpage involved
1782  if ( count( $parts ) > 1 ) {
1783  unset( $parts[count( $parts ) - 1] );
1784  }
1785  return implode( '/', $parts );
1786  }
1787 
1800  public function getBaseTitle() {
1801  return self::makeTitle( $this->mNamespace, $this->getBaseText() );
1802  }
1803 
1815  public function getSubpageText() {
1816  if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1817  return $this->mTextform;
1818  }
1819  $parts = explode( '/', $this->mTextform );
1820  return $parts[count( $parts ) - 1];
1821  }
1822 
1836  public function getSubpage( $text ) {
1837  return self::makeTitleSafe( $this->mNamespace, $this->getText() . '/' . $text );
1838  }
1839 
1845  public function getSubpageUrlForm() {
1846  $text = $this->getSubpageText();
1847  $text = wfUrlencode( strtr( $text, ' ', '_' ) );
1848  return $text;
1849  }
1850 
1856  public function getPrefixedURL() {
1857  $s = $this->prefix( $this->mDbkeyform );
1858  $s = wfUrlencode( strtr( $s, ' ', '_' ) );
1859  return $s;
1860  }
1861 
1875  private static function fixUrlQueryArgs( $query, $query2 = false ) {
1876  if ( $query2 !== false ) {
1877  wfDeprecated( "Title::get{Canonical,Full,Link,Local,Internal}URL " .
1878  "method called with a second parameter is deprecated. Add your " .
1879  "parameter to an array passed as the first parameter.", "1.19" );
1880  }
1881  if ( is_array( $query ) ) {
1882  $query = wfArrayToCgi( $query );
1883  }
1884  if ( $query2 ) {
1885  if ( is_string( $query2 ) ) {
1886  // $query2 is a string, we will consider this to be
1887  // a deprecated $variant argument and add it to the query
1888  $query2 = wfArrayToCgi( [ 'variant' => $query2 ] );
1889  } else {
1890  $query2 = wfArrayToCgi( $query2 );
1891  }
1892  // If we have $query content add a & to it first
1893  if ( $query ) {
1894  $query .= '&';
1895  }
1896  // Now append the queries together
1897  $query .= $query2;
1898  }
1899  return $query;
1900  }
1901 
1913  public function getFullURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) {
1914  $query = self::fixUrlQueryArgs( $query, $query2 );
1915 
1916  # Hand off all the decisions on urls to getLocalURL
1917  $url = $this->getLocalURL( $query );
1918 
1919  # Expand the url to make it a full url. Note that getLocalURL has the
1920  # potential to output full urls for a variety of reasons, so we use
1921  # wfExpandUrl instead of simply prepending $wgServer
1922  $url = wfExpandUrl( $url, $proto );
1923 
1924  # Finally, add the fragment.
1925  $url .= $this->getFragmentForURL();
1926  // Avoid PHP 7.1 warning from passing $this by reference
1927  $titleRef = $this;
1928  Hooks::run( 'GetFullURL', [ &$titleRef, &$url, $query ] );
1929  return $url;
1930  }
1931 
1948  public function getFullUrlForRedirect( $query = '', $proto = PROTO_CURRENT ) {
1949  $target = $this;
1950  if ( $this->isExternal() ) {
1951  $target = SpecialPage::getTitleFor(
1952  'GoToInterwiki',
1953  $this->getPrefixedDBkey()
1954  );
1955  }
1956  return $target->getFullURL( $query, false, $proto );
1957  }
1958 
1982  public function getLocalURL( $query = '', $query2 = false ) {
1984 
1985  $query = self::fixUrlQueryArgs( $query, $query2 );
1986 
1987  $interwiki = self::getInterwikiLookup()->fetch( $this->mInterwiki );
1988  if ( $interwiki ) {
1989  $namespace = $this->getNsText();
1990  if ( $namespace != '' ) {
1991  # Can this actually happen? Interwikis shouldn't be parsed.
1992  # Yes! It can in interwiki transclusion. But... it probably shouldn't.
1993  $namespace .= ':';
1994  }
1995  $url = $interwiki->getURL( $namespace . $this->mDbkeyform );
1996  $url = wfAppendQuery( $url, $query );
1997  } else {
1998  $dbkey = wfUrlencode( $this->getPrefixedDBkey() );
1999  if ( $query == '' ) {
2000  $url = str_replace( '$1', $dbkey, $wgArticlePath );
2001  // Avoid PHP 7.1 warning from passing $this by reference
2002  $titleRef = $this;
2003  Hooks::run( 'GetLocalURL::Article', [ &$titleRef, &$url ] );
2004  } else {
2006  $url = false;
2007  $matches = [];
2008 
2009  if ( !empty( $wgActionPaths )
2010  && preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches )
2011  ) {
2012  $action = urldecode( $matches[2] );
2013  if ( isset( $wgActionPaths[$action] ) ) {
2014  $query = $matches[1];
2015  if ( isset( $matches[4] ) ) {
2016  $query .= $matches[4];
2017  }
2018  $url = str_replace( '$1', $dbkey, $wgActionPaths[$action] );
2019  if ( $query != '' ) {
2020  $url = wfAppendQuery( $url, $query );
2021  }
2022  }
2023  }
2024 
2025  if ( $url === false
2026  && $wgVariantArticlePath
2027  && preg_match( '/^variant=([^&]*)$/', $query, $matches )
2028  && $this->getPageLanguage()->equals(
2029  MediaWikiServices::getInstance()->getContentLanguage() )
2030  && $this->getPageLanguage()->hasVariants()
2031  ) {
2032  $variant = urldecode( $matches[1] );
2033  if ( $this->getPageLanguage()->hasVariant( $variant ) ) {
2034  // Only do the variant replacement if the given variant is a valid
2035  // variant for the page's language.
2036  $url = str_replace( '$2', urlencode( $variant ), $wgVariantArticlePath );
2037  $url = str_replace( '$1', $dbkey, $url );
2038  }
2039  }
2040 
2041  if ( $url === false ) {
2042  if ( $query == '-' ) {
2043  $query = '';
2044  }
2045  $url = "{$wgScript}?title={$dbkey}&{$query}";
2046  }
2047  }
2048  // Avoid PHP 7.1 warning from passing $this by reference
2049  $titleRef = $this;
2050  Hooks::run( 'GetLocalURL::Internal', [ &$titleRef, &$url, $query ] );
2051 
2052  // @todo FIXME: This causes breakage in various places when we
2053  // actually expected a local URL and end up with dupe prefixes.
2054  if ( $wgRequest->getVal( 'action' ) == 'render' ) {
2055  $url = $wgServer . $url;
2056  }
2057  }
2058  // Avoid PHP 7.1 warning from passing $this by reference
2059  $titleRef = $this;
2060  Hooks::run( 'GetLocalURL', [ &$titleRef, &$url, $query ] );
2061  return $url;
2062  }
2063 
2081  public function getLinkURL( $query = '', $query2 = false, $proto = false ) {
2082  if ( $this->isExternal() || $proto !== false ) {
2083  $ret = $this->getFullURL( $query, $query2, $proto );
2084  } elseif ( $this->getPrefixedText() === '' && $this->hasFragment() ) {
2085  $ret = $this->getFragmentForURL();
2086  } else {
2087  $ret = $this->getLocalURL( $query, $query2 ) . $this->getFragmentForURL();
2088  }
2089  return $ret;
2090  }
2091 
2106  public function getInternalURL( $query = '', $query2 = false ) {
2107  global $wgInternalServer, $wgServer;
2108  $query = self::fixUrlQueryArgs( $query, $query2 );
2109  $server = $wgInternalServer !== false ? $wgInternalServer : $wgServer;
2110  $url = wfExpandUrl( $server . $this->getLocalURL( $query ), PROTO_HTTP );
2111  // Avoid PHP 7.1 warning from passing $this by reference
2112  $titleRef = $this;
2113  Hooks::run( 'GetInternalURL', [ &$titleRef, &$url, $query ] );
2114  return $url;
2115  }
2116 
2130  public function getCanonicalURL( $query = '', $query2 = false ) {
2131  $query = self::fixUrlQueryArgs( $query, $query2 );
2132  $url = wfExpandUrl( $this->getLocalURL( $query ) . $this->getFragmentForURL(), PROTO_CANONICAL );
2133  // Avoid PHP 7.1 warning from passing $this by reference
2134  $titleRef = $this;
2135  Hooks::run( 'GetCanonicalURL', [ &$titleRef, &$url, $query ] );
2136  return $url;
2137  }
2138 
2144  public function getEditURL() {
2145  if ( $this->isExternal() ) {
2146  return '';
2147  }
2148  $s = $this->getLocalURL( 'action=edit' );
2149 
2150  return $s;
2151  }
2152 
2167  public function quickUserCan( $action, $user = null ) {
2168  return $this->userCan( $action, $user, false );
2169  }
2170 
2180  public function userCan( $action, $user = null, $rigor = 'secure' ) {
2181  if ( !$user instanceof User ) {
2182  global $wgUser;
2183  $user = $wgUser;
2184  }
2185 
2186  return !count( $this->getUserPermissionsErrorsInternal( $action, $user, $rigor, true ) );
2187  }
2188 
2204  public function getUserPermissionsErrors(
2205  $action, $user, $rigor = 'secure', $ignoreErrors = []
2206  ) {
2207  $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $rigor );
2208 
2209  // Remove the errors being ignored.
2210  foreach ( $errors as $index => $error ) {
2211  $errKey = is_array( $error ) ? $error[0] : $error;
2212 
2213  if ( in_array( $errKey, $ignoreErrors ) ) {
2214  unset( $errors[$index] );
2215  }
2216  if ( $errKey instanceof MessageSpecifier && in_array( $errKey->getKey(), $ignoreErrors ) ) {
2217  unset( $errors[$index] );
2218  }
2219  }
2220 
2221  return $errors;
2222  }
2223 
2235  private function checkQuickPermissions( $action, $user, $errors, $rigor, $short ) {
2236  if ( !Hooks::run( 'TitleQuickPermissions',
2237  [ $this, $user, $action, &$errors, ( $rigor !== 'quick' ), $short ] )
2238  ) {
2239  return $errors;
2240  }
2241 
2242  if ( $action == 'create' ) {
2243  if (
2244  ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
2245  ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) )
2246  ) {
2247  $errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ 'nocreate-loggedin' ];
2248  }
2249  } elseif ( $action == 'move' ) {
2250  if ( !$user->isAllowed( 'move-rootuserpages' )
2251  && $this->mNamespace == NS_USER && !$this->isSubpage() ) {
2252  // Show user page-specific message only if the user can move other pages
2253  $errors[] = [ 'cant-move-user-page' ];
2254  }
2255 
2256  // Check if user is allowed to move files if it's a file
2257  if ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) {
2258  $errors[] = [ 'movenotallowedfile' ];
2259  }
2260 
2261  // Check if user is allowed to move category pages if it's a category page
2262  if ( $this->mNamespace == NS_CATEGORY && !$user->isAllowed( 'move-categorypages' ) ) {
2263  $errors[] = [ 'cant-move-category-page' ];
2264  }
2265 
2266  if ( !$user->isAllowed( 'move' ) ) {
2267  // User can't move anything
2268  $userCanMove = User::groupHasPermission( 'user', 'move' );
2269  $autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' );
2270  if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
2271  // custom message if logged-in users without any special rights can move
2272  $errors[] = [ 'movenologintext' ];
2273  } else {
2274  $errors[] = [ 'movenotallowed' ];
2275  }
2276  }
2277  } elseif ( $action == 'move-target' ) {
2278  if ( !$user->isAllowed( 'move' ) ) {
2279  // User can't move anything
2280  $errors[] = [ 'movenotallowed' ];
2281  } elseif ( !$user->isAllowed( 'move-rootuserpages' )
2282  && $this->mNamespace == NS_USER && !$this->isSubpage() ) {
2283  // Show user page-specific message only if the user can move other pages
2284  $errors[] = [ 'cant-move-to-user-page' ];
2285  } elseif ( !$user->isAllowed( 'move-categorypages' )
2286  && $this->mNamespace == NS_CATEGORY ) {
2287  // Show category page-specific message only if the user can move other pages
2288  $errors[] = [ 'cant-move-to-category-page' ];
2289  }
2290  } elseif ( !$user->isAllowed( $action ) ) {
2291  $errors[] = $this->missingPermissionError( $action, $short );
2292  }
2293 
2294  return $errors;
2295  }
2296 
2305  private function resultToError( $errors, $result ) {
2306  if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
2307  // A single array representing an error
2308  $errors[] = $result;
2309  } elseif ( is_array( $result ) && is_array( $result[0] ) ) {
2310  // A nested array representing multiple errors
2311  $errors = array_merge( $errors, $result );
2312  } elseif ( $result !== '' && is_string( $result ) ) {
2313  // A string representing a message-id
2314  $errors[] = [ $result ];
2315  } elseif ( $result instanceof MessageSpecifier ) {
2316  // A message specifier representing an error
2317  $errors[] = [ $result ];
2318  } elseif ( $result === false ) {
2319  // a generic "We don't want them to do that"
2320  $errors[] = [ 'badaccess-group0' ];
2321  }
2322  return $errors;
2323  }
2324 
2336  private function checkPermissionHooks( $action, $user, $errors, $rigor, $short ) {
2337  // Use getUserPermissionsErrors instead
2338  $result = '';
2339  // Avoid PHP 7.1 warning from passing $this by reference
2340  $titleRef = $this;
2341  if ( !Hooks::run( 'userCan', [ &$titleRef, &$user, $action, &$result ] ) ) {
2342  return $result ? [] : [ [ 'badaccess-group0' ] ];
2343  }
2344  // Check getUserPermissionsErrors hook
2345  // Avoid PHP 7.1 warning from passing $this by reference
2346  $titleRef = $this;
2347  if ( !Hooks::run( 'getUserPermissionsErrors', [ &$titleRef, &$user, $action, &$result ] ) ) {
2348  $errors = $this->resultToError( $errors, $result );
2349  }
2350  // Check getUserPermissionsErrorsExpensive hook
2351  if (
2352  $rigor !== 'quick'
2353  && !( $short && count( $errors ) > 0 )
2354  && !Hooks::run( 'getUserPermissionsErrorsExpensive', [ &$titleRef, &$user, $action, &$result ] )
2355  ) {
2356  $errors = $this->resultToError( $errors, $result );
2357  }
2358 
2359  return $errors;
2360  }
2361 
2373  private function checkSpecialsAndNSPermissions( $action, $user, $errors, $rigor, $short ) {
2374  # Only 'createaccount' can be performed on special pages,
2375  # which don't actually exist in the DB.
2376  if ( $this->isSpecialPage() && $action !== 'createaccount' ) {
2377  $errors[] = [ 'ns-specialprotected' ];
2378  }
2379 
2380  # Check $wgNamespaceProtection for restricted namespaces
2381  if ( $this->isNamespaceProtected( $user ) ) {
2382  $ns = $this->mNamespace == NS_MAIN ?
2383  wfMessage( 'nstab-main' )->text() : $this->getNsText();
2384  $errors[] = $this->mNamespace == NS_MEDIAWIKI ?
2385  [ 'protectedinterface', $action ] : [ 'namespaceprotected', $ns, $action ];
2386  }
2387 
2388  return $errors;
2389  }
2390 
2402  private function checkSiteConfigPermissions( $action, $user, $errors, $rigor, $short ) {
2403  if ( $action != 'patrol' ) {
2404  $error = null;
2405  // Sitewide CSS/JSON/JS changes, like all NS_MEDIAWIKI changes, also require the
2406  // editinterface right. That's implemented as a restriction so no check needed here.
2407  if ( $this->isSiteCssConfigPage() && !$user->isAllowed( 'editsitecss' ) ) {
2408  $error = [ 'sitecssprotected', $action ];
2409  } elseif ( $this->isSiteJsonConfigPage() && !$user->isAllowed( 'editsitejson' ) ) {
2410  $error = [ 'sitejsonprotected', $action ];
2411  } elseif ( $this->isSiteJsConfigPage() && !$user->isAllowed( 'editsitejs' ) ) {
2412  $error = [ 'sitejsprotected', $action ];
2413  } elseif ( $this->isRawHtmlMessage() ) {
2414  // Raw HTML can be used to deploy CSS or JS so require rights for both.
2415  if ( !$user->isAllowed( 'editsitejs' ) ) {
2416  $error = [ 'sitejsprotected', $action ];
2417  } elseif ( !$user->isAllowed( 'editsitecss' ) ) {
2418  $error = [ 'sitecssprotected', $action ];
2419  }
2420  }
2421 
2422  if ( $error ) {
2423  if ( $user->isAllowed( 'editinterface' ) ) {
2424  // Most users / site admins will probably find out about the new, more restrictive
2425  // permissions by failing to edit something. Give them more info.
2426  // TODO remove this a few release cycles after 1.32
2427  $error = [ 'interfaceadmin-info', wfMessage( $error[0], $error[1] ) ];
2428  }
2429  $errors[] = $error;
2430  }
2431  }
2432 
2433  return $errors;
2434  }
2435 
2447  private function checkUserConfigPermissions( $action, $user, $errors, $rigor, $short ) {
2448  # Protect css/json/js subpages of user pages
2449  # XXX: this might be better using restrictions
2450 
2451  if ( $action === 'patrol' ) {
2452  return $errors;
2453  }
2454 
2455  if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) {
2456  // Users need editmyuser* to edit their own CSS/JSON/JS subpages.
2457  if (
2458  $this->isUserCssConfigPage()
2459  && !$user->isAllowedAny( 'editmyusercss', 'editusercss' )
2460  ) {
2461  $errors[] = [ 'mycustomcssprotected', $action ];
2462  } elseif (
2463  $this->isUserJsonConfigPage()
2464  && !$user->isAllowedAny( 'editmyuserjson', 'edituserjson' )
2465  ) {
2466  $errors[] = [ 'mycustomjsonprotected', $action ];
2467  } elseif (
2468  $this->isUserJsConfigPage()
2469  && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' )
2470  ) {
2471  $errors[] = [ 'mycustomjsprotected', $action ];
2472  }
2473  } else {
2474  // Users need editmyuser* to edit their own CSS/JSON/JS subpages, except for
2475  // deletion/suppression which cannot be used for attacks and we want to avoid the
2476  // situation where an unprivileged user can post abusive content on their subpages
2477  // and only very highly privileged users could remove it.
2478  if ( !in_array( $action, [ 'delete', 'deleterevision', 'suppressrevision' ], true ) ) {
2479  if (
2480  $this->isUserCssConfigPage()
2481  && !$user->isAllowed( 'editusercss' )
2482  ) {
2483  $errors[] = [ 'customcssprotected', $action ];
2484  } elseif (
2485  $this->isUserJsonConfigPage()
2486  && !$user->isAllowed( 'edituserjson' )
2487  ) {
2488  $errors[] = [ 'customjsonprotected', $action ];
2489  } elseif (
2490  $this->isUserJsConfigPage()
2491  && !$user->isAllowed( 'edituserjs' )
2492  ) {
2493  $errors[] = [ 'customjsprotected', $action ];
2494  }
2495  }
2496  }
2497 
2498  return $errors;
2499  }
2500 
2514  private function checkPageRestrictions( $action, $user, $errors, $rigor, $short ) {
2515  foreach ( $this->getRestrictions( $action ) as $right ) {
2516  // Backwards compatibility, rewrite sysop -> editprotected
2517  if ( $right == 'sysop' ) {
2518  $right = 'editprotected';
2519  }
2520  // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
2521  if ( $right == 'autoconfirmed' ) {
2522  $right = 'editsemiprotected';
2523  }
2524  if ( $right == '' ) {
2525  continue;
2526  }
2527  if ( !$user->isAllowed( $right ) ) {
2528  $errors[] = [ 'protectedpagetext', $right, $action ];
2529  } elseif ( $this->mCascadeRestriction && !$user->isAllowed( 'protect' ) ) {
2530  $errors[] = [ 'protectedpagetext', 'protect', $action ];
2531  }
2532  }
2533 
2534  return $errors;
2535  }
2536 
2548  private function checkCascadingSourcesRestrictions( $action, $user, $errors, $rigor, $short ) {
2549  if ( $rigor !== 'quick' && !$this->isUserConfigPage() ) {
2550  # We /could/ use the protection level on the source page, but it's
2551  # fairly ugly as we have to establish a precedence hierarchy for pages
2552  # included by multiple cascade-protected pages. So just restrict
2553  # it to people with 'protect' permission, as they could remove the
2554  # protection anyway.
2555  list( $cascadingSources, $restrictions ) = $this->getCascadeProtectionSources();
2556  # Cascading protection depends on more than this page...
2557  # Several cascading protected pages may include this page...
2558  # Check each cascading level
2559  # This is only for protection restrictions, not for all actions
2560  if ( isset( $restrictions[$action] ) ) {
2561  foreach ( $restrictions[$action] as $right ) {
2562  // Backwards compatibility, rewrite sysop -> editprotected
2563  if ( $right == 'sysop' ) {
2564  $right = 'editprotected';
2565  }
2566  // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
2567  if ( $right == 'autoconfirmed' ) {
2568  $right = 'editsemiprotected';
2569  }
2570  if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) {
2571  $pages = '';
2572  foreach ( $cascadingSources as $page ) {
2573  $pages .= '* [[:' . $page->getPrefixedText() . "]]\n";
2574  }
2575  $errors[] = [ 'cascadeprotected', count( $cascadingSources ), $pages, $action ];
2576  }
2577  }
2578  }
2579  }
2580 
2581  return $errors;
2582  }
2583 
2595  private function checkActionPermissions( $action, $user, $errors, $rigor, $short ) {
2597 
2598  if ( $action == 'protect' ) {
2599  if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $rigor, true ) ) ) {
2600  // If they can't edit, they shouldn't protect.
2601  $errors[] = [ 'protect-cantedit' ];
2602  }
2603  } elseif ( $action == 'create' ) {
2604  $title_protection = $this->getTitleProtection();
2605  if ( $title_protection ) {
2606  if ( $title_protection['permission'] == ''
2607  || !$user->isAllowed( $title_protection['permission'] )
2608  ) {
2609  $errors[] = [
2610  'titleprotected',
2611  User::whoIs( $title_protection['user'] ),
2612  $title_protection['reason']
2613  ];
2614  }
2615  }
2616  } elseif ( $action == 'move' ) {
2617  // Check for immobile pages
2618  if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
2619  // Specific message for this case
2620  $errors[] = [ 'immobile-source-namespace', $this->getNsText() ];
2621  } elseif ( !$this->isMovable() ) {
2622  // Less specific message for rarer cases
2623  $errors[] = [ 'immobile-source-page' ];
2624  }
2625  } elseif ( $action == 'move-target' ) {
2626  if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
2627  $errors[] = [ 'immobile-target-namespace', $this->getNsText() ];
2628  } elseif ( !$this->isMovable() ) {
2629  $errors[] = [ 'immobile-target-page' ];
2630  }
2631  } elseif ( $action == 'delete' ) {
2632  $tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true );
2633  if ( !$tempErrors ) {
2634  $tempErrors = $this->checkCascadingSourcesRestrictions( 'edit',
2635  $user, $tempErrors, $rigor, true );
2636  }
2637  if ( $tempErrors ) {
2638  // If protection keeps them from editing, they shouldn't be able to delete.
2639  $errors[] = [ 'deleteprotected' ];
2640  }
2641  if ( $rigor !== 'quick' && $wgDeleteRevisionsLimit
2642  && !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion()
2643  ) {
2644  $errors[] = [ 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ];
2645  }
2646  } elseif ( $action === 'undelete' ) {
2647  if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $rigor, true ) ) ) {
2648  // Undeleting implies editing
2649  $errors[] = [ 'undelete-cantedit' ];
2650  }
2651  if ( !$this->exists()
2652  && count( $this->getUserPermissionsErrorsInternal( 'create', $user, $rigor, true ) )
2653  ) {
2654  // Undeleting where nothing currently exists implies creating
2655  $errors[] = [ 'undelete-cantcreate' ];
2656  }
2657  }
2658  return $errors;
2659  }
2660 
2672  private function checkUserBlock( $action, $user, $errors, $rigor, $short ) {
2674  // Account creation blocks handled at userlogin.
2675  // Unblocking handled in SpecialUnblock
2676  if ( $rigor === 'quick' || in_array( $action, [ 'createaccount', 'unblock' ] ) ) {
2677  return $errors;
2678  }
2679 
2680  // Optimize for a very common case
2681  if ( $action === 'read' && !$wgBlockDisablesLogin ) {
2682  return $errors;
2683  }
2684 
2685  if ( $wgEmailConfirmToEdit
2686  && !$user->isEmailConfirmed()
2687  && $action === 'edit'
2688  ) {
2689  $errors[] = [ 'confirmedittext' ];
2690  }
2691 
2692  $useReplica = ( $rigor !== 'secure' );
2693  if ( ( $action == 'edit' || $action == 'create' )
2694  && !$user->isBlockedFrom( $this, $useReplica )
2695  ) {
2696  // Don't block the user from editing their own talk page unless they've been
2697  // explicitly blocked from that too.
2698  } elseif ( $user->isBlocked() && $user->getBlock()->prevents( $action ) !== false ) {
2699  // @todo FIXME: Pass the relevant context into this function.
2700  $errors[] = $user->getBlock()->getPermissionsError( RequestContext::getMain() );
2701  }
2702 
2703  return $errors;
2704  }
2705 
2717  private function checkReadPermissions( $action, $user, $errors, $rigor, $short ) {
2719 
2720  $whitelisted = false;
2721  if ( User::isEveryoneAllowed( 'read' ) ) {
2722  # Shortcut for public wikis, allows skipping quite a bit of code
2723  $whitelisted = true;
2724  } elseif ( $user->isAllowed( 'read' ) ) {
2725  # If the user is allowed to read pages, he is allowed to read all pages
2726  $whitelisted = true;
2727  } elseif ( $this->isSpecial( 'Userlogin' )
2728  || $this->isSpecial( 'PasswordReset' )
2729  || $this->isSpecial( 'Userlogout' )
2730  ) {
2731  # Always grant access to the login page.
2732  # Even anons need to be able to log in.
2733  $whitelisted = true;
2734  } elseif ( is_array( $wgWhitelistRead ) && count( $wgWhitelistRead ) ) {
2735  # Time to check the whitelist
2736  # Only do these checks is there's something to check against
2737  $name = $this->getPrefixedText();
2738  $dbName = $this->getPrefixedDBkey();
2739 
2740  // Check for explicit whitelisting with and without underscores
2741  if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) {
2742  $whitelisted = true;
2743  } elseif ( $this->mNamespace == NS_MAIN ) {
2744  # Old settings might have the title prefixed with
2745  # a colon for main-namespace pages
2746  if ( in_array( ':' . $name, $wgWhitelistRead ) ) {
2747  $whitelisted = true;
2748  }
2749  } elseif ( $this->isSpecialPage() ) {
2750  # If it's a special page, ditch the subpage bit and check again
2752  list( $name, /* $subpage */ ) =
2753  MediaWikiServices::getInstance()->getSpecialPageFactory()->
2754  resolveAlias( $name );
2755  if ( $name ) {
2756  $pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
2757  if ( in_array( $pure, $wgWhitelistRead, true ) ) {
2758  $whitelisted = true;
2759  }
2760  }
2761  }
2762  }
2763 
2764  if ( !$whitelisted && is_array( $wgWhitelistReadRegexp ) && !empty( $wgWhitelistReadRegexp ) ) {
2765  $name = $this->getPrefixedText();
2766  // Check for regex whitelisting
2767  foreach ( $wgWhitelistReadRegexp as $listItem ) {
2768  if ( preg_match( $listItem, $name ) ) {
2769  $whitelisted = true;
2770  break;
2771  }
2772  }
2773  }
2774 
2775  if ( !$whitelisted ) {
2776  # If the title is not whitelisted, give extensions a chance to do so...
2777  Hooks::run( 'TitleReadWhitelist', [ $this, $user, &$whitelisted ] );
2778  if ( !$whitelisted ) {
2779  $errors[] = $this->missingPermissionError( $action, $short );
2780  }
2781  }
2782 
2783  return $errors;
2784  }
2785 
2794  private function missingPermissionError( $action, $short ) {
2795  // We avoid expensive display logic for quickUserCan's and such
2796  if ( $short ) {
2797  return [ 'badaccess-group0' ];
2798  }
2799 
2800  return User::newFatalPermissionDeniedStatus( $action )->getErrorsArray()[0];
2801  }
2802 
2818  $action, $user, $rigor = 'secure', $short = false
2819  ) {
2820  if ( $rigor === true ) {
2821  $rigor = 'secure'; // b/c
2822  } elseif ( $rigor === false ) {
2823  $rigor = 'quick'; // b/c
2824  } elseif ( !in_array( $rigor, [ 'quick', 'full', 'secure' ] ) ) {
2825  throw new Exception( "Invalid rigor parameter '$rigor'." );
2826  }
2827 
2828  # Read has special handling
2829  if ( $action == 'read' ) {
2830  $checks = [
2831  'checkPermissionHooks',
2832  'checkReadPermissions',
2833  'checkUserBlock', // for wgBlockDisablesLogin
2834  ];
2835  # Don't call checkSpecialsAndNSPermissions, checkSiteConfigPermissions
2836  # or checkUserConfigPermissions here as it will lead to duplicate
2837  # error messages. This is okay to do since anywhere that checks for
2838  # create will also check for edit, and those checks are called for edit.
2839  } elseif ( $action == 'create' ) {
2840  $checks = [
2841  'checkQuickPermissions',
2842  'checkPermissionHooks',
2843  'checkPageRestrictions',
2844  'checkCascadingSourcesRestrictions',
2845  'checkActionPermissions',
2846  'checkUserBlock'
2847  ];
2848  } else {
2849  $checks = [
2850  'checkQuickPermissions',
2851  'checkPermissionHooks',
2852  'checkSpecialsAndNSPermissions',
2853  'checkSiteConfigPermissions',
2854  'checkUserConfigPermissions',
2855  'checkPageRestrictions',
2856  'checkCascadingSourcesRestrictions',
2857  'checkActionPermissions',
2858  'checkUserBlock'
2859  ];
2860  }
2861 
2862  $errors = [];
2863  while ( count( $checks ) > 0 &&
2864  !( $short && count( $errors ) > 0 ) ) {
2865  $method = array_shift( $checks );
2866  $errors = $this->$method( $action, $user, $errors, $rigor, $short );
2867  }
2868 
2869  return $errors;
2870  }
2871 
2879  public static function getFilteredRestrictionTypes( $exists = true ) {
2880  global $wgRestrictionTypes;
2881  $types = $wgRestrictionTypes;
2882  if ( $exists ) {
2883  # Remove the create restriction for existing titles
2884  $types = array_diff( $types, [ 'create' ] );
2885  } else {
2886  # Only the create and upload restrictions apply to non-existing titles
2887  $types = array_intersect( $types, [ 'create', 'upload' ] );
2888  }
2889  return $types;
2890  }
2891 
2897  public function getRestrictionTypes() {
2898  if ( $this->isSpecialPage() ) {
2899  return [];
2900  }
2901 
2902  $types = self::getFilteredRestrictionTypes( $this->exists() );
2903 
2904  if ( $this->mNamespace != NS_FILE ) {
2905  # Remove the upload restriction for non-file titles
2906  $types = array_diff( $types, [ 'upload' ] );
2907  }
2908 
2909  Hooks::run( 'TitleGetRestrictionTypes', [ $this, &$types ] );
2910 
2911  wfDebug( __METHOD__ . ': applicable restrictions to [[' .
2912  $this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" );
2913 
2914  return $types;
2915  }
2916 
2924  public function getTitleProtection() {
2925  $protection = $this->getTitleProtectionInternal();
2926  if ( $protection ) {
2927  if ( $protection['permission'] == 'sysop' ) {
2928  $protection['permission'] = 'editprotected'; // B/C
2929  }
2930  if ( $protection['permission'] == 'autoconfirmed' ) {
2931  $protection['permission'] = 'editsemiprotected'; // B/C
2932  }
2933  }
2934  return $protection;
2935  }
2936 
2947  protected function getTitleProtectionInternal() {
2948  // Can't protect pages in special namespaces
2949  if ( $this->mNamespace < 0 ) {
2950  return false;
2951  }
2952 
2953  // Can't protect pages that exist.
2954  if ( $this->exists() ) {
2955  return false;
2956  }
2957 
2958  if ( $this->mTitleProtection === null ) {
2959  $dbr = wfGetDB( DB_REPLICA );
2960  $commentStore = CommentStore::getStore();
2961  $commentQuery = $commentStore->getJoin( 'pt_reason' );
2962  $res = $dbr->select(
2963  [ 'protected_titles' ] + $commentQuery['tables'],
2964  [
2965  'user' => 'pt_user',
2966  'expiry' => 'pt_expiry',
2967  'permission' => 'pt_create_perm'
2968  ] + $commentQuery['fields'],
2969  [ 'pt_namespace' => $this->mNamespace, 'pt_title' => $this->mDbkeyform ],
2970  __METHOD__,
2971  [],
2972  $commentQuery['joins']
2973  );
2974 
2975  // fetchRow returns false if there are no rows.
2976  $row = $dbr->fetchRow( $res );
2977  if ( $row ) {
2978  $this->mTitleProtection = [
2979  'user' => $row['user'],
2980  'expiry' => $dbr->decodeExpiry( $row['expiry'] ),
2981  'permission' => $row['permission'],
2982  'reason' => $commentStore->getComment( 'pt_reason', $row )->text,
2983  ];
2984  } else {
2985  $this->mTitleProtection = false;
2986  }
2987  }
2988  return $this->mTitleProtection;
2989  }
2990 
2994  public function deleteTitleProtection() {
2995  $dbw = wfGetDB( DB_MASTER );
2996 
2997  $dbw->delete(
2998  'protected_titles',
2999  [ 'pt_namespace' => $this->mNamespace, 'pt_title' => $this->mDbkeyform ],
3000  __METHOD__
3001  );
3002  $this->mTitleProtection = false;
3003  }
3004 
3012  public function isSemiProtected( $action = 'edit' ) {
3014 
3015  $restrictions = $this->getRestrictions( $action );
3017  if ( !$restrictions || !$semi ) {
3018  // Not protected, or all protection is full protection
3019  return false;
3020  }
3021 
3022  // Remap autoconfirmed to editsemiprotected for BC
3023  foreach ( array_keys( $semi, 'autoconfirmed' ) as $key ) {
3024  $semi[$key] = 'editsemiprotected';
3025  }
3026  foreach ( array_keys( $restrictions, 'autoconfirmed' ) as $key ) {
3027  $restrictions[$key] = 'editsemiprotected';
3028  }
3029 
3030  return !array_diff( $restrictions, $semi );
3031  }
3032 
3040  public function isProtected( $action = '' ) {
3041  global $wgRestrictionLevels;
3042 
3043  $restrictionTypes = $this->getRestrictionTypes();
3044 
3045  # Special pages have inherent protection
3046  if ( $this->isSpecialPage() ) {
3047  return true;
3048  }
3049 
3050  # Check regular protection levels
3051  foreach ( $restrictionTypes as $type ) {
3052  if ( $action == $type || $action == '' ) {
3053  $r = $this->getRestrictions( $type );
3054  foreach ( $wgRestrictionLevels as $level ) {
3055  if ( in_array( $level, $r ) && $level != '' ) {
3056  return true;
3057  }
3058  }
3059  }
3060  }
3061 
3062  return false;
3063  }
3064 
3072  public function isNamespaceProtected( User $user ) {
3073  global $wgNamespaceProtection;
3074 
3075  if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) {
3076  foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) {
3077  if ( $right != '' && !$user->isAllowed( $right ) ) {
3078  return true;
3079  }
3080  }
3081  }
3082  return false;
3083  }
3084 
3090  public function isCascadeProtected() {
3091  list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false );
3092  return ( $sources > 0 );
3093  }
3094 
3104  public function areCascadeProtectionSourcesLoaded( $getPages = true ) {
3105  return $getPages ? $this->mCascadeSources !== null : $this->mHasCascadingRestrictions !== null;
3106  }
3107 
3121  public function getCascadeProtectionSources( $getPages = true ) {
3122  $pagerestrictions = [];
3123 
3124  if ( $this->mCascadeSources !== null && $getPages ) {
3126  } elseif ( $this->mHasCascadingRestrictions !== null && !$getPages ) {
3127  return [ $this->mHasCascadingRestrictions, $pagerestrictions ];
3128  }
3129 
3130  $dbr = wfGetDB( DB_REPLICA );
3131 
3132  if ( $this->mNamespace == NS_FILE ) {
3133  $tables = [ 'imagelinks', 'page_restrictions' ];
3134  $where_clauses = [
3135  'il_to' => $this->mDbkeyform,
3136  'il_from=pr_page',
3137  'pr_cascade' => 1
3138  ];
3139  } else {
3140  $tables = [ 'templatelinks', 'page_restrictions' ];
3141  $where_clauses = [
3142  'tl_namespace' => $this->mNamespace,
3143  'tl_title' => $this->mDbkeyform,
3144  'tl_from=pr_page',
3145  'pr_cascade' => 1
3146  ];
3147  }
3148 
3149  if ( $getPages ) {
3150  $cols = [ 'pr_page', 'page_namespace', 'page_title',
3151  'pr_expiry', 'pr_type', 'pr_level' ];
3152  $where_clauses[] = 'page_id=pr_page';
3153  $tables[] = 'page';
3154  } else {
3155  $cols = [ 'pr_expiry' ];
3156  }
3157 
3158  $res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ );
3159 
3160  $sources = $getPages ? [] : false;
3161  $now = wfTimestampNow();
3162 
3163  foreach ( $res as $row ) {
3164  $expiry = $dbr->decodeExpiry( $row->pr_expiry );
3165  if ( $expiry > $now ) {
3166  if ( $getPages ) {
3167  $page_id = $row->pr_page;
3168  $page_ns = $row->page_namespace;
3169  $page_title = $row->page_title;
3170  $sources[$page_id] = self::makeTitle( $page_ns, $page_title );
3171  # Add groups needed for each restriction type if its not already there
3172  # Make sure this restriction type still exists
3173 
3174  if ( !isset( $pagerestrictions[$row->pr_type] ) ) {
3175  $pagerestrictions[$row->pr_type] = [];
3176  }
3177 
3178  if (
3179  isset( $pagerestrictions[$row->pr_type] )
3180  && !in_array( $row->pr_level, $pagerestrictions[$row->pr_type] )
3181  ) {
3182  $pagerestrictions[$row->pr_type][] = $row->pr_level;
3183  }
3184  } else {
3185  $sources = true;
3186  }
3187  }
3188  }
3189 
3190  if ( $getPages ) {
3191  $this->mCascadeSources = $sources;
3192  $this->mCascadingRestrictions = $pagerestrictions;
3193  } else {
3194  $this->mHasCascadingRestrictions = $sources;
3195  }
3196 
3197  return [ $sources, $pagerestrictions ];
3198  }
3199 
3207  public function areRestrictionsLoaded() {
3209  }
3210 
3220  public function getRestrictions( $action ) {
3221  if ( !$this->mRestrictionsLoaded ) {
3222  $this->loadRestrictions();
3223  }
3224  return $this->mRestrictions[$action] ?? [];
3225  }
3226 
3234  public function getAllRestrictions() {
3235  if ( !$this->mRestrictionsLoaded ) {
3236  $this->loadRestrictions();
3237  }
3238  return $this->mRestrictions;
3239  }
3240 
3248  public function getRestrictionExpiry( $action ) {
3249  if ( !$this->mRestrictionsLoaded ) {
3250  $this->loadRestrictions();
3251  }
3252  return $this->mRestrictionsExpiry[$action] ?? false;
3253  }
3254 
3261  if ( !$this->mRestrictionsLoaded ) {
3262  $this->loadRestrictions();
3263  }
3264 
3266  }
3267 
3279  public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) {
3280  $dbr = wfGetDB( DB_REPLICA );
3281 
3282  $restrictionTypes = $this->getRestrictionTypes();
3283 
3284  foreach ( $restrictionTypes as $type ) {
3285  $this->mRestrictions[$type] = [];
3286  $this->mRestrictionsExpiry[$type] = 'infinity';
3287  }
3288 
3289  $this->mCascadeRestriction = false;
3290 
3291  # Backwards-compatibility: also load the restrictions from the page record (old format).
3292  if ( $oldFashionedRestrictions !== null ) {
3293  $this->mOldRestrictions = $oldFashionedRestrictions;
3294  }
3295 
3296  if ( $this->mOldRestrictions === false ) {
3297  $this->mOldRestrictions = $dbr->selectField( 'page', 'page_restrictions',
3298  [ 'page_id' => $this->getArticleID() ], __METHOD__ );
3299  }
3300 
3301  if ( $this->mOldRestrictions != '' ) {
3302  foreach ( explode( ':', trim( $this->mOldRestrictions ) ) as $restrict ) {
3303  $temp = explode( '=', trim( $restrict ) );
3304  if ( count( $temp ) == 1 ) {
3305  // old old format should be treated as edit/move restriction
3306  $this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) );
3307  $this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) );
3308  } else {
3309  $restriction = trim( $temp[1] );
3310  if ( $restriction != '' ) { // some old entries are empty
3311  $this->mRestrictions[$temp[0]] = explode( ',', $restriction );
3312  }
3313  }
3314  }
3315  }
3316 
3317  if ( count( $rows ) ) {
3318  # Current system - load second to make them override.
3319  $now = wfTimestampNow();
3320 
3321  # Cycle through all the restrictions.
3322  foreach ( $rows as $row ) {
3323  // Don't take care of restrictions types that aren't allowed
3324  if ( !in_array( $row->pr_type, $restrictionTypes ) ) {
3325  continue;
3326  }
3327 
3328  $expiry = $dbr->decodeExpiry( $row->pr_expiry );
3329 
3330  // Only apply the restrictions if they haven't expired!
3331  if ( !$expiry || $expiry > $now ) {
3332  $this->mRestrictionsExpiry[$row->pr_type] = $expiry;
3333  $this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) );
3334 
3335  $this->mCascadeRestriction |= $row->pr_cascade;
3336  }
3337  }
3338  }
3339 
3340  $this->mRestrictionsLoaded = true;
3341  }
3342 
3351  public function loadRestrictions( $oldFashionedRestrictions = null ) {
3352  if ( $this->mRestrictionsLoaded ) {
3353  return;
3354  }
3355 
3356  $id = $this->getArticleID();
3357  if ( $id ) {
3359  $fname = __METHOD__;
3360  $rows = $cache->getWithSetCallback(
3361  // Page protections always leave a new null revision
3362  $cache->makeKey( 'page-restrictions', $id, $this->getLatestRevID() ),
3363  $cache::TTL_DAY,
3364  function ( $curValue, &$ttl, array &$setOpts ) use ( $fname ) {
3365  $dbr = wfGetDB( DB_REPLICA );
3366 
3367  $setOpts += Database::getCacheSetOptions( $dbr );
3368 
3369  return iterator_to_array(
3370  $dbr->select(
3371  'page_restrictions',
3372  [ 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ],
3373  [ 'pr_page' => $this->getArticleID() ],
3374  $fname
3375  )
3376  );
3377  }
3378  );
3379 
3380  $this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions );
3381  } else {
3382  $title_protection = $this->getTitleProtectionInternal();
3383 
3384  if ( $title_protection ) {
3385  $now = wfTimestampNow();
3386  $expiry = wfGetDB( DB_REPLICA )->decodeExpiry( $title_protection['expiry'] );
3387 
3388  if ( !$expiry || $expiry > $now ) {
3389  // Apply the restrictions
3390  $this->mRestrictionsExpiry['create'] = $expiry;
3391  $this->mRestrictions['create'] =
3392  explode( ',', trim( $title_protection['permission'] ) );
3393  } else { // Get rid of the old restrictions
3394  $this->mTitleProtection = false;
3395  }
3396  } else {
3397  $this->mRestrictionsExpiry['create'] = 'infinity';
3398  }
3399  $this->mRestrictionsLoaded = true;
3400  }
3401  }
3402 
3407  public function flushRestrictions() {
3408  $this->mRestrictionsLoaded = false;
3409  $this->mTitleProtection = null;
3410  }
3411 
3417  static function purgeExpiredRestrictions() {
3418  if ( wfReadOnly() ) {
3419  return;
3420  }
3421 
3423  wfGetDB( DB_MASTER ),
3424  __METHOD__,
3425  function ( IDatabase $dbw, $fname ) {
3426  $config = MediaWikiServices::getInstance()->getMainConfig();
3427  $ids = $dbw->selectFieldValues(
3428  'page_restrictions',
3429  'pr_id',
3430  [ 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
3431  $fname,
3432  [ 'LIMIT' => $config->get( 'UpdateRowsPerQuery' ) ] // T135470
3433  );
3434  if ( $ids ) {
3435  $dbw->delete( 'page_restrictions', [ 'pr_id' => $ids ], $fname );
3436  }
3437  }
3438  ) );
3439 
3441  wfGetDB( DB_MASTER ),
3442  __METHOD__,
3443  function ( IDatabase $dbw, $fname ) {
3444  $dbw->delete(
3445  'protected_titles',
3446  [ 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
3447  $fname
3448  );
3449  }
3450  ) );
3451  }
3452 
3458  public function hasSubpages() {
3459  if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
3460  # Duh
3461  return false;
3462  }
3463 
3464  # We dynamically add a member variable for the purpose of this method
3465  # alone to cache the result. There's no point in having it hanging
3466  # around uninitialized in every Title object; therefore we only add it
3467  # if needed and don't declare it statically.
3468  if ( $this->mHasSubpages === null ) {
3469  $this->mHasSubpages = false;
3470  $subpages = $this->getSubpages( 1 );
3471  if ( $subpages instanceof TitleArray ) {
3472  $this->mHasSubpages = (bool)$subpages->count();
3473  }
3474  }
3475 
3476  return $this->mHasSubpages;
3477  }
3478 
3486  public function getSubpages( $limit = -1 ) {
3487  if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
3488  return [];
3489  }
3490 
3491  $dbr = wfGetDB( DB_REPLICA );
3492  $conds['page_namespace'] = $this->mNamespace;
3493  $conds[] = 'page_title ' . $dbr->buildLike( $this->mDbkeyform . '/', $dbr->anyString() );
3494  $options = [];
3495  if ( $limit > -1 ) {
3496  $options['LIMIT'] = $limit;
3497  }
3499  $dbr->select( 'page',
3500  [ 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ],
3501  $conds,
3502  __METHOD__,
3503  $options
3504  )
3505  );
3506  }
3507 
3513  public function isDeleted() {
3514  if ( $this->mNamespace < 0 ) {
3515  $n = 0;
3516  } else {
3517  $dbr = wfGetDB( DB_REPLICA );
3518 
3519  $n = $dbr->selectField( 'archive', 'COUNT(*)',
3520  [ 'ar_namespace' => $this->mNamespace, 'ar_title' => $this->mDbkeyform ],
3521  __METHOD__
3522  );
3523  if ( $this->mNamespace == NS_FILE ) {
3524  $n += $dbr->selectField( 'filearchive', 'COUNT(*)',
3525  [ 'fa_name' => $this->mDbkeyform ],
3526  __METHOD__
3527  );
3528  }
3529  }
3530  return (int)$n;
3531  }
3532 
3538  public function isDeletedQuick() {
3539  if ( $this->mNamespace < 0 ) {
3540  return false;
3541  }
3542  $dbr = wfGetDB( DB_REPLICA );
3543  $deleted = (bool)$dbr->selectField( 'archive', '1',
3544  [ 'ar_namespace' => $this->mNamespace, 'ar_title' => $this->mDbkeyform ],
3545  __METHOD__
3546  );
3547  if ( !$deleted && $this->mNamespace == NS_FILE ) {
3548  $deleted = (bool)$dbr->selectField( 'filearchive', '1',
3549  [ 'fa_name' => $this->mDbkeyform ],
3550  __METHOD__
3551  );
3552  }
3553  return $deleted;
3554  }
3555 
3564  public function getArticleID( $flags = 0 ) {
3565  if ( $this->mNamespace < 0 ) {
3566  $this->mArticleID = 0;
3567  return $this->mArticleID;
3568  }
3569  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3570  if ( $flags & self::GAID_FOR_UPDATE ) {
3571  $oldUpdate = $linkCache->forUpdate( true );
3572  $linkCache->clearLink( $this );
3573  $this->mArticleID = $linkCache->addLinkObj( $this );
3574  $linkCache->forUpdate( $oldUpdate );
3575  } else {
3576  if ( $this->mArticleID == -1 ) {
3577  $this->mArticleID = $linkCache->addLinkObj( $this );
3578  }
3579  }
3580  return $this->mArticleID;
3581  }
3582 
3590  public function isRedirect( $flags = 0 ) {
3591  if ( !is_null( $this->mRedirect ) ) {
3592  return $this->mRedirect;
3593  }
3594  if ( !$this->getArticleID( $flags ) ) {
3595  $this->mRedirect = false;
3596  return $this->mRedirect;
3597  }
3598 
3599  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3600  $linkCache->addLinkObj( $this ); # in case we already had an article ID
3601  $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' );
3602  if ( $cached === null ) {
3603  # Trust LinkCache's state over our own
3604  # LinkCache is telling us that the page doesn't exist, despite there being cached
3605  # data relating to an existing page in $this->mArticleID. Updaters should clear
3606  # LinkCache as appropriate, or use $flags = Title::GAID_FOR_UPDATE. If that flag is
3607  # set, then LinkCache will definitely be up to date here, since getArticleID() forces
3608  # LinkCache to refresh its data from the master.
3609  $this->mRedirect = false;
3610  return $this->mRedirect;
3611  }
3612 
3613  $this->mRedirect = (bool)$cached;
3614 
3615  return $this->mRedirect;
3616  }
3617 
3625  public function getLength( $flags = 0 ) {
3626  if ( $this->mLength != -1 ) {
3627  return $this->mLength;
3628  }
3629  if ( !$this->getArticleID( $flags ) ) {
3630  $this->mLength = 0;
3631  return $this->mLength;
3632  }
3633  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3634  $linkCache->addLinkObj( $this ); # in case we already had an article ID
3635  $cached = $linkCache->getGoodLinkFieldObj( $this, 'length' );
3636  if ( $cached === null ) {
3637  # Trust LinkCache's state over our own, as for isRedirect()
3638  $this->mLength = 0;
3639  return $this->mLength;
3640  }
3641 
3642  $this->mLength = intval( $cached );
3643 
3644  return $this->mLength;
3645  }
3646 
3653  public function getLatestRevID( $flags = 0 ) {
3654  if ( !( $flags & self::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) {
3655  return intval( $this->mLatestID );
3656  }
3657  if ( !$this->getArticleID( $flags ) ) {
3658  $this->mLatestID = 0;
3659  return $this->mLatestID;
3660  }
3661  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3662  $linkCache->addLinkObj( $this ); # in case we already had an article ID
3663  $cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' );
3664  if ( $cached === null ) {
3665  # Trust LinkCache's state over our own, as for isRedirect()
3666  $this->mLatestID = 0;
3667  return $this->mLatestID;
3668  }
3669 
3670  $this->mLatestID = intval( $cached );
3671 
3672  return $this->mLatestID;
3673  }
3674 
3685  public function resetArticleID( $newid ) {
3686  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3687  $linkCache->clearLink( $this );
3688 
3689  if ( $newid === false ) {
3690  $this->mArticleID = -1;
3691  } else {
3692  $this->mArticleID = intval( $newid );
3693  }
3694  $this->mRestrictionsLoaded = false;
3695  $this->mRestrictions = [];
3696  $this->mOldRestrictions = false;
3697  $this->mRedirect = null;
3698  $this->mLength = -1;
3699  $this->mLatestID = false;
3700  $this->mContentModel = false;
3701  $this->mEstimateRevisions = null;
3702  $this->mPageLanguage = false;
3703  $this->mDbPageLanguage = false;
3704  $this->mIsBigDeletion = null;
3705  }
3706 
3707  public static function clearCaches() {
3708  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3709  $linkCache->clear();
3710 
3711  $titleCache = self::getTitleCache();
3712  $titleCache->clear();
3713  }
3714 
3722  public static function capitalize( $text, $ns = NS_MAIN ) {
3723  if ( MWNamespace::isCapitalized( $ns ) ) {
3724  return MediaWikiServices::getInstance()->getContentLanguage()->ucfirst( $text );
3725  } else {
3726  return $text;
3727  }
3728  }
3729 
3742  private function secureAndSplit() {
3743  // @note: splitTitleString() is a temporary hack to allow MediaWikiTitleCodec to share
3744  // the parsing code with Title, while avoiding massive refactoring.
3745  // @todo: get rid of secureAndSplit, refactor parsing code.
3746  // @note: getTitleParser() returns a TitleParser implementation which does not have a
3747  // splitTitleString method, but the only implementation (MediaWikiTitleCodec) does
3748  $titleCodec = MediaWikiServices::getInstance()->getTitleParser();
3749  // MalformedTitleException can be thrown here
3750  $parts = $titleCodec->splitTitleString( $this->mDbkeyform, $this->mDefaultNamespace );
3751 
3752  # Fill fields
3753  $this->setFragment( '#' . $parts['fragment'] );
3754  $this->mInterwiki = $parts['interwiki'];
3755  $this->mLocalInterwiki = $parts['local_interwiki'];
3756  $this->mNamespace = $parts['namespace'];
3757  $this->mUserCaseDBKey = $parts['user_case_dbkey'];
3758 
3759  $this->mDbkeyform = $parts['dbkey'];
3760  $this->mUrlform = wfUrlencode( $this->mDbkeyform );
3761  $this->mTextform = strtr( $this->mDbkeyform, '_', ' ' );
3762 
3763  # We already know that some pages won't be in the database!
3764  if ( $this->isExternal() || $this->isSpecialPage() ) {
3765  $this->mArticleID = 0;
3766  }
3767 
3768  return true;
3769  }
3770 
3783  public function getLinksTo( $options = [], $table = 'pagelinks', $prefix = 'pl' ) {
3784  if ( count( $options ) > 0 ) {
3785  $db = wfGetDB( DB_MASTER );
3786  } else {
3787  $db = wfGetDB( DB_REPLICA );
3788  }
3789 
3790  $res = $db->select(
3791  [ 'page', $table ],
3792  self::getSelectFields(),
3793  [
3794  "{$prefix}_from=page_id",
3795  "{$prefix}_namespace" => $this->mNamespace,
3796  "{$prefix}_title" => $this->mDbkeyform ],
3797  __METHOD__,
3798  $options
3799  );
3800 
3801  $retVal = [];
3802  if ( $res->numRows() ) {
3803  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3804  foreach ( $res as $row ) {
3805  $titleObj = self::makeTitle( $row->page_namespace, $row->page_title );
3806  if ( $titleObj ) {
3807  $linkCache->addGoodLinkObjFromRow( $titleObj, $row );
3808  $retVal[] = $titleObj;
3809  }
3810  }
3811  }
3812  return $retVal;
3813  }
3814 
3825  public function getTemplateLinksTo( $options = [] ) {
3826  return $this->getLinksTo( $options, 'templatelinks', 'tl' );
3827  }
3828 
3841  public function getLinksFrom( $options = [], $table = 'pagelinks', $prefix = 'pl' ) {
3842  $id = $this->getArticleID();
3843 
3844  # If the page doesn't exist; there can't be any link from this page
3845  if ( !$id ) {
3846  return [];
3847  }
3848 
3849  $db = wfGetDB( DB_REPLICA );
3850 
3851  $blNamespace = "{$prefix}_namespace";
3852  $blTitle = "{$prefix}_title";
3853 
3854  $pageQuery = WikiPage::getQueryInfo();
3855  $res = $db->select(
3856  [ $table, 'nestpage' => $pageQuery['tables'] ],
3857  array_merge(
3858  [ $blNamespace, $blTitle ],
3859  $pageQuery['fields']
3860  ),
3861  [ "{$prefix}_from" => $id ],
3862  __METHOD__,
3863  $options,
3864  [ 'nestpage' => [
3865  'LEFT JOIN',
3866  [ "page_namespace=$blNamespace", "page_title=$blTitle" ]
3867  ] ] + $pageQuery['joins']
3868  );
3869 
3870  $retVal = [];
3871  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3872  foreach ( $res as $row ) {
3873  if ( $row->page_id ) {
3874  $titleObj = self::newFromRow( $row );
3875  } else {
3876  $titleObj = self::makeTitle( $row->$blNamespace, $row->$blTitle );
3877  $linkCache->addBadLinkObj( $titleObj );
3878  }
3879  $retVal[] = $titleObj;
3880  }
3881 
3882  return $retVal;
3883  }
3884 
3895  public function getTemplateLinksFrom( $options = [] ) {
3896  return $this->getLinksFrom( $options, 'templatelinks', 'tl' );
3897  }
3898 
3907  public function getBrokenLinksFrom() {
3908  if ( $this->getArticleID() == 0 ) {
3909  # All links from article ID 0 are false positives
3910  return [];
3911  }
3912 
3913  $dbr = wfGetDB( DB_REPLICA );
3914  $res = $dbr->select(
3915  [ 'page', 'pagelinks' ],
3916  [ 'pl_namespace', 'pl_title' ],
3917  [
3918  'pl_from' => $this->getArticleID(),
3919  'page_namespace IS NULL'
3920  ],
3921  __METHOD__, [],
3922  [
3923  'page' => [
3924  'LEFT JOIN',
3925  [ 'pl_namespace=page_namespace', 'pl_title=page_title' ]
3926  ]
3927  ]
3928  );
3929 
3930  $retVal = [];
3931  foreach ( $res as $row ) {
3932  $retVal[] = self::makeTitle( $row->pl_namespace, $row->pl_title );
3933  }
3934  return $retVal;
3935  }
3936 
3943  public function getCdnUrls() {
3944  $urls = [
3945  $this->getInternalURL(),
3946  $this->getInternalURL( 'action=history' )
3947  ];
3948 
3949  $pageLang = $this->getPageLanguage();
3950  if ( $pageLang->hasVariants() ) {
3951  $variants = $pageLang->getVariants();
3952  foreach ( $variants as $vCode ) {
3953  $urls[] = $this->getInternalURL( $vCode );
3954  }
3955  }
3956 
3957  // If we are looking at a css/js user subpage, purge the action=raw.
3958  if ( $this->isUserJsConfigPage() ) {
3959  $urls[] = $this->getInternalURL( 'action=raw&ctype=text/javascript' );
3960  } elseif ( $this->isUserJsonConfigPage() ) {
3961  $urls[] = $this->getInternalURL( 'action=raw&ctype=application/json' );
3962  } elseif ( $this->isUserCssConfigPage() ) {
3963  $urls[] = $this->getInternalURL( 'action=raw&ctype=text/css' );
3964  }
3965 
3966  Hooks::run( 'TitleSquidURLs', [ $this, &$urls ] );
3967  return $urls;
3968  }
3969 
3973  public function getSquidURLs() {
3974  return $this->getCdnUrls();
3975  }
3976 
3980  public function purgeSquid() {
3982  new CdnCacheUpdate( $this->getCdnUrls() ),
3984  );
3985  }
3986 
3997  public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) {
3998  global $wgUser;
3999 
4000  if ( !( $nt instanceof Title ) ) {
4001  // Normally we'd add this to $errors, but we'll get
4002  // lots of syntax errors if $nt is not an object
4003  return [ [ 'badtitletext' ] ];
4004  }
4005 
4006  $mp = new MovePage( $this, $nt );
4007  $errors = $mp->isValidMove()->getErrorsArray();
4008  if ( $auth ) {
4009  $errors = wfMergeErrorArrays(
4010  $errors,
4011  $mp->checkPermissions( $wgUser, $reason )->getErrorsArray()
4012  );
4013  }
4014 
4015  return $errors ?: true;
4016  }
4017 
4024  protected function validateFileMoveOperation( $nt ) {
4025  global $wgUser;
4026 
4027  $errors = [];
4028 
4029  $destFile = wfLocalFile( $nt );
4030  $destFile->load( File::READ_LATEST );
4031  if ( !$wgUser->isAllowed( 'reupload-shared' )
4032  && !$destFile->exists() && wfFindFile( $nt )
4033  ) {
4034  $errors[] = [ 'file-exists-sharedrepo' ];
4035  }
4036 
4037  return $errors;
4038  }
4039 
4053  public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true,
4054  array $changeTags = []
4055  ) {
4056  global $wgUser;
4057  $err = $this->isValidMoveOperation( $nt, $auth, $reason );
4058  if ( is_array( $err ) ) {
4059  // Auto-block user's IP if the account was "hard" blocked
4060  $wgUser->spreadAnyEditBlock();
4061  return $err;
4062  }
4063  // Check suppressredirect permission
4064  if ( $auth && !$wgUser->isAllowed( 'suppressredirect' ) ) {
4065  $createRedirect = true;
4066  }
4067 
4068  $mp = new MovePage( $this, $nt );
4069  $status = $mp->move( $wgUser, $reason, $createRedirect, $changeTags );
4070  if ( $status->isOK() ) {
4071  return true;
4072  } else {
4073  return $status->getErrorsArray();
4074  }
4075  }
4076 
4091  public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true,
4092  array $changeTags = []
4093  ) {
4094  global $wgMaximumMovedPages;
4095  // Check permissions
4096  if ( !$this->userCan( 'move-subpages' ) ) {
4097  return [
4098  [ 'cant-move-subpages' ],
4099  ];
4100  }
4101  // Do the source and target namespaces support subpages?
4102  if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
4103  return [
4104  [ 'namespace-nosubpages', MWNamespace::getCanonicalName( $this->mNamespace ) ],
4105  ];
4106  }
4107  if ( !MWNamespace::hasSubpages( $nt->getNamespace() ) ) {
4108  return [
4109  [ 'namespace-nosubpages', MWNamespace::getCanonicalName( $nt->getNamespace() ) ],
4110  ];
4111  }
4112 
4113  $subpages = $this->getSubpages( $wgMaximumMovedPages + 1 );
4114  $retval = [];
4115  $count = 0;
4116  foreach ( $subpages as $oldSubpage ) {
4117  $count++;
4118  if ( $count > $wgMaximumMovedPages ) {
4119  $retval[$oldSubpage->getPrefixedText()] = [
4120  [ 'movepage-max-pages', $wgMaximumMovedPages ],
4121  ];
4122  break;
4123  }
4124 
4125  // We don't know whether this function was called before
4126  // or after moving the root page, so check both
4127  // $this and $nt
4128  if ( $oldSubpage->getArticleID() == $this->getArticleID()
4129  || $oldSubpage->getArticleID() == $nt->getArticleID()
4130  ) {
4131  // When moving a page to a subpage of itself,
4132  // don't move it twice
4133  continue;
4134  }
4135  $newPageName = preg_replace(
4136  '#^' . preg_quote( $this->mDbkeyform, '#' ) . '#',
4137  StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # T23234
4138  $oldSubpage->getDBkey() );
4139  if ( $oldSubpage->isTalkPage() ) {
4140  $newNs = $nt->getTalkPage()->getNamespace();
4141  } else {
4142  $newNs = $nt->getSubjectPage()->getNamespace();
4143  }
4144  # T16385: we need makeTitleSafe because the new page names may
4145  # be longer than 255 characters.
4146  $newSubpage = self::makeTitleSafe( $newNs, $newPageName );
4147 
4148  $success = $oldSubpage->moveTo( $newSubpage, $auth, $reason, $createRedirect, $changeTags );
4149  if ( $success === true ) {
4150  $retval[$oldSubpage->getPrefixedText()] = $newSubpage->getPrefixedText();
4151  } else {
4152  $retval[$oldSubpage->getPrefixedText()] = $success;
4153  }
4154  }
4155  return $retval;
4156  }
4157 
4164  public function isSingleRevRedirect() {
4165  global $wgContentHandlerUseDB;
4166 
4167  $dbw = wfGetDB( DB_MASTER );
4168 
4169  # Is it a redirect?
4170  $fields = [ 'page_is_redirect', 'page_latest', 'page_id' ];
4171  if ( $wgContentHandlerUseDB ) {
4172  $fields[] = 'page_content_model';
4173  }
4174 
4175  $row = $dbw->selectRow( 'page',
4176  $fields,
4177  $this->pageCond(),
4178  __METHOD__,
4179  [ 'FOR UPDATE' ]
4180  );
4181  # Cache some fields we may want
4182  $this->mArticleID = $row ? intval( $row->page_id ) : 0;
4183  $this->mRedirect = $row ? (bool)$row->page_is_redirect : false;
4184  $this->mLatestID = $row ? intval( $row->page_latest ) : false;
4185  $this->mContentModel = $row && isset( $row->page_content_model )
4186  ? strval( $row->page_content_model )
4187  : false;
4188 
4189  if ( !$this->mRedirect ) {
4190  return false;
4191  }
4192  # Does the article have a history?
4193  $row = $dbw->selectField( [ 'page', 'revision' ],
4194  'rev_id',
4195  [ 'page_namespace' => $this->mNamespace,
4196  'page_title' => $this->mDbkeyform,
4197  'page_id=rev_page',
4198  'page_latest != rev_id'
4199  ],
4200  __METHOD__,
4201  [ 'FOR UPDATE' ]
4202  );
4203  # Return true if there was no history
4204  return ( $row === false );
4205  }
4206 
4215  public function isValidMoveTarget( $nt ) {
4216  # Is it an existing file?
4217  if ( $nt->getNamespace() == NS_FILE ) {
4218  $file = wfLocalFile( $nt );
4219  $file->load( File::READ_LATEST );
4220  if ( $file->exists() ) {
4221  wfDebug( __METHOD__ . ": file exists\n" );
4222  return false;
4223  }
4224  }
4225  # Is it a redirect with no history?
4226  if ( !$nt->isSingleRevRedirect() ) {
4227  wfDebug( __METHOD__ . ": not a one-rev redirect\n" );
4228  return false;
4229  }
4230  # Get the article text
4231  $rev = Revision::newFromTitle( $nt, false, Revision::READ_LATEST );
4232  if ( !is_object( $rev ) ) {
4233  return false;
4234  }
4235  $content = $rev->getContent();
4236  # Does the redirect point to the source?
4237  # Or is it a broken self-redirect, usually caused by namespace collisions?
4238  $redirTitle = $content ? $content->getRedirectTarget() : null;
4239 
4240  if ( $redirTitle ) {
4241  if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
4242  $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) {
4243  wfDebug( __METHOD__ . ": redirect points to other page\n" );
4244  return false;
4245  } else {
4246  return true;
4247  }
4248  } else {
4249  # Fail safe (not a redirect after all. strange.)
4250  wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() .
4251  " is a redirect, but it doesn't contain a valid redirect.\n" );
4252  return false;
4253  }
4254  }
4255 
4263  public function getParentCategories() {
4264  $data = [];
4265 
4266  $titleKey = $this->getArticleID();
4267 
4268  if ( $titleKey === 0 ) {
4269  return $data;
4270  }
4271 
4272  $dbr = wfGetDB( DB_REPLICA );
4273 
4274  $res = $dbr->select(
4275  'categorylinks',
4276  'cl_to',
4277  [ 'cl_from' => $titleKey ],
4278  __METHOD__
4279  );
4280 
4281  if ( $res->numRows() > 0 ) {
4282  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
4283  foreach ( $res as $row ) {
4284  // $data[] = Title::newFromText( $contLang->getNsText ( NS_CATEGORY ).':'.$row->cl_to);
4285  $data[$contLang->getNsText( NS_CATEGORY ) . ':' . $row->cl_to] =
4286  $this->getFullText();
4287  }
4288  }
4289  return $data;
4290  }
4291 
4298  public function getParentCategoryTree( $children = [] ) {
4299  $stack = [];
4300  $parents = $this->getParentCategories();
4301 
4302  if ( $parents ) {
4303  foreach ( $parents as $parent => $current ) {
4304  if ( array_key_exists( $parent, $children ) ) {
4305  # Circular reference
4306  $stack[$parent] = [];
4307  } else {
4308  $nt = self::newFromText( $parent );
4309  if ( $nt ) {
4310  $stack[$parent] = $nt->getParentCategoryTree( $children + [ $parent => 1 ] );
4311  }
4312  }
4313  }
4314  }
4315 
4316  return $stack;
4317  }
4318 
4325  public function pageCond() {
4326  if ( $this->mArticleID > 0 ) {
4327  // PK avoids secondary lookups in InnoDB, shouldn't hurt other DBs
4328  return [ 'page_id' => $this->mArticleID ];
4329  } else {
4330  return [ 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform ];
4331  }
4332  }
4333 
4341  private function getRelativeRevisionID( $revId, $flags, $dir ) {
4342  $revId = (int)$revId;
4343  if ( $dir === 'next' ) {
4344  $op = '>';
4345  $sort = 'ASC';
4346  } elseif ( $dir === 'prev' ) {
4347  $op = '<';
4348  $sort = 'DESC';
4349  } else {
4350  throw new InvalidArgumentException( '$dir must be "next" or "prev"' );
4351  }
4352 
4353  if ( $flags & self::GAID_FOR_UPDATE ) {
4354  $db = wfGetDB( DB_MASTER );
4355  } else {
4356  $db = wfGetDB( DB_REPLICA, 'contributions' );
4357  }
4358 
4359  // Intentionally not caring if the specified revision belongs to this
4360  // page. We only care about the timestamp.
4361  $ts = $db->selectField( 'revision', 'rev_timestamp', [ 'rev_id' => $revId ], __METHOD__ );
4362  if ( $ts === false ) {
4363  $ts = $db->selectField( 'archive', 'ar_timestamp', [ 'ar_rev_id' => $revId ], __METHOD__ );
4364  if ( $ts === false ) {
4365  // Or should this throw an InvalidArgumentException or something?
4366  return false;
4367  }
4368  }
4369  $ts = $db->addQuotes( $ts );
4370 
4371  $revId = $db->selectField( 'revision', 'rev_id',
4372  [
4373  'rev_page' => $this->getArticleID( $flags ),
4374  "rev_timestamp $op $ts OR (rev_timestamp = $ts AND rev_id $op $revId)"
4375  ],
4376  __METHOD__,
4377  [
4378  'ORDER BY' => "rev_timestamp $sort, rev_id $sort",
4379  'IGNORE INDEX' => 'rev_timestamp', // Probably needed for T159319
4380  ]
4381  );
4382 
4383  if ( $revId === false ) {
4384  return false;
4385  } else {
4386  return intval( $revId );
4387  }
4388  }
4389 
4397  public function getPreviousRevisionID( $revId, $flags = 0 ) {
4398  return $this->getRelativeRevisionID( $revId, $flags, 'prev' );
4399  }
4400 
4408  public function getNextRevisionID( $revId, $flags = 0 ) {
4409  return $this->getRelativeRevisionID( $revId, $flags, 'next' );
4410  }
4411 
4418  public function getFirstRevision( $flags = 0 ) {
4419  $pageId = $this->getArticleID( $flags );
4420  if ( $pageId ) {
4421  $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_REPLICA );
4423  $row = $db->selectRow( $revQuery['tables'], $revQuery['fields'],
4424  [ 'rev_page' => $pageId ],
4425  __METHOD__,
4426  [
4427  'ORDER BY' => 'rev_timestamp ASC, rev_id ASC',
4428  'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319
4429  ],
4430  $revQuery['joins']
4431  );
4432  if ( $row ) {
4433  return new Revision( $row, 0, $this );
4434  }
4435  }
4436  return null;
4437  }
4438 
4445  public function getEarliestRevTime( $flags = 0 ) {
4446  $rev = $this->getFirstRevision( $flags );
4447  return $rev ? $rev->getTimestamp() : null;
4448  }
4449 
4455  public function isNewPage() {
4456  $dbr = wfGetDB( DB_REPLICA );
4457  return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ );
4458  }
4459 
4465  public function isBigDeletion() {
4466  global $wgDeleteRevisionsLimit;
4467 
4468  if ( !$wgDeleteRevisionsLimit ) {
4469  return false;
4470  }
4471 
4472  if ( $this->mIsBigDeletion === null ) {
4473  $dbr = wfGetDB( DB_REPLICA );
4474 
4475  $revCount = $dbr->selectRowCount(
4476  'revision',
4477  '1',
4478  [ 'rev_page' => $this->getArticleID() ],
4479  __METHOD__,
4480  [ 'LIMIT' => $wgDeleteRevisionsLimit + 1 ]
4481  );
4482 
4483  $this->mIsBigDeletion = $revCount > $wgDeleteRevisionsLimit;
4484  }
4485 
4486  return $this->mIsBigDeletion;
4487  }
4488 
4494  public function estimateRevisionCount() {
4495  if ( !$this->exists() ) {
4496  return 0;
4497  }
4498 
4499  if ( $this->mEstimateRevisions === null ) {
4500  $dbr = wfGetDB( DB_REPLICA );
4501  $this->mEstimateRevisions = $dbr->estimateRowCount( 'revision', '*',
4502  [ 'rev_page' => $this->getArticleID() ], __METHOD__ );
4503  }
4504 
4506  }
4507 
4517  public function countRevisionsBetween( $old, $new, $max = null ) {
4518  if ( !( $old instanceof Revision ) ) {
4519  $old = Revision::newFromTitle( $this, (int)$old );
4520  }
4521  if ( !( $new instanceof Revision ) ) {
4522  $new = Revision::newFromTitle( $this, (int)$new );
4523  }
4524  if ( !$old || !$new ) {
4525  return 0; // nothing to compare
4526  }
4527  $dbr = wfGetDB( DB_REPLICA );
4528  $conds = [
4529  'rev_page' => $this->getArticleID(),
4530  'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
4531  'rev_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
4532  ];
4533  if ( $max !== null ) {
4534  return $dbr->selectRowCount( 'revision', '1',
4535  $conds,
4536  __METHOD__,
4537  [ 'LIMIT' => $max + 1 ] // extra to detect truncation
4538  );
4539  } else {
4540  return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ );
4541  }
4542  }
4543 
4560  public function getAuthorsBetween( $old, $new, $limit, $options = [] ) {
4561  if ( !( $old instanceof Revision ) ) {
4562  $old = Revision::newFromTitle( $this, (int)$old );
4563  }
4564  if ( !( $new instanceof Revision ) ) {
4565  $new = Revision::newFromTitle( $this, (int)$new );
4566  }
4567  // XXX: what if Revision objects are passed in, but they don't refer to this title?
4568  // Add $old->getPage() != $new->getPage() || $old->getPage() != $this->getArticleID()
4569  // in the sanity check below?
4570  if ( !$old || !$new ) {
4571  return null; // nothing to compare
4572  }
4573  $authors = [];
4574  $old_cmp = '>';
4575  $new_cmp = '<';
4576  $options = (array)$options;
4577  if ( in_array( 'include_old', $options ) ) {
4578  $old_cmp = '>=';
4579  }
4580  if ( in_array( 'include_new', $options ) ) {
4581  $new_cmp = '<=';
4582  }
4583  if ( in_array( 'include_both', $options ) ) {
4584  $old_cmp = '>=';
4585  $new_cmp = '<=';
4586  }
4587  // No DB query needed if $old and $new are the same or successive revisions:
4588  if ( $old->getId() === $new->getId() ) {
4589  return ( $old_cmp === '>' && $new_cmp === '<' ) ?
4590  [] :
4591  [ $old->getUserText( Revision::RAW ) ];
4592  } elseif ( $old->getId() === $new->getParentId() ) {
4593  if ( $old_cmp === '>=' && $new_cmp === '<=' ) {
4594  $authors[] = $old->getUserText( Revision::RAW );
4595  if ( $old->getUserText( Revision::RAW ) != $new->getUserText( Revision::RAW ) ) {
4596  $authors[] = $new->getUserText( Revision::RAW );
4597  }
4598  } elseif ( $old_cmp === '>=' ) {
4599  $authors[] = $old->getUserText( Revision::RAW );
4600  } elseif ( $new_cmp === '<=' ) {
4601  $authors[] = $new->getUserText( Revision::RAW );
4602  }
4603  return $authors;
4604  }
4605  $dbr = wfGetDB( DB_REPLICA );
4607  $authors = $dbr->selectFieldValues(
4608  $revQuery['tables'],
4609  $revQuery['fields']['rev_user_text'],
4610  [
4611  'rev_page' => $this->getArticleID(),
4612  "rev_timestamp $old_cmp " . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
4613  "rev_timestamp $new_cmp " . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
4614  ], __METHOD__,
4615  [ 'DISTINCT', 'LIMIT' => $limit + 1 ], // add one so caller knows it was truncated
4616  $revQuery['joins']
4617  );
4618  return $authors;
4619  }
4620 
4635  public function countAuthorsBetween( $old, $new, $limit, $options = [] ) {
4636  $authors = $this->getAuthorsBetween( $old, $new, $limit, $options );
4637  return $authors ? count( $authors ) : 0;
4638  }
4639 
4646  public function equals( Title $title ) {
4647  // Note: === is necessary for proper matching of number-like titles.
4648  return $this->mInterwiki === $title->mInterwiki
4649  && $this->mNamespace == $title->mNamespace
4650  && $this->mDbkeyform === $title->mDbkeyform;
4651  }
4652 
4659  public function isSubpageOf( Title $title ) {
4660  return $this->mInterwiki === $title->mInterwiki
4661  && $this->mNamespace == $title->mNamespace
4662  && strpos( $this->mDbkeyform, $title->mDbkeyform . '/' ) === 0;
4663  }
4664 
4676  public function exists( $flags = 0 ) {
4677  $exists = $this->getArticleID( $flags ) != 0;
4678  Hooks::run( 'TitleExists', [ $this, &$exists ] );
4679  return $exists;
4680  }
4681 
4698  public function isAlwaysKnown() {
4699  $isKnown = null;
4700 
4711  Hooks::run( 'TitleIsAlwaysKnown', [ $this, &$isKnown ] );
4712 
4713  if ( !is_null( $isKnown ) ) {
4714  return $isKnown;
4715  }
4716 
4717  if ( $this->isExternal() ) {
4718  return true; // any interwiki link might be viewable, for all we know
4719  }
4720 
4721  switch ( $this->mNamespace ) {
4722  case NS_MEDIA:
4723  case NS_FILE:
4724  // file exists, possibly in a foreign repo
4725  return (bool)wfFindFile( $this );
4726  case NS_SPECIAL:
4727  // valid special page
4728  return MediaWikiServices::getInstance()->getSpecialPageFactory()->
4729  exists( $this->mDbkeyform );
4730  case NS_MAIN:
4731  // selflink, possibly with fragment
4732  return $this->mDbkeyform == '';
4733  case NS_MEDIAWIKI:
4734  // known system message
4735  return $this->hasSourceText() !== false;
4736  default:
4737  return false;
4738  }
4739  }
4740 
4752  public function isKnown() {
4753  return $this->isAlwaysKnown() || $this->exists();
4754  }
4755 
4761  public function hasSourceText() {
4762  if ( $this->exists() ) {
4763  return true;
4764  }
4765 
4766  if ( $this->mNamespace == NS_MEDIAWIKI ) {
4767  // If the page doesn't exist but is a known system message, default
4768  // message content will be displayed, same for language subpages-
4769  // Use always content language to avoid loading hundreds of languages
4770  // to get the link color.
4771  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
4772  list( $name, ) = MessageCache::singleton()->figureMessage(
4773  $contLang->lcfirst( $this->getText() )
4774  );
4775  $message = wfMessage( $name )->inLanguage( $contLang )->useDatabase( false );
4776  return $message->exists();
4777  }
4778 
4779  return false;
4780  }
4781 
4819  public function getDefaultMessageText() {
4820  if ( $this->mNamespace != NS_MEDIAWIKI ) { // Just in case
4821  return false;
4822  }
4823 
4824  list( $name, $lang ) = MessageCache::singleton()->figureMessage(
4825  MediaWikiServices::getInstance()->getContentLanguage()->lcfirst( $this->getText() )
4826  );
4827  $message = wfMessage( $name )->inLanguage( $lang )->useDatabase( false );
4828 
4829  if ( $message->exists() ) {
4830  return $message->plain();
4831  } else {
4832  return false;
4833  }
4834  }
4835 
4842  public function invalidateCache( $purgeTime = null ) {
4843  if ( wfReadOnly() ) {
4844  return false;
4845  } elseif ( $this->mArticleID === 0 ) {
4846  return true; // avoid gap locking if we know it's not there
4847  }
4848 
4849  $dbw = wfGetDB( DB_MASTER );
4850  $dbw->onTransactionPreCommitOrIdle(
4851  function () use ( $dbw ) {
4853  $this, null, null, $dbw->getDomainId() );
4854  },
4855  __METHOD__
4856  );
4857 
4858  $conds = $this->pageCond();
4860  new AutoCommitUpdate(
4861  $dbw,
4862  __METHOD__,
4863  function ( IDatabase $dbw, $fname ) use ( $conds, $purgeTime ) {
4864  $dbTimestamp = $dbw->timestamp( $purgeTime ?: time() );
4865  $dbw->update(
4866  'page',
4867  [ 'page_touched' => $dbTimestamp ],
4868  $conds + [ 'page_touched < ' . $dbw->addQuotes( $dbTimestamp ) ],
4869  $fname
4870  );
4871  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $this );
4872  }
4873  ),
4875  );
4876 
4877  return true;
4878  }
4879 
4885  public function touchLinks() {
4886  DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'pagelinks', 'page-touch' ) );
4887  if ( $this->mNamespace == NS_CATEGORY ) {
4889  new HTMLCacheUpdate( $this, 'categorylinks', 'category-touch' )
4890  );
4891  }
4892  }
4893 
4900  public function getTouched( $db = null ) {
4901  if ( $db === null ) {
4902  $db = wfGetDB( DB_REPLICA );
4903  }
4904  $touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ );
4905  return $touched;
4906  }
4907 
4914  public function getNotificationTimestamp( $user = null ) {
4915  global $wgUser;
4916 
4917  // Assume current user if none given
4918  if ( !$user ) {
4919  $user = $wgUser;
4920  }
4921  // Check cache first
4922  $uid = $user->getId();
4923  if ( !$uid ) {
4924  return false;
4925  }
4926  // avoid isset here, as it'll return false for null entries
4927  if ( array_key_exists( $uid, $this->mNotificationTimestamp ) ) {
4928  return $this->mNotificationTimestamp[$uid];
4929  }
4930  // Don't cache too much!
4931  if ( count( $this->mNotificationTimestamp ) >= self::CACHE_MAX ) {
4932  $this->mNotificationTimestamp = [];
4933  }
4934 
4935  $store = MediaWikiServices::getInstance()->getWatchedItemStore();
4936  $watchedItem = $store->getWatchedItem( $user, $this );
4937  if ( $watchedItem ) {
4938  $this->mNotificationTimestamp[$uid] = $watchedItem->getNotificationTimestamp();
4939  } else {
4940  $this->mNotificationTimestamp[$uid] = false;
4941  }
4942 
4943  return $this->mNotificationTimestamp[$uid];
4944  }
4945 
4952  public function getNamespaceKey( $prepend = 'nstab-' ) {
4953  // Gets the subject namespace of this title
4954  $subjectNS = MWNamespace::getSubject( $this->mNamespace );
4955  // Prefer canonical namespace name for HTML IDs
4956  $namespaceKey = MWNamespace::getCanonicalName( $subjectNS );
4957  if ( $namespaceKey === false ) {
4958  // Fallback to localised text
4959  $namespaceKey = $this->getSubjectNsText();
4960  }
4961  // Makes namespace key lowercase
4962  $namespaceKey = MediaWikiServices::getInstance()->getContentLanguage()->lc( $namespaceKey );
4963  // Uses main
4964  if ( $namespaceKey == '' ) {
4965  $namespaceKey = 'main';
4966  }
4967  // Changes file to image for backwards compatibility
4968  if ( $namespaceKey == 'file' ) {
4969  $namespaceKey = 'image';
4970  }
4971  return $prepend . $namespaceKey;
4972  }
4973 
4980  public function getRedirectsHere( $ns = null ) {
4981  $redirs = [];
4982 
4983  $dbr = wfGetDB( DB_REPLICA );
4984  $where = [
4985  'rd_namespace' => $this->mNamespace,
4986  'rd_title' => $this->mDbkeyform,
4987  'rd_from = page_id'
4988  ];
4989  if ( $this->isExternal() ) {
4990  $where['rd_interwiki'] = $this->mInterwiki;
4991  } else {
4992  $where[] = 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL';
4993  }
4994  if ( !is_null( $ns ) ) {
4995  $where['page_namespace'] = $ns;
4996  }
4997 
4998  $res = $dbr->select(
4999  [ 'redirect', 'page' ],
5000  [ 'page_namespace', 'page_title' ],
5001  $where,
5002  __METHOD__
5003  );
5004 
5005  foreach ( $res as $row ) {
5006  $redirs[] = self::newFromRow( $row );
5007  }
5008  return $redirs;
5009  }
5010 
5016  public function isValidRedirectTarget() {
5018 
5019  if ( $this->isSpecialPage() ) {
5020  // invalid redirect targets are stored in a global array, but explicitly disallow Userlogout here
5021  if ( $this->isSpecial( 'Userlogout' ) ) {
5022  return false;
5023  }
5024 
5025  foreach ( $wgInvalidRedirectTargets as $target ) {
5026  if ( $this->isSpecial( $target ) ) {
5027  return false;
5028  }
5029  }
5030  }
5031 
5032  return true;
5033  }
5034 
5040  public function getBacklinkCache() {
5041  return BacklinkCache::get( $this );
5042  }
5043 
5049  public function canUseNoindex() {
5051 
5052  $bannedNamespaces = is_null( $wgExemptFromUserRobotsControl )
5055 
5056  return !in_array( $this->mNamespace, $bannedNamespaces );
5057  }
5058 
5069  public function getCategorySortkey( $prefix = '' ) {
5070  $unprefixed = $this->getText();
5071 
5072  // Anything that uses this hook should only depend
5073  // on the Title object passed in, and should probably
5074  // tell the users to run updateCollations.php --force
5075  // in order to re-sort existing category relations.
5076  Hooks::run( 'GetDefaultSortkey', [ $this, &$unprefixed ] );
5077  if ( $prefix !== '' ) {
5078  # Separate with a line feed, so the unprefixed part is only used as
5079  # a tiebreaker when two pages have the exact same prefix.
5080  # In UCA, tab is the only character that can sort above LF
5081  # so we strip both of them from the original prefix.
5082  $prefix = strtr( $prefix, "\n\t", ' ' );
5083  return "$prefix\n$unprefixed";
5084  }
5085  return $unprefixed;
5086  }
5087 
5095  private function getDbPageLanguageCode() {
5096  global $wgPageLanguageUseDB;
5097 
5098  // check, if the page language could be saved in the database, and if so and
5099  // the value is not requested already, lookup the page language using LinkCache
5100  if ( $wgPageLanguageUseDB && $this->mDbPageLanguage === false ) {
5101  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
5102  $linkCache->addLinkObj( $this );
5103  $this->mDbPageLanguage = $linkCache->getGoodLinkFieldObj( $this, 'lang' );
5104  }
5105 
5106  return $this->mDbPageLanguage;
5107  }
5108 
5117  public function getPageLanguage() {
5118  global $wgLang, $wgLanguageCode;
5119  if ( $this->isSpecialPage() ) {
5120  // special pages are in the user language
5121  return $wgLang;
5122  }
5123 
5124  // Checking if DB language is set
5125  $dbPageLanguage = $this->getDbPageLanguageCode();
5126  if ( $dbPageLanguage ) {
5127  return wfGetLangObj( $dbPageLanguage );
5128  }
5129 
5130  if ( !$this->mPageLanguage || $this->mPageLanguage[1] !== $wgLanguageCode ) {
5131  // Note that this may depend on user settings, so the cache should
5132  // be only per-request.
5133  // NOTE: ContentHandler::getPageLanguage() may need to load the
5134  // content to determine the page language!
5135  // Checking $wgLanguageCode hasn't changed for the benefit of unit
5136  // tests.
5137  $contentHandler = ContentHandler::getForTitle( $this );
5138  $langObj = $contentHandler->getPageLanguage( $this );
5139  $this->mPageLanguage = [ $langObj->getCode(), $wgLanguageCode ];
5140  } else {
5141  $langObj = Language::factory( $this->mPageLanguage[0] );
5142  }
5143 
5144  return $langObj;
5145  }
5146 
5155  public function getPageViewLanguage() {
5156  global $wgLang;
5157 
5158  if ( $this->isSpecialPage() ) {
5159  // If the user chooses a variant, the content is actually
5160  // in a language whose code is the variant code.
5161  $variant = $wgLang->getPreferredVariant();
5162  if ( $wgLang->getCode() !== $variant ) {
5163  return Language::factory( $variant );
5164  }
5165 
5166  return $wgLang;
5167  }
5168 
5169  // Checking if DB language is set
5170  $dbPageLanguage = $this->getDbPageLanguageCode();
5171  if ( $dbPageLanguage ) {
5172  $pageLang = wfGetLangObj( $dbPageLanguage );
5173  $variant = $pageLang->getPreferredVariant();
5174  if ( $pageLang->getCode() !== $variant ) {
5175  $pageLang = Language::factory( $variant );
5176  }
5177 
5178  return $pageLang;
5179  }
5180 
5181  // @note Can't be cached persistently, depends on user settings.
5182  // @note ContentHandler::getPageViewLanguage() may need to load the
5183  // content to determine the page language!
5184  $contentHandler = ContentHandler::getForTitle( $this );
5185  $pageLang = $contentHandler->getPageViewLanguage( $this );
5186  return $pageLang;
5187  }
5188 
5199  public function getEditNotices( $oldid = 0 ) {
5200  $notices = [];
5201 
5202  // Optional notice for the entire namespace
5203  $editnotice_ns = 'editnotice-' . $this->mNamespace;
5204  $msg = wfMessage( $editnotice_ns );
5205  if ( $msg->exists() ) {
5206  $html = $msg->parseAsBlock();
5207  // Edit notices may have complex logic, but output nothing (T91715)
5208  if ( trim( $html ) !== '' ) {
5209  $notices[$editnotice_ns] = Html::rawElement(
5210  'div',
5211  [ 'class' => [
5212  'mw-editnotice',
5213  'mw-editnotice-namespace',
5214  Sanitizer::escapeClass( "mw-$editnotice_ns" )
5215  ] ],
5216  $html
5217  );
5218  }
5219  }
5220 
5221  if ( MWNamespace::hasSubpages( $this->mNamespace ) ) {
5222  // Optional notice for page itself and any parent page
5223  $parts = explode( '/', $this->mDbkeyform );
5224  $editnotice_base = $editnotice_ns;
5225  while ( count( $parts ) > 0 ) {
5226  $editnotice_base .= '-' . array_shift( $parts );
5227  $msg = wfMessage( $editnotice_base );
5228  if ( $msg->exists() ) {
5229  $html = $msg->parseAsBlock();
5230  if ( trim( $html ) !== '' ) {
5231  $notices[$editnotice_base] = Html::rawElement(
5232  'div',
5233  [ 'class' => [
5234  'mw-editnotice',
5235  'mw-editnotice-base',
5236  Sanitizer::escapeClass( "mw-$editnotice_base" )
5237  ] ],
5238  $html
5239  );
5240  }
5241  }
5242  }
5243  } else {
5244  // Even if there are no subpages in namespace, we still don't want "/" in MediaWiki message keys
5245  $editnoticeText = $editnotice_ns . '-' . strtr( $this->mDbkeyform, '/', '-' );
5246  $msg = wfMessage( $editnoticeText );
5247  if ( $msg->exists() ) {
5248  $html = $msg->parseAsBlock();
5249  if ( trim( $html ) !== '' ) {
5250  $notices[$editnoticeText] = Html::rawElement(
5251  'div',
5252  [ 'class' => [
5253  'mw-editnotice',
5254  'mw-editnotice-page',
5255  Sanitizer::escapeClass( "mw-$editnoticeText" )
5256  ] ],
5257  $html
5258  );
5259  }
5260  }
5261  }
5262 
5263  Hooks::run( 'TitleGetEditNotices', [ $this, $oldid, &$notices ] );
5264  return $notices;
5265  }
5266 
5270  public function __sleep() {
5271  return [
5272  'mNamespace',
5273  'mDbkeyform',
5274  'mFragment',
5275  'mInterwiki',
5276  'mLocalInterwiki',
5277  'mUserCaseDBKey',
5278  'mDefaultNamespace',
5279  ];
5280  }
5281 
5282  public function __wakeup() {
5283  $this->mArticleID = ( $this->mNamespace >= 0 ) ? -1 : 0;
5284  $this->mUrlform = wfUrlencode( $this->mDbkeyform );
5285  $this->mTextform = strtr( $this->mDbkeyform, '_', ' ' );
5286  }
5287 
5288 }
bool $mHasSubpages
Whether a page has any subpages.
Definition: Title.php:167
static getFilteredRestrictionTypes( $exists=true)
Get a filtered list of all restriction types supported by this wiki.
Definition: Title.php:2879
isAlwaysKnown()
Should links to this title be shown as potentially viewable (i.e.
Definition: Title.php:4698
The wiki should then use memcached to cache various data To use multiple just add more items to the array To increase the weight of a make its entry a array("192.168.0.1:11211", 2))
areCascadeProtectionSourcesLoaded( $getPages=true)
Determines whether cascading protection sources have already been loaded from the database...
Definition: Title.php:3104
static purgeExpiredRestrictions()
Purge expired restrictions from the page_restrictions table.
Definition: Title.php:3417
clear( $keys=null)
Clear one or several cache entries, or all cache entries.
checkUserBlock( $action, $user, $errors, $rigor, $short)
Check that the user isn&#39;t blocked from editing.
Definition: Title.php:2672
touchLinks()
Update page_touched timestamps and send CDN purge messages for pages linking to this title...
Definition: Title.php:4885
getFragment()
Get the Title fragment (i.e.
Definition: Title.php:1589
static getMainWANInstance()
Get the main WAN cache object.
loadRestrictions( $oldFashionedRestrictions=null)
Load restrictions from the page_restrictions table.
Definition: Title.php:3351
do that in ParserLimitReportFormat instead use this to modify the parameters of the image all existing parser cache entries will be invalid To avoid you ll need to handle that somehow(e.g. with the RejectParserCacheValue hook) because MediaWiki won 't do it for you. & $defaults also a ContextSource after deleting those rows but within the same transaction $rows
Definition: hooks.txt:2626
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses & $html
Definition: hooks.txt:1996
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition: deferred.txt:11
mixed $mTitleProtection
Cached value for getTitleProtection (create protection)
Definition: Title.php:148
invalidateCache( $purgeTime=null)
Updates page_touched for this page; called from LinksUpdate.php.
Definition: Title.php:4842
checkUserConfigPermissions( $action, $user, $errors, $rigor, $short)
Check CSS/JSON/JS sub-page permissions.
Definition: Title.php:2447
getRestrictions( $action)
Accessor/initialisation for mRestrictions.
Definition: Title.php:3220
isMovable()
Would anybody with sufficient privileges be able to move this page? Some pages just aren&#39;t movable...
Definition: Title.php:1231
getRootTitle()
Get the root page name title, i.e.
Definition: Title.php:1760
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that probably a stub it is not rendered in wiki pages or galleries in category pages allow injecting custom HTML after the section Any uses of the hook need to handle escaping see BaseTemplate::getToolbox and BaseTemplate::makeListItem for details on the format of individual items inside of this array or by returning and letting standard HTTP rendering take place modifiable or by returning false and taking over the output $out
Definition: hooks.txt:785
getArticleID( $flags=0)
Get the article ID for this Title from the link cache, adding it if necessary.
Definition: Title.php:3564
getAuthorsBetween( $old, $new, $limit, $options=[])
Get the authors between the given revisions or revision IDs.
Definition: Title.php:4560
null for the local wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition: hooks.txt:1599
static whoIs( $id)
Get the username corresponding to a given user ID.
Definition: User.php:884
canHaveTalkPage()
Can this title have a corresponding talk page?
Definition: Title.php:1092
canUseNoindex()
Whether the magic words INDEX and NOINDEX function for this page.
Definition: Title.php:5049
isJsSubpage()
Definition: Title.php:1426
const CONTENT_MODEL_WIKITEXT
Definition: Defines.php:235
wasLocalInterwiki()
Was this a local interwiki link?
Definition: Title.php:870
getSquidURLs()
Definition: Title.php:3973
$wgScript
The URL path to index.php.
static escapeId( $id, $options=[])
Given a value, escape it so that it can be used in an id attribute and return it. ...
Definition: Sanitizer.php:1255
checkPermissionHooks( $action, $user, $errors, $rigor, $short)
Check various permission hooks.
Definition: Title.php:2336
isContentPage()
Is this Title in a namespace which contains content? In other words, is this a content page...
Definition: Title.php:1221
MalformedTitleException is thrown when a TitleParser is unable to parse a title string.
getUserCaseDBKey()
Get the DB key with the initial letter case as specified by the user.
Definition: Title.php:960
isSemiProtected( $action='edit')
Is this page "semi-protected" - the only protection levels are listed in $wgSemiprotectedRestrictionL...
Definition: Title.php:3012
static newFromID( $id, $flags=0)
Create a new Title from an article ID.
Definition: Title.php:427
Handles the backend logic of moving a page from one title to another.
Definition: MovePage.php:31
isNamespaceProtected(User $user)
Determines if $user is unable to edit this page because it has been protected by $wgNamespaceProtecti...
Definition: Title.php:3072
static clearCaches()
Definition: Title.php:3707
countRevisionsBetween( $old, $new, $max=null)
Get the number of revisions between the given revision.
Definition: Title.php:4517
hasSubpages()
Does this have subpages? (Warning, usually requires an extra DB query.)
Definition: Title.php:3458
const NS_MAIN
Definition: Defines.php:64
$success
getSkinFromConfigSubpage()
Trim down a .css, .json, or .js subpage title to get the corresponding skin name. ...
Definition: Title.php:1352
getText()
Get the text form (spaces not underscores) of the main part.
Definition: Title.php:933
getSubpageText()
Get the lowest-level subpage name, i.e.
Definition: Title.php:1815
getBaseText()
Get the base page name without a namespace, i.e.
Definition: Title.php:1775
processing should stop and the error should be shown to the user * false
Definition: hooks.txt:187
static newMainPage()
Create a new Title for the Main Page.
Definition: Title.php:597
getParentCategoryTree( $children=[])
Get a tree of parent categories.
Definition: Title.php:4298
getDefaultMessageText()
Get the default (plain) message contents for an page that overrides an interface message key...
Definition: Title.php:4819
loadRestrictionsFromRows( $rows, $oldFashionedRestrictions=null)
Compiles list of active page restrictions from both page table (pre 1.10) and page_restrictions table...
Definition: Title.php:3279
checkSiteConfigPermissions( $action, $user, $errors, $rigor, $short)
Check sitewide CSS/JSON/JS permissions.
Definition: Title.php:2402
checkPageRestrictions( $action, $user, $errors, $rigor, $short)
Check against page_restrictions table requirements on this page.
Definition: Title.php:2514
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses & $ret
Definition: hooks.txt:1996
static isEveryoneAllowed( $right)
Check if all users may be assumed to have the given permission.
Definition: User.php:5058
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:1875
loadFromRow( $row)
Load Title object fields from a DB row.
Definition: Title.php:487
Apache License January AND DISTRIBUTION Definitions License shall mean the terms and conditions for use
setFragment( $fragment)
Set the fragment for this title.
Definition: Title.php:1631
checkSpecialsAndNSPermissions( $action, $user, $errors, $rigor, $short)
Check permissions on special pages & namespaces.
Definition: Title.php:2373
getSubjectNsText()
Get the namespace text of the subject (rather than talk) page.
Definition: Title.php:1058
isUserJsConfigPage()
Is this a JS "config" sub-page of a user page?
Definition: Title.php:1414
__wakeup()
Definition: Title.php:5282
getTalkPageIfDefined()
Get a Title object associated with the talk page of this article, if such a talk page can exist...
Definition: Title.php:1528
getTransWikiID()
Returns the DB name of the distant wiki which owns the object.
Definition: Title.php:893
get( $key, $maxAge=0.0)
Get the value for a key.
bool $mForcedContentModel
If a content model was forced via setContentModel() this will be true to avoid having other code path...
Definition: Title.php:103
wfExpandUrl( $url, $defaultProto=PROTO_CURRENT)
Expand a potentially local URL to a fully-qualified URL.
$wgActionPaths
Definition: img_auth.php:47
resultToError( $errors, $result)
Add the resulting error code to the errors array.
Definition: Title.php:2305
getFullUrlForRedirect( $query='', $proto=PROTO_CURRENT)
Get a url appropriate for making redirects based on an untrusted url arg.
Definition: Title.php:1948
isCssJsSubpage()
Definition: Title.php:1339
$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:1110
if(!isset( $args[0])) $lang
static escapeRegexReplacement( $string)
Escape a string to make it suitable for inclusion in a preg_replace() replacement parameter...
isUserCssConfigPage()
Is this a CSS "config" sub-page of a user page?
Definition: Title.php:1377
Status::newGood()` to allow deletion, and then `return false` from the hook function. Ensure you consume the 'ChangeTagAfterDelete' hook to carry out custom deletion actions. $tag:name of the tag $user:user initiating the action & $status:Status object. See above. 'ChangeTagsListActive':Allows you to nominate which of the tags your extension uses are in active use. & $tags:list of all active tags. Append to this array. 'ChangeTagsAfterUpdateTags':Called after tags have been updated with the ChangeTags::updateTags function. Params:$addedTags:tags effectively added in the update $removedTags:tags effectively removed in the update $prevTags:tags that were present prior to the update $rc_id:recentchanges table id $rev_id:revision table id $log_id:logging table id $params:tag params $rc:RecentChange being tagged when the tagging accompanies the action, or null $user:User who performed the tagging when the tagging is subsequent to the action, or null 'ChangeTagsAllowedAdd':Called when checking if a user can add tags to a change. & $allowedTags:List of all the tags the user is allowed to add. Any tags the user wants to add( $addTags) that are not in this array will cause it to fail. You may add or remove tags to this array as required. $addTags:List of tags user intends to add. $user:User who is adding the tags. 'ChangeUserGroups':Called before user groups are changed. $performer:The User who will perform the change $user:The User whose groups will be changed & $add:The groups that will be added & $remove:The groups that will be removed 'Collation::factory':Called if $wgCategoryCollation is an unknown collation. $collationName:Name of the collation in question & $collationObject:Null. Replace with a subclass of the Collation class that implements the collation given in $collationName. 'ConfirmEmailComplete':Called after a user 's email has been confirmed successfully. $user:user(object) whose email is being confirmed 'ContentAlterParserOutput':Modify parser output for a given content object. Called by Content::getParserOutput after parsing has finished. Can be used for changes that depend on the result of the parsing but have to be done before LinksUpdate is called(such as adding tracking categories based on the rendered HTML). $content:The Content to render $title:Title of the page, as context $parserOutput:ParserOutput to manipulate 'ContentGetParserOutput':Customize parser output for a given content object, called by AbstractContent::getParserOutput. May be used to override the normal model-specific rendering of page content. $content:The Content to render $title:Title of the page, as context $revId:The revision ID, as context $options:ParserOptions for rendering. To avoid confusing the parser cache, the output can only depend on parameters provided to this hook function, not on global state. $generateHtml:boolean, indicating whether full HTML should be generated. If false, generation of HTML may be skipped, but other information should still be present in the ParserOutput object. & $output:ParserOutput, to manipulate or replace 'ContentHandlerDefaultModelFor':Called when the default content model is determined for a given title. May be used to assign a different model for that title. $title:the Title in question & $model:the model name. Use with CONTENT_MODEL_XXX constants. 'ContentHandlerForModelID':Called when a ContentHandler is requested for a given content model name, but no entry for that model exists in $wgContentHandlers. Note:if your extension implements additional models via this hook, please use GetContentModels hook to make them known to core. $modeName:the requested content model name & $handler:set this to a ContentHandler object, if desired. 'ContentModelCanBeUsedOn':Called to determine whether that content model can be used on a given page. This is especially useful to prevent some content models to be used in some special location. $contentModel:ID of the content model in question $title:the Title in question. & $ok:Output parameter, whether it is OK to use $contentModel on $title. Handler functions that modify $ok should generally return false to prevent further hooks from further modifying $ok. 'ContribsPager::getQueryInfo':Before the contributions query is about to run & $pager:Pager object for contributions & $queryInfo:The query for the contribs Pager 'ContribsPager::reallyDoQuery':Called before really executing the query for My Contributions & $data:an array of results of all contribs queries $pager:The ContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'ContributionsLineEnding':Called before a contributions HTML line is finished $page:SpecialPage object for contributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'ContributionsToolLinks':Change tool links above Special:Contributions $id:User identifier $title:User page title & $tools:Array of tool links $specialPage:SpecialPage instance for context and services. Can be either SpecialContributions or DeletedContributionsPage. Extensions should type hint against a generic SpecialPage though. 'ConvertContent':Called by AbstractContent::convert when a conversion to another content model is requested. Handler functions that modify $result should generally return false to disable further attempts at conversion. $content:The Content object to be converted. $toModel:The ID of the content model to convert to. $lossy:boolean indicating whether lossy conversion is allowed. & $result:Output parameter, in case the handler function wants to provide a converted Content object. Note that $result->getContentModel() must return $toModel. 'ContentSecurityPolicyDefaultSource':Modify the allowed CSP load sources. This affects all directives except for the script directive. If you want to add a script source, see ContentSecurityPolicyScriptSource hook. & $defaultSrc:Array of Content-Security-Policy allowed sources $policyConfig:Current configuration for the Content-Security-Policy header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyDirectives':Modify the content security policy directives. Use this only if ContentSecurityPolicyDefaultSource and ContentSecurityPolicyScriptSource do not meet your needs. & $directives:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'ContentSecurityPolicyScriptSource':Modify the allowed CSP script sources. Note that you also have to use ContentSecurityPolicyDefaultSource if you want non-script sources to be loaded from whatever you add. & $scriptSrc:Array of CSP directives $policyConfig:Current configuration for the CSP header $mode:ContentSecurityPolicy::REPORT_ONLY_MODE or ContentSecurityPolicy::FULL_MODE depending on type of header 'CustomEditor':When invoking the page editor Return true to allow the normal editor to be used, or false if implementing a custom editor, e.g. for a special namespace, etc. $article:Article being edited $user:User performing the edit 'DatabaseOraclePostInit':Called after initialising an Oracle database $db:the DatabaseOracle object 'DeletedContribsPager::reallyDoQuery':Called before really executing the query for Special:DeletedContributions Similar to ContribsPager::reallyDoQuery & $data:an array of results of all contribs queries $pager:The DeletedContribsPager object hooked into $offset:Index offset, inclusive $limit:Exact query limit $descending:Query direction, false for ascending, true for descending 'DeletedContributionsLineEnding':Called before a DeletedContributions HTML line is finished. Similar to ContributionsLineEnding $page:SpecialPage object for DeletedContributions & $ret:the HTML line $row:the DB row for this line & $classes:the classes to add to the surrounding< li > & $attribs:associative array of other HTML attributes for the< li > element. Currently only data attributes reserved to MediaWiki are allowed(see Sanitizer::isReservedDataAttribute). 'DeleteUnknownPreferences':Called by the cleanupPreferences.php maintenance script to build a WHERE clause with which to delete preferences that are not known about. This hook is used by extensions that have dynamically-named preferences that should not be deleted in the usual cleanup process. For example, the Gadgets extension creates preferences prefixed with 'gadget-', and so anything with that prefix is excluded from the deletion. &where:An array that will be passed as the $cond parameter to IDatabase::select() to determine what will be deleted from the user_properties table. $db:The IDatabase object, useful for accessing $db->buildLike() etc. 'DifferenceEngineAfterLoadNewText':called in DifferenceEngine::loadNewText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before returning true from this function. $differenceEngine:DifferenceEngine object 'DifferenceEngineLoadTextAfterNewContentIsLoaded':called in DifferenceEngine::loadText() after the new revision 's content has been loaded into the class member variable $differenceEngine->mNewContent but before checking if the variable 's value is null. This hook can be used to inject content into said class member variable. $differenceEngine:DifferenceEngine object 'DifferenceEngineMarkPatrolledLink':Allows extensions to change the "mark as patrolled" link which is shown both on the diff header as well as on the bottom of a page, usually wrapped in a span element which has class="patrollink". $differenceEngine:DifferenceEngine object & $markAsPatrolledLink:The "mark as patrolled" link HTML(string) $rcid:Recent change ID(rc_id) for this change(int) 'DifferenceEngineMarkPatrolledRCID':Allows extensions to possibly change the rcid parameter. For example the rcid might be set to zero due to the user being the same as the performer of the change but an extension might still want to show it under certain conditions. & $rcid:rc_id(int) of the change or 0 $differenceEngine:DifferenceEngine object $change:RecentChange object $user:User object representing the current user 'DifferenceEngineNewHeader':Allows extensions to change the $newHeader variable, which contains information about the new revision, such as the revision 's author, whether the revision was marked as a minor edit or not, etc. $differenceEngine:DifferenceEngine object & $newHeader:The string containing the various #mw-diff-otitle[1-5] divs, which include things like revision author info, revision comment, RevisionDelete link and more $formattedRevisionTools:Array containing revision tools, some of which may have been injected with the DiffRevisionTools hook $nextlink:String containing the link to the next revision(if any) $status
Definition: hooks.txt:1277
static getSubject( $index)
Get the subject namespace index for a given namespace Special namespaces (NS_MEDIA, NS_SPECIAL) are always the subject.
static rawElement( $element, $attribs=[], $contents='')
Returns an HTML element in a string.
Definition: Html.php:210
$wgRestrictionLevels
Rights which can be required for each protection level (via action=protect)
secureAndSplit()
Secure and split - main initialisation function for this object.
Definition: Title.php:3742
bool $mIsBigDeletion
Would deleting this page be a big deletion?
Definition: Title.php:180
inNamespaces()
Returns true if the title is inside one of the specified namespaces.
Definition: Title.php:1182
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:1982
isTalkPage()
Is this a talk page of some sort?
Definition: Title.php:1506
Handles purging appropriate CDN URLs given a title (or titles)
string $mUrlform
URL-encoded form of the main part.
Definition: Title.php:67
$sort
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:5095
null for the local wiki Added in
Definition: hooks.txt:1599
This code would result in ircNotify being run twice when an article is and once for brion Hooks can return three possible true was required This is the default since MediaWiki *some string
Definition: hooks.txt:175
isBigDeletion()
Check whether the number of revisions of this page surpasses $wgDeleteRevisionsLimit.
Definition: Title.php:4465
userCan( $action, $user=null, $rigor='secure')
Can $user perform $action on this page?
Definition: Title.php:2180
getUserPermissionsErrors( $action, $user, $rigor='secure', $ignoreErrors=[])
Can $user perform $action on this page?
Definition: Title.php:2204
const NS_SPECIAL
Definition: Defines.php:53
const PROTO_CURRENT
Definition: Defines.php:222
getNotificationTimestamp( $user=null)
Get the timestamp when this page was updated since the user last saw it.
Definition: Title.php:4914
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:1559
string $prefixedText
Text form including namespace/interwiki, initialised on demand.
Definition: Title.php:145
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency MediaWikiServices
Definition: injection.txt:23
static getInterwikiLookup()
B/C kludge: provide an InterwikiLookup for use by Title.
Definition: Title.php:203
$wgSemiprotectedRestrictionLevels
Restriction levels that should be considered "semiprotected".
static isTalk( $index)
Is the given namespace a talk namespace?
$wgEmailConfirmToEdit
Should editors be required to have a validated e-mail address before being allowed to edit...
and how to run hooks for an and one after Each event has a preferably in CamelCase For ArticleDelete hook A clump of code and data that should be run when an event happens This can be either a function and a chunk of or an object and a method hook function The function part of a third party developers and local administrators to define code that will be run at certain points in the mainline and to modify the data run by that mainline code Hooks can keep mainline code and make it easier to write extensions Hooks are a principled alternative to local patches for two options in MediaWiki One reverses the order of a title before displaying the article
Definition: hooks.txt:23
inNamespace( $ns)
Returns true if the title is inside the specified namespace.
Definition: Title.php:1171
getPrefixedText()
Get the prefixed title with spaces.
Definition: Title.php:1696
moveTo(&$nt, $auth=true, $reason='', $createRedirect=true, array $changeTags=[])
Move a title to a new location.
Definition: Title.php:4053
getBaseTitle()
Get the base page name title, i.e.
Definition: Title.php:1800
getParentCategories()
Get categories to which this Title belongs and return an array of categories&#39; names.
Definition: Title.php:4263
static newFromRow( $row)
Make a Title object from a DB row.
Definition: Title.php:475
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:3783
getBacklinkCache()
Get a backlink cache object.
Definition: Title.php:5040
$wgArticlePath
Definition: img_auth.php:46
static newFromLinkTarget(LinkTarget $linkTarget)
Create a new Title from a LinkTarget.
Definition: Title.php:251
getCategorySortkey( $prefix='')
Returns the raw sort key to be used for categories, with the specified prefix.
Definition: Title.php:5069
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:239
wfLocalFile( $title)
Get an object referring to a locally registered file.
The TitleArray class only exists to provide the newFromResult method at pre- sent.
Definition: TitleArray.php:33
see documentation in includes Linker php for Linker::makeImageLink or false for current used if you return false $parser
Definition: hooks.txt:1813
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:3841
getSkinFromCssJsSubpage()
Definition: Title.php:1366
This list may contain false positives That usually means there is additional text with links below the first Each row contains links to the first and second as well as the first line of the second redirect text
const DB_MASTER
Definition: defines.php:26
$wgRestrictionTypes
Set of available actions that can be restricted via action=protect You probably shouldn&#39;t change this...
this hook is for auditing only RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist & $tables
Definition: hooks.txt:990
getTalkPage()
Get a Title object associated with the talk page of this article.
Definition: Title.php:1515
getNamespace()
Get the namespace index.
static isContent( $index)
Does this namespace contain content, for the purposes of calculating statistics, etc?
wfGetLangObj( $langcode=false)
Return a Language object from $langcode.
timestamp( $ts=0)
Convert a timestamp in one of the formats accepted by wfTimestamp() to the format used for inserting ...
array $mCascadeSources
Where are the cascading restrictions coming from on this page?
Definition: Title.php:132
missingPermissionError( $action, $short)
Get a description array when the user doesn&#39;t have the right to perform $action (i.e.
Definition: Title.php:2794
isSiteConfigPage()
Could this MediaWiki namespace page contain custom CSS, JSON, or JavaScript for the global UI...
Definition: Title.php:1302
fixSpecialName()
If the Title refers to a special page alias which is not the local default, resolve the alias...
Definition: Title.php:1147
The index of the header message $result[1]=The index of the body text message $result[2 through n]=Parameters passed to body text message. Please note the header message cannot receive/use parameters. 'ImportHandleLogItemXMLTag':When parsing a XML tag in a log item. Return false to stop further processing of the tag $reader:XMLReader object $logInfo:Array of information 'ImportHandlePageXMLTag':When parsing a XML tag in a page. Return false to stop further processing of the tag $reader:XMLReader object & $pageInfo:Array of information 'ImportHandleRevisionXMLTag':When parsing a XML tag in a page revision. Return false to stop further processing of the tag $reader:XMLReader object $pageInfo:Array of page information $revisionInfo:Array of revision information 'ImportHandleToplevelXMLTag':When parsing a top level XML tag. Return false to stop further processing of the tag $reader:XMLReader object 'ImportHandleUnknownUser':When a user doesn 't exist locally, this hook is called to give extensions an opportunity to auto-create it. If the auto-creation is successful, return false. $name:User name 'ImportHandleUploadXMLTag':When parsing a XML tag in a file upload. Return false to stop further processing of the tag $reader:XMLReader object $revisionInfo:Array of information 'ImportLogInterwikiLink':Hook to change the interwiki link used in log entries and edit summaries for transwiki imports. & $fullInterwikiPrefix:Interwiki prefix, may contain colons. & $pageTitle:String that contains page title. 'ImportSources':Called when reading from the $wgImportSources configuration variable. Can be used to lazy-load the import sources list. & $importSources:The value of $wgImportSources. Modify as necessary. See the comment in DefaultSettings.php for the detail of how to structure this array. 'InfoAction':When building information to display on the action=info page. $context:IContextSource object & $pageInfo:Array of information 'InitializeArticleMaybeRedirect':MediaWiki check to see if title is a redirect. & $title:Title object for the current page & $request:WebRequest & $ignoreRedirect:boolean to skip redirect check & $target:Title/string of redirect target & $article:Article object 'InternalParseBeforeLinks':during Parser 's internalParse method before links but after nowiki/noinclude/includeonly/onlyinclude and other processings. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InternalParseBeforeSanitize':during Parser 's internalParse method just before the parser removes unwanted/dangerous HTML tags and after nowiki/noinclude/includeonly/onlyinclude and other processings. Ideal for syntax-extensions after template/parser function execution which respect nowiki and HTML-comments. & $parser:Parser object & $text:string containing partially parsed text & $stripState:Parser 's internal StripState object 'InterwikiLoadPrefix':When resolving if a given prefix is an interwiki or not. Return true without providing an interwiki to continue interwiki search. $prefix:interwiki prefix we are looking for. & $iwData:output array describing the interwiki with keys iw_url, iw_local, iw_trans and optionally iw_api and iw_wikiid. 'InvalidateEmailComplete':Called after a user 's email has been invalidated successfully. $user:user(object) whose email is being invalidated 'IRCLineURL':When constructing the URL to use in an IRC notification. Callee may modify $url and $query, URL will be constructed as $url . $query & $url:URL to index.php & $query:Query string $rc:RecentChange object that triggered url generation 'IsFileCacheable':Override the result of Article::isFileCacheable()(if true) & $article:article(object) being checked 'IsTrustedProxy':Override the result of IP::isTrustedProxy() & $ip:IP being check & $result:Change this value to override the result of IP::isTrustedProxy() 'IsUploadAllowedFromUrl':Override the result of UploadFromUrl::isAllowedUrl() $url:URL used to upload from & $allowed:Boolean indicating if uploading is allowed for given URL 'isValidEmailAddr':Override the result of Sanitizer::validateEmail(), for instance to return false if the domain name doesn 't match your organization. $addr:The e-mail address entered by the user & $result:Set this and return false to override the internal checks 'isValidPassword':Override the result of User::isValidPassword() $password:The password entered by the user & $result:Set this and return false to override the internal checks $user:User the password is being validated for 'Language::getMessagesFileName':$code:The language code or the language we 're looking for a messages file for & $file:The messages file path, you can override this to change the location. 'LanguageGetNamespaces':Provide custom ordering for namespaces or remove namespaces. Do not use this hook to add namespaces. Use CanonicalNamespaces for that. & $namespaces:Array of namespaces indexed by their numbers 'LanguageGetTranslatedLanguageNames':Provide translated language names. & $names:array of language code=> language name $code:language of the preferred translations 'LanguageLinks':Manipulate a page 's language links. This is called in various places to allow extensions to define the effective language links for a page. $title:The page 's Title. & $links:Array with elements of the form "language:title" in the order that they will be output. & $linkFlags:Associative array mapping prefixed links to arrays of flags. Currently unused, but planned to provide support for marking individual language links in the UI, e.g. for featured articles. 'LanguageSelector':Hook to change the language selector available on a page. $out:The output page. $cssClassName:CSS class name of the language selector. 'LinkBegin':DEPRECATED since 1.28! Use HtmlPageLinkRendererBegin instead. Used when generating internal and interwiki links in Linker::link(), before processing starts. Return false to skip default processing and return $ret. See documentation for Linker::link() for details on the expected meanings of parameters. $skin:the Skin object $target:the Title that the link is pointing to & $html:the contents that the< a > tag should have(raw HTML) $result
Definition: hooks.txt:1994
checkReadPermissions( $action, $user, $errors, $rigor, $short)
Check that the user is allowed to read this page.
Definition: Title.php:2717
static convertByteClassToUnicodeClass( $byteClass)
Utility method for converting a character sequence from bytes to Unicode.
Definition: Title.php:648
getFragment()
Get the link fragment (i.e.
static hasTalkNamespace( $index)
Does this namespace ever have a talk namespace?
static nameOf( $id)
Get the prefixed DB key associated with an ID.
Definition: Title.php:612
Deferrable Update for closure/callback updates that should use auto-commit mode.
checkCascadingSourcesRestrictions( $action, $user, $errors, $rigor, $short)
Check restrictions on cascading pages.
Definition: Title.php:2548
The User object encapsulates all of the user-specific settings (user_id, name, rights, email address, options, last login time).
Definition: User.php:47
$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.
getSubpage( $text)
Get the title for a subpage of the current page.
Definition: Title.php:1836
$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:120
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:2081
quickUserCan( $action, $user=null)
Can $user perform $action on this page? This skips potentially expensive cascading permission checks ...
Definition: Title.php:2167
isSiteCssConfigPage()
Is this a sitewide CSS "config" page?
Definition: Title.php:1437
flushRestrictions()
Flush the protection cache in this object and force reload from the database.
Definition: Title.php:3407
string $mDbkeyform
Main part with underscores.
Definition: Title.php:70
getNsText()
Get the namespace text.
Definition: Title.php:1034
getCanonicalURL( $query='', $query2=false)
Get the URL for a canonical link, for use in things like IRC and e-mail notifications.
Definition: Title.php:2130
__sleep()
Definition: Title.php:5270
string bool $mOldRestrictions
Comma-separated set of permission keys indicating who can move or edit the page from the page table...
Definition: Title.php:117
__construct()
Definition: Title.php:210
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:850
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:902
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation use $formDescriptor instead default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt
isUserConfigPage()
Is this a "config" (.css, .json, or .js) sub-page of a user page?
Definition: Title.php:1327
deleteTitleProtection()
Remove any title protection due to page existing.
Definition: Title.php:2994
static getMain()
Get the RequestContext object associated with the main request.
int $mNamespace
Namespace index, i.e.
Definition: Title.php:76
set( $key, $value, $rank=self::RANK_TOP)
Set a key/value pair.
hasSourceText()
Does this page have source text?
Definition: Title.php:4761
isUserJsonConfigPage()
Is this a JSON "config" sub-page of a user page?
Definition: Title.php:1400
resetArticleID( $newid)
This clears some fields in this object, and clears any associated keys in the "bad links" section of ...
Definition: Title.php:3685
$wgWhitelistRead
Pages anonymous user may see, set as an array of pages titles.
static getTitleCache()
Definition: Title.php:387
static getTitleFormatter()
B/C kludge: provide a TitleParser for use by Title.
Definition: Title.php:191
Class to invalidate the HTML cache of all the pages linking to a given title.
getDBkey()
Get the main part with underscores.
Definition: Title.php:951
__toString()
Return a string representation of this title.
Definition: Title.php:1710
getPrefixedURL()
Get a URL-encoded title (not an actual URL) including interwiki.
Definition: Title.php:1856
isSpecial( $name)
Returns true if this title resolves to the named special page.
Definition: Title.php:1129
hasFragment()
Check if a Title fragment is set.
Definition: Title.php:1599
getRedirectsHere( $ns=null)
Get all extant redirects to this Title.
Definition: Title.php:4980
$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:3825
areRestrictionsCascading()
Returns cascading restrictions for the current article.
Definition: Title.php:3260
wfFindFile( $title, $options=[])
Find a file.
isConversionTable()
Is this a conversion table for the LanguageConverter?
Definition: Title.php:1272
const NS_MEDIA
Definition: Defines.php:52
getRootText()
Get the root page name text without a namespace, i.e.
Definition: Title.php:1740
$res
Definition: database.txt:21
bool string $mContentModel
ID of the page&#39;s content model, i.e.
Definition: Title.php:97
canExist()
Is this in a namespace that allows actual pages?
Definition: Title.php:1101
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:3486
$wgBlockDisablesLogin
If true, blocked users will not be allowed to login.
const GAID_FOR_UPDATE
Used to be GAID_FOR_UPDATE define.
Definition: Title.php:54
null $mRedirect
Is the article at this title a redirect?
Definition: Title.php:161
wfTimestampNow()
Convenience function; returns MediaWiki timestamp for the present time.
$wgWhitelistReadRegexp
Pages anonymous user may see, set as an array of regular expressions.
hasContentModel( $id)
Convenience method for checking a title&#39;s content model name.
Definition: Title.php:1009
isValidRedirectTarget()
Check if this Title is a valid redirect target.
Definition: Title.php:5016
isCascadeProtected()
Cascading protection: Return true if cascading restrictions apply to this page, false if not...
Definition: Title.php:3090
isValidMoveTarget( $nt)
Checks if $this can be moved to a given Title.
Definition: Title.php:4215
isValidMoveOperation(&$nt, $auth=true, $reason='')
Check whether a given move operation would be valid.
Definition: Title.php:3997
getCdnUrls()
Get a list of URLs to purge from the CDN cache when this page changes.
Definition: Title.php:3943
$cache
Definition: mcc.php:33
getDefaultNamespace()
Get the default namespace index, for when there is no namespace.
Definition: Title.php:1578
const NS_CATEGORY
Definition: Defines.php:78
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition: hooks.txt:1996
isAllowed( $action='')
Internal mechanics of testing a permission.
Definition: User.php:3869
isSubpageOf(Title $title)
Check if this title is a subpage of another title.
Definition: Title.php:4659
array $mNotificationTimestamp
Associative array of user ID -> timestamp/false.
Definition: Title.php:164
static exists( $index)
Returns whether the specified namespace exists.
static getContentNamespaces()
Get a list of all namespace indices which are considered to contain content.
getFullText()
Get the prefixed title with spaces, plus any fragment (part beginning with &#39;#&#39;)
Definition: Title.php:1720
static newFromIDs( $ids)
Make an array of titles from an array of IDs.
Definition: Title.php:449
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:174
isCssSubpage()
Definition: Title.php:1389
static newFromResult( $res)
Definition: TitleArray.php:40
isMainPage()
Is this the mainpage?
Definition: Title.php:1252
prefix( $name)
Prefix some arbitrary text with the namespace or interwiki prefix of this object. ...
Definition: Title.php:1658
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:936
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:526
getNamespaceKey( $prepend='nstab-')
Generate strings used for xml &#39;id&#39; names in monobook tabs.
Definition: Title.php:4952
static factory( $code)
Get a cached or new language object for a given language code.
Definition: Language.php:214
getFullURL( $query='', $query2=false, $proto=PROTO_RELATIVE)
Get a real URL referring to this title, with interwiki link and fragment.
Definition: Title.php:1913
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:974
const PROTO_RELATIVE
Definition: Defines.php:221
string $mInterwiki
Interwiki prefix.
Definition: Title.php:79
equals(Title $title)
Compare with another title.
Definition: Title.php:4646
static MapCacheLRU $titleCache
Definition: Title.php:41
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:4752
isProtected( $action='')
Does the title correspond to a protected article?
Definition: Title.php:3040
const NS_FILE
Definition: Defines.php:70
areRestrictionsLoaded()
Accessor for mRestrictionsLoaded.
Definition: Title.php:3207
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:82
presenting them properly to the user as errors is done by the caller return true use this to change the list i e etc $rev
Definition: hooks.txt:1780
isSiteJsonConfigPage()
Is this a sitewide JSON "config" page?
Definition: Title.php:1455
isSubpage()
Is this a subpage?
Definition: Title.php:1261
getInterwiki()
Get the interwiki prefix.
Definition: Title.php:861
const RAW
Definition: Revision.php:57
static getForTitle(Title $title)
Returns the appropriate ContentHandler singleton for the given title.
namespace and then decline to actually register it & $namespaces
Definition: hooks.txt:936
This document is intended to provide useful advice for parties seeking to redistribute MediaWiki to end users It s targeted particularly at maintainers for Linux since it s been observed that distribution packages of MediaWiki often break We ve consistently had to recommend that users seeking support use official tarballs instead of their distribution s and this often solves whatever problem the user is having It would be nice if this could such as
Definition: distributors.txt:9
getBrokenLinksFrom()
Get an array of Title objects referring to non-existent articles linked from this page...
Definition: Title.php:3907
const NS_MEDIAWIKI
Definition: Defines.php:72
const PROTO_HTTP
Definition: Defines.php:219
if(defined( 'MW_SETUP_CALLBACK')) $fname
Customization point after all loading (constants, functions, classes, DefaultSettings, LocalSettings).
Definition: Setup.php:121
static groupHasPermission( $group, $role)
Check, if the given group has the given permission.
Definition: User.php:5038
array $mRestrictionsExpiry
When do the restrictions on this page expire?
Definition: Title.php:126
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:3895
static isWatchable( $index)
Can pages in a namespace be watched?
static newFromTextThrow( $text, $defaultNamespace=NS_MAIN)
Like Title::newFromText(), but throws MalformedTitleException when the title is invalid, rather than returning null.
Definition: Title.php:313
static newFromTitleValue(TitleValue $titleValue)
Create a new Title from a TitleValue.
Definition: Title.php:240
isValid()
Returns true if the title is valid, false if it is invalid.
Definition: Title.php:814
CONTENT_MODEL_JAVASCRIPT
Allow users to upload files.
static equals( $ns1, $ns2)
Returns whether the specified namespaces are the same namespace.
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:1542
int $mLength
The page length, 0 for special pages.
Definition: Title.php:158
static makeTitleSafe( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:573
hasSubjectNamespace( $ns)
Returns true if the title has the same subject namespace as the namespace specified.
Definition: Title.php:1210
isSpecialPage()
Returns true if this is a special page.
Definition: Title.php:1119
getEditURL()
Get the edit URL for this Title.
Definition: Title.php:2144
static makeTitle( $ns, $title, $fragment='', $interwiki='')
Create a new Title from a namespace index and a DB key.
Definition: Title.php:545
$wgLegalTitleChars
Allowed title characters – regex character class Don&#39;t change this unless you know what you&#39;re doing...
static isCapitalized( $index)
Is the namespace first-letter capitalized?
injection txt This is an overview of how MediaWiki makes use of dependency injection The design described here grew from the discussion of RFC T384 The term dependency this means that anything an object needs to operate should be injected from the the object itself should only know narrow no concrete implementation of the logic it relies on The requirement to inject everything typically results in an architecture that based on two main types of and essentially stateless service objects that use other service objects to operate on the value objects As of the beginning MediaWiki is only starting to use the DI approach Much of the code still relies on global state or direct resulting in a highly cyclical dependency which acts as the top level factory for services in MediaWiki which can be used to gain access to default instances of various services MediaWikiServices however also allows new services to be defined and default services to be redefined Services are defined or redefined by providing a callback the instantiator that will return a new instance of the service When it will create an instance of MediaWikiServices and populate it with the services defined in the files listed by thereby bootstrapping the DI framework Per $wgServiceWiringFiles lists includes ServiceWiring php
Definition: injection.txt:35
$wgNamespaceProtection
Set the minimum permissions required to edit pages in each namespace.
validateFileMoveOperation( $nt)
Check if the requested move target is a valid file move target.
Definition: Title.php:4024
selectFieldValues( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a list of single field values from result rows.
isSingleRevRedirect()
Checks if this page is just a one-rev redirect.
Definition: Title.php:4164
const PROTO_CANONICAL
Definition: Defines.php:223
static getCanonicalName( $index)
Returns the canonical (English) name for a given index.
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:4418
static newFromDBkey( $key)
Create a new Title from a prefixed DB key.
Definition: Title.php:221
getPageViewLanguage()
Get the language in which the content of this page is written when viewed by user.
Definition: Title.php:5155
getSubpageUrlForm()
Get a URL-encoded form of the subpage text.
Definition: Title.php:1845
$mCascadingRestrictions
Caching the results of getCascadeProtectionSources.
Definition: Title.php:123
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:1642
linkcache txt The LinkCache class maintains a list of article titles and the information about whether or not the article exists in the database This is used to mark up links when displaying a page If the same link appears more than once on any page then it only has to be looked up once In most cases link lookups are done in batches with the LinkBatch class or the equivalent in so the link cache is mostly useful for short snippets of parsed and for links in the navigation areas of the skin The link cache was formerly used to track links used in a document for the purposes of updating the link tables This application is now deprecated To create a you can use the following $titles
Definition: linkcache.txt:17
static newFromURL( $url)
THIS IS NOT THE FUNCTION YOU WANT.
Definition: Title.php:364
countAuthorsBetween( $old, $new, $limit, $options=[])
Get the number of authors between the given revisions or revision IDs.
Definition: Title.php:4635
getTitleProtectionInternal()
Fetch title protection settings.
Definition: Title.php:2947
getRestrictionTypes()
Returns restriction types for the current Title.
Definition: Title.php:2897
TitleValue $mTitleValue
A corresponding TitleValue object.
Definition: Title.php:177
$parent
Definition: pageupdater.txt:71
bool $mPageLanguage
The (string) language code of the page&#39;s language and content code.
Definition: Title.php:170
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:1316
isTrans()
Determine whether the object refers to a page within this project and is transcludable.
Definition: Title.php:880
getContentModel( $flags=0)
Get the page&#39;s content model id, see the CONTENT_MODEL_XXX constants.
Definition: Title.php:986
string $mFragment
Title fragment (i.e.
Definition: Title.php:85
static hasSubpages( $index)
Does the namespace allow subpages?
int $mArticleID
Article ID, fetched from the link cache on demand.
Definition: Title.php:88
static getTalk( $index)
Get the talk namespace index for a given namespace.
int $mDefaultNamespace
Namespace index when there is no namespace.
Definition: Title.php:155
static capitalize( $text, $ns=NS_MAIN)
Capitalize a text string for a title if it belongs to a namespace that capitalizes.
Definition: Title.php:3722
string [] $wgRawHtmlMessages
List of messages which might contain raw HTML.
static newFatalPermissionDeniedStatus( $permission)
Factory function for fatal permission-denied errors.
Definition: User.php:5686
getPreviousRevisionID( $revId, $flags=0)
Get the revision ID of the previous revision.
Definition: Title.php:4397
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:5117
const CACHE_MAX
Title::newFromText maintains a cache to avoid expensive re-normalization of commonly used titles...
Definition: Title.php:48
static isMovable( $index)
Can pages in the given namespace be moved?
Definition: MWNamespace.php:89
$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:1339
isRawHtmlMessage()
Is this a message which can contain raw HTML?
Definition: Title.php:1491
isCssOrJsPage()
Definition: Title.php:1314
static escapeFragmentForURL( $fragment)
Escape a text fragment, say from a link, for a URL.
Definition: Title.php:778
getFragmentForURL()
Get the fragment in URL form, including the "#" character if there is one.
Definition: Title.php:1608
$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:4676
getLatestRevID( $flags=0)
What is the page_latest field for this page?
Definition: Title.php:3653
pageCond()
Get an associative array for selecting this title from the "page" table.
Definition: Title.php:4325
static invalidateModuleCache(Title $title, Revision $old=null, Revision $new=null, $wikiId)
Clear the preloadTitleInfo() cache for all wiki modules on this wiki on page change if it was a JS or...
getInterwiki()
The interwiki component of this LinkTarget.
const CONTENT_MODEL_CSS
Definition: Defines.php:237
getEarliestRevTime( $flags=0)
Get the oldest revision timestamp of this page.
Definition: Title.php:4445
isDeletedQuick()
Is there a version of this page in the deletion archive?
Definition: Title.php:3538
isNewPage()
Check if this is a new page.
Definition: Title.php:4455
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:3248
static compare(LinkTarget $a, LinkTarget $b)
Callback for usort() to do title sorts by (namespace, title)
Definition: Title.php:795
canTalk()
Can this title have a corresponding talk page?
Definition: Title.php:1080
bool $mRestrictionsLoaded
Boolean for initialisation on demand.
Definition: Title.php:135
static legalChars()
Get a regex character class describing the legal characters in a link.
Definition: Title.php:634
getCascadeProtectionSources( $getPages=true)
Cascading protection: Get the source of any cascading restrictions on this page.
Definition: Title.php:3121
isRedirect( $flags=0)
Is this an article that is a redirect page? Uses link cache, adding it if necessary.
Definition: Title.php:3590
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:276
isSiteJsConfigPage()
Is this a sitewide JS "config" page?
Definition: Title.php:1473
if(! $wgDBerrorLogTZ) $wgRequest
Definition: Setup.php:747
$wgServer
URL of the server.
isDeleted()
Is there a version of this page in the deletion archive?
Definition: Title.php:3513
isLocal()
Determine whether the object refers to a page within this project (either this wiki or a wiki with a ...
Definition: Title.php:835
array $mRestrictions
Array of groups allowed to edit this article.
Definition: Title.php:109
checkQuickPermissions( $action, $user, $errors, $rigor, $short)
Permissions checks that fail most often, and which are easiest to test.
Definition: Title.php:2235
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:2924
bool $mLocalInterwiki
Was this Title created from a string with a local interwiki prefix?
Definition: Title.php:82
int $mEstimateRevisions
Estimated number of revisions; null of not loaded.
Definition: Title.php:106
getEditNotices( $oldid=0)
Get a list of rendered edit notices for this page.
Definition: Title.php:5199
update( $table, $values, $conds, $fname=__METHOD__, $options=[])
UPDATE wrapper.
getTouched( $db=null)
Get the last touched timestamp.
Definition: Title.php:4900
getTalkNsText()
Get the namespace text of the talk page.
Definition: Title.php:1068
static getSelectFields()
Returns a list of fields that are to be selected for initializing Title objects or LinkCache entries...
Definition: Title.php:401
static subjectEquals( $ns1, $ns2)
Returns whether the specified namespaces share the same subject.
delete( $table, $conds, $fname=__METHOD__)
DELETE query wrapper.
string $mTextform
Text form (spaces not underscores) of the main part.
Definition: Title.php:64
getAllRestrictions()
Accessor/initialisation for mRestrictions.
Definition: Title.php:3234
getInternalURL( $query='', $query2=false)
Get the URL form for an internal link.
Definition: Title.php:2106
static makeName( $ns, $title, $fragment='', $interwiki='', $canonicalNamespace=false)
Make a prefixed DB key from a DB key and a namespace index.
Definition: Title.php:752
$content
Definition: pageupdater.txt:72
addQuotes( $s)
Adds quotes and backslashes.
bool int $mLatestID
ID of most recent revision.
Definition: Title.php:91
setContentModel( $model)
Set a proposed content model for the page for permissions checking.
Definition: Title.php:1024
$wgVariantArticlePath
Like $wgArticlePath, but on multi-variant wikis, this provides a path format that describes which par...
getRelativeRevisionID( $revId, $flags, $dir)
Get next/previous revision ID relative to another revision ID.
Definition: Title.php:4341
static getQueryInfo()
Return the tables, fields, and join conditions to be selected to create a new page object...
Definition: WikiPage.php:373
$wgMaximumMovedPages
Maximum number of pages to move at once when moving subpages with a page.
getUserPermissionsErrorsInternal( $action, $user, $rigor='secure', $short=false)
Can $user perform $action on this page? This is an internal function, with multiple levels of checks ...
Definition: Title.php:2817
within a display generated by the Derivative if and wherever such third party notices normally appear The contents of the NOTICE file are for informational purposes only and do not modify the License You may add Your own attribution notices within Derivative Works that You alongside or as an addendum to the NOTICE text from the provided that such additional attribution notices cannot be construed as modifying the License You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for or distribution of Your or for any such Derivative Works as a provided Your and distribution of the Work otherwise complies with the conditions stated in this License Submission of Contributions Unless You explicitly state any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this without any additional terms or conditions Notwithstanding the nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions Trademarks This License does not grant permission to use the trade names
getLength( $flags=0)
What is the length of this page? Uses link cache, adding it if necessary.
Definition: Title.php:3625
isWikitextPage()
Does that page contain wikitext, or it is JS, CSS or whatever?
Definition: Title.php:1284
static singleton()
Get the signleton instance of this class.
purgeSquid()
Purge all applicable CDN URLs.
Definition: Title.php:3980
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1487
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:910
bool $mHasCascadingRestrictions
Are cascading restrictions in effect on this page?
Definition: Title.php:129
getPartialURL()
Get the URL-encoded form of the main part.
Definition: Title.php:942
getPrefixedDBkey()
Get the prefixed database key form.
Definition: Title.php:1684
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:4091
estimateRevisionCount()
Get the approximate revision count of this page.
Definition: Title.php:4494
checkActionPermissions( $action, $user, $errors, $rigor, $short)
Check action permissions not already checked in checkQuickPermissions.
Definition: Title.php:2595
$matches
getNextRevisionID( $revId, $flags=0)
Get the revision ID of the next revision.
Definition: Title.php:4408
string $mUserCaseDBKey
Database key with the initial letter in the case specified by the user.
Definition: Title.php:73
static newFromText( $text, $defaultNamespace=NS_MAIN)
Create a new Title from text, such as what one would find in a link.
Definition: Title.php:280