From 68ca2fe2fddc204317b4721f5685f7aea049d14e Mon Sep 17 00:00:00 2001 From: Alexandre Emsenhuber Date: Sun, 11 Dec 2011 14:48:45 +0000 Subject: [PATCH] Group related functions --- includes/Title.php | 1511 ++++++++++++++++++++++---------------------- 1 file changed, 753 insertions(+), 758 deletions(-) diff --git a/includes/Title.php b/includes/Title.php index 2caa5cadb2..5ee2eab539 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -479,6 +479,33 @@ class Title { return $wgLegalTitleChars; } + /** + * Returns a simple regex that will match on characters and sequences invalid in titles. + * Note that this doesn't pick up many things that could be wrong with titles, but that + * replacing this regex with something valid will make many titles valid. + * + * @return String regex string + */ + static function getTitleInvalidRegex() { + static $rxTc = false; + if ( !$rxTc ) { + # Matching titles will be held as illegal. + $rxTc = '/' . + # Any character not allowed is forbidden... + '[^' . self::legalChars() . ']' . + # URL percent encoding sequences interfere with the ability + # to round-trip titles -- you can't link to them consistently. + '|%[0-9A-Fa-f]{2}' . + # XML/HTML character references produce similar issues. + '|&[A-Za-z0-9\x80-\xff]+;' . + '|&#[0-9]+;' . + '|&#x[0-9A-Fa-f]+;' . + '/S'; + } + + return $rxTc; + } + /** * Get a string representation of a title suitable for * including in a search index @@ -544,6 +571,22 @@ class Title { return Sanitizer::escapeId( $fragment, 'noninitial' ); } + /** + * Callback for usort() to do title sorts by (namespace, title) + * + * @param $a Title + * @param $b Title + * + * @return Integer: result of string comparison, or namespace comparison + */ + public static function compare( $a, $b ) { + if ( $a->getNamespace() == $b->getNamespace() ) { + return strcmp( $a->getText(), $b->getText() ); + } else { + return $a->getNamespace() - $b->getNamespace(); + } + } + /** * Determine whether the object refers to a page within * this project. @@ -630,6 +673,15 @@ class Title { return $this->mDbkeyform; } + /** + * Get the DB key with the initial letter case as specified by the user + * + * @return String DB key + */ + function getUserCaseDBKey() { + return $this->mUserCaseDBKey; + } + /** * Get the namespace index, i.e. one of the NS_xxxx constants. * @@ -676,15 +728,6 @@ class Title { return $wgContLang->getNsText( $this->mNamespace ); } - /** - * Get the DB key with the initial letter case as specified by the user - * - * @return String DB key - */ - function getUserCaseDBKey() { - return $this->mUserCaseDBKey; - } - /** * Get the namespace text of the subject (rather than talk) page * @@ -715,163 +758,491 @@ class Title { } /** - * Get the Title fragment (i.e.\ the bit after the #) in text form + * Is this in a namespace that allows actual pages? * - * @return String Title fragment - */ - public function getFragment() { return $this->mFragment; } - - /** - * Get the fragment in URL form, including the "#" character if there is one - * @return String Fragment in URL form + * @return Bool + * @internal note -- uses hardcoded namespace index instead of constants */ - public function getFragmentForURL() { - if ( $this->mFragment == '' ) { - return ''; - } else { - return '#' . Title::escapeFragmentForURL( $this->mFragment ); - } + public function canExist() { + return $this->mNamespace >= 0 && $this->mNamespace != NS_MEDIA; } /** - * Get the default namespace index, for when there is no namespace + * Can this title be added to a user's watchlist? * - * @return Int Default namespace index + * @return Bool TRUE or FALSE */ - public function getDefaultNamespace() { - return $this->mDefaultNamespace; + public function isWatchable() { + return !$this->isExternal() && MWNamespace::isWatchable( $this->getNamespace() ); } /** - * Get title for search index + * Returns true if this is a special page. * - * @return String a stripped-down title string ready for the - * search index + * @return boolean */ - public function getIndexTitle() { - return Title::indexTitle( $this->mNamespace, $this->mTextform ); + public function isSpecialPage() { + return $this->getNamespace() == NS_SPECIAL; } /** - * Get the prefixed database key form + * Returns true if this title resolves to the named special page * - * @return String the prefixed title, with underscores and - * any interwiki and namespace prefixes + * @param $name String The special page name + * @return boolean */ - public function getPrefixedDBkey() { - $s = $this->prefix( $this->mDbkeyform ); - $s = str_replace( ' ', '_', $s ); - return $s; + public function isSpecial( $name ) { + if ( $this->isSpecialPage() ) { + list( $thisName, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $this->getDBkey() ); + if ( $name == $thisName ) { + return true; + } + } + return false; } /** - * Get the prefixed title with spaces. - * This is the form usually used for display + * 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 * - * @return String the prefixed title, with spaces + * @return Title */ - public function getPrefixedText() { - // @todo FIXME: Bad usage of empty() ? - if ( empty( $this->mPrefixedText ) ) { - $s = $this->prefix( $this->mTextform ); - $s = str_replace( '_', ' ', $s ); - $this->mPrefixedText = $s; + public function fixSpecialName() { + if ( $this->isSpecialPage() ) { + list( $canonicalName, $par ) = SpecialPageFactory::resolveAlias( $this->mDbkeyform ); + if ( $canonicalName ) { + $localName = SpecialPageFactory::getLocalNameFor( $canonicalName, $par ); + if ( $localName != $this->mDbkeyform ) { + return Title::makeTitle( NS_SPECIAL, $localName ); + } + } } - return $this->mPrefixedText; + return $this; } /** - /** - * Get the prefixed title with spaces, plus any fragment - * (part beginning with '#') - * - * @return String the prefixed title, with spaces and the fragment, including '#' + * Returns true if the title is inside the specified namespace. + * + * 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 $ns int The namespace + * @return bool + * @since 1.19 */ - public function getFullText() { - $text = $this->getPrefixedText(); - if ( $this->mFragment != '' ) { - $text .= '#' . $this->mFragment; - } - return $text; + public function inNamespace( $ns ) { + return MWNamespace::equals( $this->getNamespace(), $ns ); } /** - * Get the base page name, i.e. the leftmost part before any slashes + * Returns true if the title is inside one of the specified namespaces. * - * @return String Base name + * @param ...$namespaces The namespaces to check for + * @return bool + * @since 1.19 */ - public function getBaseText() { - if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { - return $this->getText(); + public function inNamespaces( /* ... */ ) { + $namespaces = func_get_args(); + if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) { + $namespaces = $namespaces[0]; } - $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] ); + foreach ( $namespaces as $ns ) { + if ( $this->inNamespace( $ns ) ) { + return true; + } } - return implode( '/', $parts ); + + return false; } /** - * Get the lowest-level subpage name, i.e. the rightmost part after any slashes + * 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. * - * @return String Subpage name + * 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 */ - public function getSubpageText() { - if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { - return( $this->mTextform ); - } - $parts = explode( '/', $this->mTextform ); - return( $parts[count( $parts ) - 1] ); + public function hasSubjectNamespace( $ns ) { + return MWNamespace::subjectEquals( $this->getNamespace(), $ns ); } /** - * Get the HTML-escaped displayable text form. - * Used for the title field in tags. + * 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 String the text, including any prefixes + * @return Boolean */ - public function getEscapedText() { - return htmlspecialchars( $this->getPrefixedText() ); + public function isContentPage() { + return MWNamespace::isContent( $this->getNamespace() ); } /** - * Get a URL-encoded form of the subpage text + * Would anybody with sufficient privileges be able to move this page? + * Some pages just aren't movable. * - * @return String URL-encoded subpage name + * @return Bool TRUE or FALSE */ - public function getSubpageUrlForm() { - $text = $this->getSubpageText(); - $text = wfUrlencode( str_replace( ' ', '_', $text ) ); - return( $text ); + public function isMovable() { + if ( !MWNamespace::isMovable( $this->getNamespace() ) || $this->getInterwiki() != '' ) { + // Interwiki title or immovable namespace. Hooks don't get to override here + return false; + } + + $result = true; + wfRunHooks( 'TitleIsMovable', array( $this, &$result ) ); + return $result; } /** - * Get a URL-encoded title (not an actual URL) including interwiki + * Is this the mainpage? + * @note Title::newFromText seams to be sufficiently optimized by the title + * cache that we don't need to over-optimize by doing direct comparisons and + * acidentally creating new bugs where $title->equals( Title::newFromText() ) + * ends up reporting something differently than $title->isMainPage(); * - * @return String the URL-encoded form + * @since 1.18 + * @return Bool */ - public function getPrefixedURL() { - $s = $this->prefix( $this->mDbkeyform ); - $s = wfUrlencode( str_replace( ' ', '_', $s ) ); - return $s; + public function isMainPage() { + return $this->equals( Title::newMainPage() ); } /** - * Get a real URL referring to this title, with interwiki link and - * fragment + * Is this a subpage? * - * @param $query \twotypes{\string,\array} 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). - * @param $variant String language variant of url (for sr, zh..) - * @return String the URL + * @return Bool */ - public function getFullURL( $query = '', $variant = false ) { - # Hand off all the decisions on urls to getLocalURL - $url = $this->getLocalURL( $query, $variant ); - + public function isSubpage() { + return MWNamespace::hasSubpages( $this->mNamespace ) + ? strpos( $this->getText(), '/' ) !== false + : false; + } + + /** + * Is this a conversion table for the LanguageConverter? + * + * @return Bool + */ + public function isConversionTable() { + return $this->getNamespace() == NS_MEDIAWIKI && + strpos( $this->getText(), 'Conversiontable' ) !== false; + } + + /** + * Does that page contain wikitext, or it is JS, CSS or whatever? + * + * @return Bool + */ + public function isWikitextPage() { + $retval = !$this->isCssOrJsPage() && !$this->isCssJsSubpage(); + wfRunHooks( 'TitleIsWikitextPage', array( $this, &$retval ) ); + return $retval; + } + + /** + * Could this page contain custom CSS or JavaScript, based + * on the title? + * + * @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; + } + + /** + * Is this a .css or .js subpage of a user page? + * @return Bool + */ + public function isCssJsSubpage() { + return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) ); + } + + /** + * Is this a *valid* .css or .js subpage of a user page? + * + * @return Bool + * @deprecated since 1.17 + */ + public function isValidCssJsSubpage() { + return $this->isCssJsSubpage(); + } + + /** + * Trim down a .css or .js subpage title to get the corresponding skin name + * + * @return string containing skin name from .css or .js subpage title + */ + public function getSkinFromCssJsSubpage() { + $subpage = explode( '/', $this->mTextform ); + $subpage = $subpage[ count( $subpage ) - 1 ]; + $lastdot = strrpos( $subpage, '.' ); + if ( $lastdot === false ) + return $subpage; # Never happens: only called for names ending in '.css' or '.js' + return substr( $subpage, 0, $lastdot ); + } + + /** + * Is this a .css subpage of a user page? + * + * @return Bool + */ + public function isCssSubpage() { + return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) ); + } + + /** + * Is this a .js subpage of a user page? + * + * @return Bool + */ + public function isJsSubpage() { + return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.js$/", $this->mTextform ) ); + } + + /** + * Is this a talk page of some sort? + * + * @return Bool + */ + public function isTalkPage() { + return MWNamespace::isTalk( $this->getNamespace() ); + } + + /** + * Get a Title object associated with the talk page of this article + * + * @return Title the object for the talk page + */ + public function getTalkPage() { + return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() ); + } + + /** + * Get a title object associated with the subject page of this + * talk page + * + * @return Title the object for the subject page + */ + public function getSubjectPage() { + // Is this the same title? + $subjectNS = MWNamespace::getSubject( $this->getNamespace() ); + if ( $this->getNamespace() == $subjectNS ) { + return $this; + } + return Title::makeTitle( $subjectNS, $this->getDBkey() ); + } + + /** + * Get the default namespace index, for when there is no namespace + * + * @return Int Default namespace index + */ + public function getDefaultNamespace() { + return $this->mDefaultNamespace; + } + + /** + * Get title for search index + * + * @return String a stripped-down title string ready for the + * search index + */ + public function getIndexTitle() { + return Title::indexTitle( $this->mNamespace, $this->mTextform ); + } + + /** + * Get the Title fragment (i.e.\ the bit after the #) in text form + * + * @return String Title fragment + */ + public function getFragment() { + return $this->mFragment; + } + + /** + * Get the fragment in URL form, including the "#" character if there is one + * @return String Fragment in URL form + */ + public function getFragmentForURL() { + if ( $this->mFragment == '' ) { + return ''; + } else { + return '#' . Title::escapeFragmentForURL( $this->mFragment ); + } + } + + /** + * 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. + * Still in active use privately. + * + * @param $fragment String text + */ + public function setFragment( $fragment ) { + $this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) ); + } + + /** + * Prefix some arbitrary text with the namespace or interwiki prefix + * of this object + * + * @param $name String the text + * @return String the prefixed text + * @private + */ + private function prefix( $name ) { + $p = ''; + if ( $this->mInterwiki != '' ) { + $p = $this->mInterwiki . ':'; + } + + if ( 0 != $this->mNamespace ) { + $p .= $this->getNsText() . ':'; + } + return $p . $name; + } + + /** + * Get the prefixed database key form + * + * @return String the prefixed title, with underscores and + * any interwiki and namespace prefixes + */ + public function getPrefixedDBkey() { + $s = $this->prefix( $this->mDbkeyform ); + $s = str_replace( ' ', '_', $s ); + return $s; + } + + /** + * Get the prefixed title with spaces. + * This is the form usually used for display + * + * @return String the prefixed title, with spaces + */ + public function getPrefixedText() { + // @todo FIXME: Bad usage of empty() ? + if ( empty( $this->mPrefixedText ) ) { + $s = $this->prefix( $this->mTextform ); + $s = str_replace( '_', ' ', $s ); + $this->mPrefixedText = $s; + } + return $this->mPrefixedText; + } + + /** + * Return a string representation of this title + * + * @return String representation of this title + */ + 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 '#' + */ + public function getFullText() { + $text = $this->getPrefixedText(); + if ( $this->mFragment != '' ) { + $text .= '#' . $this->mFragment; + } + return $text; + } + + /** + * Get the base page name, i.e. the leftmost part before any slashes + * + * @return String Base name + */ + 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 lowest-level subpage name, i.e. the rightmost part after any slashes + * + * @return String Subpage name + */ + public function getSubpageText() { + if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { + return( $this->mTextform ); + } + $parts = explode( '/', $this->mTextform ); + return( $parts[count( $parts ) - 1] ); + } + + /** + * Get the HTML-escaped displayable text form. + * Used for the title field in tags. + * + * @return String the text, including any prefixes + */ + public function getEscapedText() { + return htmlspecialchars( $this->getPrefixedText() ); + } + + /** + * Get a URL-encoded form of the subpage text + * + * @return String URL-encoded subpage name + */ + public function getSubpageUrlForm() { + $text = $this->getSubpageText(); + $text = wfUrlencode( str_replace( ' ', '_', $text ) ); + return( $text ); + } + + /** + * Get a URL-encoded title (not an actual URL) including interwiki + * + * @return String the URL-encoded form + */ + public function getPrefixedURL() { + $s = $this->prefix( $this->mDbkeyform ); + $s = wfUrlencode( str_replace( ' ', '_', $s ) ); + return $s; + } + + /** + * Get a real URL referring to this title, with interwiki link and + * fragment + * + * @param $query \twotypes{\string,\array} 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). + * @param $variant String language variant of url (for sr, zh..) + * @return String the URL + */ + public function getFullURL( $query = '', $variant = false ) { + # Hand off all the decisions on urls to getLocalURL + $url = $this->getLocalURL( $query, $variant ); + # 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 @@ -1082,101 +1453,6 @@ class Title { return $s; } - /** - * Is this page "semi-protected" - the *only* protection is autoconfirm? - * - * @param $action String Action to check (default: edit) - * @return Bool - */ - public function isSemiProtected( $action = 'edit' ) { - if ( $this->exists() ) { - $restrictions = $this->getRestrictions( $action ); - if ( count( $restrictions ) > 0 ) { - foreach ( $restrictions as $restriction ) { - if ( strtolower( $restriction ) != 'autoconfirmed' ) { - return false; - } - } - } else { - # Not protected - return false; - } - return true; - } else { - # If it doesn't exist, it can't be protected - return false; - } - } - - /** - * Does the title correspond to a protected article? - * - * @param $action String the action the page is protected from, - * by default checks all actions. - * @return Bool - */ - public function isProtected( $action = '' ) { - global $wgRestrictionLevels; - - $restrictionTypes = $this->getRestrictionTypes(); - - # Special pages have inherent protection - if( $this->isSpecialPage() ) { - 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; - } - - /** - * Determines if $user is unable to edit this page because it has been protected - * by $wgNamespaceProtection. - * - * @param $user User object to check permissions - * @return Bool - */ - 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; - } - - /** - * Is this a conversion table for the LanguageConverter? - * - * @return Bool - */ - public function isConversionTable() { - if( - $this->getNamespace() == NS_MEDIAWIKI && - strpos( $this->getText(), 'Conversiontable' ) !== false - ) - { - return true; - } - - return false; - } - /** * Is $wgUser watching this page? * @@ -1784,15 +2060,88 @@ class Title { ); } - $errors = array(); - while( count( $checks ) > 0 && - !( $short && count( $errors ) > 0 ) ) { - $method = array_shift( $checks ); - $errors = $this->$method( $action, $user, $errors, $doExpensiveQueries, $short ); + $errors = array(); + while( count( $checks ) > 0 && + !( $short && count( $errors ) > 0 ) ) { + $method = array_shift( $checks ); + $errors = $this->$method( $action, $user, $errors, $doExpensiveQueries, $short ); + } + + wfProfileOut( __METHOD__ ); + return $errors; + } + + /** + * Protect css subpages of user pages: can $wgUser edit + * this page? + * + * @deprecated in 1.19; will be removed in 1.20. Use getUserPermissionsErrors() instead. + * @return Bool + */ + public function userCanEditCssSubpage() { + global $wgUser; + wfDeprecated( __METHOD__ ); + return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'editusercss' ) ) + || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) ); + } + + /** + * Protect js subpages of user pages: can $wgUser edit + * this page? + * + * @deprecated in 1.19; will be removed in 1.20. Use getUserPermissionsErrors() instead. + * @return Bool + */ + public function userCanEditJsSubpage() { + global $wgUser; + wfDeprecated( __METHOD__ ); + return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'edituserjs' ) ) + || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) ); + } + + /** + * 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, array( 'create' ) ); + } else { + # Only the create and upload restrictions apply to non-existing titles + $types = array_intersect( $types, array( 'create', 'upload' ) ); + } + return $types; + } + + /** + * Returns restriction types for the current Title + * + * @return array applicable restriction types + */ + public function getRestrictionTypes() { + if ( $this->isSpecialPage() ) { + return array(); + } + + $types = self::getFilteredRestrictionTypes( $this->exists() ); + + if ( $this->getNamespace() != NS_FILE ) { + # Remove the upload restriction for non-file titles + $types = array_diff( $types, array( 'upload' ) ); } - wfProfileOut( __METHOD__ ); - return $errors; + wfRunHooks( 'TitleGetRestrictionTypes', array( $this, &$types ) ); + + wfDebug( __METHOD__ . ': applicable restrictions to [[' . + $this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" ); + + return $types; } /** @@ -1882,285 +2231,104 @@ class Title { if ( $create_perm ) { $params = array( "[create=$create_perm] $expiry_description", '' ); $log->addEntry( ( isset( $this->mRestrictions['create'] ) && $this->mRestrictions['create'] ) ? 'modify' : 'protect', $this, trim( $reason ), $params ); - } else { - $log->addEntry( 'unprotect', $this, $reason ); - } - } - - return true; - } - - /** - * Remove any title protection due to page existing - */ - public function deleteTitleProtection() { - $dbw = wfGetDB( DB_MASTER ); - - $dbw->delete( - 'protected_titles', - array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ), - __METHOD__ - ); - $this->mTitleProtection = false; - } - - /** - * Would anybody with sufficient privileges be able to move this page? - * Some pages just aren't movable. - * - * @return Bool TRUE or FALSE - */ - public function isMovable() { - if ( !MWNamespace::isMovable( $this->getNamespace() ) || $this->getInterwiki() != '' ) { - // Interwiki title or immovable namespace. Hooks don't get to override here - return false; - } - - $result = true; - wfRunHooks( 'TitleIsMovable', array( $this, &$result ) ); - return $result; - } - - /** - * Is this the mainpage? - * @note Title::newFromText seams to be sufficiently optimized by the title - * cache that we don't need to over-optimize by doing direct comparisons and - * acidentally creating new bugs where $title->equals( Title::newFromText() ) - * ends up reporting something differently than $title->isMainPage(); - * - * @since 1.18 - * @return Bool - */ - public function isMainPage() { - return $this->equals( Title::newMainPage() ); - } - - /** - * Is this a talk page of some sort? - * - * @return Bool - */ - public function isTalkPage() { - return MWNamespace::isTalk( $this->getNamespace() ); - } - - /** - * Is this a subpage? - * - * @return Bool - */ - public function isSubpage() { - return MWNamespace::hasSubpages( $this->mNamespace ) - ? strpos( $this->getText(), '/' ) !== false - : false; - } - - /** - * Returns true if the title is inside the specified namespace. - * - * 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 $ns int The namespace - * @return bool - * @since 1.19 - */ - public function inNamespace( $ns ) { - return MWNamespace::equals( $this->getNamespace(), $ns ); - } - - /** - * Returns true if the title is inside one of the specified namespaces. - * - * @param ...$namespaces The namespaces to check for - * @return bool - * @since 1.19 - */ - public function inNamespaces( /* ... */ ) { - $namespaces = func_get_args(); - if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) { - $namespaces = $namespaces[0]; - } - - foreach ( $namespaces as $ns ) { - if ( $this->inNamespace( $ns ) ) { - return true; - } - } - - return false; - } - - /** - * 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 equivilance - * against both NS_USER and NS_USER_TALK, and is also forward compatible. - * @since 1.19 - */ - public function hasSubjectNamespace( $ns ) { - return MWNamespace::subjectEquals( $this->getNamespace(), $ns ); - } - - /** - * Does this have subpages? (Warning, usually requires an extra DB query.) - * - * @return Bool - */ - public function hasSubpages() { - if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { - # Duh - return false; - } - - # 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 ( isset( $this->mHasSubpages ) ) { - return $this->mHasSubpages; - } - - $subpages = $this->getSubpages( 1 ); - if ( $subpages instanceof TitleArray ) { - return $this->mHasSubpages = (bool)$subpages->count(); - } - return $this->mHasSubpages = false; - } - - /** - * Get all subpages of this page. - * - * @param $limit Int maximum number of subpages to fetch; -1 for no limit - * @return mixed TitleArray, or empty array if this page's namespace - * doesn't allow subpages - */ - public function getSubpages( $limit = -1 ) { - if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) { - return array(); - } - - $dbr = wfGetDB( DB_SLAVE ); - $conds['page_namespace'] = $this->getNamespace(); - $conds[] = 'page_title ' . $dbr->buildLike( $this->getDBkey() . '/', $dbr->anyString() ); - $options = array(); - if ( $limit > -1 ) { - $options['LIMIT'] = $limit; - } - return $this->mSubpages = TitleArray::newFromResult( - $dbr->select( 'page', - array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ), - $conds, - __METHOD__, - $options - ) - ); - } - - /** - * Does that page contain wikitext, or it is JS, CSS or whatever? - * - * @return Bool - */ - public function isWikitextPage() { - $retval = !$this->isCssOrJsPage() && !$this->isCssJsSubpage(); - wfRunHooks( 'TitleIsWikitextPage', array( $this, &$retval ) ); - return $retval; - } - - /** - * Could this page contain custom CSS or JavaScript, based - * on the title? - * - * @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; - } - - /** - * Is this a .css or .js subpage of a user page? - * @return Bool - */ - public function isCssJsSubpage() { - return ( NS_USER == $this->mNamespace and preg_match( "/\\/.*\\.(?:css|js)$/", $this->mTextform ) ); - } - - /** - * Is this a *valid* .css or .js subpage of a user page? - * - * @return Bool - * @deprecated since 1.17 - */ - public function isValidCssJsSubpage() { - return $this->isCssJsSubpage(); - } - - /** - * Trim down a .css or .js subpage title to get the corresponding skin name - * - * @return string containing skin name from .css or .js subpage title - */ - public function getSkinFromCssJsSubpage() { - $subpage = explode( '/', $this->mTextform ); - $subpage = $subpage[ count( $subpage ) - 1 ]; - $lastdot = strrpos( $subpage, '.' ); - if ( $lastdot === false ) - return $subpage; # Never happens: only called for names ending in '.css' or '.js' - return substr( $subpage, 0, $lastdot ); + } else { + $log->addEntry( 'unprotect', $this, $reason ); + } + } + + return true; } /** - * Is this a .css subpage of a user page? - * - * @return Bool + * Remove any title protection due to page existing */ - public function isCssSubpage() { - return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.css$/", $this->mTextform ) ); + public function deleteTitleProtection() { + $dbw = wfGetDB( DB_MASTER ); + + $dbw->delete( + 'protected_titles', + array( 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ), + __METHOD__ + ); + $this->mTitleProtection = false; } /** - * Is this a .js subpage of a user page? + * Is this page "semi-protected" - the *only* protection is autoconfirm? * + * @param $action String Action to check (default: edit) * @return Bool */ - public function isJsSubpage() { - return ( NS_USER == $this->mNamespace && preg_match( "/\\/.*\\.js$/", $this->mTextform ) ); + public function isSemiProtected( $action = 'edit' ) { + if ( $this->exists() ) { + $restrictions = $this->getRestrictions( $action ); + if ( count( $restrictions ) > 0 ) { + foreach ( $restrictions as $restriction ) { + if ( strtolower( $restriction ) != 'autoconfirmed' ) { + return false; + } + } + } else { + # Not protected + return false; + } + return true; + } else { + # If it doesn't exist, it can't be protected + return false; + } } /** - * Protect css subpages of user pages: can $wgUser edit - * this page? + * Does the title correspond to a protected article? * - * @deprecated in 1.19; will be removed in 1.20. Use getUserPermissionsErrors() instead. + * @param $action String the action the page is protected from, + * by default checks all actions. * @return Bool */ - public function userCanEditCssSubpage() { - global $wgUser; - wfDeprecated( __METHOD__ ); - return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'editusercss' ) ) - || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) ); + public function isProtected( $action = '' ) { + global $wgRestrictionLevels; + + $restrictionTypes = $this->getRestrictionTypes(); + + # Special pages have inherent protection + if( $this->isSpecialPage() ) { + 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; } /** - * Protect js subpages of user pages: can $wgUser edit - * this page? + * Determines if $user is unable to edit this page because it has been protected + * by $wgNamespaceProtection. * - * @deprecated in 1.19; will be removed in 1.20. Use getUserPermissionsErrors() instead. + * @param $user User object to check permissions * @return Bool */ - public function userCanEditJsSubpage() { - global $wgUser; - wfDeprecated( __METHOD__ ); - return ( ( $wgUser->isAllowedAll( 'editusercssjs', 'edituserjs' ) ) - || preg_match( '/^' . preg_quote( $wgUser->getName(), '/' ) . '\//', $this->mTextform ) ); + 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; } /** @@ -2271,6 +2439,34 @@ class Title { return array( $sources, $pagerestrictions ); } + /** + * Accessor/initialisation for mRestrictions + * + * @param $action String action that permission needs to be checked for + * @return Array of Strings the array of groups allowed to edit this article + */ + public function getRestrictions( $action ) { + if ( !$this->mRestrictionsLoaded ) { + $this->loadRestrictions(); + } + return isset( $this->mRestrictions[$action] ) + ? $this->mRestrictions[$action] + : array(); + } + + /** + * Get the expiry time for the restriction against a given 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. + */ + public function getRestrictionExpiry( $action ) { + if ( !$this->mRestrictionsLoaded ) { + $this->loadRestrictions(); + } + return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false; + } + /** * Returns cascading restrictions for the current article * @@ -2445,31 +2641,58 @@ class Title { } /** - * Accessor/initialisation for mRestrictions + * Does this have subpages? (Warning, usually requires an extra DB query.) * - * @param $action String action that permission needs to be checked for - * @return Array of Strings the array of groups allowed to edit this article + * @return Bool */ - public function getRestrictions( $action ) { - if ( !$this->mRestrictionsLoaded ) { - $this->loadRestrictions(); + public function hasSubpages() { + if ( !MWNamespace::hasSubpages( $this->mNamespace ) ) { + # Duh + return false; } - return isset( $this->mRestrictions[$action] ) - ? $this->mRestrictions[$action] - : array(); + + # 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 ( isset( $this->mHasSubpages ) ) { + return $this->mHasSubpages; + } + + $subpages = $this->getSubpages( 1 ); + if ( $subpages instanceof TitleArray ) { + return $this->mHasSubpages = (bool)$subpages->count(); + } + return $this->mHasSubpages = false; } /** - * Get the expiry time for the restriction against a given action + * Get all subpages of this page. * - * @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. + * @param $limit Int maximum number of subpages to fetch; -1 for no limit + * @return mixed TitleArray, or empty array if this page's namespace + * doesn't allow subpages */ - public function getRestrictionExpiry( $action ) { - if ( !$this->mRestrictionsLoaded ) { - $this->loadRestrictions(); + public function getSubpages( $limit = -1 ) { + if ( !MWNamespace::hasSubpages( $this->getNamespace() ) ) { + return array(); } - return isset( $this->mRestrictionsExpiry[$action] ) ? $this->mRestrictionsExpiry[$action] : false; + + $dbr = wfGetDB( DB_SLAVE ); + $conds['page_namespace'] = $this->getNamespace(); + $conds[] = 'page_title ' . $dbr->buildLike( $this->getDBkey() . '/', $dbr->anyString() ); + $options = array(); + if ( $limit > -1 ) { + $options['LIMIT'] = $limit; + } + return $this->mSubpages = TitleArray::newFromResult( + $dbr->select( 'page', + array( 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ), + $conds, + __METHOD__, + $options + ) + ); } /** @@ -2611,117 +2834,50 @@ class Title { } /** - * What is the page_latest field for this page? - * - * @param $flags Int a bit field; may be Title::GAID_FOR_UPDATE to select for update - * @return Int or 0 if the page doesn't exist - */ - public function getLatestRevID( $flags = 0 ) { - if ( $this->mLatestID !== false ) { - return intval( $this->mLatestID ); - } - # Calling getArticleID() loads the field from cache as needed - if ( !$this->getArticleID( $flags ) ) { - return $this->mLatestID = 0; - } - $linkCache = LinkCache::singleton(); - $this->mLatestID = intval( $linkCache->getGoodLinkFieldObj( $this, 'revision' ) ); - - return $this->mLatestID; - } - - /** - * 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 Article::doEdit() and Article::insertOn() to allow - * loading of the new page_id. It's also called from - * Article::doDeleteArticle() - * - * @param $newid Int the new Article ID - */ - public function resetArticleID( $newid ) { - $linkCache = LinkCache::singleton(); - $linkCache->clearLink( $this ); - - if ( $newid === false ) { - $this->mArticleID = -1; - } else { - $this->mArticleID = intval( $newid ); - } - $this->mRestrictionsLoaded = false; - $this->mRestrictions = array(); - $this->mRedirect = null; - $this->mLength = -1; - $this->mLatestID = false; - $this->mCounter = -1; - } - - /** - * Updates page_touched for this page; called from LinksUpdate.php - * - * @return Bool true if the update succeded - */ - public function invalidateCache() { - if ( wfReadOnly() ) { - return false; - } - $dbw = wfGetDB( DB_MASTER ); - $success = $dbw->update( - 'page', - array( 'page_touched' => $dbw->timestamp() ), - $this->pageCond(), - __METHOD__ - ); - HTMLFileCache::clearFileCache( $this ); - return $success; - } - - /** - * Prefix some arbitrary text with the namespace or interwiki prefix - * of this object + * What is the page_latest field for this page? * - * @param $name String the text - * @return String the prefixed text - * @private + * @param $flags Int a bit field; may be Title::GAID_FOR_UPDATE to select for update + * @return Int or 0 if the page doesn't exist */ - private function prefix( $name ) { - $p = ''; - if ( $this->mInterwiki != '' ) { - $p = $this->mInterwiki . ':'; + public function getLatestRevID( $flags = 0 ) { + if ( $this->mLatestID !== false ) { + return intval( $this->mLatestID ); } - - if ( 0 != $this->mNamespace ) { - $p .= $this->getNsText() . ':'; + # Calling getArticleID() loads the field from cache as needed + if ( !$this->getArticleID( $flags ) ) { + return $this->mLatestID = 0; } - return $p . $name; + $linkCache = LinkCache::singleton(); + $this->mLatestID = intval( $linkCache->getGoodLinkFieldObj( $this, 'revision' ) ); + + return $this->mLatestID; } /** - * Returns a simple regex that will match on characters and sequences invalid in titles. - * Note that this doesn't pick up many things that could be wrong with titles, but that - * replacing this regex with something valid will make many titles valid. + * This clears some fields in this object, and clears any associated + * keys in the "bad links" section of the link cache. * - * @return String regex string + * - This is called from Article::doEdit() and Article::insertOn() to allow + * loading of the new page_id. It's also called from + * Article::doDeleteArticle() + * + * @param $newid Int the new Article ID */ - static function getTitleInvalidRegex() { - static $rxTc = false; - if ( !$rxTc ) { - # Matching titles will be held as illegal. - $rxTc = '/' . - # Any character not allowed is forbidden... - '[^' . Title::legalChars() . ']' . - # URL percent encoding sequences interfere with the ability - # to round-trip titles -- you can't link to them consistently. - '|%[0-9A-Fa-f]{2}' . - # XML/HTML character references produce similar issues. - '|&[A-Za-z0-9\x80-\xff]+;' . - '|&#[0-9]+;' . - '|&#x[0-9A-Fa-f]+;' . - '/S'; - } + public function resetArticleID( $newid ) { + $linkCache = LinkCache::singleton(); + $linkCache->clearLink( $this ); - return $rxTc; + if ( $newid === false ) { + $this->mArticleID = -1; + } else { + $this->mArticleID = intval( $newid ); + } + $this->mRestrictionsLoaded = false; + $this->mRestrictions = array(); + $this->mRedirect = null; + $this->mLength = -1; + $this->mLatestID = false; + $this->mCounter = -1; } /** @@ -2938,44 +3094,6 @@ class Title { return true; } - /** - * 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. - * Still in active use privately. - * - * @param $fragment String text - */ - public function setFragment( $fragment ) { - $this->mFragment = str_replace( '_', ' ', substr( $fragment, 1 ) ); - } - - /** - * Get a Title object associated with the talk page of this article - * - * @return Title the object for the talk page - */ - public function getTalkPage() { - return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() ); - } - - /** - * Get a title object associated with the subject page of this - * talk page - * - * @return Title the object for the subject page - */ - public function getSubjectPage() { - // Is this the same title? - $subjectNS = MWNamespace::getSubject( $this->getNamespace() ); - if ( $this->getNamespace() == $subjectNS ) { - return $this; - } - return Title::makeTitle( $subjectNS, $this->getDBkey() ); - } - /** * Get an array of Title objects linking to this Title * Also stores the IDs in the link cache. @@ -3713,15 +3831,6 @@ class Title { return true; } - /** - * Can this title be added to a user's watchlist? - * - * @return Bool TRUE or FALSE - */ - public function isWatchable() { - return !$this->isExternal() && MWNamespace::isWatchable( $this->getNamespace() ); - } - /** * Get categories to which this Title belongs and return an array of * categories' names. @@ -3967,31 +4076,6 @@ class Title { && strpos( $this->getDBkey(), $title->getDBkey() . '/' ) === 0; } - /** - * Callback for usort() to do title sorts by (namespace, title) - * - * @param $a Title - * @param $b Title - * - * @return Integer: result of string comparison, or namespace comparison - */ - public static function compare( $a, $b ) { - if ( $a->getNamespace() == $b->getNamespace() ) { - return strcmp( $a->getText(), $b->getText() ); - } else { - return $a->getNamespace() - $b->getNamespace(); - } - } - - /** - * Return a string representation of this title - * - * @return String representation of this title - */ - public function __toString() { - return $this->getPrefixedText(); - } - /** * Check if page exists. For historical reasons, this function simply * checks for the existence of the title in the page table, and will @@ -4103,13 +4187,23 @@ class Title { } /** - * Is this in a namespace that allows actual pages? + * Updates page_touched for this page; called from LinksUpdate.php * - * @return Bool - * @internal note -- uses hardcoded namespace index instead of constants + * @return Bool true if the update succeded */ - public function canExist() { - return $this->mNamespace >= 0 && $this->mNamespace != NS_MEDIA; + public function invalidateCache() { + if ( wfReadOnly() ) { + return false; + } + $dbw = wfGetDB( DB_MASTER ); + $success = $dbw->update( + 'page', + array( 'page_touched' => $dbw->timestamp() ), + $this->pageCond(), + __METHOD__ + ); + HTMLFileCache::clearFileCache( $this ); + return $success; } /** @@ -4207,61 +4301,6 @@ class Title { return $prepend . $namespaceKey; } - /** - * Returns true if this is a special page. - * - * @return boolean - */ - public function isSpecialPage() { - return $this->getNamespace() == NS_SPECIAL; - } - - /** - * Returns true if this title resolves to the named special page - * - * @param $name String The special page name - * @return boolean - */ - public function isSpecial( $name ) { - if ( $this->isSpecialPage() ) { - list( $thisName, /* $subpage */ ) = SpecialPageFactory::resolveAlias( $this->getDBkey() ); - if ( $name == $thisName ) { - return true; - } - } - return false; - } - - /** - * 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 - * - * @return Title - */ - public function fixSpecialName() { - if ( $this->isSpecialPage() ) { - list( $canonicalName, $par ) = SpecialPageFactory::resolveAlias( $this->mDbkeyform ); - if ( $canonicalName ) { - $localName = SpecialPageFactory::getLocalNameFor( $canonicalName, $par ); - if ( $localName != $this->mDbkeyform ) { - return Title::makeTitle( NS_SPECIAL, $localName ); - } - } - } - return $this; - } - - /** - * 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 Boolean - */ - public function isContentPage() { - return MWNamespace::isContent( $this->getNamespace() ); - } - /** * Get all extant redirects to this Title * @@ -4344,50 +4383,6 @@ class Title { } - /** - * Returns restriction types for the current Title - * - * @return array applicable restriction types - */ - public function getRestrictionTypes() { - if ( $this->isSpecialPage() ) { - return array(); - } - - $types = self::getFilteredRestrictionTypes( $this->exists() ); - - if ( $this->getNamespace() != NS_FILE ) { - # Remove the upload restriction for non-file titles - $types = array_diff( $types, array( 'upload' ) ); - } - - wfRunHooks( 'TitleGetRestrictionTypes', array( $this, &$types ) ); - - wfDebug( __METHOD__ . ': applicable restrictions to [[' . - $this->getPrefixedText() . ']] are {' . implode( ',', $types ) . "}\n" ); - - return $types; - } - /** - * 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, array( 'create' ) ); - } else { - # Only the create and upload restrictions apply to non-existing titles - $types = array_intersect( $types, array( 'create', 'upload' ) ); - } - return $types; - } - /** * Returns the raw sort key to be used for categories, with the specified * prefix. This will be fed to Collation::getSortKey() to get a -- 2.20.1