X-Git-Url: https://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2FTitle.php;h=8a9d3eee6a4ea6f64f0e3aa2852f235bc321753e;hb=3518d9bb74a5cf2962712d6f8ecccbf2a5cbd0fa;hp=bd3d7d583172fed8aa2a36827daac33671a274f0;hpb=e087a0e57e7f941d75816c1f1e05048d77910388;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/Title.php b/includes/Title.php index bd3d7d5831..8a9d3eee6a 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -40,13 +40,14 @@ class Title { * Please use the accessor functions */ - /**#@+ + /**#@+ * @private */ var $mTextform; # Text form (spaces not underscores) of the main part var $mUrlform; # URL-encoded form of the main part var $mDbkeyform; # Main part with underscores + var $mUserCaseDBKey; # DB key with the initial letter in the case specified by the user var $mNamespace; # Namespace index, i.e. one of the NS_xxxx constants var $mInterwiki; # Interwiki prefix (or null string) var $mFragment; # Title fragment (i.e. the bit after the #) @@ -206,6 +207,9 @@ class Title { * Make an array of titles from an array of IDs */ public static function newFromIDs( $ids ) { + if ( !count( $ids ) ) { + return array(); + } $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( 'page', array( 'page_namespace', 'page_title' ), 'page_id IN (' . $dbr->makeList( $ids ) . ')', __METHOD__ ); @@ -232,7 +236,7 @@ class Title { $t = new Title(); $t->mInterwiki = ''; $t->mFragment = ''; - $t->mNamespace = intval( $ns ); + $t->mNamespace = $ns = intval( $ns ); $t->mDbkeyform = str_replace( ' ', '_', $title ); $t->mArticleID = ( $ns >= 0 ) ? -1 : 0; $t->mUrlform = wfUrlencode( $t->mDbkeyform ); @@ -264,36 +268,42 @@ class Title { * @return Title the new object */ public static function newMainPage() { - return Title::newFromText( wfMsgForContent( 'mainpage' ) ); + $title = Title::newFromText( wfMsgForContent( 'mainpage' ) ); + // Don't give fatal errors if the message is broken + if ( !$title ) { + $title = Title::newFromText( 'Main Page' ); + } + return $title; } /** - * Create a new Title for a redirect - * @param string $text the redirect title text - * @return Title the new object, or NULL if the text is not a - * valid redirect + * Extract a redirect destination from a string and return the + * Title, or null if the text doesn't contain a valid redirect + * + * @param string $text Text with possible redirect + * @return Title */ public static function newFromRedirect( $text ) { - $mwRedir = MagicWord::get( 'redirect' ); - $rt = NULL; - if ( $mwRedir->matchStart( $text ) ) { + $redir = MagicWord::get( 'redirect' ); + if( $redir->matchStart( $text ) ) { + // Extract the first link and see if it's usable $m = array(); - if ( preg_match( '/\[{2}(.*?)(?:\||\]{2})/', $text, $m ) ) { - # categories are escaped using : for example one can enter: - # #REDIRECT [[:Category:Music]]. Need to remove it. - if ( substr($m[1],0,1) == ':') { - # We don't want to keep the ':' - $m[1] = substr( $m[1], 1 ); - } - - $rt = Title::newFromText( $m[1] ); - # Disallow redirects to Special:Userlogout - if ( !is_null($rt) && $rt->isSpecial( 'Userlogout' ) ) { - $rt = NULL; + if( preg_match( '!\[{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; + // don't interpret + as " " most of the time! + // It might be safe to just use rawurldecode instead, though. + $m[1] = urldecode( ltrim( $m[1], ':' ) ); } + $title = Title::newFromText( $m[1] ); + // Redirects to Special:Userlogout are not permitted + if( $title instanceof Title && !$title->isSpecial( 'Userlogout' ) ) + return $title; } } - return $rt; + return null; } #---------------------------------------------------------------------------- @@ -497,60 +507,6 @@ class Title { return (bool)(Title::$interwikiCache[$k]->iw_trans); } - /** - * Update the page_touched field for an array of title objects - * @todo Inefficient unless the IDs are already loaded into the - * link cache - * @param array $titles an array of Title objects to be touched - * @param string $timestamp the timestamp to use instead of the - * default current time - * @static - * @access public - */ - function touchArray( $titles, $timestamp = '' ) { - - if ( count( $titles ) == 0 ) { - return; - } - $dbw = wfGetDB( DB_MASTER ); - if ( $timestamp == '' ) { - $timestamp = $dbw->timestamp(); - } - /* - $page = $dbw->tableName( 'page' ); - $sql = "UPDATE $page SET page_touched='{$timestamp}' WHERE page_id IN ("; - $first = true; - - foreach ( $titles as $title ) { - if ( $wgUseFileCache ) { - $cm = new HTMLFileCache($title); - @unlink($cm->fileCacheName()); - } - - if ( ! $first ) { - $sql .= ','; - } - $first = false; - $sql .= $title->getArticleID(); - } - $sql .= ')'; - if ( ! $first ) { - $dbw->query( $sql, 'Title::touchArray' ); - } - */ - // hack hack hack -- brion 2005-07-11. this was unfriendly to db. - // do them in small chunks: - $fname = 'Title::touchArray'; - foreach( $titles as $title ) { - $dbw->update( 'page', - array( 'page_touched' => $timestamp ), - array( - 'page_namespace' => $title->getNamespace(), - 'page_title' => $title->getDBkey() ), - $fname ); - } - } - /** * Escape a text fragment, say from a link, for a URL */ @@ -609,6 +565,12 @@ class Title { } return $wgContLang->getNsText( $this->mNamespace ); } + /** + * Get the DB key with the initial letter case as specified by the user + */ + function getUserCaseDBKey() { + return $this->mUserCaseDBKey; + } /** * Get the namespace text of the subject (rather than talk) page * @return string @@ -716,7 +678,7 @@ class Title { */ public function getBaseText() { global $wgNamespacesWithSubpages; - if( isset( $wgNamespacesWithSubpages[ $this->mNamespace ] ) && $wgNamespacesWithSubpages[ $this->mNamespace ] ) { + if( !empty( $wgNamespacesWithSubpages[$this->mNamespace] ) ) { $parts = explode( '/', $this->getText() ); # Don't discard the real title if there's no subpage involved if( count( $parts ) > 1 ) @@ -840,16 +802,15 @@ class Title { } else { $dbkey = wfUrlencode( $this->getPrefixedDBkey() ); if ( $query == '' ) { - if($variant!=false && $wgContLang->hasVariants()){ - if($wgVariantArticlePath==false) { + if( $variant != false && $wgContLang->hasVariants() ) { + if( $wgVariantArticlePath == false ) { $variantArticlePath = "$wgScript?title=$1&variant=$2"; // default } else { $variantArticlePath = $wgVariantArticlePath; } $url = str_replace( '$2', urlencode( $variant ), $variantArticlePath ); $url = str_replace( '$1', $dbkey, $url ); - } - else { + } else { $url = str_replace( '$1', $dbkey, $wgArticlePath ); } } else { @@ -977,7 +938,7 @@ class Title { /** * Does the title correspond to a protected article? * @param string $what the action the page is protected from, - * by default checks move and edit + * by default checks move and edit * @return boolean */ public function isProtected( $action = '' ) { @@ -1026,7 +987,7 @@ class Title { return $this->mWatched; } - /** + /** * Can $wgUser perform $action on this page? * This skips potentially expensive cascading permission checks. * @@ -1042,55 +1003,170 @@ class Title { return $this->userCan( $action, false ); } - /** + /** + * Determines if $wgUser is unable to edit this page because it has been protected + * by $wgNamespaceProtection. + * + * @return boolean + */ + public function isNamespaceProtected() { + global $wgNamespaceProtection, $wgUser; + if( isset( $wgNamespaceProtection[ $this->mNamespace ] ) ) { + foreach( (array)$wgNamespaceProtection[ $this->mNamespace ] as $right ) { + if( $right != '' && !$wgUser->isAllowed( $right ) ) + return true; + } + } + return false; + } + + /** * Can $wgUser perform $action on this page? * @param string $action action that permission needs to be checked for * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary queries. * @return boolean */ public function userCan( $action, $doExpensiveQueries = true ) { - $fname = 'Title::userCan'; - wfProfileIn( $fname ); + global $wgUser; + return ( $this->getUserPermissionsErrorsInternal( $action, $wgUser, $doExpensiveQueries ) === array()); + } + + /** + * Can $user perform $action on this page? + * + * FIXME: This *does not* check throttles (User::pingLimiter()). + * + * @param string $action action that permission needs to be checked for + * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary queries. + * @return array Array of arrays of the arguments to wfMsg to explain permissions problems. + */ + public function getUserPermissionsErrors( $action, $user, $doExpensiveQueries = true ) { + $errors = $this->getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries ); - global $wgUser, $wgNamespaceProtection; + global $wgContLang; + global $wgLang; + global $wgEmailConfirmToEdit, $wgUser; - $result = null; - wfRunHooks( 'userCan', array( &$this, &$wgUser, $action, &$result ) ); - if ( $result !== null ) { - wfProfileOut( $fname ); - return $result; + if ( $wgEmailConfirmToEdit && !$user->isEmailConfirmed() ) { + $errors[] = array( 'confirmedittext' ); + } + + if ( $user->isBlockedFrom( $this ) ) { + $block = $user->mBlock; + + // This is from OutputPage::blockedPage + // Copied at r23888 by werdna + + $id = $user->blockedBy(); + $reason = $user->blockedFor(); + if( $reason == '' ) { + $reason = wfMsg( 'blockednoreason' ); + } + $ip = wfGetIP(); + + if ( is_numeric( $id ) ) { + $name = User::whoIs( $id ); + } else { + $name = $id; + } + + $link = '[[' . $wgContLang->getNsText( NS_USER ) . ":{$name}|{$name}]]"; + $blockid = $block->mId; + $blockExpiry = $user->mBlock->mExpiry; + $blockTimestamp = $wgLang->timeanddate( wfTimestamp( TS_MW, $user->mBlock->mTimestamp ), true ); + + if ( $blockExpiry == 'infinity' ) { + // Entry in database (table ipblocks) is 'infinity' but 'ipboptions' uses 'infinite' or 'indefinite' + $scBlockExpiryOptions = wfMsg( 'ipboptions' ); + + foreach ( explode( ',', $scBlockExpiryOptions ) as $option ) { + if ( strpos( $option, ':' ) == false ) + continue; + + list ($show, $value) = explode( ":", $option ); + + if ( $value == 'infinite' || $value == 'indefinite' ) { + $blockExpiry = $show; + break; + } + } + } else { + $blockExpiry = $wgLang->timeanddate( wfTimestamp( TS_MW, $blockExpiry ), true ); + } + + $intended = $user->mBlock->mAddress; + + $errors[] = array ( ($block->mAuto ? 'autoblockedtext' : 'blockedtext'), $link, $reason, $ip, $name, $blockid, $blockExpiry, $intended, $blockTimestamp ); + } + + return $errors; + } + + /** + * Can $user perform $action on this page? This is an internal function, + * which checks ONLY that previously checked by userCan (i.e. it leaves out + * checks on wfReadOnly() and blocks) + * + * @param string $action action that permission needs to be checked for + * @param bool $doExpensiveQueries Set this to false to avoid doing unnecessary queries. + * @return array Array of arrays of the arguments to wfMsg to explain permissions problems. + */ + private function getUserPermissionsErrorsInternal( $action, $user, $doExpensiveQueries = true ) { + wfProfileIn( __METHOD__ ); + + $errors = array(); + + // Use getUserPermissionsErrors instead + if ( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) { + return $result ? array() : array( array( 'badaccess-group0' ) ); + } + + if (!wfRunHooks( 'getUserPermissionsErrors', array( &$this, &$user, $action, &$result ) ) ) { + if ($result != array() && is_array($result) && !is_array($result[0])) + $errors[] = $result; # A single array representing an error + else if (is_array($result) && is_array($result[0])) + $errors = array_merge( $errors, $result ); # A nested array representing multiple errors + else if ($result != '' && $result != null && $result !== true && $result !== false) + $errors[] = array($result); # A string representing a message-id + else if ($result === false ) + $errors[] = array('badaccess-group0'); # a generic "We don't want them to do that" + } + if ($doExpensiveQueries && !wfRunHooks( 'getUserPermissionsErrorsExpensive', array( &$this, &$user, $action, &$result ) ) ) { + if ($result != array() && is_array($result) && !is_array($result[0])) + $errors[] = $result; # A single array representing an error + else if (is_array($result) && is_array($result[0])) + $errors = array_merge( $errors, $result ); # A nested array representing multiple errors + else if ($result != '' && $result != null && $result !== true && $result !== false) + $errors[] = array($result); # A string representing a message-id + else if ($result === false ) + $errors[] = array('badaccess-group0'); # a generic "We don't want them to do that" } if( NS_SPECIAL == $this->mNamespace ) { - wfProfileOut( $fname ); - return false; + $errors[] = array('ns-specialprotected'); } - if ( array_key_exists( $this->mNamespace, $wgNamespaceProtection ) ) { - $nsProt = $wgNamespaceProtection[ $this->mNamespace ]; - if ( !is_array($nsProt) ) $nsProt = array($nsProt); - foreach( $nsProt as $right ) { - if( '' != $right && !$wgUser->isAllowed( $right ) ) { - wfProfileOut( $fname ); - return false; - } - } + if ( $this->isNamespaceProtected() ) { + $ns = $this->getNamespace() == NS_MAIN + ? wfMsg( 'nstab-main' ) + : $this->getNsText(); + $errors[] = (NS_MEDIAWIKI == $this->mNamespace + ? array('protectedinterface') + : array( 'namespaceprotected', $ns ) ); } if( $this->mDbkeyform == '_' ) { # FIXME: Is this necessary? Shouldn't be allowed anyway... - wfProfileOut( $fname ); - return false; + $errors[] = array('badaccess-group0'); } # protect css/js subpages of user pages # XXX: this might be better using restrictions # XXX: Find a way to work around the php bug that prevents using $this->userCanEditCssJsSubpage() from working if( $this->isCssJsSubpage() - && !$wgUser->isAllowed('editinterface') - && !preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) ) { - wfProfileOut( $fname ); - return false; + && !$user->isAllowed('editusercssjs') + && !preg_match('/^'.preg_quote($user->getName(), '/').'\//', $this->mTextform) ) { + $errors[] = array('customcssjsprotected'); } if ( $doExpensiveQueries && !$this->isCssJsSubpage() ) { @@ -1106,9 +1182,11 @@ class Title { if( $cascadingSources > 0 && isset($restrictions[$action]) ) { foreach( $restrictions[$action] as $right ) { $right = ( $right == 'sysop' ) ? 'protect' : $right; - if( '' != $right && !$wgUser->isAllowed( $right ) ) { - wfProfileOut( $fname ); - return false; + if( '' != $right && !$user->isAllowed( $right ) ) { + $pages = ''; + foreach( $cascadingSources as $page ) + $pages .= '* [[:' . $page->getPrefixedText() . "]]\n"; + $errors[] = array( 'cascadeprotected', count( $cascadingSources ), $pages ); } } } @@ -1119,30 +1197,146 @@ class Title { if ( $right == 'sysop' ) { $right = 'protect'; } - if( '' != $right && !$wgUser->isAllowed( $right ) ) { - wfProfileOut( $fname ); - return false; + if( '' != $right && !$user->isAllowed( $right ) ) { + $errors[] = array( 'protectedpagetext', $right ); } } - if( $action == 'move' && - !( $this->isMovable() && $wgUser->isAllowed( 'move' ) ) ) { - wfProfileOut( $fname ); - return false; + if ($action == 'protect') { + if ($this->getUserPermissionsErrors('edit', $user) != array()) { + $errors[] = array( 'protect-cantedit' ); // If they can't edit, they shouldn't protect. + } } - if( $action == 'create' ) { - if( ( $this->isTalkPage() && !$wgUser->isAllowed( 'createtalk' ) ) || - ( !$this->isTalkPage() && !$wgUser->isAllowed( 'createpage' ) ) ) { - wfProfileOut( $fname ); - return false; + if ($action == 'create') { + $title_protection = $this->getTitleProtection(); + + if (is_array($title_protection)) { + extract($title_protection); + + if ($pt_create_perm == 'sysop') + $pt_create_perm = 'protect'; + + if ($pt_create_perm == '' || !$user->isAllowed($pt_create_perm)) { + $errors[] = array ( 'titleprotected', User::whoIs($pt_user), $pt_reason ); + } + } + + if( ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) || + ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) ) { + $errors[] = $user->isAnon() ? array ('nocreatetext') : array ('nocreate-loggedin'); } + } elseif( $action == 'move' && !( $this->isMovable() && $user->isAllowed( 'move' ) ) ) { + $errors[] = $user->isAnon() ? array ( 'movenologintext' ) : array ('movenotallowed'); + } elseif ( !$user->isAllowed( $action ) ) { + $return = null; + $groups = array(); + global $wgGroupPermissions; + foreach( $wgGroupPermissions as $key => $value ) { + if( isset( $value[$action] ) && $value[$action] == true ) { + $groupName = User::getGroupName( $key ); + $groupPage = User::getGroupPage( $key ); + if( $groupPage ) { + $groups[] = '[['.$groupPage->getPrefixedText().'|'.$groupName.']]'; + } else { + $groups[] = $groupName; + } + } + } + $n = count( $groups ); + $groups = implode( ', ', $groups ); + switch( $n ) { + case 0: + case 1: + case 2: + $return = array( "badaccess-group$n", $groups ); + break; + default: + $return = array( 'badaccess-groups', $groups ); + } + $errors[] = $return; + } + + wfProfileOut( __METHOD__ ); + return $errors; + } + + /** + * Is this title subject to title protection? + * @return mixed An associative array representing any existent title + * protection, or false if there's none. + */ + private function getTitleProtection() { + // Can't protect pages in special namespaces + if ( $this->getNamespace() < 0 ) { + return false; + } + + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( 'protected_titles', '*', + array ('pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey()) ); + + if ($row = $dbr->fetchRow( $res )) { + return $row; + } else { + return false; + } + } + + public function updateTitleProtection( $create_perm, $reason, $expiry ) { + global $wgGroupPermissions,$wgUser,$wgContLang; + + if ($create_perm == implode(',',$this->getRestrictions('create')) + && $expiry == $this->mRestrictionsExpiry) { + // No change + return true; + } + + list ($namespace, $title) = array( $this->getNamespace(), $this->getDBkey() ); + + $dbw = wfGetDB( DB_MASTER ); + + $encodedExpiry = Block::encodeExpiry($expiry, $dbw ); + + $expiry_description = ''; + if ( $encodedExpiry != 'infinity' ) { + $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry ) ).')'; + } + + # Update protection table + if ($create_perm != '' ) { + $dbw->replace( 'protected_titles', array(array('pt_namespace', 'pt_title')), + array( 'pt_namespace' => $namespace, 'pt_title' => $title + , 'pt_create_perm' => $create_perm + , 'pt_timestamp' => Block::encodeExpiry(wfTimestampNow(), $dbw) + , 'pt_expiry' => $encodedExpiry + , 'pt_user' => $wgUser->getId(), 'pt_reason' => $reason ), __METHOD__ ); + } else { + $dbw->delete( 'protected_titles', array( 'pt_namespace' => $namespace, + 'pt_title' => $title ), __METHOD__ ); + } + # Update the protection log + $log = new LogPage( 'protect' ); + + if( $create_perm ) { + $log->addEntry( $this->mRestrictions['create'] ? 'modify' : 'protect', $this, trim( $reason . " [create=$create_perm] $expiry_description" ) ); + } else { + $log->addEntry( 'unprotect', $this, $reason ); } - wfProfileOut( $fname ); 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__ ); + } + /** * Can $wgUser edit this page? * @return boolean @@ -1187,15 +1381,19 @@ class Title { * @todo fold these checks into userCan() */ public function userCanRead() { - global $wgUser; - + global $wgUser, $wgGroupPermissions; + + # Shortcut for public wikis, allows skipping quite a bit of code path + if ($wgGroupPermissions['*']['read']) + return true; + $result = null; wfRunHooks( 'userCan', array( &$this, &$wgUser, 'read', &$result ) ); if ( $result !== null ) { return $result; } - if( $wgUser->isAllowed('read') ) { + if( $wgUser->isAllowed( 'read' ) ) { return true; } else { global $wgWhitelistRead; @@ -1208,18 +1406,46 @@ class Title { return true; } - /** some pages are explicitly allowed */ + /** + * Bail out if there isn't whitelist + */ + if( !is_array($wgWhitelistRead) ) { + return false; + } + + /** + * Check for explicit whitelisting + */ $name = $this->getPrefixedText(); - if( $wgWhitelistRead && in_array( $name, $wgWhitelistRead ) ) { + if( in_array( $name, $wgWhitelistRead, true ) ) return true; + + /** + * Old settings might have the title prefixed with + * a colon for main-namespace pages + */ + if( $this->getNamespace() == NS_MAIN ) { + if( in_array( ':' . $name, $wgWhitelistRead ) ) + return true; } + + /** + * If it's a special page, ditch the subpage bit + * and check again + */ + if( $this->getNamespace() == NS_SPECIAL ) { + $name = $this->getDBkey(); + list( $name, /* $subpage */) = SpecialPage::resolveAliasWithSubpage( $name ); + if ( $name === false ) { + # Invalid special page, but we show standard login required message + return false; + } - # Compatibility with old settings - if( $wgWhitelistRead && $this->getNamespace() == NS_MAIN ) { - if( in_array( ':' . $name, $wgWhitelistRead ) ) { + $pure = SpecialPage::getTitleFor( $name )->getPrefixedText(); + if( in_array( $pure, $wgWhitelistRead, true ) ) return true; - } } + } return false; } @@ -1245,6 +1471,17 @@ class Title { return false; } } + + /** + * Could this page contain custom CSS or JavaScript, based + * on the title? + * + * @return bool + */ + public function isCssOrJsPage() { + return $this->mNamespace == NS_MEDIAWIKI + && preg_match( '!\.(?:css|js)$!u', $this->mTextform ) > 0; + } /** * Is this a .css or .js subpage of a user page? @@ -1296,7 +1533,7 @@ class Title { */ public function userCanEditCssJsSubpage() { global $wgUser; - return ( $wgUser->isAllowed('editinterface') or preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) ); + return ( $wgUser->isAllowed('editusercssjs') or preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) ); } /** @@ -1305,7 +1542,7 @@ class Title { * @return bool If the page is subject to cascading restrictions. */ public function isCascadeProtected() { - list( $sources, $restrictions ) = $this->getCascadeProtectionSources( false ); + list( $sources, /* $restrictions */ ) = $this->getCascadeProtectionSources( false ); return ( $sources > 0 ); } @@ -1481,12 +1718,32 @@ class Title { public function loadRestrictions( $oldFashionedRestrictions = NULL ) { if( !$this->mRestrictionsLoaded ) { - $dbr = wfGetDB( DB_SLAVE ); + if ($this->exists()) { + $dbr = wfGetDB( DB_SLAVE ); + + $res = $dbr->select( 'page_restrictions', '*', + array ( 'pr_page' => $this->getArticleId() ), __METHOD__ ); + + $this->loadRestrictionsFromRow( $res, $oldFashionedRestrictions ); + } else { + $title_protection = $this->getTitleProtection(); - $res = $dbr->select( 'page_restrictions', '*', - array ( 'pr_page' => $this->getArticleId() ), __METHOD__ ); + if (is_array($title_protection)) { + extract($title_protection); - $this->loadRestrictionsFromRow( $res, $oldFashionedRestrictions ); + $now = wfTimestampNow(); + $expiry = Block::decodeExpiry($pt_expiry); + + if (!$expiry || $expiry > $now) { + // Apply the restrictions + $this->mRestrictionsExpiry = $expiry; + $this->mRestrictions['create'] = explode(',', trim($pt_create_perm) ); + } else { // Get rid of the old restrictions + Title::purgeExpiredRestrictions(); + } + } + $this->mRestrictionsLoaded = true; + } } } @@ -1498,6 +1755,10 @@ class Title { $dbw->delete( 'page_restrictions', array( 'pr_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), __METHOD__ ); + + $dbw->delete( 'protected_titles', + array( 'pt_expiry < ' . $dbw->addQuotes( $dbw->timestamp() ) ), + __METHOD__ ); } /** @@ -1507,16 +1768,12 @@ class Title { * @return array the array of groups allowed to edit this article */ public function getRestrictions( $action ) { - if( $this->exists() ) { - if( !$this->mRestrictionsLoaded ) { - $this->loadRestrictions(); - } - return isset( $this->mRestrictions[$action] ) - ? $this->mRestrictions[$action] - : array(); - } else { - return array(); + if( !$this->mRestrictionsLoaded ) { + $this->loadRestrictions(); } + return isset( $this->mRestrictions[$action] ) + ? $this->mRestrictions[$action] + : array(); } /** @@ -1655,8 +1912,18 @@ class Title { # Initialisation static $rxTc = false; if( !$rxTc ) { - # % is needed as well - $rxTc = '/[^' . Title::legalChars() . ']|%[0-9A-Fa-f]{2}/S'; + # 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'; } $this->mInterwiki = $this->mFragment = ''; @@ -1691,6 +1958,7 @@ class Title { if ( ':' == $dbkey{0} ) { $this->mNamespace = NS_MAIN; $dbkey = substr( $dbkey, 1 ); # remove the colon but continue processing + $dbkey = trim( $dbkey, '_' ); # remove any subsequent whitespace } # Namespace or interwiki prefix @@ -1699,12 +1967,7 @@ class Title { $m = array(); if ( preg_match( "/^(.+?)_*:_*(.*)$/S", $dbkey, $m ) ) { $p = $m[1]; - $lowerNs = $wgContLang->lc( $p ); - if ( $ns = Namespace::getCanonicalIndex( $lowerNs ) ) { - # Canonical namespace - $dbkey = $m[2]; - $this->mNamespace = $ns; - } elseif ( $ns = $wgContLang->getNsIndex( $lowerNs )) { + if ( $ns = $wgContLang->getNsIndex( $p )) { # Ordinary namespace $dbkey = $m[2]; $this->mNamespace = $ns; @@ -1774,7 +2037,9 @@ class Title { strpos( $dbkey, './' ) === 0 || strpos( $dbkey, '../' ) === 0 || strpos( $dbkey, '/./' ) !== false || - strpos( $dbkey, '/../' ) !== false ) ) + strpos( $dbkey, '/../' ) !== false || + substr( $dbkey, -2 ) == '/.' || + substr( $dbkey, -3 ) == '/..' ) ) { return false; } @@ -1807,6 +2072,7 @@ class Title { * Don't force it for interwikis, since the other * site might be case-sensitive. */ + $this->mUserCaseDBKey = $dbkey; if( $wgCapitalLinks && $this->mInterwiki == '') { $dbkey = $wgContLang->ucfirst( $dbkey ); } @@ -1821,7 +2087,14 @@ class Title { $this->mNamespace != NS_MAIN ) { return false; } - + // Allow IPv6 usernames to start with '::' by canonicalizing IPv6 titles. + // IP names are not allowed for accounts, and can only be referring to + // edits from the IP. Given '::' abbreviations and caps/lowercaps, + // there are numerous ways to present the same IP. Having sp:contribs scan + // them all is silly and having some show the edits and others not is + // inconsistent. Same for talk/userpages. Keep them normalized instead. + $dbkey = ($this->mNamespace == NS_USER || $this->mNamespace == NS_USER_TALK) ? + IP::sanitizeIP( $dbkey ) : $dbkey; // Any remaining initial :s are illegal. if ( $dbkey !== '' && ':' == $dbkey{0} ) { return false; @@ -1891,7 +2164,7 @@ class Title { array( "{$prefix}_from=page_id", "{$prefix}_namespace" => $this->getNamespace(), - "{$prefix}_title" => $this->getDbKey() ), + "{$prefix}_title" => $this->getDBkey() ), 'Title::getLinksTo', $options ); @@ -1925,10 +2198,16 @@ class Title { /** * Get an array of Title objects referring to non-existent articles linked from this page * + * @todo check if needed (used only in SpecialBrokenRedirects.php, and should use redirect table in this case) * @param string $options may be FOR UPDATE * @return array the Title objects */ public function getBrokenLinksFrom( $options = '' ) { + if ( $this->getArticleId() == 0 ) { + # All links from article ID 0 are false positives + return array(); + } + if ( $options ) { $db = wfGetDB( DB_MASTER ); } else { @@ -2035,10 +2314,20 @@ class Title { return 'badarticleerror'; } - if ( $auth && ( - !$this->userCan( 'edit' ) || !$nt->userCan( 'edit' ) || - !$this->userCan( 'move' ) || !$nt->userCan( 'move' ) ) ) { - return 'protectedpage'; + if ( $auth ) { + global $wgUser; + $errors = array_merge($this->getUserPermissionsErrors('move', $wgUser), + $this->getUserPermissionsErrors('edit', $wgUser), + $nt->getUserPermissionsErrors('move', $wgUser), + $nt->getUserPermissionsErrors('edit', $wgUser)); + if($errors !== array()) + return $errors[0][0]; + } + + global $wgUser; + $err = null; + if( !wfRunHooks( 'AbortMove', array( $this, $nt, $wgUser, &$err ) ) ) { + return 'hookaborted'; } # The move is allowed only if (1) the target doesn't exist, or @@ -2049,6 +2338,11 @@ class Title { if ( ! $this->isValidMoveTarget( $nt ) ) { return 'articleexists'; } + } else { + $tp = $nt->getTitleProtection(); + if ( $tp and !$wgUser->isAllowed( $tp['pt_create_perm'] ) ) { + return 'cantmove-titleprotected'; + } } return true; } @@ -2058,9 +2352,12 @@ class Title { * @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. * @return mixed true on success, message name on failure */ - public function moveTo( &$nt, $auth = true, $reason = '' ) { + public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) { $err = $this->isValidMoveOperation( $nt, $auth ); if( is_string( $err ) ) { return $err; @@ -2068,21 +2365,34 @@ class Title { $pageid = $this->getArticleID(); if( $nt->exists() ) { - $this->moveOverExistingRedirect( $nt, $reason ); - $pageCountChange = 0; + $this->moveOverExistingRedirect( $nt, $reason, $createRedirect ); + $pageCountChange = ($createRedirect ? 0 : -1); } else { # Target didn't exist, do normal move. - $this->moveToNewTitle( $nt, $reason ); - $pageCountChange = 1; + $this->moveToNewTitle( $nt, $reason, $createRedirect ); + $pageCountChange = ($createRedirect ? 1 : 0); } $redirid = $this->getArticleID(); - # Fixing category links (those without piped 'alternate' names) to be sorted under the new title + // Category memberships include a sort key which may be customized. + // If it's left as the default (the page title), we need to update + // the sort key to match the new title. + // + // Be careful to avoid resetting cl_timestamp, which may disturb + // time-based lists on some sites. + // + // Warning -- if the sort key is *explicitly* set to the old title, + // we can't actually distinguish it from a default here, and it'll + // be set to the new title even though it really shouldn't. + // It'll get corrected on the next edit, but resetting cl_timestamp. $dbw = wfGetDB( DB_MASTER ); - $categorylinks = $dbw->tableName( 'categorylinks' ); - $sql = "UPDATE $categorylinks SET cl_sortkey=" . $dbw->addQuotes( $nt->getPrefixedText() ) . - " WHERE cl_from=" . $dbw->addQuotes( $pageid ) . - " AND cl_sortkey=" . $dbw->addQuotes( $this->getPrefixedText() ); - $dbw->query( $sql, 'SpecialMovepage::doSubmit' ); + $dbw->update( 'categorylinks', + array( + 'cl_sortkey' => $nt->getPrefixedText(), + 'cl_timestamp=cl_timestamp' ), + array( + 'cl_from' => $pageid, + 'cl_sortkey' => $this->getPrefixedText() ), + __METHOD__ ); # Update watchlists @@ -2119,6 +2429,14 @@ class Title { } if( $u ) $u->doUpdate(); + # Update message cache for interface messages + if( $nt->getNamespace() == NS_MEDIAWIKI ) { + global $wgMessageCache; + $oldarticle = new Article( $this ); + $wgMessageCache->replace( $this->getDBkey(), $oldarticle->getContent() ); + $newarticle = new Article( $nt ); + $wgMessageCache->replace( $nt->getDBkey(), $newarticle->getContent() ); + } global $wgUser; wfRunHooks( 'TitleMoveComplete', array( &$this, &$nt, &$wgUser, $pageid, $redirid ) ); @@ -2131,9 +2449,12 @@ class Title { * * @param Title &$nt the page to move to, which should currently * be a redirect + * @param string $reason The reason for the move + * @param bool $createRedirect Whether to leave a redirect at the old title. + * Ignored if the user doesn't have the suppressredirect right */ - private function moveOverExistingRedirect( &$nt, $reason = '' ) { - global $wgUseSquid; + private function moveOverExistingRedirect( &$nt, $reason = '', $createRedirect = true ) { + global $wgUseSquid, $wgUser; $fname = 'Title::moveOverExistingRedirect'; $comment = wfMsgForContent( '1movedto2_redir', $this->getPrefixedText(), $nt->getPrefixedText() ); @@ -2145,13 +2466,25 @@ class Title { $newid = $nt->getArticleID(); $oldid = $this->getArticleID(); $dbw = wfGetDB( DB_MASTER ); - $linkCache =& LinkCache::singleton(); # Delete the old redirect. We don't save it to history since # by definition if we've got here it's rather uninteresting. # We have to remove it so that the next step doesn't trigger # a conflict on the unique namespace+title index... $dbw->delete( 'page', array( 'page_id' => $newid ), $fname ); + if ( !$dbw->cascadingDeletes() ) { + $dbw->delete( 'revision', array( 'rev_page' => $newid ), __METHOD__ ); + global $wgUseTrackbacks; + if ($wgUseTrackbacks) + $dbw->delete( 'trackbacks', array( 'tb_page' => $newid ), __METHOD__ ); + $dbw->delete( 'pagelinks', array( 'pl_from' => $newid ), __METHOD__ ); + $dbw->delete( 'imagelinks', array( 'il_from' => $newid ), __METHOD__ ); + $dbw->delete( 'categorylinks', array( 'cl_from' => $newid ), __METHOD__ ); + $dbw->delete( 'templatelinks', array( 'tl_from' => $newid ), __METHOD__ ); + $dbw->delete( 'externallinks', array( 'el_from' => $newid ), __METHOD__ ); + $dbw->delete( 'langlinks', array( 'll_from' => $newid ), __METHOD__ ); + $dbw->delete( 'redirect', array( 'rd_from' => $newid ), __METHOD__ ); + } # Save a null revision in the page's history notifying of the move $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true ); @@ -2168,35 +2501,39 @@ class Title { /* WHERE */ array( 'page_id' => $oldid ), $fname ); - $linkCache->clearLink( $nt->getPrefixedDBkey() ); + $nt->resetArticleID( $oldid ); # Recreate the redirect, this time in the other direction. - $mwRedir = MagicWord::get( 'redirect' ); - $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n"; - $redirectArticle = new Article( $this ); - $newid = $redirectArticle->insertOn( $dbw ); - $redirectRevision = new Revision( array( - 'page' => $newid, - 'comment' => $comment, - 'text' => $redirectText ) ); - $redirectRevision->insertOn( $dbw ); - $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 ); - $linkCache->clearLink( $this->getPrefixedDBkey() ); - + if($createRedirect || !$wgUser->isAllowed('suppressredirect')) + { + $mwRedir = MagicWord::get( 'redirect' ); + $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n"; + $redirectArticle = new Article( $this ); + $newid = $redirectArticle->insertOn( $dbw ); + $redirectRevision = new Revision( array( + 'page' => $newid, + 'comment' => $comment, + 'text' => $redirectText ) ); + $redirectRevision->insertOn( $dbw ); + $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 ); + + # Now, we record the link from the redirect to the new title. + # It should have no other outgoing links... + $dbw->delete( 'pagelinks', array( 'pl_from' => $newid ), $fname ); + $dbw->insert( 'pagelinks', + array( + 'pl_from' => $newid, + 'pl_namespace' => $nt->getNamespace(), + 'pl_title' => $nt->getDBkey() ), + $fname ); + } else { + $this->resetArticleID( 0 ); + } + # Log the move $log = new LogPage( 'move' ); $log->addEntry( 'move_redir', $this, $reason, array( 1 => $nt->getPrefixedText() ) ); - # Now, we record the link from the redirect to the new title. - # It should have no other outgoing links... - $dbw->delete( 'pagelinks', array( 'pl_from' => $newid ), $fname ); - $dbw->insert( 'pagelinks', - array( - 'pl_from' => $newid, - 'pl_namespace' => $nt->getNamespace(), - 'pl_title' => $nt->getDbKey() ), - $fname ); - # Purge squid if ( $wgUseSquid ) { $urls = array_merge( $nt->getSquidURLs(), $this->getSquidURLs() ); @@ -2208,9 +2545,12 @@ class Title { /** * Move page to non-existing title. * @param Title &$nt the new Title + * @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 */ - private function moveToNewTitle( &$nt, $reason = '' ) { - global $wgUseSquid; + private function moveToNewTitle( &$nt, $reason = '', $createRedirect = true ) { + global $wgUseSquid, $wgUser; $fname = 'MovePageForm::moveToNewTitle'; $comment = wfMsgForContent( '1movedto2', $this->getPrefixedText(), $nt->getPrefixedText() ); if ( $reason ) { @@ -2221,13 +2561,12 @@ class Title { $oldid = $this->getArticleID(); $dbw = wfGetDB( DB_MASTER ); $now = $dbw->timestamp(); - $linkCache =& LinkCache::singleton(); # Save a null revision in the page's history notifying of the move $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true ); $nullRevId = $nullRevision->insertOn( $dbw ); - # Rename cur entry + # Rename page entry $dbw->update( 'page', /* SET */ array( 'page_touched' => $now, @@ -2238,21 +2577,32 @@ class Title { /* WHERE */ array( 'page_id' => $oldid ), $fname ); + $nt->resetArticleID( $oldid ); - $linkCache->clearLink( $nt->getPrefixedDBkey() ); - - # Insert redirect - $mwRedir = MagicWord::get( 'redirect' ); - $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n"; - $redirectArticle = new Article( $this ); - $newid = $redirectArticle->insertOn( $dbw ); - $redirectRevision = new Revision( array( - 'page' => $newid, - 'comment' => $comment, - 'text' => $redirectText ) ); - $redirectRevision->insertOn( $dbw ); - $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 ); - $linkCache->clearLink( $this->getPrefixedDBkey() ); + if($createRedirect || !$wgUser->isAllowed('suppressredirect')) + { + # Insert redirect + $mwRedir = MagicWord::get( 'redirect' ); + $redirectText = $mwRedir->getSynonym( 0 ) . ' [[' . $nt->getPrefixedText() . "]]\n"; + $redirectArticle = new Article( $this ); + $newid = $redirectArticle->insertOn( $dbw ); + $redirectRevision = new Revision( array( + 'page' => $newid, + 'comment' => $comment, + 'text' => $redirectText ) ); + $redirectRevision->insertOn( $dbw ); + $redirectArticle->updateRevisionOn( $dbw, $redirectRevision, 0 ); + + # Record the just-created redirect's linking to the page + $dbw->insert( 'pagelinks', + array( + 'pl_from' => $newid, + 'pl_namespace' => $nt->getNamespace(), + 'pl_title' => $nt->getDBkey() ), + $fname ); + } else { + $this->resetArticleID( 0 ); + } # Log the move $log = new LogPage( 'move' ); @@ -2261,14 +2611,6 @@ class Title { # Purge caches as per article creation Article::onArticleCreate( $nt ); - # Record the just-created redirect's linking to the page - $dbw->insert( 'pagelinks', - array( - 'pl_from' => $newid, - 'pl_namespace' => $nt->getNamespace(), - 'pl_title' => $nt->getDBkey() ), - $fname ); - # Purge old title from squid # The new title, and links to the new title, are purged in Article::onArticleCreate() $this->purgeSquid(); @@ -2328,6 +2670,16 @@ class Title { # Return true if there was no history return $row === false; } + + /** + * Can this title be added to a user's watchlist? + * + * @return bool + */ + public function isWatchable() { + return !$this->isExternal() + && Namespace::isWatchable( $this->getNamespace() ); + } /** * Get categories to which this Title belongs and return an array of @@ -2357,7 +2709,7 @@ class Title { $data[$wgContLang->getNSText ( NS_CATEGORY ).':'.$x->cl_to] = $this->getFullText(); $dbr->freeResult ( $res ) ; } else { - $data = ''; + $data = array(); } return $data; } @@ -2450,7 +2802,16 @@ class Title { // Note: === is necessary for proper matching of number-like titles. return $this->getInterwiki() === $title->getInterwiki() && $this->getNamespace() == $title->getNamespace() - && $this->getDbkey() === $title->getDbkey(); + && $this->getDBkey() === $title->getDBkey(); + } + + /** + * Return a string representation of this title + * + * @return string + */ + public function __toString() { + return $this->getPrefixedText(); } /** @@ -2462,14 +2823,21 @@ class Title { } /** - * Should a link should be displayed as a known link, just based on its title? + * Do we know that this title definitely exists, or should we otherwise + * consider that it exists? * - * Currently, a self-link with a fragment and special pages are in - * this category. Special pages never exist in the database. + * @return bool */ public function isAlwaysKnown() { - return $this->isExternal() || ( 0 == $this->mNamespace && "" == $this->mDbkeyform ) - || NS_SPECIAL == $this->mNamespace; + // If the page is form Mediawiki:message/lang, calling wfMsgWeirdKey causes + // the full l10n of that language to be loaded. That takes much memory and + // isn't needed. So we strip the language part away. + // Also, extension messages which are not loaded, are shown as red, because + // we don't call MessageCache::loadAllMessages. + list( $basename, /* rest */ ) = explode( '/', $this->mDbkeyform, 2 ); + return $this->isExternal() + || ( $this->mNamespace == NS_MAIN && $this->mDbkeyform == '' ) + || ( $this->mNamespace == NS_MEDIAWIKI && wfMsgWeirdKey( $basename ) ); } /** @@ -2501,18 +2869,6 @@ class Title { return $touched; } - /** - * Get a cached value from a global cache that is invalidated when this page changes - * @param string $key the key - * @param callback $callback A callback function which generates the value on cache miss - * - * @deprecated use DependencyWrapper - */ - function getRelatedCache( $memc, $key, $expiry, $callback, $params = array() ) { - return DependencyWrapper::getValueFromCache( $memc, $key, $expiry, $callback, - $params, new TitleDependency( $this ) ); - } - public function trackbackURL() { global $wgTitle, $wgScriptPath, $wgServer; @@ -2620,5 +2976,3 @@ class Title { } } - -?>