[ Index ]

PHP Cross Reference of MediaWiki-1.24.0

title

Body

[close]

/includes/ -> Title.php (source)

   1  <?php
   2  /**
   3   * Representation of a title within %MediaWiki.
   4   *
   5   * See title.txt
   6   *
   7   * This program is free software; you can redistribute it and/or modify
   8   * it under the terms of the GNU General Public License as published by
   9   * the Free Software Foundation; either version 2 of the License, or
  10   * (at your option) any later version.
  11   *
  12   * This program is distributed in the hope that it will be useful,
  13   * but WITHOUT ANY WARRANTY; without even the implied warranty of
  14   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  15   * GNU General Public License for more details.
  16   *
  17   * You should have received a copy of the GNU General Public License along
  18   * with this program; if not, write to the Free Software Foundation, Inc.,
  19   * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  20   * http://www.gnu.org/copyleft/gpl.html
  21   *
  22   * @file
  23   */
  24  
  25  /**
  26   * Represents a title within MediaWiki.
  27   * Optionally may contain an interwiki designation or namespace.
  28   * @note This class can fetch various kinds of data from the database;
  29   *       however, it does so inefficiently.
  30   * @note Consider using a TitleValue object instead. TitleValue is more lightweight
  31   *       and does not rely on global state or the database.
  32   *
  33   * @internal documentation reviewed 15 Mar 2010
  34   */
  35  class Title {
  36      /** @var MapCacheLRU */
  37      static private $titleCache = null;
  38  
  39      /**
  40       * Title::newFromText maintains a cache to avoid expensive re-normalization of
  41       * commonly used titles. On a batch operation this can become a memory leak
  42       * if not bounded. After hitting this many titles reset the cache.
  43       */
  44      const CACHE_MAX = 1000;
  45  
  46      /**
  47       * Used to be GAID_FOR_UPDATE define. Used with getArticleID() and friends
  48       * to use the master DB
  49       */
  50      const GAID_FOR_UPDATE = 1;
  51  
  52      /**
  53       * @name Private member variables
  54       * Please use the accessor functions instead.
  55       * @private
  56       */
  57      // @{
  58  
  59      /** @var string Text form (spaces not underscores) of the main part */
  60      public $mTextform = '';
  61  
  62      /** @var string URL-encoded form of the main part */
  63      public $mUrlform = '';
  64  
  65      /** @var string Main part with underscores */
  66      public $mDbkeyform = '';
  67  
  68      /** @var string Database key with the initial letter in the case specified by the user */
  69      protected $mUserCaseDBKey;
  70  
  71      /** @var int Namespace index, i.e. one of the NS_xxxx constants */
  72      public $mNamespace = NS_MAIN;
  73  
  74      /** @var string Interwiki prefix */
  75      public $mInterwiki = '';
  76  
  77      /** @var bool Was this Title created from a string with a local interwiki prefix? */
  78      private $mLocalInterwiki = false;
  79  
  80      /** @var string Title fragment (i.e. the bit after the #) */
  81      public $mFragment = '';
  82  
  83      /** @var int Article ID, fetched from the link cache on demand */
  84      public $mArticleID = -1;
  85  
  86      /** @var bool|int ID of most recent revision */
  87      protected $mLatestID = false;
  88  
  89      /**
  90       * @var bool|string ID of the page's content model, i.e. one of the
  91       *   CONTENT_MODEL_XXX constants
  92       */
  93      public $mContentModel = false;
  94  
  95      /** @var int Estimated number of revisions; null of not loaded */
  96      private $mEstimateRevisions;
  97  
  98      /** @var array Array of groups allowed to edit this article */
  99      public $mRestrictions = array();
 100  
 101      /** @var bool */
 102      protected $mOldRestrictions = false;
 103  
 104      /** @var bool Cascade restrictions on this page to included templates and images? */
 105      public $mCascadeRestriction;
 106  
 107      /** Caching the results of getCascadeProtectionSources */
 108      public $mCascadingRestrictions;
 109  
 110      /** @var array When do the restrictions on this page expire? */
 111      protected $mRestrictionsExpiry = array();
 112  
 113      /** @var bool Are cascading restrictions in effect on this page? */
 114      protected $mHasCascadingRestrictions;
 115  
 116      /** @var array Where are the cascading restrictions coming from on this page? */
 117      public $mCascadeSources;
 118  
 119      /** @var bool Boolean for initialisation on demand */
 120      public $mRestrictionsLoaded = false;
 121  
 122      /** @var string Text form including namespace/interwiki, initialised on demand */
 123      protected $mPrefixedText = null;
 124  
 125      /** @var mixed Cached value for getTitleProtection (create protection) */
 126      public $mTitleProtection;
 127  
 128      /**
 129       * @var int Namespace index when there is no namespace. Don't change the
 130       *   following default, NS_MAIN is hardcoded in several places. See bug 696.
 131       *   Zero except in {{transclusion}} tags.
 132       */
 133      public $mDefaultNamespace = NS_MAIN;
 134  
 135      /**
 136       * @var bool Is $wgUser watching this page? null if unfilled, accessed
 137       * through userIsWatching()
 138       */
 139      protected $mWatched = null;
 140  
 141      /** @var int The page length, 0 for special pages */
 142      protected $mLength = -1;
 143  
 144      /** @var null Is the article at this title a redirect? */
 145      public $mRedirect = null;
 146  
 147      /** @var array Associative array of user ID -> timestamp/false */
 148      private $mNotificationTimestamp = array();
 149  
 150      /** @var bool Whether a page has any subpages */
 151      private $mHasSubpages;
 152  
 153      /** @var bool The (string) language code of the page's language and content code. */
 154      private $mPageLanguage = false;
 155  
 156      /** @var string The page language code from the database */
 157      private $mDbPageLanguage = null;
 158  
 159      /** @var TitleValue A corresponding TitleValue object */
 160      private $mTitleValue = null;
 161  
 162      /** @var bool Would deleting this page be a big deletion? */
 163      private $mIsBigDeletion = null;
 164      // @}
 165  
 166      /**
 167       * B/C kludge: provide a TitleParser for use by Title.
 168       * Ideally, Title would have no methods that need this.
 169       * Avoid usage of this singleton by using TitleValue
 170       * and the associated services when possible.
 171       *
 172       * @return TitleParser
 173       */
 174  	private static function getTitleParser() {
 175          global $wgContLang, $wgLocalInterwikis;
 176  
 177          static $titleCodec = null;
 178          static $titleCodecFingerprint = null;
 179  
 180          // $wgContLang and $wgLocalInterwikis may change (especially while testing),
 181          // make sure we are using the right one. To detect changes over the course
 182          // of a request, we remember a fingerprint of the config used to create the
 183          // codec singleton, and re-create it if the fingerprint doesn't match.
 184          $fingerprint = spl_object_hash( $wgContLang ) . '|' . join( '+', $wgLocalInterwikis );
 185  
 186          if ( $fingerprint !== $titleCodecFingerprint ) {
 187              $titleCodec = null;
 188          }
 189  
 190          if ( !$titleCodec ) {
 191              $titleCodec = new MediaWikiTitleCodec(
 192                  $wgContLang,
 193                  GenderCache::singleton(),
 194                  $wgLocalInterwikis
 195              );
 196              $titleCodecFingerprint = $fingerprint;
 197          }
 198  
 199          return $titleCodec;
 200      }
 201  
 202      /**
 203       * B/C kludge: provide a TitleParser for use by Title.
 204       * Ideally, Title would have no methods that need this.
 205       * Avoid usage of this singleton by using TitleValue
 206       * and the associated services when possible.
 207       *
 208       * @return TitleFormatter
 209       */
 210  	private static function getTitleFormatter() {
 211          //NOTE: we know that getTitleParser() returns a MediaWikiTitleCodec,
 212          //      which implements TitleFormatter.
 213          return self::getTitleParser();
 214      }
 215  
 216  	function __construct() {
 217      }
 218  
 219      /**
 220       * Create a new Title from a prefixed DB key
 221       *
 222       * @param string $key The database key, which has underscores
 223       *    instead of spaces, possibly including namespace and
 224       *    interwiki prefixes
 225       * @return Title|null Title, or null on an error
 226       */
 227  	public static function newFromDBkey( $key ) {
 228          $t = new Title();
 229          $t->mDbkeyform = $key;
 230          if ( $t->secureAndSplit() ) {
 231              return $t;
 232          } else {
 233              return null;
 234          }
 235      }
 236  
 237      /**
 238       * Create a new Title from a TitleValue
 239       *
 240       * @param TitleValue $titleValue Assumed to be safe.
 241       *
 242       * @return Title
 243       */
 244  	public static function newFromTitleValue( TitleValue $titleValue ) {
 245          return self::makeTitle(
 246              $titleValue->getNamespace(),
 247              $titleValue->getText(),
 248              $titleValue->getFragment() );
 249      }
 250  
 251      /**
 252       * Create a new Title from text, such as what one would find in a link. De-
 253       * codes any HTML entities in the text.
 254       *
 255       * @param string $text The link text; spaces, prefixes, and an
 256       *   initial ':' indicating the main namespace are accepted.
 257       * @param int $defaultNamespace The namespace to use if none is specified
 258       *   by a prefix.  If you want to force a specific namespace even if
 259       *   $text might begin with a namespace prefix, use makeTitle() or
 260       *   makeTitleSafe().
 261       * @throws MWException
 262       * @return Title|null Title or null on an error.
 263       */
 264  	public static function newFromText( $text, $defaultNamespace = NS_MAIN ) {
 265          if ( is_object( $text ) ) {
 266              throw new MWException( 'Title::newFromText given an object' );
 267          }
 268  
 269          $cache = self::getTitleCache();
 270  
 271          /**
 272           * Wiki pages often contain multiple links to the same page.
 273           * Title normalization and parsing can become expensive on
 274           * pages with many links, so we can save a little time by
 275           * caching them.
 276           *
 277           * In theory these are value objects and won't get changed...
 278           */
 279          if ( $defaultNamespace == NS_MAIN && $cache->has( $text ) ) {
 280              return $cache->get( $text );
 281          }
 282  
 283          # Convert things like &eacute; &#257; or &#x3017; into normalized (bug 14952) text
 284          $filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text );
 285  
 286          $t = new Title();
 287          $t->mDbkeyform = str_replace( ' ', '_', $filteredText );
 288          $t->mDefaultNamespace = intval( $defaultNamespace );
 289  
 290          if ( $t->secureAndSplit() ) {
 291              if ( $defaultNamespace == NS_MAIN ) {
 292                  $cache->set( $text, $t );
 293              }
 294              return $t;
 295          } else {
 296              return null;
 297          }
 298      }
 299  
 300      /**
 301       * THIS IS NOT THE FUNCTION YOU WANT. Use Title::newFromText().
 302       *
 303       * Example of wrong and broken code:
 304       * $title = Title::newFromURL( $wgRequest->getVal( 'title' ) );
 305       *
 306       * Example of right code:
 307       * $title = Title::newFromText( $wgRequest->getVal( 'title' ) );
 308       *
 309       * Create a new Title from URL-encoded text. Ensures that
 310       * the given title's length does not exceed the maximum.
 311       *
 312       * @param string $url The title, as might be taken from a URL
 313       * @return Title|null The new object, or null on an error
 314       */
 315  	public static function newFromURL( $url ) {
 316          $t = new Title();
 317  
 318          # For compatibility with old buggy URLs. "+" is usually not valid in titles,
 319          # but some URLs used it as a space replacement and they still come
 320          # from some external search tools.
 321          if ( strpos( self::legalChars(), '+' ) === false ) {
 322              $url = str_replace( '+', ' ', $url );
 323          }
 324  
 325          $t->mDbkeyform = str_replace( ' ', '_', $url );
 326          if ( $t->secureAndSplit() ) {
 327              return $t;
 328          } else {
 329              return null;
 330          }
 331      }
 332  
 333      /**
 334       * @return MapCacheLRU
 335       */
 336  	private static function getTitleCache() {
 337          if ( self::$titleCache == null ) {
 338              self::$titleCache = new MapCacheLRU( self::CACHE_MAX );
 339          }
 340          return self::$titleCache;
 341      }
 342  
 343      /**
 344       * Returns a list of fields that are to be selected for initializing Title
 345       * objects or LinkCache entries. Uses $wgContentHandlerUseDB to determine
 346       * whether to include page_content_model.
 347       *
 348       * @return array
 349       */
 350  	protected static function getSelectFields() {
 351          global $wgContentHandlerUseDB;
 352  
 353          $fields = array(
 354              'page_namespace', 'page_title', 'page_id',
 355              'page_len', 'page_is_redirect', 'page_latest',
 356          );
 357  
 358          if ( $wgContentHandlerUseDB ) {
 359              $fields[] = 'page_content_model';
 360          }
 361  
 362          return $fields;
 363      }
 364  
 365      /**
 366       * Create a new Title from an article ID
 367       *
 368       * @param int $id The page_id corresponding to the Title to create
 369       * @param int $flags Use Title::GAID_FOR_UPDATE to use master
 370       * @return Title|null The new object, or null on an error
 371       */
 372  	public static function newFromID( $id, $flags = 0 ) {
 373          $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
 374          $row = $db->selectRow(
 375              'page',
 376              self::getSelectFields(),
 377              array( 'page_id' => $id ),
 378              __METHOD__
 379          );
 380          if ( $row !== false ) {
 381              $title = Title::newFromRow( $row );
 382          } else {
 383              $title = null;
 384          }
 385          return $title;
 386      }
 387  
 388      /**
 389       * Make an array of titles from an array of IDs
 390       *
 391       * @param int[] $ids Array of IDs
 392       * @return Title[] Array of Titles
 393       */
 394  	public static function newFromIDs( $ids ) {
 395          if ( !count( $ids ) ) {
 396              return array();
 397          }
 398          $dbr = wfGetDB( DB_SLAVE );
 399  
 400          $res = $dbr->select(
 401              'page',
 402              self::getSelectFields(),
 403              array( 'page_id' => $ids ),
 404              __METHOD__
 405          );
 406  
 407          $titles = array();
 408          foreach ( $res as $row ) {
 409              $titles[] = Title::newFromRow( $row );
 410          }
 411          return $titles;
 412      }
 413  
 414      /**
 415       * Make a Title object from a DB row
 416       *
 417       * @param stdClass $row Object database row (needs at least page_title,page_namespace)
 418       * @return Title Corresponding Title
 419       */
 420  	public static function newFromRow( $row ) {
 421          $t = self::makeTitle( $row->page_namespace, $row->page_title );
 422          $t->loadFromRow( $row );
 423          return $t;
 424      }
 425  
 426      /**
 427       * Load Title object fields from a DB row.
 428       * If false is given, the title will be treated as non-existing.
 429       *
 430       * @param stdClass|bool $row Database row
 431       */
 432  	public function loadFromRow( $row ) {
 433          if ( $row ) { // page found
 434              if ( isset( $row->page_id ) ) {
 435                  $this->mArticleID = (int)$row->page_id;
 436              }
 437              if ( isset( $row->page_len ) ) {
 438                  $this->mLength = (int)$row->page_len;
 439              }
 440              if ( isset( $row->page_is_redirect ) ) {
 441                  $this->mRedirect = (bool)$row->page_is_redirect;
 442              }
 443              if ( isset( $row->page_latest ) ) {
 444                  $this->mLatestID = (int)$row->page_latest;
 445              }
 446              if ( isset( $row->page_content_model ) ) {
 447                  $this->mContentModel = strval( $row->page_content_model );
 448              } else {
 449                  $this->mContentModel = false; # initialized lazily in getContentModel()
 450              }
 451              if ( isset( $row->page_lang ) ) {
 452                  $this->mDbPageLanguage = (string)$row->page_lang;
 453              }
 454          } else { // page not found
 455              $this->mArticleID = 0;
 456              $this->mLength = 0;
 457              $this->mRedirect = false;
 458              $this->mLatestID = 0;
 459              $this->mContentModel = false; # initialized lazily in getContentModel()
 460          }
 461      }
 462  
 463      /**
 464       * Create a new Title from a namespace index and a DB key.
 465       * It's assumed that $ns and $title are *valid*, for instance when
 466       * they came directly from the database or a special page name.
 467       * For convenience, spaces are converted to underscores so that
 468       * eg user_text fields can be used directly.
 469       *
 470       * @param int $ns The namespace of the article
 471       * @param string $title The unprefixed database key form
 472       * @param string $fragment The link fragment (after the "#")
 473       * @param string $interwiki The interwiki prefix
 474       * @return Title The new object
 475       */
 476      public static function &makeTitle( $ns, $title, $fragment = '', $interwiki = '' ) {
 477          $t = new Title();
 478          $t->mInterwiki = $interwiki;
 479          $t->mFragment = $fragment;
 480          $t->mNamespace = $ns = intval( $ns );
 481          $t->mDbkeyform = str_replace( ' ', '_', $title );
 482          $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
 483          $t->mUrlform = wfUrlencode( $t->mDbkeyform );
 484          $t->mTextform = str_replace( '_', ' ', $title );
 485          $t->mContentModel = false; # initialized lazily in getContentModel()
 486          return $t;
 487      }
 488  
 489      /**
 490       * Create a new Title from a namespace index and a DB key.
 491       * The parameters will be checked for validity, which is a bit slower
 492       * than makeTitle() but safer for user-provided data.
 493       *
 494       * @param int $ns The namespace of the article
 495       * @param string $title Database key form
 496       * @param string $fragment The link fragment (after the "#")
 497       * @param string $interwiki Interwiki prefix
 498       * @return Title The new object, or null on an error
 499       */
 500  	public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) {
 501          if ( !MWNamespace::exists( $ns ) ) {
 502              return null;
 503          }
 504  
 505          $t = new Title();
 506          $t->mDbkeyform = Title::makeName( $ns, $title, $fragment, $interwiki );
 507          if ( $t->secureAndSplit() ) {
 508              return $t;
 509          } else {
 510              return null;
 511          }
 512      }
 513  
 514      /**
 515       * Create a new Title for the Main Page
 516       *
 517       * @return Title The new object
 518       */
 519  	public static function newMainPage() {
 520          $title = Title::newFromText( wfMessage( 'mainpage' )->inContentLanguage()->text() );
 521          // Don't give fatal errors if the message is broken
 522          if ( !$title ) {
 523              $title = Title::newFromText( 'Main Page' );
 524          }
 525          return $title;
 526      }
 527  
 528      /**
 529       * Extract a redirect destination from a string and return the
 530       * Title, or null if the text doesn't contain a valid redirect
 531       * This will only return the very next target, useful for
 532       * the redirect table and other checks that don't need full recursion
 533       *
 534       * @param string $text Text with possible redirect
 535       * @return Title The corresponding Title
 536       * @deprecated since 1.21, use Content::getRedirectTarget instead.
 537       */
 538  	public static function newFromRedirect( $text ) {
 539          ContentHandler::deprecated( __METHOD__, '1.21' );
 540  
 541          $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
 542          return $content->getRedirectTarget();
 543      }
 544  
 545      /**
 546       * Extract a redirect destination from a string and return the
 547       * Title, or null if the text doesn't contain a valid redirect
 548       * This will recurse down $wgMaxRedirects times or until a non-redirect target is hit
 549       * in order to provide (hopefully) the Title of the final destination instead of another redirect
 550       *
 551       * @param string $text Text with possible redirect
 552       * @return Title
 553       * @deprecated since 1.21, use Content::getUltimateRedirectTarget instead.
 554       */
 555  	public static function newFromRedirectRecurse( $text ) {
 556          ContentHandler::deprecated( __METHOD__, '1.21' );
 557  
 558          $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
 559          return $content->getUltimateRedirectTarget();
 560      }
 561  
 562      /**
 563       * Extract a redirect destination from a string and return an
 564       * array of Titles, or null if the text doesn't contain a valid redirect
 565       * The last element in the array is the final destination after all redirects
 566       * have been resolved (up to $wgMaxRedirects times)
 567       *
 568       * @param string $text Text with possible redirect
 569       * @return Title[] Array of Titles, with the destination last
 570       * @deprecated since 1.21, use Content::getRedirectChain instead.
 571       */
 572  	public static function newFromRedirectArray( $text ) {
 573          ContentHandler::deprecated( __METHOD__, '1.21' );
 574  
 575          $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
 576          return $content->getRedirectChain();
 577      }
 578  
 579      /**
 580       * Get the prefixed DB key associated with an ID
 581       *
 582       * @param int $id The page_id of the article
 583       * @return Title|null An object representing the article, or null if no such article was found
 584       */
 585  	public static function nameOf( $id ) {
 586          $dbr = wfGetDB( DB_SLAVE );
 587  
 588          $s = $dbr->selectRow(
 589              'page',
 590              array( 'page_namespace', 'page_title' ),
 591              array( 'page_id' => $id ),
 592              __METHOD__
 593          );
 594          if ( $s === false ) {
 595              return null;
 596          }
 597  
 598          $n = self::makeName( $s->page_namespace, $s->page_title );
 599          return $n;
 600      }
 601  
 602      /**
 603       * Get a regex character class describing the legal characters in a link
 604       *
 605       * @return string The list of characters, not delimited
 606       */
 607  	public static function legalChars() {
 608          global $wgLegalTitleChars;
 609          return $wgLegalTitleChars;
 610      }
 611  
 612      /**
 613       * Returns a simple regex that will match on characters and sequences invalid in titles.
 614       * Note that this doesn't pick up many things that could be wrong with titles, but that
 615       * replacing this regex with something valid will make many titles valid.
 616       *
 617       * @todo move this into MediaWikiTitleCodec
 618       *
 619       * @return string Regex string
 620       */
 621  	static function getTitleInvalidRegex() {
 622          static $rxTc = false;
 623          if ( !$rxTc ) {
 624              # Matching titles will be held as illegal.
 625              $rxTc = '/' .
 626                  # Any character not allowed is forbidden...
 627                  '[^' . self::legalChars() . ']' .
 628                  # URL percent encoding sequences interfere with the ability
 629                  # to round-trip titles -- you can't link to them consistently.
 630                  '|%[0-9A-Fa-f]{2}' .
 631                  # XML/HTML character references produce similar issues.
 632                  '|&[A-Za-z0-9\x80-\xff]+;' .
 633                  '|&#[0-9]+;' .
 634                  '|&#x[0-9A-Fa-f]+;' .
 635                  '/S';
 636          }
 637  
 638          return $rxTc;
 639      }
 640  
 641      /**
 642       * Utility method for converting a character sequence from bytes to Unicode.
 643       *
 644       * Primary usecase being converting $wgLegalTitleChars to a sequence usable in
 645       * javascript, as PHP uses UTF-8 bytes where javascript uses Unicode code units.
 646       *
 647       * @param string $byteClass
 648       * @return string
 649       */
 650  	public static function convertByteClassToUnicodeClass( $byteClass ) {
 651          $length = strlen( $byteClass );
 652          // Input token queue
 653          $x0 = $x1 = $x2 = '';
 654          // Decoded queue
 655          $d0 = $d1 = $d2 = '';
 656          // Decoded integer codepoints
 657          $ord0 = $ord1 = $ord2 = 0;
 658          // Re-encoded queue
 659          $r0 = $r1 = $r2 = '';
 660          // Output
 661          $out = '';
 662          // Flags
 663          $allowUnicode = false;
 664          for ( $pos = 0; $pos < $length; $pos++ ) {
 665              // Shift the queues down
 666              $x2 = $x1;
 667              $x1 = $x0;
 668              $d2 = $d1;
 669              $d1 = $d0;
 670              $ord2 = $ord1;
 671              $ord1 = $ord0;
 672              $r2 = $r1;
 673              $r1 = $r0;
 674              // Load the current input token and decoded values
 675              $inChar = $byteClass[$pos];
 676              if ( $inChar == '\\' ) {
 677                  if ( preg_match( '/x([0-9a-fA-F]{2})/A', $byteClass, $m, 0, $pos + 1 ) ) {
 678                      $x0 = $inChar . $m[0];
 679                      $d0 = chr( hexdec( $m[1] ) );
 680                      $pos += strlen( $m[0] );
 681                  } elseif ( preg_match( '/[0-7]{3}/A', $byteClass, $m, 0, $pos + 1 ) ) {
 682                      $x0 = $inChar . $m[0];
 683                      $d0 = chr( octdec( $m[0] ) );
 684                      $pos += strlen( $m[0] );
 685                  } elseif ( $pos + 1 >= $length ) {
 686                      $x0 = $d0 = '\\';
 687                  } else {
 688                      $d0 = $byteClass[$pos + 1];
 689                      $x0 = $inChar . $d0;
 690                      $pos += 1;
 691                  }
 692              } else {
 693                  $x0 = $d0 = $inChar;
 694              }
 695              $ord0 = ord( $d0 );
 696              // Load the current re-encoded value
 697              if ( $ord0 < 32 || $ord0 == 0x7f ) {
 698                  $r0 = sprintf( '\x%02x', $ord0 );
 699              } elseif ( $ord0 >= 0x80 ) {
 700                  // Allow unicode if a single high-bit character appears
 701                  $r0 = sprintf( '\x%02x', $ord0 );
 702                  $allowUnicode = true;
 703              } elseif ( strpos( '-\\[]^', $d0 ) !== false ) {
 704                  $r0 = '\\' . $d0;
 705              } else {
 706                  $r0 = $d0;
 707              }
 708              // Do the output
 709              if ( $x0 !== '' && $x1 === '-' && $x2 !== '' ) {
 710                  // Range
 711                  if ( $ord2 > $ord0 ) {
 712                      // Empty range
 713                  } elseif ( $ord0 >= 0x80 ) {
 714                      // Unicode range
 715                      $allowUnicode = true;
 716                      if ( $ord2 < 0x80 ) {
 717                          // Keep the non-unicode section of the range
 718                          $out .= "$r2-\\x7F";
 719                      }
 720                  } else {
 721                      // Normal range
 722                      $out .= "$r2-$r0";
 723                  }
 724                  // Reset state to the initial value
 725                  $x0 = $x1 = $d0 = $d1 = $r0 = $r1 = '';
 726              } elseif ( $ord2 < 0x80 ) {
 727                  // ASCII character
 728                  $out .= $r2;
 729              }
 730          }
 731          if ( $ord1 < 0x80 ) {
 732              $out .= $r1;
 733          }
 734          if ( $ord0 < 0x80 ) {
 735              $out .= $r0;
 736          }
 737          if ( $allowUnicode ) {
 738              $out .= '\u0080-\uFFFF';
 739          }
 740          return $out;
 741      }
 742  
 743      /**
 744       * Make a prefixed DB key from a DB key and a namespace index
 745       *
 746       * @param int $ns Numerical representation of the namespace
 747       * @param string $title The DB key form the title
 748       * @param string $fragment The link fragment (after the "#")
 749       * @param string $interwiki The interwiki prefix
 750       * @return string The prefixed form of the title
 751       */
 752  	public static function makeName( $ns, $title, $fragment = '', $interwiki = '' ) {
 753          global $wgContLang;
 754  
 755          $namespace = $wgContLang->getNsText( $ns );
 756          $name = $namespace == '' ? $title : "$namespace:$title";
 757          if ( strval( $interwiki ) != '' ) {
 758              $name = "$interwiki:$name";
 759          }
 760          if ( strval( $fragment ) != '' ) {
 761              $name .= '#' . $fragment;
 762          }
 763          return $name;
 764      }
 765  
 766      /**
 767       * Escape a text fragment, say from a link, for a URL
 768       *
 769       * @param string $fragment Containing a URL or link fragment (after the "#")
 770       * @return string Escaped string
 771       */
 772  	static function escapeFragmentForURL( $fragment ) {
 773          # Note that we don't urlencode the fragment.  urlencoded Unicode
 774          # fragments appear not to work in IE (at least up to 7) or in at least
 775          # one version of Opera 9.x.  The W3C validator, for one, doesn't seem
 776          # to care if they aren't encoded.
 777          return Sanitizer::escapeId( $fragment, 'noninitial' );
 778      }
 779  
 780      /**
 781       * Callback for usort() to do title sorts by (namespace, title)
 782       *
 783       * @param Title $a
 784       * @param Title $b
 785       *
 786       * @return int Result of string comparison, or namespace comparison
 787       */
 788  	public static function compare( $a, $b ) {
 789          if ( $a->getNamespace() == $b->getNamespace() ) {
 790              return strcmp( $a->getText(), $b->getText() );
 791          } else {
 792              return $a->getNamespace() - $b->getNamespace();
 793          }
 794      }
 795  
 796      /**
 797       * Determine whether the object refers to a page within
 798       * this project.
 799       *
 800       * @return bool True if this is an in-project interwiki link or a wikilink, false otherwise
 801       */
 802  	public function isLocal() {
 803          if ( $this->isExternal() ) {
 804              $iw = Interwiki::fetch( $this->mInterwiki );
 805              if ( $iw ) {
 806                  return $iw->isLocal();
 807              }
 808          }
 809          return true;
 810      }
 811  
 812      /**
 813       * Is this Title interwiki?
 814       *
 815       * @return bool
 816       */
 817  	public function isExternal() {
 818          return $this->mInterwiki !== '';
 819      }
 820  
 821      /**
 822       * Get the interwiki prefix
 823       *
 824       * Use Title::isExternal to check if a interwiki is set
 825       *
 826       * @return string Interwiki prefix
 827       */
 828  	public function getInterwiki() {
 829          return $this->mInterwiki;
 830      }
 831  
 832      /**
 833       * Was this a local interwiki link?
 834       *
 835       * @return bool
 836       */
 837  	public function wasLocalInterwiki() {
 838          return $this->mLocalInterwiki;
 839      }
 840  
 841      /**
 842       * Determine whether the object refers to a page within
 843       * this project and is transcludable.
 844       *
 845       * @return bool True if this is transcludable
 846       */
 847  	public function isTrans() {
 848          if ( !$this->isExternal() ) {
 849              return false;
 850          }
 851  
 852          return Interwiki::fetch( $this->mInterwiki )->isTranscludable();
 853      }
 854  
 855      /**
 856       * Returns the DB name of the distant wiki which owns the object.
 857       *
 858       * @return string The DB name
 859       */
 860  	public function getTransWikiID() {
 861          if ( !$this->isExternal() ) {
 862              return false;
 863          }
 864  
 865          return Interwiki::fetch( $this->mInterwiki )->getWikiID();
 866      }
 867  
 868      /**
 869       * Get a TitleValue object representing this Title.
 870       *
 871       * @note Not all valid Titles have a corresponding valid TitleValue
 872       * (e.g. TitleValues cannot represent page-local links that have a
 873       * fragment but no title text).
 874       *
 875       * @return TitleValue|null
 876       */
 877  	public function getTitleValue() {
 878          if ( $this->mTitleValue === null ) {
 879              try {
 880                  $this->mTitleValue = new TitleValue(
 881                      $this->getNamespace(),
 882                      $this->getDBkey(),
 883                      $this->getFragment() );
 884              } catch ( InvalidArgumentException $ex ) {
 885                  wfDebug( __METHOD__ . ': Can\'t create a TitleValue for [[' .
 886                      $this->getPrefixedText() . ']]: ' . $ex->getMessage() . "\n" );
 887              }
 888          }
 889  
 890          return $this->mTitleValue;
 891      }
 892  
 893      /**
 894       * Get the text form (spaces not underscores) of the main part
 895       *
 896       * @return string Main part of the title
 897       */
 898  	public function getText() {
 899          return $this->mTextform;
 900      }
 901  
 902      /**
 903       * Get the URL-encoded form of the main part
 904       *
 905       * @return string Main part of the title, URL-encoded
 906       */
 907  	public function getPartialURL() {
 908          return $this->mUrlform;
 909      }
 910  
 911      /**
 912       * Get the main part with underscores
 913       *
 914       * @return string Main part of the title, with underscores
 915       */
 916  	public function getDBkey() {
 917          return $this->mDbkeyform;
 918      }
 919  
 920      /**
 921       * Get the DB key with the initial letter case as specified by the user
 922       *
 923       * @return string DB key
 924       */
 925  	function getUserCaseDBKey() {
 926          if ( !is_null( $this->mUserCaseDBKey ) ) {
 927              return $this->mUserCaseDBKey;
 928          } else {
 929              // If created via makeTitle(), $this->mUserCaseDBKey is not set.
 930              return $this->mDbkeyform;
 931          }
 932      }
 933  
 934      /**
 935       * Get the namespace index, i.e. one of the NS_xxxx constants.
 936       *
 937       * @return int Namespace index
 938       */
 939  	public function getNamespace() {
 940          return $this->mNamespace;
 941      }
 942  
 943      /**
 944       * Get the page's content model id, see the CONTENT_MODEL_XXX constants.
 945       *
 946       * @throws MWException
 947       * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
 948       * @return string Content model id
 949       */
 950  	public function getContentModel( $flags = 0 ) {
 951          # Calling getArticleID() loads the field from cache as needed
 952          if ( !$this->mContentModel && $this->getArticleID( $flags ) ) {
 953              $linkCache = LinkCache::singleton();
 954              $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' );
 955          }
 956  
 957          if ( !$this->mContentModel ) {
 958              $this->mContentModel = ContentHandler::getDefaultModelFor( $this );
 959          }
 960  
 961          if ( !$this->mContentModel ) {
 962              throw new MWException( 'Failed to determine content model!' );
 963          }
 964  
 965          return $this->mContentModel;
 966      }
 967  
 968      /**
 969       * Convenience method for checking a title's content model name
 970       *
 971       * @param string $id The content model ID (use the CONTENT_MODEL_XXX constants).
 972       * @return bool True if $this->getContentModel() == $id
 973       */
 974  	public function hasContentModel( $id ) {
 975          return $this->getContentModel() == $id;
 976      }
 977  
 978      /**
 979       * Get the namespace text
 980       *
 981       * @return string Namespace text
 982       */
 983  	public function getNsText() {
 984          if ( $this->isExternal() ) {
 985              // This probably shouldn't even happen. ohh man, oh yuck.
 986              // But for interwiki transclusion it sometimes does.
 987              // Shit. Shit shit shit.
 988              //
 989              // Use the canonical namespaces if possible to try to
 990              // resolve a foreign namespace.
 991              if ( MWNamespace::exists( $this->mNamespace ) ) {
 992                  return MWNamespace::getCanonicalName( $this->mNamespace );
 993              }
 994          }
 995  
 996          try {
 997              $formatter = self::getTitleFormatter();
 998              return $formatter->getNamespaceName( $this->mNamespace, $this->mDbkeyform );
 999          } catch ( InvalidArgumentException $ex ) {
1000              wfDebug( __METHOD__ . ': ' . $ex->getMessage() . "\n" );
1001              return false;
1002          }
1003      }
1004  
1005      /**
1006       * Get the namespace text of the subject (rather than talk) page
1007       *
1008       * @return string Namespace text
1009       */
1010  	public function getSubjectNsText() {
1011          global $wgContLang;
1012          return $wgContLang->getNsText( MWNamespace::getSubject( $this->mNamespace ) );
1013      }
1014  
1015      /**
1016       * Get the namespace text of the talk page
1017       *
1018       * @return string Namespace text
1019       */
1020  	public function getTalkNsText() {
1021          global $wgContLang;
1022          return $wgContLang->getNsText( MWNamespace::getTalk( $this->mNamespace ) );
1023      }
1024  
1025      /**
1026       * Could this title have a corresponding talk page?
1027       *
1028       * @return bool
1029       */
1030  	public function canTalk() {
1031          return MWNamespace::canTalk( $this->mNamespace );
1032      }
1033  
1034      /**
1035       * Is this in a namespace that allows actual pages?
1036       *
1037       * @return bool
1038       * @internal note -- uses hardcoded namespace index instead of constants
1039       */
1040  	public function canExist() {
1041          return $this->mNamespace >= NS_MAIN;
1042      }
1043  
1044      /**
1045       * Can this title be added to a user's watchlist?
1046       *
1047       * @return bool
1048       */
1049  	public function isWatchable() {
1050          return !$this->isExternal() && MWNamespace::isWatchable( $this->getNamespace() );
1051      }
1052  
1053      /**
1054       * Returns true if this is a special page.
1055       *
1056       * @return bool
1057       */
1058  	public function isSpecialPage() {
1059          return $this->getNamespace() == NS_SPECIAL;
1060      }
1061  
1062      /**
1063       * Returns true if this title resolves to the named special page
1064       *
1065       * @param string $name The special page name
1066       * @return bool
1067       */
1068  	public function isSpecial( $name ) {
1069          if ( $this->isSpecialPage() ) {
1070              list( $thisName, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $this->getDBkey() );
1071              if ( $name == $thisName ) {
1072                  return true;
1073              }
1074          }
1075          return false;
1076      }
1077  
1078      /**
1079       * If the Title refers to a special page alias which is not the local default, resolve
1080       * the alias, and localise the name as necessary.  Otherwise, return $this
1081       *
1082       * @return Title
1083       */
1084  	public function fixSpecialName() {
1085          if ( $this->isSpecialPage() ) {
1086              list( $canonicalName, $par ) = SpecialPageFactory::resolveAlias( $this->mDbkeyform );
1087              if ( $canonicalName ) {
1088                  $localName = SpecialPageFactory::getLocalNameFor( $canonicalName, $par );
1089                  if ( $localName != $this->mDbkeyform ) {
1090                      return Title::makeTitle( NS_SPECIAL, $localName );
1091                  }
1092              }
1093          }
1094          return $this;
1095      }
1096  
1097      /**
1098       * Returns true if the title is inside the specified namespace.
1099       *
1100       * Please make use of this instead of comparing to getNamespace()
1101       * This function is much more resistant to changes we may make
1102       * to namespaces than code that makes direct comparisons.
1103       * @param int $ns The namespace
1104       * @return bool
1105       * @since 1.19
1106       */
1107  	public function inNamespace( $ns ) {
1108          return MWNamespace::equals( $this->getNamespace(), $ns );
1109      }
1110  
1111      /**
1112       * Returns true if the title is inside one of the specified namespaces.
1113       *
1114       * @param int $namespaces,... The namespaces to check for
1115       * @return bool
1116       * @since 1.19
1117       */
1118  	public function inNamespaces( /* ... */ ) {
1119          $namespaces = func_get_args();
1120          if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) {
1121              $namespaces = $namespaces[0];
1122          }
1123  
1124          foreach ( $namespaces as $ns ) {
1125              if ( $this->inNamespace( $ns ) ) {
1126                  return true;
1127              }
1128          }
1129  
1130          return false;
1131      }
1132  
1133      /**
1134       * Returns true if the title has the same subject namespace as the
1135       * namespace specified.
1136       * For example this method will take NS_USER and return true if namespace
1137       * is either NS_USER or NS_USER_TALK since both of them have NS_USER
1138       * as their subject namespace.
1139       *
1140       * This is MUCH simpler than individually testing for equivalence
1141       * against both NS_USER and NS_USER_TALK, and is also forward compatible.
1142       * @since 1.19
1143       * @param int $ns
1144       * @return bool
1145       */
1146  	public function hasSubjectNamespace( $ns ) {
1147          return MWNamespace::subjectEquals( $this->getNamespace(), $ns );
1148      }
1149  
1150      /**
1151       * Is this Title in a namespace which contains content?
1152       * In other words, is this a content page, for the purposes of calculating
1153       * statistics, etc?
1154       *
1155       * @return bool
1156       */
1157  	public function isContentPage() {
1158          return MWNamespace::isContent( $this->getNamespace() );
1159      }
1160  
1161      /**
1162       * Would anybody with sufficient privileges be able to move this page?
1163       * Some pages just aren't movable.
1164       *
1165       * @return bool
1166       */
1167  	public function isMovable() {
1168          if ( !MWNamespace::isMovable( $this->getNamespace() ) || $this->isExternal() ) {
1169              // Interwiki title or immovable namespace. Hooks don't get to override here
1170              return false;
1171          }
1172  
1173          $result = true;
1174          wfRunHooks( 'TitleIsMovable', array( $this, &$result ) );
1175          return $result;
1176      }
1177  
1178      /**
1179       * Is this the mainpage?
1180       * @note Title::newFromText seems to be sufficiently optimized by the title
1181       * cache that we don't need to over-optimize by doing direct comparisons and
1182       * accidentally creating new bugs where $title->equals( Title::newFromText() )
1183       * ends up reporting something differently than $title->isMainPage();
1184       *
1185       * @since 1.18
1186       * @return bool
1187       */
1188  	public function isMainPage() {
1189          return $this->equals( Title::newMainPage() );
1190      }
1191  
1192      /**
1193       * Is this a subpage?
1194       *
1195       * @return bool
1196       */
1197  	public function isSubpage() {
1198          return MWNamespace::hasSubpages( $this->mNamespace )
1199              ? strpos( $this->getText(), '/' ) !== false
1200              : false;
1201      }
1202  
1203      /**
1204       * Is this a conversion table for the LanguageConverter?
1205       *
1206       * @return bool
1207       */
1208  	public function isConversionTable() {
1209          // @todo ConversionTable should become a separate content model.
1210  
1211          return $this->getNamespace() == NS_MEDIAWIKI &&
1212              strpos( $this->getText(), 'Conversiontable/' ) === 0;
1213      }
1214  
1215      /**
1216       * Does that page contain wikitext, or it is JS, CSS or whatever?
1217       *
1218       * @return bool
1219       */
1220  	public function isWikitextPage() {
1221          return $this->hasContentModel( CONTENT_MODEL_WIKITEXT );
1222      }
1223  
1224      /**
1225       * Could this page contain custom CSS or JavaScript for the global UI.
1226       * This is generally true for pages in the MediaWiki namespace having CONTENT_MODEL_CSS
1227       * or CONTENT_MODEL_JAVASCRIPT.
1228       *
1229       * This method does *not* return true for per-user JS/CSS. Use isCssJsSubpage()
1230       * for that!
1231       *
1232       * Note that this method should not return true for pages that contain and
1233       * show "inactive" CSS or JS.
1234       *
1235       * @return bool
1236       */
1237  	public function isCssOrJsPage() {
1238          $isCssOrJsPage = NS_MEDIAWIKI == $this->mNamespace
1239              && ( $this->hasContentModel( CONTENT_MODEL_CSS )
1240                  || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
1241  
1242          # @note This hook is also called in ContentHandler::getDefaultModel.
1243          #   It's called here again to make sure hook functions can force this
1244          #   method to return true even outside the mediawiki namespace.
1245  
1246          wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$isCssOrJsPage ) );
1247  
1248          return $isCssOrJsPage;
1249      }
1250  
1251      /**
1252       * Is this a .css or .js subpage of a user page?
1253       * @return bool
1254       */
1255  	public function isCssJsSubpage() {
1256          return ( NS_USER == $this->mNamespace && $this->isSubpage()
1257                  && ( $this->hasContentModel( CONTENT_MODEL_CSS )
1258                      || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
1259      }
1260  
1261      /**
1262       * Trim down a .css or .js subpage title to get the corresponding skin name
1263       *
1264       * @return string Containing skin name from .css or .js subpage title
1265       */
1266  	public function getSkinFromCssJsSubpage() {
1267          $subpage = explode( '/', $this->mTextform );
1268          $subpage = $subpage[count( $subpage ) - 1];
1269          $lastdot = strrpos( $subpage, '.' );
1270          if ( $lastdot === false ) {
1271              return $subpage; # Never happens: only called for names ending in '.css' or '.js'
1272          }
1273          return substr( $subpage, 0, $lastdot );
1274      }
1275  
1276      /**
1277       * Is this a .css subpage of a user page?
1278       *
1279       * @return bool
1280       */
1281  	public function isCssSubpage() {
1282          return ( NS_USER == $this->mNamespace && $this->isSubpage()
1283              && $this->hasContentModel( CONTENT_MODEL_CSS ) );
1284      }
1285  
1286      /**
1287       * Is this a .js subpage of a user page?
1288       *
1289       * @return bool
1290       */
1291  	public function isJsSubpage() {
1292          return ( NS_USER == $this->mNamespace && $this->isSubpage()
1293              && $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
1294      }
1295  
1296      /**
1297       * Is this a talk page of some sort?
1298       *
1299       * @return bool
1300       */
1301  	public function isTalkPage() {
1302          return MWNamespace::isTalk( $this->getNamespace() );
1303      }
1304  
1305      /**
1306       * Get a Title object associated with the talk page of this article
1307       *
1308       * @return Title The object for the talk page
1309       */
1310  	public function getTalkPage() {
1311          return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() );
1312      }
1313  
1314      /**
1315       * Get a title object associated with the subject page of this
1316       * talk page
1317       *
1318       * @return Title The object for the subject page
1319       */
1320  	public function getSubjectPage() {
1321          // Is this the same title?
1322          $subjectNS = MWNamespace::getSubject( $this->getNamespace() );
1323          if ( $this->getNamespace() == $subjectNS ) {
1324              return $this;
1325          }
1326          return Title::makeTitle( $subjectNS, $this->getDBkey() );
1327      }
1328  
1329      /**
1330       * Get the default namespace index, for when there is no namespace
1331       *
1332       * @return int Default namespace index
1333       */
1334  	public function getDefaultNamespace() {
1335          return $this->mDefaultNamespace;
1336      }
1337  
1338      /**
1339       * Get the Title fragment (i.e.\ the bit after the #) in text form
1340       *
1341       * Use Title::hasFragment to check for a fragment
1342       *
1343       * @return string Title fragment
1344       */
1345  	public function getFragment() {
1346          return $this->mFragment;
1347      }
1348  
1349      /**
1350       * Check if a Title fragment is set
1351       *
1352       * @return bool
1353       * @since 1.23
1354       */
1355  	public function hasFragment() {
1356          return $this->mFragment !== '';
1357      }
1358  
1359      /**
1360       * Get the fragment in URL form, including the "#" character if there is one
1361       * @return string Fragment in URL form
1362       */
1363  	public function getFragmentForURL() {
1364          if ( !$this->hasFragment() ) {
1365              return '';
1366          } else {
1367              return '#' . Title::escapeFragmentForURL( $this->getFragment() );
1368          }
1369      }
1370  
1371      /**
1372       * Set the fragment for this title. Removes the first character from the
1373       * specified fragment before setting, so it assumes you're passing it with
1374       * an initial "#".
1375       *
1376       * Deprecated for public use, use Title::makeTitle() with fragment parameter.
1377       * Still in active use privately.
1378       *
1379       * @param string $fragment Text
1380       */
1381  	public function setFragment( $fragment ) {
1382          $this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) );
1383      }
1384  
1385      /**
1386       * Prefix some arbitrary text with the namespace or interwiki prefix
1387       * of this object
1388       *
1389       * @param string $name The text
1390       * @return string The prefixed text
1391       */
1392  	private function prefix( $name ) {
1393          $p = '';
1394          if ( $this->isExternal() ) {
1395              $p = $this->mInterwiki . ':';
1396          }
1397  
1398          if ( 0 != $this->mNamespace ) {
1399              $p .= $this->getNsText() . ':';
1400          }
1401          return $p . $name;
1402      }
1403  
1404      /**
1405       * Get the prefixed database key form
1406       *
1407       * @return string The prefixed title, with underscores and
1408       *  any interwiki and namespace prefixes
1409       */
1410  	public function getPrefixedDBkey() {
1411          $s = $this->prefix( $this->mDbkeyform );
1412          $s = str_replace( ' ', '_', $s );
1413          return $s;
1414      }
1415  
1416      /**
1417       * Get the prefixed title with spaces.
1418       * This is the form usually used for display
1419       *
1420       * @return string The prefixed title, with spaces
1421       */
1422  	public function getPrefixedText() {
1423          if ( $this->mPrefixedText === null ) {
1424              $s = $this->prefix( $this->mTextform );
1425              $s = str_replace( '_', ' ', $s );
1426              $this->mPrefixedText = $s;
1427          }
1428          return $this->mPrefixedText;
1429      }
1430  
1431      /**
1432       * Return a string representation of this title
1433       *
1434       * @return string Representation of this title
1435       */
1436  	public function __toString() {
1437          return $this->getPrefixedText();
1438      }
1439  
1440      /**
1441       * Get the prefixed title with spaces, plus any fragment
1442       * (part beginning with '#')
1443       *
1444       * @return string The prefixed title, with spaces and the fragment, including '#'
1445       */
1446  	public function getFullText() {
1447          $text = $this->getPrefixedText();
1448          if ( $this->hasFragment() ) {
1449              $text .= '#' . $this->getFragment();
1450          }
1451          return $text;
1452      }
1453  
1454      /**
1455       * Get the root page name text without a namespace, i.e. the leftmost part before any slashes
1456       *
1457       * @par Example:
1458       * @code
1459       * Title::newFromText('User:Foo/Bar/Baz')->getRootText();
1460       * # returns: 'Foo'
1461       * @endcode
1462       *
1463       * @return string Root name
1464       * @since 1.20
1465       */
1466  	public function getRootText() {
1467          if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1468              return $this->getText();
1469          }
1470  
1471          return strtok( $this->getText(), '/' );
1472      }
1473  
1474      /**
1475       * Get the root page name title, i.e. the leftmost part before any slashes
1476       *
1477       * @par Example:
1478       * @code
1479       * Title::newFromText('User:Foo/Bar/Baz')->getRootTitle();
1480       * # returns: Title{User:Foo}
1481       * @endcode
1482       *
1483       * @return Title Root title
1484       * @since 1.20
1485       */
1486  	public function getRootTitle() {
1487          return Title::makeTitle( $this->getNamespace(), $this->getRootText() );
1488      }
1489  
1490      /**
1491       * Get the base page name without a namespace, i.e. the part before the subpage name
1492       *
1493       * @par Example:
1494       * @code
1495       * Title::newFromText('User:Foo/Bar/Baz')->getBaseText();
1496       * # returns: 'Foo/Bar'
1497       * @endcode
1498       *
1499       * @return string Base name
1500       */
1501  	public function getBaseText() {
1502          if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1503              return $this->getText();
1504          }
1505  
1506          $parts = explode( '/', $this->getText() );
1507          # Don't discard the real title if there's no subpage involved
1508          if ( count( $parts ) > 1 ) {
1509              unset( $parts[count( $parts ) - 1] );
1510          }
1511          return implode( '/', $parts );
1512      }
1513  
1514      /**
1515       * Get the base page name title, i.e. the part before the subpage name
1516       *
1517       * @par Example:
1518       * @code
1519       * Title::newFromText('User:Foo/Bar/Baz')->getBaseTitle();
1520       * # returns: Title{User:Foo/Bar}
1521       * @endcode
1522       *
1523       * @return Title Base title
1524       * @since 1.20
1525       */
1526  	public function getBaseTitle() {
1527          return Title::makeTitle( $this->getNamespace(), $this->getBaseText() );
1528      }
1529  
1530      /**
1531       * Get the lowest-level subpage name, i.e. the rightmost part after any slashes
1532       *
1533       * @par Example:
1534       * @code
1535       * Title::newFromText('User:Foo/Bar/Baz')->getSubpageText();
1536       * # returns: "Baz"
1537       * @endcode
1538       *
1539       * @return string Subpage name
1540       */
1541  	public function getSubpageText() {
1542          if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
1543              return $this->mTextform;
1544          }
1545          $parts = explode( '/', $this->mTextform );
1546          return $parts[count( $parts ) - 1];
1547      }
1548  
1549      /**
1550       * Get the title for a subpage of the current page
1551       *
1552       * @par Example:
1553       * @code
1554       * Title::newFromText('User:Foo/Bar/Baz')->getSubpage("Asdf");
1555       * # returns: Title{User:Foo/Bar/Baz/Asdf}
1556       * @endcode
1557       *
1558       * @param string $text The subpage name to add to the title
1559       * @return Title Subpage title
1560       * @since 1.20
1561       */
1562  	public function getSubpage( $text ) {
1563          return Title::makeTitleSafe( $this->getNamespace(), $this->getText() . '/' . $text );
1564      }
1565  
1566      /**
1567       * Get a URL-encoded form of the subpage text
1568       *
1569       * @return string URL-encoded subpage name
1570       */
1571  	public function getSubpageUrlForm() {
1572          $text = $this->getSubpageText();
1573          $text = wfUrlencode( str_replace( ' ', '_', $text ) );
1574          return $text;
1575      }
1576  
1577      /**
1578       * Get a URL-encoded title (not an actual URL) including interwiki
1579       *
1580       * @return string The URL-encoded form
1581       */
1582  	public function getPrefixedURL() {
1583          $s = $this->prefix( $this->mDbkeyform );
1584          $s = wfUrlencode( str_replace( ' ', '_', $s ) );
1585          return $s;
1586      }
1587  
1588      /**
1589       * Helper to fix up the get{Canonical,Full,Link,Local,Internal}URL args
1590       * get{Canonical,Full,Link,Local,Internal}URL methods accepted an optional
1591       * second argument named variant. This was deprecated in favor
1592       * of passing an array of option with a "variant" key
1593       * Once $query2 is removed for good, this helper can be dropped
1594       * and the wfArrayToCgi moved to getLocalURL();
1595       *
1596       * @since 1.19 (r105919)
1597       * @param array|string $query
1598       * @param bool $query2
1599       * @return string
1600       */
1601  	private static function fixUrlQueryArgs( $query, $query2 = false ) {
1602          if ( $query2 !== false ) {
1603              wfDeprecated( "Title::get{Canonical,Full,Link,Local,Internal}URL " .
1604                  "method called with a second parameter is deprecated. Add your " .
1605                  "parameter to an array passed as the first parameter.", "1.19" );
1606          }
1607          if ( is_array( $query ) ) {
1608              $query = wfArrayToCgi( $query );
1609          }
1610          if ( $query2 ) {
1611              if ( is_string( $query2 ) ) {
1612                  // $query2 is a string, we will consider this to be
1613                  // a deprecated $variant argument and add it to the query
1614                  $query2 = wfArrayToCgi( array( 'variant' => $query2 ) );
1615              } else {
1616                  $query2 = wfArrayToCgi( $query2 );
1617              }
1618              // If we have $query content add a & to it first
1619              if ( $query ) {
1620                  $query .= '&';
1621              }
1622              // Now append the queries together
1623              $query .= $query2;
1624          }
1625          return $query;
1626      }
1627  
1628      /**
1629       * Get a real URL referring to this title, with interwiki link and
1630       * fragment
1631       *
1632       * @see self::getLocalURL for the arguments.
1633       * @see wfExpandUrl
1634       * @param array|string $query
1635       * @param bool $query2
1636       * @param string $proto Protocol type to use in URL
1637       * @return string The URL
1638       */
1639  	public function getFullURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) {
1640          $query = self::fixUrlQueryArgs( $query, $query2 );
1641  
1642          # Hand off all the decisions on urls to getLocalURL
1643          $url = $this->getLocalURL( $query );
1644  
1645          # Expand the url to make it a full url. Note that getLocalURL has the
1646          # potential to output full urls for a variety of reasons, so we use
1647          # wfExpandUrl instead of simply prepending $wgServer
1648          $url = wfExpandUrl( $url, $proto );
1649  
1650          # Finally, add the fragment.
1651          $url .= $this->getFragmentForURL();
1652  
1653          wfRunHooks( 'GetFullURL', array( &$this, &$url, $query ) );
1654          return $url;
1655      }
1656  
1657      /**
1658       * Get a URL with no fragment or server name (relative URL) from a Title object.
1659       * If this page is generated with action=render, however,
1660       * $wgServer is prepended to make an absolute URL.
1661       *
1662       * @see self::getFullURL to always get an absolute URL.
1663       * @see self::getLinkURL to always get a URL that's the simplest URL that will be
1664       *  valid to link, locally, to the current Title.
1665       * @see self::newFromText to produce a Title object.
1666       *
1667       * @param string|array $query An optional query string,
1668       *   not used for interwiki links. Can be specified as an associative array as well,
1669       *   e.g., array( 'action' => 'edit' ) (keys and values will be URL-escaped).
1670       *   Some query patterns will trigger various shorturl path replacements.
1671       * @param array $query2 An optional secondary query array. This one MUST
1672       *   be an array. If a string is passed it will be interpreted as a deprecated
1673       *   variant argument and urlencoded into a variant= argument.
1674       *   This second query argument will be added to the $query
1675       *   The second parameter is deprecated since 1.19. Pass it as a key,value
1676       *   pair in the first parameter array instead.
1677       *
1678       * @return string String of the URL.
1679       */
1680  	public function getLocalURL( $query = '', $query2 = false ) {
1681          global $wgArticlePath, $wgScript, $wgServer, $wgRequest;
1682  
1683          $query = self::fixUrlQueryArgs( $query, $query2 );
1684  
1685          $interwiki = Interwiki::fetch( $this->mInterwiki );
1686          if ( $interwiki ) {
1687              $namespace = $this->getNsText();
1688              if ( $namespace != '' ) {
1689                  # Can this actually happen? Interwikis shouldn't be parsed.
1690                  # Yes! It can in interwiki transclusion. But... it probably shouldn't.
1691                  $namespace .= ':';
1692              }
1693              $url = $interwiki->getURL( $namespace . $this->getDBkey() );
1694              $url = wfAppendQuery( $url, $query );
1695          } else {
1696              $dbkey = wfUrlencode( $this->getPrefixedDBkey() );
1697              if ( $query == '' ) {
1698                  $url = str_replace( '$1', $dbkey, $wgArticlePath );
1699                  wfRunHooks( 'GetLocalURL::Article', array( &$this, &$url ) );
1700              } else {
1701                  global $wgVariantArticlePath, $wgActionPaths, $wgContLang;
1702                  $url = false;
1703                  $matches = array();
1704  
1705                  if ( !empty( $wgActionPaths )
1706                      && preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches )
1707                  ) {
1708                      $action = urldecode( $matches[2] );
1709                      if ( isset( $wgActionPaths[$action] ) ) {
1710                          $query = $matches[1];
1711                          if ( isset( $matches[4] ) ) {
1712                              $query .= $matches[4];
1713                          }
1714                          $url = str_replace( '$1', $dbkey, $wgActionPaths[$action] );
1715                          if ( $query != '' ) {
1716                              $url = wfAppendQuery( $url, $query );
1717                          }
1718                      }
1719                  }
1720  
1721                  if ( $url === false
1722                      && $wgVariantArticlePath
1723                      && $wgContLang->getCode() === $this->getPageLanguage()->getCode()
1724                      && $this->getPageLanguage()->hasVariants()
1725                      && preg_match( '/^variant=([^&]*)$/', $query, $matches )
1726                  ) {
1727                      $variant = urldecode( $matches[1] );
1728                      if ( $this->getPageLanguage()->hasVariant( $variant ) ) {
1729                          // Only do the variant replacement if the given variant is a valid
1730                          // variant for the page's language.
1731                          $url = str_replace( '$2', urlencode( $variant ), $wgVariantArticlePath );
1732                          $url = str_replace( '$1', $dbkey, $url );
1733                      }
1734                  }
1735  
1736                  if ( $url === false ) {
1737                      if ( $query == '-' ) {
1738                          $query = '';
1739                      }
1740                      $url = "{$wgScript}?title={$dbkey}&{$query}";
1741                  }
1742              }
1743  
1744              wfRunHooks( 'GetLocalURL::Internal', array( &$this, &$url, $query ) );
1745  
1746              // @todo FIXME: This causes breakage in various places when we
1747              // actually expected a local URL and end up with dupe prefixes.
1748              if ( $wgRequest->getVal( 'action' ) == 'render' ) {
1749                  $url = $wgServer . $url;
1750              }
1751          }
1752          wfRunHooks( 'GetLocalURL', array( &$this, &$url, $query ) );
1753          return $url;
1754      }
1755  
1756      /**
1757       * Get a URL that's the simplest URL that will be valid to link, locally,
1758       * to the current Title.  It includes the fragment, but does not include
1759       * the server unless action=render is used (or the link is external).  If
1760       * there's a fragment but the prefixed text is empty, we just return a link
1761       * to the fragment.
1762       *
1763       * The result obviously should not be URL-escaped, but does need to be
1764       * HTML-escaped if it's being output in HTML.
1765       *
1766       * @param array $query
1767       * @param bool $query2
1768       * @param string $proto Protocol to use; setting this will cause a full URL to be used
1769       * @see self::getLocalURL for the arguments.
1770       * @return string The URL
1771       */
1772  	public function getLinkURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) {
1773          wfProfileIn( __METHOD__ );
1774          if ( $this->isExternal() || $proto !== PROTO_RELATIVE ) {
1775              $ret = $this->getFullURL( $query, $query2, $proto );
1776          } elseif ( $this->getPrefixedText() === '' && $this->hasFragment() ) {
1777              $ret = $this->getFragmentForURL();
1778          } else {
1779              $ret = $this->getLocalURL( $query, $query2 ) . $this->getFragmentForURL();
1780          }
1781          wfProfileOut( __METHOD__ );
1782          return $ret;
1783      }
1784  
1785      /**
1786       * Get the URL form for an internal link.
1787       * - Used in various Squid-related code, in case we have a different
1788       * internal hostname for the server from the exposed one.
1789       *
1790       * This uses $wgInternalServer to qualify the path, or $wgServer
1791       * if $wgInternalServer is not set. If the server variable used is
1792       * protocol-relative, the URL will be expanded to http://
1793       *
1794       * @see self::getLocalURL for the arguments.
1795       * @return string The URL
1796       */
1797  	public function getInternalURL( $query = '', $query2 = false ) {
1798          global $wgInternalServer, $wgServer;
1799          $query = self::fixUrlQueryArgs( $query, $query2 );
1800          $server = $wgInternalServer !== false ? $wgInternalServer : $wgServer;
1801          $url = wfExpandUrl( $server . $this->getLocalURL( $query ), PROTO_HTTP );
1802          wfRunHooks( 'GetInternalURL', array( &$this, &$url, $query ) );
1803          return $url;
1804      }
1805  
1806      /**
1807       * Get the URL for a canonical link, for use in things like IRC and
1808       * e-mail notifications. Uses $wgCanonicalServer and the
1809       * GetCanonicalURL hook.
1810       *
1811       * NOTE: Unlike getInternalURL(), the canonical URL includes the fragment
1812       *
1813       * @see self::getLocalURL for the arguments.
1814       * @return string The URL
1815       * @since 1.18
1816       */
1817  	public function getCanonicalURL( $query = '', $query2 = false ) {
1818          $query = self::fixUrlQueryArgs( $query, $query2 );
1819          $url = wfExpandUrl( $this->getLocalURL( $query ) . $this->getFragmentForURL(), PROTO_CANONICAL );
1820          wfRunHooks( 'GetCanonicalURL', array( &$this, &$url, $query ) );
1821          return $url;
1822      }
1823  
1824      /**
1825       * Get the edit URL for this Title
1826       *
1827       * @return string The URL, or a null string if this is an interwiki link
1828       */
1829  	public function getEditURL() {
1830          if ( $this->isExternal() ) {
1831              return '';
1832          }
1833          $s = $this->getLocalURL( 'action=edit' );
1834  
1835          return $s;
1836      }
1837  
1838      /**
1839       * Is $wgUser watching this page?
1840       *
1841       * @deprecated since 1.20; use User::isWatched() instead.
1842       * @return bool
1843       */
1844  	public function userIsWatching() {
1845          global $wgUser;
1846  
1847          if ( is_null( $this->mWatched ) ) {
1848              if ( NS_SPECIAL == $this->mNamespace || !$wgUser->isLoggedIn() ) {
1849                  $this->mWatched = false;
1850              } else {
1851                  $this->mWatched = $wgUser->isWatched( $this );
1852              }
1853          }
1854          return $this->mWatched;
1855      }
1856  
1857      /**
1858       * Can $user perform $action on this page?
1859       * This skips potentially expensive cascading permission checks
1860       * as well as avoids expensive error formatting
1861       *
1862       * Suitable for use for nonessential UI controls in common cases, but
1863       * _not_ for functional access control.
1864       *
1865       * May provide false positives, but should never provide a false negative.
1866       *
1867       * @param string $action Action that permission needs to be checked for
1868       * @param User $user User to check (since 1.19); $wgUser will be used if not provided.
1869       * @return bool
1870       */
1871  	public function quickUserCan( $action, $user = null ) {
1872          return $this->userCan( $action, $user, false );
1873      }
1874  
1875      /**
1876       * Can $user perform $action on this page?
1877       *
1878       * @param string $action Action that permission needs to be checked for
1879       * @param User $user User to check (since 1.19); $wgUser will be used if not
1880       *   provided.
1881       * @param bool $doExpensiveQueries Set this to false to avoid doing
1882       *   unnecessary queries.
1883       * @return bool
1884       */
1885  	public function userCan( $action, $user = null, $doExpensiveQueries = true ) {
1886          if ( !$user instanceof User ) {
1887              global $wgUser;
1888              $user = $wgUser;
1889          }
1890  
1891          return !count( $this->getUserPermissionsErrorsInternal(
1892              $action, $user, $doExpensiveQueries, true ) );
1893      }
1894  
1895      /**
1896       * Can $user perform $action on this page?
1897       *
1898       * @todo FIXME: This *does not* check throttles (User::pingLimiter()).
1899       *
1900       * @param string $action Action that permission needs to be checked for
1901       * @param User $user User to check
1902       * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary
1903       *   queries by skipping checks for cascading protections and user blocks.
1904       * @param array $ignoreErrors Array of Strings Set this to a list of message keys
1905       *   whose corresponding errors may be ignored.
1906       * @return array Array of arguments to wfMessage to explain permissions problems.
1907       */
1908  	public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true,
1909          $ignoreErrors = array()
1910      ) {
1911          $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries );
1912  
1913          // Remove the errors being ignored.
1914          foreach ( $errors as $index => $error ) {
1915              $error_key = is_array( $error ) ? $error[0] : $error;
1916  
1917              if ( in_array( $error_key, $ignoreErrors ) ) {
1918                  unset( $errors[$index] );
1919              }
1920          }
1921  
1922          return $errors;
1923      }
1924  
1925      /**
1926       * Permissions checks that fail most often, and which are easiest to test.
1927       *
1928       * @param string $action The action to check
1929       * @param User $user User to check
1930       * @param array $errors List of current errors
1931       * @param bool $doExpensiveQueries Whether or not to perform expensive queries
1932       * @param bool $short Short circuit on first error
1933       *
1934       * @return array List of errors
1935       */
1936  	private function checkQuickPermissions( $action, $user, $errors,
1937          $doExpensiveQueries, $short
1938      ) {
1939          if ( !wfRunHooks( 'TitleQuickPermissions',
1940              array( $this, $user, $action, &$errors, $doExpensiveQueries, $short ) )
1941          ) {
1942              return $errors;
1943          }
1944  
1945          if ( $action == 'create' ) {
1946              if (
1947                  ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
1948                  ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) )
1949              ) {
1950                  $errors[] = $user->isAnon() ? array( 'nocreatetext' ) : array( 'nocreate-loggedin' );
1951              }
1952          } elseif ( $action == 'move' ) {
1953              if ( !$user->isAllowed( 'move-rootuserpages' )
1954                      && $this->mNamespace == NS_USER && !$this->isSubpage() ) {
1955                  // Show user page-specific message only if the user can move other pages
1956                  $errors[] = array( 'cant-move-user-page' );
1957              }
1958  
1959              // Check if user is allowed to move files if it's a file
1960              if ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) {
1961                  $errors[] = array( 'movenotallowedfile' );
1962              }
1963  
1964              // Check if user is allowed to move category pages if it's a category page
1965              if ( $this->mNamespace == NS_CATEGORY && !$user->isAllowed( 'move-categorypages' ) ) {
1966                  $errors[] = array( 'cant-move-category-page' );
1967              }
1968  
1969              if ( !$user->isAllowed( 'move' ) ) {
1970                  // User can't move anything
1971                  $userCanMove = User::groupHasPermission( 'user', 'move' );
1972                  $autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' );
1973                  if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
1974                      // custom message if logged-in users without any special rights can move
1975                      $errors[] = array( 'movenologintext' );
1976                  } else {
1977                      $errors[] = array( 'movenotallowed' );
1978                  }
1979              }
1980          } elseif ( $action == 'move-target' ) {
1981              if ( !$user->isAllowed( 'move' ) ) {
1982                  // User can't move anything
1983                  $errors[] = array( 'movenotallowed' );
1984              } elseif ( !$user->isAllowed( 'move-rootuserpages' )
1985                      && $this->mNamespace == NS_USER && !$this->isSubpage() ) {
1986                  // Show user page-specific message only if the user can move other pages
1987                  $errors[] = array( 'cant-move-to-user-page' );
1988              } elseif ( !$user->isAllowed( 'move-categorypages' )
1989                      && $this->mNamespace == NS_CATEGORY ) {
1990                  // Show category page-specific message only if the user can move other pages
1991                  $errors[] = array( 'cant-move-to-category-page' );
1992              }
1993          } elseif ( !$user->isAllowed( $action ) ) {
1994              $errors[] = $this->missingPermissionError( $action, $short );
1995          }
1996  
1997          return $errors;
1998      }
1999  
2000      /**
2001       * Add the resulting error code to the errors array
2002       *
2003       * @param array $errors List of current errors
2004       * @param array $result Result of errors
2005       *
2006       * @return array List of errors
2007       */
2008  	private function resultToError( $errors, $result ) {
2009          if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
2010              // A single array representing an error
2011              $errors[] = $result;
2012          } elseif ( is_array( $result ) && is_array( $result[0] ) ) {
2013              // A nested array representing multiple errors
2014              $errors = array_merge( $errors, $result );
2015          } elseif ( $result !== '' && is_string( $result ) ) {
2016              // A string representing a message-id
2017              $errors[] = array( $result );
2018          } elseif ( $result === false ) {
2019              // a generic "We don't want them to do that"
2020              $errors[] = array( 'badaccess-group0' );
2021          }
2022          return $errors;
2023      }
2024  
2025      /**
2026       * Check various permission hooks
2027       *
2028       * @param string $action The action to check
2029       * @param User $user User to check
2030       * @param array $errors List of current errors
2031       * @param bool $doExpensiveQueries Whether or not to perform expensive queries
2032       * @param bool $short Short circuit on first error
2033       *
2034       * @return array List of errors
2035       */
2036  	private function checkPermissionHooks( $action, $user, $errors, $doExpensiveQueries, $short ) {
2037          // Use getUserPermissionsErrors instead
2038          $result = '';
2039          if ( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) {
2040              return $result ? array() : array( array( 'badaccess-group0' ) );
2041          }
2042          // Check getUserPermissionsErrors hook
2043          if ( !wfRunHooks( 'getUserPermissionsErrors', array( &$this, &$user, $action, &$result ) ) ) {
2044              $errors = $this->resultToError( $errors, $result );
2045          }
2046          // Check getUserPermissionsErrorsExpensive hook
2047          if (
2048              $doExpensiveQueries
2049              && !( $short && count( $errors ) > 0 )
2050              && !wfRunHooks( 'getUserPermissionsErrorsExpensive', array( &$this, &$user, $action, &$result ) )
2051          ) {
2052              $errors = $this->resultToError( $errors, $result );
2053          }
2054  
2055          return $errors;
2056      }
2057  
2058      /**
2059       * Check permissions on special pages & namespaces
2060       *
2061       * @param string $action The action to check
2062       * @param User $user User to check
2063       * @param array $errors List of current errors
2064       * @param bool $doExpensiveQueries Whether or not to perform expensive queries
2065       * @param bool $short Short circuit on first error
2066       *
2067       * @return array List of errors
2068       */
2069  	private function checkSpecialsAndNSPermissions( $action, $user, $errors,
2070          $doExpensiveQueries, $short
2071      ) {
2072          # Only 'createaccount' can be performed on special pages,
2073          # which don't actually exist in the DB.
2074          if ( NS_SPECIAL == $this->mNamespace && $action !== 'createaccount' ) {
2075              $errors[] = array( 'ns-specialprotected' );
2076          }
2077  
2078          # Check $wgNamespaceProtection for restricted namespaces
2079          if ( $this->isNamespaceProtected( $user ) ) {
2080              $ns = $this->mNamespace == NS_MAIN ?
2081                  wfMessage( 'nstab-main' )->text() : $this->getNsText();
2082              $errors[] = $this->mNamespace == NS_MEDIAWIKI ?
2083                  array( 'protectedinterface', $action ) : array( 'namespaceprotected', $ns, $action );
2084          }
2085  
2086          return $errors;
2087      }
2088  
2089      /**
2090       * Check CSS/JS sub-page permissions
2091       *
2092       * @param string $action The action to check
2093       * @param User $user User to check
2094       * @param array $errors List of current errors
2095       * @param bool $doExpensiveQueries Whether or not to perform expensive queries
2096       * @param bool $short Short circuit on first error
2097       *
2098       * @return array List of errors
2099       */
2100  	private function checkCSSandJSPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
2101          # Protect css/js subpages of user pages
2102          # XXX: this might be better using restrictions
2103          # XXX: right 'editusercssjs' is deprecated, for backward compatibility only
2104          if ( $action != 'patrol' && !$user->isAllowed( 'editusercssjs' ) ) {
2105              if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) {
2106                  if ( $this->isCssSubpage() && !$user->isAllowedAny( 'editmyusercss', 'editusercss' ) ) {
2107                      $errors[] = array( 'mycustomcssprotected', $action );
2108                  } elseif ( $this->isJsSubpage() && !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' ) ) {
2109                      $errors[] = array( 'mycustomjsprotected', $action );
2110                  }
2111              } else {
2112                  if ( $this->isCssSubpage() && !$user->isAllowed( 'editusercss' ) ) {
2113                      $errors[] = array( 'customcssprotected', $action );
2114                  } elseif ( $this->isJsSubpage() && !$user->isAllowed( 'edituserjs' ) ) {
2115                      $errors[] = array( 'customjsprotected', $action );
2116                  }
2117              }
2118          }
2119  
2120          return $errors;
2121      }
2122  
2123      /**
2124       * Check against page_restrictions table requirements on this
2125       * page. The user must possess all required rights for this
2126       * action.
2127       *
2128       * @param string $action The action to check
2129       * @param User $user User to check
2130       * @param array $errors List of current errors
2131       * @param bool $doExpensiveQueries Whether or not to perform expensive queries
2132       * @param bool $short Short circuit on first error
2133       *
2134       * @return array List of errors
2135       */
2136  	private function checkPageRestrictions( $action, $user, $errors, $doExpensiveQueries, $short ) {
2137          foreach ( $this->getRestrictions( $action ) as $right ) {
2138              // Backwards compatibility, rewrite sysop -> editprotected
2139              if ( $right == 'sysop' ) {
2140                  $right = 'editprotected';
2141              }
2142              // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
2143              if ( $right == 'autoconfirmed' ) {
2144                  $right = 'editsemiprotected';
2145              }
2146              if ( $right == '' ) {
2147                  continue;
2148              }
2149              if ( !$user->isAllowed( $right ) ) {
2150                  $errors[] = array( 'protectedpagetext', $right, $action );
2151              } elseif ( $this->mCascadeRestriction && !$user->isAllowed( 'protect' ) ) {
2152                  $errors[] = array( 'protectedpagetext', 'protect', $action );
2153              }
2154          }
2155  
2156          return $errors;
2157      }
2158  
2159      /**
2160       * Check restrictions on cascading pages.
2161       *
2162       * @param string $action The action to check
2163       * @param User $user User to check
2164       * @param array $errors List of current errors
2165       * @param bool $doExpensiveQueries Whether or not to perform expensive queries
2166       * @param bool $short Short circuit on first error
2167       *
2168       * @return array List of errors
2169       */
2170  	private function checkCascadingSourcesRestrictions( $action, $user, $errors,
2171          $doExpensiveQueries, $short
2172      ) {
2173          if ( $doExpensiveQueries && !$this->isCssJsSubpage() ) {
2174              # We /could/ use the protection level on the source page, but it's
2175              # fairly ugly as we have to establish a precedence hierarchy for pages
2176              # included by multiple cascade-protected pages. So just restrict
2177              # it to people with 'protect' permission, as they could remove the
2178              # protection anyway.
2179              list( $cascadingSources, $restrictions ) = $this->getCascadeProtectionSources();
2180              # Cascading protection depends on more than this page...
2181              # Several cascading protected pages may include this page...
2182              # Check each cascading level
2183              # This is only for protection restrictions, not for all actions
2184              if ( isset( $restrictions[$action] ) ) {
2185                  foreach ( $restrictions[$action] as $right ) {
2186                      // Backwards compatibility, rewrite sysop -> editprotected
2187                      if ( $right == 'sysop' ) {
2188                          $right = 'editprotected';
2189                      }
2190                      // Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
2191                      if ( $right == 'autoconfirmed' ) {
2192                          $right = 'editsemiprotected';
2193                      }
2194                      if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) {
2195                          $pages = '';
2196                          foreach ( $cascadingSources as $page ) {
2197                              $pages .= '* [[:' . $page->getPrefixedText() . "]]\n";
2198                          }
2199                          $errors[] = array( 'cascadeprotected', count( $cascadingSources ), $pages, $action );
2200                      }
2201                  }
2202              }
2203          }
2204  
2205          return $errors;
2206      }
2207  
2208      /**
2209       * Check action permissions not already checked in checkQuickPermissions
2210       *
2211       * @param string $action The action to check
2212       * @param User $user User to check
2213       * @param array $errors List of current errors
2214       * @param bool $doExpensiveQueries Whether or not to perform expensive queries
2215       * @param bool $short Short circuit on first error
2216       *
2217       * @return array List of errors
2218       */
2219  	private function checkActionPermissions( $action, $user, $errors,
2220          $doExpensiveQueries, $short
2221      ) {
2222          global $wgDeleteRevisionsLimit, $wgLang;
2223  
2224          if ( $action == 'protect' ) {
2225              if ( count( $this->getUserPermissionsErrorsInternal( 'edit',
2226                  $user, $doExpensiveQueries, true ) )
2227              ) {
2228                  // If they can't edit, they shouldn't protect.
2229                  $errors[] = array( 'protect-cantedit' );
2230              }
2231          } elseif ( $action == 'create' ) {
2232              $title_protection = $this->getTitleProtection();
2233              if ( $title_protection ) {
2234                  if ( $title_protection['pt_create_perm'] == 'sysop' ) {
2235                      $title_protection['pt_create_perm'] = 'editprotected'; // B/C
2236                  }
2237                  if ( $title_protection['pt_create_perm'] == 'autoconfirmed' ) {
2238                      $title_protection['pt_create_perm'] = 'editsemiprotected'; // B/C
2239                  }
2240                  if ( $title_protection['pt_create_perm'] == ''
2241                      || !$user->isAllowed( $title_protection['pt_create_perm'] )
2242                  ) {
2243                      $errors[] = array(
2244                          'titleprotected',
2245                          User::whoIs( $title_protection['pt_user'] ),
2246                          $title_protection['pt_reason']
2247                      );
2248                  }
2249              }
2250          } elseif ( $action == 'move' ) {
2251              // Check for immobile pages
2252              if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
2253                  // Specific message for this case
2254                  $errors[] = array( 'immobile-source-namespace', $this->getNsText() );
2255              } elseif ( !$this->isMovable() ) {
2256                  // Less specific message for rarer cases
2257                  $errors[] = array( 'immobile-source-page' );
2258              }
2259          } elseif ( $action == 'move-target' ) {
2260              if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
2261                  $errors[] = array( 'immobile-target-namespace', $this->getNsText() );
2262              } elseif ( !$this->isMovable() ) {
2263                  $errors[] = array( 'immobile-target-page' );
2264              }
2265          } elseif ( $action == 'delete' ) {
2266              $tempErrors = $this->checkPageRestrictions( 'edit',
2267                  $user, array(), $doExpensiveQueries, true );
2268              if ( !$tempErrors ) {
2269                  $tempErrors = $this->checkCascadingSourcesRestrictions( 'edit',
2270                      $user, $tempErrors, $doExpensiveQueries, true );
2271              }
2272              if ( $tempErrors ) {
2273                  // If protection keeps them from editing, they shouldn't be able to delete.
2274                  $errors[] = array( 'deleteprotected' );
2275              }
2276              if ( $doExpensiveQueries && $wgDeleteRevisionsLimit
2277                  && !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion()
2278              ) {
2279                  $errors[] = array( 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) );
2280              }
2281          }
2282          return $errors;
2283      }
2284  
2285      /**
2286       * Check that the user isn't blocked from editing.
2287       *
2288       * @param string $action The action to check
2289       * @param User $user User to check
2290       * @param array $errors List of current errors
2291       * @param bool $doExpensiveQueries Whether or not to perform expensive queries
2292       * @param bool $short Short circuit on first error
2293       *
2294       * @return array List of errors
2295       */
2296  	private function checkUserBlock( $action, $user, $errors, $doExpensiveQueries, $short ) {
2297          // Account creation blocks handled at userlogin.
2298          // Unblocking handled in SpecialUnblock
2299          if ( !$doExpensiveQueries || in_array( $action, array( 'createaccount', 'unblock' ) ) ) {
2300              return $errors;
2301          }
2302  
2303          global $wgEmailConfirmToEdit;
2304  
2305          if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) {
2306              $errors[] = array( 'confirmedittext' );
2307          }
2308  
2309          if ( ( $action == 'edit' || $action == 'create' ) && !$user->isBlockedFrom( $this ) ) {
2310              // Don't block the user from editing their own talk page unless they've been
2311              // explicitly blocked from that too.
2312          } elseif ( $user->isBlocked() && $user->mBlock->prevents( $action ) !== false ) {
2313              // @todo FIXME: Pass the relevant context into this function.
2314              $errors[] = $user->getBlock()->getPermissionsError( RequestContext::getMain() );
2315          }
2316  
2317          return $errors;
2318      }
2319  
2320      /**
2321       * Check that the user is allowed to read this page.
2322       *
2323       * @param string $action The action to check
2324       * @param User $user User to check
2325       * @param array $errors List of current errors
2326       * @param bool $doExpensiveQueries Whether or not to perform expensive queries
2327       * @param bool $short Short circuit on first error
2328       *
2329       * @return array List of errors
2330       */
2331  	private function checkReadPermissions( $action, $user, $errors, $doExpensiveQueries, $short ) {
2332          global $wgWhitelistRead, $wgWhitelistReadRegexp;
2333  
2334          $whitelisted = false;
2335          if ( User::isEveryoneAllowed( 'read' ) ) {
2336              # Shortcut for public wikis, allows skipping quite a bit of code
2337              $whitelisted = true;
2338          } elseif ( $user->isAllowed( 'read' ) ) {
2339              # If the user is allowed to read pages, he is allowed to read all pages
2340              $whitelisted = true;
2341          } elseif ( $this->isSpecial( 'Userlogin' )
2342              || $this->isSpecial( 'ChangePassword' )
2343              || $this->isSpecial( 'PasswordReset' )
2344          ) {
2345              # Always grant access to the login page.
2346              # Even anons need to be able to log in.
2347              $whitelisted = true;
2348          } elseif ( is_array( $wgWhitelistRead ) && count( $wgWhitelistRead ) ) {
2349              # Time to check the whitelist
2350              # Only do these checks is there's something to check against
2351              $name = $this->getPrefixedText();
2352              $dbName = $this->getPrefixedDBkey();
2353  
2354              // Check for explicit whitelisting with and without underscores
2355              if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) {
2356                  $whitelisted = true;
2357              } elseif ( $this->getNamespace() == NS_MAIN ) {
2358                  # Old settings might have the title prefixed with
2359                  # a colon for main-namespace pages
2360                  if ( in_array( ':' . $name, $wgWhitelistRead ) ) {
2361                      $whitelisted = true;
2362                  }
2363              } elseif ( $this->isSpecialPage() ) {
2364                  # If it's a special page, ditch the subpage bit and check again
2365                  $name = $this->getDBkey();
2366                  list( $name, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $name );
2367                  if ( $name ) {
2368                      $pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
2369                      if ( in_array( $pure, $wgWhitelistRead, true ) ) {
2370                          $whitelisted = true;
2371                      }
2372                  }
2373              }
2374          }
2375  
2376          if ( !$whitelisted && is_array( $wgWhitelistReadRegexp ) && !empty( $wgWhitelistReadRegexp ) ) {
2377              $name = $this->getPrefixedText();
2378              // Check for regex whitelisting
2379              foreach ( $wgWhitelistReadRegexp as $listItem ) {
2380                  if ( preg_match( $listItem, $name ) ) {
2381                      $whitelisted = true;
2382                      break;
2383                  }
2384              }
2385          }
2386  
2387          if ( !$whitelisted ) {
2388              # If the title is not whitelisted, give extensions a chance to do so...
2389              wfRunHooks( 'TitleReadWhitelist', array( $this, $user, &$whitelisted ) );
2390              if ( !$whitelisted ) {
2391                  $errors[] = $this->missingPermissionError( $action, $short );
2392              }
2393          }
2394  
2395          return $errors;
2396      }
2397  
2398      /**
2399       * Get a description array when the user doesn't have the right to perform
2400       * $action (i.e. when User::isAllowed() returns false)
2401       *
2402       * @param string $action The action to check
2403       * @param bool $short Short circuit on first error
2404       * @return array List of errors
2405       */
2406  	private function missingPermissionError( $action, $short ) {
2407          // We avoid expensive display logic for quickUserCan's and such
2408          if ( $short ) {
2409              return array( 'badaccess-group0' );
2410          }
2411  
2412          $groups = array_map( array( 'User', 'makeGroupLinkWiki' ),
2413              User::getGroupsWithPermission( $action ) );
2414  
2415          if ( count( $groups ) ) {
2416              global $wgLang;
2417              return array(
2418                  'badaccess-groups',
2419                  $wgLang->commaList( $groups ),
2420                  count( $groups )
2421              );
2422          } else {
2423              return array( 'badaccess-group0' );
2424          }
2425      }
2426  
2427      /**
2428       * Can $user perform $action on this page? This is an internal function,
2429       * which checks ONLY that previously checked by userCan (i.e. it leaves out
2430       * checks on wfReadOnly() and blocks)
2431       *
2432       * @param string $action Action that permission needs to be checked for
2433       * @param User $user User to check
2434       * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary queries.
2435       * @param bool $short Set this to true to stop after the first permission error.
2436       * @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
2437       */
2438  	protected function getUserPermissionsErrorsInternal( $action, $user,
2439          $doExpensiveQueries = true, $short = false
2440      ) {
2441          wfProfileIn( __METHOD__ );
2442  
2443          # Read has special handling
2444          if ( $action == 'read' ) {
2445              $checks = array(
2446                  'checkPermissionHooks',
2447                  'checkReadPermissions',
2448              );
2449          # Don't call checkSpecialsAndNSPermissions or checkCSSandJSPermissions
2450          # here as it will lead to duplicate error messages. This is okay to do
2451          # since anywhere that checks for create will also check for edit, and
2452          # those checks are called for edit.
2453          } elseif ( $action == 'create' ) {
2454              $checks = array(
2455                  'checkQuickPermissions',
2456                  'checkPermissionHooks',
2457                  'checkPageRestrictions',
2458                  'checkCascadingSourcesRestrictions',
2459                  'checkActionPermissions',
2460                  'checkUserBlock'
2461              );
2462          } else {
2463              $checks = array(
2464                  'checkQuickPermissions',
2465                  'checkPermissionHooks',
2466                  'checkSpecialsAndNSPermissions',
2467                  'checkCSSandJSPermissions',
2468                  'checkPageRestrictions',
2469                  'checkCascadingSourcesRestrictions',
2470                  'checkActionPermissions',
2471                  'checkUserBlock'
2472              );
2473          }
2474  
2475          $errors = array();
2476          while ( count( $checks ) > 0 &&
2477                  !( $short && count( $errors ) > 0 ) ) {
2478              $method = array_shift( $checks );
2479              $errors = $this->$method( $action, $user, $errors, $doExpensiveQueries, $short );
2480          }
2481  
2482          wfProfileOut( __METHOD__ );
2483          return $errors;
2484      }
2485  
2486      /**
2487       * Get a filtered list of all restriction types supported by this wiki.
2488       * @param bool $exists True to get all restriction types that apply to
2489       * titles that do exist, False for all restriction types that apply to
2490       * titles that do not exist
2491       * @return array
2492       */
2493  	public static function getFilteredRestrictionTypes( $exists = true ) {
2494          global $wgRestrictionTypes;
2495          $types = $wgRestrictionTypes;
2496          if ( $exists ) {
2497              # Remove the create restriction for existing titles
2498              $types = array_diff( $types, array( 'create' ) );
2499          } else {
2500              # Only the create and upload restrictions apply to non-existing titles
2501              $types = array_intersect( $types, array( 'create', 'upload' ) );
2502          }
2503          return $types;
2504      }
2505  
2506      /**
2507       * Returns restriction types for the current Title
2508       *
2509       * @return array Applicable restriction types
2510       */
2511  	public function getRestrictionTypes() {
2512          if ( $this->isSpecialPage() ) {
2513              return array();
2514          }
2515  
2516          $types = self::getFilteredRestrictionTypes( $this->exists() );
2517  
2518          if ( $this->getNamespace() != NS_FILE ) {
2519              # Remove the upload restriction for non-file titles
2520              $types = array_diff( $types, array( 'upload' ) );
2521          }
2522  
2523          wfRunHooks( 'TitleGetRestrictionTypes', array( $this, &$types ) );
2524  
2525          wfDebug( __METHOD__ . ': applicable restrictions to [[' .
2526              $this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" );
2527  
2528          return $types;
2529      }
2530  
2531      /**
2532       * Is this title subject to title protection?
2533       * Title protection is the one applied against creation of such title.
2534       *
2535       * @return array|bool An associative array representing any existent title
2536       *   protection, or false if there's none.
2537       */
2538  	private function getTitleProtection() {
2539          // Can't protect pages in special namespaces
2540          if ( $this->getNamespace() < 0 ) {
2541              return false;
2542          }
2543  
2544          // Can't protect pages that exist.
2545          if ( $this->exists() ) {
2546              return false;
2547          }
2548  
2549          if ( $this->mTitleProtection === null ) {
2550              $dbr = wfGetDB( DB_SLAVE );
2551              $res = $dbr->select(
2552                  'protected_titles',
2553                  array( 'pt_user', 'pt_reason', 'pt_expiry', 'pt_create_perm' ),
2554                  array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ),
2555                  __METHOD__
2556              );
2557  
2558              // fetchRow returns false if there are no rows.
2559              $this->mTitleProtection = $dbr->fetchRow( $res );
2560          }
2561          return $this->mTitleProtection;
2562      }
2563  
2564      /**
2565       * Remove any title protection due to page existing
2566       */
2567  	public function deleteTitleProtection() {
2568          $dbw = wfGetDB( DB_MASTER );
2569  
2570          $dbw->delete(
2571              'protected_titles',
2572              array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ),
2573              __METHOD__
2574          );
2575          $this->mTitleProtection = false;
2576      }
2577  
2578      /**
2579       * Is this page "semi-protected" - the *only* protection levels are listed
2580       * in $wgSemiprotectedRestrictionLevels?
2581       *
2582       * @param string $action Action to check (default: edit)
2583       * @return bool
2584       */
2585  	public function isSemiProtected( $action = 'edit' ) {
2586          global $wgSemiprotectedRestrictionLevels;
2587  
2588          $restrictions = $this->getRestrictions( $action );
2589          $semi = $wgSemiprotectedRestrictionLevels;
2590          if ( !$restrictions || !$semi ) {
2591              // Not protected, or all protection is full protection
2592              return false;
2593          }
2594  
2595          // Remap autoconfirmed to editsemiprotected for BC
2596          foreach ( array_keys( $semi, 'autoconfirmed' ) as $key ) {
2597              $semi[$key] = 'editsemiprotected';
2598          }
2599          foreach ( array_keys( $restrictions, 'autoconfirmed' ) as $key ) {
2600              $restrictions[$key] = 'editsemiprotected';
2601          }
2602  
2603          return !array_diff( $restrictions, $semi );
2604      }
2605  
2606      /**
2607       * Does the title correspond to a protected article?
2608       *
2609       * @param string $action The action the page is protected from,
2610       * by default checks all actions.
2611       * @return bool
2612       */
2613  	public function isProtected( $action = '' ) {
2614          global $wgRestrictionLevels;
2615  
2616          $restrictionTypes = $this->getRestrictionTypes();
2617  
2618          # Special pages have inherent protection
2619          if ( $this->isSpecialPage() ) {
2620              return true;
2621          }
2622  
2623          # Check regular protection levels
2624          foreach ( $restrictionTypes as $type ) {
2625              if ( $action == $type || $action == '' ) {
2626                  $r = $this->getRestrictions( $type );
2627                  foreach ( $wgRestrictionLevels as $level ) {
2628                      if ( in_array( $level, $r ) && $level != '' ) {
2629                          return true;
2630                      }
2631                  }
2632              }
2633          }
2634  
2635          return false;
2636      }
2637  
2638      /**
2639       * Determines if $user is unable to edit this page because it has been protected
2640       * by $wgNamespaceProtection.
2641       *
2642       * @param User $user User object to check permissions
2643       * @return bool
2644       */
2645  	public function isNamespaceProtected( User $user ) {
2646          global $wgNamespaceProtection;
2647  
2648          if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) {
2649              foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) {
2650                  if ( $right != '' && !$user->isAllowed( $right ) ) {
2651                      return true;
2652                  }
2653              }
2654          }
2655          return false;
2656      }
2657  
2658      /**
2659       * Cascading protection: Return true if cascading restrictions apply to this page, false if not.
2660       *
2661       * @return bool If the page is subject to cascading restrictions.
2662       */
2663  	public function isCascadeProtected() {
2664          list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false );
2665          return ( $sources > 0 );
2666      }
2667  
2668      /**
2669       * Determines whether cascading protection sources have already been loaded from
2670       * the database.
2671       *
2672       * @param bool $getPages True to check if the pages are loaded, or false to check
2673       * if the status is loaded.
2674       * @return bool Whether or not the specified information has been loaded
2675       * @since 1.23
2676       */
2677  	public function areCascadeProtectionSourcesLoaded( $getPages = true ) {
2678          return $getPages ? $this->mCascadeSources !== null : $this->mHasCascadingRestrictions !== null;
2679      }
2680  
2681      /**
2682       * Cascading protection: Get the source of any cascading restrictions on this page.
2683       *
2684       * @param bool $getPages Whether or not to retrieve the actual pages
2685       *        that the restrictions have come from and the actual restrictions
2686       *        themselves.
2687       * @return array Two elements: First is an array of Title objects of the
2688       *        pages from which cascading restrictions have come, false for
2689       *        none, or true if such restrictions exist but $getPages was not
2690       *        set. Second is an array like that returned by
2691       *        Title::getAllRestrictions(), or an empty array if $getPages is
2692       *        false.
2693       */
2694  	public function getCascadeProtectionSources( $getPages = true ) {
2695          global $wgContLang;
2696          $pagerestrictions = array();
2697  
2698          if ( $this->mCascadeSources !== null && $getPages ) {
2699              return array( $this->mCascadeSources, $this->mCascadingRestrictions );
2700          } elseif ( $this->mHasCascadingRestrictions !== null && !$getPages ) {
2701              return array( $this->mHasCascadingRestrictions, $pagerestrictions );
2702          }
2703  
2704          wfProfileIn( __METHOD__ );
2705  
2706          $dbr = wfGetDB( DB_SLAVE );
2707  
2708          if ( $this->getNamespace() == NS_FILE ) {
2709              $tables = array( 'imagelinks', 'page_restrictions' );
2710              $where_clauses = array(
2711                  'il_to' => $this->getDBkey(),
2712                  'il_from=pr_page',
2713                  'pr_cascade' => 1
2714              );
2715          } else {
2716              $tables = array( 'templatelinks', 'page_restrictions' );
2717              $where_clauses = array(
2718                  'tl_namespace' => $this->getNamespace(),
2719                  'tl_title' => $this->getDBkey(),
2720                  'tl_from=pr_page',
2721                  'pr_cascade' => 1
2722              );
2723          }
2724  
2725          if ( $getPages ) {
2726              $cols = array( 'pr_page', 'page_namespace', 'page_title',
2727                  'pr_expiry', 'pr_type', 'pr_level' );
2728              $where_clauses[] = 'page_id=pr_page';
2729              $tables[] = 'page';
2730          } else {
2731              $cols = array( 'pr_expiry' );
2732          }
2733  
2734          $res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ );
2735  
2736          $sources = $getPages ? array() : false;
2737          $now = wfTimestampNow();
2738          $purgeExpired = false;
2739  
2740          foreach ( $res as $row ) {
2741              $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW );
2742              if ( $expiry > $now ) {
2743                  if ( $getPages ) {
2744                      $page_id = $row->pr_page;
2745                      $page_ns = $row->page_namespace;
2746                      $page_title = $row->page_title;
2747                      $sources[$page_id] = Title::makeTitle( $page_ns, $page_title );
2748                      # Add groups needed for each restriction type if its not already there
2749                      # Make sure this restriction type still exists
2750  
2751                      if ( !isset( $pagerestrictions[$row->pr_type] ) ) {
2752                          $pagerestrictions[$row->pr_type] = array();
2753                      }
2754  
2755                      if (
2756                          isset( $pagerestrictions[$row->pr_type] )
2757                          && !in_array( $row->pr_level, $pagerestrictions[$row->pr_type] )
2758                      ) {
2759                          $pagerestrictions[$row->pr_type][] = $row->pr_level;
2760                      }
2761                  } else {
2762                      $sources = true;
2763                  }
2764              } else {
2765                  // Trigger lazy purge of expired restrictions from the db
2766                  $purgeExpired = true;
2767              }
2768          }
2769          if ( $purgeExpired ) {
2770              Title::purgeExpiredRestrictions();
2771          }
2772  
2773          if ( $getPages ) {
2774              $this->mCascadeSources = $sources;
2775              $this->mCascadingRestrictions = $pagerestrictions;
2776          } else {
2777              $this->mHasCascadingRestrictions = $sources;
2778          }
2779  
2780          wfProfileOut( __METHOD__ );
2781          return array( $sources, $pagerestrictions );
2782      }
2783  
2784      /**
2785       * Accessor for mRestrictionsLoaded
2786       *
2787       * @return bool Whether or not the page's restrictions have already been
2788       * loaded from the database
2789       * @since 1.23
2790       */
2791  	public function areRestrictionsLoaded() {
2792          return $this->mRestrictionsLoaded;
2793      }
2794  
2795      /**
2796       * Accessor/initialisation for mRestrictions
2797       *
2798       * @param string $action Action that permission needs to be checked for
2799       * @return array Restriction levels needed to take the action. All levels
2800       *     are required.
2801       */
2802  	public function getRestrictions( $action ) {
2803          if ( !$this->mRestrictionsLoaded ) {
2804              $this->loadRestrictions();
2805          }
2806          return isset( $this->mRestrictions[$action] )
2807                  ? $this->mRestrictions[$action]
2808                  : array();
2809      }
2810  
2811      /**
2812       * Accessor/initialisation for mRestrictions
2813       *
2814       * @return array Keys are actions, values are arrays as returned by
2815       *     Title::getRestrictions()
2816       * @since 1.23
2817       */
2818  	public function getAllRestrictions() {
2819          if ( !$this->mRestrictionsLoaded ) {
2820              $this->loadRestrictions();
2821          }
2822          return $this->mRestrictions;
2823      }
2824  
2825      /**
2826       * Get the expiry time for the restriction against a given action
2827       *
2828       * @param string $action
2829       * @return string|bool 14-char timestamp, or 'infinity' if the page is protected forever
2830       *     or not protected at all, or false if the action is not recognised.
2831       */
2832  	public function getRestrictionExpiry( $action ) {
2833          if ( !$this->mRestrictionsLoaded ) {
2834              $this->loadRestrictions();
2835          }
2836          return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false;
2837      }
2838  
2839      /**
2840       * Returns cascading restrictions for the current article
2841       *
2842       * @return bool
2843       */
2844  	function areRestrictionsCascading() {
2845          if ( !$this->mRestrictionsLoaded ) {
2846              $this->loadRestrictions();
2847          }
2848  
2849          return $this->mCascadeRestriction;
2850      }
2851  
2852      /**
2853       * Loads a string into mRestrictions array
2854       *
2855       * @param ResultWrapper $res Resource restrictions as an SQL result.
2856       * @param string $oldFashionedRestrictions Comma-separated list of page
2857       *        restrictions from page table (pre 1.10)
2858       */
2859  	private function loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions = null ) {
2860          $rows = array();
2861  
2862          foreach ( $res as $row ) {
2863              $rows[] = $row;
2864          }
2865  
2866          $this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions );
2867      }
2868  
2869      /**
2870       * Compiles list of active page restrictions from both page table (pre 1.10)
2871       * and page_restrictions table for this existing page.
2872       * Public for usage by LiquidThreads.
2873       *
2874       * @param array $rows Array of db result objects
2875       * @param string $oldFashionedRestrictions Comma-separated list of page
2876       *   restrictions from page table (pre 1.10)
2877       */
2878  	public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) {
2879          global $wgContLang;
2880          $dbr = wfGetDB( DB_SLAVE );
2881  
2882          $restrictionTypes = $this->getRestrictionTypes();
2883  
2884          foreach ( $restrictionTypes as $type ) {
2885              $this->mRestrictions[$type] = array();
2886              $this->mRestrictionsExpiry[$type] = $wgContLang->formatExpiry( '', TS_MW );
2887          }
2888  
2889          $this->mCascadeRestriction = false;
2890  
2891          # Backwards-compatibility: also load the restrictions from the page record (old format).
2892  
2893          if ( $oldFashionedRestrictions === null ) {
2894              $oldFashionedRestrictions = $dbr->selectField( 'page', 'page_restrictions',
2895                  array( 'page_id' => $this->getArticleID() ), __METHOD__ );
2896          }
2897  
2898          if ( $oldFashionedRestrictions != '' ) {
2899  
2900              foreach ( explode( ':', trim( $oldFashionedRestrictions ) ) as $restrict ) {
2901                  $temp = explode( '=', trim( $restrict ) );
2902                  if ( count( $temp ) == 1 ) {
2903                      // old old format should be treated as edit/move restriction
2904                      $this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) );
2905                      $this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) );
2906                  } else {
2907                      $restriction = trim( $temp[1] );
2908                      if ( $restriction != '' ) { //some old entries are empty
2909                          $this->mRestrictions[$temp[0]] = explode( ',', $restriction );
2910                      }
2911                  }
2912              }
2913  
2914              $this->mOldRestrictions = true;
2915  
2916          }
2917  
2918          if ( count( $rows ) ) {
2919              # Current system - load second to make them override.
2920              $now = wfTimestampNow();
2921              $purgeExpired = false;
2922  
2923              # Cycle through all the restrictions.
2924              foreach ( $rows as $row ) {
2925  
2926                  // Don't take care of restrictions types that aren't allowed
2927                  if ( !in_array( $row->pr_type, $restrictionTypes ) ) {
2928                      continue;
2929                  }
2930  
2931                  // This code should be refactored, now that it's being used more generally,
2932                  // But I don't really see any harm in leaving it in Block for now -werdna
2933                  $expiry = $wgContLang->formatExpiry( $row->pr_expiry, TS_MW );
2934  
2935                  // Only apply the restrictions if they haven't expired!
2936                  if ( !$expiry || $expiry > $now ) {
2937                      $this->mRestrictionsExpiry[$row->pr_type] = $expiry;
2938                      $this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) );
2939  
2940                      $this->mCascadeRestriction |= $row->pr_cascade;
2941                  } else {
2942                      // Trigger a lazy purge of expired restrictions
2943                      $purgeExpired = true;
2944                  }
2945              }
2946  
2947              if ( $purgeExpired ) {
2948                  Title::purgeExpiredRestrictions();
2949              }
2950          }
2951  
2952          $this->mRestrictionsLoaded = true;
2953      }
2954  
2955      /**
2956       * Load restrictions from the page_restrictions table
2957       *
2958       * @param string $oldFashionedRestrictions Comma-separated list of page
2959       *   restrictions from page table (pre 1.10)
2960       */
2961  	public function loadRestrictions( $oldFashionedRestrictions = null ) {
2962          global $wgContLang;
2963          if ( !$this->mRestrictionsLoaded ) {
2964              if ( $this->exists() ) {
2965                  $dbr = wfGetDB( DB_SLAVE );
2966  
2967                  $res = $dbr->select(
2968                      'page_restrictions',
2969                      array( 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ),
2970                      array( 'pr_page' => $this->getArticleID() ),
2971                      __METHOD__
2972                  );
2973  
2974                  $this->loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions );
2975              } else {
2976                  $title_protection = $this->getTitleProtection();
2977  
2978                  if ( $title_protection ) {
2979                      $now = wfTimestampNow();
2980                      $expiry = $wgContLang->formatExpiry( $title_protection['pt_expiry'], TS_MW );
2981  
2982                      if ( !$expiry || $expiry > $now ) {
2983                          // Apply the restrictions
2984                          $this->mRestrictionsExpiry['create'] = $expiry;
2985                          $this->mRestrictions['create'] = explode( ',', trim( $title_protection['pt_create_perm'] ) );
2986                      } else { // Get rid of the old restrictions
2987                          Title::purgeExpiredRestrictions();
2988                          $this->mTitleProtection = false;
2989                      }
2990                  } else {
2991                      $this->mRestrictionsExpiry['create'] = $wgContLang->formatExpiry( '', TS_MW );
2992                  }
2993                  $this->mRestrictionsLoaded = true;
2994              }
2995          }
2996      }
2997  
2998      /**
2999       * Flush the protection cache in this object and force reload from the database.
3000       * This is used when updating protection from WikiPage::doUpdateRestrictions().
3001       */
3002  	public function flushRestrictions() {
3003          $this->mRestrictionsLoaded = false;
3004          $this->mTitleProtection = null;
3005      }
3006  
3007      /**
3008       * Purge expired restrictions from the page_restrictions table
3009       */
3010  	static function purgeExpiredRestrictions() {
3011          if ( wfReadOnly() ) {
3012              return;
3013          }
3014  
3015          $method = __METHOD__;
3016          $dbw = wfGetDB( DB_MASTER );
3017          $dbw->onTransactionIdle( function () use ( $dbw, $method ) {
3018              $dbw->delete(
3019                  'page_restrictions',
3020                  array( 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ),
3021                  $method
3022              );
3023              $dbw->delete(
3024                  'protected_titles',
3025                  array( 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ),
3026                  $method
3027              );
3028          } );
3029      }
3030  
3031      /**
3032       * Does this have subpages?  (Warning, usually requires an extra DB query.)
3033       *
3034       * @return bool
3035       */
3036  	public function hasSubpages() {
3037          if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
3038              # Duh
3039              return false;
3040          }
3041  
3042          # We dynamically add a member variable for the purpose of this method
3043          # alone to cache the result.  There's no point in having it hanging
3044          # around uninitialized in every Title object; therefore we only add it
3045          # if needed and don't declare it statically.
3046          if ( $this->mHasSubpages === null ) {
3047              $this->mHasSubpages = false;
3048              $subpages = $this->getSubpages( 1 );
3049              if ( $subpages instanceof TitleArray ) {
3050                  $this->mHasSubpages = (bool)$subpages->count();
3051              }
3052          }
3053  
3054          return $this->mHasSubpages;
3055      }
3056  
3057      /**
3058       * Get all subpages of this page.
3059       *
3060       * @param int $limit Maximum number of subpages to fetch; -1 for no limit
3061       * @return TitleArray|array TitleArray, or empty array if this page's namespace
3062       *  doesn't allow subpages
3063       */
3064  	public function getSubpages( $limit = -1 ) {
3065          if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) {
3066              return array();
3067          }
3068  
3069          $dbr = wfGetDB( DB_SLAVE );
3070          $conds['page_namespace'] = $this->getNamespace();
3071          $conds[] = 'page_title ' . $dbr->buildLike( $this->getDBkey() . '/', $dbr->anyString() );
3072          $options = array();
3073          if ( $limit > -1 ) {
3074              $options['LIMIT'] = $limit;
3075          }
3076          $this->mSubpages = TitleArray::newFromResult(
3077              $dbr->select( 'page',
3078                  array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ),
3079                  $conds,
3080                  __METHOD__,
3081                  $options
3082              )
3083          );
3084          return $this->mSubpages;
3085      }
3086  
3087      /**
3088       * Is there a version of this page in the deletion archive?
3089       *
3090       * @return int The number of archived revisions
3091       */
3092  	public function isDeleted() {
3093          if ( $this->getNamespace() < 0 ) {
3094              $n = 0;
3095          } else {
3096              $dbr = wfGetDB( DB_SLAVE );
3097  
3098              $n = $dbr->selectField( 'archive', 'COUNT(*)',
3099                  array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ),
3100                  __METHOD__
3101              );
3102              if ( $this->getNamespace() == NS_FILE ) {
3103                  $n += $dbr->selectField( 'filearchive', 'COUNT(*)',
3104                      array( 'fa_name' => $this->getDBkey() ),
3105                      __METHOD__
3106                  );
3107              }
3108          }
3109          return (int)$n;
3110      }
3111  
3112      /**
3113       * Is there a version of this page in the deletion archive?
3114       *
3115       * @return bool
3116       */
3117  	public function isDeletedQuick() {
3118          if ( $this->getNamespace() < 0 ) {
3119              return false;
3120          }
3121          $dbr = wfGetDB( DB_SLAVE );
3122          $deleted = (bool)$dbr->selectField( 'archive', '1',
3123              array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ),
3124              __METHOD__
3125          );
3126          if ( !$deleted && $this->getNamespace() == NS_FILE ) {
3127              $deleted = (bool)$dbr->selectField( 'filearchive', '1',
3128                  array( 'fa_name' => $this->getDBkey() ),
3129                  __METHOD__
3130              );
3131          }
3132          return $deleted;
3133      }
3134  
3135      /**
3136       * Get the article ID for this Title from the link cache,
3137       * adding it if necessary
3138       *
3139       * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select
3140       *  for update
3141       * @return int The ID
3142       */
3143  	public function getArticleID( $flags = 0 ) {
3144          if ( $this->getNamespace() < 0 ) {
3145              $this->mArticleID = 0;
3146              return $this->mArticleID;
3147          }
3148          $linkCache = LinkCache::singleton();
3149          if ( $flags & self::GAID_FOR_UPDATE ) {
3150              $oldUpdate = $linkCache->forUpdate( true );
3151              $linkCache->clearLink( $this );
3152              $this->mArticleID = $linkCache->addLinkObj( $this );
3153              $linkCache->forUpdate( $oldUpdate );
3154          } else {
3155              if ( -1 == $this->mArticleID ) {
3156                  $this->mArticleID = $linkCache->addLinkObj( $this );
3157              }
3158          }
3159          return $this->mArticleID;
3160      }
3161  
3162      /**
3163       * Is this an article that is a redirect page?
3164       * Uses link cache, adding it if necessary
3165       *
3166       * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
3167       * @return bool
3168       */
3169  	public function isRedirect( $flags = 0 ) {
3170          if ( !is_null( $this->mRedirect ) ) {
3171              return $this->mRedirect;
3172          }
3173          # Calling getArticleID() loads the field from cache as needed
3174          if ( !$this->getArticleID( $flags ) ) {
3175              $this->mRedirect = false;
3176              return $this->mRedirect;
3177          }
3178  
3179          $linkCache = LinkCache::singleton();
3180          $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' );
3181          if ( $cached === null ) {
3182              # Trust LinkCache's state over our own
3183              # LinkCache is telling us that the page doesn't exist, despite there being cached
3184              # data relating to an existing page in $this->mArticleID. Updaters should clear
3185              # LinkCache as appropriate, or use $flags = Title::GAID_FOR_UPDATE. If that flag is
3186              # set, then LinkCache will definitely be up to date here, since getArticleID() forces
3187              # LinkCache to refresh its data from the master.
3188              $this->mRedirect = false;
3189              return $this->mRedirect;
3190          }
3191  
3192          $this->mRedirect = (bool)$cached;
3193  
3194          return $this->mRedirect;
3195      }
3196  
3197      /**
3198       * What is the length of this page?
3199       * Uses link cache, adding it if necessary
3200       *
3201       * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
3202       * @return int
3203       */
3204  	public function getLength( $flags = 0 ) {
3205          if ( $this->mLength != -1 ) {
3206              return $this->mLength;
3207          }
3208          # Calling getArticleID() loads the field from cache as needed
3209          if ( !$this->getArticleID( $flags ) ) {
3210              $this->mLength = 0;
3211              return $this->mLength;
3212          }
3213          $linkCache = LinkCache::singleton();
3214          $cached = $linkCache->getGoodLinkFieldObj( $this, 'length' );
3215          if ( $cached === null ) {
3216              # Trust LinkCache's state over our own, as for isRedirect()
3217              $this->mLength = 0;
3218              return $this->mLength;
3219          }
3220  
3221          $this->mLength = intval( $cached );
3222  
3223          return $this->mLength;
3224      }
3225  
3226      /**
3227       * What is the page_latest field for this page?
3228       *
3229       * @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
3230       * @return int Int or 0 if the page doesn't exist
3231       */
3232  	public function getLatestRevID( $flags = 0 ) {
3233          if ( !( $flags & Title::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) {
3234              return intval( $this->mLatestID );
3235          }
3236          # Calling getArticleID() loads the field from cache as needed
3237          if ( !$this->getArticleID( $flags ) ) {
3238              $this->mLatestID = 0;
3239              return $this->mLatestID;
3240          }
3241          $linkCache = LinkCache::singleton();
3242          $linkCache->addLinkObj( $this );
3243          $cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' );
3244          if ( $cached === null ) {
3245              # Trust LinkCache's state over our own, as for isRedirect()
3246              $this->mLatestID = 0;
3247              return $this->mLatestID;
3248          }
3249  
3250          $this->mLatestID = intval( $cached );
3251  
3252          return $this->mLatestID;
3253      }
3254  
3255      /**
3256       * This clears some fields in this object, and clears any associated
3257       * keys in the "bad links" section of the link cache.
3258       *
3259       * - This is called from WikiPage::doEdit() and WikiPage::insertOn() to allow
3260       * loading of the new page_id. It's also called from
3261       * WikiPage::doDeleteArticleReal()
3262       *
3263       * @param int $newid The new Article ID
3264       */
3265  	public function resetArticleID( $newid ) {
3266          $linkCache = LinkCache::singleton();
3267          $linkCache->clearLink( $this );
3268  
3269          if ( $newid === false ) {
3270              $this->mArticleID = -1;
3271          } else {
3272              $this->mArticleID = intval( $newid );
3273          }
3274          $this->mRestrictionsLoaded = false;
3275          $this->mRestrictions = array();
3276          $this->mRedirect = null;
3277          $this->mLength = -1;
3278          $this->mLatestID = false;
3279          $this->mContentModel = false;
3280          $this->mEstimateRevisions = null;
3281          $this->mPageLanguage = false;
3282          $this->mDbPageLanguage = null;
3283          $this->mIsBigDeletion = null;
3284      }
3285  
3286      /**
3287       * Capitalize a text string for a title if it belongs to a namespace that capitalizes
3288       *
3289       * @param string $text Containing title to capitalize
3290       * @param int $ns Namespace index, defaults to NS_MAIN
3291       * @return string Containing capitalized title
3292       */
3293  	public static function capitalize( $text, $ns = NS_MAIN ) {
3294          global $wgContLang;
3295  
3296          if ( MWNamespace::isCapitalized( $ns ) ) {
3297              return $wgContLang->ucfirst( $text );
3298          } else {
3299              return $text;
3300          }
3301      }
3302  
3303      /**
3304       * Secure and split - main initialisation function for this object
3305       *
3306       * Assumes that mDbkeyform has been set, and is urldecoded
3307       * and uses underscores, but not otherwise munged.  This function
3308       * removes illegal characters, splits off the interwiki and
3309       * namespace prefixes, sets the other forms, and canonicalizes
3310       * everything.
3311       *
3312       * @return bool True on success
3313       */
3314  	private function secureAndSplit() {
3315          # Initialisation
3316          $this->mInterwiki = '';
3317          $this->mFragment = '';
3318          $this->mNamespace = $this->mDefaultNamespace; # Usually NS_MAIN
3319  
3320          $dbkey = $this->mDbkeyform;
3321  
3322          try {
3323              // @note: splitTitleString() is a temporary hack to allow MediaWikiTitleCodec to share
3324              //        the parsing code with Title, while avoiding massive refactoring.
3325              // @todo: get rid of secureAndSplit, refactor parsing code.
3326              $titleParser = self::getTitleParser();
3327              $parts = $titleParser->splitTitleString( $dbkey, $this->getDefaultNamespace() );
3328          } catch ( MalformedTitleException $ex ) {
3329              return false;
3330          }
3331  
3332          # Fill fields
3333          $this->setFragment( '#' . $parts['fragment'] );
3334          $this->mInterwiki = $parts['interwiki'];
3335          $this->mLocalInterwiki = $parts['local_interwiki'];
3336          $this->mNamespace = $parts['namespace'];
3337          $this->mUserCaseDBKey = $parts['user_case_dbkey'];
3338  
3339          $this->mDbkeyform = $parts['dbkey'];
3340          $this->mUrlform = wfUrlencode( $this->mDbkeyform );
3341          $this->mTextform = str_replace( '_', ' ', $this->mDbkeyform );
3342  
3343          # We already know that some pages won't be in the database!
3344          if ( $this->isExternal() || $this->mNamespace == NS_SPECIAL ) {
3345              $this->mArticleID = 0;
3346          }
3347  
3348          return true;
3349      }
3350  
3351      /**
3352       * Get an array of Title objects linking to this Title
3353       * Also stores the IDs in the link cache.
3354       *
3355       * WARNING: do not use this function on arbitrary user-supplied titles!
3356       * On heavily-used templates it will max out the memory.
3357       *
3358       * @param array $options May be FOR UPDATE
3359       * @param string $table Table name
3360       * @param string $prefix Fields prefix
3361       * @return Title[] Array of Title objects linking here
3362       */
3363  	public function getLinksTo( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) {
3364          if ( count( $options ) > 0 ) {
3365              $db = wfGetDB( DB_MASTER );
3366          } else {
3367              $db = wfGetDB( DB_SLAVE );
3368          }
3369  
3370          $res = $db->select(
3371              array( 'page', $table ),
3372              self::getSelectFields(),
3373              array(
3374                  "{$prefix}_from=page_id",
3375                  "{$prefix}_namespace" => $this->getNamespace(),
3376                  "{$prefix}_title" => $this->getDBkey() ),
3377              __METHOD__,
3378              $options
3379          );
3380  
3381          $retVal = array();
3382          if ( $res->numRows() ) {
3383              $linkCache = LinkCache::singleton();
3384              foreach ( $res as $row ) {
3385                  $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title );
3386                  if ( $titleObj ) {
3387                      $linkCache->addGoodLinkObjFromRow( $titleObj, $row );
3388                      $retVal[] = $titleObj;
3389                  }
3390              }
3391          }
3392          return $retVal;
3393      }
3394  
3395      /**
3396       * Get an array of Title objects using this Title as a template
3397       * Also stores the IDs in the link cache.
3398       *
3399       * WARNING: do not use this function on arbitrary user-supplied titles!
3400       * On heavily-used templates it will max out the memory.
3401       *
3402       * @param array $options May be FOR UPDATE
3403       * @return Title[] Array of Title the Title objects linking here
3404       */
3405  	public function getTemplateLinksTo( $options = array() ) {
3406          return $this->getLinksTo( $options, 'templatelinks', 'tl' );
3407      }
3408  
3409      /**
3410       * Get an array of Title objects linked from this Title
3411       * Also stores the IDs in the link cache.
3412       *
3413       * WARNING: do not use this function on arbitrary user-supplied titles!
3414       * On heavily-used templates it will max out the memory.
3415       *
3416       * @param array $options May be FOR UPDATE
3417       * @param string $table Table name
3418       * @param string $prefix Fields prefix
3419       * @return array Array of Title objects linking here
3420       */
3421  	public function getLinksFrom( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) {
3422          global $wgContentHandlerUseDB;
3423  
3424          $id = $this->getArticleID();
3425  
3426          # If the page doesn't exist; there can't be any link from this page
3427          if ( !$id ) {
3428              return array();
3429          }
3430  
3431          if ( count( $options ) > 0 ) {
3432              $db = wfGetDB( DB_MASTER );
3433          } else {
3434              $db = wfGetDB( DB_SLAVE );
3435          }
3436  
3437          $namespaceFiled = "{$prefix}_namespace";
3438          $titleField = "{$prefix}_title";
3439  
3440          $fields = array(
3441              $namespaceFiled,
3442              $titleField,
3443              'page_id',
3444              'page_len',
3445              'page_is_redirect',
3446              'page_latest'
3447          );
3448  
3449          if ( $wgContentHandlerUseDB ) {
3450              $fields[] = 'page_content_model';
3451          }
3452  
3453          $res = $db->select(
3454              array( $table, 'page' ),
3455              $fields,
3456              array( "{$prefix}_from" => $id ),
3457              __METHOD__,
3458              $options,
3459              array( 'page' => array(
3460                  'LEFT JOIN',
3461                  array( "page_namespace=$namespaceFiled", "page_title=$titleField" )
3462              ) )
3463          );
3464  
3465          $retVal = array();
3466          if ( $res->numRows() ) {
3467              $linkCache = LinkCache::singleton();
3468              foreach ( $res as $row ) {
3469                  $titleObj = Title::makeTitle( $row->$namespaceFiled, $row->$titleField );
3470                  if ( $titleObj ) {
3471                      if ( $row->page_id ) {
3472                          $linkCache->addGoodLinkObjFromRow( $titleObj, $row );
3473                      } else {
3474                          $linkCache->addBadLinkObj( $titleObj );
3475                      }
3476                      $retVal[] = $titleObj;
3477                  }
3478              }
3479          }
3480          return $retVal;
3481      }
3482  
3483      /**
3484       * Get an array of Title objects used on this Title as a template
3485       * Also stores the IDs in the link cache.
3486       *
3487       * WARNING: do not use this function on arbitrary user-supplied titles!
3488       * On heavily-used templates it will max out the memory.
3489       *
3490       * @param array $options May be FOR UPDATE
3491       * @return Title[] Array of Title the Title objects used here
3492       */
3493  	public function getTemplateLinksFrom( $options = array() ) {
3494          return $this->getLinksFrom( $options, 'templatelinks', 'tl' );
3495      }
3496  
3497      /**
3498       * Get an array of Title objects referring to non-existent articles linked
3499       * from this page.
3500       *
3501       * @todo check if needed (used only in SpecialBrokenRedirects.php, and
3502       *   should use redirect table in this case).
3503       * @return Title[] Array of Title the Title objects
3504       */
3505  	public function getBrokenLinksFrom() {
3506          if ( $this->getArticleID() == 0 ) {
3507              # All links from article ID 0 are false positives
3508              return array();
3509          }
3510  
3511          $dbr = wfGetDB( DB_SLAVE );
3512          $res = $dbr->select(
3513              array( 'page', 'pagelinks' ),
3514              array( 'pl_namespace', 'pl_title' ),
3515              array(
3516                  'pl_from' => $this->getArticleID(),
3517                  'page_namespace IS NULL'
3518              ),
3519              __METHOD__, array(),
3520              array(
3521                  'page' => array(
3522                      'LEFT JOIN',
3523                      array( 'pl_namespace=page_namespace', 'pl_title=page_title' )
3524                  )
3525              )
3526          );
3527  
3528          $retVal = array();
3529          foreach ( $res as $row ) {
3530              $retVal[] = Title::makeTitle( $row->pl_namespace, $row->pl_title );
3531          }
3532          return $retVal;
3533      }
3534  
3535      /**
3536       * Get a list of URLs to purge from the Squid cache when this
3537       * page changes
3538       *
3539       * @return string[] Array of String the URLs
3540       */
3541  	public function getSquidURLs() {
3542          $urls = array(
3543              $this->getInternalURL(),
3544              $this->getInternalURL( 'action=history' )
3545          );
3546  
3547          $pageLang = $this->getPageLanguage();
3548          if ( $pageLang->hasVariants() ) {
3549              $variants = $pageLang->getVariants();
3550              foreach ( $variants as $vCode ) {
3551                  $urls[] = $this->getInternalURL( '', $vCode );
3552              }
3553          }
3554  
3555          // If we are looking at a css/js user subpage, purge the action=raw.
3556          if ( $this->isJsSubpage() ) {
3557              $urls[] = $this->getInternalUrl( 'action=raw&ctype=text/javascript' );
3558          } elseif ( $this->isCssSubpage() ) {
3559              $urls[] = $this->getInternalUrl( 'action=raw&ctype=text/css' );
3560          }
3561  
3562          wfRunHooks( 'TitleSquidURLs', array( $this, &$urls ) );
3563          return $urls;
3564      }
3565  
3566      /**
3567       * Purge all applicable Squid URLs
3568       */
3569  	public function purgeSquid() {
3570          global $wgUseSquid;
3571          if ( $wgUseSquid ) {
3572              $urls = $this->getSquidURLs();
3573              $u = new SquidUpdate( $urls );
3574              $u->doUpdate();
3575          }
3576      }
3577  
3578      /**
3579       * Move this page without authentication
3580       *
3581       * @param Title $nt The new page Title
3582       * @return array|bool True on success, getUserPermissionsErrors()-like array on failure
3583       */
3584  	public function moveNoAuth( &$nt ) {
3585          return $this->moveTo( $nt, false );
3586      }
3587  
3588      /**
3589       * Check whether a given move operation would be valid.
3590       * Returns true if ok, or a getUserPermissionsErrors()-like array otherwise
3591       *
3592       * @todo move this into MovePage
3593       * @param Title $nt The new title
3594       * @param bool $auth Indicates whether $wgUser's permissions
3595       *  should be checked
3596       * @param string $reason Is the log summary of the move, used for spam checking
3597       * @return array|bool True on success, getUserPermissionsErrors()-like array on failure
3598       */
3599  	public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) {
3600          global $wgUser, $wgContentHandlerUseDB;
3601  
3602          $errors = array();
3603          if ( !$nt ) {
3604              // Normally we'd add this to $errors, but we'll get
3605              // lots of syntax errors if $nt is not an object
3606              return array( array( 'badtitletext' ) );
3607          }
3608          if ( $this->equals( $nt ) ) {
3609              $errors[] = array( 'selfmove' );
3610          }
3611          if ( !$this->isMovable() ) {
3612              $errors[] = array( 'immobile-source-namespace', $this->getNsText() );
3613          }
3614          if ( $nt->isExternal() ) {
3615              $errors[] = array( 'immobile-target-namespace-iw' );
3616          }
3617          if ( !$nt->isMovable() ) {
3618              $errors[] = array( 'immobile-target-namespace', $nt->getNsText() );
3619          }
3620  
3621          $oldid = $this->getArticleID();
3622          $newid = $nt->getArticleID();
3623  
3624          if ( strlen( $nt->getDBkey() ) < 1 ) {
3625              $errors[] = array( 'articleexists' );
3626          }
3627          if (
3628              ( $this->getDBkey() == '' ) ||
3629              ( !$oldid ) ||
3630              ( $nt->getDBkey() == '' )
3631          ) {
3632              $errors[] = array( 'badarticleerror' );
3633          }
3634  
3635          // Content model checks
3636          if ( !$wgContentHandlerUseDB &&
3637                  $this->getContentModel() !== $nt->getContentModel() ) {
3638              // can't move a page if that would change the page's content model
3639              $errors[] = array(
3640                  'bad-target-model',
3641                  ContentHandler::getLocalizedName( $this->getContentModel() ),
3642                  ContentHandler::getLocalizedName( $nt->getContentModel() )
3643              );
3644          }
3645  
3646          // Image-specific checks
3647          if ( $this->getNamespace() == NS_FILE ) {
3648              $errors = array_merge( $errors, $this->validateFileMoveOperation( $nt ) );
3649          }
3650  
3651          if ( $nt->getNamespace() == NS_FILE && $this->getNamespace() != NS_FILE ) {
3652              $errors[] = array( 'nonfile-cannot-move-to-file' );
3653          }
3654  
3655          if ( $auth ) {
3656              $errors = wfMergeErrorArrays( $errors,
3657                  $this->getUserPermissionsErrors( 'move', $wgUser ),
3658                  $this->getUserPermissionsErrors( 'edit', $wgUser ),
3659                  $nt->getUserPermissionsErrors( 'move-target', $wgUser ),
3660                  $nt->getUserPermissionsErrors( 'edit', $wgUser ) );
3661          }
3662  
3663          $match = EditPage::matchSummarySpamRegex( $reason );
3664          if ( $match !== false ) {
3665              // This is kind of lame, won't display nice
3666              $errors[] = array( 'spamprotectiontext' );
3667          }
3668  
3669          $err = null;
3670          if ( !wfRunHooks( 'AbortMove', array( $this, $nt, $wgUser, &$err, $reason ) ) ) {
3671              $errors[] = array( 'hookaborted', $err );
3672          }
3673  
3674          # The move is allowed only if (1) the target doesn't exist, or
3675          # (2) the target is a redirect to the source, and has no history
3676          # (so we can undo bad moves right after they're done).
3677  
3678          if ( 0 != $newid ) { # Target exists; check for validity
3679              if ( !$this->isValidMoveTarget( $nt ) ) {
3680                  $errors[] = array( 'articleexists' );
3681              }
3682          } else {
3683              $tp = $nt->getTitleProtection();
3684              $right = $tp['pt_create_perm'];
3685              if ( $right == 'sysop' ) {
3686                  $right = 'editprotected'; // B/C
3687              }
3688              if ( $right == 'autoconfirmed' ) {
3689                  $right = 'editsemiprotected'; // B/C
3690              }
3691              if ( $tp and !$wgUser->isAllowed( $right ) ) {
3692                  $errors[] = array( 'cantmove-titleprotected' );
3693              }
3694          }
3695          if ( empty( $errors ) ) {
3696              return true;
3697          }
3698          return $errors;
3699      }
3700  
3701      /**
3702       * Check if the requested move target is a valid file move target
3703       * @param Title $nt Target title
3704       * @return array List of errors
3705       */
3706  	protected function validateFileMoveOperation( $nt ) {
3707          global $wgUser;
3708  
3709          $errors = array();
3710  
3711          // wfFindFile( $nt ) / wfLocalFile( $nt ) is not allowed until below
3712  
3713          $file = wfLocalFile( $this );
3714          if ( $file->exists() ) {
3715              if ( $nt->getText() != wfStripIllegalFilenameChars( $nt->getText() ) ) {
3716                  $errors[] = array( 'imageinvalidfilename' );
3717              }
3718              if ( !File::checkExtensionCompatibility( $file, $nt->getDBkey() ) ) {
3719                  $errors[] = array( 'imagetypemismatch' );
3720              }
3721          }
3722  
3723          if ( $nt->getNamespace() != NS_FILE ) {
3724              $errors[] = array( 'imagenocrossnamespace' );
3725              // From here we want to do checks on a file object, so if we can't
3726              // create one, we must return.
3727              return $errors;
3728          }
3729  
3730          // wfFindFile( $nt ) / wfLocalFile( $nt ) is allowed below here
3731  
3732          $destFile = wfLocalFile( $nt );
3733          if ( !$wgUser->isAllowed( 'reupload-shared' ) && !$destFile->exists() && wfFindFile( $nt ) ) {
3734              $errors[] = array( 'file-exists-sharedrepo' );
3735          }
3736  
3737          return $errors;
3738      }
3739  
3740      /**
3741       * Move a title to a new location
3742       *
3743       * @todo Deprecate this in favor of MovePage
3744       * @param Title $nt The new title
3745       * @param bool $auth Indicates whether $wgUser's permissions
3746       *  should be checked
3747       * @param string $reason The reason for the move
3748       * @param bool $createRedirect Whether to create a redirect from the old title to the new title.
3749       *  Ignored if the user doesn't have the suppressredirect right.
3750       * @return array|bool True on success, getUserPermissionsErrors()-like array on failure
3751       */
3752  	public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) {
3753          global $wgUser;
3754          $err = $this->isValidMoveOperation( $nt, $auth, $reason );
3755          if ( is_array( $err ) ) {
3756              // Auto-block user's IP if the account was "hard" blocked
3757              $wgUser->spreadAnyEditBlock();
3758              return $err;
3759          }
3760          // Check suppressredirect permission
3761          if ( $auth && !$wgUser->isAllowed( 'suppressredirect' ) ) {
3762              $createRedirect = true;
3763          }
3764  
3765          wfRunHooks( 'TitleMove', array( $this, $nt, $wgUser ) );
3766  
3767          $mp = new MovePage( $this, $nt );
3768          $status = $mp->move( $wgUser, $reason, $createRedirect );
3769          if ( $status->isOK() ) {
3770              return true;
3771          } else {
3772              return $status->getErrorsArray();
3773          }
3774      }
3775  
3776      /**
3777       * Move this page's subpages to be subpages of $nt
3778       *
3779       * @param Title $nt Move target
3780       * @param bool $auth Whether $wgUser's permissions should be checked
3781       * @param string $reason The reason for the move
3782       * @param bool $createRedirect Whether to create redirects from the old subpages to
3783       *     the new ones Ignored if the user doesn't have the 'suppressredirect' right
3784       * @return array Array with old page titles as keys, and strings (new page titles) or
3785       *     arrays (errors) as values, or an error array with numeric indices if no pages
3786       *     were moved
3787       */
3788  	public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true ) {
3789          global $wgMaximumMovedPages;
3790          // Check permissions
3791          if ( !$this->userCan( 'move-subpages' ) ) {
3792              return array( 'cant-move-subpages' );
3793          }
3794          // Do the source and target namespaces support subpages?
3795          if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) {
3796              return array( 'namespace-nosubpages',
3797                  MWNamespace::getCanonicalName( $this->getNamespace() ) );
3798          }
3799          if ( !MWNamespace::hasSubpages( $nt->getNamespace() ) ) {
3800              return array( 'namespace-nosubpages',
3801                  MWNamespace::getCanonicalName( $nt->getNamespace() ) );
3802          }
3803  
3804          $subpages = $this->getSubpages( $wgMaximumMovedPages + 1 );
3805          $retval = array();
3806          $count = 0;
3807          foreach ( $subpages as $oldSubpage ) {
3808              $count++;
3809              if ( $count > $wgMaximumMovedPages ) {
3810                  $retval[$oldSubpage->getPrefixedText()] =
3811                          array( 'movepage-max-pages',
3812                              $wgMaximumMovedPages );
3813                  break;
3814              }
3815  
3816              // We don't know whether this function was called before
3817              // or after moving the root page, so check both
3818              // $this and $nt
3819              if ( $oldSubpage->getArticleID() == $this->getArticleID()
3820                  || $oldSubpage->getArticleID() == $nt->getArticleID()
3821              ) {
3822                  // When moving a page to a subpage of itself,
3823                  // don't move it twice
3824                  continue;
3825              }
3826              $newPageName = preg_replace(
3827                      '#^' . preg_quote( $this->getDBkey(), '#' ) . '#',
3828                      StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # bug 21234
3829                      $oldSubpage->getDBkey() );
3830              if ( $oldSubpage->isTalkPage() ) {
3831                  $newNs = $nt->getTalkPage()->getNamespace();
3832              } else {
3833                  $newNs = $nt->getSubjectPage()->getNamespace();
3834              }
3835              # Bug 14385: we need makeTitleSafe because the new page names may
3836              # be longer than 255 characters.
3837              $newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
3838  
3839              $success = $oldSubpage->moveTo( $newSubpage, $auth, $reason, $createRedirect );
3840              if ( $success === true ) {
3841                  $retval[$oldSubpage->getPrefixedText()] = $newSubpage->getPrefixedText();
3842              } else {
3843                  $retval[$oldSubpage->getPrefixedText()] = $success;
3844              }
3845          }
3846          return $retval;
3847      }
3848  
3849      /**
3850       * Checks if this page is just a one-rev redirect.
3851       * Adds lock, so don't use just for light purposes.
3852       *
3853       * @return bool
3854       */
3855  	public function isSingleRevRedirect() {
3856          global $wgContentHandlerUseDB;
3857  
3858          $dbw = wfGetDB( DB_MASTER );
3859  
3860          # Is it a redirect?
3861          $fields = array( 'page_is_redirect', 'page_latest', 'page_id' );
3862          if ( $wgContentHandlerUseDB ) {
3863              $fields[] = 'page_content_model';
3864          }
3865  
3866          $row = $dbw->selectRow( 'page',
3867              $fields,
3868              $this->pageCond(),
3869              __METHOD__,
3870              array( 'FOR UPDATE' )
3871          );
3872          # Cache some fields we may want
3873          $this->mArticleID = $row ? intval( $row->page_id ) : 0;
3874          $this->mRedirect = $row ? (bool)$row->page_is_redirect : false;
3875          $this->mLatestID = $row ? intval( $row->page_latest ) : false;
3876          $this->mContentModel = $row && isset( $row->page_content_model )
3877              ? strval( $row->page_content_model )
3878              : false;
3879  
3880          if ( !$this->mRedirect ) {
3881              return false;
3882          }
3883          # Does the article have a history?
3884          $row = $dbw->selectField( array( 'page', 'revision' ),
3885              'rev_id',
3886              array( 'page_namespace' => $this->getNamespace(),
3887                  'page_title' => $this->getDBkey(),
3888                  'page_id=rev_page',
3889                  'page_latest != rev_id'
3890              ),
3891              __METHOD__,
3892              array( 'FOR UPDATE' )
3893          );
3894          # Return true if there was no history
3895          return ( $row === false );
3896      }
3897  
3898      /**
3899       * Checks if $this can be moved to a given Title
3900       * - Selects for update, so don't call it unless you mean business
3901       *
3902       * @param Title $nt The new title to check
3903       * @return bool
3904       */
3905  	public function isValidMoveTarget( $nt ) {
3906          # Is it an existing file?
3907          if ( $nt->getNamespace() == NS_FILE ) {
3908              $file = wfLocalFile( $nt );
3909              if ( $file->exists() ) {
3910                  wfDebug( __METHOD__ . ": file exists\n" );
3911                  return false;
3912              }
3913          }
3914          # Is it a redirect with no history?
3915          if ( !$nt->isSingleRevRedirect() ) {
3916              wfDebug( __METHOD__ . ": not a one-rev redirect\n" );
3917              return false;
3918          }
3919          # Get the article text
3920          $rev = Revision::newFromTitle( $nt, false, Revision::READ_LATEST );
3921          if ( !is_object( $rev ) ) {
3922              return false;
3923          }
3924          $content = $rev->getContent();
3925          # Does the redirect point to the source?
3926          # Or is it a broken self-redirect, usually caused by namespace collisions?
3927          $redirTitle = $content ? $content->getRedirectTarget() : null;
3928  
3929          if ( $redirTitle ) {
3930              if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
3931                  $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) {
3932                  wfDebug( __METHOD__ . ": redirect points to other page\n" );
3933                  return false;
3934              } else {
3935                  return true;
3936              }
3937          } else {
3938              # Fail safe (not a redirect after all. strange.)
3939              wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() .
3940                          " is a redirect, but it doesn't contain a valid redirect.\n" );
3941              return false;
3942          }
3943      }
3944  
3945      /**
3946       * Get categories to which this Title belongs and return an array of
3947       * categories' names.
3948       *
3949       * @return array Array of parents in the form:
3950       *      $parent => $currentarticle
3951       */
3952  	public function getParentCategories() {
3953          global $wgContLang;
3954  
3955          $data = array();
3956  
3957          $titleKey = $this->getArticleID();
3958  
3959          if ( $titleKey === 0 ) {
3960              return $data;
3961          }
3962  
3963          $dbr = wfGetDB( DB_SLAVE );
3964  
3965          $res = $dbr->select(
3966              'categorylinks',
3967              'cl_to',
3968              array( 'cl_from' => $titleKey ),
3969              __METHOD__
3970          );
3971  
3972          if ( $res->numRows() > 0 ) {
3973              foreach ( $res as $row ) {
3974                  // $data[] = Title::newFromText($wgContLang->getNsText ( NS_CATEGORY ).':'.$row->cl_to);
3975                  $data[$wgContLang->getNsText( NS_CATEGORY ) . ':' . $row->cl_to] = $this->getFullText();
3976              }
3977          }
3978          return $data;
3979      }
3980  
3981      /**
3982       * Get a tree of parent categories
3983       *
3984       * @param array $children Array with the children in the keys, to check for circular refs
3985       * @return array Tree of parent categories
3986       */
3987  	public function getParentCategoryTree( $children = array() ) {
3988          $stack = array();
3989          $parents = $this->getParentCategories();
3990  
3991          if ( $parents ) {
3992              foreach ( $parents as $parent => $current ) {
3993                  if ( array_key_exists( $parent, $children ) ) {
3994                      # Circular reference
3995                      $stack[$parent] = array();
3996                  } else {
3997                      $nt = Title::newFromText( $parent );
3998                      if ( $nt ) {
3999                          $stack[$parent] = $nt->getParentCategoryTree( $children + array( $parent => 1 ) );
4000                      }
4001                  }
4002              }
4003          }
4004  
4005          return $stack;
4006      }
4007  
4008      /**
4009       * Get an associative array for selecting this title from
4010       * the "page" table
4011       *
4012       * @return array Array suitable for the $where parameter of DB::select()
4013       */
4014  	public function pageCond() {
4015          if ( $this->mArticleID > 0 ) {
4016              // PK avoids secondary lookups in InnoDB, shouldn't hurt other DBs
4017              return array( 'page_id' => $this->mArticleID );
4018          } else {
4019              return array( 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform );
4020          }
4021      }
4022  
4023      /**
4024       * Get the revision ID of the previous revision
4025       *
4026       * @param int $revId Revision ID. Get the revision that was before this one.
4027       * @param int $flags Title::GAID_FOR_UPDATE
4028       * @return int|bool Old revision ID, or false if none exists
4029       */
4030  	public function getPreviousRevisionID( $revId, $flags = 0 ) {
4031          $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
4032          $revId = $db->selectField( 'revision', 'rev_id',
4033              array(
4034                  'rev_page' => $this->getArticleID( $flags ),
4035                  'rev_id < ' . intval( $revId )
4036              ),
4037              __METHOD__,
4038              array( 'ORDER BY' => 'rev_id DESC' )
4039          );
4040  
4041          if ( $revId === false ) {
4042              return false;
4043          } else {
4044              return intval( $revId );
4045          }
4046      }
4047  
4048      /**
4049       * Get the revision ID of the next revision
4050       *
4051       * @param int $revId Revision ID. Get the revision that was after this one.
4052       * @param int $flags Title::GAID_FOR_UPDATE
4053       * @return int|bool Next revision ID, or false if none exists
4054       */
4055  	public function getNextRevisionID( $revId, $flags = 0 ) {
4056          $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
4057          $revId = $db->selectField( 'revision', 'rev_id',
4058              array(
4059                  'rev_page' => $this->getArticleID( $flags ),
4060                  'rev_id > ' . intval( $revId )
4061              ),
4062              __METHOD__,
4063              array( 'ORDER BY' => 'rev_id' )
4064          );
4065  
4066          if ( $revId === false ) {
4067              return false;
4068          } else {
4069              return intval( $revId );
4070          }
4071      }
4072  
4073      /**
4074       * Get the first revision of the page
4075       *
4076       * @param int $flags Title::GAID_FOR_UPDATE
4077       * @return Revision|null If page doesn't exist
4078       */
4079  	public function getFirstRevision( $flags = 0 ) {
4080          $pageId = $this->getArticleID( $flags );
4081          if ( $pageId ) {
4082              $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
4083              $row = $db->selectRow( 'revision', Revision::selectFields(),
4084                  array( 'rev_page' => $pageId ),
4085                  __METHOD__,
4086                  array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 )
4087              );
4088              if ( $row ) {
4089                  return new Revision( $row );
4090              }
4091          }
4092          return null;
4093      }
4094  
4095      /**
4096       * Get the oldest revision timestamp of this page
4097       *
4098       * @param int $flags Title::GAID_FOR_UPDATE
4099       * @return string MW timestamp
4100       */
4101  	public function getEarliestRevTime( $flags = 0 ) {
4102          $rev = $this->getFirstRevision( $flags );
4103          return $rev ? $rev->getTimestamp() : null;
4104      }
4105  
4106      /**
4107       * Check if this is a new page
4108       *
4109       * @return bool
4110       */
4111  	public function isNewPage() {
4112          $dbr = wfGetDB( DB_SLAVE );
4113          return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ );
4114      }
4115  
4116      /**
4117       * Check whether the number of revisions of this page surpasses $wgDeleteRevisionsLimit
4118       *
4119       * @return bool
4120       */
4121  	public function isBigDeletion() {
4122          global $wgDeleteRevisionsLimit;
4123  
4124          if ( !$wgDeleteRevisionsLimit ) {
4125              return false;
4126          }
4127  
4128          if ( $this->mIsBigDeletion === null ) {
4129              $dbr = wfGetDB( DB_SLAVE );
4130  
4131              $innerQuery = $dbr->selectSQLText(
4132                  'revision',
4133                  '1',
4134                  array( 'rev_page' => $this->getArticleID() ),
4135                  __METHOD__,
4136                  array( 'LIMIT' => $wgDeleteRevisionsLimit + 1 )
4137              );
4138  
4139              $revCount = $dbr->query(
4140                  'SELECT COUNT(*) FROM (' . $innerQuery . ') AS innerQuery',
4141                  __METHOD__
4142              );
4143              $revCount = $revCount->fetchRow();
4144              $revCount = $revCount['COUNT(*)'];
4145  
4146              $this->mIsBigDeletion = $revCount > $wgDeleteRevisionsLimit;
4147          }
4148  
4149          return $this->mIsBigDeletion;
4150      }
4151  
4152      /**
4153       * Get the approximate revision count of this page.
4154       *
4155       * @return int
4156       */
4157  	public function estimateRevisionCount() {
4158          if ( !$this->exists() ) {
4159              return 0;
4160          }
4161  
4162          if ( $this->mEstimateRevisions === null ) {
4163              $dbr = wfGetDB( DB_SLAVE );
4164              $this->mEstimateRevisions = $dbr->estimateRowCount( 'revision', '*',
4165                  array( 'rev_page' => $this->getArticleID() ), __METHOD__ );
4166          }
4167  
4168          return $this->mEstimateRevisions;
4169      }
4170  
4171      /**
4172       * Get the number of revisions between the given revision.
4173       * Used for diffs and other things that really need it.
4174       *
4175       * @param int|Revision $old Old revision or rev ID (first before range)
4176       * @param int|Revision $new New revision or rev ID (first after range)
4177       * @param int|null $max Limit of Revisions to count, will be incremented to detect truncations
4178       * @return int Number of revisions between these revisions.
4179       */
4180  	public function countRevisionsBetween( $old, $new, $max = null ) {
4181          if ( !( $old instanceof Revision ) ) {
4182              $old = Revision::newFromTitle( $this, (int)$old );
4183          }
4184          if ( !( $new instanceof Revision ) ) {
4185              $new = Revision::newFromTitle( $this, (int)$new );
4186          }
4187          if ( !$old || !$new ) {
4188              return 0; // nothing to compare
4189          }
4190          $dbr = wfGetDB( DB_SLAVE );
4191          $conds = array(
4192              'rev_page' => $this->getArticleID(),
4193              'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
4194              'rev_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
4195          );
4196          if ( $max !== null ) {
4197              $res = $dbr->select( 'revision', '1',
4198                  $conds,
4199                  __METHOD__,
4200                  array( 'LIMIT' => $max + 1 ) // extra to detect truncation
4201              );
4202              return $res->numRows();
4203          } else {
4204              return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ );
4205          }
4206      }
4207  
4208      /**
4209       * Get the authors between the given revisions or revision IDs.
4210       * Used for diffs and other things that really need it.
4211       *
4212       * @since 1.23
4213       *
4214       * @param int|Revision $old Old revision or rev ID (first before range by default)
4215       * @param int|Revision $new New revision or rev ID (first after range by default)
4216       * @param int $limit Maximum number of authors
4217       * @param string|array $options (Optional): Single option, or an array of options:
4218       *     'include_old' Include $old in the range; $new is excluded.
4219       *     'include_new' Include $new in the range; $old is excluded.
4220       *     'include_both' Include both $old and $new in the range.
4221       *     Unknown option values are ignored.
4222       * @return array|null Names of revision authors in the range; null if not both revisions exist
4223       */
4224  	public function getAuthorsBetween( $old, $new, $limit, $options = array() ) {
4225          if ( !( $old instanceof Revision ) ) {
4226              $old = Revision::newFromTitle( $this, (int)$old );
4227          }
4228          if ( !( $new instanceof Revision ) ) {
4229              $new = Revision::newFromTitle( $this, (int)$new );
4230          }
4231          // XXX: what if Revision objects are passed in, but they don't refer to this title?
4232          // Add $old->getPage() != $new->getPage() || $old->getPage() != $this->getArticleID()
4233          // in the sanity check below?
4234          if ( !$old || !$new ) {
4235              return null; // nothing to compare
4236          }
4237          $authors = array();
4238          $old_cmp = '>';
4239          $new_cmp = '<';
4240          $options = (array)$options;
4241          if ( in_array( 'include_old', $options ) ) {
4242              $old_cmp = '>=';
4243          }
4244          if ( in_array( 'include_new', $options ) ) {
4245              $new_cmp = '<=';
4246          }
4247          if ( in_array( 'include_both', $options ) ) {
4248              $old_cmp = '>=';
4249              $new_cmp = '<=';
4250          }
4251          // No DB query needed if $old and $new are the same or successive revisions:
4252          if ( $old->getId() === $new->getId() ) {
4253              return ( $old_cmp === '>' && $new_cmp === '<' ) ? array() : array( $old->getRawUserText() );
4254          } elseif ( $old->getId() === $new->getParentId() ) {
4255              if ( $old_cmp === '>=' && $new_cmp === '<=' ) {
4256                  $authors[] = $old->getRawUserText();
4257                  if ( $old->getRawUserText() != $new->getRawUserText() ) {
4258                      $authors[] = $new->getRawUserText();
4259                  }
4260              } elseif ( $old_cmp === '>=' ) {
4261                  $authors[] = $old->getRawUserText();
4262              } elseif ( $new_cmp === '<=' ) {
4263                  $authors[] = $new->getRawUserText();
4264              }
4265              return $authors;
4266          }
4267          $dbr = wfGetDB( DB_SLAVE );
4268          $res = $dbr->select( 'revision', 'DISTINCT rev_user_text',
4269              array(
4270                  'rev_page' => $this->getArticleID(),
4271                  "rev_timestamp $old_cmp " . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
4272                  "rev_timestamp $new_cmp " . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
4273              ), __METHOD__,
4274              array( 'LIMIT' => $limit + 1 ) // add one so caller knows it was truncated
4275          );
4276          foreach ( $res as $row ) {
4277              $authors[] = $row->rev_user_text;
4278          }
4279          return $authors;
4280      }
4281  
4282      /**
4283       * Get the number of authors between the given revisions or revision IDs.
4284       * Used for diffs and other things that really need it.
4285       *
4286       * @param int|Revision $old Old revision or rev ID (first before range by default)
4287       * @param int|Revision $new New revision or rev ID (first after range by default)
4288       * @param int $limit Maximum number of authors
4289       * @param string|array $options (Optional): Single option, or an array of options:
4290       *     'include_old' Include $old in the range; $new is excluded.
4291       *     'include_new' Include $new in the range; $old is excluded.
4292       *     'include_both' Include both $old and $new in the range.
4293       *     Unknown option values are ignored.
4294       * @return int Number of revision authors in the range; zero if not both revisions exist
4295       */
4296  	public function countAuthorsBetween( $old, $new, $limit, $options = array() ) {
4297          $authors = $this->getAuthorsBetween( $old, $new, $limit, $options );
4298          return $authors ? count( $authors ) : 0;
4299      }
4300  
4301      /**
4302       * Compare with another title.
4303       *
4304       * @param Title $title
4305       * @return bool
4306       */
4307  	public function equals( Title $title ) {
4308          // Note: === is necessary for proper matching of number-like titles.
4309          return $this->getInterwiki() === $title->getInterwiki()
4310              && $this->getNamespace() == $title->getNamespace()
4311              && $this->getDBkey() === $title->getDBkey();
4312      }
4313  
4314      /**
4315       * Check if this title is a subpage of another title
4316       *
4317       * @param Title $title
4318       * @return bool
4319       */
4320  	public function isSubpageOf( Title $title ) {
4321          return $this->getInterwiki() === $title->getInterwiki()
4322              && $this->getNamespace() == $title->getNamespace()
4323              && strpos( $this->getDBkey(), $title->getDBkey() . '/' ) === 0;
4324      }
4325  
4326      /**
4327       * Check if page exists.  For historical reasons, this function simply
4328       * checks for the existence of the title in the page table, and will
4329       * thus return false for interwiki links, special pages and the like.
4330       * If you want to know if a title can be meaningfully viewed, you should
4331       * probably call the isKnown() method instead.
4332       *
4333       * @return bool
4334       */
4335  	public function exists() {
4336          $exists = $this->getArticleID() != 0;
4337          wfRunHooks( 'TitleExists', array( $this, &$exists ) );
4338          return $exists;
4339      }
4340  
4341      /**
4342       * Should links to this title be shown as potentially viewable (i.e. as
4343       * "bluelinks"), even if there's no record by this title in the page
4344       * table?
4345       *
4346       * This function is semi-deprecated for public use, as well as somewhat
4347       * misleadingly named.  You probably just want to call isKnown(), which
4348       * calls this function internally.
4349       *
4350       * (ISSUE: Most of these checks are cheap, but the file existence check
4351       * can potentially be quite expensive.  Including it here fixes a lot of
4352       * existing code, but we might want to add an optional parameter to skip
4353       * it and any other expensive checks.)
4354       *
4355       * @return bool
4356       */
4357  	public function isAlwaysKnown() {
4358          $isKnown = null;
4359  
4360          /**
4361           * Allows overriding default behavior for determining if a page exists.
4362           * If $isKnown is kept as null, regular checks happen. If it's
4363           * a boolean, this value is returned by the isKnown method.
4364           *
4365           * @since 1.20
4366           *
4367           * @param Title $title
4368           * @param bool|null $isKnown
4369           */
4370          wfRunHooks( 'TitleIsAlwaysKnown', array( $this, &$isKnown ) );
4371  
4372          if ( !is_null( $isKnown ) ) {
4373              return $isKnown;
4374          }
4375  
4376          if ( $this->isExternal() ) {
4377              return true;  // any interwiki link might be viewable, for all we know
4378          }
4379  
4380          switch ( $this->mNamespace ) {
4381              case NS_MEDIA:
4382              case NS_FILE:
4383                  // file exists, possibly in a foreign repo
4384                  return (bool)wfFindFile( $this );
4385              case NS_SPECIAL:
4386                  // valid special page
4387                  return SpecialPageFactory::exists( $this->getDBkey() );
4388              case NS_MAIN:
4389                  // selflink, possibly with fragment
4390                  return $this->mDbkeyform == '';
4391              case NS_MEDIAWIKI:
4392                  // known system message
4393                  return $this->hasSourceText() !== false;
4394              default:
4395                  return false;
4396          }
4397      }
4398  
4399      /**
4400       * Does this title refer to a page that can (or might) be meaningfully
4401       * viewed?  In particular, this function may be used to determine if
4402       * links to the title should be rendered as "bluelinks" (as opposed to
4403       * "redlinks" to non-existent pages).
4404       * Adding something else to this function will cause inconsistency
4405       * since LinkHolderArray calls isAlwaysKnown() and does its own
4406       * page existence check.
4407       *
4408       * @return bool
4409       */
4410  	public function isKnown() {
4411          return $this->isAlwaysKnown() || $this->exists();
4412      }
4413  
4414      /**
4415       * Does this page have source text?
4416       *
4417       * @return bool
4418       */
4419  	public function hasSourceText() {
4420          if ( $this->exists() ) {
4421              return true;
4422          }
4423  
4424          if ( $this->mNamespace == NS_MEDIAWIKI ) {
4425              // If the page doesn't exist but is a known system message, default
4426              // message content will be displayed, same for language subpages-
4427              // Use always content language to avoid loading hundreds of languages
4428              // to get the link color.
4429              global $wgContLang;
4430              list( $name, ) = MessageCache::singleton()->figureMessage(
4431                  $wgContLang->lcfirst( $this->getText() )
4432              );
4433              $message = wfMessage( $name )->inLanguage( $wgContLang )->useDatabase( false );
4434              return $message->exists();
4435          }
4436  
4437          return false;
4438      }
4439  
4440      /**
4441       * Get the default message text or false if the message doesn't exist
4442       *
4443       * @return string|bool
4444       */
4445  	public function getDefaultMessageText() {
4446          global $wgContLang;
4447  
4448          if ( $this->getNamespace() != NS_MEDIAWIKI ) { // Just in case
4449              return false;
4450          }
4451  
4452          list( $name, $lang ) = MessageCache::singleton()->figureMessage(
4453              $wgContLang->lcfirst( $this->getText() )
4454          );
4455          $message = wfMessage( $name )->inLanguage( $lang )->useDatabase( false );
4456  
4457          if ( $message->exists() ) {
4458              return $message->plain();
4459          } else {
4460              return false;
4461          }
4462      }
4463  
4464      /**
4465       * Updates page_touched for this page; called from LinksUpdate.php
4466       *
4467       * @return bool True if the update succeeded
4468       */
4469  	public function invalidateCache() {
4470          if ( wfReadOnly() ) {
4471              return false;
4472          }
4473  
4474          if ( $this->mArticleID === 0 ) {
4475              return true; // avoid gap locking if we know it's not there
4476          }
4477  
4478          $method = __METHOD__;
4479          $dbw = wfGetDB( DB_MASTER );
4480          $conds = $this->pageCond();
4481          $dbw->onTransactionIdle( function () use ( $dbw, $conds, $method ) {
4482              $dbw->update(
4483                  'page',
4484                  array( 'page_touched' => $dbw->timestamp() ),
4485                  $conds,
4486                  $method
4487              );
4488          } );
4489  
4490          return true;
4491      }
4492  
4493      /**
4494       * Update page_touched timestamps and send squid purge messages for
4495       * pages linking to this title. May be sent to the job queue depending
4496       * on the number of links. Typically called on create and delete.
4497       */
4498  	public function touchLinks() {
4499          $u = new HTMLCacheUpdate( $this, 'pagelinks' );
4500          $u->doUpdate();
4501  
4502          if ( $this->getNamespace() == NS_CATEGORY ) {
4503              $u = new HTMLCacheUpdate( $this, 'categorylinks' );
4504              $u->doUpdate();
4505          }
4506      }
4507  
4508      /**
4509       * Get the last touched timestamp
4510       *
4511       * @param DatabaseBase $db Optional db
4512       * @return string Last-touched timestamp
4513       */
4514  	public function getTouched( $db = null ) {
4515          if ( $db === null ) {
4516              $db = wfGetDB( DB_SLAVE );
4517          }
4518          $touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ );
4519          return $touched;
4520      }
4521  
4522      /**
4523       * Get the timestamp when this page was updated since the user last saw it.
4524       *
4525       * @param User $user
4526       * @return string|null
4527       */
4528  	public function getNotificationTimestamp( $user = null ) {
4529          global $wgUser, $wgShowUpdatedMarker;
4530          // Assume current user if none given
4531          if ( !$user ) {
4532              $user = $wgUser;
4533          }
4534          // Check cache first
4535          $uid = $user->getId();
4536          // avoid isset here, as it'll return false for null entries
4537          if ( array_key_exists( $uid, $this->mNotificationTimestamp ) ) {
4538              return $this->mNotificationTimestamp[$uid];
4539          }
4540          if ( !$uid || !$wgShowUpdatedMarker || !$user->isAllowed( 'viewmywatchlist' ) ) {
4541              $this->mNotificationTimestamp[$uid] = false;
4542              return $this->mNotificationTimestamp[$uid];
4543          }
4544          // Don't cache too much!
4545          if ( count( $this->mNotificationTimestamp ) >= self::CACHE_MAX ) {
4546              $this->mNotificationTimestamp = array();
4547          }
4548          $dbr = wfGetDB( DB_SLAVE );
4549          $this->mNotificationTimestamp[$uid] = $dbr->selectField( 'watchlist',
4550              'wl_notificationtimestamp',
4551              array(
4552                  'wl_user' => $user->getId(),
4553                  'wl_namespace' => $this->getNamespace(),
4554                  'wl_title' => $this->getDBkey(),
4555              ),
4556              __METHOD__
4557          );
4558          return $this->mNotificationTimestamp[$uid];
4559      }
4560  
4561      /**
4562       * Generate strings used for xml 'id' names in monobook tabs
4563       *
4564       * @param string $prepend Defaults to 'nstab-'
4565       * @return string XML 'id' name
4566       */
4567  	public function getNamespaceKey( $prepend = 'nstab-' ) {
4568          global $wgContLang;
4569          // Gets the subject namespace if this title
4570          $namespace = MWNamespace::getSubject( $this->getNamespace() );
4571          // Checks if canonical namespace name exists for namespace
4572          if ( MWNamespace::exists( $this->getNamespace() ) ) {
4573              // Uses canonical namespace name
4574              $namespaceKey = MWNamespace::getCanonicalName( $namespace );
4575          } else {
4576              // Uses text of namespace
4577              $namespaceKey = $this->getSubjectNsText();
4578          }
4579          // Makes namespace key lowercase
4580          $namespaceKey = $wgContLang->lc( $namespaceKey );
4581          // Uses main
4582          if ( $namespaceKey == '' ) {
4583              $namespaceKey = 'main';
4584          }
4585          // Changes file to image for backwards compatibility
4586          if ( $namespaceKey == 'file' ) {
4587              $namespaceKey = 'image';
4588          }
4589          return $prepend . $namespaceKey;
4590      }
4591  
4592      /**
4593       * Get all extant redirects to this Title
4594       *
4595       * @param int|null $ns Single namespace to consider; null to consider all namespaces
4596       * @return Title[] Array of Title redirects to this title
4597       */
4598  	public function getRedirectsHere( $ns = null ) {
4599          $redirs = array();
4600  
4601          $dbr = wfGetDB( DB_SLAVE );
4602          $where = array(
4603              'rd_namespace' => $this->getNamespace(),
4604              'rd_title' => $this->getDBkey(),
4605              'rd_from = page_id'
4606          );
4607          if ( $this->isExternal() ) {
4608              $where['rd_interwiki'] = $this->getInterwiki();
4609          } else {
4610              $where[] = 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL';
4611          }
4612          if ( !is_null( $ns ) ) {
4613              $where['page_namespace'] = $ns;
4614          }
4615  
4616          $res = $dbr->select(
4617              array( 'redirect', 'page' ),
4618              array( 'page_namespace', 'page_title' ),
4619              $where,
4620              __METHOD__
4621          );
4622  
4623          foreach ( $res as $row ) {
4624              $redirs[] = self::newFromRow( $row );
4625          }
4626          return $redirs;
4627      }
4628  
4629      /**
4630       * Check if this Title is a valid redirect target
4631       *
4632       * @return bool
4633       */
4634  	public function isValidRedirectTarget() {
4635          global $wgInvalidRedirectTargets;
4636  
4637          // invalid redirect targets are stored in a global array, but explicitly disallow Userlogout here
4638          if ( $this->isSpecial( 'Userlogout' ) ) {
4639              return false;
4640          }
4641  
4642          foreach ( $wgInvalidRedirectTargets as $target ) {
4643              if ( $this->isSpecial( $target ) ) {
4644                  return false;
4645              }
4646          }
4647  
4648          return true;
4649      }
4650  
4651      /**
4652       * Get a backlink cache object
4653       *
4654       * @return BacklinkCache
4655       */
4656  	public function getBacklinkCache() {
4657          return BacklinkCache::get( $this );
4658      }
4659  
4660      /**
4661       * Whether the magic words __INDEX__ and __NOINDEX__ function for  this page.
4662       *
4663       * @return bool
4664       */
4665  	public function canUseNoindex() {
4666          global $wgContentNamespaces, $wgExemptFromUserRobotsControl;
4667  
4668          $bannedNamespaces = is_null( $wgExemptFromUserRobotsControl )
4669              ? $wgContentNamespaces
4670              : $wgExemptFromUserRobotsControl;
4671  
4672          return !in_array( $this->mNamespace, $bannedNamespaces );
4673  
4674      }
4675  
4676      /**
4677       * Returns the raw sort key to be used for categories, with the specified
4678       * prefix.  This will be fed to Collation::getSortKey() to get a
4679       * binary sortkey that can be used for actual sorting.
4680       *
4681       * @param string $prefix The prefix to be used, specified using
4682       *   {{defaultsort:}} or like [[Category:Foo|prefix]].  Empty for no
4683       *   prefix.
4684       * @return string
4685       */
4686  	public function getCategorySortkey( $prefix = '' ) {
4687          $unprefixed = $this->getText();
4688  
4689          // Anything that uses this hook should only depend
4690          // on the Title object passed in, and should probably
4691          // tell the users to run updateCollations.php --force
4692          // in order to re-sort existing category relations.
4693          wfRunHooks( 'GetDefaultSortkey', array( $this, &$unprefixed ) );
4694          if ( $prefix !== '' ) {
4695              # Separate with a line feed, so the unprefixed part is only used as
4696              # a tiebreaker when two pages have the exact same prefix.
4697              # In UCA, tab is the only character that can sort above LF
4698              # so we strip both of them from the original prefix.
4699              $prefix = strtr( $prefix, "\n\t", '  ' );
4700              return "$prefix\n$unprefixed";
4701          }
4702          return $unprefixed;
4703      }
4704  
4705      /**
4706       * Get the language in which the content of this page is written in
4707       * wikitext. Defaults to $wgContLang, but in certain cases it can be
4708       * e.g. $wgLang (such as special pages, which are in the user language).
4709       *
4710       * @since 1.18
4711       * @return Language
4712       */
4713  	public function getPageLanguage() {
4714          global $wgLang, $wgLanguageCode;
4715          wfProfileIn( __METHOD__ );
4716          if ( $this->isSpecialPage() ) {
4717              // special pages are in the user language
4718              wfProfileOut( __METHOD__ );
4719              return $wgLang;
4720          }
4721  
4722          // Checking if DB language is set
4723          if ( $this->mDbPageLanguage ) {
4724              wfProfileOut( __METHOD__ );
4725              return wfGetLangObj( $this->mDbPageLanguage );
4726          }
4727  
4728          if ( !$this->mPageLanguage || $this->mPageLanguage[1] !== $wgLanguageCode ) {
4729              // Note that this may depend on user settings, so the cache should
4730              // be only per-request.
4731              // NOTE: ContentHandler::getPageLanguage() may need to load the
4732              // content to determine the page language!
4733              // Checking $wgLanguageCode hasn't changed for the benefit of unit
4734              // tests.
4735              $contentHandler = ContentHandler::getForTitle( $this );
4736              $langObj = wfGetLangObj( $contentHandler->getPageLanguage( $this ) );
4737              $this->mPageLanguage = array( $langObj->getCode(), $wgLanguageCode );
4738          } else {
4739              $langObj = wfGetLangObj( $this->mPageLanguage[0] );
4740          }
4741  
4742          wfProfileOut( __METHOD__ );
4743          return $langObj;
4744      }
4745  
4746      /**
4747       * Get the language in which the content of this page is written when
4748       * viewed by user. Defaults to $wgContLang, but in certain cases it can be
4749       * e.g. $wgLang (such as special pages, which are in the user language).
4750       *
4751       * @since 1.20
4752       * @return Language
4753       */
4754  	public function getPageViewLanguage() {
4755          global $wgLang;
4756  
4757          if ( $this->isSpecialPage() ) {
4758              // If the user chooses a variant, the content is actually
4759              // in a language whose code is the variant code.
4760              $variant = $wgLang->getPreferredVariant();
4761              if ( $wgLang->getCode() !== $variant ) {
4762                  return Language::factory( $variant );
4763              }
4764  
4765              return $wgLang;
4766          }
4767  
4768          // @note Can't be cached persistently, depends on user settings.
4769          // @note ContentHandler::getPageViewLanguage() may need to load the
4770          //   content to determine the page language!
4771          $contentHandler = ContentHandler::getForTitle( $this );
4772          $pageLang = $contentHandler->getPageViewLanguage( $this );
4773          return $pageLang;
4774      }
4775  
4776      /**
4777       * Get a list of rendered edit notices for this page.
4778       *
4779       * Array is keyed by the original message key, and values are rendered using parseAsBlock, so
4780       * they will already be wrapped in paragraphs.
4781       *
4782       * @since 1.21
4783       * @param int $oldid Revision ID that's being edited
4784       * @return array
4785       */
4786  	public function getEditNotices( $oldid = 0 ) {
4787          $notices = array();
4788  
4789          # Optional notices on a per-namespace and per-page basis
4790          $editnotice_ns = 'editnotice-' . $this->getNamespace();
4791          $editnotice_ns_message = wfMessage( $editnotice_ns );
4792          if ( $editnotice_ns_message->exists() ) {
4793              $notices[$editnotice_ns] = $editnotice_ns_message->parseAsBlock();
4794          }
4795          if ( MWNamespace::hasSubpages( $this->getNamespace() ) ) {
4796              $parts = explode( '/', $this->getDBkey() );
4797              $editnotice_base = $editnotice_ns;
4798              while ( count( $parts ) > 0 ) {
4799                  $editnotice_base .= '-' . array_shift( $parts );
4800                  $editnotice_base_msg = wfMessage( $editnotice_base );
4801                  if ( $editnotice_base_msg->exists() ) {
4802                      $notices[$editnotice_base] = $editnotice_base_msg->parseAsBlock();
4803                  }
4804              }
4805          } else {
4806              # Even if there are no subpages in namespace, we still don't want / in MW ns.
4807              $editnoticeText = $editnotice_ns . '-' . str_replace( '/', '-', $this->getDBkey() );
4808              $editnoticeMsg = wfMessage( $editnoticeText );
4809              if ( $editnoticeMsg->exists() ) {
4810                  $notices[$editnoticeText] = $editnoticeMsg->parseAsBlock();
4811              }
4812          }
4813  
4814          wfRunHooks( 'TitleGetEditNotices', array( $this, $oldid, &$notices ) );
4815          return $notices;
4816      }
4817  }


Generated: Fri Nov 28 14:03:12 2014 Cross-referenced by PHPXref 0.7.1