<?php
/**
+ * Representation a title within %MediaWiki.
+ *
* See title.txt
*
* This program is free software; you can redistribute it and/or modify
var $mFragment; // /< Title fragment (i.e. the bit after the #)
var $mArticleID = -1; // /< Article ID, fetched from the link cache on demand
var $mLatestID = false; // /< ID of most recent revision
+ var $mContentModel = false; // /< ID of the page's content model, i.e. one of the CONTENT_MODEL_XXX constants
private $mEstimateRevisions; // /< Estimated number of revisions; null of not loaded
var $mRestrictions = array(); // /< Array of groups allowed to edit this article
var $mOldRestrictions = false;
var $mRedirect = null; // /< Is the article at this title a redirect?
var $mNotificationTimestamp = array(); // /< Associative array of user ID -> timestamp/false
var $mBacklinkCache = null; // /< Cache of links to this title
+ var $mHasSubpage; // /< Whether a page has any subpages
// @}
* fied by a prefix. If you want to force a specific namespace even if
* $text might begin with a namespace prefix, use makeTitle() or
* makeTitleSafe().
- * @return Title, or null on an error.
+ * @throws MWException
+ * @return Title|null - Title or null on an error.
*/
public static function newFromText( $text, $defaultNamespace = NS_MAIN ) {
if ( is_object( $text ) ) {
* @return Title the new object, or NULL on an error
*/
public static function newFromURL( $url ) {
- global $wgLegalTitleChars;
$t = new Title();
# 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( $wgLegalTitleChars, '+' ) === false ) {
+ if ( strpos( self::legalChars(), '+' ) === false ) {
$url = str_replace( '+', ' ', $url );
}
}
}
+ /**
+ * 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;
+
+ $fields = array(
+ 'page_namespace', 'page_title', 'page_id',
+ 'page_len', 'page_is_redirect', 'page_latest',
+ );
+
+ if ( $wgContentHandlerUseDB ) {
+ $fields[] = 'page_content_model';
+ }
+
+ return $fields;
+ }
+
/**
* Create a new Title from an article ID
*
*/
public static function newFromID( $id, $flags = 0 ) {
$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
- $row = $db->selectRow( 'page', '*', array( 'page_id' => $id ), __METHOD__ );
+ $row = $db->selectRow(
+ 'page',
+ self::getSelectFields(),
+ array( 'page_id' => $id ),
+ __METHOD__
+ );
if ( $row !== false ) {
$title = Title::newFromRow( $row );
} else {
$res = $dbr->select(
'page',
- array(
- 'page_namespace', 'page_title', 'page_id',
- 'page_len', 'page_is_redirect', 'page_latest',
- ),
+ self::getSelectFields(),
array( 'page_id' => $ids ),
__METHOD__
);
$this->mRedirect = (bool)$row->page_is_redirect;
if ( isset( $row->page_latest ) )
$this->mLatestID = (int)$row->page_latest;
+ if ( isset( $row->page_content_model ) )
+ $this->mContentModel = intval( $row->page_content_model );
+ else
+ $this->mContentModel = false; # initialized lazily in getContentModel()
} else { // page not found
$this->mArticleID = 0;
$this->mLength = 0;
$this->mRedirect = false;
$this->mLatestID = 0;
+ $this->mContentModel = false; # initialized lazily in getContentModel()
}
}
$t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
$t->mUrlform = wfUrlencode( $t->mDbkeyform );
$t->mTextform = str_replace( '_', ' ', $title );
+ $t->mContentModel = false; # initialized lazily in getContentModel()
return $t;
}
*
* @param $text String: Text with possible redirect
* @return Title: The corresponding Title
+ * @deprecated since 1.WD, use Content::getRedirectTarget instead.
*/
public static function newFromRedirect( $text ) {
- return self::newFromRedirectInternal( $text );
+ $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
+ return $content->getRedirectTarget();
}
/**
*
* @param $text String Text with possible redirect
* @return Title
+ * @deprecated since 1.WD, use Content::getUltimateRedirectTarget instead.
*/
public static function newFromRedirectRecurse( $text ) {
- $titles = self::newFromRedirectArray( $text );
- return $titles ? array_pop( $titles ) : null;
+ $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
+ return $content->getUltimateRedirectTarget();
}
/**
*
* @param $text String Text with possible redirect
* @return Array of Titles, with the destination last
+ * @deprecated since 1.WD, use Content::getRedirectChain instead.
*/
public static function newFromRedirectArray( $text ) {
- global $wgMaxRedirects;
- $title = self::newFromRedirectInternal( $text );
- if ( is_null( $title ) ) {
- return null;
- }
- // recursive check to follow double redirects
- $recurse = $wgMaxRedirects;
- $titles = array( $title );
- while ( --$recurse > 0 ) {
- if ( $title->isRedirect() ) {
- $page = WikiPage::factory( $title );
- $newtitle = $page->getRedirectTarget();
- } else {
- break;
- }
- // Redirects to some special pages are not permitted
- if ( $newtitle instanceOf Title && $newtitle->isValidRedirectTarget() ) {
- // the new title passes the checks, so make that our current title so that further recursion can be checked
- $title = $newtitle;
- $titles[] = $newtitle;
- } else {
- break;
- }
- }
- return $titles;
- }
-
- /**
- * Really extract the redirect destination
- * Do not call this function directly, use one of the newFromRedirect* functions above
- *
- * @param $text String Text with possible redirect
- * @return Title
- */
- protected static function newFromRedirectInternal( $text ) {
- global $wgMaxRedirects;
- if ( $wgMaxRedirects < 1 ) {
- //redirects are disabled, so quit early
- return null;
- }
- $redir = MagicWord::get( 'redirect' );
- $text = trim( $text );
- if ( $redir->matchStartAndRemove( $text ) ) {
- // Extract the first link and see if it's usable
- // Ensure that it really does come directly after #REDIRECT
- // Some older redirects included a colon, so don't freak about that!
- $m = array();
- if ( preg_match( '!^\s*:?\s*\[{2}(.*?)(?:\|.*?)?\]{2}!', $text, $m ) ) {
- // Strip preceding colon used to "escape" categories, etc.
- // and URL-decode links
- if ( strpos( $m[1], '%' ) !== false ) {
- // Match behavior of inline link parsing here;
- $m[1] = rawurldecode( ltrim( $m[1], ':' ) );
- }
- $title = Title::newFromText( $m[1] );
- // If the title is a redirect to bad special pages or is invalid, return null
- if ( !$title instanceof Title || !$title->isValidRedirectTarget() ) {
- return null;
- }
- return $title;
- }
- }
- return null;
+ $content = ContentHandler::makeContent( $text, null, CONTENT_MODEL_WIKITEXT );
+ return $content->getRedirectChain();
}
/**
return $this->mNamespace;
}
+ /**
+ * Get the page's content model id, see the CONTENT_MODEL_XXX constants.
+ *
+ * @return Integer: Content model id
+ */
+ public function getContentModel() {
+ if ( !$this->mContentModel ) {
+ $linkCache = LinkCache::singleton();
+ $this->mContentModel = $linkCache->getGoodLinkFieldObj( $this, 'model' );
+ }
+
+ if ( !$this->mContentModel ) {
+ $this->mContentModel = ContentHandler::getDefaultModelFor( $this );
+ }
+
+ if( !$this->mContentModel ) {
+ throw new MWException( "failed to determin content model!" );
+ }
+
+ return $this->mContentModel;
+ }
+
+ /**
+ * Convenience method for checking a title's content model name
+ *
+ * @param int $id
+ * @return Boolean true if $this->getContentModel() == $id
+ */
+ public function hasContentModel( $id ) {
+ return $this->getContentModel() == $id;
+ }
+
/**
* Get the namespace text
*
}
}
- // Strip off subpages
- $pagename = $this->getText();
- if ( strpos( $pagename, '/' ) !== false ) {
- list( $username , ) = explode( '/', $pagename, 2 );
- } else {
- $username = $pagename;
- }
-
if ( $wgContLang->needsGenderDistinction() &&
MWNamespace::hasGenderDistinction( $this->mNamespace ) ) {
- $gender = GenderCache::singleton()->getGenderOf( $username, __METHOD__ );
+ $gender = GenderCache::singleton()->getGenderOf( $this->getText(), __METHOD__ );
return $wgContLang->getGenderNsText( $this->mNamespace, $gender );
}
* This is MUCH simpler than individually testing for equivilance
* against both NS_USER and NS_USER_TALK, and is also forward compatible.
* @since 1.19
+ * @param $ns int
* @return bool
*/
public function hasSubjectNamespace( $ns ) {
*/
public function isConversionTable() {
return $this->getNamespace() == NS_MEDIAWIKI &&
- strpos( $this->getText(), 'Conversiontable' ) !== false;
+ strpos( $this->getText(), 'Conversiontable/' ) === 0;
}
/**
* @return Bool
*/
public function isWikitextPage() {
- $retval = !$this->isCssOrJsPage() && !$this->isCssJsSubpage();
- wfRunHooks( 'TitleIsWikitextPage', array( $this, &$retval ) );
- return $retval;
+ return $this->hasContentModel( CONTENT_MODEL_WIKITEXT );
}
/**
- * Could this page contain custom CSS or JavaScript, based
- * on the title?
+ * Could this page contain custom CSS or JavaScript for the global UI.
+ * This is generally true for pages in the MediaWiki namespace having CONTENT_MODEL_CSS
+ * or CONTENT_MODEL_JAVASCRIPT.
+ *
+ * This method does *not* return true for per-user JS/CSS. Use isCssJsSubpage() for that!
+ *
+ * Note that this method should not return true for pages that contain and show "inactive" CSS or JS.
*
* @return Bool
*/
public function isCssOrJsPage() {
- $retval = $this->mNamespace == NS_MEDIAWIKI
- && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0;
- wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$retval ) );
- return $retval;
+ $isCssOrJsPage = NS_MEDIAWIKI == $this->mNamespace
+ && ( $this->hasContentModel( CONTENT_MODEL_CSS )
+ || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
+
+ #NOTE: this hook is also called in ContentHandler::getDefaultModel. It's called here again to make sure
+ # hook funktions can force this method to return true even outside the mediawiki namespace.
+
+ wfRunHooks( 'TitleIsCssOrJsPage', array( $this, &$isCssOrJsPage ) );
+
+ return $isCssOrJsPage;
}
/**
* @return Bool
*/
public function isCssJsSubpage() {
- return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) );
+ return ( NS_USER == $this->mNamespace && $this->isSubpage()
+ && ( $this->hasContentModel( CONTENT_MODEL_CSS )
+ || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) ) );
}
/**
* @return Bool
*/
public function isCssSubpage() {
- return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) );
+ return ( NS_USER == $this->mNamespace && $this->isSubpage()
+ && $this->hasContentModel( CONTENT_MODEL_CSS ) );
}
/**
* @return Bool
*/
public function isJsSubpage() {
- return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.js$/", $this->mTextform ) );
+ return ( NS_USER == $this->mNamespace && $this->isSubpage()
+ && $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
}
/**
* andthe wfArrayToCGI moved to getLocalURL();
*
* @since 1.19 (r105919)
+ * @param $query
+ * @param $query2 bool
* @return String
*/
private static function fixUrlQueryArgs( $query, $query2 = false ) {
* See getLocalURL for the arguments.
*
* @see self::getLocalURL
+ * @param $query string
+ * @param $query2 bool|string
* @return String the URL
*/
public function escapeLocalURL( $query = '', $query2 = false ) {
/**
* Get the expiry time for the restriction against a given action
*
+ * @param $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.
+ * or not protected at all, or false if the action is not recognised.
*/
public function getRestrictionExpiry( $action ) {
if ( !$this->mRestrictionsLoaded ) {
$this->mRestrictions['edit'] = explode( ',', trim( $temp[0] ) );
$this->mRestrictions['move'] = explode( ',', trim( $temp[0] ) );
} else {
- $this->mRestrictions[$temp[0]] = explode( ',', trim( $temp[1] ) );
+ $restriction = trim( $temp[1] );
+ if( $restriction != '' ) { //some old entries are empty
+ $this->mRestrictions[$temp[0]] = explode( ',', $restriction );
+ }
}
}
if ( !$this->getArticleID( $flags ) ) {
return $this->mRedirect = false;
}
+
$linkCache = LinkCache::singleton();
- $this->mRedirect = (bool)$linkCache->getGoodLinkFieldObj( $this, 'redirect' );
+ $cached = $linkCache->getGoodLinkFieldObj( $this, 'redirect' );
+ if ( $cached === null ) { # check the assumption that the cache actually knows about this title
+ # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209
+ # as a stop gap, perhaps log this, but don't throw an exception?
+ throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() );
+ }
+
+ $this->mRedirect = (bool)$cached;
return $this->mRedirect;
}
return $this->mLength = 0;
}
$linkCache = LinkCache::singleton();
- $this->mLength = intval( $linkCache->getGoodLinkFieldObj( $this, 'length' ) );
+ $cached = $linkCache->getGoodLinkFieldObj( $this, 'length' );
+ if ( $cached === null ) { # check the assumption that the cache actually knows about this title
+ # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209
+ # as a stop gap, perhaps log this, but don't throw an exception?
+ throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() );
+ }
+
+ $this->mLength = intval( $cached );
return $this->mLength;
}
return $this->mLatestID = 0;
}
$linkCache = LinkCache::singleton();
- $this->mLatestID = intval( $linkCache->getGoodLinkFieldObj( $this, 'revision' ) );
+ $cached = $linkCache->getGoodLinkFieldObj( $this, 'revision' );
+ if ( $cached === null ) { # check the assumption that the cache actually knows about this title
+ # XXX: this does apparently happen, see https://bugzilla.wikimedia.org/show_bug.cgi?id=37209
+ # as a stop gap, perhaps log this, but don't throw an exception?
+ throw new MWException( "LinkCache doesn't currently know about this title: " . $this->getPrefixedDBkey() );
+ }
+
+ $this->mLatestID = intval( $cached );
return $this->mLatestID;
}
$this->mRedirect = null;
$this->mLength = -1;
$this->mLatestID = false;
+ $this->mContentModel = false;
$this->mEstimateRevisions = null;
}
$res = $db->select(
array( 'page', $table ),
- array( 'page_namespace', 'page_title', 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
+ self::getSelectFields(),
array(
"{$prefix}_from=page_id",
"{$prefix}_namespace" => $this->getNamespace(),
* @return Array of Title objects linking here
*/
public function getLinksFrom( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) {
+ global $wgContentHandlerUseDB;
+
$id = $this->getArticleID();
# If the page doesn't exist; there can't be any link from this page
$namespaceFiled = "{$prefix}_namespace";
$titleField = "{$prefix}_title";
+ $fields = array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' );
+ if ( $wgContentHandlerUseDB ) $fields[] = 'page_content_model';
+
$res = $db->select(
array( $table, 'page' ),
- array( $namespaceFiled, $titleField, 'page_id', 'page_len', 'page_is_redirect', 'page_latest' ),
+ $fields,
array( "{$prefix}_from" => $id ),
__METHOD__,
$options,
$protected = $this->isProtected();
// Do the actual move
- $err = $this->moveToInternal( $nt, $reason, $createRedirect );
- if ( is_array( $err ) ) {
- # @todo FIXME: What about the File we have already moved?
- $dbw->rollback( __METHOD__ );
- return $err;
- }
+ $this->moveToInternal( $nt, $reason, $createRedirect );
// Refresh the sortkey for this row. Be careful to avoid resetting
// cl_timestamp, which may disturb time-based lists on some sites.
* @param $reason String The reason for the move
* @param $createRedirect Bool Whether to leave a redirect at the old title. Ignored
* if the user doesn't have the suppressredirect right
+ * @throws MWException
*/
private function moveToInternal( &$nt, $reason = '', $createRedirect = true ) {
global $wgUser, $wgContLang;
* @return Bool
*/
public function isSingleRevRedirect() {
+ global $wgContentHandlerUseDB;
+
$dbw = wfGetDB( DB_MASTER );
+
# Is it a redirect?
+ $fields = array( 'page_is_redirect', 'page_latest', 'page_id' );
+ if ( $wgContentHandlerUseDB ) $fields[] = 'page_content_model';
+
$row = $dbw->selectRow( 'page',
- array( 'page_is_redirect', 'page_latest', 'page_id' ),
+ $fields,
$this->pageCond(),
__METHOD__,
array( 'FOR UPDATE' )
$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 ) ? intval( $row->page_content_model ) : false;
if ( !$this->mRedirect ) {
return false;
}
if( !is_object( $rev ) ){
return false;
}
- $text = $rev->getText();
+ $content = $rev->getContent();
# Does the redirect point to the source?
# Or is it a broken self-redirect, usually caused by namespace collisions?
- $m = array();
- if ( preg_match( "/\\[\\[\\s*([^\\]\\|]*)]]/", $text, $m ) ) {
- $redirTitle = Title::newFromText( $m[1] );
- if ( !is_object( $redirTitle ) ||
- ( $redirTitle->getPrefixedDBkey() != $this->getPrefixedDBkey() &&
- $redirTitle->getPrefixedDBkey() != $nt->getPrefixedDBkey() ) ) {
+ $redirTitle = $content->getRedirectTarget();
+
+ 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
- wfDebug( __METHOD__ . ": failsafe\n" );
+ # 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;
}
- return true;
}
/**
*/
public function getPreviousRevisionID( $revId, $flags = 0 ) {
$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
- return $db->selectField( 'revision', 'rev_id',
+ $revId = $db->selectField( 'revision', 'rev_id',
array(
'rev_page' => $this->getArticleID( $flags ),
'rev_id < ' . intval( $revId )
__METHOD__,
array( 'ORDER BY' => 'rev_id DESC' )
);
+
+ if ( $revId === false ) {
+ return false;
+ } else {
+ return intval( $revId );
+ }
}
/**
*/
public function getNextRevisionID( $revId, $flags = 0 ) {
$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
- return $db->selectField( 'revision', 'rev_id',
+ $revId = $db->selectField( 'revision', 'rev_id',
array(
'rev_page' => $this->getArticleID( $flags ),
'rev_id > ' . intval( $revId )
__METHOD__,
array( 'ORDER BY' => 'rev_id' )
);
+
+ if ( $revId === false ) {
+ return false;
+ } else {
+ return intval( $revId );
+ }
}
/**
$pageId = $this->getArticleID( $flags );
if ( $pageId ) {
$db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE );
- $row = $db->selectRow( 'revision', '*',
+ $row = $db->selectRow( 'revision', Revision::selectFields(),
array( 'rev_page' => $pageId ),
__METHOD__,
array( 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 )
$dbr = wfGetDB( DB_SLAVE );
$this->mNotificationTimestamp[$uid] = $dbr->selectField( 'watchlist',
'wl_notificationtimestamp',
- array( 'wl_namespace' => $this->getNamespace(),
+ array(
+ 'wl_user' => $user->getId(),
+ 'wl_namespace' => $this->getNamespace(),
'wl_title' => $this->getDBkey(),
- 'wl_user' => $user->getId()
),
__METHOD__
);
* $wgLang (such as special pages, which are in the user language).
*
* @since 1.18
- * @return object Language
+ * @return Language
*/
public function getPageLanguage() {
global $wgLang;