wiki.techinc.nl/includes/Title.php

5289 lines
156 KiB
PHP
Raw Normal View History

<?php
/**
* Representation of a title within %MediaWiki.
*
* See title.txt
2011-06-28 18:21:59 +00:00
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
*/
use Wikimedia\Rdbms\Database;
use Wikimedia\Rdbms\IDatabase;
use MediaWiki\Linker\LinkTarget;
use MediaWiki\Interwiki\InterwikiLookup;
use MediaWiki\MediaWikiServices;
/**
2008-08-11 04:39:00 +00:00
* Represents a title within MediaWiki.
* Optionally may contain an interwiki designation or namespace.
* @note This class can fetch various kinds of data from the database;
* however, it does so inefficiently.
* @note Consider using a TitleValue object instead. TitleValue is more lightweight
* and does not rely on global state or the database.
*/
class Title implements LinkTarget {
/** @var MapCacheLRU */
static private $titleCache = null;
/**
* Title::newFromText maintains a cache to avoid expensive re-normalization of
* commonly used titles. On a batch operation this can become a memory leak
* if not bounded. After hitting this many titles reset the cache.
*/
const CACHE_MAX = 1000;
/**
* Used to be GAID_FOR_UPDATE define. Used with getArticleID() and friends
* to use the master DB
*/
const GAID_FOR_UPDATE = 1;
/**
2008-08-11 04:39:00 +00:00
* @name Private member variables
* Please use the accessor functions instead.
2006-06-10 18:28:50 +00:00
* @private
*/
// @{
/** @var string Text form (spaces not underscores) of the main part */
public $mTextform = '';
/** @var string URL-encoded form of the main part */
public $mUrlform = '';
/** @var string Main part with underscores */
public $mDbkeyform = '';
/** @var string Database key with the initial letter in the case specified by the user */
protected $mUserCaseDBKey;
/** @var int Namespace index, i.e. one of the NS_xxxx constants */
public $mNamespace = NS_MAIN;
/** @var string Interwiki prefix */
public $mInterwiki = '';
/** @var bool Was this Title created from a string with a local interwiki prefix? */
private $mLocalInterwiki = false;
/** @var string Title fragment (i.e. the bit after the #) */
public $mFragment = '';
/** @var int Article ID, fetched from the link cache on demand */
public $mArticleID = -1;
/** @var bool|int ID of most recent revision */
protected $mLatestID = false;
/**
* @var bool|string ID of the page's content model, i.e. one of the
* CONTENT_MODEL_XXX constants
*/
private $mContentModel = false;
/**
* @var bool If a content model was forced via setContentModel()
* this will be true to avoid having other code paths reset it
*/
private $mForcedContentModel = false;
/** @var int Estimated number of revisions; null of not loaded */
private $mEstimateRevisions;
/** @var array Array of groups allowed to edit this article */
public $mRestrictions = [];
/**
* @var string|bool Comma-separated set of permission keys
* indicating who can move or edit the page from the page table, (pre 1.10) rows.
* Edit and move sections are separated by a colon
* Example: "edit=autoconfirmed,sysop:move=sysop"
*/
protected $mOldRestrictions = false;
/** @var bool Cascade restrictions on this page to included templates and images? */
public $mCascadeRestriction;
/** Caching the results of getCascadeProtectionSources */
public $mCascadingRestrictions;
/** @var array When do the restrictions on this page expire? */
protected $mRestrictionsExpiry = [];
/** @var bool Are cascading restrictions in effect on this page? */
protected $mHasCascadingRestrictions;
/** @var array Where are the cascading restrictions coming from on this page? */
public $mCascadeSources;
/** @var bool Boolean for initialisation on demand */
public $mRestrictionsLoaded = false;
/**
* Text form including namespace/interwiki, initialised on demand
*
* Only public to share cache with TitleFormatter
*
* @private
* @var string
*/
public $prefixedText = null;
/** @var mixed Cached value for getTitleProtection (create protection) */
public $mTitleProtection;
/**
* @var int Namespace index when there is no namespace. Don't change the
* following default, NS_MAIN is hardcoded in several places. See T2696.
* Zero except in {{transclusion}} tags.
*/
public $mDefaultNamespace = NS_MAIN;
/** @var int The page length, 0 for special pages */
protected $mLength = -1;
/** @var null Is the article at this title a redirect? */
public $mRedirect = null;
/** @var array Associative array of user ID -> timestamp/false */
private $mNotificationTimestamp = [];
/** @var bool Whether a page has any subpages */
private $mHasSubpages;
/** @var bool The (string) language code of the page's language and content code. */
private $mPageLanguage = false;
/** @var string|bool|null The page language code from the database, null if not saved in
* the database or false if not loaded, yet. */
private $mDbPageLanguage = false;
/** @var TitleValue A corresponding TitleValue object */
private $mTitleValue = null;
/** @var bool Would deleting this page be a big deletion? */
private $mIsBigDeletion = null;
// @}
/**
* B/C kludge: provide a TitleParser for use by Title.
* Ideally, Title would have no methods that need this.
* Avoid usage of this singleton by using TitleValue
* and the associated services when possible.
*
* @return TitleFormatter
*/
private static function getTitleFormatter() {
return MediaWikiServices::getInstance()->getTitleFormatter();
}
/**
* B/C kludge: provide an InterwikiLookup for use by Title.
* Ideally, Title would have no methods that need this.
* Avoid usage of this singleton by using TitleValue
* and the associated services when possible.
*
* @return InterwikiLookup
*/
private static function getInterwikiLookup() {
return MediaWikiServices::getInstance()->getInterwikiLookup();
}
/**
* @access protected
*/
function __construct() {
}
2003-04-14 23:10:40 +00:00
/**
* Create a new Title from a prefixed DB key
*
* @param string $key The database key, which has underscores
* instead of spaces, possibly including namespace and
* interwiki prefixes
* @return Title|null Title, or null on an error
*/
public static function newFromDBkey( $key ) {
2003-04-14 23:10:40 +00:00
$t = new Title();
$t->mDbkeyform = $key;
try {
$t->secureAndSplit();
return $t;
} catch ( MalformedTitleException $ex ) {
return null;
2010-07-25 15:53:22 +00:00
}
2003-04-14 23:10:40 +00:00
}
/**
* Create a new Title from a TitleValue
*
* @param TitleValue $titleValue Assumed to be safe.
*
* @return Title
*/
public static function newFromTitleValue( TitleValue $titleValue ) {
return self::newFromLinkTarget( $titleValue );
}
/**
* Create a new Title from a LinkTarget
*
* @param LinkTarget $linkTarget Assumed to be safe.
*
* @return Title
*/
public static function newFromLinkTarget( LinkTarget $linkTarget ) {
if ( $linkTarget instanceof Title ) {
// Special case if it's already a Title object
return $linkTarget;
}
return self::makeTitle(
$linkTarget->getNamespace(),
$linkTarget->getText(),
$linkTarget->getFragment(),
$linkTarget->getInterwiki()
);
}
/**
* Create a new Title from text, such as what one would find in a link. De-
* codes any HTML entities in the text.
*
* Title objects returned by this method are guaranteed to be valid, and
* thus return true from the isValid() method.
*
* @param string|int|null $text The link text; spaces, prefixes, and an
* initial ':' indicating the main namespace are accepted.
* @param int $defaultNamespace The namespace to use if none is specified
* by a prefix. If you want to force a specific namespace even if
* $text might begin with a namespace prefix, use makeTitle() or
* makeTitleSafe().
* @throws InvalidArgumentException
* @return Title|null Title or null on an error.
*/
2006-07-10 15:08:51 +00:00
public static function newFromText( $text, $defaultNamespace = NS_MAIN ) {
// DWIM: Integers can be passed in here when page titles are used as array keys.
if ( $text !== null && !is_string( $text ) && !is_int( $text ) ) {
throw new InvalidArgumentException( '$text must be a string.' );
}
if ( $text === null ) {
return null;
}
try {
return self::newFromTextThrow( strval( $text ), $defaultNamespace );
} catch ( MalformedTitleException $ex ) {
return null;
}
}
/**
* Like Title::newFromText(), but throws MalformedTitleException when the title is invalid,
* rather than returning null.
*
* The exception subclasses encode detailed information about why the title is invalid.
*
* Title objects returned by this method are guaranteed to be valid, and
* thus return true from the isValid() method.
*
* @see Title::newFromText
*
* @since 1.25
* @param string $text Title text to check
* @param int $defaultNamespace
* @throws MalformedTitleException If the title is invalid
* @return Title
*/
public static function newFromTextThrow( $text, $defaultNamespace = NS_MAIN ) {
if ( is_object( $text ) ) {
throw new MWException( '$text must be a string, given an object' );
} elseif ( $text === null ) {
// Legacy code relies on MalformedTitleException being thrown in this case
// (happens when URL with no title in it is parsed). TODO fix
throw new MalformedTitleException( 'title-invalid-empty' );
}
$titleCache = self::getTitleCache();
// Wiki pages often contain multiple links to the same page.
// Title normalization and parsing can become expensive on pages with many
// links, so we can save a little time by caching them.
// In theory these are value objects and won't get changed...
if ( $defaultNamespace == NS_MAIN ) {
$t = $titleCache->get( $text );
if ( $t ) {
return $t;
}
}
// Convert things like &eacute; &#257; or &#x3017; into normalized (T16952) text
$filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text );
2003-04-14 23:10:40 +00:00
$t = new Title();
$t->mDbkeyform = strtr( $filteredText, ' ', '_' );
$t->mDefaultNamespace = intval( $defaultNamespace );
$t->secureAndSplit();
if ( $defaultNamespace == NS_MAIN ) {
$titleCache->set( $text, $t );
}
return $t;
2003-04-14 23:10:40 +00:00
}
/**
* THIS IS NOT THE FUNCTION YOU WANT. Use Title::newFromText().
*
* Example of wrong and broken code:
* $title = Title::newFromURL( $wgRequest->getVal( 'title' ) );
*
* Example of right code:
* $title = Title::newFromText( $wgRequest->getVal( 'title' ) );
*
* Create a new Title from URL-encoded text. Ensures that
* the given title's length does not exceed the maximum.
*
* @param string $url The title, as might be taken from a URL
* @return Title|null The new object, or null on an error
*/
public static function newFromURL( $url ) {
2003-04-14 23:10:40 +00:00
$t = new Title();
2006-01-05 05:27:16 +00:00
# For compatibility with old buggy URLs. "+" is usually not valid in titles,
# but some URLs used it as a space replacement and they still come
# from some external search tools.
if ( strpos( self::legalChars(), '+' ) === false ) {
$url = strtr( $url, '+', ' ' );
2006-01-05 05:27:16 +00:00
}
$t->mDbkeyform = strtr( $url, ' ', '_' );
try {
$t->secureAndSplit();
return $t;
} catch ( MalformedTitleException $ex ) {
return null;
}
2003-04-14 23:10:40 +00:00
}
/**
* @return MapCacheLRU
*/
private static function getTitleCache() {
if ( self::$titleCache == null ) {
self::$titleCache = new MapCacheLRU( self::CACHE_MAX );
}
return self::$titleCache;
}
/**
* Returns a list of fields that are to be selected for initializing Title
* objects or LinkCache entries. Uses $wgContentHandlerUseDB to determine
* whether to include page_content_model.
*
* @return array
*/
protected static function getSelectFields() {
global $wgContentHandlerUseDB, $wgPageLanguageUseDB;
$fields = [
'page_namespace', 'page_title', 'page_id',
'page_len', 'page_is_redirect', 'page_latest',
];
if ( $wgContentHandlerUseDB ) {
$fields[] = 'page_content_model';
}
if ( $wgPageLanguageUseDB ) {
$fields[] = 'page_lang';
}
return $fields;
}
/**
* Create a new Title from an article ID
2005-05-04 00:33:08 +00:00
*
* @param int $id The page_id corresponding to the Title to create
* @param int $flags Use Title::GAID_FOR_UPDATE to use master
* @return Title|null The new object, or null on an error
*/
2008-05-10 23:31:52 +00:00
public static function newFromID( $id, $flags = 0 ) {
$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_REPLICA );
$row = $db->selectRow(
'page',
self::getSelectFields(),
[ 'page_id' => $id ],
__METHOD__
);
if ( $row !== false ) {
$title = self::newFromRow( $row );
} else {
$title = null;
}
return $title;
}
/**
* Make an array of titles from an array of IDs
*
* @param int[] $ids Array of IDs
* @return Title[] Array of Titles
*/
public static function newFromIDs( $ids ) {
if ( !count( $ids ) ) {
return [];
}
$dbr = wfGetDB( DB_REPLICA );
2011-02-12 04:06:22 +00:00
$res = $dbr->select(
'page',
self::getSelectFields(),
[ 'page_id' => $ids ],
__METHOD__
);
$titles = [];
foreach ( $res as $row ) {
$titles[] = self::newFromRow( $row );
}
return $titles;
}
/**
* Make a Title object from a DB row
*
* @param stdClass $row Object database row (needs at least page_title,page_namespace)
* @return Title Corresponding Title
*/
public static function newFromRow( $row ) {
$t = self::makeTitle( $row->page_namespace, $row->page_title );
$t->loadFromRow( $row );
return $t;
}
/**
* Load Title object fields from a DB row.
* If false is given, the title will be treated as non-existing.
*
* @param stdClass|bool $row Database row
*/
public function loadFromRow( $row ) {
if ( $row ) { // page found
if ( isset( $row->page_id ) ) {
Give Title a decent loading mechanism: * Added Title::load() to factorise common code that load member variables instead of having each accessor doing it own loading system for its related member variable * Removed usage of LinkCache::addLinkObj() to do the database query and do this directly in Title::load(). This allows to select the complete database row and populate all member variables; previously, requesting a field not stored in LinkCache (using getCount(), getTouched() or isNewPage()) results in two database query, one to load LinkCache data and the second to load the requested field; now there'll be only one query. * Added Title::FIELD_IN_LINKCACHE and Title::FIELD_NOT_IN_LINKCACHE to specify whether the requested field is stored in LinkCache or not. LinkCache will be used if possible (i.e. Title::FIELD_IN_LINKCACHE is passed), otherwise a DB query to select the complete row is issued. * Made Title::loadFromRow() save the row to LinkCache if possible. * Added $wasFromMaster parameter to Title::loadFromRow() to tell that method whether the row was loaded from the master database or not and pass it from WikiPage::loadPageData() * Added Title::GAID_USE_MASTER in addition to Title::GAID_FOR_UPDATE to get the row from the master database without having to do a SELECT FROM UPDATE query * Added Title::selectFields() method to return the fields to select to given Title::loadFromRow() (and methods using it such as Title::newFromRow()) a complete row * Made Title::$mCounter private since it has only been added recently (in r105790) * Mark the object as loaded if Title::resetArticleID() is called with as new ID as 0
2012-01-03 19:28:03 +00:00
$this->mArticleID = (int)$row->page_id;
}
if ( isset( $row->page_len ) ) {
Give Title a decent loading mechanism: * Added Title::load() to factorise common code that load member variables instead of having each accessor doing it own loading system for its related member variable * Removed usage of LinkCache::addLinkObj() to do the database query and do this directly in Title::load(). This allows to select the complete database row and populate all member variables; previously, requesting a field not stored in LinkCache (using getCount(), getTouched() or isNewPage()) results in two database query, one to load LinkCache data and the second to load the requested field; now there'll be only one query. * Added Title::FIELD_IN_LINKCACHE and Title::FIELD_NOT_IN_LINKCACHE to specify whether the requested field is stored in LinkCache or not. LinkCache will be used if possible (i.e. Title::FIELD_IN_LINKCACHE is passed), otherwise a DB query to select the complete row is issued. * Made Title::loadFromRow() save the row to LinkCache if possible. * Added $wasFromMaster parameter to Title::loadFromRow() to tell that method whether the row was loaded from the master database or not and pass it from WikiPage::loadPageData() * Added Title::GAID_USE_MASTER in addition to Title::GAID_FOR_UPDATE to get the row from the master database without having to do a SELECT FROM UPDATE query * Added Title::selectFields() method to return the fields to select to given Title::loadFromRow() (and methods using it such as Title::newFromRow()) a complete row * Made Title::$mCounter private since it has only been added recently (in r105790) * Mark the object as loaded if Title::resetArticleID() is called with as new ID as 0
2012-01-03 19:28:03 +00:00
$this->mLength = (int)$row->page_len;
}
if ( isset( $row->page_is_redirect ) ) {
Give Title a decent loading mechanism: * Added Title::load() to factorise common code that load member variables instead of having each accessor doing it own loading system for its related member variable * Removed usage of LinkCache::addLinkObj() to do the database query and do this directly in Title::load(). This allows to select the complete database row and populate all member variables; previously, requesting a field not stored in LinkCache (using getCount(), getTouched() or isNewPage()) results in two database query, one to load LinkCache data and the second to load the requested field; now there'll be only one query. * Added Title::FIELD_IN_LINKCACHE and Title::FIELD_NOT_IN_LINKCACHE to specify whether the requested field is stored in LinkCache or not. LinkCache will be used if possible (i.e. Title::FIELD_IN_LINKCACHE is passed), otherwise a DB query to select the complete row is issued. * Made Title::loadFromRow() save the row to LinkCache if possible. * Added $wasFromMaster parameter to Title::loadFromRow() to tell that method whether the row was loaded from the master database or not and pass it from WikiPage::loadPageData() * Added Title::GAID_USE_MASTER in addition to Title::GAID_FOR_UPDATE to get the row from the master database without having to do a SELECT FROM UPDATE query * Added Title::selectFields() method to return the fields to select to given Title::loadFromRow() (and methods using it such as Title::newFromRow()) a complete row * Made Title::$mCounter private since it has only been added recently (in r105790) * Mark the object as loaded if Title::resetArticleID() is called with as new ID as 0
2012-01-03 19:28:03 +00:00
$this->mRedirect = (bool)$row->page_is_redirect;
}
if ( isset( $row->page_latest ) ) {
Give Title a decent loading mechanism: * Added Title::load() to factorise common code that load member variables instead of having each accessor doing it own loading system for its related member variable * Removed usage of LinkCache::addLinkObj() to do the database query and do this directly in Title::load(). This allows to select the complete database row and populate all member variables; previously, requesting a field not stored in LinkCache (using getCount(), getTouched() or isNewPage()) results in two database query, one to load LinkCache data and the second to load the requested field; now there'll be only one query. * Added Title::FIELD_IN_LINKCACHE and Title::FIELD_NOT_IN_LINKCACHE to specify whether the requested field is stored in LinkCache or not. LinkCache will be used if possible (i.e. Title::FIELD_IN_LINKCACHE is passed), otherwise a DB query to select the complete row is issued. * Made Title::loadFromRow() save the row to LinkCache if possible. * Added $wasFromMaster parameter to Title::loadFromRow() to tell that method whether the row was loaded from the master database or not and pass it from WikiPage::loadPageData() * Added Title::GAID_USE_MASTER in addition to Title::GAID_FOR_UPDATE to get the row from the master database without having to do a SELECT FROM UPDATE query * Added Title::selectFields() method to return the fields to select to given Title::loadFromRow() (and methods using it such as Title::newFromRow()) a complete row * Made Title::$mCounter private since it has only been added recently (in r105790) * Mark the object as loaded if Title::resetArticleID() is called with as new ID as 0
2012-01-03 19:28:03 +00:00
$this->mLatestID = (int)$row->page_latest;
}
if ( !$this->mForcedContentModel && isset( $row->page_content_model ) ) {
$this->mContentModel = strval( $row->page_content_model );
} elseif ( !$this->mForcedContentModel ) {
$this->mContentModel = false; # initialized lazily in getContentModel()
}
if ( isset( $row->page_lang ) ) {
$this->mDbPageLanguage = (string)$row->page_lang;
}
if ( isset( $row->page_restrictions ) ) {
$this->mOldRestrictions = $row->page_restrictions;
}
} else { // page not found
$this->mArticleID = 0;
$this->mLength = 0;
$this->mRedirect = false;
$this->mLatestID = 0;
if ( !$this->mForcedContentModel ) {
$this->mContentModel = false; # initialized lazily in getContentModel()
}
}
}
/**
* Create a new Title from a namespace index and a DB key.
*
* It's assumed that $ns and $title are safe, for instance when
* they came directly from the database or a special page name,
* not from user input.
*
* No validation is applied. For convenience, spaces are normalized
* to underscores, so that e.g. user_text fields can be used directly.
*
* @note This method may return Title objects that are "invalid"
* according to the isValid() method. This is usually caused by
* configuration changes: e.g. a namespace that was once defined is
* no longer configured, or a character that was once allowed in
* titles is now forbidden.
*
* @param int $ns The namespace of the article
* @param string $title The unprefixed database key form
* @param string $fragment The link fragment (after the "#")
* @param string $interwiki The interwiki prefix
* @return Title The new object
*/
public static function makeTitle( $ns, $title, $fragment = '', $interwiki = '' ) {
$t = new Title();
$t->mInterwiki = $interwiki;
$t->mFragment = $fragment;
$t->mNamespace = $ns = intval( $ns );
$t->mDbkeyform = strtr( $title, ' ', '_' );
$t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
$t->mUrlform = wfUrlencode( $t->mDbkeyform );
$t->mTextform = strtr( $title, '_', ' ' );
$t->mContentModel = false; # initialized lazily in getContentModel()
return $t;
}
/**
* Create a new Title from a namespace index and a DB key.
* The parameters will be checked for validity, which is a bit slower
* than makeTitle() but safer for user-provided data.
2005-05-04 00:33:08 +00:00
*
* Title objects returned by makeTitleSafe() are guaranteed to be valid,
* that is, they return true from the isValid() method. If no valid Title
* can be constructed from the input, this method returns null.
*
* @param int $ns The namespace of the article
* @param string $title Database key form
* @param string $fragment The link fragment (after the "#")
* @param string $interwiki Interwiki prefix
* @return Title|null The new object, or null on an error
*/
public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) {
// NOTE: ideally, this would just call makeTitle() and then isValid(),
// but presently, that means more overhead on a potential performance hotspot.
if ( !MWNamespace::exists( $ns ) ) {
return null;
}
$t = new Title();
$t->mDbkeyform = self::makeName( $ns, $title, $fragment, $interwiki, true );
try {
$t->secureAndSplit();
return $t;
} catch ( MalformedTitleException $ex ) {
return null;
}
}
2003-04-14 23:10:40 +00:00
/**
* Create a new Title for the Main Page
*
* @return Title The new object
*/
public static function newMainPage() {
$title = self::newFromText( wfMessage( 'mainpage' )->inContentLanguage()->text() );
// Don't give fatal errors if the message is broken
if ( !$title ) {
$title = self::newFromText( 'Main Page' );
}
return $title;
}
/**
* Get the prefixed DB key associated with an ID
*
* @param int $id The page_id of the article
* @return Title|null An object representing the article, or null if no such article was found
*/
public static function nameOf( $id ) {
$dbr = wfGetDB( DB_REPLICA );
2010-07-25 15:53:22 +00:00
$s = $dbr->selectRow(
'page',
[ 'page_namespace', 'page_title' ],
[ 'page_id' => $id ],
2010-07-25 15:53:22 +00:00
__METHOD__
);
if ( $s === false ) {
return null;
}
$n = self::makeName( $s->page_namespace, $s->page_title );
return $n;
}
/**
* Get a regex character class describing the legal characters in a link
*
* @return string The list of characters, not delimited
*/
public static function legalChars() {
global $wgLegalTitleChars;
return $wgLegalTitleChars;
}
/**
* Utility method for converting a character sequence from bytes to Unicode.
*
* Primary usecase being converting $wgLegalTitleChars to a sequence usable in
* javascript, as PHP uses UTF-8 bytes where javascript uses Unicode code units.
*
* @param string $byteClass
* @return string
*/
public static function convertByteClassToUnicodeClass( $byteClass ) {
$length = strlen( $byteClass );
// Input token queue
$x0 = $x1 = $x2 = '';
// Decoded queue
$d0 = $d1 = $d2 = '';
// Decoded integer codepoints
$ord0 = $ord1 = $ord2 = 0;
// Re-encoded queue
$r0 = $r1 = $r2 = '';
// Output
$out = '';
// Flags
$allowUnicode = false;
for ( $pos = 0; $pos < $length; $pos++ ) {
// Shift the queues down
$x2 = $x1;
$x1 = $x0;
$d2 = $d1;
$d1 = $d0;
$ord2 = $ord1;
$ord1 = $ord0;
$r2 = $r1;
$r1 = $r0;
// Load the current input token and decoded values
$inChar = $byteClass[$pos];
if ( $inChar == '\\' ) {
if ( preg_match( '/x([0-9a-fA-F]{2})/A', $byteClass, $m, 0, $pos + 1 ) ) {
$x0 = $inChar . $m[0];
$d0 = chr( hexdec( $m[1] ) );
$pos += strlen( $m[0] );
} elseif ( preg_match( '/[0-7]{3}/A', $byteClass, $m, 0, $pos + 1 ) ) {
$x0 = $inChar . $m[0];
$d0 = chr( octdec( $m[0] ) );
$pos += strlen( $m[0] );
} elseif ( $pos + 1 >= $length ) {
$x0 = $d0 = '\\';
} else {
$d0 = $byteClass[$pos + 1];
$x0 = $inChar . $d0;
$pos += 1;
}
} else {
$x0 = $d0 = $inChar;
}
$ord0 = ord( $d0 );
// Load the current re-encoded value
if ( $ord0 < 32 || $ord0 == 0x7f ) {
$r0 = sprintf( '\x%02x', $ord0 );
} elseif ( $ord0 >= 0x80 ) {
// Allow unicode if a single high-bit character appears
$r0 = sprintf( '\x%02x', $ord0 );
$allowUnicode = true;
} elseif ( strpos( '-\\[]^', $d0 ) !== false ) {
$r0 = '\\' . $d0;
} else {
$r0 = $d0;
}
// Do the output
if ( $x0 !== '' && $x1 === '-' && $x2 !== '' ) {
// Range
if ( $ord2 > $ord0 ) {
// Empty range
} elseif ( $ord0 >= 0x80 ) {
// Unicode range
$allowUnicode = true;
if ( $ord2 < 0x80 ) {
// Keep the non-unicode section of the range
$out .= "$r2-\\x7F";
}
} else {
// Normal range
$out .= "$r2-$r0";
}
// Reset state to the initial value
$x0 = $x1 = $d0 = $d1 = $r0 = $r1 = '';
} elseif ( $ord2 < 0x80 ) {
// ASCII character
$out .= $r2;
}
}
if ( $ord1 < 0x80 ) {
$out .= $r1;
}
if ( $ord0 < 0x80 ) {
$out .= $r0;
}
if ( $allowUnicode ) {
$out .= '\u0080-\uFFFF';
}
return $out;
}
/**
* Make a prefixed DB key from a DB key and a namespace index
*
* @param int $ns Numerical representation of the namespace
* @param string $title The DB key form the title
* @param string $fragment The link fragment (after the "#")
* @param string $interwiki The interwiki prefix
* @param bool $canonicalNamespace If true, use the canonical name for
* $ns instead of the localized version.
* @return string The prefixed form of the title
*/
public static function makeName( $ns, $title, $fragment = '', $interwiki = '',
$canonicalNamespace = false
) {
if ( $canonicalNamespace ) {
$namespace = MWNamespace::getCanonicalName( $ns );
} else {
$namespace = MediaWikiServices::getInstance()->getContentLanguage()->getNsText( $ns );
}
$name = $namespace == '' ? $title : "$namespace:$title";
if ( strval( $interwiki ) != '' ) {
$name = "$interwiki:$name";
}
if ( strval( $fragment ) != '' ) {
$name .= '#' . $fragment;
}
return $name;
}
/**
* Escape a text fragment, say from a link, for a URL
*
* @deprecated since 1.30, use Sanitizer::escapeIdForLink() or escapeIdForExternalInterwiki()
*
* @param string $fragment Containing a URL or link fragment (after the "#")
* @return string Escaped string
*/
static function escapeFragmentForURL( $fragment ) {
wfDeprecated( __METHOD__, '1.30' );
# Note that we don't urlencode the fragment. urlencoded Unicode
# fragments appear not to work in IE (at least up to 7) or in at least
# one version of Opera 9.x. The W3C validator, for one, doesn't seem
# to care if they aren't encoded.
return Sanitizer::escapeId( $fragment, 'noninitial' );
}
2011-12-11 14:48:45 +00:00
/**
* Callback for usort() to do title sorts by (namespace, title)
*
* @param LinkTarget $a
* @param LinkTarget $b
2011-12-11 14:48:45 +00:00
*
* @return int Result of string comparison, or namespace comparison
2011-12-11 14:48:45 +00:00
*/
public static function compare( LinkTarget $a, LinkTarget $b ) {
return $a->getNamespace() <=> $b->getNamespace()
?: strcmp( $a->getText(), $b->getText() );
2011-12-11 14:48:45 +00:00
}
/**
* Returns true if the title is valid, false if it is invalid.
*
* Valid titles can be round-tripped via makeTitleSafe() and newFromText().
* Invalid titles may get returned from makeTitle(), and it may be useful to
* allow them to exist, e.g. in order to process log entries about pages in
* namespaces that belong to extensions that are no longer installed.
*
* @note This method is relatively expensive. When constructing Title
* objects that need to be valid, use an instantiator method that is guaranteed
* to return valid titles, such as makeTitleSafe() or newFromText().
*
* @return bool
*/
public function isValid() {
if ( !MWNamespace::exists( $this->mNamespace ) ) {
return false;
}
try {
$parser = MediaWikiServices::getInstance()->getTitleParser();
$parser->parseTitle( $this->mDbkeyform, $this->mNamespace );
return true;
} catch ( MalformedTitleException $ex ) {
return false;
}
}
/**
* Determine whether the object refers to a page within
* this project (either this wiki or a wiki with a local
* interwiki, see https://www.mediawiki.org/wiki/Manual:Interwiki_table#iw_local )
*
* @return bool True if this is an in-project interwiki link or a wikilink, false otherwise
*/
public function isLocal() {
if ( $this->isExternal() ) {
$iw = self::getInterwikiLookup()->fetch( $this->mInterwiki );
if ( $iw ) {
return $iw->isLocal();
}
}
return true;
}
/**
* Is this Title interwiki?
*
* @return bool
*/
public function isExternal() {
return $this->mInterwiki !== '';
}
/**
* Get the interwiki prefix
*
* Use Title::isExternal to check if a interwiki is set
*
* @return string Interwiki prefix
*/
public function getInterwiki() {
return $this->mInterwiki;
}
/**
* Was this a local interwiki link?
*
* @return bool
*/
public function wasLocalInterwiki() {
return $this->mLocalInterwiki;
}
/**
* Determine whether the object refers to a page within
* this project and is transcludable.
*
* @return bool True if this is transcludable
*/
public function isTrans() {
if ( !$this->isExternal() ) {
return false;
2010-07-25 15:53:22 +00:00
}
return self::getInterwikiLookup()->fetch( $this->mInterwiki )->isTranscludable();
}
/**
* Returns the DB name of the distant wiki which owns the object.
*
* @return string|false The DB name
*/
public function getTransWikiID() {
if ( !$this->isExternal() ) {
return false;
2010-07-25 15:53:22 +00:00
}
return self::getInterwikiLookup()->fetch( $this->mInterwiki )->getWikiID();
}
/**
* Get a TitleValue object representing this Title.
*
* @note Not all valid Titles have a corresponding valid TitleValue
* (e.g. TitleValues cannot represent page-local links that have a
* fragment but no title text).
*
* @return TitleValue|null
*/
public function getTitleValue() {
if ( $this->mTitleValue === null ) {
try {
$this->mTitleValue = new TitleValue(
$this->mNamespace,
$this->mDbkeyform,
$this->mFragment,
$this->mInterwiki
);
} catch ( InvalidArgumentException $ex ) {
wfDebug( __METHOD__ . ': Can\'t create a TitleValue for [[' .
$this->getPrefixedText() . ']]: ' . $ex->getMessage() . "\n" );
}
}
return $this->mTitleValue;
}
/**
* Get the text form (spaces not underscores) of the main part
*
* @return string Main part of the title
*/
public function getText() {
return $this->mTextform;
}
/**
* Get the URL-encoded form of the main part
*
* @return string Main part of the title, URL-encoded
*/
public function getPartialURL() {
return $this->mUrlform;
}
/**
* Get the main part with underscores
*
* @return string Main part of the title, with underscores
*/
public function getDBkey() {
return $this->mDbkeyform;
}
2011-12-11 14:48:45 +00:00
/**
* Get the DB key with the initial letter case as specified by the user
*
* @return string DB key
2011-12-11 14:48:45 +00:00
*/
function getUserCaseDBKey() {
if ( !is_null( $this->mUserCaseDBKey ) ) {
return $this->mUserCaseDBKey;
} else {
// If created via makeTitle(), $this->mUserCaseDBKey is not set.
return $this->mDbkeyform;
}
2011-12-11 14:48:45 +00:00
}
/**
* Get the namespace index, i.e. one of the NS_xxxx constants.
*
* @return int Namespace index
*/
public function getNamespace() {
return $this->mNamespace;
}
2012-04-30 16:18:04 +00:00
/**
* Get the page's content model id, see the CONTENT_MODEL_XXX constants.
2012-04-30 16:18:04 +00:00
*
* @todo Deprecate this in favor of SlotRecord::getModel()
*
* @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
* @return string Content model id
2012-04-30 16:18:04 +00:00
*/
public function getContentModel( $flags = 0 ) {
if ( !$this->mForcedContentModel
&& ( !$this->mContentModel || $flags === self::GAID_FOR_UPDATE )
&& $this->getArticleID( $flags )
) {
$linkCache = MediaWikiServices::getInstance()->getLinkCache();
$linkCache->addLinkObj( $this ); # in case we already had an article ID
$this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' );
}
if ( !$this->mContentModel ) {
$this->mContentModel = ContentHandler::getDefaultModelFor( $this );
2012-04-30 16:18:04 +00:00
}
return $this->mContentModel;
2012-04-30 16:18:04 +00:00
}
/**
* Convenience method for checking a title's content model name
2012-04-30 16:18:04 +00:00
*
* @param string $id The content model ID (use the CONTENT_MODEL_XXX constants).
* @return bool True if $this->getContentModel() == $id
2012-04-30 16:18:04 +00:00
*/
public function hasContentModel( $id ) {
return $this->getContentModel() == $id;
2012-04-30 16:18:04 +00:00
}
/**
* Set a proposed content model for the page for permissions
* checking. This does not actually change the content model
* of a title!
*
* Additionally, you should make sure you've checked
* ContentHandler::canBeUsedOn() first.
*
* @since 1.28
* @param string $model CONTENT_MODEL_XXX constant
*/
public function setContentModel( $model ) {
$this->mContentModel = $model;
$this->mForcedContentModel = true;
}
/**
* Get the namespace text
*
* @return string|false Namespace text
*/
public function getNsText() {
if ( $this->isExternal() ) {
// This probably shouldn't even happen, except for interwiki transclusion.
// If possible, use the canonical name for the foreign namespace.
$nsText = MWNamespace::getCanonicalName( $this->mNamespace );
if ( $nsText !== false ) {
return $nsText;
}
}
try {
$formatter = self::getTitleFormatter();
return $formatter->getNamespaceName( $this->mNamespace, $this->mDbkeyform );
} catch ( InvalidArgumentException $ex ) {
wfDebug( __METHOD__ . ': ' . $ex->getMessage() . "\n" );
return false;
}
}
/**
* Get the namespace text of the subject (rather than talk) page
*
* @return string Namespace text
*/
public function getSubjectNsText() {
return MediaWikiServices::getInstance()->getContentLanguage()->
getNsText( MWNamespace::getSubject( $this->mNamespace ) );
}
/**
* Get the namespace text of the talk page
*
* @return string Namespace text
*/
public function getTalkNsText() {
return MediaWikiServices::getInstance()->getContentLanguage()->
getNsText( MWNamespace::getTalk( $this->mNamespace ) );
}
/**
* Can this title have a corresponding talk page?
*
* @deprecated since 1.30, use canHaveTalkPage() instead.
*
* @return bool True if this title either is a talk page or can have a talk page associated.
*/
public function canTalk() {
return $this->canHaveTalkPage();
}
/**
* Can this title have a corresponding talk page?
*
* @see MWNamespace::hasTalkNamespace
* @since 1.30
*
* @return bool True if this title either is a talk page or can have a talk page associated.
*/
public function canHaveTalkPage() {
return MWNamespace::hasTalkNamespace( $this->mNamespace );
}
/**
2011-12-11 14:48:45 +00:00
* Is this in a namespace that allows actual pages?
*
* @return bool
*/
2011-12-11 14:48:45 +00:00
public function canExist() {
2011-12-26 08:07:56 +00:00
return $this->mNamespace >= NS_MAIN;
}
/**
2011-12-11 14:48:45 +00:00
* Can this title be added to a user's watchlist?
*
* @return bool
*/
2011-12-11 14:48:45 +00:00
public function isWatchable() {
return !$this->isExternal() && MWNamespace::isWatchable( $this->mNamespace );
}
2003-04-14 23:10:40 +00:00
/**
2011-12-11 14:48:45 +00:00
* Returns true if this is a special page.
*
* @return bool
*/
2011-12-11 14:48:45 +00:00
public function isSpecialPage() {
return $this->mNamespace == NS_SPECIAL;
2003-04-14 23:10:40 +00:00
}
/**
2011-12-11 14:48:45 +00:00
* Returns true if this title resolves to the named special page
*
* @param string $name The special page name
* @return bool
*/
2011-12-11 14:48:45 +00:00
public function isSpecial( $name ) {
if ( $this->isSpecialPage() ) {
list( $thisName, /* $subpage */ ) =
MediaWikiServices::getInstance()->getSpecialPageFactory()->
resolveAlias( $this->mDbkeyform );
2011-12-11 14:48:45 +00:00
if ( $name == $thisName ) {
return true;
}
}
return false;
2003-04-14 23:10:40 +00:00
}
/**
2011-12-11 14:48:45 +00:00
* If the Title refers to a special page alias which is not the local default, resolve
* the alias, and localise the name as necessary. Otherwise, return $this
*
2011-12-11 14:48:45 +00:00
* @return Title
*/
2011-12-11 14:48:45 +00:00
public function fixSpecialName() {
if ( $this->isSpecialPage() ) {
$spFactory = MediaWikiServices::getInstance()->getSpecialPageFactory();
list( $canonicalName, $par ) = $spFactory->resolveAlias( $this->mDbkeyform );
2011-12-11 14:48:45 +00:00
if ( $canonicalName ) {
$localName = $spFactory->getLocalNameFor( $canonicalName, $par );
2011-12-11 14:48:45 +00:00
if ( $localName != $this->mDbkeyform ) {
return self::makeTitle( NS_SPECIAL, $localName );
2011-12-11 14:48:45 +00:00
}
}
2003-10-22 23:56:49 +00:00
}
2011-12-11 14:48:45 +00:00
return $this;
2003-04-14 23:10:40 +00:00
}
/**
2011-12-11 14:48:45 +00:00
* Returns true if the title is inside the specified namespace.
*
2011-12-11 14:48:45 +00:00
* Please make use of this instead of comparing to getNamespace()
* This function is much more resistant to changes we may make
* to namespaces than code that makes direct comparisons.
* @param int $ns The namespace
2011-12-11 14:48:45 +00:00
* @return bool
* @since 1.19
*/
2011-12-11 14:48:45 +00:00
public function inNamespace( $ns ) {
return MWNamespace::equals( $this->mNamespace, $ns );
}
2003-04-14 23:10:40 +00:00
/**
2011-12-11 14:48:45 +00:00
* Returns true if the title is inside one of the specified namespaces.
*
* @param int|int[] $namespaces,... The namespaces to check for
2011-12-11 14:48:45 +00:00
* @return bool
* @since 1.19
*/
2011-12-11 14:48:45 +00:00
public function inNamespaces( /* ... */ ) {
$namespaces = func_get_args();
if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) {
$namespaces = $namespaces[0];
}
2011-12-11 14:48:45 +00:00
foreach ( $namespaces as $ns ) {
if ( $this->inNamespace( $ns ) ) {
return true;
}
2010-07-25 15:53:22 +00:00
}
2011-12-11 14:48:45 +00:00
return false;
}
/**
2011-12-11 14:48:45 +00:00
* Returns true if the title has the same subject namespace as the
* namespace specified.
* For example this method will take NS_USER and return true if namespace
* is either NS_USER or NS_USER_TALK since both of them have NS_USER
* as their subject namespace.
*
* This is MUCH simpler than individually testing for equivalence
2011-12-11 14:48:45 +00:00
* against both NS_USER and NS_USER_TALK, and is also forward compatible.
* @since 1.19
* @param int $ns
2012-02-09 21:36:14 +00:00
* @return bool
*/
2011-12-11 14:48:45 +00:00
public function hasSubjectNamespace( $ns ) {
return MWNamespace::subjectEquals( $this->mNamespace, $ns );
}
/**
2011-12-11 14:48:45 +00:00
* Is this Title in a namespace which contains content?
* In other words, is this a content page, for the purposes of calculating
* statistics, etc?
*
* @return bool
*/
2011-12-11 14:48:45 +00:00
public function isContentPage() {
return MWNamespace::isContent( $this->mNamespace );
}
2006-04-02 16:19:29 +00:00
/**
2011-12-11 14:48:45 +00:00
* Would anybody with sufficient privileges be able to move this page?
* Some pages just aren't movable.
*
* @return bool
2006-04-02 16:19:29 +00:00
*/
2011-12-11 14:48:45 +00:00
public function isMovable() {
if ( !MWNamespace::isMovable( $this->mNamespace ) || $this->isExternal() ) {
2011-12-11 14:48:45 +00:00
// Interwiki title or immovable namespace. Hooks don't get to override here
return false;
}
$result = true;
Hooks::run( 'TitleIsMovable', [ $this, &$result ] );
2011-12-11 14:48:45 +00:00
return $result;
2006-04-02 16:19:29 +00:00
}
/**
2011-12-11 14:48:45 +00:00
* Is this the mainpage?
* @note Title::newFromText seems to be sufficiently optimized by the title
2011-12-11 14:48:45 +00:00
* cache that we don't need to over-optimize by doing direct comparisons and
* accidentally creating new bugs where $title->equals( Title::newFromText() )
2011-12-11 14:48:45 +00:00
* ends up reporting something differently than $title->isMainPage();
*
2011-12-11 14:48:45 +00:00
* @since 1.18
* @return bool
*/
2011-12-11 14:48:45 +00:00
public function isMainPage() {
return $this->equals( self::newMainPage() );
2003-04-14 23:10:40 +00:00
}
/**
2011-12-11 14:48:45 +00:00
* Is this a subpage?
*
* @return bool
*/
2011-12-11 14:48:45 +00:00
public function isSubpage() {
return MWNamespace::hasSubpages( $this->mNamespace )
? strpos( $this->getText(), '/' ) !== false
: false;
}
/**
* Is this a conversion table for the LanguageConverter?
*
* @return bool
2011-12-11 14:48:45 +00:00
*/
public function isConversionTable() {
// @todo ConversionTable should become a separate content model.
return $this->mNamespace == NS_MEDIAWIKI &&
strpos( $this->getText(), 'Conversiontable/' ) === 0;
2011-12-11 14:48:45 +00:00
}
/**
* Does that page contain wikitext, or it is JS, CSS or whatever?
*
* @return bool
2011-12-11 14:48:45 +00:00
*/
public function isWikitextPage() {
return $this->hasContentModel( CONTENT_MODEL_WIKITEXT );
2011-12-11 14:48:45 +00:00
}
/**
* Could this MediaWiki namespace page contain custom CSS, JSON, or JavaScript for the
* global UI. This is generally true for pages in the MediaWiki namespace having
* CONTENT_MODEL_CSS, CONTENT_MODEL_JSON, or CONTENT_MODEL_JAVASCRIPT.
2012-04-30 16:18:04 +00:00
*
* This method does *not* return true for per-user JS/JSON/CSS. Use isUserConfigPage()
* for that!
2012-04-30 16:18:04 +00:00
*
* Note that this method should not return true for pages that contain and show
* "inactive" CSS, JSON, or JS.
2011-12-11 14:48:45 +00:00
*
* @return bool
* @since 1.31
*/
public function isSiteConfigPage() {
return (
$this->isSiteCssConfigPage()
|| $this->isSiteJsonConfigPage()
|| $this->isSiteJsConfigPage()
);
}
/**
* @return bool
* @deprecated Since 1.31; use ::isSiteConfigPage() instead (which also checks for JSON pages)
2011-12-11 14:48:45 +00:00
*/
public function isCssOrJsPage() {
wfDeprecated( __METHOD__, '1.31' );
return ( NS_MEDIAWIKI == $this->mNamespace
&& ( $this->hasContentModel( CONTENT_MODEL_CSS )
|| $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
}
/**
* Is this a "config" (.css, .json, or .js) sub-page of a user page?
*
* @return bool
* @since 1.31
*/
public function isUserConfigPage() {
return (
$this->isUserCssConfigPage()
|| $this->isUserJsonConfigPage()
|| $this->isUserJsConfigPage()
);
2011-12-11 14:48:45 +00:00
}
/**
* @return bool
* @deprecated Since 1.31; use ::isUserConfigPage() instead (which also checks for JSON pages)
2011-12-11 14:48:45 +00:00
*/
public function isCssJsSubpage() {
wfDeprecated( __METHOD__, '1.31' );
return ( NS_USER == $this->mNamespace && $this->isSubpage()
2012-04-30 16:18:04 +00:00
&& ( $this->hasContentModel( CONTENT_MODEL_CSS )
|| $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
2011-12-11 14:48:45 +00:00
}
/**
* Trim down a .css, .json, or .js subpage title to get the corresponding skin name
2011-12-11 14:48:45 +00:00
*
* @return string Containing skin name from .css, .json, or .js subpage title
* @since 1.31
2011-12-11 14:48:45 +00:00
*/
public function getSkinFromConfigSubpage() {
2011-12-11 14:48:45 +00:00
$subpage = explode( '/', $this->mTextform );
$subpage = $subpage[count( $subpage ) - 1];
2011-12-11 14:48:45 +00:00
$lastdot = strrpos( $subpage, '.' );
if ( $lastdot === false ) {
return $subpage; # Never happens: only called for names ending in '.css'/'.json'/'.js'
}
2011-12-11 14:48:45 +00:00
return substr( $subpage, 0, $lastdot );
}
/**
* @deprecated Since 1.31; use ::getSkinFromConfigSubpage() instead
* @return string Containing skin name from .css, .json, or .js subpage title
*/
public function getSkinFromCssJsSubpage() {
wfDeprecated( __METHOD__, '1.31' );
return $this->getSkinFromConfigSubpage();
}
/**
* Is this a CSS "config" sub-page of a user page?
2011-12-11 14:48:45 +00:00
*
* @return bool
* @since 1.31
*/
public function isUserCssConfigPage() {
return (
NS_USER == $this->mNamespace
&& $this->isSubpage()
&& $this->hasContentModel( CONTENT_MODEL_CSS )
);
}
/**
* @deprecated Since 1.31; use ::isUserCssConfigPage()
* @return bool
2011-12-11 14:48:45 +00:00
*/
public function isCssSubpage() {
wfDeprecated( __METHOD__, '1.31' );
return $this->isUserCssConfigPage();
2011-12-11 14:48:45 +00:00
}
/**
* Is this a JSON "config" sub-page of a user page?
*
* @return bool
* @since 1.31
*/
public function isUserJsonConfigPage() {
return (
NS_USER == $this->mNamespace
&& $this->isSubpage()
&& $this->hasContentModel( CONTENT_MODEL_JSON )
);
}
/**
* Is this a JS "config" sub-page of a user page?
2011-12-11 14:48:45 +00:00
*
* @return bool
* @since 1.31
*/
public function isUserJsConfigPage() {
return (
NS_USER == $this->mNamespace
&& $this->isSubpage()
&& $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
);
}
/**
* @deprecated Since 1.31; use ::isUserJsConfigPage()
* @return bool
2011-12-11 14:48:45 +00:00
*/
public function isJsSubpage() {
wfDeprecated( __METHOD__, '1.31' );
return $this->isUserJsConfigPage();
2011-12-11 14:48:45 +00:00
}
/**
* Is this a sitewide CSS "config" page?
*
* @return bool
* @since 1.32
*/
public function isSiteCssConfigPage() {
return (
NS_MEDIAWIKI == $this->mNamespace
&& (
$this->hasContentModel( CONTENT_MODEL_CSS )
// paranoia - a MediaWiki: namespace page with mismatching extension and content
// model is probably by mistake and might get handled incorrectly (see e.g. T112937)
|| substr( $this->mDbkeyform, -4 ) === '.css'
)
);
}
/**
* Is this a sitewide JSON "config" page?
*
* @return bool
* @since 1.32
*/
public function isSiteJsonConfigPage() {
return (
NS_MEDIAWIKI == $this->mNamespace
&& (
$this->hasContentModel( CONTENT_MODEL_JSON )
// paranoia - a MediaWiki: namespace page with mismatching extension and content
// model is probably by mistake and might get handled incorrectly (see e.g. T112937)
|| substr( $this->mDbkeyform, -5 ) === '.json'
)
);
}
/**
* Is this a sitewide JS "config" page?
*
* @return bool
* @since 1.31
*/
public function isSiteJsConfigPage() {
return (
NS_MEDIAWIKI == $this->mNamespace
&& (
$this->hasContentModel( CONTENT_MODEL_JAVASCRIPT )
// paranoia - a MediaWiki: namespace page with mismatching extension and content
// model is probably by mistake and might get handled incorrectly (see e.g. T112937)
|| substr( $this->mDbkeyform, -3 ) === '.js'
)
);
}
/**
* Is this a message which can contain raw HTML?
*
* @return bool
* @since 1.32
*/
public function isRawHtmlMessage() {
global $wgRawHtmlMessages;
if ( !$this->inNamespace( NS_MEDIAWIKI ) ) {
return false;
}
$message = lcfirst( $this->getRootTitle()->getDBkey() );
return in_array( $message, $wgRawHtmlMessages, true );
}
2011-12-11 14:48:45 +00:00
/**
* Is this a talk page of some sort?
*
* @return bool
2011-12-11 14:48:45 +00:00
*/
public function isTalkPage() {
return MWNamespace::isTalk( $this->mNamespace );
2011-12-11 14:48:45 +00:00
}
/**
* Get a Title object associated with the talk page of this article
*
* @return Title The object for the talk page
2011-12-11 14:48:45 +00:00
*/
public function getTalkPage() {
return self::makeTitle( MWNamespace::getTalk( $this->mNamespace ), $this->mDbkeyform );
2011-12-11 14:48:45 +00:00
}
/**
* Get a Title object associated with the talk page of this article,
* if such a talk page can exist.
*
* @since 1.30
*
* @return Title|null The object for the talk page,
* or null if no associated talk page can exist, according to canHaveTalkPage().
*/
public function getTalkPageIfDefined() {
if ( !$this->canHaveTalkPage() ) {
return null;
}
return $this->getTalkPage();
}
2011-12-11 14:48:45 +00:00
/**
* Get a title object associated with the subject page of this
* talk page
*
* @return Title The object for the subject page
2011-12-11 14:48:45 +00:00
*/
public function getSubjectPage() {
// Is this the same title?
$subjectNS = MWNamespace::getSubject( $this->mNamespace );
if ( $this->mNamespace == $subjectNS ) {
2011-12-11 14:48:45 +00:00
return $this;
}
return self::makeTitle( $subjectNS, $this->mDbkeyform );
2011-12-11 14:48:45 +00:00
}
/**
* Get the other title for this page, if this is a subject page
* get the talk page, if it is a subject page get the talk page
*
* @since 1.25
* @throws MWException If the page doesn't have an other page
* @return Title
*/
public function getOtherPage() {
if ( $this->isSpecialPage() ) {
throw new MWException( 'Special pages cannot have other pages' );
}
if ( $this->isTalkPage() ) {
return $this->getSubjectPage();
} else {
if ( !$this->canHaveTalkPage() ) {
throw new MWException( "{$this->getPrefixedText()} does not have an other page" );
}
return $this->getTalkPage();
}
}
2011-12-11 14:48:45 +00:00
/**
* Get the default namespace index, for when there is no namespace
*
* @return int Default namespace index
2011-12-11 14:48:45 +00:00
*/
public function getDefaultNamespace() {
return $this->mDefaultNamespace;
}
/**
* Get the Title fragment (i.e.\ the bit after the #) in text form
*
* Use Title::hasFragment to check for a fragment
*
* @return string Title fragment
2011-12-11 14:48:45 +00:00
*/
public function getFragment() {
return $this->mFragment;
}
/**
* Check if a Title fragment is set
*
* @return bool
* @since 1.23
*/
public function hasFragment() {
return $this->mFragment !== '';
}
2011-12-11 14:48:45 +00:00
/**
* Get the fragment in URL form, including the "#" character if there is one
*
* @return string Fragment in URL form
2011-12-11 14:48:45 +00:00
*/
public function getFragmentForURL() {
if ( !$this->hasFragment() ) {
2011-12-11 14:48:45 +00:00
return '';
} elseif ( $this->isExternal()
&& !self::getInterwikiLookup()->fetch( $this->mInterwiki )->isLocal()
) {
return '#' . Sanitizer::escapeIdForExternalInterwiki( $this->mFragment );
2011-12-11 14:48:45 +00:00
}
return '#' . Sanitizer::escapeIdForLink( $this->mFragment );
2011-12-11 14:48:45 +00:00
}
/**
* Set the fragment for this title. Removes the first character from the
* specified fragment before setting, so it assumes you're passing it with
* an initial "#".
*
* Deprecated for public use, use Title::makeTitle() with fragment parameter,
* or Title::createFragmentTarget().
2011-12-11 14:48:45 +00:00
* Still in active use privately.
*
* @private
* @param string $fragment Text
2011-12-11 14:48:45 +00:00
*/
public function setFragment( $fragment ) {
$this->mFragment = strtr( substr( $fragment, 1 ), '_', ' ' );
2011-12-11 14:48:45 +00:00
}
/**
* Creates a new Title for a different fragment of the same page.
*
* @since 1.27
* @param string $fragment
* @return Title
*/
public function createFragmentTarget( $fragment ) {
return self::makeTitle(
$this->mNamespace,
$this->getText(),
$fragment,
$this->mInterwiki
);
}
2011-12-11 14:48:45 +00:00
/**
* Prefix some arbitrary text with the namespace or interwiki prefix
* of this object
*
* @param string $name The text
* @return string The prefixed text
2011-12-11 14:48:45 +00:00
*/
private function prefix( $name ) {
$p = '';
if ( $this->isExternal() ) {
2011-12-11 14:48:45 +00:00
$p = $this->mInterwiki . ':';
}
if ( $this->mNamespace != 0 ) {
$nsText = $this->getNsText();
if ( $nsText === false ) {
// See T165149. Awkward, but better than erroneously linking to the main namespace.
$nsText = MediaWikiServices::getInstance()->getContentLanguage()->
getNsText( NS_SPECIAL ) . ":Badtitle/NS{$this->mNamespace}";
}
$p .= $nsText . ':';
2011-12-11 14:48:45 +00:00
}
return $p . $name;
}
/**
* Get the prefixed database key form
*
* @return string The prefixed title, with underscores and
2011-12-11 14:48:45 +00:00
* any interwiki and namespace prefixes
*/
public function getPrefixedDBkey() {
$s = $this->prefix( $this->mDbkeyform );
$s = strtr( $s, ' ', '_' );
2011-12-11 14:48:45 +00:00
return $s;
}
/**
* Get the prefixed title with spaces.
* This is the form usually used for display
*
* @return string The prefixed title, with spaces
2011-12-11 14:48:45 +00:00
*/
public function getPrefixedText() {
if ( $this->prefixedText === null ) {
2011-12-11 14:48:45 +00:00
$s = $this->prefix( $this->mTextform );
$s = strtr( $s, '_', ' ' );
$this->prefixedText = $s;
2011-12-11 14:48:45 +00:00
}
return $this->prefixedText;
2011-12-11 14:48:45 +00:00
}
/**
* Return a string representation of this title
*
* @return string Representation of this title
2011-12-11 14:48:45 +00:00
*/
public function __toString() {
return $this->getPrefixedText();
}
/**
* Get the prefixed title with spaces, plus any fragment
* (part beginning with '#')
*
* @return string The prefixed title, with spaces and the fragment, including '#'
2011-12-11 14:48:45 +00:00
*/
public function getFullText() {
$text = $this->getPrefixedText();
if ( $this->hasFragment() ) {
$text .= '#' . $this->mFragment;
2011-12-11 14:48:45 +00:00
}
return $text;
}
/**
* Get the root page name text without a namespace, i.e. the leftmost part before any slashes
*
* @par Example:
* @code
* Title::newFromText('User:Foo/Bar/Baz')->getRootText();
* # returns: 'Foo'
* @endcode
*
* @return string Root name
* @since 1.20
*/
public function getRootText() {
if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
return $this->getText();
}
return strtok( $this->getText(), '/' );
}
/**
* Get the root page name title, i.e. the leftmost part before any slashes
*
* @par Example:
* @code
* Title::newFromText('User:Foo/Bar/Baz')->getRootTitle();
* # returns: Title{User:Foo}
* @endcode
*
* @return Title Root title
* @since 1.20
*/
public function getRootTitle() {
return self::makeTitle( $this->mNamespace, $this->getRootText() );
}
/**
* Get the base page name without a namespace, i.e. the part before the subpage name
*
* @par Example:
* @code
* Title::newFromText('User:Foo/Bar/Baz')->getBaseText();
* # returns: 'Foo/Bar'
* @endcode
2011-12-11 14:48:45 +00:00
*
* @return string Base name
2011-12-11 14:48:45 +00:00
*/
public function getBaseText() {
if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
return $this->getText();
}
$parts = explode( '/', $this->getText() );
# Don't discard the real title if there's no subpage involved
if ( count( $parts ) > 1 ) {
unset( $parts[count( $parts ) - 1] );
}
return implode( '/', $parts );
}
/**
* Get the base page name title, i.e. the part before the subpage name
*
* @par Example:
* @code
* Title::newFromText('User:Foo/Bar/Baz')->getBaseTitle();
* # returns: Title{User:Foo/Bar}
* @endcode
*
* @return Title Base title
* @since 1.20
*/
public function getBaseTitle() {
return self::makeTitle( $this->mNamespace, $this->getBaseText() );
}
2011-12-11 14:48:45 +00:00
/**
* Get the lowest-level subpage name, i.e. the rightmost part after any slashes
*
* @par Example:
* @code
* Title::newFromText('User:Foo/Bar/Baz')->getSubpageText();
* # returns: "Baz"
* @endcode
*
* @return string Subpage name
2011-12-11 14:48:45 +00:00
*/
public function getSubpageText() {
if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
return $this->mTextform;
2011-12-11 14:48:45 +00:00
}
$parts = explode( '/', $this->mTextform );
return $parts[count( $parts ) - 1];
2011-12-11 14:48:45 +00:00
}
/**
* Get the title for a subpage of the current page
*
* @par Example:
* @code
* Title::newFromText('User:Foo/Bar/Baz')->getSubpage("Asdf");
* # returns: Title{User:Foo/Bar/Baz/Asdf}
* @endcode
*
* @param string $text The subpage name to add to the title
* @return Title Subpage title
* @since 1.20
*/
public function getSubpage( $text ) {
return self::makeTitleSafe( $this->mNamespace, $this->getText() . '/' . $text );
}
2011-12-11 14:48:45 +00:00
/**
* Get a URL-encoded form of the subpage text
*
* @return string URL-encoded subpage name
2011-12-11 14:48:45 +00:00
*/
public function getSubpageUrlForm() {
$text = $this->getSubpageText();
$text = wfUrlencode( strtr( $text, ' ', '_' ) );
return $text;
2011-12-11 14:48:45 +00:00
}
/**
* Get a URL-encoded title (not an actual URL) including interwiki
*
* @return string The URL-encoded form
2011-12-11 14:48:45 +00:00
*/
public function getPrefixedURL() {
$s = $this->prefix( $this->mDbkeyform );
$s = wfUrlencode( strtr( $s, ' ', '_' ) );
2011-12-11 14:48:45 +00:00
return $s;
}
/**
* Helper to fix up the get{Canonical,Full,Link,Local,Internal}URL args
* get{Canonical,Full,Link,Local,Internal}URL methods accepted an optional
* second argument named variant. This was deprecated in favor
* of passing an array of option with a "variant" key
* Once $query2 is removed for good, this helper can be dropped
* and the wfArrayToCgi moved to getLocalURL();
*
* @since 1.19 (r105919)
* @param array|string $query
* @param string|string[]|bool $query2
* @return string
*/
private static function fixUrlQueryArgs( $query, $query2 = false ) {
if ( $query2 !== false ) {
wfDeprecated( "Title::get{Canonical,Full,Link,Local,Internal}URL " .
"method called with a second parameter is deprecated. Add your " .
"parameter to an array passed as the first parameter.", "1.19" );
}
if ( is_array( $query ) ) {
$query = wfArrayToCgi( $query );
}
if ( $query2 ) {
if ( is_string( $query2 ) ) {
// $query2 is a string, we will consider this to be
// a deprecated $variant argument and add it to the query
$query2 = wfArrayToCgi( [ 'variant' => $query2 ] );
} else {
$query2 = wfArrayToCgi( $query2 );
}
// If we have $query content add a & to it first
if ( $query ) {
$query .= '&';
}
// Now append the queries together
$query .= $query2;
}
return $query;
}
2011-12-11 14:48:45 +00:00
/**
* Get a real URL referring to this title, with interwiki link and
* fragment
*
* @see self::getLocalURL for the arguments.
* @see wfExpandUrl
* @param string|string[] $query
* @param string|string[]|bool $query2
* @param string|int|null $proto Protocol type to use in URL
* @return string The URL
2011-12-11 14:48:45 +00:00
*/
public function getFullURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) {
$query = self::fixUrlQueryArgs( $query, $query2 );
2011-12-11 14:48:45 +00:00
# Hand off all the decisions on urls to getLocalURL
$url = $this->getLocalURL( $query );
2011-12-11 14:48:45 +00:00
# Expand the url to make it a full url. Note that getLocalURL has the
# potential to output full urls for a variety of reasons, so we use
# wfExpandUrl instead of simply prepending $wgServer
$url = wfExpandUrl( $url, $proto );
# Finally, add the fragment.
$url .= $this->getFragmentForURL();
// Avoid PHP 7.1 warning from passing $this by reference
$titleRef = $this;
Hooks::run( 'GetFullURL', [ &$titleRef, &$url, $query ] );
return $url;
}
/**
* Get a url appropriate for making redirects based on an untrusted url arg
*
* This is basically the same as getFullUrl(), but in the case of external
* interwikis, we send the user to a landing page, to prevent possible
* phishing attacks and the like.
*
* @note Uses current protocol by default, since technically relative urls
* aren't allowed in redirects per HTTP spec, so this is not suitable for
* places where the url gets cached, as might pollute between
* https and non-https users.
* @see self::getLocalURL for the arguments.
* @param array|string $query
* @param string $proto Protocol type to use in URL
* @return string A url suitable to use in an HTTP location header.
*/
public function getFullUrlForRedirect( $query = '', $proto = PROTO_CURRENT ) {
$target = $this;
if ( $this->isExternal() ) {
$target = SpecialPage::getTitleFor(
'GoToInterwiki',
$this->getPrefixedDBkey()
);
}
return $target->getFullURL( $query, false, $proto );
}
/**
* Get a URL with no fragment or server name (relative URL) from a Title object.
* If this page is generated with action=render, however,
* $wgServer is prepended to make an absolute URL.
*
* @see self::getFullURL to always get an absolute URL.
* @see self::getLinkURL to always get a URL that's the simplest URL that will be
* valid to link, locally, to the current Title.
* @see self::newFromText to produce a Title object.
*
* @param string|string[] $query An optional query string,
* not used for interwiki links. Can be specified as an associative array as well,
* e.g., array( 'action' => 'edit' ) (keys and values will be URL-escaped).
* Some query patterns will trigger various shorturl path replacements.
* @param string|string[]|bool $query2 An optional secondary query array. This one MUST
* be an array. If a string is passed it will be interpreted as a deprecated
* variant argument and urlencoded into a variant= argument.
* This second query argument will be added to the $query
* The second parameter is deprecated since 1.19. Pass it as a key,value
* pair in the first parameter array instead.
*
* @return string String of the URL.
*/
public function getLocalURL( $query = '', $query2 = false ) {
global $wgArticlePath, $wgScript, $wgServer, $wgRequest;
$query = self::fixUrlQueryArgs( $query, $query2 );
$interwiki = self::getInterwikiLookup()->fetch( $this->mInterwiki );
if ( $interwiki ) {
$namespace = $this->getNsText();
if ( $namespace != '' ) {
# Can this actually happen? Interwikis shouldn't be parsed.
# Yes! It can in interwiki transclusion. But... it probably shouldn't.
$namespace .= ':';
}
$url = $interwiki->getURL( $namespace . $this->mDbkeyform );
$url = wfAppendQuery( $url, $query );
} else {
$dbkey = wfUrlencode( $this->getPrefixedDBkey() );
if ( $query == '' ) {
$url = str_replace( '$1', $dbkey, $wgArticlePath );
// Avoid PHP 7.1 warning from passing $this by reference
$titleRef = $this;
Hooks::run( 'GetLocalURL::Article', [ &$titleRef, &$url ] );
} else {
global $wgVariantArticlePath, $wgActionPaths;
$url = false;
$matches = [];
if ( !empty( $wgActionPaths )
&& preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches )
) {
$action = urldecode( $matches[2] );
if ( isset( $wgActionPaths[$action] ) ) {
$query = $matches[1];
if ( isset( $matches[4] ) ) {
$query .= $matches[4];
}
$url = str_replace( '$1', $dbkey, $wgActionPaths[$action] );
if ( $query != '' ) {
$url = wfAppendQuery( $url, $query );
}
}
}
if ( $url === false
&& $wgVariantArticlePath
&& preg_match( '/^variant=([^&]*)$/', $query, $matches )
&& $this->getPageLanguage()->equals(
MediaWikiServices::getInstance()->getContentLanguage() )
&& $this->getPageLanguage()->hasVariants()
) {
$variant = urldecode( $matches[1] );
if ( $this->getPageLanguage()->hasVariant( $variant ) ) {
// Only do the variant replacement if the given variant is a valid
// variant for the page's language.
$url = str_replace( '$2', urlencode( $variant ), $wgVariantArticlePath );
$url = str_replace( '$1', $dbkey, $url );
}
}
if ( $url === false ) {
if ( $query == '-' ) {
$query = '';
}
$url = "{$wgScript}?title={$dbkey}&{$query}";
}
}
// Avoid PHP 7.1 warning from passing $this by reference
$titleRef = $this;
Hooks::run( 'GetLocalURL::Internal', [ &$titleRef, &$url, $query ] );
// @todo FIXME: This causes breakage in various places when we
// actually expected a local URL and end up with dupe prefixes.
if ( $wgRequest->getVal( 'action' ) == 'render' ) {
$url = $wgServer . $url;
}
}
// Avoid PHP 7.1 warning from passing $this by reference
$titleRef = $this;
Hooks::run( 'GetLocalURL', [ &$titleRef, &$url, $query ] );
return $url;
}
/**
* Get a URL that's the simplest URL that will be valid to link, locally,
* to the current Title. It includes the fragment, but does not include
* the server unless action=render is used (or the link is external). If
* there's a fragment but the prefixed text is empty, we just return a link
* to the fragment.
*
* The result obviously should not be URL-escaped, but does need to be
* HTML-escaped if it's being output in HTML.
*
* @param string|string[] $query
* @param bool $query2
* @param string|int|bool $proto A PROTO_* constant on how the URL should be expanded,
* or false (default) for no expansion
* @see self::getLocalURL for the arguments.
* @return string The URL
*/
public function getLinkURL( $query = '', $query2 = false, $proto = false ) {
if ( $this->isExternal() || $proto !== false ) {
$ret = $this->getFullURL( $query, $query2, $proto );
} elseif ( $this->getPrefixedText() === '' && $this->hasFragment() ) {
$ret = $this->getFragmentForURL();
} else {
$ret = $this->getLocalURL( $query, $query2 ) . $this->getFragmentForURL();
}
return $ret;
}
/**
* Get the URL form for an internal link.
* - Used in various CDN-related code, in case we have a different
* internal hostname for the server from the exposed one.
2011-09-23 20:20:41 +00:00
*
* This uses $wgInternalServer to qualify the path, or $wgServer
* if $wgInternalServer is not set. If the server variable used is
* protocol-relative, the URL will be expanded to http://
*
* @see self::getLocalURL for the arguments.
* @param string $query
* @param string|bool $query2
* @return string The URL
*/
public function getInternalURL( $query = '', $query2 = false ) {
2011-09-29 22:08:00 +00:00
global $wgInternalServer, $wgServer;
$query = self::fixUrlQueryArgs( $query, $query2 );
2011-09-29 22:08:00 +00:00
$server = $wgInternalServer !== false ? $wgInternalServer : $wgServer;
$url = wfExpandUrl( $server . $this->getLocalURL( $query ), PROTO_HTTP );
// Avoid PHP 7.1 warning from passing $this by reference
$titleRef = $this;
Hooks::run( 'GetInternalURL', [ &$titleRef, &$url, $query ] );
return $url;
}
Per CR on r44412 and my promise in the commit summary of r94990, stop abusing $wgInternalServer (intended for Squid URLs) for IRC/e-mail URLs and introduce $wgCanonicalServer for these purposes instead. This revision introduces two new hooks for WMF hacks, in exchange for making the core code saner. * Introduce $wgCanonicalServer, which should typically be a fully qualified version of $wgServer but in practice can be anything that you'd like to be used in IRC/e-mail notifs ** Default value is $wgServer, expanded to http:// if protocol-relative ** This means you can easily set HTTPS as the 'default' protocol to use in IRC and e-mail notifs by setting $wgCanonicalServer to https://example.com * Introduce Title::getCanonicalURL(). Similar to getInternalURL(), including a hook for WMF usage (which will be needed as long as secure.wikimedia.org is used) ** Also add escapeCanonicalURL(). Due to some ridiculous accident of history, the other escapeFooURL() functions don't have a $variant parameter; I decided not to follow that bad example * Reinstate the spirit of r44406 and r44412: instead of calling getInternalURL() (or getCanonicalURL()) and regexing the title parameter out, obtain the path to index.php using $wgCanonicalServer . $wgScript and append params to that. Sadly, we need to add a hook here to support the secure server hack for WMF, but that's the price of saner code in this case * Introduce the {{canonicalurl:}} and {{canonicalurle:}} parser functions, which work just like {{fullurl:}} and {{fullurle:}} except that they use getCanonicalURL() instead of getFullURL() * Use {{canonicalurl:}} in the enotif_body message, fixing bug 29993 (protocol-relative URLs appear in e-mail notifications)
2011-08-19 11:23:17 +00:00
/**
* Get the URL for a canonical link, for use in things like IRC and
* e-mail notifications. Uses $wgCanonicalServer and the
* GetCanonicalURL hook.
2011-09-23 20:20:41 +00:00
*
* NOTE: Unlike getInternalURL(), the canonical URL includes the fragment
2011-09-23 20:20:41 +00:00
*
* @see self::getLocalURL for the arguments.
* @param string $query
* @param string|bool $query2
Per CR on r44412 and my promise in the commit summary of r94990, stop abusing $wgInternalServer (intended for Squid URLs) for IRC/e-mail URLs and introduce $wgCanonicalServer for these purposes instead. This revision introduces two new hooks for WMF hacks, in exchange for making the core code saner. * Introduce $wgCanonicalServer, which should typically be a fully qualified version of $wgServer but in practice can be anything that you'd like to be used in IRC/e-mail notifs ** Default value is $wgServer, expanded to http:// if protocol-relative ** This means you can easily set HTTPS as the 'default' protocol to use in IRC and e-mail notifs by setting $wgCanonicalServer to https://example.com * Introduce Title::getCanonicalURL(). Similar to getInternalURL(), including a hook for WMF usage (which will be needed as long as secure.wikimedia.org is used) ** Also add escapeCanonicalURL(). Due to some ridiculous accident of history, the other escapeFooURL() functions don't have a $variant parameter; I decided not to follow that bad example * Reinstate the spirit of r44406 and r44412: instead of calling getInternalURL() (or getCanonicalURL()) and regexing the title parameter out, obtain the path to index.php using $wgCanonicalServer . $wgScript and append params to that. Sadly, we need to add a hook here to support the secure server hack for WMF, but that's the price of saner code in this case * Introduce the {{canonicalurl:}} and {{canonicalurle:}} parser functions, which work just like {{fullurl:}} and {{fullurle:}} except that they use getCanonicalURL() instead of getFullURL() * Use {{canonicalurl:}} in the enotif_body message, fixing bug 29993 (protocol-relative URLs appear in e-mail notifications)
2011-08-19 11:23:17 +00:00
* @return string The URL
2011-11-06 12:15:28 +00:00
* @since 1.18
Per CR on r44412 and my promise in the commit summary of r94990, stop abusing $wgInternalServer (intended for Squid URLs) for IRC/e-mail URLs and introduce $wgCanonicalServer for these purposes instead. This revision introduces two new hooks for WMF hacks, in exchange for making the core code saner. * Introduce $wgCanonicalServer, which should typically be a fully qualified version of $wgServer but in practice can be anything that you'd like to be used in IRC/e-mail notifs ** Default value is $wgServer, expanded to http:// if protocol-relative ** This means you can easily set HTTPS as the 'default' protocol to use in IRC and e-mail notifs by setting $wgCanonicalServer to https://example.com * Introduce Title::getCanonicalURL(). Similar to getInternalURL(), including a hook for WMF usage (which will be needed as long as secure.wikimedia.org is used) ** Also add escapeCanonicalURL(). Due to some ridiculous accident of history, the other escapeFooURL() functions don't have a $variant parameter; I decided not to follow that bad example * Reinstate the spirit of r44406 and r44412: instead of calling getInternalURL() (or getCanonicalURL()) and regexing the title parameter out, obtain the path to index.php using $wgCanonicalServer . $wgScript and append params to that. Sadly, we need to add a hook here to support the secure server hack for WMF, but that's the price of saner code in this case * Introduce the {{canonicalurl:}} and {{canonicalurle:}} parser functions, which work just like {{fullurl:}} and {{fullurle:}} except that they use getCanonicalURL() instead of getFullURL() * Use {{canonicalurl:}} in the enotif_body message, fixing bug 29993 (protocol-relative URLs appear in e-mail notifications)
2011-08-19 11:23:17 +00:00
*/
public function getCanonicalURL( $query = '', $query2 = false ) {
$query = self::fixUrlQueryArgs( $query, $query2 );
$url = wfExpandUrl( $this->getLocalURL( $query ) . $this->getFragmentForURL(), PROTO_CANONICAL );
// Avoid PHP 7.1 warning from passing $this by reference
$titleRef = $this;
Hooks::run( 'GetCanonicalURL', [ &$titleRef, &$url, $query ] );
Per CR on r44412 and my promise in the commit summary of r94990, stop abusing $wgInternalServer (intended for Squid URLs) for IRC/e-mail URLs and introduce $wgCanonicalServer for these purposes instead. This revision introduces two new hooks for WMF hacks, in exchange for making the core code saner. * Introduce $wgCanonicalServer, which should typically be a fully qualified version of $wgServer but in practice can be anything that you'd like to be used in IRC/e-mail notifs ** Default value is $wgServer, expanded to http:// if protocol-relative ** This means you can easily set HTTPS as the 'default' protocol to use in IRC and e-mail notifs by setting $wgCanonicalServer to https://example.com * Introduce Title::getCanonicalURL(). Similar to getInternalURL(), including a hook for WMF usage (which will be needed as long as secure.wikimedia.org is used) ** Also add escapeCanonicalURL(). Due to some ridiculous accident of history, the other escapeFooURL() functions don't have a $variant parameter; I decided not to follow that bad example * Reinstate the spirit of r44406 and r44412: instead of calling getInternalURL() (or getCanonicalURL()) and regexing the title parameter out, obtain the path to index.php using $wgCanonicalServer . $wgScript and append params to that. Sadly, we need to add a hook here to support the secure server hack for WMF, but that's the price of saner code in this case * Introduce the {{canonicalurl:}} and {{canonicalurle:}} parser functions, which work just like {{fullurl:}} and {{fullurle:}} except that they use getCanonicalURL() instead of getFullURL() * Use {{canonicalurl:}} in the enotif_body message, fixing bug 29993 (protocol-relative URLs appear in e-mail notifications)
2011-08-19 11:23:17 +00:00
return $url;
}
/**
* Get the edit URL for this Title
*
* @return string The URL, or a null string if this is an interwiki link
*/
public function getEditURL() {
if ( $this->isExternal() ) {
2010-07-25 15:53:22 +00:00
return '';
}
2004-08-16 20:14:35 +00:00
$s = $this->getLocalURL( 'action=edit' );
2003-04-14 23:10:40 +00:00
return $s;
}
2007-11-15 14:47:40 +00:00
/**
* Can $user perform $action on this page?
* This skips potentially expensive cascading permission checks
* as well as avoids expensive error formatting
*
* Suitable for use for nonessential UI controls in common cases, but
* _not_ for functional access control.
*
* May provide false positives, but should never provide a false negative.
*
* @param string $action Action that permission needs to be checked for
* @param User|null $user User to check (since 1.19); $wgUser will be used if not provided.
* @return bool
*/
public function quickUserCan( $action, $user = null ) {
return $this->userCan( $action, $user, false );
}
2007-11-15 14:47:40 +00:00
/**
* Can $user perform $action on this page?
*
* @param string $action Action that permission needs to be checked for
* @param User|null $user User to check (since 1.19); $wgUser will be used if not
* provided.
* @param string $rigor Same format as Title::getUserPermissionsErrors()
* @return bool
*/
public function userCan( $action, $user = null, $rigor = 'secure' ) {
if ( !$user instanceof User ) {
global $wgUser;
$user = $wgUser;
}
return !count( $this->getUserPermissionsErrorsInternal( $action, $user, $rigor, true ) );
}
2007-11-15 14:47:40 +00:00
/**
2008-01-13 22:26:08 +00:00
* Can $user perform $action on this page?
*
* @todo FIXME: This *does not* check throttles (User::pingLimiter()).
*
* @param string $action Action that permission needs to be checked for
* @param User $user User to check
* @param string $rigor One of (quick,full,secure)
* - quick : does cheap permission checks from replica DBs (usable for GUI creation)
* - full : does cheap and expensive checks possibly from a replica DB
* - secure : does cheap and expensive checks, using the master as needed
* @param array $ignoreErrors Array of Strings Set this to a list of message keys
* whose corresponding errors may be ignored.
* @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
*/
public function getUserPermissionsErrors(
$action, $user, $rigor = 'secure', $ignoreErrors = []
) {
$errors = $this->getUserPermissionsErrorsInternal( $action, $user, $rigor );
// Remove the errors being ignored.
foreach ( $errors as $index => $error ) {
$errKey = is_array( $error ) ? $error[0] : $error;
if ( in_array( $errKey, $ignoreErrors ) ) {
unset( $errors[$index] );
}
if ( $errKey instanceof MessageSpecifier && in_array( $errKey->getKey(), $ignoreErrors ) ) {
unset( $errors[$index] );
}
}
return $errors;
}
/**
* Permissions checks that fail most often, and which are easiest to test.
*
* @param string $action The action to check
* @param User $user User to check
* @param array $errors List of current errors
* @param string $rigor Same format as Title::getUserPermissionsErrors()
* @param bool $short Short circuit on first error
*
* @return array List of errors
*/
private function checkQuickPermissions( $action, $user, $errors, $rigor, $short ) {
if ( !Hooks::run( 'TitleQuickPermissions',
[ $this, $user, $action, &$errors, ( $rigor !== 'quick' ), $short ] )
) {
return $errors;
}
if ( $action == 'create' ) {
if (
( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) ||
( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) )
) {
$errors[] = $user->isAnon() ? [ 'nocreatetext' ] : [ 'nocreate-loggedin' ];
}
} elseif ( $action == 'move' ) {
if ( !$user->isAllowed( 'move-rootuserpages' )
&& $this->mNamespace == NS_USER && !$this->isSubpage() ) {
// Show user page-specific message only if the user can move other pages
$errors[] = [ 'cant-move-user-page' ];
}
// Check if user is allowed to move files if it's a file
if ( $this->mNamespace == NS_FILE && !$user->isAllowed( 'movefile' ) ) {
$errors[] = [ 'movenotallowedfile' ];
}
// Check if user is allowed to move category pages if it's a category page
if ( $this->mNamespace == NS_CATEGORY && !$user->isAllowed( 'move-categorypages' ) ) {
$errors[] = [ 'cant-move-category-page' ];
}
if ( !$user->isAllowed( 'move' ) ) {
// User can't move anything
$userCanMove = User::groupHasPermission( 'user', 'move' );
$autoconfirmedCanMove = User::groupHasPermission( 'autoconfirmed', 'move' );
if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) {
// custom message if logged-in users without any special rights can move
$errors[] = [ 'movenologintext' ];
} else {
$errors[] = [ 'movenotallowed' ];
}
}
} elseif ( $action == 'move-target' ) {
if ( !$user->isAllowed( 'move' ) ) {
// User can't move anything
$errors[] = [ 'movenotallowed' ];
} elseif ( !$user->isAllowed( 'move-rootuserpages' )
&& $this->mNamespace == NS_USER && !$this->isSubpage() ) {
// Show user page-specific message only if the user can move other pages
$errors[] = [ 'cant-move-to-user-page' ];
} elseif ( !$user->isAllowed( 'move-categorypages' )
&& $this->mNamespace == NS_CATEGORY ) {
// Show category page-specific message only if the user can move other pages
$errors[] = [ 'cant-move-to-category-page' ];
}
} elseif ( !$user->isAllowed( $action ) ) {
$errors[] = $this->missingPermissionError( $action, $short );
}
return $errors;
}
/**
* Add the resulting error code to the errors array
*
* @param array $errors List of current errors
* @param array $result Result of errors
*
* @return array List of errors
*/
private function resultToError( $errors, $result ) {
if ( is_array( $result ) && count( $result ) && !is_array( $result[0] ) ) {
// A single array representing an error
$errors[] = $result;
} elseif ( is_array( $result ) && is_array( $result[0] ) ) {
// A nested array representing multiple errors
$errors = array_merge( $errors, $result );
} elseif ( $result !== '' && is_string( $result ) ) {
// A string representing a message-id
$errors[] = [ $result ];
} elseif ( $result instanceof MessageSpecifier ) {
// A message specifier representing an error
$errors[] = [ $result ];
} elseif ( $result === false ) {
// a generic "We don't want them to do that"
$errors[] = [ 'badaccess-group0' ];
}
return $errors;
}
/**
* Check various permission hooks
*
* @param string $action The action to check
* @param User $user User to check
* @param array $errors List of current errors
* @param string $rigor Same format as Title::getUserPermissionsErrors()
* @param bool $short Short circuit on first error
*
* @return array List of errors
*/
private function checkPermissionHooks( $action, $user, $errors, $rigor, $short ) {
// Use getUserPermissionsErrors instead
$result = '';
// Avoid PHP 7.1 warning from passing $this by reference
$titleRef = $this;
if ( !Hooks::run( 'userCan', [ &$titleRef, &$user, $action, &$result ] ) ) {
return $result ? [] : [ [ 'badaccess-group0' ] ];
}
// Check getUserPermissionsErrors hook
// Avoid PHP 7.1 warning from passing $this by reference
$titleRef = $this;
if ( !Hooks::run( 'getUserPermissionsErrors', [ &$titleRef, &$user, $action, &$result ] ) ) {
$errors = $this->resultToError( $errors, $result );
}
// Check getUserPermissionsErrorsExpensive hook
if (
$rigor !== 'quick'
&& !( $short && count( $errors ) > 0 )
&& !Hooks::run( 'getUserPermissionsErrorsExpensive', [ &$titleRef, &$user, $action, &$result ] )
) {
$errors = $this->resultToError( $errors, $result );
}
return $errors;
}
/**
* Check permissions on special pages & namespaces
*
* @param string $action The action to check
* @param User $user User to check
* @param array $errors List of current errors
* @param string $rigor Same format as Title::getUserPermissionsErrors()
* @param bool $short Short circuit on first error
*
* @return array List of errors
*/
private function checkSpecialsAndNSPermissions( $action, $user, $errors, $rigor, $short ) {
# Only 'createaccount' can be performed on special pages,
# which don't actually exist in the DB.
if ( $this->isSpecialPage() && $action !== 'createaccount' ) {
$errors[] = [ 'ns-specialprotected' ];
}
# Check $wgNamespaceProtection for restricted namespaces
if ( $this->isNamespaceProtected( $user ) ) {
$ns = $this->mNamespace == NS_MAIN ?
wfMessage( 'nstab-main' )->text() : $this->getNsText();
$errors[] = $this->mNamespace == NS_MEDIAWIKI ?
[ 'protectedinterface', $action ] : [ 'namespaceprotected', $ns, $action ];
}
return $errors;
}
/**
* Check sitewide CSS/JSON/JS permissions
*
* @param string $action The action to check
* @param User $user User to check
* @param array $errors List of current errors
* @param string $rigor Same format as Title::getUserPermissionsErrors()
* @param bool $short Short circuit on first error
*
* @return array List of errors
*/
private function checkSiteConfigPermissions( $action, $user, $errors, $rigor, $short ) {
if ( $action != 'patrol' ) {
$error = null;
// Sitewide CSS/JSON/JS changes, like all NS_MEDIAWIKI changes, also require the
// editinterface right. That's implemented as a restriction so no check needed here.
if ( $this->isSiteCssConfigPage() && !$user->isAllowed( 'editsitecss' ) ) {
$error = [ 'sitecssprotected', $action ];
} elseif ( $this->isSiteJsonConfigPage() && !$user->isAllowed( 'editsitejson' ) ) {
$error = [ 'sitejsonprotected', $action ];
} elseif ( $this->isSiteJsConfigPage() && !$user->isAllowed( 'editsitejs' ) ) {
$error = [ 'sitejsprotected', $action ];
} elseif ( $this->isRawHtmlMessage() ) {
// Raw HTML can be used to deploy CSS or JS so require rights for both.
if ( !$user->isAllowed( 'editsitejs' ) ) {
$error = [ 'sitejsprotected', $action ];
} elseif ( !$user->isAllowed( 'editsitecss' ) ) {
$error = [ 'sitecssprotected', $action ];
}
}
if ( $error ) {
if ( $user->isAllowed( 'editinterface' ) ) {
// Most users / site admins will probably find out about the new, more restrictive
// permissions by failing to edit something. Give them more info.
// TODO remove this a few release cycles after 1.32
$error = [ 'interfaceadmin-info', wfMessage( $error[0], $error[1] ) ];
}
$errors[] = $error;
}
}
return $errors;
}
/**
* Check CSS/JSON/JS sub-page permissions
*
* @param string $action The action to check
* @param User $user User to check
* @param array $errors List of current errors
* @param string $rigor Same format as Title::getUserPermissionsErrors()
* @param bool $short Short circuit on first error
*
* @return array List of errors
*/
private function checkUserConfigPermissions( $action, $user, $errors, $rigor, $short ) {
# Protect css/json/js subpages of user pages
# XXX: this might be better using restrictions
if ( $action === 'patrol' ) {
return $errors;
}
if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) {
// Users need editmyuser* to edit their own CSS/JSON/JS subpages.
if (
$this->isUserCssConfigPage()
&& !$user->isAllowedAny( 'editmyusercss', 'editusercss' )
) {
$errors[] = [ 'mycustomcssprotected', $action ];
} elseif (
$this->isUserJsonConfigPage()
&& !$user->isAllowedAny( 'editmyuserjson', 'edituserjson' )
) {
$errors[] = [ 'mycustomjsonprotected', $action ];
} elseif (
$this->isUserJsConfigPage()
&& !$user->isAllowedAny( 'editmyuserjs', 'edituserjs' )
) {
$errors[] = [ 'mycustomjsprotected', $action ];
}
} else {
// Users need editmyuser* to edit their own CSS/JSON/JS subpages, except for
// deletion/suppression which cannot be used for attacks and we want to avoid the
// situation where an unprivileged user can post abusive content on their subpages
// and only very highly privileged users could remove it.
if ( !in_array( $action, [ 'delete', 'deleterevision', 'suppressrevision' ], true ) ) {
if (
$this->isUserCssConfigPage()
&& !$user->isAllowed( 'editusercss' )
) {
$errors[] = [ 'customcssprotected', $action ];
} elseif (
$this->isUserJsonConfigPage()
&& !$user->isAllowed( 'edituserjson' )
) {
$errors[] = [ 'customjsonprotected', $action ];
} elseif (
$this->isUserJsConfigPage()
&& !$user->isAllowed( 'edituserjs' )
) {
$errors[] = [ 'customjsprotected', $action ];
}
}
}
return $errors;
}
/**
* Check against page_restrictions table requirements on this
* page. The user must possess all required rights for this
* action.
*
* @param string $action The action to check
* @param User $user User to check
* @param array $errors List of current errors
* @param string $rigor Same format as Title::getUserPermissionsErrors()
* @param bool $short Short circuit on first error
*
* @return array List of errors
*/
private function checkPageRestrictions( $action, $user, $errors, $rigor, $short ) {
foreach ( $this->getRestrictions( $action ) as $right ) {
// Backwards compatibility, rewrite sysop -> editprotected
if ( $right == 'sysop' ) {
$right = 'editprotected';
}
// Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
if ( $right == 'autoconfirmed' ) {
$right = 'editsemiprotected';
}
if ( $right == '' ) {
continue;
}
if ( !$user->isAllowed( $right ) ) {
$errors[] = [ 'protectedpagetext', $right, $action ];
} elseif ( $this->mCascadeRestriction && !$user->isAllowed( 'protect' ) ) {
$errors[] = [ 'protectedpagetext', 'protect', $action ];
}
}
return $errors;
}
/**
* Check restrictions on cascading pages.
2011-02-12 04:06:22 +00:00
*
* @param string $action The action to check
* @param User $user User to check
* @param array $errors List of current errors
* @param string $rigor Same format as Title::getUserPermissionsErrors()
* @param bool $short Short circuit on first error
*
* @return array List of errors
*/
private function checkCascadingSourcesRestrictions( $action, $user, $errors, $rigor, $short ) {
if ( $rigor !== 'quick' && !$this->isUserConfigPage() ) {
# We /could/ use the protection level on the source page, but it's
# fairly ugly as we have to establish a precedence hierarchy for pages
# included by multiple cascade-protected pages. So just restrict
# it to people with 'protect' permission, as they could remove the
# protection anyway.
list( $cascadingSources, $restrictions ) = $this->getCascadeProtectionSources();
# Cascading protection depends on more than this page...
# Several cascading protected pages may include this page...
# Check each cascading level
# This is only for protection restrictions, not for all actions
if ( isset( $restrictions[$action] ) ) {
foreach ( $restrictions[$action] as $right ) {
// Backwards compatibility, rewrite sysop -> editprotected
if ( $right == 'sysop' ) {
$right = 'editprotected';
}
// Backwards compatibility, rewrite autoconfirmed -> editsemiprotected
if ( $right == 'autoconfirmed' ) {
$right = 'editsemiprotected';
}
if ( $right != '' && !$user->isAllowedAll( 'protect', $right ) ) {
$pages = '';
foreach ( $cascadingSources as $page ) {
$pages .= '* [[:' . $page->getPrefixedText() . "]]\n";
}
$errors[] = [ 'cascadeprotected', count( $cascadingSources ), $pages, $action ];
}
}
}
}
return $errors;
}
/**
* Check action permissions not already checked in checkQuickPermissions
*
* @param string $action The action to check
* @param User $user User to check
* @param array $errors List of current errors
* @param string $rigor Same format as Title::getUserPermissionsErrors()
* @param bool $short Short circuit on first error
*
* @return array List of errors
*/
private function checkActionPermissions( $action, $user, $errors, $rigor, $short ) {
global $wgDeleteRevisionsLimit, $wgLang;
if ( $action == 'protect' ) {
if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $rigor, true ) ) ) {
// If they can't edit, they shouldn't protect.
$errors[] = [ 'protect-cantedit' ];
}
} elseif ( $action == 'create' ) {
$title_protection = $this->getTitleProtection();
if ( $title_protection ) {
if ( $title_protection['permission'] == ''
|| !$user->isAllowed( $title_protection['permission'] )
) {
$errors[] = [
'titleprotected',
User::whoIs( $title_protection['user'] ),
$title_protection['reason']
];
}
}
} elseif ( $action == 'move' ) {
// Check for immobile pages
if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
// Specific message for this case
$errors[] = [ 'immobile-source-namespace', $this->getNsText() ];
} elseif ( !$this->isMovable() ) {
// Less specific message for rarer cases
$errors[] = [ 'immobile-source-page' ];
}
} elseif ( $action == 'move-target' ) {
if ( !MWNamespace::isMovable( $this->mNamespace ) ) {
$errors[] = [ 'immobile-target-namespace', $this->getNsText() ];
} elseif ( !$this->isMovable() ) {
$errors[] = [ 'immobile-target-page' ];
}
} elseif ( $action == 'delete' ) {
$tempErrors = $this->checkPageRestrictions( 'edit', $user, [], $rigor, true );
if ( !$tempErrors ) {
$tempErrors = $this->checkCascadingSourcesRestrictions( 'edit',
$user, $tempErrors, $rigor, true );
}
if ( $tempErrors ) {
// If protection keeps them from editing, they shouldn't be able to delete.
$errors[] = [ 'deleteprotected' ];
}
if ( $rigor !== 'quick' && $wgDeleteRevisionsLimit
&& !$this->userCan( 'bigdelete', $user ) && $this->isBigDeletion()
) {
$errors[] = [ 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ];
}
} elseif ( $action === 'undelete' ) {
if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $rigor, true ) ) ) {
// Undeleting implies editing
$errors[] = [ 'undelete-cantedit' ];
}
if ( !$this->exists()
&& count( $this->getUserPermissionsErrorsInternal( 'create', $user, $rigor, true ) )
) {
// Undeleting where nothing currently exists implies creating
$errors[] = [ 'undelete-cantcreate' ];
}
}
return $errors;
}
/**
* Check that the user isn't blocked from editing.
*
* @param string $action The action to check
* @param User $user User to check
* @param array $errors List of current errors
* @param string $rigor Same format as Title::getUserPermissionsErrors()
* @param bool $short Short circuit on first error
*
* @return array List of errors
*/
private function checkUserBlock( $action, $user, $errors, $rigor, $short ) {
global $wgEmailConfirmToEdit, $wgBlockDisablesLogin;
// Account creation blocks handled at userlogin.
// Unblocking handled in SpecialUnblock
if ( $rigor === 'quick' || in_array( $action, [ 'createaccount', 'unblock' ] ) ) {
return $errors;
}
// Optimize for a very common case
if ( $action === 'read' && !$wgBlockDisablesLogin ) {
return $errors;
}
if ( $wgEmailConfirmToEdit
&& !$user->isEmailConfirmed()
&& $action === 'edit'
) {
$errors[] = [ 'confirmedittext' ];
}
$useReplica = ( $rigor !== 'secure' );
if ( ( $action == 'edit' || $action == 'create' )
&& !$user->isBlockedFrom( $this, $useReplica )
) {
// Don't block the user from editing their own talk page unless they've been
// explicitly blocked from that too.
} elseif ( $user->isBlocked() && $user->getBlock()->prevents( $action ) !== false ) {
// @todo FIXME: Pass the relevant context into this function.
$errors[] = $user->getBlock()->getPermissionsError( RequestContext::getMain() );
}
return $errors;
}
/**
* Check that the user is allowed to read this page.
*
* @param string $action The action to check
* @param User $user User to check
* @param array $errors List of current errors
* @param string $rigor Same format as Title::getUserPermissionsErrors()
* @param bool $short Short circuit on first error
*
* @return array List of errors
*/
private function checkReadPermissions( $action, $user, $errors, $rigor, $short ) {
global $wgWhitelistRead, $wgWhitelistReadRegexp;
$whitelisted = false;
if ( User::isEveryoneAllowed( 'read' ) ) {
# Shortcut for public wikis, allows skipping quite a bit of code
$whitelisted = true;
} elseif ( $user->isAllowed( 'read' ) ) {
# If the user is allowed to read pages, he is allowed to read all pages
$whitelisted = true;
} elseif ( $this->isSpecial( 'Userlogin' )
|| $this->isSpecial( 'PasswordReset' )
|| $this->isSpecial( 'Userlogout' )
) {
# Always grant access to the login page.
# Even anons need to be able to log in.
$whitelisted = true;
} elseif ( is_array( $wgWhitelistRead ) && count( $wgWhitelistRead ) ) {
# Time to check the whitelist
# Only do these checks is there's something to check against
$name = $this->getPrefixedText();
$dbName = $this->getPrefixedDBkey();
// Check for explicit whitelisting with and without underscores
if ( in_array( $name, $wgWhitelistRead, true ) || in_array( $dbName, $wgWhitelistRead, true ) ) {
$whitelisted = true;
} elseif ( $this->mNamespace == NS_MAIN ) {
# Old settings might have the title prefixed with
# a colon for main-namespace pages
if ( in_array( ':' . $name, $wgWhitelistRead ) ) {
$whitelisted = true;
}
} elseif ( $this->isSpecialPage() ) {
# If it's a special page, ditch the subpage bit and check again
$name = $this->mDbkeyform;
list( $name, /* $subpage */ ) =
MediaWikiServices::getInstance()->getSpecialPageFactory()->
resolveAlias( $name );
if ( $name ) {
$pure = SpecialPage::getTitleFor( $name )->getPrefixedText();
if ( in_array( $pure, $wgWhitelistRead, true ) ) {
$whitelisted = true;
}
}
}
}
if ( !$whitelisted && is_array( $wgWhitelistReadRegexp ) && !empty( $wgWhitelistReadRegexp ) ) {
$name = $this->getPrefixedText();
// Check for regex whitelisting
foreach ( $wgWhitelistReadRegexp as $listItem ) {
if ( preg_match( $listItem, $name ) ) {
$whitelisted = true;
break;
}
}
}
if ( !$whitelisted ) {
# If the title is not whitelisted, give extensions a chance to do so...
Hooks::run( 'TitleReadWhitelist', [ $this, $user, &$whitelisted ] );
if ( !$whitelisted ) {
$errors[] = $this->missingPermissionError( $action, $short );
}
}
return $errors;
}
/**
* Get a description array when the user doesn't have the right to perform
* $action (i.e. when User::isAllowed() returns false)
*
* @param string $action The action to check
* @param bool $short Short circuit on first error
User group memberships that expire This patch adds an ug_expiry column to the user_groups table, a timestamp giving a date when the user group expires. A new UserGroupMembership class, based on the Block class, manages entries in this table. When the expiry date passes, the row in user_groups is ignored, and will eventually be purged from the DB when UserGroupMembership::insert is next called. Old, expired user group memberships are not kept; instead, the log entries are available to find the history of these memberships, similar to the way it has always worked for blocks and protections. Anyone getting user group info through the User object will get correct information. However, code that reads the user_groups table directly will now need to skip over rows with ug_expiry < wfTimestampNow(). See UsersPager for an example of how to do this. NULL is used to represent infinite (no) expiry, rather than a string 'infinity' or similar (except in the API). This allows existing user group assignments and log entries, which are all infinite in duration, to be treated the same as new, infinite-length memberships, without special casing everything. The whole thing is behind the temporary feature flag $wgDisableUserGroupExpiry, in accordance with the WMF schema change policy. The opportunity has been taken to refactor some static user-group-related functions out of User into UserGroupMembership, and also to add a primary key (ug_user, ug_group) to the user_groups table. There are a few breaking changes: - UserRightsProxy-like objects are now required to have a getGroupMemberships() function. - $user->mGroups (on a User object) is no longer present. - Some protected functions in UsersPager are altered or removed. - The UsersPagerDoBatchLookups hook (unused in any Wikimedia Git-hosted extension) has a change of parameter. Bug: T12493 Depends-On: Ia9616e1e35184fed9058d2d39afbe1038f56d7fa Depends-On: I86eb1d5619347ce54a5f33a591417742ebe5d6f8 Change-Id: I93c955dc7a970f78e32aa503c01c67da30971d1a
2017-01-12 06:07:56 +00:00
* @return array Array containing an error message key and any parameters
*/
private function missingPermissionError( $action, $short ) {
// We avoid expensive display logic for quickUserCan's and such
if ( $short ) {
return [ 'badaccess-group0' ];
}
User group memberships that expire This patch adds an ug_expiry column to the user_groups table, a timestamp giving a date when the user group expires. A new UserGroupMembership class, based on the Block class, manages entries in this table. When the expiry date passes, the row in user_groups is ignored, and will eventually be purged from the DB when UserGroupMembership::insert is next called. Old, expired user group memberships are not kept; instead, the log entries are available to find the history of these memberships, similar to the way it has always worked for blocks and protections. Anyone getting user group info through the User object will get correct information. However, code that reads the user_groups table directly will now need to skip over rows with ug_expiry < wfTimestampNow(). See UsersPager for an example of how to do this. NULL is used to represent infinite (no) expiry, rather than a string 'infinity' or similar (except in the API). This allows existing user group assignments and log entries, which are all infinite in duration, to be treated the same as new, infinite-length memberships, without special casing everything. The whole thing is behind the temporary feature flag $wgDisableUserGroupExpiry, in accordance with the WMF schema change policy. The opportunity has been taken to refactor some static user-group-related functions out of User into UserGroupMembership, and also to add a primary key (ug_user, ug_group) to the user_groups table. There are a few breaking changes: - UserRightsProxy-like objects are now required to have a getGroupMemberships() function. - $user->mGroups (on a User object) is no longer present. - Some protected functions in UsersPager are altered or removed. - The UsersPagerDoBatchLookups hook (unused in any Wikimedia Git-hosted extension) has a change of parameter. Bug: T12493 Depends-On: Ia9616e1e35184fed9058d2d39afbe1038f56d7fa Depends-On: I86eb1d5619347ce54a5f33a591417742ebe5d6f8 Change-Id: I93c955dc7a970f78e32aa503c01c67da30971d1a
2017-01-12 06:07:56 +00:00
return User::newFatalPermissionDeniedStatus( $action )->getErrorsArray()[0];
}
/**
* Can $user perform $action on this page? This is an internal function,
* with multiple levels of checks depending on performance needs; see $rigor below.
* It does not check wfReadOnly().
*
* @param string $action Action that permission needs to be checked for
* @param User $user User to check
* @param string $rigor One of (quick,full,secure)
* - quick : does cheap permission checks from replica DBs (usable for GUI creation)
* - full : does cheap and expensive checks possibly from a replica DB
* - secure : does cheap and expensive checks, using the master as needed
* @param bool $short Set this to true to stop after the first permission error.
* @return array Array of arrays of the arguments to wfMessage to explain permissions problems.
*/
protected function getUserPermissionsErrorsInternal(
$action, $user, $rigor = 'secure', $short = false
) {
if ( $rigor === true ) {
$rigor = 'secure'; // b/c
} elseif ( $rigor === false ) {
$rigor = 'quick'; // b/c
} elseif ( !in_array( $rigor, [ 'quick', 'full', 'secure' ] ) ) {
throw new Exception( "Invalid rigor parameter '$rigor'." );
}
# Read has special handling
if ( $action == 'read' ) {
$checks = [
'checkPermissionHooks',
'checkReadPermissions',
'checkUserBlock', // for wgBlockDisablesLogin
];
# Don't call checkSpecialsAndNSPermissions, checkSiteConfigPermissions
# or checkUserConfigPermissions here as it will lead to duplicate
# error messages. This is okay to do since anywhere that checks for
# create will also check for edit, and those checks are called for edit.
} elseif ( $action == 'create' ) {
$checks = [
'checkQuickPermissions',
'checkPermissionHooks',
'checkPageRestrictions',
'checkCascadingSourcesRestrictions',
'checkActionPermissions',
'checkUserBlock'
];
} else {
$checks = [
'checkQuickPermissions',
'checkPermissionHooks',
'checkSpecialsAndNSPermissions',
'checkSiteConfigPermissions',
'checkUserConfigPermissions',
'checkPageRestrictions',
'checkCascadingSourcesRestrictions',
'checkActionPermissions',
'checkUserBlock'
];
}
$errors = [];
while ( count( $checks ) > 0 &&
2011-12-11 14:48:45 +00:00
!( $short && count( $errors ) > 0 ) ) {
$method = array_shift( $checks );
$errors = $this->$method( $action, $user, $errors, $rigor, $short );
2011-12-11 14:48:45 +00:00
}
return $errors;
}
/**
* Get a filtered list of all restriction types supported by this wiki.
* @param bool $exists True to get all restriction types that apply to
* titles that do exist, False for all restriction types that apply to
* titles that do not exist
* @return array
*/
public static function getFilteredRestrictionTypes( $exists = true ) {
global $wgRestrictionTypes;
$types = $wgRestrictionTypes;
if ( $exists ) {
# Remove the create restriction for existing titles
$types = array_diff( $types, [ 'create' ] );
2011-12-11 14:48:45 +00:00
} else {
# Only the create and upload restrictions apply to non-existing titles
$types = array_intersect( $types, [ 'create', 'upload' ] );
2011-12-11 14:48:45 +00:00
}
return $types;
}
/**
* Returns restriction types for the current Title
*
* @return array Applicable restriction types
2011-12-11 14:48:45 +00:00
*/
public function getRestrictionTypes() {
if ( $this->isSpecialPage() ) {
return [];
2011-12-11 14:48:45 +00:00
}
$types = self::getFilteredRestrictionTypes( $this->exists() );
if ( $this->mNamespace != NS_FILE ) {
2011-12-11 14:48:45 +00:00
# Remove the upload restriction for non-file titles
$types = array_diff( $types, [ 'upload' ] );
}
Hooks::run( 'TitleGetRestrictionTypes', [ $this, &$types ] );
2011-12-11 14:48:45 +00:00
wfDebug( __METHOD__ . ': applicable restrictions to [[' .
$this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" );
return $types;
2003-04-14 23:10:40 +00:00
}
/**
* Is this title subject to title protection?
* Title protection is the one applied against creation of such title.
*
* @return array|bool An associative array representing any existent title
* protection, or false if there's none.
*/
public function getTitleProtection() {
$protection = $this->getTitleProtectionInternal();
if ( $protection ) {
if ( $protection['permission'] == 'sysop' ) {
$protection['permission'] = 'editprotected'; // B/C
}
if ( $protection['permission'] == 'autoconfirmed' ) {
$protection['permission'] = 'editsemiprotected'; // B/C
}
}
return $protection;
}
/**
* Fetch title protection settings
*
* To work correctly, $this->loadRestrictions() needs to have access to the
* actual protections in the database without munging 'sysop' =>
* 'editprotected' and 'autoconfirmed' => 'editsemiprotected'. Other
* callers probably want $this->getTitleProtection() instead.
*
* @return array|bool
*/
protected function getTitleProtectionInternal() {
// Can't protect pages in special namespaces
if ( $this->mNamespace < 0 ) {
return false;
}
// Can't protect pages that exist.
if ( $this->exists() ) {
return false;
}
if ( $this->mTitleProtection === null ) {
$dbr = wfGetDB( DB_REPLICA );
$commentStore = CommentStore::getStore();
$commentQuery = $commentStore->getJoin( 'pt_reason' );
$res = $dbr->select(
[ 'protected_titles' ] + $commentQuery['tables'],
[
'user' => 'pt_user',
'expiry' => 'pt_expiry',
'permission' => 'pt_create_perm'
] + $commentQuery['fields'],
[ 'pt_namespace' => $this->mNamespace, 'pt_title' => $this->mDbkeyform ],
__METHOD__,
[],
$commentQuery['joins']
);
// fetchRow returns false if there are no rows.
$row = $dbr->fetchRow( $res );
if ( $row ) {
$this->mTitleProtection = [
'user' => $row['user'],
'expiry' => $dbr->decodeExpiry( $row['expiry'] ),
'permission' => $row['permission'],
'reason' => $commentStore->getComment( 'pt_reason', $row )->text,
];
} else {
$this->mTitleProtection = false;
}
}
return $this->mTitleProtection;
}
/**
2011-12-11 14:48:45 +00:00
* Remove any title protection due to page existing
*/
2011-12-11 14:48:45 +00:00
public function deleteTitleProtection() {
$dbw = wfGetDB( DB_MASTER );
$dbw->delete(
'protected_titles',
[ 'pt_namespace' => $this->mNamespace, 'pt_title' => $this->mDbkeyform ],
2011-12-11 14:48:45 +00:00
__METHOD__
);
$this->mTitleProtection = false;
}
/**
* Is this page "semi-protected" - the *only* protection levels are listed
* in $wgSemiprotectedRestrictionLevels?
*
* @param string $action Action to check (default: edit)
* @return bool
*/
2011-12-11 14:48:45 +00:00
public function isSemiProtected( $action = 'edit' ) {
global $wgSemiprotectedRestrictionLevels;
$restrictions = $this->getRestrictions( $action );
$semi = $wgSemiprotectedRestrictionLevels;
if ( !$restrictions || !$semi ) {
// Not protected, or all protection is full protection
2011-12-11 14:48:45 +00:00
return false;
}
// Remap autoconfirmed to editsemiprotected for BC
foreach ( array_keys( $semi, 'autoconfirmed' ) as $key ) {
$semi[$key] = 'editsemiprotected';
}
foreach ( array_keys( $restrictions, 'autoconfirmed' ) as $key ) {
$restrictions[$key] = 'editsemiprotected';
}
return !array_diff( $restrictions, $semi );
}
/**
2011-12-11 14:48:45 +00:00
* Does the title correspond to a protected article?
*
* @param string $action The action the page is protected from,
2011-12-11 14:48:45 +00:00
* by default checks all actions.
* @return bool
*/
2011-12-11 14:48:45 +00:00
public function isProtected( $action = '' ) {
global $wgRestrictionLevels;
$restrictionTypes = $this->getRestrictionTypes();
# Special pages have inherent protection
if ( $this->isSpecialPage() ) {
2011-12-11 14:48:45 +00:00
return true;
}
# Check regular protection levels
foreach ( $restrictionTypes as $type ) {
if ( $action == $type || $action == '' ) {
$r = $this->getRestrictions( $type );
foreach ( $wgRestrictionLevels as $level ) {
if ( in_array( $level, $r ) && $level != '' ) {
return true;
}
}
}
}
return false;
}
2010-07-25 15:53:22 +00:00
/**
2011-12-11 14:48:45 +00:00
* Determines if $user is unable to edit this page because it has been protected
* by $wgNamespaceProtection.
*
* @param User $user User object to check permissions
* @return bool
*/
2011-12-11 14:48:45 +00:00
public function isNamespaceProtected( User $user ) {
global $wgNamespaceProtection;
if ( isset( $wgNamespaceProtection[$this->mNamespace] ) ) {
foreach ( (array)$wgNamespaceProtection[$this->mNamespace] as $right ) {
if ( $right != '' && !$user->isAllowed( $right ) ) {
return true;
}
}
}
return false;
}
/**
* Cascading protection: Return true if cascading restrictions apply to this page, false if not.
*
* @return bool If the page is subject to cascading restrictions.
*/
public function isCascadeProtected() {
Static code analysis housekeeping time... things that could be double-checked are marked with "[Note: some-comment]" : if-if-else without curly braces [api/ApiQuerySiteinfo.php] --> adding Unused global declaration: $wgGroupPermissions --> removing Unused global declaration: $wgEmailConfirmToEdit (line 301) --> removing Variable $id appears only once (line 1021) --> removing Variable $m was used before it was defined (line 805) --> defining. Variable $retval was used before it was defined (line 2346) --> renaming to $result Variable $rcid appears only once (line 244 of RecentChange.php) --> using this instead of $change [Note: was left over from r24607 refactoring, revert if wrong please] Unused global declaration: $wgCommandLineMode (line 11) --> removing Variable $k appears only once (line 132 of ImagePage.php) --> removing. Variable $info appears only once (line 311 of ImagePage.php) --> removing. Unused global declaration: $wgTitle (line 569 of ImagePage.php) -> removing. Variable $handlerParams was used before it was defined (line 616 of Linker.php) --> resolved by Raymond in r24966 Variable $match was used before it was defined (line 1031 of Linker.php) --> defining. Unused global declaration: $wgEnotifWatchlist (line 253 of UserMailer.php) --> removing Unused global declaration: $wgShowUpdatedMarker (line 253 of UserMailer.php) --> removing Variable $img appears only once (line 446 of SpecialUpload.php) --> added definition, defined as null, flagged with @todo [Note: should $img be defined in this context, or is it intended to be null? And should the return value after the hook be checked in some way?] Unused global declaration: $wgEnableAPI (line 739 of SpecialUpload.php) --> removing. Unused global declaration: $wgNamespaceProtection (line 1030 of OutputPage.php) --> removing. Unused global declaration: $wgContLang (line 18 of SpecialWatchlist.php) --> removing. Unused global declaration: $wgRawHtml (line 269 of SpecialMovepage.php) --> removing. The value of variable $page was never used (line 331 of SpecialUndelete.php) --> removing line, as $page gets redefined a few lines down. Variable $synIndex appears only once (line 521 of MagicWord.php) --> commenting out. Variable $case appears only once (line 539 of MagicWord.php) --> removing from foreach index key usage. Variable $wgUser appears only once (line 1039 of Title.php) --> adding line to declare as a global, would be null otherwise. Variable $m was used before it was defined (line 285 of Title.php) --> defining. Variable $id appears only once (line 1150 of Title.php) --> removing from foreach index key usage. Variable $subpage appears only once (line 1297 of Title.php) --> commenting out. Variable $restrictions appears only once (line 1399 of Title.php) --> commenting out. Variable $mime appears only once (line 210 of filerepo/OldLocalFile.php) --> removing. Variable $deprefixedName appears only once (line 213 of filerepo/LocalFile.php) --> removing. Variable $m appears only once (line 541 of filerepo/LocalFile.php) --> removing. Variable $where appears only once (line 1245 of filerepo/LocalFile.php) --> removing. Variable $info appears only once (line 1427 of filerepo/LocalFile.php) --> removing. Variable $rel appears only once (line 138 of filerepo/RepoGroup.php) --> commenting out. Variable $zone appears only once (line 138 of filerepo/RepoGroup.php) --> commenting out. Variable $nbytes appears only once (line 208 of media/Generic.php) --> added a return line that uses $nbytes. [Note: I'm assuming that this was the intent] Variable $offset appears only once (line 201 of SpecialListusers.php) --> removing. Variable $limit appears only once (line 201 of SpecialListusers.php) --> removing. Variable $groupTarget appears only once (line 203 of SpecialListusers.php) --> removing. Unused global declaration: $wgLang (line 74 of SpecialWantedpages.php) --> removing. Variable $block appears only once (line 244 of SpecialProtectedpages.php) --> removing. Variable $offset appears only once (line 281 of SpecialProtectedpages.php) --> removing. Variable $limit appears only once (line 281 of SpecialProtectedpages.php) --> removing. Unused global declaration: $wgLang (line 30 of FileDeleteForm.php) --> removing. Unused global declaration: $wgServer (line 30 of FileDeleteForm.php) --> removing.
2007-08-21 03:57:54 +00:00
list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false );
return ( $sources > 0 );
}
/**
* Determines whether cascading protection sources have already been loaded from
* the database.
*
* @param bool $getPages True to check if the pages are loaded, or false to check
* if the status is loaded.
* @return bool Whether or not the specified information has been loaded
* @since 1.23
*/
public function areCascadeProtectionSourcesLoaded( $getPages = true ) {
return $getPages ? $this->mCascadeSources !== null : $this->mHasCascadingRestrictions !== null;
}
/**
* Cascading protection: Get the source of any cascading restrictions on this page.
*
* @param bool $getPages Whether or not to retrieve the actual pages
* that the restrictions have come from and the actual restrictions
* themselves.
* @return array Two elements: First is an array of Title objects of the
* pages from which cascading restrictions have come, false for
* none, or true if such restrictions exist but $getPages was not
* set. Second is an array like that returned by
* Title::getAllRestrictions(), or an empty array if $getPages is
* false.
*/
public function getCascadeProtectionSources( $getPages = true ) {
$pagerestrictions = [];
if ( $this->mCascadeSources !== null && $getPages ) {
return [ $this->mCascadeSources, $this->mCascadingRestrictions ];
} elseif ( $this->mHasCascadingRestrictions !== null && !$getPages ) {
return [ $this->mHasCascadingRestrictions, $pagerestrictions ];
}
$dbr = wfGetDB( DB_REPLICA );
if ( $this->mNamespace == NS_FILE ) {
$tables = [ 'imagelinks', 'page_restrictions' ];
$where_clauses = [
'il_to' => $this->mDbkeyform,
'il_from=pr_page',
2010-07-25 15:53:22 +00:00
'pr_cascade' => 1
];
} else {
$tables = [ 'templatelinks', 'page_restrictions' ];
$where_clauses = [
'tl_namespace' => $this->mNamespace,
'tl_title' => $this->mDbkeyform,
'tl_from=pr_page',
2010-07-25 15:53:22 +00:00
'pr_cascade' => 1
];
}
if ( $getPages ) {
$cols = [ 'pr_page', 'page_namespace', 'page_title',
'pr_expiry', 'pr_type', 'pr_level' ];
$where_clauses[] = 'page_id=pr_page';
2007-01-12 09:10:30 +00:00
$tables[] = 'page';
2007-01-12 12:46:01 +00:00
} else {
$cols = [ 'pr_expiry' ];
}
$res = $dbr->select( $tables, $cols, $where_clauses, __METHOD__ );
$sources = $getPages ? [] : false;
$now = wfTimestampNow();
foreach ( $res as $row ) {
Clean up handling of 'infinity' There's a bunch of stuff that probably only works because the database representation of infinity is actually 'infinity' on all databases besides Oracle, and Oracle in general isn't maintained. Generally, we should probably use 'infinity' everywhere except where directly dealing with the database. * Many extension callers of Language::formatExpiry() with $format !== true are assuming it'll return 'infinity', none are checking for $db->getInfinity(). * And Language::formatExpiry() would choke if passed 'infinity', despite callers doing this. * And Language::formatExpiry() could be more useful for the API if we can override the string returned for infinity. * As for core, Title is using Language::formatExpiry() with TS_MW which is going to be changing anyway. Extension callers mostly don't exist. * Block already normalizes its mExpiry field (and ->getExpiry()), but some stuff is comparing it with $db->getInfinity() anyway. A few external users set mExpiry to $db->getInfinity(), but this is mostly because SpecialBlock::parseExpiryInput() returns $db->getInfinity() while most callers (including all extensions) are assuming 'infinity'. * And for that matter, Block should use $db->decodeExpiry() instead of manually doing it, once we make that safe to call with 'infinity' for all the extensions passing $db->getInfinity() to Block's contructor. * WikiPage::doUpdateRestrictions() and some of its callers are using $db->getInfinity(), when all the inserts using that value are using $db->encodeExpiry() which will convert 'infinity'. This also cleans up a slave-lag issue I noticed in ApiBlock while testing. Bug: T92550 Change-Id: I5eb68c1fb6029da8289276ecf7c81330575029ef
2015-03-12 16:37:04 +00:00
$expiry = $dbr->decodeExpiry( $row->pr_expiry );
if ( $expiry > $now ) {
if ( $getPages ) {
$page_id = $row->pr_page;
$page_ns = $row->page_namespace;
$page_title = $row->page_title;
$sources[$page_id] = self::makeTitle( $page_ns, $page_title );
# Add groups needed for each restriction type if its not already there
# Make sure this restriction type still exists
if ( !isset( $pagerestrictions[$row->pr_type] ) ) {
$pagerestrictions[$row->pr_type] = [];
}
if (
isset( $pagerestrictions[$row->pr_type] )
&& !in_array( $row->pr_level, $pagerestrictions[$row->pr_type] )
) {
$pagerestrictions[$row->pr_type][] = $row->pr_level;
}
} else {
$sources = true;
}
2007-01-12 09:10:30 +00:00
}
}
if ( $getPages ) {
$this->mCascadeSources = $sources;
$this->mCascadingRestrictions = $pagerestrictions;
2007-01-12 09:10:30 +00:00
} else {
$this->mHasCascadingRestrictions = $sources;
}
return [ $sources, $pagerestrictions ];
}
/**
* Accessor for mRestrictionsLoaded
*
* @return bool Whether or not the page's restrictions have already been
* loaded from the database
* @since 1.23
*/
public function areRestrictionsLoaded() {
return $this->mRestrictionsLoaded;
}
2011-12-11 14:48:45 +00:00
/**
* Accessor/initialisation for mRestrictions
*
* @param string $action Action that permission needs to be checked for
* @return array Restriction levels needed to take the action. All levels are
* required. Note that restriction levels are normally user rights, but 'sysop'
* and 'autoconfirmed' are also allowed for backwards compatibility. These should
* be mapped to 'editprotected' and 'editsemiprotected' respectively.
2011-12-11 14:48:45 +00:00
*/
public function getRestrictions( $action ) {
if ( !$this->mRestrictionsLoaded ) {
$this->loadRestrictions();
}
return $this->mRestrictions[$action] ?? [];
2011-12-11 14:48:45 +00:00
}
/**
* Accessor/initialisation for mRestrictions
*
* @return array Keys are actions, values are arrays as returned by
* Title::getRestrictions()
* @since 1.23
*/
public function getAllRestrictions() {
if ( !$this->mRestrictionsLoaded ) {
$this->loadRestrictions();
}
return $this->mRestrictions;
}
2011-12-11 14:48:45 +00:00
/**
* Get the expiry time for the restriction against a given action
*
* @param string $action
* @return string|bool 14-char timestamp, or 'infinity' if the page is protected forever
* or not protected at all, or false if the action is not recognised.
2011-12-11 14:48:45 +00:00
*/
public function getRestrictionExpiry( $action ) {
if ( !$this->mRestrictionsLoaded ) {
$this->loadRestrictions();
}
return $this->mRestrictionsExpiry[$action] ?? false;
2011-12-11 14:48:45 +00:00
}
/**
* Returns cascading restrictions for the current article
*
* @return bool
*/
function areRestrictionsCascading() {
if ( !$this->mRestrictionsLoaded ) {
$this->loadRestrictions();
}
return $this->mCascadeRestriction;
}
/**
* Compiles list of active page restrictions from both page table (pre 1.10)
* and page_restrictions table for this existing page.
* Public for usage by LiquidThreads.
*
* @param array $rows Array of db result objects
* @param string|null $oldFashionedRestrictions Comma-separated set of permission keys
* indicating who can move or edit the page from the page table, (pre 1.10) rows.
* Edit and move sections are separated by a colon
* Example: "edit=autoconfirmed,sysop:move=sysop"
*/
public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) {
$dbr = wfGetDB( DB_REPLICA );
$restrictionTypes = $this->getRestrictionTypes();
foreach ( $restrictionTypes as $type ) {
$this->mRestrictions[$type] = [];
Clean up handling of 'infinity' There's a bunch of stuff that probably only works because the database representation of infinity is actually 'infinity' on all databases besides Oracle, and Oracle in general isn't maintained. Generally, we should probably use 'infinity' everywhere except where directly dealing with the database. * Many extension callers of Language::formatExpiry() with $format !== true are assuming it'll return 'infinity', none are checking for $db->getInfinity(). * And Language::formatExpiry() would choke if passed 'infinity', despite callers doing this. * And Language::formatExpiry() could be more useful for the API if we can override the string returned for infinity. * As for core, Title is using Language::formatExpiry() with TS_MW which is going to be changing anyway. Extension callers mostly don't exist. * Block already normalizes its mExpiry field (and ->getExpiry()), but some stuff is comparing it with $db->getInfinity() anyway. A few external users set mExpiry to $db->getInfinity(), but this is mostly because SpecialBlock::parseExpiryInput() returns $db->getInfinity() while most callers (including all extensions) are assuming 'infinity'. * And for that matter, Block should use $db->decodeExpiry() instead of manually doing it, once we make that safe to call with 'infinity' for all the extensions passing $db->getInfinity() to Block's contructor. * WikiPage::doUpdateRestrictions() and some of its callers are using $db->getInfinity(), when all the inserts using that value are using $db->encodeExpiry() which will convert 'infinity'. This also cleans up a slave-lag issue I noticed in ApiBlock while testing. Bug: T92550 Change-Id: I5eb68c1fb6029da8289276ecf7c81330575029ef
2015-03-12 16:37:04 +00:00
$this->mRestrictionsExpiry[$type] = 'infinity';
}
$this->mCascadeRestriction = false;
# Backwards-compatibility: also load the restrictions from the page record (old format).
if ( $oldFashionedRestrictions !== null ) {
$this->mOldRestrictions = $oldFashionedRestrictions;
}
if ( $this->mOldRestrictions === false ) {
$this->mOldRestrictions = $dbr->selectField( 'page', 'page_restrictions',
[ 'page_id' => $this->getArticleID() ], __METHOD__ );
}
if ( $this->mOldRestrictions != '' ) {
foreach ( explode( ':', trim( $this->mOldRestrictions ) ) as $restrict ) {
$temp = explode( '=', trim( $restrict ) );
if ( count( $temp ) == 1 ) {
// old old format should be treated as edit/move restriction
$this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) );
$this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) );
} else {
$restriction = trim( $temp[1] );
if ( $restriction != '' ) { // some old entries are empty
$this->mRestrictions[$temp[0]] = explode( ',', $restriction );
}
}
}
}
if ( count( $rows ) ) {
# Current system - load second to make them override.
$now = wfTimestampNow();
# Cycle through all the restrictions.
foreach ( $rows as $row ) {
// Don't take care of restrictions types that aren't allowed
if ( !in_array( $row->pr_type, $restrictionTypes ) ) {
continue;
}
Clean up handling of 'infinity' There's a bunch of stuff that probably only works because the database representation of infinity is actually 'infinity' on all databases besides Oracle, and Oracle in general isn't maintained. Generally, we should probably use 'infinity' everywhere except where directly dealing with the database. * Many extension callers of Language::formatExpiry() with $format !== true are assuming it'll return 'infinity', none are checking for $db->getInfinity(). * And Language::formatExpiry() would choke if passed 'infinity', despite callers doing this. * And Language::formatExpiry() could be more useful for the API if we can override the string returned for infinity. * As for core, Title is using Language::formatExpiry() with TS_MW which is going to be changing anyway. Extension callers mostly don't exist. * Block already normalizes its mExpiry field (and ->getExpiry()), but some stuff is comparing it with $db->getInfinity() anyway. A few external users set mExpiry to $db->getInfinity(), but this is mostly because SpecialBlock::parseExpiryInput() returns $db->getInfinity() while most callers (including all extensions) are assuming 'infinity'. * And for that matter, Block should use $db->decodeExpiry() instead of manually doing it, once we make that safe to call with 'infinity' for all the extensions passing $db->getInfinity() to Block's contructor. * WikiPage::doUpdateRestrictions() and some of its callers are using $db->getInfinity(), when all the inserts using that value are using $db->encodeExpiry() which will convert 'infinity'. This also cleans up a slave-lag issue I noticed in ApiBlock while testing. Bug: T92550 Change-Id: I5eb68c1fb6029da8289276ecf7c81330575029ef
2015-03-12 16:37:04 +00:00
$expiry = $dbr->decodeExpiry( $row->pr_expiry );
// Only apply the restrictions if they haven't expired!
if ( !$expiry || $expiry > $now ) {
$this->mRestrictionsExpiry[$row->pr_type] = $expiry;
$this->mRestrictions[$row->pr_type] = explode( ',', trim( $row->pr_level ) );
$this->mCascadeRestriction |= $row->pr_cascade;
}
}
}
$this->mRestrictionsLoaded = true;
}
2008-08-11 04:39:00 +00:00
/**
* Load restrictions from the page_restrictions table
*
* @param string|null $oldFashionedRestrictions Comma-separated set of permission keys
* indicating who can move or edit the page from the page table, (pre 1.10) rows.
* Edit and move sections are separated by a colon
* Example: "edit=autoconfirmed,sysop:move=sysop"
2008-08-11 04:39:00 +00:00
*/
public function loadRestrictions( $oldFashionedRestrictions = null ) {
if ( $this->mRestrictionsLoaded ) {
return;
}
$id = $this->getArticleID();
if ( $id ) {
$cache = ObjectCache::getMainWANInstance();
$fname = __METHOD__;
$rows = $cache->getWithSetCallback(
// Page protections always leave a new null revision
$cache->makeKey( 'page-restrictions', $id, $this->getLatestRevID() ),
$cache::TTL_DAY,
function ( $curValue, &$ttl, array &$setOpts ) use ( $fname ) {
$dbr = wfGetDB( DB_REPLICA );
$setOpts += Database::getCacheSetOptions( $dbr );
return iterator_to_array(
$dbr->select(
'page_restrictions',
[ 'pr_type', 'pr_expiry', 'pr_level', 'pr_cascade' ],
[ 'pr_page' => $this->getArticleID() ],
$fname
)
);
}
);
$this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions );
} else {
$title_protection = $this->getTitleProtectionInternal();
if ( $title_protection ) {
$now = wfTimestampNow();
$expiry = wfGetDB( DB_REPLICA )->decodeExpiry( $title_protection['expiry'] );
if ( !$expiry || $expiry > $now ) {
// Apply the restrictions
$this->mRestrictionsExpiry['create'] = $expiry;
$this->mRestrictions['create'] =
explode( ',', trim( $title_protection['permission'] ) );
} else { // Get rid of the old restrictions
$this->mTitleProtection = false;
}
} else {
$this->mRestrictionsExpiry['create'] = 'infinity';
}
$this->mRestrictionsLoaded = true;
}
}
/**
* Flush the protection cache in this object and force reload from the database.
* This is used when updating protection from WikiPage::doUpdateRestrictions().
*/
public function flushRestrictions() {
$this->mRestrictionsLoaded = false;
$this->mTitleProtection = null;
}
/**
* Purge expired restrictions from the page_restrictions table
*
* This will purge no more than $wgUpdateRowsPerQuery page_restrictions rows
*/
static function purgeExpiredRestrictions() {
if ( wfReadOnly() ) {
return;
}
DeferredUpdates::addUpdate( new AtomicSectionUpdate(
wfGetDB( DB_MASTER ),
__METHOD__,
function ( IDatabase $dbw, $fname ) {
$config = MediaWikiServices::getInstance()->getMainConfig();
$ids = $dbw->selectFieldValues(
'page_restrictions',
'pr_id',
[ 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
$fname,
[ 'LIMIT' => $config->get( 'UpdateRowsPerQuery' ) ] // T135470
);
if ( $ids ) {
$dbw->delete( 'page_restrictions', [ 'pr_id' => $ids ], $fname );
}
}
) );
DeferredUpdates::addUpdate( new AtomicSectionUpdate(
wfGetDB( DB_MASTER ),
__METHOD__,
function ( IDatabase $dbw, $fname ) {
$dbw->delete(
'protected_titles',
[ 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ],
$fname
);
}
) );
}
/**
2011-12-11 14:48:45 +00:00
* Does this have subpages? (Warning, usually requires an extra DB query.)
*
* @return bool
*/
2011-12-11 14:48:45 +00:00
public function hasSubpages() {
if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
# Duh
return false;
2004-11-24 22:31:48 +00:00
}
2011-12-11 14:48:45 +00:00
# We dynamically add a member variable for the purpose of this method
# alone to cache the result. There's no point in having it hanging
# around uninitialized in every Title object; therefore we only add it
# if needed and don't declare it statically.
if ( $this->mHasSubpages === null ) {
$this->mHasSubpages = false;
$subpages = $this->getSubpages( 1 );
if ( $subpages instanceof TitleArray ) {
$this->mHasSubpages = (bool)$subpages->count();
}
2011-12-11 14:48:45 +00:00
}
return $this->mHasSubpages;
2003-04-14 23:10:40 +00:00
}
/**
2011-12-11 14:48:45 +00:00
* Get all subpages of this page.
*
* @param int $limit Maximum number of subpages to fetch; -1 for no limit
* @return TitleArray|array TitleArray, or empty array if this page's namespace
2011-12-11 14:48:45 +00:00
* doesn't allow subpages
*/
2011-12-11 14:48:45 +00:00
public function getSubpages( $limit = -1 ) {
if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
return [];
}
2011-12-11 14:48:45 +00:00
$dbr = wfGetDB( DB_REPLICA );
$conds['page_namespace'] = $this->mNamespace;
$conds[] = 'page_title ' . $dbr->buildLike( $this->mDbkeyform . '/', $dbr->anyString() );
$options = [];
2011-12-11 14:48:45 +00:00
if ( $limit > -1 ) {
$options['LIMIT'] = $limit;
}
return TitleArray::newFromResult(
2011-12-11 14:48:45 +00:00
$dbr->select( 'page',
[ 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ],
2011-12-11 14:48:45 +00:00
$conds,
__METHOD__,
$options
)
);
}
/**
* Is there a version of this page in the deletion archive?
*
* @return int The number of archived revisions
*/
2011-09-16 16:55:39 +00:00
public function isDeleted() {
if ( $this->mNamespace < 0 ) {
$n = 0;
} else {
$dbr = wfGetDB( DB_REPLICA );
$n = $dbr->selectField( 'archive', 'COUNT(*)',
[ 'ar_namespace' => $this->mNamespace, 'ar_title' => $this->mDbkeyform ],
__METHOD__
);
if ( $this->mNamespace == NS_FILE ) {
2011-09-16 16:55:39 +00:00
$n += $dbr->selectField( 'filearchive', 'COUNT(*)',
[ 'fa_name' => $this->mDbkeyform ],
__METHOD__
);
}
}
return (int)$n;
}
/**
* Is there a version of this page in the deletion archive?
*
* @return bool
*/
public function isDeletedQuick() {
if ( $this->mNamespace < 0 ) {
return false;
}
$dbr = wfGetDB( DB_REPLICA );
$deleted = (bool)$dbr->selectField( 'archive', '1',
[ 'ar_namespace' => $this->mNamespace, 'ar_title' => $this->mDbkeyform ],
__METHOD__
);
if ( !$deleted && $this->mNamespace == NS_FILE ) {
$deleted = (bool)$dbr->selectField( 'filearchive', '1',
[ 'fa_name' => $this->mDbkeyform ],
__METHOD__
);
}
return $deleted;
}
2003-04-14 23:10:40 +00:00
/**
* Get the article ID for this Title from the link cache,
* adding it if necessary
*
* @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select
* for update
* @return int The ID
*/
public function getArticleID( $flags = 0 ) {
if ( $this->mNamespace < 0 ) {
$this->mArticleID = 0;
return $this->mArticleID;
}
$linkCache = MediaWikiServices::getInstance()->getLinkCache();
if ( $flags & self::GAID_FOR_UPDATE ) {
$oldUpdate = $linkCache->forUpdate( true );
$linkCache->clearLink( $this );
$this->mArticleID = $linkCache->addLinkObj( $this );
$linkCache->forUpdate( $oldUpdate );
} else {
if ( $this->mArticleID == -1 ) {
$this->mArticleID = $linkCache->addLinkObj( $this );
}
}
2003-04-14 23:10:40 +00:00
return $this->mArticleID;
}
/**
* Is this an article that is a redirect page?
* Uses link cache, adding it if necessary
*
* @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
* @return bool
*/
public function isRedirect( $flags = 0 ) {
if ( !is_null( $this->mRedirect ) ) {
return $this->mRedirect;
}
if ( !$this->getArticleID( $flags ) ) {
$this->mRedirect = false;
return $this->mRedirect;
}
$linkCache = MediaWikiServices::getInstance()->getLinkCache();
$linkCache->addLinkObj( $this ); # in case we already had an article ID
$cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' );
if ( $cached === null ) {
# Trust LinkCache's state over our own
# LinkCache is telling us that the page doesn't exist, despite there being cached
# data relating to an existing page in $this->mArticleID. Updaters should clear
# LinkCache as appropriate, or use $flags = Title::GAID_FOR_UPDATE. If that flag is
# set, then LinkCache will definitely be up to date here, since getArticleID() forces
# LinkCache to refresh its data from the master.
$this->mRedirect = false;
return $this->mRedirect;
2012-06-11 14:19:39 +00:00
}
$this->mRedirect = (bool)$cached;
2008-04-09 16:32:14 +00:00
return $this->mRedirect;
}
/**
* What is the length of this page?
* Uses link cache, adding it if necessary
*
* @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
* @return int
*/
public function getLength( $flags = 0 ) {
if ( $this->mLength != -1 ) {
return $this->mLength;
}
if ( !$this->getArticleID( $flags ) ) {
$this->mLength = 0;
return $this->mLength;
}
$linkCache = MediaWikiServices::getInstance()->getLinkCache();
$linkCache->addLinkObj( $this ); # in case we already had an article ID
$cached = $linkCache->getGoodLinkFieldObj( $this, 'length' );
if ( $cached === null ) {
# Trust LinkCache's state over our own, as for isRedirect()
$this->mLength = 0;
return $this->mLength;
2012-06-11 14:19:39 +00:00
}
$this->mLength = intval( $cached );
2008-04-09 16:32:14 +00:00
return $this->mLength;
}
2003-04-14 23:10:40 +00:00
2008-05-06 14:54:17 +00:00
/**
2011-12-11 14:48:45 +00:00
* What is the page_latest field for this page?
*
* @param int $flags A bit field; may be Title::GAID_FOR_UPDATE to select for update
* @return int Int or 0 if the page doesn't exist
*/
2011-12-11 14:48:45 +00:00
public function getLatestRevID( $flags = 0 ) {
if ( !( $flags & self::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) {
return intval( $this->mLatestID );
2003-04-14 23:10:40 +00:00
}
if ( !$this->getArticleID( $flags ) ) {
$this->mLatestID = 0;
return $this->mLatestID;
}
$linkCache = MediaWikiServices::getInstance()->getLinkCache();
$linkCache->addLinkObj( $this ); # in case we already had an article ID
$cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' );
if ( $cached === null ) {
# Trust LinkCache's state over our own, as for isRedirect()
$this->mLatestID = 0;
return $this->mLatestID;
2012-06-11 14:19:39 +00:00
}
$this->mLatestID = intval( $cached );
2011-12-11 14:48:45 +00:00
return $this->mLatestID;
2003-04-14 23:10:40 +00:00
}
/**
2011-12-11 14:48:45 +00:00
* This clears some fields in this object, and clears any associated
* keys in the "bad links" section of the link cache.
*
* - This is called from WikiPage::doEditContent() and WikiPage::insertOn() to allow
2011-12-11 14:48:45 +00:00
* loading of the new page_id. It's also called from
* WikiPage::doDeleteArticleReal()
2011-12-11 14:48:45 +00:00
*
* @param int $newid The new Article ID
*/
2011-12-11 14:48:45 +00:00
public function resetArticleID( $newid ) {
$linkCache = MediaWikiServices::getInstance()->getLinkCache();
$linkCache->clearLink( $this );
if ( $newid === false ) {
$this->mArticleID = -1;
2011-12-11 14:48:45 +00:00
} else {
$this->mArticleID = intval( $newid );
2011-12-11 14:48:45 +00:00
}
$this->mRestrictionsLoaded = false;
$this->mRestrictions = [];
$this->mOldRestrictions = false;
$this->mRedirect = null;
$this->mLength = -1;
$this->mLatestID = false;
$this->mContentModel = false;
$this->mEstimateRevisions = null;
$this->mPageLanguage = false;
$this->mDbPageLanguage = false;
$this->mIsBigDeletion = null;
}
public static function clearCaches() {
$linkCache = MediaWikiServices::getInstance()->getLinkCache();
$linkCache->clear();
$titleCache = self::getTitleCache();
$titleCache->clear();
}
/**
* Capitalize a text string for a title if it belongs to a namespace that capitalizes
*
* @param string $text Containing title to capitalize
* @param int $ns Namespace index, defaults to NS_MAIN
* @return string Containing capitalized title
*/
public static function capitalize( $text, $ns = NS_MAIN ) {
2010-07-25 15:53:22 +00:00
if ( MWNamespace::isCapitalized( $ns ) ) {
return MediaWikiServices::getInstance()->getContentLanguage()->ucfirst( $text );
2010-07-25 15:53:22 +00:00
} else {
return $text;
2010-07-25 15:53:22 +00:00
}
}
/**
* Secure and split - main initialisation function for this object
*
* Assumes that mDbkeyform has been set, and is urldecoded
* and uses underscores, but not otherwise munged. This function
* removes illegal characters, splits off the interwiki and
* namespace prefixes, sets the other forms, and canonicalizes
* everything.
*
* @throws MalformedTitleException On invalid titles
* @return bool True on success
*/
private function secureAndSplit() {
// @note: splitTitleString() is a temporary hack to allow MediaWikiTitleCodec to share
// the parsing code with Title, while avoiding massive refactoring.
// @todo: get rid of secureAndSplit, refactor parsing code.
// @note: getTitleParser() returns a TitleParser implementation which does not have a
// splitTitleString method, but the only implementation (MediaWikiTitleCodec) does
$titleCodec = MediaWikiServices::getInstance()->getTitleParser();
// MalformedTitleException can be thrown here
$parts = $titleCodec->splitTitleString( $this->mDbkeyform, $this->mDefaultNamespace );
# Fill fields
$this->setFragment( '#' . $parts['fragment'] );
$this->mInterwiki = $parts['interwiki'];
$this->mLocalInterwiki = $parts['local_interwiki'];
$this->mNamespace = $parts['namespace'];
$this->mUserCaseDBKey = $parts['user_case_dbkey'];
$this->mDbkeyform = $parts['dbkey'];
$this->mUrlform = wfUrlencode( $this->mDbkeyform );
$this->mTextform = strtr( $this->mDbkeyform, '_', ' ' );
# We already know that some pages won't be in the database!
if ( $this->isExternal() || $this->isSpecialPage() ) {
$this->mArticleID = 0;
}
return true;
2003-04-14 23:10:40 +00:00
}
/**
* Get an array of Title objects linking to this Title
2005-05-04 00:33:08 +00:00
* Also stores the IDs in the link cache.
*
* WARNING: do not use this function on arbitrary user-supplied titles!
* On heavily-used templates it will max out the memory.
*
* @param array $options May be FOR UPDATE
* @param string $table Table name
* @param string $prefix Fields prefix
* @return Title[] Array of Title objects linking here
*/
public function getLinksTo( $options = [], $table = 'pagelinks', $prefix = 'pl' ) {
if ( count( $options ) > 0 ) {
$db = wfGetDB( DB_MASTER );
} else {
$db = wfGetDB( DB_REPLICA );
}
2010-07-25 15:53:22 +00:00
$res = $db->select(
[ 'page', $table ],
self::getSelectFields(),
[
"{$prefix}_from=page_id",
"{$prefix}_namespace" => $this->mNamespace,
"{$prefix}_title" => $this->mDbkeyform ],
__METHOD__,
2010-07-25 15:53:22 +00:00
$options
);
$retVal = [];
if ( $res->numRows() ) {
$linkCache = MediaWikiServices::getInstance()->getLinkCache();
foreach ( $res as $row ) {
$titleObj = self::makeTitle( $row->page_namespace, $row->page_title );
if ( $titleObj ) {
$linkCache->addGoodLinkObjFromRow( $titleObj, $row );
$retVal[] = $titleObj;
}
}
}
return $retVal;
}
/**
* Get an array of Title objects using this Title as a template
* Also stores the IDs in the link cache.
*
* WARNING: do not use this function on arbitrary user-supplied titles!
* On heavily-used templates it will max out the memory.
*
* @param array $options Query option to Database::select()
* @return Title[] Array of Title the Title objects linking here
*/
public function getTemplateLinksTo( $options = [] ) {
return $this->getLinksTo( $options, 'templatelinks', 'tl' );
}
/**
* Get an array of Title objects linked from this Title
* Also stores the IDs in the link cache.
*
* WARNING: do not use this function on arbitrary user-supplied titles!
* On heavily-used templates it will max out the memory.
*
* @param array $options Query option to Database::select()
* @param string $table Table name
* @param string $prefix Fields prefix
* @return array Array of Title objects linking here
*/
public function getLinksFrom( $options = [], $table = 'pagelinks', $prefix = 'pl' ) {
2012-03-11 22:05:54 +00:00
$id = $this->getArticleID();
# If the page doesn't exist; there can't be any link from this page
if ( !$id ) {
return [];
}
$db = wfGetDB( DB_REPLICA );
$blNamespace = "{$prefix}_namespace";
$blTitle = "{$prefix}_title";
$pageQuery = WikiPage::getQueryInfo();
$res = $db->select(
[ $table, 'nestpage' => $pageQuery['tables'] ],
array_merge(
[ $blNamespace, $blTitle ],
$pageQuery['fields']
),
[ "{$prefix}_from" => $id ],
__METHOD__,
$options,
[ 'nestpage' => [
'LEFT JOIN',
[ "page_namespace=$blNamespace", "page_title=$blTitle" ]
] ] + $pageQuery['joins']
);
$retVal = [];
$linkCache = MediaWikiServices::getInstance()->getLinkCache();
foreach ( $res as $row ) {
if ( $row->page_id ) {
$titleObj = self::newFromRow( $row );
} else {
$titleObj = self::makeTitle( $row->$blNamespace, $row->$blTitle );
$linkCache->addBadLinkObj( $titleObj );
}
$retVal[] = $titleObj;
}
return $retVal;
}
/**
* Get an array of Title objects used on this Title as a template
* Also stores the IDs in the link cache.
*
* WARNING: do not use this function on arbitrary user-supplied titles!
* On heavily-used templates it will max out the memory.
*
* @param array $options May be FOR UPDATE
* @return Title[] Array of Title the Title objects used here
*/
public function getTemplateLinksFrom( $options = [] ) {
return $this->getLinksFrom( $options, 'templatelinks', 'tl' );
}
2005-04-24 04:13:47 +00:00
/**
* Get an array of Title objects referring to non-existent articles linked
* from this page.
2005-04-24 04:13:47 +00:00
*
* @todo check if needed (used only in SpecialBrokenRedirects.php, and
* should use redirect table in this case).
* @return Title[] Array of Title the Title objects
2005-04-24 04:13:47 +00:00
*/
public function getBrokenLinksFrom() {
2012-03-11 22:05:54 +00:00
if ( $this->getArticleID() == 0 ) {
# All links from article ID 0 are false positives
return [];
}
$dbr = wfGetDB( DB_REPLICA );
$res = $dbr->select(
[ 'page', 'pagelinks' ],
[ 'pl_namespace', 'pl_title' ],
[
2012-03-11 22:05:54 +00:00
'pl_from' => $this->getArticleID(),
'page_namespace IS NULL'
],
__METHOD__, [],
[
'page' => [
'LEFT JOIN',
[ 'pl_namespace=page_namespace', 'pl_title=page_title' ]
]
]
);
$retVal = [];
foreach ( $res as $row ) {
$retVal[] = self::makeTitle( $row->pl_namespace, $row->pl_title );
2005-04-24 04:13:47 +00:00
}
return $retVal;
}
/**
* Get a list of URLs to purge from the CDN cache when this
* page changes
*
* @return string[] Array of String the URLs
*/
public function getCdnUrls() {
$urls = [
$this->getInternalURL(),
2004-08-16 20:14:35 +00:00
$this->getInternalURL( 'action=history' )
];
$pageLang = $this->getPageLanguage();
if ( $pageLang->hasVariants() ) {
$variants = $pageLang->getVariants();
foreach ( $variants as $vCode ) {
$urls[] = $this->getInternalURL( $vCode );
}
}
// If we are looking at a css/js user subpage, purge the action=raw.
if ( $this->isUserJsConfigPage() ) {
$urls[] = $this->getInternalURL( 'action=raw&ctype=text/javascript' );
} elseif ( $this->isUserJsonConfigPage() ) {
$urls[] = $this->getInternalURL( 'action=raw&ctype=application/json' );
} elseif ( $this->isUserCssConfigPage() ) {
$urls[] = $this->getInternalURL( 'action=raw&ctype=text/css' );
}
Hooks::run( 'TitleSquidURLs', [ $this, &$urls ] );
return $urls;
}
/**
* @deprecated since 1.27 use getCdnUrls()
*/
public function getSquidURLs() {
return $this->getCdnUrls();
}
2008-08-11 04:39:00 +00:00
/**
* Purge all applicable CDN URLs
2008-08-11 04:39:00 +00:00
*/
public function purgeSquid() {
DeferredUpdates::addUpdate(
new CdnCacheUpdate( $this->getCdnUrls() ),
DeferredUpdates::PRESEND
);
}
/**
* Check whether a given move operation would be valid.
* Returns true if ok, or a getUserPermissionsErrors()-like array otherwise
*
* @deprecated since 1.25, use MovePage's methods instead
* @param Title &$nt The new title
* @param bool $auth Whether to check user permissions (uses $wgUser)
* @param string $reason Is the log summary of the move, used for spam checking
* @return array|bool True on success, getUserPermissionsErrors()-like array on failure
*/
public function isValidMoveOperation( &$nt, $auth = true, $reason = '' ) {
global $wgUser;
if ( !( $nt instanceof Title ) ) {
// Normally we'd add this to $errors, but we'll get
// lots of syntax errors if $nt is not an object
return [ [ 'badtitletext' ] ];
}
$mp = new MovePage( $this, $nt );
$errors = $mp->isValidMove()->getErrorsArray();
if ( $auth ) {
$errors = wfMergeErrorArrays(
$errors,
$mp->checkPermissions( $wgUser, $reason )->getErrorsArray()
);
}
return $errors ?: true;
}
/**
* Check if the requested move target is a valid file move target
* @todo move this to MovePage
* @param Title $nt Target title
* @return array List of errors
*/
protected function validateFileMoveOperation( $nt ) {
global $wgUser;
$errors = [];
$destFile = wfLocalFile( $nt );
$destFile->load( File::READ_LATEST );
if ( !$wgUser->isAllowed( 'reupload-shared' )
&& !$destFile->exists() && wfFindFile( $nt )
) {
$errors[] = [ 'file-exists-sharedrepo' ];
}
return $errors;
}
/**
* Move a title to a new location
*
* @deprecated since 1.25, use the MovePage class instead
* @param Title &$nt The new title
* @param bool $auth Indicates whether $wgUser's permissions
* should be checked
* @param string $reason The reason for the move
* @param bool $createRedirect Whether to create a redirect from the old title to the new title.
* Ignored if the user doesn't have the suppressredirect right.
* @param array $changeTags Applied to the entry in the move log and redirect page revision
* @return array|bool True on success, getUserPermissionsErrors()-like array on failure
*/
public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true,
array $changeTags = []
) {
global $wgUser;
$err = $this->isValidMoveOperation( $nt, $auth, $reason );
if ( is_array( $err ) ) {
// Auto-block user's IP if the account was "hard" blocked
$wgUser->spreadAnyEditBlock();
return $err;
}
// Check suppressredirect permission
if ( $auth && !$wgUser->isAllowed( 'suppressredirect' ) ) {
$createRedirect = true;
}
$mp = new MovePage( $this, $nt );
$status = $mp->move( $wgUser, $reason, $createRedirect, $changeTags );
if ( $status->isOK() ) {
return true;
} else {
return $status->getErrorsArray();
}
}
/**
* Move this page's subpages to be subpages of $nt
*
* @param Title $nt Move target
* @param bool $auth Whether $wgUser's permissions should be checked
* @param string $reason The reason for the move
* @param bool $createRedirect Whether to create redirects from the old subpages to
* the new ones Ignored if the user doesn't have the 'suppressredirect' right
* @param array $changeTags Applied to the entry in the move log and redirect page revision
* @return array Array with old page titles as keys, and strings (new page titles) or
* getUserPermissionsErrors()-like arrays (errors) as values, or a
* getUserPermissionsErrors()-like error array with numeric indices if
* no pages were moved
*/
public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true,
array $changeTags = []
) {
2009-04-28 03:03:48 +00:00
global $wgMaximumMovedPages;
// Check permissions
2010-07-25 15:53:22 +00:00
if ( !$this->userCan( 'move-subpages' ) ) {
return [
[ 'cant-move-subpages' ],
];
2010-07-25 15:53:22 +00:00
}
// Do the source and target namespaces support subpages?
if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) {
return [
[ 'namespace-nosubpages', MWNamespace::getCanonicalName( $this->mNamespace ) ],
];
2010-07-25 15:53:22 +00:00
}
if ( !MWNamespace::hasSubpages( $nt->getNamespace() ) ) {
return [
[ 'namespace-nosubpages', MWNamespace::getCanonicalName( $nt->getNamespace() ) ],
];
2010-07-25 15:53:22 +00:00
}
$subpages = $this->getSubpages( $wgMaximumMovedPages + 1 );
$retval = [];
$count = 0;
foreach ( $subpages as $oldSubpage ) {
$count++;
if ( $count > $wgMaximumMovedPages ) {
$retval[$oldSubpage->getPrefixedText()] = [
[ 'movepage-max-pages', $wgMaximumMovedPages ],
];
break;
}
// We don't know whether this function was called before
// or after moving the root page, so check both
// $this and $nt
if ( $oldSubpage->getArticleID() == $this->getArticleID()
|| $oldSubpage->getArticleID() == $nt->getArticleID()
) {
// When moving a page to a subpage of itself,
// don't move it twice
continue;
2010-07-25 15:53:22 +00:00
}
$newPageName = preg_replace(
'#^' . preg_quote( $this->mDbkeyform, '#' ) . '#',
StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # T23234
$oldSubpage->getDBkey() );
if ( $oldSubpage->isTalkPage() ) {
$newNs = $nt->getTalkPage()->getNamespace();
} else {
$newNs = $nt->getSubjectPage()->getNamespace();
}
# T16385: we need makeTitleSafe because the new page names may
# be longer than 255 characters.
$newSubpage = self::makeTitleSafe( $newNs, $newPageName );
$success = $oldSubpage->moveTo( $newSubpage, $auth, $reason, $createRedirect, $changeTags );
if ( $success === true ) {
$retval[$oldSubpage->getPrefixedText()] = $newSubpage->getPrefixedText();
} else {
$retval[$oldSubpage->getPrefixedText()] = $success;
}
}
return $retval;
}
/**
* Checks if this page is just a one-rev redirect.
* Adds lock, so don't use just for light purposes.
*
* @return bool
*/
2008-11-13 22:20:51 +00:00
public function isSingleRevRedirect() {
global $wgContentHandlerUseDB;
$dbw = wfGetDB( DB_MASTER );
# Is it a redirect?
$fields = [ 'page_is_redirect', 'page_latest', 'page_id' ];
if ( $wgContentHandlerUseDB ) {
$fields[] = 'page_content_model';
}
$row = $dbw->selectRow( 'page',
$fields,
2008-11-13 22:20:51 +00:00
$this->pageCond(),
__METHOD__,
[ 'FOR UPDATE' ]
);
# Cache some fields we may want
$this->mArticleID = $row ? intval( $row->page_id ) : 0;
$this->mRedirect = $row ? (bool)$row->page_is_redirect : false;
$this->mLatestID = $row ? intval( $row->page_latest ) : false;
$this->mContentModel = $row && isset( $row->page_content_model )
? strval( $row->page_content_model )
: false;
if ( !$this->mRedirect ) {
return false;
}
# Does the article have a history?
$row = $dbw->selectField( [ 'page', 'revision' ],
'rev_id',
[ 'page_namespace' => $this->mNamespace,
'page_title' => $this->mDbkeyform,
'page_id=rev_page',
'page_latest != rev_id'
],
__METHOD__,
[ 'FOR UPDATE' ]
);
# Return true if there was no history
return ( $row === false );
}
/**
* Checks if $this can be moved to a given Title
* - Selects for update, so don't call it unless you mean business
*
* @deprecated since 1.25, use MovePage's methods instead
* @param Title $nt The new title to check
* @return bool
*/
public function isValidMoveTarget( $nt ) {
2010-07-23 17:11:20 +00:00
# Is it an existing file?
if ( $nt->getNamespace() == NS_FILE ) {
$file = wfLocalFile( $nt );
$file->load( File::READ_LATEST );
if ( $file->exists() ) {
wfDebug( __METHOD__ . ": file exists\n" );
return false;
}
}
# Is it a redirect with no history?
if ( !$nt->isSingleRevRedirect() ) {
wfDebug( __METHOD__ . ": not a one-rev redirect\n" );
return false;
}
# Get the article text
$rev = Revision::newFromTitle( $nt, false, Revision::READ_LATEST );
if ( !is_object( $rev ) ) {
return false;
}
$content = $rev->getContent();
# Does the redirect point to the source?
# Or is it a broken self-redirect, usually caused by namespace collisions?
$redirTitle = $content ? $content->getRedirectTarget() : null;
if ( $redirTitle ) {
if ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
$redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) {
wfDebug( __METHOD__ . ": redirect points to other page\n" );
return false;
} else {
return true;
}
} else {
# Fail safe (not a redirect after all. strange.)
wfDebug( __METHOD__ . ": failsafe: database sais " . $nt->getPrefixedDBkey() .
" is a redirect, but it doesn't contain a valid redirect.\n" );
return false;
}
}
/**
* Get categories to which this Title belongs and return an array of
* categories' names.
*
* @return array Array of parents in the form:
* $parent => $currentarticle
*/
public function getParentCategories() {
$data = [];
2012-03-11 22:05:54 +00:00
$titleKey = $this->getArticleID();
if ( $titleKey === 0 ) {
return $data;
}
$dbr = wfGetDB( DB_REPLICA );
$res = $dbr->select(
'categorylinks',
'cl_to',
[ 'cl_from' => $titleKey ],
__METHOD__
);
if ( $res->numRows() > 0 ) {
$contLang = MediaWikiServices::getInstance()->getContentLanguage();
foreach ( $res as $row ) {
// $data[] = Title::newFromText( $contLang->getNsText ( NS_CATEGORY ).':'.$row->cl_to);
$data[$contLang->getNsText( NS_CATEGORY ) . ':' . $row->cl_to] =
$this->getFullText();
}
}
return $data;
}
/**
* Get a tree of parent categories
*
* @param array $children Array with the children in the keys, to check for circular refs
* @return array Tree of parent categories
*/
public function getParentCategoryTree( $children = [] ) {
$stack = [];
$parents = $this->getParentCategories();
if ( $parents ) {
foreach ( $parents as $parent => $current ) {
if ( array_key_exists( $parent, $children ) ) {
# Circular reference
$stack[$parent] = [];
} else {
$nt = self::newFromText( $parent );
if ( $nt ) {
$stack[$parent] = $nt->getParentCategoryTree( $children + [ $parent => 1 ] );
}
}
}
}
return $stack;
}
/**
* Get an associative array for selecting this title from
2006-01-09 03:52:24 +00:00
* the "page" table
*
* @return array Array suitable for the $where parameter of DB::select()
*/
public function pageCond() {
if ( $this->mArticleID > 0 ) {
2008-12-08 23:10:24 +00:00
// PK avoids secondary lookups in InnoDB, shouldn't hurt other DBs
return [ 'page_id' => $this->mArticleID ];
} else {
return [ 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform ];
}
}
/**
* Get next/previous revision ID relative to another revision ID
* @param int $revId Revision ID. Get the revision that was before this one.
* @param int $flags Title::GAID_FOR_UPDATE
* @param string $dir 'next' or 'prev'
* @return int|bool New revision ID, or false if none exists
*/
private function getRelativeRevisionID( $revId, $flags, $dir ) {
$revId = (int)$revId;
if ( $dir === 'next' ) {
$op = '>';
$sort = 'ASC';
} elseif ( $dir === 'prev' ) {
$op = '<';
$sort = 'DESC';
} else {
throw new InvalidArgumentException( '$dir must be "next" or "prev"' );
}
if ( $flags & self::GAID_FOR_UPDATE ) {
$db = wfGetDB( DB_MASTER );
} else {
$db = wfGetDB( DB_REPLICA, 'contributions' );
}
// Intentionally not caring if the specified revision belongs to this
// page. We only care about the timestamp.
$ts = $db->selectField( 'revision', 'rev_timestamp', [ 'rev_id' => $revId ], __METHOD__ );
if ( $ts === false ) {
$ts = $db->selectField( 'archive', 'ar_timestamp', [ 'ar_rev_id' => $revId ], __METHOD__ );
if ( $ts === false ) {
// Or should this throw an InvalidArgumentException or something?
return false;
}
}
$ts = $db->addQuotes( $ts );
$revId = $db->selectField( 'revision', 'rev_id',
[
2012-03-11 22:05:54 +00:00
'rev_page' => $this->getArticleID( $flags ),
"rev_timestamp $op $ts OR (rev_timestamp = $ts AND rev_id $op $revId)"
],
__METHOD__,
[
'ORDER BY' => "rev_timestamp $sort, rev_id $sort",
'IGNORE INDEX' => 'rev_timestamp', // Probably needed for T159319
]
);
if ( $revId === false ) {
return false;
} else {
return intval( $revId );
}
}
/**
* Get the revision ID of the previous revision
*
* @param int $revId Revision ID. Get the revision that was before this one.
* @param int $flags Title::GAID_FOR_UPDATE
* @return int|bool Old revision ID, or false if none exists
*/
public function getPreviousRevisionID( $revId, $flags = 0 ) {
return $this->getRelativeRevisionID( $revId, $flags, 'prev' );
}
/**
* Get the revision ID of the next revision
*
* @param int $revId Revision ID. Get the revision that was after this one.
* @param int $flags Title::GAID_FOR_UPDATE
* @return int|bool Next revision ID, or false if none exists
*/
public function getNextRevisionID( $revId, $flags = 0 ) {
return $this->getRelativeRevisionID( $revId, $flags, 'next' );
}
/**
* Get the first revision of the page
*
* @param int $flags Title::GAID_FOR_UPDATE
* @return Revision|null If page doesn't exist
*/
public function getFirstRevision( $flags = 0 ) {
2012-03-11 22:05:54 +00:00
$pageId = $this->getArticleID( $flags );
if ( $pageId ) {
$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_REPLICA );
$revQuery = Revision::getQueryInfo();
$row = $db->selectRow( $revQuery['tables'], $revQuery['fields'],
[ 'rev_page' => $pageId ],
__METHOD__,
[
'ORDER BY' => 'rev_timestamp ASC, rev_id ASC',
'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319
],
$revQuery['joins']
);
if ( $row ) {
return new Revision( $row, 0, $this );
}
}
return null;
}
2008-12-21 09:33:04 +00:00
/**
* Get the oldest revision timestamp of this page
2008-12-21 09:33:04 +00:00
*
* @param int $flags Title::GAID_FOR_UPDATE
* @return string|null MW timestamp
2008-12-21 09:33:04 +00:00
*/
public function getEarliestRevTime( $flags = 0 ) {
$rev = $this->getFirstRevision( $flags );
return $rev ? $rev->getTimestamp() : null;
2008-12-21 09:33:04 +00:00
}
/**
* Check if this is a new page
*
* @return bool
*/
public function isNewPage() {
$dbr = wfGetDB( DB_REPLICA );
return (bool)$dbr->selectField( 'page', 'page_is_new', $this->pageCond(), __METHOD__ );
}
/**
* Check whether the number of revisions of this page surpasses $wgDeleteRevisionsLimit
*
* @return bool
*/
public function isBigDeletion() {
global $wgDeleteRevisionsLimit;
if ( !$wgDeleteRevisionsLimit ) {
return false;
}
if ( $this->mIsBigDeletion === null ) {
$dbr = wfGetDB( DB_REPLICA );
$revCount = $dbr->selectRowCount(
'revision',
'1',
[ 'rev_page' => $this->getArticleID() ],
__METHOD__,
[ 'LIMIT' => $wgDeleteRevisionsLimit + 1 ]
);
$this->mIsBigDeletion = $revCount > $wgDeleteRevisionsLimit;
}
return $this->mIsBigDeletion;
}
/**
* Get the approximate revision count of this page.
*
* @return int
*/
public function estimateRevisionCount() {
if ( !$this->exists() ) {
return 0;
}
if ( $this->mEstimateRevisions === null ) {
$dbr = wfGetDB( DB_REPLICA );
$this->mEstimateRevisions = $dbr->estimateRowCount( 'revision', '*',
[ 'rev_page' => $this->getArticleID() ], __METHOD__ );
}
return $this->mEstimateRevisions;
}
/**
* Get the number of revisions between the given revision.
2008-04-10 17:55:12 +00:00
* Used for diffs and other things that really need it.
*
* @param int|Revision $old Old revision or rev ID (first before range)
* @param int|Revision $new New revision or rev ID (first after range)
* @param int|null $max Limit of Revisions to count, will be incremented to detect truncations
* @return int Number of revisions between these revisions.
*/
public function countRevisionsBetween( $old, $new, $max = null ) {
if ( !( $old instanceof Revision ) ) {
$old = Revision::newFromTitle( $this, (int)$old );
}
if ( !( $new instanceof Revision ) ) {
$new = Revision::newFromTitle( $this, (int)$new );
}
if ( !$old || !$new ) {
return 0; // nothing to compare
}
$dbr = wfGetDB( DB_REPLICA );
$conds = [
'rev_page' => $this->getArticleID(),
'rev_timestamp > ' . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
'rev_timestamp < ' . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
];
if ( $max !== null ) {
return $dbr->selectRowCount( 'revision', '1',
$conds,
__METHOD__,
[ 'LIMIT' => $max + 1 ] // extra to detect truncation
);
} else {
return (int)$dbr->selectField( 'revision', 'count(*)', $conds, __METHOD__ );
}
}
/**
* Get the authors between the given revisions or revision IDs.
* Used for diffs and other things that really need it.
*
* @since 1.23
*
* @param int|Revision $old Old revision or rev ID (first before range by default)
* @param int|Revision $new New revision or rev ID (first after range by default)
* @param int $limit Maximum number of authors
* @param string|array $options (Optional): Single option, or an array of options:
* 'include_old' Include $old in the range; $new is excluded.
* 'include_new' Include $new in the range; $old is excluded.
* 'include_both' Include both $old and $new in the range.
* Unknown option values are ignored.
* @return array|null Names of revision authors in the range; null if not both revisions exist
*/
public function getAuthorsBetween( $old, $new, $limit, $options = [] ) {
if ( !( $old instanceof Revision ) ) {
$old = Revision::newFromTitle( $this, (int)$old );
}
if ( !( $new instanceof Revision ) ) {
$new = Revision::newFromTitle( $this, (int)$new );
}
// XXX: what if Revision objects are passed in, but they don't refer to this title?
// Add $old->getPage() != $new->getPage() || $old->getPage() != $this->getArticleID()
// in the sanity check below?
if ( !$old || !$new ) {
return null; // nothing to compare
}
$authors = [];
$old_cmp = '>';
$new_cmp = '<';
$options = (array)$options;
if ( in_array( 'include_old', $options ) ) {
$old_cmp = '>=';
}
if ( in_array( 'include_new', $options ) ) {
$new_cmp = '<=';
}
if ( in_array( 'include_both', $options ) ) {
$old_cmp = '>=';
$new_cmp = '<=';
}
// No DB query needed if $old and $new are the same or successive revisions:
if ( $old->getId() === $new->getId() ) {
return ( $old_cmp === '>' && $new_cmp === '<' ) ?
[] :
[ $old->getUserText( Revision::RAW ) ];
} elseif ( $old->getId() === $new->getParentId() ) {
if ( $old_cmp === '>=' && $new_cmp === '<=' ) {
$authors[] = $old->getUserText( Revision::RAW );
if ( $old->getUserText( Revision::RAW ) != $new->getUserText( Revision::RAW ) ) {
$authors[] = $new->getUserText( Revision::RAW );
}
} elseif ( $old_cmp === '>=' ) {
$authors[] = $old->getUserText( Revision::RAW );
} elseif ( $new_cmp === '<=' ) {
$authors[] = $new->getUserText( Revision::RAW );
}
return $authors;
}
$dbr = wfGetDB( DB_REPLICA );
$revQuery = Revision::getQueryInfo();
$authors = $dbr->selectFieldValues(
$revQuery['tables'],
$revQuery['fields']['rev_user_text'],
[
2010-12-12 16:18:08 +00:00
'rev_page' => $this->getArticleID(),
"rev_timestamp $old_cmp " . $dbr->addQuotes( $dbr->timestamp( $old->getTimestamp() ) ),
"rev_timestamp $new_cmp " . $dbr->addQuotes( $dbr->timestamp( $new->getTimestamp() ) )
], __METHOD__,
[ 'DISTINCT', 'LIMIT' => $limit + 1 ], // add one so caller knows it was truncated
$revQuery['joins']
);
return $authors;
}
/**
* Get the number of authors between the given revisions or revision IDs.
* Used for diffs and other things that really need it.
*
* @param int|Revision $old Old revision or rev ID (first before range by default)
* @param int|Revision $new New revision or rev ID (first after range by default)
* @param int $limit Maximum number of authors
* @param string|array $options (Optional): Single option, or an array of options:
* 'include_old' Include $old in the range; $new is excluded.
* 'include_new' Include $new in the range; $old is excluded.
* 'include_both' Include both $old and $new in the range.
* Unknown option values are ignored.
* @return int Number of revision authors in the range; zero if not both revisions exist
*/
public function countAuthorsBetween( $old, $new, $limit, $options = [] ) {
$authors = $this->getAuthorsBetween( $old, $new, $limit, $options );
return $authors ? count( $authors ) : 0;
}
/**
* Compare with another title.
*
* @param Title $title
* @return bool
*/
public function equals( Title $title ) {
// Note: === is necessary for proper matching of number-like titles.
return $this->mInterwiki === $title->mInterwiki
&& $this->mNamespace == $title->mNamespace
&& $this->mDbkeyform === $title->mDbkeyform;
}
/**
* Check if this title is a subpage of another title
*
* @param Title $title
* @return bool
*/
public function isSubpageOf( Title $title ) {
return $this->mInterwiki === $title->mInterwiki
&& $this->mNamespace == $title->mNamespace
&& strpos( $this->mDbkeyform, $title->mDbkeyform . '/' ) === 0;
}
/**
* Check if page exists. For historical reasons, this function simply
* checks for the existence of the title in the page table, and will
* thus return false for interwiki links, special pages and the like.
* If you want to know if a title can be meaningfully viewed, you should
* probably call the isKnown() method instead.
*
* @param int $flags An optional bit field; may be Title::GAID_FOR_UPDATE to check
* from master/for update
* @return bool
*/
public function exists( $flags = 0 ) {
$exists = $this->getArticleID( $flags ) != 0;
Hooks::run( 'TitleExists', [ $this, &$exists ] );
return $exists;
}
/**
* Should links to this title be shown as potentially viewable (i.e. as
* "bluelinks"), even if there's no record by this title in the page
* table?
*
* This function is semi-deprecated for public use, as well as somewhat
* misleadingly named. You probably just want to call isKnown(), which
* calls this function internally.
*
* (ISSUE: Most of these checks are cheap, but the file existence check
* can potentially be quite expensive. Including it here fixes a lot of
* existing code, but we might want to add an optional parameter to skip
* it and any other expensive checks.)
*
* @return bool
*/
public function isAlwaysKnown() {
$isKnown = null;
/**
* Allows overriding default behavior for determining if a page exists.
* If $isKnown is kept as null, regular checks happen. If it's
* a boolean, this value is returned by the isKnown method.
*
* @since 1.20
*
* @param Title $title
* @param bool|null $isKnown
*/
Hooks::run( 'TitleIsAlwaysKnown', [ $this, &$isKnown ] );
if ( !is_null( $isKnown ) ) {
return $isKnown;
}
if ( $this->isExternal() ) {
return true; // any interwiki link might be viewable, for all we know
}
switch ( $this->mNamespace ) {
case NS_MEDIA:
case NS_FILE:
// file exists, possibly in a foreign repo
return (bool)wfFindFile( $this );
case NS_SPECIAL:
// valid special page
return MediaWikiServices::getInstance()->getSpecialPageFactory()->
exists( $this->mDbkeyform );
case NS_MAIN:
// selflink, possibly with fragment
return $this->mDbkeyform == '';
case NS_MEDIAWIKI:
// known system message
return $this->hasSourceText() !== false;
default:
return false;
}
}
/**
* Does this title refer to a page that can (or might) be meaningfully
* viewed? In particular, this function may be used to determine if
* links to the title should be rendered as "bluelinks" (as opposed to
* "redlinks" to non-existent pages).
* Adding something else to this function will cause inconsistency
* since LinkHolderArray calls isAlwaysKnown() and does its own
* page existence check.
*
* @return bool
*/
public function isKnown() {
return $this->isAlwaysKnown() || $this->exists();
}
/**
* Does this page have source text?
*
* @return bool
*/
public function hasSourceText() {
2010-07-25 15:53:22 +00:00
if ( $this->exists() ) {
return true;
2010-07-25 15:53:22 +00:00
}
if ( $this->mNamespace == NS_MEDIAWIKI ) {
// If the page doesn't exist but is a known system message, default
// message content will be displayed, same for language subpages-
// Use always content language to avoid loading hundreds of languages
// to get the link color.
$contLang = MediaWikiServices::getInstance()->getContentLanguage();
list( $name, ) = MessageCache::singleton()->figureMessage(
$contLang->lcfirst( $this->getText() )
);
$message = wfMessage( $name )->inLanguage( $contLang )->useDatabase( false );
return $message->exists();
}
return false;
}
/**
* Get the default (plain) message contents for an page that overrides an
* interface message key.
*
* Primary use cases:
*
* - Article:
* - Show default when viewing the page. The Article::getSubstituteContent
* method displays the default message content, instead of the
* 'noarticletext' placeholder message normally used.
*
* - EditPage:
* - Title of edit page. When creating an interface message override,
* the editor is told they are "Editing the page", instead of
* "Creating the page". (EditPage::setHeaders)
* - Edit notice. The 'translateinterface' edit notice is shown when creating
* or editing a an interface message override. (EditPage::showIntro)
* - Opening the editor. The contents of the localisation message are used
* as contents of the editor when creating a new page in the MediaWiki
* namespace. This simplifies the process for editors when "changing"
* an interface message by creating an override. (EditPage::getContentObject)
* - Showing a diff. The left-hand side of a diff when an editor is
* previewing their changes before saving the creation of a page in the
* MediaWiki namespace. (EditPage::showDiff)
* - Disallowing a save. When attempting to create a a MediaWiki-namespace
* page with the proposed content matching the interface message default,
* the save is rejected, the same way we disallow blank pages from being
* created. (EditPage::internalAttemptSave)
*
* - ApiEditPage:
* - Default content, when using the 'prepend' or 'append' feature.
*
* - SkinTemplate:
* - Label the create action as "Edit", if the page can be an override.
*
* @return string|bool
*/
public function getDefaultMessageText() {
if ( $this->mNamespace != NS_MEDIAWIKI ) { // Just in case
return false;
}
list( $name, $lang ) = MessageCache::singleton()->figureMessage(
MediaWikiServices::getInstance()->getContentLanguage()->lcfirst( $this->getText() )
);
$message = wfMessage( $name )->inLanguage( $lang )->useDatabase( false );
if ( $message->exists() ) {
return $message->plain();
} else {
return false;
}
}
/**
2011-12-11 14:48:45 +00:00
* Updates page_touched for this page; called from LinksUpdate.php
2010-07-25 15:53:22 +00:00
*
* @param string|null $purgeTime [optional] TS_MW timestamp
* @return bool True if the update succeeded
2010-07-25 15:53:22 +00:00
*/
public function invalidateCache( $purgeTime = null ) {
2011-12-11 14:48:45 +00:00
if ( wfReadOnly() ) {
return false;
} elseif ( $this->mArticleID === 0 ) {
return true; // avoid gap locking if we know it's not there
}
$dbw = wfGetDB( DB_MASTER );
$dbw->onTransactionPreCommitOrIdle(
function () use ( $dbw ) {
ResourceLoaderWikiModule::invalidateModuleCache(
$this, null, null, $dbw->getDomainId() );
},
__METHOD__
);
$conds = $this->pageCond();
DeferredUpdates::addUpdate(
new AutoCommitUpdate(
$dbw,
__METHOD__,
function ( IDatabase $dbw, $fname ) use ( $conds, $purgeTime ) {
$dbTimestamp = $dbw->timestamp( $purgeTime ?: time() );
$dbw->update(
'page',
[ 'page_touched' => $dbTimestamp ],
$conds + [ 'page_touched < ' . $dbw->addQuotes( $dbTimestamp ) ],
$fname
);
MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $this );
}
),
DeferredUpdates::PRESEND
);
return true;
}
/**
* Update page_touched timestamps and send CDN purge messages for
* pages linking to this title. May be sent to the job queue depending
* on the number of links. Typically called on create and delete.
*/
public function touchLinks() {
DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'pagelinks', 'page-touch' ) );
if ( $this->mNamespace == NS_CATEGORY ) {
DeferredUpdates::addUpdate(
new HTMLCacheUpdate( $this, 'categorylinks', 'category-touch' )
);
}
}
2005-07-23 05:47:25 +00:00
/**
* Get the last touched timestamp
*
* @param IDatabase|null $db
* @return string|false Last-touched timestamp
*/
public function getTouched( $db = null ) {
if ( $db === null ) {
$db = wfGetDB( DB_REPLICA );
}
$touched = $db->selectField( 'page', 'page_touched', $this->pageCond(), __METHOD__ );
return $touched;
}
/**
* Get the timestamp when this page was updated since the user last saw it.
*
* @param User|null $user
* @return string|null
*/
public function getNotificationTimestamp( $user = null ) {
global $wgUser;
// Assume current user if none given
2010-07-25 15:53:22 +00:00
if ( !$user ) {
$user = $wgUser;
}
// Check cache first
$uid = $user->getId();
if ( !$uid ) {
return false;
}
// avoid isset here, as it'll return false for null entries
if ( array_key_exists( $uid, $this->mNotificationTimestamp ) ) {
return $this->mNotificationTimestamp[$uid];
}
// Don't cache too much!
if ( count( $this->mNotificationTimestamp ) >= self::CACHE_MAX ) {
$this->mNotificationTimestamp = [];
}
$store = MediaWikiServices::getInstance()->getWatchedItemStore();
$watchedItem = $store->getWatchedItem( $user, $this );
if ( $watchedItem ) {
$this->mNotificationTimestamp[$uid] = $watchedItem->getNotificationTimestamp();
} else {
$this->mNotificationTimestamp[$uid] = false;
}
return $this->mNotificationTimestamp[$uid];
}
2006-09-05 14:44:50 +00:00
/**
* Generate strings used for xml 'id' names in monobook tabs
*
* @param string $prepend Defaults to 'nstab-'
* @return string XML 'id' name
*/
public function getNamespaceKey( $prepend = 'nstab-' ) {
// Gets the subject namespace of this title
$subjectNS = MWNamespace::getSubject( $this->mNamespace );
// Prefer canonical namespace name for HTML IDs
$namespaceKey = MWNamespace::getCanonicalName( $subjectNS );
if ( $namespaceKey === false ) {
// Fallback to localised text
$namespaceKey = $this->getSubjectNsText();
}
// Makes namespace key lowercase
$namespaceKey = MediaWikiServices::getInstance()->getContentLanguage()->lc( $namespaceKey );
// Uses main
if ( $namespaceKey == '' ) {
$namespaceKey = 'main';
}
// Changes file to image for backwards compatibility
if ( $namespaceKey == 'file' ) {
$namespaceKey = 'image';
}
return $prepend . $namespaceKey;
}
2008-08-11 04:39:00 +00:00
/**
* Get all extant redirects to this Title
*
* @param int|null $ns Single namespace to consider; null to consider all namespaces
* @return Title[] Array of Title redirects to this title
2008-08-11 04:39:00 +00:00
*/
public function getRedirectsHere( $ns = null ) {
$redirs = [];
$dbr = wfGetDB( DB_REPLICA );
$where = [
'rd_namespace' => $this->mNamespace,
'rd_title' => $this->mDbkeyform,
'rd_from = page_id'
];
if ( $this->isExternal() ) {
$where['rd_interwiki'] = $this->mInterwiki;
} else {
$where[] = 'rd_interwiki = ' . $dbr->addQuotes( '' ) . ' OR rd_interwiki IS NULL';
}
2010-07-25 15:53:22 +00:00
if ( !is_null( $ns ) ) {
$where['page_namespace'] = $ns;
}
$res = $dbr->select(
[ 'redirect', 'page' ],
[ 'page_namespace', 'page_title' ],
$where,
__METHOD__
);
foreach ( $res as $row ) {
$redirs[] = self::newFromRow( $row );
}
return $redirs;
}
/**
* Check if this Title is a valid redirect target
*
* @return bool
*/
public function isValidRedirectTarget() {
global $wgInvalidRedirectTargets;
if ( $this->isSpecialPage() ) {
// invalid redirect targets are stored in a global array, but explicitly disallow Userlogout here
if ( $this->isSpecial( 'Userlogout' ) ) {
return false;
}
foreach ( $wgInvalidRedirectTargets as $target ) {
if ( $this->isSpecial( $target ) ) {
return false;
}
}
}
return true;
}
/**
* Get a backlink cache object
*
2011-11-20 18:02:38 +00:00
* @return BacklinkCache
*/
public function getBacklinkCache() {
return BacklinkCache::get( $this );
}
/**
* Whether the magic words __INDEX__ and __NOINDEX__ function for this page.
*
* @return bool
*/
public function canUseNoindex() {
global $wgExemptFromUserRobotsControl;
$bannedNamespaces = is_null( $wgExemptFromUserRobotsControl )
? MWNamespace::getContentNamespaces()
: $wgExemptFromUserRobotsControl;
return !in_array( $this->mNamespace, $bannedNamespaces );
}
Reconcept cl_raw_sortkey as cl_sortkey_prefix In response to feedback by Phillipe Verdy on bug 164. Now if a bunch of pages have [[Category:Foo| ]], they'll sort amongst themselves according to page name, instead of in basically random order as it is currently. This also makes storage more elegant and intuitive: instead of giving NULL a magic meaning when there's no custom sortkey specified, we just store an empty string, since there's no prefix. This means {{defaultsort:}} really now means {{defaultsortprefix:}}, which is slightly confusing, and a lot of code is now slightly misleading or poorly named. But it should all work fine. Also, while I was at it, I made updateCollation.php work as a transition script, so you can apply the SQL patch and then run updateCollation.php and things will work. However, with the new schema it's not trivial to reverse this -- you'd have to recover the raw sort keys with some PHP. Conversion goes at about a thousand rows a second for me, and seems to be CPU-bound. Could probably be optimized. I also adjusted the transition script so it will fix rows with collation versions *greater* than the current one, as well as less. Thus if some site wants to use their own collation, they can call it 137 or something, and if they later want to switch back to MediaWiki stock collation 7, it will work. Also fixed a silly bug in updateCollation.php where it would say "1000 done" if it did nothing, and changed $res->numRows() >= self::BATCH_SIZE to == so people don't wonder how it could be bigger (since it can't, I hope).
2010-07-26 19:27:13 +00:00
/**
* Returns the raw sort key to be used for categories, with the specified
* prefix. This will be fed to Collation::getSortKey() to get a
* binary sortkey that can be used for actual sorting.
Reconcept cl_raw_sortkey as cl_sortkey_prefix In response to feedback by Phillipe Verdy on bug 164. Now if a bunch of pages have [[Category:Foo| ]], they'll sort amongst themselves according to page name, instead of in basically random order as it is currently. This also makes storage more elegant and intuitive: instead of giving NULL a magic meaning when there's no custom sortkey specified, we just store an empty string, since there's no prefix. This means {{defaultsort:}} really now means {{defaultsortprefix:}}, which is slightly confusing, and a lot of code is now slightly misleading or poorly named. But it should all work fine. Also, while I was at it, I made updateCollation.php work as a transition script, so you can apply the SQL patch and then run updateCollation.php and things will work. However, with the new schema it's not trivial to reverse this -- you'd have to recover the raw sort keys with some PHP. Conversion goes at about a thousand rows a second for me, and seems to be CPU-bound. Could probably be optimized. I also adjusted the transition script so it will fix rows with collation versions *greater* than the current one, as well as less. Thus if some site wants to use their own collation, they can call it 137 or something, and if they later want to switch back to MediaWiki stock collation 7, it will work. Also fixed a silly bug in updateCollation.php where it would say "1000 done" if it did nothing, and changed $res->numRows() >= self::BATCH_SIZE to == so people don't wonder how it could be bigger (since it can't, I hope).
2010-07-26 19:27:13 +00:00
*
* @param string $prefix The prefix to be used, specified using
* {{defaultsort:}} or like [[Category:Foo|prefix]]. Empty for no
* prefix.
Reconcept cl_raw_sortkey as cl_sortkey_prefix In response to feedback by Phillipe Verdy on bug 164. Now if a bunch of pages have [[Category:Foo| ]], they'll sort amongst themselves according to page name, instead of in basically random order as it is currently. This also makes storage more elegant and intuitive: instead of giving NULL a magic meaning when there's no custom sortkey specified, we just store an empty string, since there's no prefix. This means {{defaultsort:}} really now means {{defaultsortprefix:}}, which is slightly confusing, and a lot of code is now slightly misleading or poorly named. But it should all work fine. Also, while I was at it, I made updateCollation.php work as a transition script, so you can apply the SQL patch and then run updateCollation.php and things will work. However, with the new schema it's not trivial to reverse this -- you'd have to recover the raw sort keys with some PHP. Conversion goes at about a thousand rows a second for me, and seems to be CPU-bound. Could probably be optimized. I also adjusted the transition script so it will fix rows with collation versions *greater* than the current one, as well as less. Thus if some site wants to use their own collation, they can call it 137 or something, and if they later want to switch back to MediaWiki stock collation 7, it will work. Also fixed a silly bug in updateCollation.php where it would say "1000 done" if it did nothing, and changed $res->numRows() >= self::BATCH_SIZE to == so people don't wonder how it could be bigger (since it can't, I hope).
2010-07-26 19:27:13 +00:00
* @return string
*/
public function getCategorySortkey( $prefix = '' ) {
$unprefixed = $this->getText();
// Anything that uses this hook should only depend
// on the Title object passed in, and should probably
// tell the users to run updateCollations.php --force
// in order to re-sort existing category relations.
Hooks::run( 'GetDefaultSortkey', [ $this, &$unprefixed ] );
if ( $prefix !== '' ) {
# Separate with a line feed, so the unprefixed part is only used as
# a tiebreaker when two pages have the exact same prefix.
# In UCA, tab is the only character that can sort above LF
# so we strip both of them from the original prefix.
$prefix = strtr( $prefix, "\n\t", ' ' );
return "$prefix\n$unprefixed";
Reconcept cl_raw_sortkey as cl_sortkey_prefix In response to feedback by Phillipe Verdy on bug 164. Now if a bunch of pages have [[Category:Foo| ]], they'll sort amongst themselves according to page name, instead of in basically random order as it is currently. This also makes storage more elegant and intuitive: instead of giving NULL a magic meaning when there's no custom sortkey specified, we just store an empty string, since there's no prefix. This means {{defaultsort:}} really now means {{defaultsortprefix:}}, which is slightly confusing, and a lot of code is now slightly misleading or poorly named. But it should all work fine. Also, while I was at it, I made updateCollation.php work as a transition script, so you can apply the SQL patch and then run updateCollation.php and things will work. However, with the new schema it's not trivial to reverse this -- you'd have to recover the raw sort keys with some PHP. Conversion goes at about a thousand rows a second for me, and seems to be CPU-bound. Could probably be optimized. I also adjusted the transition script so it will fix rows with collation versions *greater* than the current one, as well as less. Thus if some site wants to use their own collation, they can call it 137 or something, and if they later want to switch back to MediaWiki stock collation 7, it will work. Also fixed a silly bug in updateCollation.php where it would say "1000 done" if it did nothing, and changed $res->numRows() >= self::BATCH_SIZE to == so people don't wonder how it could be bigger (since it can't, I hope).
2010-07-26 19:27:13 +00:00
}
return $unprefixed;
Reconcept cl_raw_sortkey as cl_sortkey_prefix In response to feedback by Phillipe Verdy on bug 164. Now if a bunch of pages have [[Category:Foo| ]], they'll sort amongst themselves according to page name, instead of in basically random order as it is currently. This also makes storage more elegant and intuitive: instead of giving NULL a magic meaning when there's no custom sortkey specified, we just store an empty string, since there's no prefix. This means {{defaultsort:}} really now means {{defaultsortprefix:}}, which is slightly confusing, and a lot of code is now slightly misleading or poorly named. But it should all work fine. Also, while I was at it, I made updateCollation.php work as a transition script, so you can apply the SQL patch and then run updateCollation.php and things will work. However, with the new schema it's not trivial to reverse this -- you'd have to recover the raw sort keys with some PHP. Conversion goes at about a thousand rows a second for me, and seems to be CPU-bound. Could probably be optimized. I also adjusted the transition script so it will fix rows with collation versions *greater* than the current one, as well as less. Thus if some site wants to use their own collation, they can call it 137 or something, and if they later want to switch back to MediaWiki stock collation 7, it will work. Also fixed a silly bug in updateCollation.php where it would say "1000 done" if it did nothing, and changed $res->numRows() >= self::BATCH_SIZE to == so people don't wonder how it could be bigger (since it can't, I hope).
2010-07-26 19:27:13 +00:00
}
/**
* Returns the page language code saved in the database, if $wgPageLanguageUseDB is set
* to true in LocalSettings.php, otherwise returns false. If there is no language saved in
* the db, it will return NULL.
*
* @return string|null|bool
*/
private function getDbPageLanguageCode() {
global $wgPageLanguageUseDB;
// check, if the page language could be saved in the database, and if so and
// the value is not requested already, lookup the page language using LinkCache
if ( $wgPageLanguageUseDB && $this->mDbPageLanguage === false ) {
$linkCache = MediaWikiServices::getInstance()->getLinkCache();
$linkCache->addLinkObj( $this );
$this->mDbPageLanguage = $linkCache->getGoodLinkFieldObj( $this, 'lang' );
}
return $this->mDbPageLanguage;
}
/**
* Get the language in which the content of this page is written in
* wikitext. Defaults to content language, but in certain cases it can be
* e.g. $wgLang (such as special pages, which are in the user language).
*
* @since 1.18
* @return Language
*/
public function getPageLanguage() {
global $wgLang, $wgLanguageCode;
if ( $this->isSpecialPage() ) {
// special pages are in the user language
return $wgLang;
}
// Checking if DB language is set
$dbPageLanguage = $this->getDbPageLanguageCode();
if ( $dbPageLanguage ) {
return wfGetLangObj( $dbPageLanguage );
}
if ( !$this->mPageLanguage || $this->mPageLanguage[1] !== $wgLanguageCode ) {
// Note that this may depend on user settings, so the cache should
// be only per-request.
// NOTE: ContentHandler::getPageLanguage() may need to load the
// content to determine the page language!
// Checking $wgLanguageCode hasn't changed for the benefit of unit
// tests.
$contentHandler = ContentHandler::getForTitle( $this );
$langObj = $contentHandler->getPageLanguage( $this );
$this->mPageLanguage = [ $langObj->getCode(), $wgLanguageCode ];
} else {
$langObj = Language::factory( $this->mPageLanguage[0] );
}
return $langObj;
}
/**
* Get the language in which the content of this page is written when
* viewed by user. Defaults to content language, but in certain cases it can be
* e.g. $wgLang (such as special pages, which are in the user language).
*
* @since 1.20
* @return Language
*/
public function getPageViewLanguage() {
global $wgLang;
if ( $this->isSpecialPage() ) {
// If the user chooses a variant, the content is actually
// in a language whose code is the variant code.
$variant = $wgLang->getPreferredVariant();
if ( $wgLang->getCode() !== $variant ) {
return Language::factory( $variant );
}
return $wgLang;
}
// Checking if DB language is set
$dbPageLanguage = $this->getDbPageLanguageCode();
if ( $dbPageLanguage ) {
$pageLang = wfGetLangObj( $dbPageLanguage );
$variant = $pageLang->getPreferredVariant();
if ( $pageLang->getCode() !== $variant ) {
$pageLang = Language::factory( $variant );
}
return $pageLang;
}
// @note Can't be cached persistently, depends on user settings.
// @note ContentHandler::getPageViewLanguage() may need to load the
// content to determine the page language!
$contentHandler = ContentHandler::getForTitle( $this );
$pageLang = $contentHandler->getPageViewLanguage( $this );
return $pageLang;
}
/**
* Get a list of rendered edit notices for this page.
*
* Array is keyed by the original message key, and values are rendered using parseAsBlock, so
* they will already be wrapped in paragraphs.
*
* @since 1.21
* @param int $oldid Revision ID that's being edited
* @return array
*/
public function getEditNotices( $oldid = 0 ) {
$notices = [];
// Optional notice for the entire namespace
$editnotice_ns = 'editnotice-' . $this->mNamespace;
$msg = wfMessage( $editnotice_ns );
if ( $msg->exists() ) {
$html = $msg->parseAsBlock();
// Edit notices may have complex logic, but output nothing (T91715)
if ( trim( $html ) !== '' ) {
$notices[$editnotice_ns] = Html::rawElement(
'div',
[ 'class' => [
'mw-editnotice',
'mw-editnotice-namespace',
Sanitizer::escapeClass( "mw-$editnotice_ns" )
] ],
$html
);
}
}
if ( MWNamespace::hasSubpages( $this->mNamespace ) ) {
// Optional notice for page itself and any parent page
$parts = explode( '/', $this->mDbkeyform );
$editnotice_base = $editnotice_ns;
while ( count( $parts ) > 0 ) {
$editnotice_base .= '-' . array_shift( $parts );
$msg = wfMessage( $editnotice_base );
if ( $msg->exists() ) {
$html = $msg->parseAsBlock();
if ( trim( $html ) !== '' ) {
$notices[$editnotice_base] = Html::rawElement(
'div',
[ 'class' => [
'mw-editnotice',
'mw-editnotice-base',
Sanitizer::escapeClass( "mw-$editnotice_base" )
] ],
$html
);
}
}
}
} else {
// Even if there are no subpages in namespace, we still don't want "/" in MediaWiki message keys
$editnoticeText = $editnotice_ns . '-' . strtr( $this->mDbkeyform, '/', '-' );
$msg = wfMessage( $editnoticeText );
if ( $msg->exists() ) {
$html = $msg->parseAsBlock();
if ( trim( $html ) !== '' ) {
$notices[$editnoticeText] = Html::rawElement(
'div',
[ 'class' => [
'mw-editnotice',
'mw-editnotice-page',
Sanitizer::escapeClass( "mw-$editnoticeText" )
] ],
$html
);
}
}
}
Hooks::run( 'TitleGetEditNotices', [ $this, $oldid, &$notices ] );
return $notices;
}
/**
* @return array
*/
public function __sleep() {
return [
'mNamespace',
'mDbkeyform',
'mFragment',
'mInterwiki',
'mLocalInterwiki',
'mUserCaseDBKey',
'mDefaultNamespace',
];
}
public function __wakeup() {
$this->mArticleID = ( $this->mNamespace >= 0 ) ? -1 : 0;
$this->mUrlform = wfUrlencode( $this->mDbkeyform );
$this->mTextform = strtr( $this->mDbkeyform, '_', ' ' );
}
}