MediaWiki  master
Title.php
Go to the documentation of this file.
1 <?php
30 
39 class Title implements LinkTarget, IDBAccessObject {
41  private static $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 
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  public static function compare( LinkTarget $a, LinkTarget $b ) {
779  return $a->getNamespace() <=> $b->getNamespace()
780  ?: strcmp( $a->getText(), $b->getText() );
781  }
782 
797  public function isValid() {
798  if ( !MWNamespace::exists( $this->mNamespace ) ) {
799  return false;
800  }
801 
802  try {
803  $parser = MediaWikiServices::getInstance()->getTitleParser();
804  $parser->parseTitle( $this->mDbkeyform, $this->mNamespace );
805  return true;
806  } catch ( MalformedTitleException $ex ) {
807  return false;
808  }
809  }
810 
818  public function isLocal() {
819  if ( $this->isExternal() ) {
820  $iw = self::getInterwikiLookup()->fetch( $this->mInterwiki );
821  if ( $iw ) {
822  return $iw->isLocal();
823  }
824  }
825  return true;
826  }
827 
833  public function isExternal() {
834  return $this->mInterwiki !== '';
835  }
836 
844  public function getInterwiki() {
845  return $this->mInterwiki;
846  }
847 
853  public function wasLocalInterwiki() {
854  return $this->mLocalInterwiki;
855  }
856 
863  public function isTrans() {
864  if ( !$this->isExternal() ) {
865  return false;
866  }
867 
868  return self::getInterwikiLookup()->fetch( $this->mInterwiki )->isTranscludable();
869  }
870 
876  public function getTransWikiID() {
877  if ( !$this->isExternal() ) {
878  return false;
879  }
880 
881  return self::getInterwikiLookup()->fetch( $this->mInterwiki )->getWikiID();
882  }
883 
893  public function getTitleValue() {
894  if ( $this->mTitleValue === null ) {
895  try {
896  $this->mTitleValue = new TitleValue(
897  $this->mNamespace,
898  $this->mDbkeyform,
899  $this->mFragment,
900  $this->mInterwiki
901  );
902  } catch ( InvalidArgumentException $ex ) {
903  wfDebug( __METHOD__ . ': Can\'t create a TitleValue for [[' .
904  $this->getPrefixedText() . ']]: ' . $ex->getMessage() . "\n" );
905  }
906  }
907 
908  return $this->mTitleValue;
909  }
910 
916  public function getText() {
917  return $this->mTextform;
918  }
919 
925  public function getPartialURL() {
926  return $this->mUrlform;
927  }
928 
934  public function getDBkey() {
935  return $this->mDbkeyform;
936  }
937 
944  function getUserCaseDBKey() {
945  if ( !is_null( $this->mUserCaseDBKey ) ) {
946  return $this->mUserCaseDBKey;
947  } else {
948  // If created via makeTitle(), $this->mUserCaseDBKey is not set.
949  return $this->mDbkeyform;
950  }
951  }
952 
958  public function getNamespace() {
959  return $this->mNamespace;
960  }
961 
970  public function getContentModel( $flags = 0 ) {
971  if ( !$this->mForcedContentModel
972  && ( !$this->mContentModel || $flags === self::GAID_FOR_UPDATE )
973  && $this->getArticleID( $flags )
974  ) {
975  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
976  $linkCache->addLinkObj( $this ); # in case we already had an article ID
977  $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' );
978  }
979 
980  if ( !$this->mContentModel ) {
981  $this->mContentModel = ContentHandler::getDefaultModelFor( $this );
982  }
983 
984  return $this->mContentModel;
985  }
986 
993  public function hasContentModel( $id ) {
994  return $this->getContentModel() == $id;
995  }
996 
1008  public function setContentModel( $model ) {
1009  $this->mContentModel = $model;
1010  $this->mForcedContentModel = true;
1011  }
1012 
1018  public function getNsText() {
1019  if ( $this->isExternal() ) {
1020  // This probably shouldn't even happen, except for interwiki transclusion.
1021  // If possible, use the canonical name for the foreign namespace.
1022  $nsText = MWNamespace::getCanonicalName( $this->mNamespace );
1023  if ( $nsText !== false ) {
1024  return $nsText;
1025  }
1026  }
1027 
1028  try {
1029  $formatter = self::getTitleFormatter();
1030  return $formatter->getNamespaceName( $this->mNamespace, $this->mDbkeyform );
1031  } catch ( InvalidArgumentException $ex ) {
1032  wfDebug( __METHOD__ . ': ' . $ex->getMessage() . "\n" );
1033  return false;
1034  }
1035  }
1036 
1042  public function getSubjectNsText() {
1043  return MediaWikiServices::getInstance()->getContentLanguage()->
1044  getNsText( MWNamespace::getSubject( $this->mNamespace ) );
1045  }
1046 
1052  public function getTalkNsText() {
1053  return MediaWikiServices::getInstance()->getContentLanguage()->
1054  getNsText( MWNamespace::getTalk( $this->mNamespace ) );
1055  }
1056 
1065  public function canHaveTalkPage() {
1066  return MWNamespace::hasTalkNamespace( $this->mNamespace );
1067  }
1068 
1074  public function canExist() {
1075  return $this->mNamespace >= NS_MAIN;
1076  }
1077 
1083  public function isWatchable() {
1084  return !$this->isExternal() && MWNamespace::isWatchable( $this->mNamespace );
1085  }
1086 
1092  public function isSpecialPage() {
1093  return $this->mNamespace == NS_SPECIAL;
1094  }
1095 
1102  public function isSpecial( $name ) {
1103  if ( $this->isSpecialPage() ) {
1104  list( $thisName, /* $subpage */ ) =
1105  MediaWikiServices::getInstance()->getSpecialPageFactory()->
1106  resolveAlias( $this->mDbkeyform );
1107  if ( $name == $thisName ) {
1108  return true;
1109  }
1110  }
1111  return false;
1112  }
1113 
1120  public function fixSpecialName() {
1121  if ( $this->isSpecialPage() ) {
1122  $spFactory = MediaWikiServices::getInstance()->getSpecialPageFactory();
1123  list( $canonicalName, $par ) = $spFactory->resolveAlias( $this->mDbkeyform );
1124  if ( $canonicalName ) {
1125  $localName = $spFactory->getLocalNameFor( $canonicalName, $par );
1126  if ( $localName != $this->mDbkeyform ) {
1127  return self::makeTitle( NS_SPECIAL, $localName );
1128  }
1129  }
1130  }
1131  return $this;
1132  }
1133 
1144  public function inNamespace( $ns ) {
1145  return MWNamespace::equals( $this->mNamespace, $ns );
1146  }
1147 
1155  public function inNamespaces( /* ... */ ) {
1156  $namespaces = func_get_args();
1157  if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) {
1158  $namespaces = $namespaces[0];
1159  }
1160 
1161  foreach ( $namespaces as $ns ) {
1162  if ( $this->inNamespace( $ns ) ) {
1163  return true;
1164  }
1165  }
1166 
1167  return false;
1168  }
1169 
1183  public function hasSubjectNamespace( $ns ) {
1184  return MWNamespace::subjectEquals( $this->mNamespace, $ns );
1185  }
1186 
1194  public function isContentPage() {
1195  return MWNamespace::isContent( $this->mNamespace );
1196  }
1197 
1204  public function isMovable() {
1205  if ( !MWNamespace::isMovable( $this->mNamespace ) || $this->isExternal() ) {
1206  // Interwiki title or immovable namespace. Hooks don't get to override here
1207  return false;
1208  }
1209 
1210  $result = true;
1211  Hooks::run( 'TitleIsMovable', [ $this, &$result ] );
1212  return $result;
1213  }
1214 
1225  public function isMainPage() {
1226  return $this->equals( self::newMainPage() );
1227  }
1228 
1234  public function isSubpage() {
1235  return MWNamespace::hasSubpages( $this->mNamespace )
1236  ? strpos( $this->getText(), '/' ) !== false
1237  : false;
1238  }
1239 
1245  public function isConversionTable() {
1246  // @todo ConversionTable should become a separate content model.
1247 
1248  return $this->mNamespace == NS_MEDIAWIKI &&
1249  strpos( $this->getText(), 'Conversiontable/' ) === 0;
1250  }
1251 
1257  public function isWikitextPage() {
1258  return $this->hasContentModel( CONTENT_MODEL_WIKITEXT );
1259  }
1260 
1275  public function isSiteConfigPage() {
1276  return (
1277  $this->isSiteCssConfigPage()
1278  || $this->isSiteJsonConfigPage()
1279  || $this->isSiteJsConfigPage()
1280  );
1281  }
1282 
1289  public function isUserConfigPage() {
1290  return (
1291  $this->isUserCssConfigPage()
1292  || $this->isUserJsonConfigPage()
1293  || $this->isUserJsConfigPage()
1294  );
1295  }
1296 
1303  public function getSkinFromConfigSubpage() {
1304  $subpage = explode( '/', $this->mTextform );
1305  $subpage = $subpage[count( $subpage ) - 1];
1306  $lastdot = strrpos( $subpage, '.' );
1307  if ( $lastdot === false ) {
1308  return $subpage; # Never happens: only called for names ending in '.css'/'.json'/'.js'
1309  }
1310  return substr( $subpage, 0, $lastdot );
1311  }
1312 
1319  public function isUserCssConfigPage() {
1320  return (
1321  NS_USER == $this->mNamespace
1322  && $this->isSubpage()
1323  && $this->hasContentModel( CONTENT_MODEL_CSS )
1324  );
1325  }
1326 
1333  public function isUserJsonConfigPage() {
1334  return (
1335  NS_USER == $this->mNamespace
1336  && $this->isSubpage()
1337  && $this->hasContentModel( CONTENT_MODEL_JSON )
1338  );
1339  }
1340 
1347  public function isUserJsConfigPage() {
1348  return (
1349  NS_USER == $this->mNamespace
1350  && $this->isSubpage()
1352  );
1353  }
1354 
1361  public function isSiteCssConfigPage() {
1362  return (
1363  NS_MEDIAWIKI == $this->mNamespace
1364  && (
1366  // paranoia - a MediaWiki: namespace page with mismatching extension and content
1367  // model is probably by mistake and might get handled incorrectly (see e.g. T112937)
1368  || substr( $this->mDbkeyform, -4 ) === '.css'
1369  )
1370  );
1371  }
1372 
1379  public function isSiteJsonConfigPage() {
1380  return (
1381  NS_MEDIAWIKI == $this->mNamespace
1382  && (
1384  // paranoia - a MediaWiki: namespace page with mismatching extension and content
1385  // model is probably by mistake and might get handled incorrectly (see e.g. T112937)
1386  || substr( $this->mDbkeyform, -5 ) === '.json'
1387  )
1388  );
1389  }
1390 
1397  public function isSiteJsConfigPage() {
1398  return (
1399  NS_MEDIAWIKI == $this->mNamespace
1400  && (
1402  // paranoia - a MediaWiki: namespace page with mismatching extension and content
1403  // model is probably by mistake and might get handled incorrectly (see e.g. T112937)
1404  || substr( $this->mDbkeyform, -3 ) === '.js'
1405  )
1406  );
1407  }
1408 
1415  public function isRawHtmlMessage() {
1416  global $wgRawHtmlMessages;
1417 
1418  if ( !$this->inNamespace( NS_MEDIAWIKI ) ) {
1419  return false;
1420  }
1421  $message = lcfirst( $this->getRootTitle()->getDBkey() );
1422  return in_array( $message, $wgRawHtmlMessages, true );
1423  }
1424 
1430  public function isTalkPage() {
1431  return MWNamespace::isTalk( $this->mNamespace );
1432  }
1433 
1439  public function getTalkPage() {
1440  return self::makeTitle( MWNamespace::getTalk( $this->mNamespace ), $this->mDbkeyform );
1441  }
1442 
1452  public function getTalkPageIfDefined() {
1453  if ( !$this->canHaveTalkPage() ) {
1454  return null;
1455  }
1456 
1457  return $this->getTalkPage();
1458  }
1459 
1466  public function getSubjectPage() {
1467  // Is this the same title?
1468  $subjectNS = MWNamespace::getSubject( $this->mNamespace );
1469  if ( $this->mNamespace == $subjectNS ) {
1470  return $this;
1471  }
1472  return self::makeTitle( $subjectNS, $this->mDbkeyform );
1473  }
1474 
1483  public function getOtherPage() {
1484  if ( $this->isSpecialPage() ) {
1485  throw new MWException( 'Special pages cannot have other pages' );
1486  }
1487  if ( $this->isTalkPage() ) {
1488  return $this->getSubjectPage();
1489  } else {
1490  if ( !$this->canHaveTalkPage() ) {
1491  throw new MWException( "{$this->getPrefixedText()} does not have an other page" );
1492  }
1493  return $this->getTalkPage();
1494  }
1495  }
1496 
1502  public function getDefaultNamespace() {
1503  return $this->mDefaultNamespace;
1504  }
1505 
1513  public function getFragment() {
1514  return $this->mFragment;
1515  }
1516 
1523  public function hasFragment() {
1524  return $this->mFragment !== '';
1525  }
1526 
1532  public function getFragmentForURL() {
1533  if ( !$this->hasFragment() ) {
1534  return '';
1535  } elseif ( $this->isExternal() ) {
1536  // Note: If the interwiki is unknown, it's treated as a namespace on the local wiki,
1537  // so we treat it like a local interwiki.
1538  $interwiki = self::getInterwikiLookup()->fetch( $this->mInterwiki );
1539  if ( $interwiki && !$interwiki->isLocal() ) {
1540  return '#' . Sanitizer::escapeIdForExternalInterwiki( $this->mFragment );
1541  }
1542  }
1543 
1544  return '#' . Sanitizer::escapeIdForLink( $this->mFragment );
1545  }
1546 
1559  public function setFragment( $fragment ) {
1560  $this->mFragment = strtr( substr( $fragment, 1 ), '_', ' ' );
1561  }
1562 
1570  public function createFragmentTarget( $fragment ) {
1571  return self::makeTitle(
1572  $this->mNamespace,
1573  $this->getText(),
1574  $fragment,
1575  $this->mInterwiki
1576  );
1577  }
1578 
1586  private function prefix( $name ) {
1587  $p = '';
1588  if ( $this->isExternal() ) {
1589  $p = $this->mInterwiki . ':';
1590  }
1591 
1592  if ( $this->mNamespace != 0 ) {
1593  $nsText = $this->getNsText();
1594 
1595  if ( $nsText === false ) {
1596  // See T165149. Awkward, but better than erroneously linking to the main namespace.
1597  $nsText = MediaWikiServices::getInstance()->getContentLanguage()->
1598  getNsText( NS_SPECIAL ) . ":Badtitle/NS{$this->mNamespace}";
1599  }
1600 
1601  $p .= $nsText . ':';
1602  }
1603  return $p . $name;
1604  }
1605 
1612  public function getPrefixedDBkey() {
1613  $s = $this->prefix( $this->mDbkeyform );
1614  $s = strtr( $s, ' ', '_' );
1615  return $s;
1616  }
1617 
1624  public function getPrefixedText() {
1625  if ( $this->prefixedText === null ) {
1626  $s = $this->prefix( $this->mTextform );
1627  $s = strtr( $s, '_', ' ' );
1628  $this->prefixedText = $s;
1629  }
1630  return $this->prefixedText;
1631  }
1632 
1638  public function __toString() {
1639  return $this->getPrefixedText();
1640  }
1641 
1648  public function getFullText() {
1649  $text = $this->getPrefixedText();
1650  if ( $this->hasFragment() ) {
1651  $text .= '#' . $this->mFragment;
1652  }
1653  return $text;
1654  }
1655 
1668  public function getRootText() {
1669  if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1670  return $this->getText();
1671  }
1672 
1673  return strtok( $this->getText(), '/' );
1674  }
1675 
1688  public function getRootTitle() {
1689  return self::makeTitle( $this->mNamespace, $this->getRootText() );
1690  }
1691 
1703  public function getBaseText() {
1704  if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1705  return $this->getText();
1706  }
1707 
1708  $parts = explode( '/', $this->getText() );
1709  # Don't discard the real title if there's no subpage involved
1710  if ( count( $parts ) > 1 ) {
1711  unset( $parts[count( $parts ) - 1] );
1712  }
1713  return implode( '/', $parts );
1714  }
1715 
1728  public function getBaseTitle() {
1729  return self::makeTitle( $this->mNamespace, $this->getBaseText() );
1730  }
1731 
1743  public function getSubpageText() {
1744  if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1745  return $this->mTextform;
1746  }
1747  $parts = explode( '/', $this->mTextform );
1748  return $parts[count( $parts ) - 1];
1749  }
1750 
1764  public function getSubpage( $text ) {
1765  return self::makeTitleSafe( $this->mNamespace, $this->getText() . '/' . $text );
1766  }
1767 
1773  public function getSubpageUrlForm() {
1774  $text = $this->getSubpageText();
1775  $text = wfUrlencode( strtr( $text, ' ', '_' ) );
1776  return $text;
1777  }
1778 
1784  public function getPrefixedURL() {
1785  $s = $this->prefix( $this->mDbkeyform );
1786  $s = wfUrlencode( strtr( $s, ' ', '_' ) );
1787  return $s;
1788  }
1789 
1803  private static function fixUrlQueryArgs( $query, $query2 = false ) {
1804  if ( $query2 !== false ) {
1805  wfDeprecated( "Title::get{Canonical,Full,Link,Local,Internal}URL " .
1806  "method called with a second parameter is deprecated. Add your " .
1807  "parameter to an array passed as the first parameter.", "1.19" );
1808  }
1809  if ( is_array( $query ) ) {
1810  $query = wfArrayToCgi( $query );
1811  }
1812  if ( $query2 ) {
1813  if ( is_string( $query2 ) ) {
1814  // $query2 is a string, we will consider this to be
1815  // a deprecated $variant argument and add it to the query
1816  $query2 = wfArrayToCgi( [ 'variant' => $query2 ] );
1817  } else {
1818  $query2 = wfArrayToCgi( $query2 );
1819  }
1820  // If we have $query content add a & to it first
1821  if ( $query ) {
1822  $query .= '&';
1823  }
1824  // Now append the queries together
1825  $query .= $query2;
1826  }
1827  return $query;
1828  }
1829 
1841  public function getFullURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) {
1842  $query = self::fixUrlQueryArgs( $query, $query2 );
1843 
1844  # Hand off all the decisions on urls to getLocalURL
1845  $url = $this->getLocalURL( $query );
1846 
1847  # Expand the url to make it a full url. Note that getLocalURL has the
1848  # potential to output full urls for a variety of reasons, so we use
1849  # wfExpandUrl instead of simply prepending $wgServer
1850  $url = wfExpandUrl( $url, $proto );
1851 
1852  # Finally, add the fragment.
1853  $url .= $this->getFragmentForURL();
1854  // Avoid PHP 7.1 warning from passing $this by reference
1855  $titleRef = $this;
1856  Hooks::run( 'GetFullURL', [ &$titleRef, &$url, $query ] );
1857  return $url;
1858  }
1859 
1876  public function getFullUrlForRedirect( $query = '', $proto = PROTO_CURRENT ) {
1877  $target = $this;
1878  if ( $this->isExternal() ) {
1879  $target = SpecialPage::getTitleFor(
1880  'GoToInterwiki',
1881  $this->getPrefixedDBkey()
1882  );
1883  }
1884  return $target->getFullURL( $query, false, $proto );
1885  }
1886 
1910  public function getLocalURL( $query = '', $query2 = false ) {
1912 
1913  $query = self::fixUrlQueryArgs( $query, $query2 );
1914 
1915  $interwiki = self::getInterwikiLookup()->fetch( $this->mInterwiki );
1916  if ( $interwiki ) {
1917  $namespace = $this->getNsText();
1918  if ( $namespace != '' ) {
1919  # Can this actually happen? Interwikis shouldn't be parsed.
1920  # Yes! It can in interwiki transclusion. But... it probably shouldn't.
1921  $namespace .= ':';
1922  }
1923  $url = $interwiki->getURL( $namespace . $this->mDbkeyform );
1924  $url = wfAppendQuery( $url, $query );
1925  } else {
1926  $dbkey = wfUrlencode( $this->getPrefixedDBkey() );
1927  if ( $query == '' ) {
1928  $url = str_replace( '$1', $dbkey, $wgArticlePath );
1929  // Avoid PHP 7.1 warning from passing $this by reference
1930  $titleRef = $this;
1931  Hooks::run( 'GetLocalURL::Article', [ &$titleRef, &$url ] );
1932  } else {
1934  $url = false;
1935  $matches = [];
1936 
1937  if ( !empty( $wgActionPaths )
1938  && preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches )
1939  ) {
1940  $action = urldecode( $matches[2] );
1941  if ( isset( $wgActionPaths[$action] ) ) {
1942  $query = $matches[1];
1943  if ( isset( $matches[4] ) ) {
1944  $query .= $matches[4];
1945  }
1946  $url = str_replace( '$1', $dbkey, $wgActionPaths[$action] );
1947  if ( $query != '' ) {
1948  $url = wfAppendQuery( $url, $query );
1949  }
1950  }
1951  }
1952 
1953  if ( $url === false
1954  && $wgVariantArticlePath
1955  && preg_match( '/^variant=([^&]*)$/', $query, $matches )
1956  && $this->getPageLanguage()->equals(
1957  MediaWikiServices::getInstance()->getContentLanguage() )
1958  && $this->getPageLanguage()->hasVariants()
1959  ) {
1960  $variant = urldecode( $matches[1] );
1961  if ( $this->getPageLanguage()->hasVariant( $variant ) ) {
1962  // Only do the variant replacement if the given variant is a valid
1963  // variant for the page's language.
1964  $url = str_replace( '$2', urlencode( $variant ), $wgVariantArticlePath );
1965  $url = str_replace( '$1', $dbkey, $url );
1966  }
1967  }
1968 
1969  if ( $url === false ) {
1970  if ( $query == '-' ) {
1971  $query = '';
1972  }
1973  $url = "{$wgScript}?title={$dbkey}&{$query}";
1974  }
1975  }
1976  // Avoid PHP 7.1 warning from passing $this by reference
1977  $titleRef = $this;
1978  Hooks::run( 'GetLocalURL::Internal', [ &$titleRef, &$url, $query ] );
1979 
1980  // @todo FIXME: This causes breakage in various places when we
1981  // actually expected a local URL and end up with dupe prefixes.
1982  if ( $wgRequest->getVal( 'action' ) == 'render' ) {
1983  $url = $wgServer . $url;
1984  }
1985  }
1986  // Avoid PHP 7.1 warning from passing $this by reference
1987  $titleRef = $this;
1988  Hooks::run( 'GetLocalURL', [ &$titleRef, &$url, $query ] );
1989  return $url;
1990  }
1991 
2009  public function getLinkURL( $query = '', $query2 = false, $proto = false ) {
2010  if ( $this->isExternal() || $proto !== false ) {
2011  $ret = $this->getFullURL( $query, $query2, $proto );
2012  } elseif ( $this->getPrefixedText() === '' && $this->hasFragment() ) {
2013  $ret = $this->getFragmentForURL();
2014  } else {
2015  $ret = $this->getLocalURL( $query, $query2 ) . $this->getFragmentForURL();
2016  }
2017  return $ret;
2018  }
2019 
2034  public function getInternalURL( $query = '', $query2 = false ) {
2035  global $wgInternalServer, $wgServer;
2036  $query = self::fixUrlQueryArgs( $query, $query2 );
2037  $server = $wgInternalServer !== false ? $wgInternalServer : $wgServer;
2038  $url = wfExpandUrl( $server . $this->getLocalURL( $query ), PROTO_HTTP );
2039  // Avoid PHP 7.1 warning from passing $this by reference
2040  $titleRef = $this;
2041  Hooks::run( 'GetInternalURL', [ &$titleRef, &$url, $query ] );
2042  return $url;
2043  }
2044 
2058  public function getCanonicalURL( $query = '', $query2 = false ) {
2059  $query = self::fixUrlQueryArgs( $query, $query2 );
2060  $url = wfExpandUrl( $this->getLocalURL( $query ) . $this->getFragmentForURL(), PROTO_CANONICAL );
2061  // Avoid PHP 7.1 warning from passing $this by reference
2062  $titleRef = $this;
2063  Hooks::run( 'GetCanonicalURL', [ &$titleRef, &$url, $query ] );
2064  return $url;
2065  }
2066 
2072  public function getEditURL() {
2073  if ( $this->isExternal() ) {
2074  return '';
2075  }
2076  $s = $this->getLocalURL( 'action=edit' );
2077 
2078  return $s;
2079  }
2080 
2095  public function quickUserCan( $action, $user = null ) {
2096  return $this->userCan( $action, $user, false );
2097  }
2098 
2108  public function userCan( $action, $user = null, $rigor = 'secure' ) {
2109  if ( !$user instanceof User ) {
2110  global $wgUser;
2111  $user = $wgUser;
2112  }
2113 
2114  return !count( $this->getUserPermissionsErrorsInternal( $action, $user, $rigor, true ) );
2115  }
2116 
2132  public function getUserPermissionsErrors(
2133  $action, $user, $rigor = 'secure', $ignoreErrors = []
2134  ) {
2135  $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $rigor );
2136 
2137  // Remove the errors being ignored.
2138  foreach ( $errors as $index => $error ) {
2139  $errKey = is_array( $error ) ? $error[0] : $error;
2140 
2141  if ( in_array( $errKey, $ignoreErrors ) ) {
2142  unset( $errors[$index] );
2143  }
2144  if ( $errKey instanceof MessageSpecifier && in_array( $errKey->getKey(), $ignoreErrors ) ) {
2145  unset( $errors[$index] );
2146  }
2147  }
2148 
2149  return $errors;
2150  }
2151 
2163  private function checkQuickPermissions( $action, $user, $errors, $rigor, $short ) {
2164  if ( !Hooks::run( 'TitleQuickPermissions',
2165  [ $this, $user, $action, &$errors, ( $rigor !== 'quick' ), $short ] )
2166  ) {
2167  return $errors;
2168  }
2169 
2170  if ( $action == 'create' ) {
2171  if (
2172  ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
2173  ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) )
2174  ) {
2175  $errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ 'nocreate-loggedin' ];
2176  }
2177  } elseif ( $action == 'move' ) {
2178  if ( !$user->isAllowed( 'move-rootuserpages' )
2179  && $this->mNamespace == NS_USER && !$this->isSubpage() ) {
2180  // Show user page-specific message only if the user can move other pages
2181  $errors[] = [ 'cant-move-user-page' ];
2182  }
2183 
2184  // Check if user is allowed to move files if it's a file
2185  if ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) {
2186  $errors[] = [ 'movenotallowedfile' ];
2187  }
2188 
2189  // Check if user is allowed to move category pages if it's a category page
2190  if ( $this->mNamespace == NS_CATEGORY && !$user->isAllowed( 'move-categorypages' ) ) {
2191  $errors[] = [ 'cant-move-category-page' ];
2192  }
2193 
2194  if ( !$user->isAllowed( 'move' ) ) {
2195  // User can't move anything
2196  $userCanMove = User::groupHasPermission( 'user', 'move' );
2197  $autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' );
2198  if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
2199  // custom message if logged-in users without any special rights can move
2200  $errors[] = [ 'movenologintext' ];
2201  } else {
2202  $errors[] = [ 'movenotallowed' ];
2203  }
2204  }
2205  } elseif ( $action == 'move-target' ) {
2206  if ( !$user->isAllowed( 'move' ) ) {
2207  // User can't move anything
2208  $errors[] = [ 'movenotallowed' ];
2209  } elseif ( !$user->isAllowed( 'move-rootuserpages' )
2210  && $this->mNamespace == NS_USER && !$this->isSubpage() ) {
2211  // Show user page-specific message only if the user can move other pages
2212  $errors[] = [ 'cant-move-to-user-page' ];
2213  } elseif ( !$user->isAllowed( 'move-categorypages' )
2214  && $this->mNamespace == NS_CATEGORY ) {
2215  // Show category page-specific message only if the user can move other pages
2216  $errors[] = [ 'cant-move-to-category-page' ];
2217  }
2218  } elseif ( !$user->isAllowed( $action ) ) {
2219  $errors[] = $this->missingPermissionError( $action, $short );
2220  }
2221 
2222  return $errors;
2223  }
2224 
2233  private function resultToError( $errors, $result ) {
2234  if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
2235  // A single array representing an error
2236  $errors[] = $result;
2237  } elseif ( is_array( $result ) && is_array( $result[0] ) ) {
2238  // A nested array representing multiple errors
2239  $errors = array_merge( $errors, $result );
2240  } elseif ( $result !== '' && is_string( $result ) ) {
2241  // A string representing a message-id
2242  $errors[] = [ $result ];
2243  } elseif ( $result instanceof MessageSpecifier ) {
2244  // A message specifier representing an error
2245  $errors[] = [ $result ];
2246  } elseif ( $result === false ) {
2247  // a generic "We don't want them to do that"
2248  $errors[] = [ 'badaccess-group0' ];
2249  }
2250  return $errors;
2251  }
2252 
2264  private function checkPermissionHooks( $action, $user, $errors, $rigor, $short ) {
2265  // Use getUserPermissionsErrors instead
2266  $result = '';
2267  // Avoid PHP 7.1 warning from passing $this by reference
2268  $titleRef = $this;
2269  if ( !Hooks::run( 'userCan', [ &$titleRef, &$user, $action, &$result ] ) ) {
2270  return $result ? [] : [ [ 'badaccess-group0' ] ];
2271  }
2272  // Check getUserPermissionsErrors hook
2273  // Avoid PHP 7.1 warning from passing $this by reference
2274  $titleRef = $this;
2275  if ( !Hooks::run( 'getUserPermissionsErrors', [ &$titleRef, &$user, $action, &$result ] ) ) {
2276  $errors = $this->resultToError( $errors, $result );
2277  }
2278  // Check getUserPermissionsErrorsExpensive hook
2279  if (
2280  $rigor !== 'quick'
2281  && !( $short && count( $errors ) > 0 )
2282  && !Hooks::run( 'getUserPermissionsErrorsExpensive', [ &$titleRef, &$user, $action, &$result ] )
2283  ) {
2284  $errors = $this->resultToError( $errors, $result );
2285  }
2286 
2287  return $errors;
2288  }
2289 
2301  private function checkSpecialsAndNSPermissions( $action, $user, $errors, $rigor, $short ) {
2302  # Only 'createaccount' can be performed on special pages,
2303  # which don't actually exist in the DB.
2304  if ( $this->isSpecialPage() && $action !== 'createaccount' ) {
2305  $errors[] = [ 'ns-specialprotected' ];
2306  }
2307 
2308  # Check $wgNamespaceProtection for restricted namespaces
2309  if ( $this->isNamespaceProtected( $user ) ) {
2310  $ns = $this->mNamespace == NS_MAIN ?
2311  wfMessage( 'nstab-main' )->text() : $this->getNsText();
2312  $errors[] = $this->mNamespace == NS_MEDIAWIKI ?
2313  [ 'protectedinterface', $action ] : [ 'namespaceprotected', $ns, $action ];
2314  }
2315 
2316  return $errors;
2317  }
2318 
2330  private function checkSiteConfigPermissions( $action, $user, $errors, $rigor, $short ) {
2331  if ( $action != 'patrol' ) {
2332  $error = null;
2333  // Sitewide CSS/JSON/JS changes, like all NS_MEDIAWIKI changes, also require the
2334  // editinterface right. That's implemented as a restriction so no check needed here.
2335  if ( $this->isSiteCssConfigPage() && !$user->isAllowed( 'editsitecss' ) ) {
2336  $error = [ 'sitecssprotected', $action ];
2337  } elseif ( $this->isSiteJsonConfigPage() && !$user->isAllowed( 'editsitejson' ) ) {
2338  $error = [ 'sitejsonprotected', $action ];
2339  } elseif ( $this->isSiteJsConfigPage() && !$user->isAllowed( 'editsitejs' ) ) {
2340  $error = [ 'sitejsprotected', $action ];
2341  } elseif ( $this->isRawHtmlMessage() ) {
2342  // Raw HTML can be used to deploy CSS or JS so require rights for both.
2343  if ( !$user->isAllowed( 'editsitejs' ) ) {
2344  $error = [ 'sitejsprotected', $action ];
2345  } elseif ( !$user->isAllowed( 'editsitecss' ) ) {
2346  $error = [ 'sitecssprotected', $action ];
2347  }
2348  }
2349 
2350  if ( $error ) {
2351  if ( $user->isAllowed( 'editinterface' ) ) {
2352  // Most users / site admins will probably find out about the new, more restrictive
2353  // permissions by failing to edit something. Give them more info.
2354  // TODO remove this a few release cycles after 1.32
2355  $error = [ 'interfaceadmin-info', wfMessage( $error[0], $error[1] ) ];
2356  }
2357  $errors[] = $error;
2358  }
2359  }
2360 
2361  return $errors;
2362  }
2363 
2375  private function checkUserConfigPermissions( $action, $user, $errors, $rigor, $short ) {
2376  # Protect css/json/js subpages of user pages
2377  # XXX: this might be better using restrictions
2378 
2379  if ( $action === 'patrol' ) {
2380  return $errors;
2381  }
2382 
2383  if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) {
2384  // Users need editmyuser* to edit their own CSS/JSON/JS subpages.
2385  if (
2386  $this->isUserCssConfigPage()
2387  && !$user->isAllowedAny( 'editmyusercss', 'editusercss' )
2388  ) {
2389  $errors[] = [ 'mycustomcssprotected', $action ];
2390  } elseif (
2391  $this->isUserJsonConfigPage()
2392  && !$user->isAllowedAny( 'editmyuserjson', 'edituserjson' )
2393  ) {
2394  $errors[] = [ 'mycustomjsonprotected', $action ];
2395  } elseif (
2396  $this->isUserJsConfigPage()
2397  && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' )
2398  ) {
2399  $errors[] = [ 'mycustomjsprotected', $action ];
2400  }
2401  } else {
2402  // Users need editmyuser* to edit their own CSS/JSON/JS subpages, except for
2403  // deletion/suppression which cannot be used for attacks and we want to avoid the
2404  // situation where an unprivileged user can post abusive content on their subpages
2405  // and only very highly privileged users could remove it.
2406  if ( !in_array( $action, [ 'delete', 'deleterevision', 'suppressrevision' ], true ) ) {
2407  if (
2408  $this->isUserCssConfigPage()
2409  && !$user->isAllowed( 'editusercss' )
2410  ) {
2411  $errors[] = [ 'customcssprotected', $action ];
2412  } elseif (
2413  $this->isUserJsonConfigPage()
2414  && !$user->isAllowed( 'edituserjson' )
2415  ) {
2416  $errors[] = [ 'customjsonprotected', $action ];
2417  } elseif (
2418  $this->isUserJsConfigPage()
2419  && !$user->isAllowed( 'edituserjs' )
2420  ) {
2421  $errors[] = [ 'customjsprotected', $action ];
2422  }
2423  }
2424  }
2425 
2426  return $errors;
2427  }
2428 
2442  private function checkPageRestrictions( $action, $user, $errors, $rigor, $short ) {
2443  foreach ( $this->getRestrictions( $action ) as $right ) {
2444  // Backwards compatibility, rewrite sysop -> editprotected
2445  if ( $right == 'sysop' ) {
2446  $right = 'editprotected';
2447  }
2448  // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
2449  if ( $right == 'autoconfirmed' ) {
2450  $right = 'editsemiprotected';
2451  }
2452  if ( $right == '' ) {
2453  continue;
2454  }
2455  if ( !$user->isAllowed( $right ) ) {
2456  $errors[] = [ 'protectedpagetext', $right, $action ];
2457  } elseif ( $this->mCascadeRestriction && !$user->isAllowed( 'protect' ) ) {
2458  $errors[] = [ 'protectedpagetext', 'protect', $action ];
2459  }
2460  }
2461 
2462  return $errors;
2463  }
2464 
2476  private function checkCascadingSourcesRestrictions( $action, $user, $errors, $rigor, $short ) {
2477  if ( $rigor !== 'quick' && !$this->isUserConfigPage() ) {
2478  # We /could/ use the protection level on the source page, but it's
2479  # fairly ugly as we have to establish a precedence hierarchy for pages
2480  # included by multiple cascade-protected pages. So just restrict
2481  # it to people with 'protect' permission, as they could remove the
2482  # protection anyway.
2483  list( $cascadingSources, $restrictions ) = $this->getCascadeProtectionSources();
2484  # Cascading protection depends on more than this page...
2485  # Several cascading protected pages may include this page...
2486  # Check each cascading level
2487  # This is only for protection restrictions, not for all actions
2488  if ( isset( $restrictions[$action] ) ) {
2489  foreach ( $restrictions[$action] as $right ) {
2490  // Backwards compatibility, rewrite sysop -> editprotected
2491  if ( $right == 'sysop' ) {
2492  $right = 'editprotected';
2493  }
2494  // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
2495  if ( $right == 'autoconfirmed' ) {
2496  $right = 'editsemiprotected';
2497  }
2498  if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) {
2499  $pages = '';
2500  foreach ( $cascadingSources as $page ) {
2501  $pages .= '* [[:' . $page->getPrefixedText() . "]]\n";
2502  }
2503  $errors[] = [ 'cascadeprotected', count( $cascadingSources ), $pages, $action ];
2504  }
2505  }
2506  }
2507  }
2508 
2509  return $errors;
2510  }
2511 
2523  private function checkActionPermissions( $action, $user, $errors, $rigor, $short ) {
2525 
2526  if ( $action == 'protect' ) {
2527  if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $rigor, true ) ) ) {
2528  // If they can't edit, they shouldn't protect.
2529  $errors[] = [ 'protect-cantedit' ];
2530  }
2531  } elseif ( $action == 'create' ) {
2532  $title_protection = $this->getTitleProtection();
2533  if ( $title_protection ) {
2534  if ( $title_protection['permission'] == ''
2535  || !$user->isAllowed( $title_protection['permission'] )
2536  ) {
2537  $errors[] = [
2538  'titleprotected',
2539  User::whoIs( $title_protection['user'] ),
2540  $title_protection['reason']
2541  ];
2542  }
2543  }
2544  } elseif ( $action == 'move' ) {
2545  // Check for immobile pages
2546  if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
2547  // Specific message for this case
2548  $errors[] = [ 'immobile-source-namespace', $this->getNsText() ];
2549  } elseif ( !$this->isMovable() ) {
2550  // Less specific message for rarer cases
2551  $errors[] = [ 'immobile-source-page' ];
2552  }
2553  } elseif ( $action == 'move-target' ) {
2554  if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
2555  $errors[] = [ 'immobile-target-namespace', $this->getNsText() ];
2556  } elseif ( !$this->isMovable() ) {
2557  $errors[] = [ 'immobile-target-page' ];
2558  }
2559  } elseif ( $action == 'delete' ) {
2560  $tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true );
2561  if ( !$tempErrors ) {
2562  $tempErrors = $this->checkCascadingSourcesRestrictions( 'edit',
2563  $user, $tempErrors, $rigor, true );
2564  }
2565  if ( $tempErrors ) {
2566  // If protection keeps them from editing, they shouldn't be able to delete.
2567  $errors[] = [ 'deleteprotected' ];
2568  }
2569  if ( $rigor !== 'quick' && $wgDeleteRevisionsLimit
2570  && !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion()
2571  ) {
2572  $errors[] = [ 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ];
2573  }
2574  } elseif ( $action === 'undelete' ) {
2575  if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $rigor, true ) ) ) {
2576  // Undeleting implies editing
2577  $errors[] = [ 'undelete-cantedit' ];
2578  }
2579  if ( !$this->exists()
2580  && count( $this->getUserPermissionsErrorsInternal( 'create', $user, $rigor, true ) )
2581  ) {
2582  // Undeleting where nothing currently exists implies creating
2583  $errors[] = [ 'undelete-cantcreate' ];
2584  }
2585  }
2586  return $errors;
2587  }
2588 
2600  private function checkUserBlock( $action, $user, $errors, $rigor, $short ) {
2602  // Account creation blocks handled at userlogin.
2603  // Unblocking handled in SpecialUnblock
2604  if ( $rigor === 'quick' || in_array( $action, [ 'createaccount', 'unblock' ] ) ) {
2605  return $errors;
2606  }
2607 
2608  // Optimize for a very common case
2609  if ( $action === 'read' && !$wgBlockDisablesLogin ) {
2610  return $errors;
2611  }
2612 
2613  if ( $wgEmailConfirmToEdit
2614  && !$user->isEmailConfirmed()
2615  && $action === 'edit'
2616  ) {
2617  $errors[] = [ 'confirmedittext' ];
2618  }
2619 
2620  $useReplica = ( $rigor !== 'secure' );
2621  $block = $user->getBlock( $useReplica );
2622 
2623  // The block may explicitly allow an action (like "read" or "upload").
2624  if ( $block && $block->prevents( $action ) === false ) {
2625  return $errors;
2626  }
2627 
2628  // Determine if the user is blocked from this action on this page.
2629  // What gets passed into this method is a user right, not an action name.
2630  // There is no way to instantiate an action by restriction. However, this
2631  // will get the action where the restriction is the same. This may result
2632  // in actions being blocked that shouldn't be.
2633  $actionObj = null;
2634  if ( Action::exists( $action ) ) {
2635  // Clone the title to prevent mutations to this object which is done
2636  // by Title::loadFromRow() in WikiPage::loadFromRow().
2637  $page = WikiPage::factory( clone $this );
2638  // Creating an action will perform several database queries to ensure that
2639  // the action has not been overridden by the content type.
2640  // @todo FIXME: Pass the relevant context into this function.
2641  $actionObj = Action::factory( $action, $page, RequestContext::getMain() );
2642  // Ensure that the retrieved action matches the restriction.
2643  if ( $actionObj && $actionObj->getRestriction() !== $action ) {
2644  $actionObj = null;
2645  }
2646  }
2647 
2648  // If no action object is returned, assume that the action requires unblock
2649  // which is the default.
2650  if ( !$actionObj || $actionObj->requiresUnblock() ) {
2651  if ( $user->isBlockedFrom( $this, $useReplica ) ) {
2652  // @todo FIXME: Pass the relevant context into this function.
2653  $errors[] = $block
2654  ? $block->getPermissionsError( RequestContext::getMain() )
2655  : [ 'actionblockedtext' ];
2656  }
2657  }
2658 
2659  return $errors;
2660  }
2661 
2673  private function checkReadPermissions( $action, $user, $errors, $rigor, $short ) {
2675 
2676  $whitelisted = false;
2677  if ( User::isEveryoneAllowed( 'read' ) ) {
2678  # Shortcut for public wikis, allows skipping quite a bit of code
2679  $whitelisted = true;
2680  } elseif ( $user->isAllowed( 'read' ) ) {
2681  # If the user is allowed to read pages, he is allowed to read all pages
2682  $whitelisted = true;
2683  } elseif ( $this->isSpecial( 'Userlogin' )
2684  || $this->isSpecial( 'PasswordReset' )
2685  || $this->isSpecial( 'Userlogout' )
2686  ) {
2687  # Always grant access to the login page.
2688  # Even anons need to be able to log in.
2689  $whitelisted = true;
2690  } elseif ( is_array( $wgWhitelistRead ) && count( $wgWhitelistRead ) ) {
2691  # Time to check the whitelist
2692  # Only do these checks is there's something to check against
2693  $name = $this->getPrefixedText();
2694  $dbName = $this->getPrefixedDBkey();
2695 
2696  // Check for explicit whitelisting with and without underscores
2697  if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) {
2698  $whitelisted = true;
2699  } elseif ( $this->mNamespace == NS_MAIN ) {
2700  # Old settings might have the title prefixed with
2701  # a colon for main-namespace pages
2702  if ( in_array( ':' . $name, $wgWhitelistRead ) ) {
2703  $whitelisted = true;
2704  }
2705  } elseif ( $this->isSpecialPage() ) {
2706  # If it's a special page, ditch the subpage bit and check again
2708  list( $name, /* $subpage */ ) =
2709  MediaWikiServices::getInstance()->getSpecialPageFactory()->
2710  resolveAlias( $name );
2711  if ( $name ) {
2712  $pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
2713  if ( in_array( $pure, $wgWhitelistRead, true ) ) {
2714  $whitelisted = true;
2715  }
2716  }
2717  }
2718  }
2719 
2720  if ( !$whitelisted && is_array( $wgWhitelistReadRegexp ) && !empty( $wgWhitelistReadRegexp ) ) {
2721  $name = $this->getPrefixedText();
2722  // Check for regex whitelisting
2723  foreach ( $wgWhitelistReadRegexp as $listItem ) {
2724  if ( preg_match( $listItem, $name ) ) {
2725  $whitelisted = true;
2726  break;
2727  }
2728  }
2729  }
2730 
2731  if ( !$whitelisted ) {
2732  # If the title is not whitelisted, give extensions a chance to do so...
2733  Hooks::run( 'TitleReadWhitelist', [ $this, $user, &$whitelisted ] );
2734  if ( !$whitelisted ) {
2735  $errors[] = $this->missingPermissionError( $action, $short );
2736  }
2737  }
2738 
2739  return $errors;
2740  }
2741 
2750  private function missingPermissionError( $action, $short ) {
2751  // We avoid expensive display logic for quickUserCan's and such
2752  if ( $short ) {
2753  return [ 'badaccess-group0' ];
2754  }
2755 
2756  return User::newFatalPermissionDeniedStatus( $action )->getErrorsArray()[0];
2757  }
2758 
2774  $action, $user, $rigor = 'secure', $short = false
2775  ) {
2776  if ( $rigor === true ) {
2777  $rigor = 'secure'; // b/c
2778  } elseif ( $rigor === false ) {
2779  $rigor = 'quick'; // b/c
2780  } elseif ( !in_array( $rigor, [ 'quick', 'full', 'secure' ] ) ) {
2781  throw new Exception( "Invalid rigor parameter '$rigor'." );
2782  }
2783 
2784  # Read has special handling
2785  if ( $action == 'read' ) {
2786  $checks = [
2787  'checkPermissionHooks',
2788  'checkReadPermissions',
2789  'checkUserBlock', // for wgBlockDisablesLogin
2790  ];
2791  # Don't call checkSpecialsAndNSPermissions, checkSiteConfigPermissions
2792  # or checkUserConfigPermissions here as it will lead to duplicate
2793  # error messages. This is okay to do since anywhere that checks for
2794  # create will also check for edit, and those checks are called for edit.
2795  } elseif ( $action == 'create' ) {
2796  $checks = [
2797  'checkQuickPermissions',
2798  'checkPermissionHooks',
2799  'checkPageRestrictions',
2800  'checkCascadingSourcesRestrictions',
2801  'checkActionPermissions',
2802  'checkUserBlock'
2803  ];
2804  } else {
2805  $checks = [
2806  'checkQuickPermissions',
2807  'checkPermissionHooks',
2808  'checkSpecialsAndNSPermissions',
2809  'checkSiteConfigPermissions',
2810  'checkUserConfigPermissions',
2811  'checkPageRestrictions',
2812  'checkCascadingSourcesRestrictions',
2813  'checkActionPermissions',
2814  'checkUserBlock'
2815  ];
2816  }
2817 
2818  $errors = [];
2819  foreach ( $checks as $method ) {
2820  $errors = $this->$method( $action, $user, $errors, $rigor, $short );
2821 
2822  if ( $short && $errors !== [] ) {
2823  break;
2824  }
2825  }
2826 
2827  return $errors;
2828  }
2829 
2837  public static function getFilteredRestrictionTypes( $exists = true ) {
2838  global $wgRestrictionTypes;
2839  $types = $wgRestrictionTypes;
2840  if ( $exists ) {
2841  # Remove the create restriction for existing titles
2842  $types = array_diff( $types, [ 'create' ] );
2843  } else {
2844  # Only the create and upload restrictions apply to non-existing titles
2845  $types = array_intersect( $types, [ 'create', 'upload' ] );
2846  }
2847  return $types;
2848  }
2849 
2855  public function getRestrictionTypes() {
2856  if ( $this->isSpecialPage() ) {
2857  return [];
2858  }
2859 
2860  $types = self::getFilteredRestrictionTypes( $this->exists() );
2861 
2862  if ( $this->mNamespace != NS_FILE ) {
2863  # Remove the upload restriction for non-file titles
2864  $types = array_diff( $types, [ 'upload' ] );
2865  }
2866 
2867  Hooks::run( 'TitleGetRestrictionTypes', [ $this, &$types ] );
2868 
2869  wfDebug( __METHOD__ . ': applicable restrictions to [[' .
2870  $this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" );
2871 
2872  return $types;
2873  }
2874 
2882  public function getTitleProtection() {
2883  $protection = $this->getTitleProtectionInternal();
2884  if ( $protection ) {
2885  if ( $protection['permission'] == 'sysop' ) {
2886  $protection['permission'] = 'editprotected'; // B/C
2887  }
2888  if ( $protection['permission'] == 'autoconfirmed' ) {
2889  $protection['permission'] = 'editsemiprotected'; // B/C
2890  }
2891  }
2892  return $protection;
2893  }
2894 
2905  protected function getTitleProtectionInternal() {
2906  // Can't protect pages in special namespaces
2907  if ( $this->mNamespace < 0 ) {
2908  return false;
2909  }
2910 
2911  // Can't protect pages that exist.
2912  if ( $this->exists() ) {
2913  return false;
2914  }
2915 
2916  if ( $this->mTitleProtection === null ) {
2917  $dbr = wfGetDB( DB_REPLICA );
2918  $commentStore = CommentStore::getStore();
2919  $commentQuery = $commentStore->getJoin( 'pt_reason' );
2920  $res = $dbr->select(
2921  [ 'protected_titles' ] + $commentQuery['tables'],
2922  [
2923  'user' => 'pt_user',
2924  'expiry' => 'pt_expiry',
2925  'permission' => 'pt_create_perm'
2926  ] + $commentQuery['fields'],
2927  [ 'pt_namespace' => $this->mNamespace, 'pt_title' => $this->mDbkeyform ],
2928  __METHOD__,
2929  [],
2930  $commentQuery['joins']
2931  );
2932 
2933  // fetchRow returns false if there are no rows.
2934  $row = $dbr->fetchRow( $res );
2935  if ( $row ) {
2936  $this->mTitleProtection = [
2937  'user' => $row['user'],
2938  'expiry' => $dbr->decodeExpiry( $row['expiry'] ),
2939  'permission' => $row['permission'],
2940  'reason' => $commentStore->getComment( 'pt_reason', $row )->text,
2941  ];
2942  } else {
2943  $this->mTitleProtection = false;
2944  }
2945  }
2946  return $this->mTitleProtection;
2947  }
2948 
2952  public function deleteTitleProtection() {
2953  $dbw = wfGetDB( DB_MASTER );
2954 
2955  $dbw->delete(
2956  'protected_titles',
2957  [ 'pt_namespace' => $this->mNamespace, 'pt_title' => $this->mDbkeyform ],
2958  __METHOD__
2959  );
2960  $this->mTitleProtection = false;
2961  }
2962 
2970  public function isSemiProtected( $action = 'edit' ) {
2972 
2973  $restrictions = $this->getRestrictions( $action );
2975  if ( !$restrictions || !$semi ) {
2976  // Not protected, or all protection is full protection
2977  return false;
2978  }
2979 
2980  // Remap autoconfirmed to editsemiprotected for BC
2981  foreach ( array_keys( $semi, 'autoconfirmed' ) as $key ) {
2982  $semi[$key] = 'editsemiprotected';
2983  }
2984  foreach ( array_keys( $restrictions, 'autoconfirmed' ) as $key ) {
2985  $restrictions[$key] = 'editsemiprotected';
2986  }
2987 
2988  return !array_diff( $restrictions, $semi );
2989  }
2990 
2998  public function isProtected( $action = '' ) {
2999  global $wgRestrictionLevels;
3000 
3001  $restrictionTypes = $this->getRestrictionTypes();
3002 
3003  # Special pages have inherent protection
3004  if ( $this->isSpecialPage() ) {
3005  return true;
3006  }
3007 
3008  # Check regular protection levels
3009  foreach ( $restrictionTypes as $type ) {
3010  if ( $action == $type || $action == '' ) {
3011  $r = $this->getRestrictions( $type );
3012  foreach ( $wgRestrictionLevels as $level ) {
3013  if ( in_array( $level, $r ) && $level != '' ) {
3014  return true;
3015  }
3016  }
3017  }
3018  }
3019 
3020  return false;
3021  }
3022 
3030  public function isNamespaceProtected( User $user ) {
3031  global $wgNamespaceProtection;
3032 
3033  if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) {
3034  foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) {
3035  if ( $right != '' && !$user->isAllowed( $right ) ) {
3036  return true;
3037  }
3038  }
3039  }
3040  return false;
3041  }
3042 
3048  public function isCascadeProtected() {
3049  list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false );
3050  return ( $sources > 0 );
3051  }
3052 
3062  public function areCascadeProtectionSourcesLoaded( $getPages = true ) {
3063  return $getPages ? $this->mCascadeSources !== null : $this->mHasCascadingRestrictions !== null;
3064  }
3065 
3079  public function getCascadeProtectionSources( $getPages = true ) {
3080  $pagerestrictions = [];
3081 
3082  if ( $this->mCascadeSources !== null && $getPages ) {
3084  } elseif ( $this->mHasCascadingRestrictions !== null && !$getPages ) {
3085  return [ $this->mHasCascadingRestrictions, $pagerestrictions ];
3086  }
3087 
3088  $dbr = wfGetDB( DB_REPLICA );
3089 
3090  if ( $this->mNamespace == NS_FILE ) {
3091  $tables = [ 'imagelinks', 'page_restrictions' ];
3092  $where_clauses = [
3093  'il_to' => $this->mDbkeyform,
3094  'il_from=pr_page',
3095  'pr_cascade' => 1
3096  ];
3097  } else {
3098  $tables = [ 'templatelinks', 'page_restrictions' ];
3099  $where_clauses = [
3100  'tl_namespace' => $this->mNamespace,
3101  'tl_title' => $this->mDbkeyform,
3102  'tl_from=pr_page',
3103  'pr_cascade' => 1
3104  ];
3105  }
3106 
3107  if ( $getPages ) {
3108  $cols = [ 'pr_page', 'page_namespace', 'page_title',
3109  'pr_expiry', 'pr_type', 'pr_level' ];
3110  $where_clauses[] = 'page_id=pr_page';
3111  $tables[] = 'page';
3112  } else {
3113  $cols = [ 'pr_expiry' ];
3114  }
3115 
3116  $res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ );
3117 
3118  $sources = $getPages ? [] : false;
3119  $now = wfTimestampNow();
3120 
3121  foreach ( $res as $row ) {
3122  $expiry = $dbr->decodeExpiry( $row->pr_expiry );
3123  if ( $expiry > $now ) {
3124  if ( $getPages ) {
3125  $page_id = $row->pr_page;
3126  $page_ns = $row->page_namespace;
3127  $page_title = $row->page_title;
3128  $sources[$page_id] = self::makeTitle( $page_ns, $page_title );
3129  # Add groups needed for each restriction type if its not already there
3130  # Make sure this restriction type still exists
3131 
3132  if ( !isset( $pagerestrictions[$row->pr_type] ) ) {
3133  $pagerestrictions[$row->pr_type] = [];
3134  }
3135 
3136  if (
3137  isset( $pagerestrictions[$row->pr_type] )
3138  && !in_array( $row->pr_level, $pagerestrictions[$row->pr_type] )
3139  ) {
3140  $pagerestrictions[$row->pr_type][] = $row->pr_level;
3141  }
3142  } else {
3143  $sources = true;
3144  }
3145  }
3146  }
3147 
3148  if ( $getPages ) {
3149  $this->mCascadeSources = $sources;
3150  $this->mCascadingRestrictions = $pagerestrictions;
3151  } else {
3152  $this->mHasCascadingRestrictions = $sources;
3153  }
3154 
3155  return [ $sources, $pagerestrictions ];
3156  }
3157 
3165  public function areRestrictionsLoaded() {
3167  }
3168 
3178  public function getRestrictions( $action ) {
3179  if ( !$this->mRestrictionsLoaded ) {
3180  $this->loadRestrictions();
3181  }
3182  return $this->mRestrictions[$action] ?? [];
3183  }
3184 
3192  public function getAllRestrictions() {
3193  if ( !$this->mRestrictionsLoaded ) {
3194  $this->loadRestrictions();
3195  }
3196  return $this->mRestrictions;
3197  }
3198 
3206  public function getRestrictionExpiry( $action ) {
3207  if ( !$this->mRestrictionsLoaded ) {
3208  $this->loadRestrictions();
3209  }
3210  return $this->mRestrictionsExpiry[$action] ?? false;
3211  }
3212 
3219  if ( !$this->mRestrictionsLoaded ) {
3220  $this->loadRestrictions();
3221  }
3222 
3224  }
3225 
3237  public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) {
3238  // This function will only read rows from a table that we migrated away
3239  // from before adding READ_LATEST support to loadRestrictions, so we
3240  // don't need to support reading from DB_MASTER here.
3241  $dbr = wfGetDB( DB_REPLICA );
3242 
3243  $restrictionTypes = $this->getRestrictionTypes();
3244 
3245  foreach ( $restrictionTypes as $type ) {
3246  $this->mRestrictions[$type] = [];
3247  $this->mRestrictionsExpiry[$type] = 'infinity';
3248  }
3249 
3250  $this->mCascadeRestriction = false;
3251 
3252  # Backwards-compatibility: also load the restrictions from the page record (old format).
3253  if ( $oldFashionedRestrictions !== null ) {
3254  $this->mOldRestrictions = $oldFashionedRestrictions;
3255  }
3256 
3257  if ( $this->mOldRestrictions === false ) {
3258  $this->mOldRestrictions = $dbr->selectField( 'page', 'page_restrictions',
3259  [ 'page_id' => $this->getArticleID() ], __METHOD__ );
3260  }
3261 
3262  if ( $this->mOldRestrictions != '' ) {
3263  foreach ( explode( ':', trim( $this->mOldRestrictions ) ) as $restrict ) {
3264  $temp = explode( '=', trim( $restrict ) );
3265  if ( count( $temp ) == 1 ) {
3266  // old old format should be treated as edit/move restriction
3267  $this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) );
3268  $this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) );
3269  } else {
3270  $restriction = trim( $temp[1] );
3271  if ( $restriction != '' ) { // some old entries are empty
3272  $this->mRestrictions[$temp[0]] = explode( ',', $restriction );
3273  }
3274  }
3275  }
3276  }
3277 
3278  if ( count( $rows ) ) {
3279  # Current system - load second to make them override.
3280  $now = wfTimestampNow();
3281 
3282  # Cycle through all the restrictions.
3283  foreach ( $rows as $row ) {
3284  // Don't take care of restrictions types that aren't allowed
3285  if ( !in_array( $row->pr_type, $restrictionTypes ) ) {
3286  continue;
3287  }
3288 
3289  $expiry = $dbr->decodeExpiry( $row->pr_expiry );
3290 
3291  // Only apply the restrictions if they haven't expired!
3292  if ( !$expiry || $expiry > $now ) {
3293  $this->mRestrictionsExpiry[$row->pr_type] = $expiry;
3294  $this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) );
3295 
3296  $this->mCascadeRestriction |= $row->pr_cascade;
3297  }
3298  }
3299  }
3300 
3301  $this->mRestrictionsLoaded = true;
3302  }
3303 
3314  public function loadRestrictions( $oldFashionedRestrictions = null, $flags = 0 ) {
3315  $readLatest = DBAccessObjectUtils::hasFlags( $flags, self::READ_LATEST );
3316  if ( $this->mRestrictionsLoaded && !$readLatest ) {
3317  return;
3318  }
3319 
3320  // TODO: should probably pass $flags into getArticleID, but it seems hacky
3321  // to mix READ_LATEST and GAID_FOR_UPDATE, even if they have the same value.
3322  // Maybe deprecate GAID_FOR_UPDATE now that we implement IDBAccessObject?
3323  $id = $this->getArticleID();
3324  if ( $id ) {
3325  $fname = __METHOD__;
3326  $loadRestrictionsFromDb = function ( Database $dbr ) use ( $fname, $id ) {
3327  return iterator_to_array(
3328  $dbr->select(
3329  'page_restrictions',
3330  [ 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ],
3331  [ 'pr_page' => $id ],
3332  $fname
3333  )
3334  );
3335  };
3336 
3337  if ( $readLatest ) {
3338  $dbr = wfGetDB( DB_MASTER );
3339  $rows = $loadRestrictionsFromDb( $dbr );
3340  } else {
3342  $rows = $cache->getWithSetCallback(
3343  // Page protections always leave a new null revision
3344  $cache->makeKey( 'page-restrictions', $id, $this->getLatestRevID() ),
3345  $cache::TTL_DAY,
3346  function ( $curValue, &$ttl, array &$setOpts ) use ( $loadRestrictionsFromDb ) {
3347  $dbr = wfGetDB( DB_REPLICA );
3348 
3349  $setOpts += Database::getCacheSetOptions( $dbr );
3350 
3351  return $loadRestrictionsFromDb( $dbr );
3352  }
3353  );
3354  }
3355 
3356  $this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions );
3357  } else {
3358  $title_protection = $this->getTitleProtectionInternal();
3359 
3360  if ( $title_protection ) {
3361  $now = wfTimestampNow();
3362  $expiry = wfGetDB( DB_REPLICA )->decodeExpiry( $title_protection['expiry'] );
3363 
3364  if ( !$expiry || $expiry > $now ) {
3365  // Apply the restrictions
3366  $this->mRestrictionsExpiry['create'] = $expiry;
3367  $this->mRestrictions['create'] =
3368  explode( ',', trim( $title_protection['permission'] ) );
3369  } else { // Get rid of the old restrictions
3370  $this->mTitleProtection = false;
3371  }
3372  } else {
3373  $this->mRestrictionsExpiry['create'] = 'infinity';
3374  }
3375  $this->mRestrictionsLoaded = true;
3376  }
3377  }
3378 
3383  public function flushRestrictions() {
3384  $this->mRestrictionsLoaded = false;
3385  $this->mTitleProtection = null;
3386  }
3387 
3393  static function purgeExpiredRestrictions() {
3394  if ( wfReadOnly() ) {
3395  return;
3396  }
3397 
3399  wfGetDB( DB_MASTER ),
3400  __METHOD__,
3401  function ( IDatabase $dbw, $fname ) {
3402  $config = MediaWikiServices::getInstance()->getMainConfig();
3403  $ids = $dbw->selectFieldValues(
3404  'page_restrictions',
3405  'pr_id',
3406  [ 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
3407  $fname,
3408  [ 'LIMIT' => $config->get( 'UpdateRowsPerQuery' ) ] // T135470
3409  );
3410  if ( $ids ) {
3411  $dbw->delete( 'page_restrictions', [ 'pr_id' => $ids ], $fname );
3412  }
3413  }
3414  ) );
3415 
3417  wfGetDB( DB_MASTER ),
3418  __METHOD__,
3419  function ( IDatabase $dbw, $fname ) {
3420  $dbw->delete(
3421  'protected_titles',
3422  [ 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
3423  $fname
3424  );
3425  }
3426  ) );
3427  }
3428 
3434  public function hasSubpages() {
3435  if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
3436  # Duh
3437  return false;
3438  }
3439 
3440  # We dynamically add a member variable for the purpose of this method
3441  # alone to cache the result. There's no point in having it hanging
3442  # around uninitialized in every Title object; therefore we only add it
3443  # if needed and don't declare it statically.
3444  if ( $this->mHasSubpages === null ) {
3445  $this->mHasSubpages = false;
3446  $subpages = $this->getSubpages( 1 );
3447  if ( $subpages instanceof TitleArray ) {
3448  $this->mHasSubpages = (bool)$subpages->count();
3449  }
3450  }
3451 
3452  return $this->mHasSubpages;
3453  }
3454 
3462  public function getSubpages( $limit = -1 ) {
3463  if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
3464  return [];
3465  }
3466 
3467  $dbr = wfGetDB( DB_REPLICA );
3468  $conds['page_namespace'] = $this->mNamespace;
3469  $conds[] = 'page_title ' . $dbr->buildLike( $this->mDbkeyform . '/', $dbr->anyString() );
3470  $options = [];
3471  if ( $limit > -1 ) {
3472  $options['LIMIT'] = $limit;
3473  }
3475  $dbr->select( 'page',
3476  [ 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ],
3477  $conds,
3478  __METHOD__,
3479  $options
3480  )
3481  );
3482  }
3483 
3489  public function isDeleted() {
3490  if ( $this->mNamespace < 0 ) {
3491  $n = 0;
3492  } else {
3493  $dbr = wfGetDB( DB_REPLICA );
3494 
3495  $n = $dbr->selectField( 'archive', 'COUNT(*)',
3496  [ 'ar_namespace' => $this->mNamespace, 'ar_title' => $this->mDbkeyform ],
3497  __METHOD__
3498  );
3499  if ( $this->mNamespace == NS_FILE ) {
3500  $n += $dbr->selectField( 'filearchive', 'COUNT(*)',
3501  [ 'fa_name' => $this->mDbkeyform ],
3502  __METHOD__
3503  );
3504  }
3505  }
3506  return (int)$n;
3507  }
3508 
3514  public function isDeletedQuick() {
3515  if ( $this->mNamespace < 0 ) {
3516  return false;
3517  }
3518  $dbr = wfGetDB( DB_REPLICA );
3519  $deleted = (bool)$dbr->selectField( 'archive', '1',
3520  [ 'ar_namespace' => $this->mNamespace, 'ar_title' => $this->mDbkeyform ],
3521  __METHOD__
3522  );
3523  if ( !$deleted && $this->mNamespace == NS_FILE ) {
3524  $deleted = (bool)$dbr->selectField( 'filearchive', '1',
3525  [ 'fa_name' => $this->mDbkeyform ],
3526  __METHOD__
3527  );
3528  }
3529  return $deleted;
3530  }
3531 
3540  public function getArticleID( $flags = 0 ) {
3541  if ( $this->mNamespace < 0 ) {
3542  $this->mArticleID = 0;
3543  return $this->mArticleID;
3544  }
3545  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3546  if ( $flags & self::GAID_FOR_UPDATE ) {
3547  $oldUpdate = $linkCache->forUpdate( true );
3548  $linkCache->clearLink( $this );
3549  $this->mArticleID = $linkCache->addLinkObj( $this );
3550  $linkCache->forUpdate( $oldUpdate );
3551  } else {
3552  if ( $this->mArticleID == -1 ) {
3553  $this->mArticleID = $linkCache->addLinkObj( $this );
3554  }
3555  }
3556  return $this->mArticleID;
3557  }
3558 
3566  public function isRedirect( $flags = 0 ) {
3567  if ( !is_null( $this->mRedirect ) ) {
3568  return $this->mRedirect;
3569  }
3570  if ( !$this->getArticleID( $flags ) ) {
3571  $this->mRedirect = false;
3572  return $this->mRedirect;
3573  }
3574 
3575  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3576  $linkCache->addLinkObj( $this ); # in case we already had an article ID
3577  $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' );
3578  if ( $cached === null ) {
3579  # Trust LinkCache's state over our own
3580  # LinkCache is telling us that the page doesn't exist, despite there being cached
3581  # data relating to an existing page in $this->mArticleID. Updaters should clear
3582  # LinkCache as appropriate, or use $flags = Title::GAID_FOR_UPDATE. If that flag is
3583  # set, then LinkCache will definitely be up to date here, since getArticleID() forces
3584  # LinkCache to refresh its data from the master.
3585  $this->mRedirect = false;
3586  return $this->mRedirect;
3587  }
3588 
3589  $this->mRedirect = (bool)$cached;
3590 
3591  return $this->mRedirect;
3592  }
3593 
3601  public function getLength( $flags = 0 ) {
3602  if ( $this->mLength != -1 ) {
3603  return $this->mLength;
3604  }
3605  if ( !$this->getArticleID( $flags ) ) {
3606  $this->mLength = 0;
3607  return $this->mLength;
3608  }
3609  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3610  $linkCache->addLinkObj( $this ); # in case we already had an article ID
3611  $cached = $linkCache->getGoodLinkFieldObj( $this, 'length' );
3612  if ( $cached === null ) {
3613  # Trust LinkCache's state over our own, as for isRedirect()
3614  $this->mLength = 0;
3615  return $this->mLength;
3616  }
3617 
3618  $this->mLength = intval( $cached );
3619 
3620  return $this->mLength;
3621  }
3622 
3629  public function getLatestRevID( $flags = 0 ) {
3630  if ( !( $flags & self::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) {
3631  return intval( $this->mLatestID );
3632  }
3633  if ( !$this->getArticleID( $flags ) ) {
3634  $this->mLatestID = 0;
3635  return $this->mLatestID;
3636  }
3637  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3638  $linkCache->addLinkObj( $this ); # in case we already had an article ID
3639  $cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' );
3640  if ( $cached === null ) {
3641  # Trust LinkCache's state over our own, as for isRedirect()
3642  $this->mLatestID = 0;
3643  return $this->mLatestID;
3644  }
3645 
3646  $this->mLatestID = intval( $cached );
3647 
3648  return $this->mLatestID;
3649  }
3650 
3661  public function resetArticleID( $newid ) {
3662  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3663  $linkCache->clearLink( $this );
3664 
3665  if ( $newid === false ) {
3666  $this->mArticleID = -1;
3667  } else {
3668  $this->mArticleID = intval( $newid );
3669  }
3670  $this->mRestrictionsLoaded = false;
3671  $this->mRestrictions = [];
3672  $this->mOldRestrictions = false;
3673  $this->mRedirect = null;
3674  $this->mLength = -1;
3675  $this->mLatestID = false;
3676  $this->mContentModel = false;
3677  $this->mEstimateRevisions = null;
3678  $this->mPageLanguage = false;
3679  $this->mDbPageLanguage = false;
3680  $this->mIsBigDeletion = null;
3681  }
3682 
3683  public static function clearCaches() {
3684  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3685  $linkCache->clear();
3686 
3687  $titleCache = self::getTitleCache();
3688  $titleCache->clear();
3689  }
3690 
3698  public static function capitalize( $text, $ns = NS_MAIN ) {
3699  if ( MWNamespace::isCapitalized( $ns ) ) {
3700  return MediaWikiServices::getInstance()->getContentLanguage()->ucfirst( $text );
3701  } else {
3702  return $text;
3703  }
3704  }
3705 
3718  private function secureAndSplit() {
3719  // @note: splitTitleString() is a temporary hack to allow MediaWikiTitleCodec to share
3720  // the parsing code with Title, while avoiding massive refactoring.
3721  // @todo: get rid of secureAndSplit, refactor parsing code.
3722  // @note: getTitleParser() returns a TitleParser implementation which does not have a
3723  // splitTitleString method, but the only implementation (MediaWikiTitleCodec) does
3724  $titleCodec = MediaWikiServices::getInstance()->getTitleParser();
3725  // MalformedTitleException can be thrown here
3726  $parts = $titleCodec->splitTitleString( $this->mDbkeyform, $this->mDefaultNamespace );
3727 
3728  # Fill fields
3729  $this->setFragment( '#' . $parts['fragment'] );
3730  $this->mInterwiki = $parts['interwiki'];
3731  $this->mLocalInterwiki = $parts['local_interwiki'];
3732  $this->mNamespace = $parts['namespace'];
3733  $this->mUserCaseDBKey = $parts['user_case_dbkey'];
3734 
3735  $this->mDbkeyform = $parts['dbkey'];
3736  $this->mUrlform = wfUrlencode( $this->mDbkeyform );
3737  $this->mTextform = strtr( $this->mDbkeyform, '_', ' ' );
3738 
3739  # We already know that some pages won't be in the database!
3740  if ( $this->isExternal() || $this->isSpecialPage() ) {
3741  $this->mArticleID = 0;
3742  }
3743 
3744  return true;
3745  }
3746 
3759  public function getLinksTo( $options = [], $table = 'pagelinks', $prefix = 'pl' ) {
3760  if ( count( $options ) > 0 ) {
3761  $db = wfGetDB( DB_MASTER );
3762  } else {
3763  $db = wfGetDB( DB_REPLICA );
3764  }
3765 
3766  $res = $db->select(
3767  [ 'page', $table ],
3768  self::getSelectFields(),
3769  [
3770  "{$prefix}_from=page_id",
3771  "{$prefix}_namespace" => $this->mNamespace,
3772  "{$prefix}_title" => $this->mDbkeyform ],
3773  __METHOD__,
3774  $options
3775  );
3776 
3777  $retVal = [];
3778  if ( $res->numRows() ) {
3779  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3780  foreach ( $res as $row ) {
3781  $titleObj = self::makeTitle( $row->page_namespace, $row->page_title );
3782  if ( $titleObj ) {
3783  $linkCache->addGoodLinkObjFromRow( $titleObj, $row );
3784  $retVal[] = $titleObj;
3785  }
3786  }
3787  }
3788  return $retVal;
3789  }
3790 
3801  public function getTemplateLinksTo( $options = [] ) {
3802  return $this->getLinksTo( $options, 'templatelinks', 'tl' );
3803  }
3804 
3817  public function getLinksFrom( $options = [], $table = 'pagelinks', $prefix = 'pl' ) {
3818  $id = $this->getArticleID();
3819 
3820  # If the page doesn't exist; there can't be any link from this page
3821  if ( !$id ) {
3822  return [];
3823  }
3824 
3825  $db = wfGetDB( DB_REPLICA );
3826 
3827  $blNamespace = "{$prefix}_namespace";
3828  $blTitle = "{$prefix}_title";
3829 
3830  $pageQuery = WikiPage::getQueryInfo();
3831  $res = $db->select(
3832  [ $table, 'nestpage' => $pageQuery['tables'] ],
3833  array_merge(
3834  [ $blNamespace, $blTitle ],
3835  $pageQuery['fields']
3836  ),
3837  [ "{$prefix}_from" => $id ],
3838  __METHOD__,
3839  $options,
3840  [ 'nestpage' => [
3841  'LEFT JOIN',
3842  [ "page_namespace=$blNamespace", "page_title=$blTitle" ]
3843  ] ] + $pageQuery['joins']
3844  );
3845 
3846  $retVal = [];
3847  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
3848  foreach ( $res as $row ) {
3849  if ( $row->page_id ) {
3850  $titleObj = self::newFromRow( $row );
3851  } else {
3852  $titleObj = self::makeTitle( $row->$blNamespace, $row->$blTitle );
3853  $linkCache->addBadLinkObj( $titleObj );
3854  }
3855  $retVal[] = $titleObj;
3856  }
3857 
3858  return $retVal;
3859  }
3860 
3871  public function getTemplateLinksFrom( $options = [] ) {
3872  return $this->getLinksFrom( $options, 'templatelinks', 'tl' );
3873  }
3874 
3883  public function getBrokenLinksFrom() {
3884  if ( $this->getArticleID() == 0 ) {
3885  # All links from article ID 0 are false positives
3886  return [];
3887  }
3888 
3889  $dbr = wfGetDB( DB_REPLICA );
3890  $res = $dbr->select(
3891  [ 'page', 'pagelinks' ],
3892  [ 'pl_namespace', 'pl_title' ],
3893  [
3894  'pl_from' => $this->getArticleID(),
3895  'page_namespace IS NULL'
3896  ],
3897  __METHOD__, [],
3898  [
3899  'page' => [
3900  'LEFT JOIN',
3901  [ 'pl_namespace=page_namespace', 'pl_title=page_title' ]
3902  ]
3903  ]
3904  );
3905 
3906  $retVal = [];
3907  foreach ( $res as $row ) {
3908  $retVal[] = self::makeTitle( $row->pl_namespace, $row->pl_title );
3909  }
3910  return $retVal;
3911  }
3912 
3919  public function getCdnUrls() {
3920  $urls = [
3921  $this->getInternalURL(),
3922  $this->getInternalURL( 'action=history' )
3923  ];
3924 
3925  $pageLang = $this->getPageLanguage();
3926  if ( $pageLang->hasVariants() ) {
3927  $variants = $pageLang->getVariants();
3928  foreach ( $variants as $vCode ) {
3929  $urls[] = $this->getInternalURL( $vCode );
3930  }
3931  }
3932 
3933  // If we are looking at a css/js user subpage, purge the action=raw.
3934  if ( $this->isUserJsConfigPage() ) {
3935  $urls[] = $this->getInternalURL( 'action=raw&ctype=text/javascript' );
3936  } elseif ( $this->isUserJsonConfigPage() ) {
3937  $urls[] = $this->getInternalURL( 'action=raw&ctype=application/json' );
3938  } elseif ( $this->isUserCssConfigPage() ) {
3939  $urls[] = $this->getInternalURL( 'action=raw&ctype=text/css' );
3940  }
3941 
3942  Hooks::run( 'TitleSquidURLs', [ $this, &$urls ] );
3943  return $urls;
3944  }
3945 
3949  public function purgeSquid() {
3951  new CdnCacheUpdate( $this->getCdnUrls() ),
3953  );
3954  }
3955 
3966  public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) {
3967  global $wgUser;
3968 
3969  if ( !( $nt instanceof Title ) ) {
3970  // Normally we'd add this to $errors, but we'll get
3971  // lots of syntax errors if $nt is not an object
3972  return [ [ 'badtitletext' ] ];
3973  }
3974 
3975  $mp = new MovePage( $this, $nt );
3976  $errors = $mp->isValidMove()->getErrorsArray();
3977  if ( $auth ) {
3978  $errors = wfMergeErrorArrays(
3979  $errors,
3980  $mp->checkPermissions( $wgUser, $reason )->getErrorsArray()
3981  );
3982  }
3983 
3984  return $errors ?: true;
3985  }
3986 
3993  protected function validateFileMoveOperation( $nt ) {
3994  global $wgUser;
3995 
3996  $errors = [];
3997 
3998  $destFile = wfLocalFile( $nt );
3999  $destFile->load( File::READ_LATEST );
4000  if ( !$wgUser->isAllowed( 'reupload-shared' )
4001  && !$destFile->exists() && wfFindFile( $nt )
4002  ) {
4003  $errors[] = [ 'file-exists-sharedrepo' ];
4004  }
4005 
4006  return $errors;
4007  }
4008 
4022  public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true,
4023  array $changeTags = []
4024  ) {
4025  global $wgUser;
4026  $err = $this->isValidMoveOperation( $nt, $auth, $reason );
4027  if ( is_array( $err ) ) {
4028  // Auto-block user's IP if the account was "hard" blocked
4029  $wgUser->spreadAnyEditBlock();
4030  return $err;
4031  }
4032  // Check suppressredirect permission
4033  if ( $auth && !$wgUser->isAllowed( 'suppressredirect' ) ) {
4034  $createRedirect = true;
4035  }
4036 
4037  $mp = new MovePage( $this, $nt );
4038  $status = $mp->move( $wgUser, $reason, $createRedirect, $changeTags );
4039  if ( $status->isOK() ) {
4040  return true;
4041  } else {
4042  return $status->getErrorsArray();
4043  }
4044  }
4045 
4060  public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true,
4061  array $changeTags = []
4062  ) {
4063  global $wgMaximumMovedPages;
4064  // Check permissions
4065  if ( !$this->userCan( 'move-subpages' ) ) {
4066  return [
4067  [ 'cant-move-subpages' ],
4068  ];
4069  }
4070  // Do the source and target namespaces support subpages?
4071  if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
4072  return [
4073  [ 'namespace-nosubpages', MWNamespace::getCanonicalName( $this->mNamespace ) ],
4074  ];
4075  }
4076  if ( !MWNamespace::hasSubpages( $nt->getNamespace() ) ) {
4077  return [
4078  [ 'namespace-nosubpages', MWNamespace::getCanonicalName( $nt->getNamespace() ) ],
4079  ];
4080  }
4081 
4082  $subpages = $this->getSubpages( $wgMaximumMovedPages + 1 );
4083  $retval = [];
4084  $count = 0;
4085  foreach ( $subpages as $oldSubpage ) {
4086  $count++;
4087  if ( $count > $wgMaximumMovedPages ) {
4088  $retval[$oldSubpage->getPrefixedText()] = [
4089  [ 'movepage-max-pages', $wgMaximumMovedPages ],
4090  ];
4091  break;
4092  }
4093 
4094  // We don't know whether this function was called before
4095  // or after moving the root page, so check both
4096  // $this and $nt
4097  if ( $oldSubpage->getArticleID() == $this->getArticleID()
4098  || $oldSubpage->getArticleID() == $nt->getArticleID()
4099  ) {
4100  // When moving a page to a subpage of itself,
4101  // don't move it twice
4102  continue;
4103  }
4104  $newPageName = preg_replace(
4105  '#^' . preg_quote( $this->mDbkeyform, '#' ) . '#',
4106  StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # T23234
4107  $oldSubpage->getDBkey() );
4108  if ( $oldSubpage->isTalkPage() ) {
4109  $newNs = $nt->getTalkPage()->getNamespace();
4110  } else {
4111  $newNs = $nt->getSubjectPage()->getNamespace();
4112  }
4113  # T16385: we need makeTitleSafe because the new page names may
4114  # be longer than 255 characters.
4115  $newSubpage = self::makeTitleSafe( $newNs, $newPageName );
4116 
4117  $success = $oldSubpage->moveTo( $newSubpage, $auth, $reason, $createRedirect, $changeTags );
4118  if ( $success === true ) {
4119  $retval[$oldSubpage->getPrefixedText()] = $newSubpage->getPrefixedText();
4120  } else {
4121  $retval[$oldSubpage->getPrefixedText()] = $success;
4122  }
4123  }
4124  return $retval;
4125  }
4126 
4133  public function isSingleRevRedirect() {
4134  global $wgContentHandlerUseDB;
4135 
4136  $dbw = wfGetDB( DB_MASTER );
4137 
4138  # Is it a redirect?
4139  $fields = [ 'page_is_redirect', 'page_latest', 'page_id' ];
4140  if ( $wgContentHandlerUseDB ) {
4141  $fields[] = 'page_content_model';
4142  }
4143 
4144  $row = $dbw->selectRow( 'page',
4145  $fields,
4146  $this->pageCond(),
4147  __METHOD__,
4148  [ 'FOR UPDATE' ]
4149  );
4150  # Cache some fields we may want
4151  $this->mArticleID = $row ? intval( $row->page_id ) : 0;
4152  $this->mRedirect = $row ? (bool)$row->page_is_redirect : false;
4153  $this->mLatestID = $row ? intval( $row->page_latest ) : false;
4154  $this->mContentModel = $row && isset( $row->page_content_model )
4155  ? strval( $row->page_content_model )
4156  : false;
4157 
4158  if ( !$this->mRedirect ) {
4159  return false;
4160  }
4161  # Does the article have a history?
4162  $row = $dbw->selectField( [ 'page', 'revision' ],
4163  'rev_id',
4164  [ 'page_namespace' => $this->mNamespace,
4165  'page_title' => $this->mDbkeyform,
4166  'page_id=rev_page',
4167  'page_latest != rev_id'
4168  ],
4169  __METHOD__,
4170  [ 'FOR UPDATE' ]
4171  );
4172  # Return true if there was no history
4173  return ( $row === false );
4174  }
4175 
4184  public function isValidMoveTarget( $nt ) {
4185  # Is it an existing file?
4186  if ( $nt->getNamespace() == NS_FILE ) {
4187  $file = wfLocalFile( $nt );
4188  $file->load( File::READ_LATEST );
4189  if ( $file->exists() ) {
4190  wfDebug( __METHOD__ . ": file exists\n" );
4191  return false;
4192  }
4193  }
4194  # Is it a redirect with no history?
4195  if ( !$nt->isSingleRevRedirect() ) {
4196  wfDebug( __METHOD__ . ": not a one-rev redirect\n" );
4197  return false;
4198  }
4199  # Get the article text
4200  $rev = Revision::newFromTitle( $nt, false, Revision::READ_LATEST );
4201  if ( !is_object( $rev ) ) {
4202  return false;
4203  }
4204  $content = $rev->getContent();
4205  # Does the redirect point to the source?
4206  # Or is it a broken self-redirect, usually caused by namespace collisions?
4207  $redirTitle = $content ? $content->getRedirectTarget() : null;
4208 
4209  if ( $redirTitle ) {
4210  if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
4211  $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) {
4212  wfDebug( __METHOD__ . ": redirect points to other page\n" );
4213  return false;
4214  } else {
4215  return true;
4216  }
4217  } else {
4218  # Fail safe (not a redirect after all. strange.)
4219  wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() .
4220  " is a redirect, but it doesn't contain a valid redirect.\n" );
4221  return false;
4222  }
4223  }
4224 
4232  public function getParentCategories() {
4233  $data = [];
4234 
4235  $titleKey = $this->getArticleID();
4236 
4237  if ( $titleKey === 0 ) {
4238  return $data;
4239  }
4240 
4241  $dbr = wfGetDB( DB_REPLICA );
4242 
4243  $res = $dbr->select(
4244  'categorylinks',
4245  'cl_to',
4246  [ 'cl_from' => $titleKey ],
4247  __METHOD__
4248  );
4249 
4250  if ( $res->numRows() > 0 ) {
4251  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
4252  foreach ( $res as $row ) {
4253  // $data[] = Title::newFromText( $contLang->getNsText ( NS_CATEGORY ).':'.$row->cl_to);
4254  $data[$contLang->getNsText( NS_CATEGORY ) . ':' . $row->cl_to] =
4255  $this->getFullText();
4256  }
4257  }
4258  return $data;
4259  }
4260 
4267  public function getParentCategoryTree( $children = [] ) {
4268  $stack = [];
4269  $parents = $this->getParentCategories();
4270 
4271  if ( $parents ) {
4272  foreach ( $parents as $parent => $current ) {
4273  if ( array_key_exists( $parent, $children ) ) {
4274  # Circular reference
4275  $stack[$parent] = [];
4276  } else {
4277  $nt = self::newFromText( $parent );
4278  if ( $nt ) {
4279  $stack[$parent] = $nt->getParentCategoryTree( $children + [ $parent => 1 ] );
4280  }
4281  }
4282  }
4283  }
4284 
4285  return $stack;
4286  }
4287 
4294  public function pageCond() {
4295  if ( $this->mArticleID > 0 ) {
4296  // PK avoids secondary lookups in InnoDB, shouldn't hurt other DBs
4297  return [ 'page_id' => $this->mArticleID ];
4298  } else {
4299  return [ 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform ];
4300  }
4301  }
4302 
4310  private function getRelativeRevisionID( $revId, $flags, $dir ) {
4311  $revId = (int)$revId;
4312  if ( $dir === 'next' ) {
4313  $op = '>';
4314  $sort = 'ASC';
4315  } elseif ( $dir === 'prev' ) {
4316  $op = '<';
4317  $sort = 'DESC';
4318  } else {
4319  throw new InvalidArgumentException( '$dir must be "next" or "prev"' );
4320  }
4321 
4322  if ( $flags & self::GAID_FOR_UPDATE ) {
4323  $db = wfGetDB( DB_MASTER );
4324  } else {
4325  $db = wfGetDB( DB_REPLICA, 'contributions' );
4326  }
4327 
4328  // Intentionally not caring if the specified revision belongs to this
4329  // page. We only care about the timestamp.
4330  $ts = $db->selectField( 'revision', 'rev_timestamp', [ 'rev_id' => $revId ], __METHOD__ );
4331  if ( $ts === false ) {
4332  $ts = $db->selectField( 'archive', 'ar_timestamp', [ 'ar_rev_id' => $revId ], __METHOD__ );
4333  if ( $ts === false ) {
4334  // Or should this throw an InvalidArgumentException or something?
4335  return false;
4336  }
4337  }
4338  $ts = $db->addQuotes( $ts );
4339 
4340  $revId = $db->selectField( 'revision', 'rev_id',
4341  [
4342  'rev_page' => $this->getArticleID( $flags ),
4343  "rev_timestamp $op $ts OR (rev_timestamp = $ts AND rev_id $op $revId)"
4344  ],
4345  __METHOD__,
4346  [
4347  'ORDER BY' => "rev_timestamp $sort, rev_id $sort",
4348  'IGNORE INDEX' => 'rev_timestamp', // Probably needed for T159319
4349  ]
4350  );
4351 
4352  if ( $revId === false ) {
4353  return false;
4354  } else {
4355  return intval( $revId );
4356  }
4357  }
4358 
4366  public function getPreviousRevisionID( $revId, $flags = 0 ) {
4367  return $this->getRelativeRevisionID( $revId, $flags, 'prev' );
4368  }
4369 
4377  public function getNextRevisionID( $revId, $flags = 0 ) {
4378  return $this->getRelativeRevisionID( $revId, $flags, 'next' );
4379  }
4380 
4387  public function getFirstRevision( $flags = 0 ) {
4388  $pageId = $this->getArticleID( $flags );
4389  if ( $pageId ) {
4390  $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_REPLICA );
4392  $row = $db->selectRow( $revQuery['tables'], $revQuery['fields'],
4393  [ 'rev_page' => $pageId ],
4394  __METHOD__,
4395  [
4396  'ORDER BY' => 'rev_timestamp ASC, rev_id ASC',
4397  'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319
4398  ],
4399  $revQuery['joins']
4400  );
4401  if ( $row ) {
4402  return new Revision( $row, 0, $this );
4403  }
4404  }
4405  return null;
4406  }
4407 
4414  public function getEarliestRevTime( $flags = 0 ) {
4415  $rev = $this->getFirstRevision( $flags );
4416  return $rev ? $rev->getTimestamp() : null;
4417  }
4418 
4424  public function isNewPage() {
4425  $dbr = wfGetDB( DB_REPLICA );
4426  return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ );
4427  }
4428 
4434  public function isBigDeletion() {
4435  global $wgDeleteRevisionsLimit;
4436 
4437  if ( !$wgDeleteRevisionsLimit ) {
4438  return false;
4439  }
4440 
4441  if ( $this->mIsBigDeletion === null ) {
4442  $dbr = wfGetDB( DB_REPLICA );
4443 
4444  $revCount = $dbr->selectRowCount(
4445  'revision',
4446  '1',
4447  [ 'rev_page' => $this->getArticleID() ],
4448  __METHOD__,
4449  [ 'LIMIT' => $wgDeleteRevisionsLimit + 1 ]
4450  );
4451 
4452  $this->mIsBigDeletion = $revCount > $wgDeleteRevisionsLimit;
4453  }
4454 
4455  return $this->mIsBigDeletion;
4456  }
4457 
4463  public function estimateRevisionCount() {
4464  if ( !$this->exists() ) {
4465  return 0;
4466  }
4467 
4468  if ( $this->mEstimateRevisions === null ) {
4469  $dbr = wfGetDB( DB_REPLICA );
4470  $this->mEstimateRevisions = $dbr->estimateRowCount( 'revision', '*',
4471  [ 'rev_page' => $this->getArticleID() ], __METHOD__ );
4472  }
4473 
4475  }
4476 
4486  public function countRevisionsBetween( $old, $new, $max = null ) {
4487  if ( !( $old instanceof Revision ) ) {
4488  $old = Revision::newFromTitle( $this, (int)$old );
4489  }
4490  if ( !( $new instanceof Revision ) ) {
4491  $new = Revision::newFromTitle( $this, (int)$new );
4492  }
4493  if ( !$old || !$new ) {
4494  return 0; // nothing to compare
4495  }
4496  $dbr = wfGetDB( DB_REPLICA );
4497  $conds = [
4498  'rev_page' => $this->getArticleID(),
4499  'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
4500  'rev_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
4501  ];
4502  if ( $max !== null ) {
4503  return $dbr->selectRowCount( 'revision', '1',
4504  $conds,
4505  __METHOD__,
4506  [ 'LIMIT' => $max + 1 ] // extra to detect truncation
4507  );
4508  } else {
4509  return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ );
4510  }
4511  }
4512 
4529  public function getAuthorsBetween( $old, $new, $limit, $options = [] ) {
4530  if ( !( $old instanceof Revision ) ) {
4531  $old = Revision::newFromTitle( $this, (int)$old );
4532  }
4533  if ( !( $new instanceof Revision ) ) {
4534  $new = Revision::newFromTitle( $this, (int)$new );
4535  }
4536  // XXX: what if Revision objects are passed in, but they don't refer to this title?
4537  // Add $old->getPage() != $new->getPage() || $old->getPage() != $this->getArticleID()
4538  // in the sanity check below?
4539  if ( !$old || !$new ) {
4540  return null; // nothing to compare
4541  }
4542  $authors = [];
4543  $old_cmp = '>';
4544  $new_cmp = '<';
4545  $options = (array)$options;
4546  if ( in_array( 'include_old', $options ) ) {
4547  $old_cmp = '>=';
4548  }
4549  if ( in_array( 'include_new', $options ) ) {
4550  $new_cmp = '<=';
4551  }
4552  if ( in_array( 'include_both', $options ) ) {
4553  $old_cmp = '>=';
4554  $new_cmp = '<=';
4555  }
4556  // No DB query needed if $old and $new are the same or successive revisions:
4557  if ( $old->getId() === $new->getId() ) {
4558  return ( $old_cmp === '>' && $new_cmp === '<' ) ?
4559  [] :
4560  [ $old->getUserText( Revision::RAW ) ];
4561  } elseif ( $old->getId() === $new->getParentId() ) {
4562  if ( $old_cmp === '>=' && $new_cmp === '<=' ) {
4563  $authors[] = $old->getUserText( Revision::RAW );
4564  if ( $old->getUserText( Revision::RAW ) != $new->getUserText( Revision::RAW ) ) {
4565  $authors[] = $new->getUserText( Revision::RAW );
4566  }
4567  } elseif ( $old_cmp === '>=' ) {
4568  $authors[] = $old->getUserText( Revision::RAW );
4569  } elseif ( $new_cmp === '<=' ) {
4570  $authors[] = $new->getUserText( Revision::RAW );
4571  }
4572  return $authors;
4573  }
4574  $dbr = wfGetDB( DB_REPLICA );
4576  $authors = $dbr->selectFieldValues(
4577  $revQuery['tables'],
4578  $revQuery['fields']['rev_user_text'],
4579  [
4580  'rev_page' => $this->getArticleID(),
4581  "rev_timestamp $old_cmp " . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
4582  "rev_timestamp $new_cmp " . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
4583  ], __METHOD__,
4584  [ 'DISTINCT', 'LIMIT' => $limit + 1 ], // add one so caller knows it was truncated
4585  $revQuery['joins']
4586  );
4587  return $authors;
4588  }
4589 
4604  public function countAuthorsBetween( $old, $new, $limit, $options = [] ) {
4605  $authors = $this->getAuthorsBetween( $old, $new, $limit, $options );
4606  return $authors ? count( $authors ) : 0;
4607  }
4608 
4615  public function equals( Title $title ) {
4616  // Note: === is necessary for proper matching of number-like titles.
4617  return $this->mInterwiki === $title->mInterwiki
4618  && $this->mNamespace == $title->mNamespace
4619  && $this->mDbkeyform === $title->mDbkeyform;
4620  }
4621 
4628  public function isSubpageOf( Title $title ) {
4629  return $this->mInterwiki === $title->mInterwiki
4630  && $this->mNamespace == $title->mNamespace
4631  && strpos( $this->mDbkeyform, $title->mDbkeyform . '/' ) === 0;
4632  }
4633 
4645  public function exists( $flags = 0 ) {
4646  $exists = $this->getArticleID( $flags ) != 0;
4647  Hooks::run( 'TitleExists', [ $this, &$exists ] );
4648  return $exists;
4649  }
4650 
4667  public function isAlwaysKnown() {
4668  $isKnown = null;
4669 
4680  Hooks::run( 'TitleIsAlwaysKnown', [ $this, &$isKnown ] );
4681 
4682  if ( !is_null( $isKnown ) ) {
4683  return $isKnown;
4684  }
4685 
4686  if ( $this->isExternal() ) {
4687  return true; // any interwiki link might be viewable, for all we know
4688  }
4689 
4690  switch ( $this->mNamespace ) {
4691  case NS_MEDIA:
4692  case NS_FILE:
4693  // file exists, possibly in a foreign repo
4694  return (bool)wfFindFile( $this );
4695  case NS_SPECIAL:
4696  // valid special page
4697  return MediaWikiServices::getInstance()->getSpecialPageFactory()->
4698  exists( $this->mDbkeyform );
4699  case NS_MAIN:
4700  // selflink, possibly with fragment
4701  return $this->mDbkeyform == '';
4702  case NS_MEDIAWIKI:
4703  // known system message
4704  return $this->hasSourceText() !== false;
4705  default:
4706  return false;
4707  }
4708  }
4709 
4721  public function isKnown() {
4722  return $this->isAlwaysKnown() || $this->exists();
4723  }
4724 
4730  public function hasSourceText() {
4731  if ( $this->exists() ) {
4732  return true;
4733  }
4734 
4735  if ( $this->mNamespace == NS_MEDIAWIKI ) {
4736  // If the page doesn't exist but is a known system message, default
4737  // message content will be displayed, same for language subpages-
4738  // Use always content language to avoid loading hundreds of languages
4739  // to get the link color.
4740  $contLang = MediaWikiServices::getInstance()->getContentLanguage();
4741  list( $name, ) = MessageCache::singleton()->figureMessage(
4742  $contLang->lcfirst( $this->getText() )
4743  );
4744  $message = wfMessage( $name )->inLanguage( $contLang )->useDatabase( false );
4745  return $message->exists();
4746  }
4747 
4748  return false;
4749  }
4750 
4788  public function getDefaultMessageText() {
4789  if ( $this->mNamespace != NS_MEDIAWIKI ) { // Just in case
4790  return false;
4791  }
4792 
4793  list( $name, $lang ) = MessageCache::singleton()->figureMessage(
4794  MediaWikiServices::getInstance()->getContentLanguage()->lcfirst( $this->getText() )
4795  );
4796  $message = wfMessage( $name )->inLanguage( $lang )->useDatabase( false );
4797 
4798  if ( $message->exists() ) {
4799  return $message->plain();
4800  } else {
4801  return false;
4802  }
4803  }
4804 
4811  public function invalidateCache( $purgeTime = null ) {
4812  if ( wfReadOnly() ) {
4813  return false;
4814  } elseif ( $this->mArticleID === 0 ) {
4815  return true; // avoid gap locking if we know it's not there
4816  }
4817 
4818  $dbw = wfGetDB( DB_MASTER );
4819  $dbw->onTransactionPreCommitOrIdle(
4820  function () use ( $dbw ) {
4822  $this, null, null, $dbw->getDomainId() );
4823  },
4824  __METHOD__
4825  );
4826 
4827  $conds = $this->pageCond();
4829  new AutoCommitUpdate(
4830  $dbw,
4831  __METHOD__,
4832  function ( IDatabase $dbw, $fname ) use ( $conds, $purgeTime ) {
4833  $dbTimestamp = $dbw->timestamp( $purgeTime ?: time() );
4834  $dbw->update(
4835  'page',
4836  [ 'page_touched' => $dbTimestamp ],
4837  $conds + [ 'page_touched < ' . $dbw->addQuotes( $dbTimestamp ) ],
4838  $fname
4839  );
4840  MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $this );
4841  }
4842  ),
4844  );
4845 
4846  return true;
4847  }
4848 
4854  public function touchLinks() {
4855  DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'pagelinks', 'page-touch' ) );
4856  if ( $this->mNamespace == NS_CATEGORY ) {
4858  new HTMLCacheUpdate( $this, 'categorylinks', 'category-touch' )
4859  );
4860  }
4861  }
4862 
4869  public function getTouched( $db = null ) {
4870  if ( $db === null ) {
4871  $db = wfGetDB( DB_REPLICA );
4872  }
4873  $touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ );
4874  return $touched;
4875  }
4876 
4883  public function getNotificationTimestamp( $user = null ) {
4884  global $wgUser;
4885 
4886  // Assume current user if none given
4887  if ( !$user ) {
4888  $user = $wgUser;
4889  }
4890  // Check cache first
4891  $uid = $user->getId();
4892  if ( !$uid ) {
4893  return false;
4894  }
4895  // avoid isset here, as it'll return false for null entries
4896  if ( array_key_exists( $uid, $this->mNotificationTimestamp ) ) {
4897  return $this->mNotificationTimestamp[$uid];
4898  }
4899  // Don't cache too much!
4900  if ( count( $this->mNotificationTimestamp ) >= self::CACHE_MAX ) {
4901  $this->mNotificationTimestamp = [];
4902  }
4903 
4904  $store = MediaWikiServices::getInstance()->getWatchedItemStore();
4905  $watchedItem = $store->getWatchedItem( $user, $this );
4906  if ( $watchedItem ) {
4907  $this->mNotificationTimestamp[$uid] = $watchedItem->getNotificationTimestamp();
4908  } else {
4909  $this->mNotificationTimestamp[$uid] = false;
4910  }
4911 
4912  return $this->mNotificationTimestamp[$uid];
4913  }
4914 
4921  public function getNamespaceKey( $prepend = 'nstab-' ) {
4922  // Gets the subject namespace of this title
4923  $subjectNS = MWNamespace::getSubject( $this->mNamespace );
4924  // Prefer canonical namespace name for HTML IDs
4925  $namespaceKey = MWNamespace::getCanonicalName( $subjectNS );
4926  if ( $namespaceKey === false ) {
4927  // Fallback to localised text
4928  $namespaceKey = $this->getSubjectNsText();
4929  }
4930  // Makes namespace key lowercase
4931  $namespaceKey = MediaWikiServices::getInstance()->getContentLanguage()->lc( $namespaceKey );
4932  // Uses main
4933  if ( $namespaceKey == '' ) {
4934  $namespaceKey = 'main';
4935  }
4936  // Changes file to image for backwards compatibility
4937  if ( $namespaceKey == 'file' ) {
4938  $namespaceKey = 'image';
4939  }
4940  return $prepend . $namespaceKey;
4941  }
4942 
4949  public function getRedirectsHere( $ns = null ) {
4950  $redirs = [];
4951 
4952  $dbr = wfGetDB( DB_REPLICA );
4953  $where = [
4954  'rd_namespace' => $this->mNamespace,
4955  'rd_title' => $this->mDbkeyform,
4956  'rd_from = page_id'
4957  ];
4958  if ( $this->isExternal() ) {
4959  $where['rd_interwiki'] = $this->mInterwiki;
4960  } else {
4961  $where[] = 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL';
4962  }
4963  if ( !is_null( $ns ) ) {
4964  $where['page_namespace'] = $ns;
4965  }
4966 
4967  $res = $dbr->select(
4968  [ 'redirect', 'page' ],
4969  [ 'page_namespace', 'page_title' ],
4970  $where,
4971  __METHOD__
4972  );
4973 
4974  foreach ( $res as $row ) {
4975  $redirs[] = self::newFromRow( $row );
4976  }
4977  return $redirs;
4978  }
4979 
4985  public function isValidRedirectTarget() {
4987 
4988  if ( $this->isSpecialPage() ) {
4989  // invalid redirect targets are stored in a global array, but explicitly disallow Userlogout here
4990  if ( $this->isSpecial( 'Userlogout' ) ) {
4991  return false;
4992  }
4993 
4994  foreach ( $wgInvalidRedirectTargets as $target ) {
4995  if ( $this->isSpecial( $target ) ) {
4996  return false;
4997  }
4998  }
4999  }
5000 
5001  return true;
5002  }
5003 
5009  public function getBacklinkCache() {
5010  return BacklinkCache::get( $this );
5011  }
5012 
5018  public function canUseNoindex() {
5020 
5021  $bannedNamespaces = is_null( $wgExemptFromUserRobotsControl )
5024 
5025  return !in_array( $this->mNamespace, $bannedNamespaces );
5026  }
5027 
5038  public function getCategorySortkey( $prefix = '' ) {
5039  $unprefixed = $this->getText();
5040 
5041  // Anything that uses this hook should only depend
5042  // on the Title object passed in, and should probably
5043  // tell the users to run updateCollations.php --force
5044  // in order to re-sort existing category relations.
5045  Hooks::run( 'GetDefaultSortkey', [ $this, &$unprefixed ] );
5046  if ( $prefix !== '' ) {
5047  # Separate with a line feed, so the unprefixed part is only used as
5048  # a tiebreaker when two pages have the exact same prefix.
5049  # In UCA, tab is the only character that can sort above LF
5050  # so we strip both of them from the original prefix.
5051  $prefix = strtr( $prefix, "\n\t", ' ' );
5052  return "$prefix\n$unprefixed";
5053  }
5054  return $unprefixed;
5055  }
5056 
5064  private function getDbPageLanguageCode() {
5065  global $wgPageLanguageUseDB;
5066 
5067  // check, if the page language could be saved in the database, and if so and
5068  // the value is not requested already, lookup the page language using LinkCache
5069  if ( $wgPageLanguageUseDB && $this->mDbPageLanguage === false ) {
5070  $linkCache = MediaWikiServices::getInstance()->getLinkCache();
5071  $linkCache->addLinkObj( $this );
5072  $this->mDbPageLanguage = $linkCache->getGoodLinkFieldObj( $this, 'lang' );
5073  }
5074 
5075  return $this->mDbPageLanguage;
5076  }
5077 
5086  public function getPageLanguage() {
5087  global $wgLang, $wgLanguageCode;
5088  if ( $this->isSpecialPage() ) {
5089  // special pages are in the user language
5090  return $wgLang;
5091  }
5092 
5093  // Checking if DB language is set
5094  $dbPageLanguage = $this->getDbPageLanguageCode();
5095  if ( $dbPageLanguage ) {
5096  return wfGetLangObj( $dbPageLanguage );
5097  }
5098 
5099  if ( !$this->mPageLanguage || $this->mPageLanguage[1] !== $wgLanguageCode ) {
5100  // Note that this may depend on user settings, so the cache should
5101  // be only per-request.
5102  // NOTE: ContentHandler::getPageLanguage() may need to load the
5103  // content to determine the page language!
5104  // Checking $wgLanguageCode hasn't changed for the benefit of unit
5105  // tests.
5106  $contentHandler = ContentHandler::getForTitle( $this );
5107  $langObj = $contentHandler->getPageLanguage( $this );
5108  $this->mPageLanguage = [ $langObj->getCode(), $wgLanguageCode ];
5109  } else {
5110  $langObj = Language::factory( $this->mPageLanguage[0] );
5111  }
5112 
5113  return $langObj;
5114  }
5115 
5124  public function getPageViewLanguage() {
5125  global $wgLang;
5126 
5127  if ( $this->isSpecialPage() ) {
5128  // If the user chooses a variant, the content is actually
5129  // in a language whose code is the variant code.
5130  $variant = $wgLang->getPreferredVariant();
5131  if ( $wgLang->getCode() !== $variant ) {
5132  return Language::factory( $variant );
5133  }
5134 
5135  return $wgLang;
5136  }
5137 
5138  // Checking if DB language is set
5139  $dbPageLanguage = $this->getDbPageLanguageCode();
5140  if ( $dbPageLanguage ) {
5141  $pageLang = wfGetLangObj( $dbPageLanguage );
5142  $variant = $pageLang->getPreferredVariant();
5143  if ( $pageLang->getCode() !== $variant ) {
5144  $pageLang = Language::factory( $variant );
5145  }
5146 
5147  return $pageLang;
5148  }
5149 
5150  // @note Can't be cached persistently, depends on user settings.
5151  // @note ContentHandler::getPageViewLanguage() may need to load the
5152  // content to determine the page language!
5153  $contentHandler = ContentHandler::getForTitle( $this );
5154  $pageLang = $contentHandler->getPageViewLanguage( $this );
5155  return $pageLang;
5156  }
5157 
5168  public function getEditNotices( $oldid = 0 ) {
5169  $notices = [];
5170 
5171  // Optional notice for the entire namespace
5172  $editnotice_ns = 'editnotice-' . $this->mNamespace;
5173  $msg = wfMessage( $editnotice_ns );
5174  if ( $msg->exists() ) {
5175  $html = $msg->parseAsBlock();
5176  // Edit notices may have complex logic, but output nothing (T91715)
5177  if ( trim( $html ) !== '' ) {
5178  $notices[$editnotice_ns] = Html::rawElement(
5179  'div',
5180  [ 'class' => [
5181  'mw-editnotice',
5182  'mw-editnotice-namespace',
5183  Sanitizer::escapeClass( "mw-$editnotice_ns" )
5184  ] ],
5185  $html
5186  );
5187  }
5188  }
5189 
5190  if ( MWNamespace::hasSubpages( $this->mNamespace ) ) {
5191  // Optional notice for page itself and any parent page
5192  $editnotice_base = $editnotice_ns;
5193  foreach ( explode( '/', $this->mDbkeyform ) as $part ) {
5194  $editnotice_base .= '-' . $part;
5195  $msg = wfMessage( $editnotice_base );
5196  if ( $msg->exists() ) {
5197  $html = $msg->parseAsBlock();
5198  if ( trim( $html ) !== '' ) {
5199  $notices[$editnotice_base] = Html::rawElement(
5200  'div',
5201  [ 'class' => [
5202  'mw-editnotice',
5203  'mw-editnotice-base',
5204  Sanitizer::escapeClass( "mw-$editnotice_base" )
5205  ] ],
5206  $html
5207  );
5208  }
5209  }
5210  }
5211  } else {
5212  // Even if there are no subpages in namespace, we still don't want "/" in MediaWiki message keys
5213  $editnoticeText = $editnotice_ns . '-' . strtr( $this->mDbkeyform, '/', '-' );
5214  $msg = wfMessage( $editnoticeText );
5215  if ( $msg->exists() ) {
5216  $html = $msg->parseAsBlock();
5217  if ( trim( $html ) !== '' ) {
5218  $notices[$editnoticeText] = Html::rawElement(
5219  'div',
5220  [ 'class' => [
5221  'mw-editnotice',
5222  'mw-editnotice-page',
5223  Sanitizer::escapeClass( "mw-$editnoticeText" )
5224  ] ],
5225  $html
5226  );
5227  }
5228  }
5229  }
5230 
5231  Hooks::run( 'TitleGetEditNotices', [ $this, $oldid, &$notices ] );
5232  return $notices;
5233  }
5234 
5238  public function __sleep() {
5239  return [
5240  'mNamespace',
5241  'mDbkeyform',
5242  'mFragment',
5243  'mInterwiki',
5244  'mLocalInterwiki',
5245  'mUserCaseDBKey',
5246  'mDefaultNamespace',
5247  ];
5248  }
5249 
5250  public function __wakeup() {
5251  $this->mArticleID = ( $this->mNamespace >= 0 ) ? -1 : 0;
5252  $this->mUrlform = wfUrlencode( $this->mDbkeyform );
5253  $this->mTextform = strtr( $this->mDbkeyform, '_', ' ' );
5254  }
5255 
5256 }
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:2837
isAlwaysKnown()
Should links to this title be shown as potentially viewable (i.e.
Definition: Title.php:4667
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))
static factory(Title $title)
Create a WikiPage object of the appropriate class for the given title.
Definition: WikiPage.php:128
areCascadeProtectionSourcesLoaded( $getPages=true)
Determines whether cascading protection sources have already been loaded from the database...
Definition: Title.php:3062
static purgeExpiredRestrictions()
Purge expired restrictions from the page_restrictions table.
Definition: Title.php:3393
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:2600
touchLinks()
Update page_touched timestamps and send CDN purge messages for pages linking to this title...
Definition: Title.php:4854
getFragment()
Get the Title fragment (i.e.
Definition: Title.php:1513
static getMainWANInstance()
Get the main WAN cache object.
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:2625
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:1995
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:4811
checkUserConfigPermissions( $action, $user, $errors, $rigor, $short)
Check CSS/JSON/JS sub-page permissions.
Definition: Title.php:2375
getRestrictions( $action)
Accessor/initialisation for mRestrictions.
Definition: Title.php:3178
isMovable()
Would anybody with sufficient privileges be able to move this page? Some pages just aren&#39;t movable...
Definition: Title.php:1204
getRootTitle()
Get the root page name title, i.e.
Definition: Title.php:1688
getArticleID( $flags=0)
Get the article ID for this Title from the link cache, adding it if necessary.
Definition: Title.php:3540
getAuthorsBetween( $old, $new, $limit, $options=[])
Get the authors between the given revisions or revision IDs.
Definition: Title.php:4529
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:1598
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:1065
canUseNoindex()
Whether the magic words INDEX and NOINDEX function for this page.
Definition: Title.php:5018
const CONTENT_MODEL_WIKITEXT
Definition: Defines.php:235
wasLocalInterwiki()
Was this a local interwiki link?
Definition: Title.php:853
$wgScript
The URL path to index.php.
checkPermissionHooks( $action, $user, $errors, $rigor, $short)
Check various permission hooks.
Definition: Title.php:2264
isContentPage()
Is this Title in a namespace which contains content? In other words, is this a content page...
Definition: Title.php:1194
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:944
isSemiProtected( $action='edit')
Is this page "semi-protected" - the only protection levels are listed in $wgSemiprotectedRestrictionL...
Definition: Title.php:2970
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:32
isNamespaceProtected(User $user)
Determines if $user is unable to edit this page because it has been protected by $wgNamespaceProtecti...
Definition: Title.php:3030
static clearCaches()
Definition: Title.php:3683
countRevisionsBetween( $old, $new, $max=null)
Get the number of revisions between the given revision.
Definition: Title.php:4486
hasSubpages()
Does this have subpages? (Warning, usually requires an extra DB query.)
Definition: Title.php:3434
const NS_MAIN
Definition: Defines.php:64
$success
static invalidateModuleCache(Title $title, Revision $old=null, Revision $new=null, $domain)
Clear the preloadTitleInfo() cache for all wiki modules on this wiki on page change if it was a JS or...
getSkinFromConfigSubpage()
Trim down a .css, .json, or .js subpage title to get the corresponding skin name. ...
Definition: Title.php:1303
getText()
Get the text form (spaces not underscores) of the main part.
Definition: Title.php:916
getSubpageText()
Get the lowest-level subpage name, i.e.
Definition: Title.php:1743
getBaseText()
Get the base page name without a namespace, i.e.
Definition: Title.php:1703
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:4267
getDefaultMessageText()
Get the default (plain) message contents for an page that overrides an interface message key...
Definition: Title.php:4788
loadRestrictionsFromRows( $rows, $oldFashionedRestrictions=null)
Compiles list of active page restrictions from both page table (pre 1.10) and page_restrictions table...
Definition: Title.php:3237
checkSiteConfigPermissions( $action, $user, $errors, $rigor, $short)
Check sitewide CSS/JSON/JS permissions.
Definition: Title.php:2330
checkPageRestrictions( $action, $user, $errors, $rigor, $short)
Check against page_restrictions table requirements on this page.
Definition: Title.php:2442
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:1995
static isEveryoneAllowed( $right)
Check if all users may be assumed to have the given permission.
Definition: User.php:5084
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:1803
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:1559
checkSpecialsAndNSPermissions( $action, $user, $errors, $rigor, $short)
Check permissions on special pages & namespaces.
Definition: Title.php:2301
getSubjectNsText()
Get the namespace text of the subject (rather than talk) page.
Definition: Title.php:1042
isUserJsConfigPage()
Is this a JS "config" sub-page of a user page?
Definition: Title.php:1347
__wakeup()
Definition: Title.php:5250
getTalkPageIfDefined()
Get a Title object associated with the talk page of this article, if such a talk page can exist...
Definition: Title.php:1452
getTransWikiID()
Returns the DB name of the distant wiki which owns the object.
Definition: Title.php:876
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:2233
getFullUrlForRedirect( $query='', $proto=PROTO_CURRENT)
Get a url appropriate for making redirects based on an untrusted url arg.
Definition: Title.php:1876
$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:1083
if(!isset( $args[0])) $lang
static escapeRegexReplacement( $string)
Escape a string to make it suitable for inclusion in a preg_replace() replacement parameter...
static factory( $action, Page $page, IContextSource $context=null)
Get an appropriate Action subclass for the given action.
Definition: Action.php:97
isUserCssConfigPage()
Is this a CSS "config" sub-page of a user page?
Definition: Title.php:1319
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:1276
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:3718
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:1155
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:1910
isTalkPage()
Is this a talk page of some sort?
Definition: Title.php:1430
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:5064
null for the local wiki Added in
Definition: hooks.txt:1598
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:4434
userCan( $action, $user=null, $rigor='secure')
Can $user perform $action on this page?
Definition: Title.php:2108
getUserPermissionsErrors( $action, $user, $rigor='secure', $ignoreErrors=[])
Can $user perform $action on this page?
Definition: Title.php:2132
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:4883
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:1483
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:1144
getPrefixedText()
Get the prefixed title with spaces.
Definition: Title.php:1624
moveTo(&$nt, $auth=true, $reason='', $createRedirect=true, array $changeTags=[])
Move a title to a new location.
Definition: Title.php:4022
getBaseTitle()
Get the base page name title, i.e.
Definition: Title.php:1728
getParentCategories()
Get categories to which this Title belongs and return an array of categories&#39; names.
Definition: Title.php:4232
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:3759
getBacklinkCache()
Get a backlink cache object.
Definition: Title.php:5009
$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:5038
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:137
const CONTENT_MODEL_JSON
Definition: Defines.php:239
wfLocalFile( $title)
Get an object referring to a locally registered file.
static exists( $name)
Check if a given action is recognised, even if it&#39;s disabled.
Definition: Action.php:171
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:1812
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:3817
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:989
getTalkPage()
Get a Title object associated with the talk page of this article.
Definition: Title.php:1439
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:2750
isSiteConfigPage()
Could this MediaWiki namespace page contain custom CSS, JSON, or JavaScript for the global UI...
Definition: Title.php:1275
fixSpecialName()
If the Title refers to a special page alias which is not the local default, resolve the alias...
Definition: Title.php:1120
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:1993
checkReadPermissions( $action, $user, $errors, $rigor, $short)
Check that the user is allowed to read this page.
Definition: Title.php:2673
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:2476
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:1764
$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:2009
quickUserCan( $action, $user=null)
Can $user perform $action on this page? This skips potentially expensive cascading permission checks ...
Definition: Title.php:2095
isSiteCssConfigPage()
Is this a sitewide CSS "config" page?
Definition: Title.php:1361
flushRestrictions()
Flush the protection cache in this object and force reload from the database.
Definition: Title.php:3383
string $mDbkeyform
Main part with underscores.
Definition: Title.php:70
getNsText()
Get the namespace text.
Definition: Title.php:1018
getCanonicalURL( $query='', $query2=false)
Get the URL for a canonical link, for use in things like IRC and e-mail notifications.
Definition: Title.php:2058
__sleep()
Definition: Title.php:5238
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:833
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:891
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:1289
deleteTitleProtection()
Remove any title protection due to page existing.
Definition: Title.php:2952
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:4730
isUserJsonConfigPage()
Is this a JSON "config" sub-page of a user page?
Definition: Title.php:1333
resetArticleID( $newid)
This clears some fields in this object, and clears any associated keys in the "bad links" section of ...
Definition: Title.php:3661
$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:934
__toString()
Return a string representation of this title.
Definition: Title.php:1638
getPrefixedURL()
Get a URL-encoded title (not an actual URL) including interwiki.
Definition: Title.php:1784
isSpecial( $name)
Returns true if this title resolves to the named special page.
Definition: Title.php:1102
hasFragment()
Check if a Title fragment is set.
Definition: Title.php:1523
getRedirectsHere( $ns=null)
Get all extant redirects to this Title.
Definition: Title.php:4949
$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:3801
areRestrictionsCascading()
Returns cascading restrictions for the current article.
Definition: Title.php:3218
wfFindFile( $title, $options=[])
Find a file.
isConversionTable()
Is this a conversion table for the LanguageConverter?
Definition: Title.php:1245
const NS_MEDIA
Definition: Defines.php:52
getRootText()
Get the root page name text without a namespace, i.e.
Definition: Title.php:1668
$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:1074
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:3462
$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:993
isValidRedirectTarget()
Check if this Title is a valid redirect target.
Definition: Title.php:4985
isCascadeProtected()
Cascading protection: Return true if cascading restrictions apply to this page, false if not...
Definition: Title.php:3048
isValidMoveTarget( $nt)
Checks if $this can be moved to a given Title.
Definition: Title.php:4184
isValidMoveOperation(&$nt, $auth=true, $reason='')
Check whether a given move operation would be valid.
Definition: Title.php:3966
getCdnUrls()
Get a list of URLs to purge from the CDN cache when this page changes.
Definition: Title.php:3919
$cache
Definition: mcc.php:33
getDefaultNamespace()
Get the default namespace index, for when there is no namespace.
Definition: Title.php:1502
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:1995
isAllowed( $action='')
Internal mechanics of testing a permission.
Definition: User.php:3895
isSubpageOf(Title $title)
Check if this title is a subpage of another title.
Definition: Title.php:4628
array $mNotificationTimestamp
Associative array of user ID -> timestamp/false.
Definition: Title.php:164
loadRestrictions( $oldFashionedRestrictions=null, $flags=0)
Load restrictions from the page_restrictions table.
Definition: Title.php:3314
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:1648
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
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not null
Definition: hooks.txt:780
static newFromResult( $res)
Definition: TitleArray.php:40
isMainPage()
Is this the mainpage?
Definition: Title.php:1225
prefix( $name)
Prefix some arbitrary text with the namespace or interwiki prefix of this object. ...
Definition: Title.php:1586
namespace and then decline to actually register it file or subcat img or subcat $title
Definition: hooks.txt:935
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:511
getNamespaceKey( $prepend='nstab-')
Generate strings used for xml &#39;id&#39; names in monobook tabs.
Definition: Title.php:4921
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:1841
getNamespace()
Get the namespace index, i.e.
Definition: Title.php:958
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:4615
static hasFlags( $bitfield, $flags)
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:4721
isProtected( $action='')
Does the title correspond to a protected article?
Definition: Title.php:2998
const NS_FILE
Definition: Defines.php:70
areRestrictionsLoaded()
Accessor for mRestrictionsLoaded.
Definition: Title.php:3165
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:1779
isSiteJsonConfigPage()
Is this a sitewide JSON "config" page?
Definition: Title.php:1379
isSubpage()
Is this a subpage?
Definition: Title.php:1234
getInterwiki()
Get the interwiki prefix.
Definition: Title.php:844
const RAW
Definition: Revision.php:56
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:935
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:3883
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:123
static groupHasPermission( $group, $role)
Check, if the given group has the given permission.
Definition: User.php:5064
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:3871
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:797
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:1466
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:1183
isSpecialPage()
Returns true if this is a special page.
Definition: Title.php:1092
getEditURL()
Get the edit URL for this Title.
Definition: Title.php:2072
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:3993
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:4133
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()
this hook is for auditing only or null if authentication failed before getting that far or null if we can t even determine that When $user is not it can be in the form of< username >< more info > e g for bot passwords intended to be added to log contexts Fields it might only if the login was with a bot password 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:780
getFirstRevision( $flags=0)
Get the first revision of the page.
Definition: Title.php:4387
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:5124
getSubpageUrlForm()
Get a URL-encoded form of the subpage text.
Definition: Title.php:1773
$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:1570
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:4604
getTitleProtectionInternal()
Fetch title protection settings.
Definition: Title.php:2905
getRestrictionTypes()
Returns restriction types for the current Title.
Definition: Title.php:2855
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
Relational database abstraction object.
Definition: Database.php:48
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:863
getContentModel( $flags=0)
Get the page&#39;s content model id, see the CONTENT_MODEL_XXX constants.
Definition: Title.php:970
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:3698
string [] $wgRawHtmlMessages
List of messages which might contain raw HTML.
static newFatalPermissionDeniedStatus( $permission)
Factory function for fatal permission-denied errors.
Definition: User.php:5687
getPreviousRevisionID( $revId, $flags=0)
Get the revision ID of the previous revision.
Definition: Title.php:4366
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:5086
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:1415
getFragmentForURL()
Get the fragment in URL form, including the "#" character if there is one.
Definition: Title.php:1532
$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:4645
getLatestRevID( $flags=0)
What is the page_latest field for this page?
Definition: Title.php:3629
pageCond()
Get an associative array for selecting this title from the "page" table.
Definition: Title.php:4294
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:4414
isDeletedQuick()
Is there a version of this page in the deletion archive?
Definition: Title.php:3514
isNewPage()
Check if this is a new page.
Definition: Title.php:4424
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:3206
static compare(LinkTarget $a, LinkTarget $b)
Callback for usort() to do title sorts by (namespace, title)
Definition: Title.php:778
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:3079
isRedirect( $flags=0)
Is this an article that is a redirect page? Uses link cache, adding it if necessary.
Definition: Title.php:3566
Allows to change the fields on the form that will be generated $name
Definition: hooks.txt:271
isSiteJsConfigPage()
Is this a sitewide JS "config" page?
Definition: Title.php:1397
if(! $wgDBerrorLogTZ) $wgRequest
Definition: Setup.php:728
$wgServer
URL of the server.
isDeleted()
Is there a version of this page in the deletion archive?
Definition: Title.php:3489
isLocal()
Determine whether the object refers to a page within this project (either this wiki or a wiki with a ...
Definition: Title.php:818
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:2163
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:2882
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:5168
update( $table, $values, $conds, $fname=__METHOD__, $options=[])
UPDATE wrapper.
getTouched( $db=null)
Get the last touched timestamp.
Definition: Title.php:4869
getTalkNsText()
Get the namespace text of the talk page.
Definition: Title.php:1052
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:3192
getInternalURL( $query='', $query2=false)
Get the URL form for an internal link.
Definition: Title.php:2034
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:1008
$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:4310
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:2773
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:3601
isWikitextPage()
Does that page contain wikitext, or it is JS, CSS or whatever?
Definition: Title.php:1257
static singleton()
Get the signleton instance of this class.
purgeSquid()
Purge all applicable CDN URLs.
Definition: Title.php:3949
return true to allow those checks to and false if checking is done & $user
Definition: hooks.txt:1486
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:893
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:925
getPrefixedDBkey()
Get the prefixed database key form.
Definition: Title.php:1612
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:4060
estimateRevisionCount()
Get the approximate revision count of this page.
Definition: Title.php:4463
checkActionPermissions( $action, $user, $errors, $rigor, $short)
Check action permissions not already checked in checkQuickPermissions.
Definition: Title.php:2523
$matches
getNextRevisionID( $revId, $flags=0)
Get the revision ID of the next revision.
Definition: Title.php:4377
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