X-Git-Url: http://git.heureux-cyclage.org/?a=blobdiff_plain;f=includes%2FTitle.php;h=bf02a6f3596dfbad8ac61b2f795c35056e61b6ad;hb=54824a674497758605b8ff86880943741c350913;hp=7098510ab6cace51d60c33da7e878f4957daa892;hpb=62d63aefd425de440c008f89bb9aecd6604df9ee;p=lhc%2Fweb%2Fwiklou.git diff --git a/includes/Title.php b/includes/Title.php index 7098510ab6..bf02a6f359 100644 --- a/includes/Title.php +++ b/includes/Title.php @@ -69,6 +69,7 @@ class Title { var $mLength = -1; ///< The page length, 0 for special pages 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 //@} @@ -151,6 +152,14 @@ class Title { } /** + * THIS IS NOT THE FUNCTION YOU WANT. Use Title::newFromText(). + * + * Example of wrong and broken code: + * $title = Title::newFromURL( $wgRequest->getVal( 'title' ) ); + * + * Example of right code: + * $title = Title::newFromText( $wgRequest->getVal( 'title' ) ); + * * Create a new Title from URL-encoded text. Ensures that * the given title's length does not exceed the maximum. * @param $url \type{\string} the title, as might be taken from a URL @@ -178,20 +187,15 @@ class Title { /** * Create a new Title from an article ID * - * @todo This is inefficiently implemented, the page row is requested - * but not used for anything else - * * @param $id \type{\int} the page_id corresponding to the Title to create * @param $flags \type{\int} use GAID_FOR_UPDATE to use master * @return \type{Title} the new object, or NULL on an error */ public static function newFromID( $id, $flags = 0 ) { - $fname = 'Title::newFromID'; $db = ($flags & GAID_FOR_UPDATE) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_SLAVE ); - $row = $db->selectRow( 'page', array( 'page_namespace', 'page_title' ), - array( 'page_id' => $id ), $fname ); - if ( $row !== false ) { - $title = Title::makeTitle( $row->page_namespace, $row->page_title ); + $row = $db->selectRow( 'page', '*', array( 'page_id' => $id ), __METHOD__ ); + if( $row !== false ) { + $title = Title::newFromRow( $row ); } else { $title = NULL; } @@ -338,7 +342,7 @@ class Title { // recursive check to follow double redirects $recurse = $wgMaxRedirects; $titles = array( $title ); - while( --$recurse >= 0 ) { + while( --$recurse > 0 ) { if( $title->isRedirect() ) { $article = new Article( $title, 0 ); $newtitle = $article->getRedirectTarget(); @@ -453,7 +457,7 @@ class Title { return trim( $t ); } - /* + /** * Make a prefixed DB key from a DB key and a namespace index * @param $ns \type{\int} numerical representation of the namespace * @param $title \type{\string} the DB key form the title @@ -471,18 +475,6 @@ class Title { return $name; } - /** - * Returns the URL associated with an interwiki prefix - * @param $key \type{\string} the interwiki prefix (e.g. "MeatBall") - * @return \type{\string} the associated URL, containing "$1", - * which should be replaced by an article title - * @static (arguably) - * @deprecated See Interwiki class - */ - public function getInterwikiLink( $key ) { - return Interwiki::fetch( $key )->getURL( ); - } - /** * Determine whether the object refers to a page within * this project. @@ -858,6 +850,9 @@ class Title { * there's a fragment but the prefixed text is empty, we just return a link * to the fragment. * + * The result obviously should not be URL-escaped, but does need to be + * HTML-escaped if it's being output in HTML. + * * @param $query \type{\arrayof{\string}} An associative array of key => value pairs for the * query string. Keys and values will be escaped. * @param $variant \type{\string} Language variant of URL (for sr, zh..). Ignored @@ -866,19 +861,16 @@ class Title { * @return \type{\string} the URL */ public function getLinkUrl( $query = array(), $variant = false ) { - if( !is_array( $query ) ) { - throw new MWException( 'Title::getLinkUrl passed a non-array for '. - '$query' ); - } + wfProfileIn( __METHOD__ ); if( $this->isExternal() ) { - return $this->getFullURL( $query ); - } elseif( $this->getPrefixedText() === '' - and $this->getFragment() !== '' ) { - return $this->getFragmentForURL(); + $ret = $this->getFullURL( $query ); + } elseif( $this->getPrefixedText() === '' && $this->getFragment() !== '' ) { + $ret = $this->getFragmentForURL(); } else { - return $this->getLocalURL( $query, $variant ) - . $this->getFragmentForURL(); + $ret = $this->getLocalURL( $query, $variant ) . $this->getFragmentForURL(); } + wfProfileOut( __METHOD__ ); + return $ret; } /** @@ -1166,6 +1158,73 @@ class Title { $errors = array(); + // First stop is permissions checks, which fail most often, and which are easiest to test. + if ( $action == 'move' ) { + if( !$user->isAllowed( 'move-rootuserpages' ) + && $this->getNamespace() == NS_USER && !$this->isSubpage() ) + { + // Show user page-specific message only if the user can move other pages + $errors[] = array( 'cant-move-user-page' ); + } + + // Check if user is allowed to move files if it's a file + if( $this->getNamespace() == NS_FILE && !$user->isAllowed( 'movefile' ) ) { + $errors[] = array( 'movenotallowedfile' ); + } + + if( !$user->isAllowed( 'move' ) ) { + // User can't move anything + global $wgGroupPermissions; + $userCanMove = false; + if ( isset( $wgGroupPermissions['user']['move'] ) ) { + $userCanMove = $wgGroupPermissions['user']['move']; + } + $autoconfirmedCanMove = false; + if ( isset( $wgGroupPermissions['autoconfirmed']['move'] ) ) { + $autoconfirmedCanMove = $wgGroupPermissions['autoconfirmed']['move']; + } + if ( $user->isAnon() && ( $userCanMove || $autoconfirmedCanMove ) ) { + // custom message if logged-in users without any special rights can move + $errors[] = array ( 'movenologintext' ); + } else { + $errors[] = array ('movenotallowed'); + } + } + } elseif ( $action == 'create' ) { + if( ( $this->isTalkPage() && !$user->isAllowed( 'createtalk' ) ) || + ( !$this->isTalkPage() && !$user->isAllowed( 'createpage' ) ) ) + { + $errors[] = $user->isAnon() ? array ('nocreatetext') : array ('nocreate-loggedin'); + } + } elseif( $action == 'move-target' ) { + if( !$user->isAllowed( 'move' ) ) { + // User can't move anything + $errors[] = array ('movenotallowed'); + } elseif( !$user->isAllowed( 'move-rootuserpages' ) + && $this->getNamespace() == NS_USER && !$this->isSubpage() ) + { + // Show user page-specific message only if the user can move other pages + $errors[] = array( 'cant-move-to-user-page' ); + } + } elseif( !$user->isAllowed( $action ) ) { + $return = null; + $groups = array_map( array( 'User', 'makeGroupLinkWiki' ), + User::getGroupsWithPermission( $action ) ); + if( $groups ) { + $return = array( 'badaccess-groups', + array( implode( ', ', $groups ), count( $groups ) ) ); + } else { + $return = array( "badaccess-group0" ); + } + $errors[] = $return; + } + + # Short-circuit point + if( $short && count($errors) > 0 ) { + wfProfileOut( __METHOD__ ); + return $errors; + } + // Use getUserPermissionsErrors instead if( !wfRunHooks( 'userCan', array( &$this, &$user, $action, &$result ) ) ) { wfProfileOut( __METHOD__ ); @@ -1204,12 +1263,14 @@ class Title { return $errors; } - // TODO: document + # Only 'createaccount' and 'execute' can be performed on + # special pages, which don't actually exist in the DB. $specialOKActions = array( 'createaccount', 'execute' ); if( NS_SPECIAL == $this->mNamespace && !in_array( $action, $specialOKActions) ) { $errors[] = array('ns-specialprotected'); } + # Check $wgNamespaceProtection for restricted namespaces if( $this->isNamespaceProtected() ) { $ns = $this->getNamespace() == NS_MAIN ? wfMsg( 'nstab-main' ) : $this->getNsText(); @@ -1217,15 +1278,49 @@ class Title { array('protectedinterface') : array( 'namespaceprotected', $ns ); } - # protect css/js subpages of user pages + # 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() && !$user->isAllowed('editusercssjs') + # XXX: Find a way to work around the php bug that prevents using $this->userCanEditCssSubpage() + # and $this->userCanEditJsSubpage() from working + # XXX: right 'editusercssjs' is deprecated, for backward compatibility only + if( $this->isCssSubpage() && ( !$user->isAllowed('editusercssjs') || !$user->isAllowed('editusercss') ) + && $action != 'patrol' + && !preg_match('/^'.preg_quote($user->getName(), '/').'\//', $this->mTextform) ) + { + $errors[] = array('customcssjsprotected'); + } else if( $this->isJsSubpage() && ( !$user->isAllowed('editusercssjs') || !$user->isAllowed('edituserjs') ) + && $action != 'patrol' && !preg_match('/^'.preg_quote($user->getName(), '/').'\//', $this->mTextform) ) { $errors[] = array('customcssjsprotected'); } + # Check against page_restrictions table requirements on this + # page. The user must possess all required rights for this action. + foreach( $this->getRestrictions($action) as $right ) { + // Backwards compatibility, rewrite sysop -> protect + if( $right == 'sysop' ) { + $right = 'protect'; + } + if( '' != $right && !$user->isAllowed( $right ) ) { + // Users with 'editprotected' permission can edit protected pages + if( $action=='edit' && $user->isAllowed( 'editprotected' ) ) { + // Users with 'editprotected' permission cannot edit protected pages + // with cascading option turned on. + if( $this->mCascadeRestriction ) { + $errors[] = array( 'protectedpagetext', $right ); + } + } else { + $errors[] = array( 'protectedpagetext', $right ); + } + } + } + # Short-circuit point + if( $short && count($errors) > 0 ) { + wfProfileOut( __METHOD__ ); + return $errors; + } + if( $doExpensiveQueries && !$this->isCssJsSubpage() ) { # We /could/ use the protection level on the source page, but it's fairly ugly # as we have to establish a precedence hierarchy for pages included by multiple @@ -1254,30 +1349,6 @@ class Title { return $errors; } - foreach( $this->getRestrictions($action) as $right ) { - // Backwards compatibility, rewrite sysop -> protect - if( $right == 'sysop' ) { - $right = 'protect'; - } - if( '' != $right && !$user->isAllowed( $right ) ) { - // Users with 'editprotected' permission can edit protected pages - if( $action=='edit' && $user->isAllowed( 'editprotected' ) ) { - // Users with 'editprotected' permission cannot edit protected pages - // with cascading option turned on. - if( $this->mCascadeRestriction ) { - $errors[] = array( 'protectedpagetext', $right ); - } - } else { - $errors[] = array( 'protectedpagetext', $right ); - } - } - } - # Short-circuit point - if( $short && count($errors) > 0 ) { - wfProfileOut( __METHOD__ ); - return $errors; - } - if( $action == 'protect' ) { if( $this->getUserPermissionsErrors('edit', $user) != array() ) { $errors[] = array( 'protect-cantedit' ); // If they can't edit, they shouldn't protect. @@ -1296,26 +1367,7 @@ class Title { $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' ) { - if( !$user->isAllowed( 'move' ) ) { - // User can't move anything - $errors[] = $user->isAnon() ? array ( 'movenologintext' ) : array ('movenotallowed'); - } elseif( !$user->isAllowed( 'move-rootuserpages' ) - && $this->getNamespace() == NS_USER && !$this->isSubpage() ) - { - // Show user page-specific message only if the user can move other pages - $errors[] = array( 'cant-move-user-page' ); - } - // Check if user is allowed to move files if it's a file - if( $this->getNamespace() == NS_FILE && !$user->isAllowed( 'movefile' ) ) { - $errors[] = array( 'movenotallowedfile' ); - } // Check for immobile pages if( !MWNamespace::isMovable( $this->getNamespace() ) ) { // Specific message for this case @@ -1325,31 +1377,11 @@ class Title { $errors[] = array( 'immobile-page' ); } } elseif( $action == 'move-target' ) { - if( !$user->isAllowed( 'move' ) ) { - // User can't move anything - $errors[] = $user->isAnon() ? array ( 'movenologintext' ) : array ('movenotallowed'); - } elseif( !$user->isAllowed( 'move-rootuserpages' ) - && $this->getNamespace() == NS_USER && !$this->isSubpage() ) - { - // Show user page-specific message only if the user can move other pages - $errors[] = array( 'cant-move-to-user-page' ); - } if( !MWNamespace::isMovable( $this->getNamespace() ) ) { $errors[] = array( 'immobile-target-namespace', $this->getNsText() ); } elseif( !$this->isMovable() ) { $errors[] = array( 'immobile-target-page' ); } - } elseif( !$user->isAllowed( $action ) ) { - $return = null; - $groups = array_map( array( 'User', 'makeGroupLinkWiki' ), - User::getGroupsWithPermission( $action ) ); - if( $groups ) { - $return = array( 'badaccess-groups', - array( implode( ', ', $groups ), count( $groups ) ) ); - } else { - $return = array( "badaccess-group0" ); - } - $errors[] = $return; } wfProfileOut( __METHOD__ ); @@ -1366,6 +1398,11 @@ class Title { if ( $this->getNamespace() < 0 ) { return false; } + + // Can't protect pages that exist. + if ($this->exists()) { + return false; + } $dbr = wfGetDB( DB_SLAVE ); $res = $dbr->select( 'protected_titles', '*', @@ -1402,7 +1439,8 @@ class Title { $expiry_description = ''; if ( $encodedExpiry != 'infinity' ) { - $expiry_description = ' (' . wfMsgForContent( 'protect-expiring', $wgContLang->timeanddate( $expiry ) , $wgContLang->date( $expiry ) , $wgContLang->time( $expiry ) ).')'; + $expiry_description = ' (' . wfMsgForContent( 'protect-expiring',$wgContLang->timeanddate( $expiry ), + $wgContLang->date( $expiry ) , $wgContLang->time( $expiry ) ).')'; } else { $expiry_description .= ' (' . wfMsgForContent( 'protect-expiry-indefinite' ).')'; @@ -1411,23 +1449,30 @@ class Title { # 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__ ); + 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( $dbw->affectedRows() ) { + $log = new LogPage( 'protect' ); - if( $create_perm ) { - $params = array("[create=$create_perm] $expiry_description",''); - $log->addEntry( $this->mRestrictions['create'] ? 'modify' : 'protect', $this, trim( $reason ), $params ); - } else { - $log->addEntry( 'unprotect', $this, $reason ); + 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; @@ -1444,33 +1489,6 @@ class Title { __METHOD__ ); } - /** - * Can $wgUser edit this page? - * @return \type{\bool} TRUE or FALSE - * @deprecated use userCan('edit') - */ - public function userCanEdit( $doExpensiveQueries = true ) { - return $this->userCan( 'edit', $doExpensiveQueries ); - } - - /** - * Can $wgUser create this page? - * @return \type{\bool} TRUE or FALSE - * @deprecated use userCan('create') - */ - public function userCanCreate( $doExpensiveQueries = true ) { - return $this->userCan( 'create', $doExpensiveQueries ); - } - - /** - * Can $wgUser move this page? - * @return \type{\bool} TRUE or FALSE - * @deprecated use userCan('move') - */ - public function userCanMove( $doExpensiveQueries = true ) { - return $this->userCan( 'move', $doExpensiveQueries ); - } - /** * Would anybody with sufficient privileges be able to move this page? * Some pages just aren't movable. @@ -1488,7 +1506,33 @@ class Title { */ public function userCanRead() { global $wgUser, $wgGroupPermissions; - + + static $useShortcut = null; + + # Initialize the $useShortcut boolean, to determine if we can skip quite a bit of code below + if( is_null( $useShortcut ) ) { + global $wgRevokePermissions; + $useShortcut = true; + if( empty( $wgGroupPermissions['*']['read'] ) ) { + # Not a public wiki, so no shortcut + $useShortcut = false; + } elseif( !empty( $wgRevokePermissions ) ) { + /* + * Iterate through each group with permissions being revoked (key not included since we don't care + * what the group name is), then check if the read permission is being revoked. If it is, then + * we don't use the shortcut below since the user might not be able to read, even though anon + * reading is allowed. + */ + foreach( $wgRevokePermissions as $perms ) { + if( !empty( $perms['read'] ) ) { + # We might be removing the read right from the user, so no shortcut + $useShortcut = false; + break; + } + } + } + } + $result = null; wfRunHooks( 'userCan', array( &$this, &$wgUser, 'read', &$result ) ); if ( $result !== null ) { @@ -1496,7 +1540,7 @@ class Title { } # Shortcut for public wikis, allows skipping quite a bit of code - if ( !empty( $wgGroupPermissions['*']['read'] ) ) + if ( $useShortcut ) return true; if( $wgUser->isAllowed( 'read' ) ) { @@ -1594,11 +1638,36 @@ class Title { return $this->mHasSubpages; } - $db = wfGetDB( DB_SLAVE ); - return $this->mHasSubpages = (bool)$db->selectField( 'page', '1', - "page_namespace = {$this->mNamespace} AND page_title LIKE '" - . $db->escapeLike( $this->mDbkeyform ) . "/%'", - __METHOD__ + $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 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 LIKE ' . $dbr->addQuotes( + $dbr->escapeLike( $this->getDBkey() ) . '/%' ); + $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 + ) ); } @@ -1656,15 +1725,28 @@ class Title { return ( NS_USER == $this->mNamespace && preg_match("/\\/.*\\.js$/", $this->mTextform ) ); } /** - * Protect css/js subpages of user pages: can $wgUser edit + * Protect css subpages of user pages: can $wgUser edit + * this page? + * + * @return \type{\bool} TRUE or FALSE + * @todo XXX: this might be better using restrictions + */ + public function userCanEditCssSubpage() { + global $wgUser; + return ( ( $wgUser->isAllowed('editusercssjs') && $wgUser->isAllowed('editusercss') ) + || preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) ); + } + /** + * Protect js subpages of user pages: can $wgUser edit * this page? * * @return \type{\bool} TRUE or FALSE * @todo XXX: this might be better using restrictions */ - public function userCanEditCssJsSubpage() { + public function userCanEditJsSubpage() { global $wgUser; - return ( $wgUser->isAllowed('editusercssjs') || preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) ); + return ( ( $wgUser->isAllowed('editusercssjs') && $wgUser->isAllowed('edituserjs') ) + || preg_match('/^'.preg_quote($wgUser->getName(), '/').'\//', $this->mTextform) ); } /** @@ -1780,7 +1862,18 @@ class Title { * Loads a string into mRestrictions array * @param $res \type{Resource} restrictions as an SQL result. */ - private function loadRestrictionsFromRow( $res, $oldFashionedRestrictions = NULL ) { + private function loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions = NULL ) { + $rows = array(); + $dbr = wfGetDB( DB_SLAVE ); + + while( $row = $dbr->fetchObject( $res ) ) { + $rows[] = $row; + } + + $this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions ); + } + + public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = NULL ) { global $wgRestrictionTypes; $dbr = wfGetDB( DB_SLAVE ); @@ -1815,12 +1908,12 @@ class Title { } - if( $dbr->numRows( $res ) ) { + if( count($rows) ) { # Current system - load second to make them override. $now = wfTimestampNow(); $purgeExpired = false; - foreach( $res as $row ) { + foreach( $rows as $row ) { # Cycle through all the restrictions. // Don't take care of restrictions types that aren't in $wgRestrictionTypes @@ -1862,7 +1955,7 @@ class Title { $res = $dbr->select( 'page_restrictions', '*', array ( 'pr_page' => $this->getArticleId() ), __METHOD__ ); - $this->loadRestrictionsFromRow( $res, $oldFashionedRestrictions ); + $this->loadRestrictionsFromResultWrapper( $res, $oldFashionedRestrictions ); } else { $title_protection = $this->getTitleProtection(); @@ -1933,20 +2026,45 @@ class Title { * @return \type{\int} the number of archived revisions */ public function isDeleted() { - $fname = 'Title::isDeleted'; - if ( $this->getNamespace() < 0 ) { + if( $this->getNamespace() < 0 ) { $n = 0; } else { $dbr = wfGetDB( DB_SLAVE ); - $n = $dbr->selectField( 'archive', 'COUNT(*)', array( 'ar_namespace' => $this->getNamespace(), - 'ar_title' => $this->getDBkey() ), $fname ); + $n = $dbr->selectField( 'archive', 'COUNT(*)', + array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ), + __METHOD__ + ); if( $this->getNamespace() == NS_FILE ) { $n += $dbr->selectField( 'filearchive', 'COUNT(*)', - array( 'fa_name' => $this->getDBkey() ), $fname ); + array( 'fa_name' => $this->getDBkey() ), + __METHOD__ + ); } } return (int)$n; } + + /** + * Is there a version of this page in the deletion archive? + * @return bool + */ + public function isDeletedQuick() { + if( $this->getNamespace() < 0 ) { + return false; + } + $dbr = wfGetDB( DB_SLAVE ); + $deleted = (bool)$dbr->selectField( 'archive', '1', + array( 'ar_namespace' => $this->getNamespace(), 'ar_title' => $this->getDBkey() ), + __METHOD__ + ); + if( !$deleted && $this->getNamespace() == NS_FILE ) { + $deleted = (bool)$dbr->selectField( 'filearchive', '1', + array( 'fa_name' => $this->getDBkey() ), + __METHOD__ + ); + } + return $deleted; + } /** * Get the article ID for this Title from the link cache, @@ -2039,7 +2157,7 @@ class Title { $linkCache = LinkCache::singleton(); $linkCache->clearBadLink( $this->getPrefixedDBkey() ); - if ( 0 == $newid ) { $this->mArticleID = -1; } + if ( $newid === false ) { $this->mArticleID = -1; } else { $this->mArticleID = $newid; } $this->mRestrictionsLoaded = false; $this->mRestrictions = array(); @@ -2346,13 +2464,13 @@ class Title { * WARNING: do not use this function on arbitrary user-supplied titles! * On heavily-used templates it will max out the memory. * - * @param $options \type{\string} may be FOR UPDATE + * @param array $options may be FOR UPDATE * @return \type{\arrayof{Title}} the Title objects linking here */ - public function getLinksTo( $options = '', $table = 'pagelinks', $prefix = 'pl' ) { + public function getLinksTo( $options = array(), $table = 'pagelinks', $prefix = 'pl' ) { $linkCache = LinkCache::singleton(); - if ( $options ) { + if ( count( $options ) > 0 ) { $db = wfGetDB( DB_MASTER ); } else { $db = wfGetDB( DB_SLAVE ); @@ -2387,10 +2505,10 @@ class Title { * WARNING: do not use this function on arbitrary user-supplied titles! * On heavily-used templates it will max out the memory. * - * @param $options \type{\string} may be FOR UPDATE + * @param array $options may be FOR UPDATE * @return \type{\arrayof{Title}} the Title objects linking here */ - public function getTemplateLinksTo( $options = '' ) { + public function getTemplateLinksTo( $options = array() ) { return $this->getLinksTo( $options, 'templatelinks', 'tl' ); } @@ -2398,42 +2516,35 @@ 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 $options \type{\string} may be FOR UPDATE * @return \type{\arrayof{Title}} the Title objects */ - public function getBrokenLinksFrom( $options = '' ) { + public function getBrokenLinksFrom() { if ( $this->getArticleId() == 0 ) { # All links from article ID 0 are false positives return array(); } - if ( $options ) { - $db = wfGetDB( DB_MASTER ); - } else { - $db = wfGetDB( DB_SLAVE ); - } - - $res = $db->safeQuery( - "SELECT pl_namespace, pl_title - FROM ! - LEFT JOIN ! - ON pl_namespace=page_namespace - AND pl_title=page_title - WHERE pl_from=? - AND page_namespace IS NULL - !", - $db->tableName( 'pagelinks' ), - $db->tableName( 'page' ), - $this->getArticleId(), - $options ); + $dbr = wfGetDB( DB_SLAVE ); + $res = $dbr->select( + array( 'page', 'pagelinks' ), + array( 'pl_namespace', 'pl_title' ), + array( + 'pl_from' => $this->getArticleId(), + 'page_namespace IS NULL' + ), + __METHOD__, array(), + array( + 'page' => array( + 'LEFT JOIN', + array( 'pl_namespace=page_namespace', 'pl_title=page_title' ) + ) + ) + ); $retVal = array(); - if ( $db->numRows( $res ) ) { - foreach( $res as $row ) { - $retVal[] = Title::makeTitle( $row->pl_namespace, $row->pl_title ); - } + foreach( $res as $row ) { + $retVal[] = Title::makeTitle( $row->pl_namespace, $row->pl_title ); } - $db->freeResult( $res ); return $retVal; } @@ -2537,7 +2648,7 @@ class Title { if( $nt->getText() != wfStripIllegalFilenameChars( $nt->getText() ) ) { $errors[] = array('imageinvalidfilename'); } - if( !File::checkExtensionCompatibility( $file, $nt->getDBKey() ) ) { + if( !File::checkExtensionCompatibility( $file, $nt->getDBkey() ) ) { $errors[] = array('imagetypemismatch'); } } @@ -2551,7 +2662,7 @@ class Title { $nt->getUserPermissionsErrors('edit', $wgUser) ); } - $match = EditPage::matchSpamRegex( $reason ); + $match = EditPage::matchSummarySpamRegex( $reason ); if( $match !== false ) { // This is kind of lame, won't display nice $errors[] = array('spamprotectiontext'); @@ -2598,6 +2709,18 @@ class Title { return $err; } + // If it is a file, more it first. It is done before all other moving stuff is done because it's hard to revert + $dbw = wfGetDB( DB_MASTER ); + if( $this->getNamespace() == NS_FILE ) { + $file = wfLocalFile( $this ); + if( $file->exists() ) { + $status = $file->move( $nt ); + if( !$status->isOk() ) { + return $status->getErrorsArray(); + } + } + } + $pageid = $this->getArticleID(); $protected = $this->isProtected(); if( $nt->exists() ) { @@ -2624,7 +2747,6 @@ class 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 ); $dbw->update( 'categorylinks', array( 'cl_sortkey' => $nt->getPrefixedText(), @@ -2693,8 +2815,16 @@ class Title { # Update message cache for interface messages if( $nt->getNamespace() == NS_MEDIAWIKI ) { global $wgMessageCache; - $oldarticle = new Article( $this ); - $wgMessageCache->replace( $this->getDBkey(), $oldarticle->getContent() ); + + # @bug 17860: old article can be deleted, if this the case, + # delete it from message cache + if ( $this->getArticleID() === 0 ) { + $wgMessageCache->replace( $this->getDBkey(), false ); + } else { + $oldarticle = new Article( $this ); + $wgMessageCache->replace( $this->getDBkey(), $oldarticle->getContent() ); + } + $newarticle = new Article( $nt ); $wgMessageCache->replace( $nt->getDBkey(), $newarticle->getContent() ); } @@ -2730,6 +2860,10 @@ class Title { $dbw = wfGetDB( DB_MASTER ); + $rcts = $dbw->timestamp( $nt->getEarliestRevTime() ); + $newns = $nt->getNamespace(); + $newdbk = $nt->getDBkey(); + # 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 @@ -2748,6 +2882,11 @@ class Title { $dbw->delete( 'langlinks', array( 'll_from' => $newid ), __METHOD__ ); $dbw->delete( 'redirect', array( 'rd_from' => $newid ), __METHOD__ ); } + // If the redirect was recently created, it may have an entry in recentchanges still + $dbw->delete( 'recentchanges', + array( 'rc_timestamp' => $rcts, 'rc_namespace' => $newns, 'rc_title' => $newdbk, 'rc_new' => 1 ), + __METHOD__ + ); # Save a null revision in the page's history notifying of the move $nullRevision = Revision::newNullRevision( $dbw, $oldid, $comment, true ); @@ -2799,18 +2938,6 @@ class Title { $redirectSuppressed = true; } - # Move an image if this is a file - if( $this->getNamespace() == NS_FILE ) { - $file = wfLocalFile( $this ); - if( $file->exists() ) { - $status = $file->move( $nt ); - if( !$status->isOk() ) { - $dbw->rollback(); - return $status->getErrorsArray(); - } - } - } - # Log the move $log = new LogPage( 'move' ); $log->addEntry( 'move_redir', $this, $reason, array( 1 => $nt->getPrefixedText(), 2 => $redirectSuppressed ) ); @@ -2896,18 +3023,6 @@ class Title { $redirectSuppressed = true; } - # Move an image if this is a file - if( $this->getNamespace() == NS_FILE ) { - $file = wfLocalFile( $this ); - if( $file->exists() ) { - $status = $file->move( $nt ); - if( !$status->isOk() ) { - $dbw->rollback(); - return $status->getErrorsArray(); - } - } - } - # Log the move $log = new LogPage( 'move' ); $log->addEntry( 'move', $this, $reason, array( 1 => $nt->getPrefixedText(), 2 => $redirectSuppressed ) ); @@ -2921,6 +3036,67 @@ class Title { } + /** + * Move this page's subpages to be subpages of $nt + * @param $nt Title Move target + * @param $auth bool Whether $wgUser's permissions should be checked + * @param $reason string The reason for the move + * @param $createRedirect bool Whether to create redirects from the old subpages to the new ones + * Ignored if the user doesn't have the 'suppressredirect' right + * @return mixed array with old page titles as keys, and strings (new page titles) or + * arrays (errors) as values, or an error array with numeric indices if no pages were moved + */ + public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true ) { + global $wgMaximumMovedPages; + // Check permissions + if( !$this->userCan( 'move-subpages' ) ) + return array( 'cant-move-subpages' ); + // Do the source and target namespaces support subpages? + if( !MWNamespace::hasSubpages( $this->getNamespace() ) ) + return array( 'namespace-nosubpages', + MWNamespace::getCanonicalName( $this->getNamespace() ) ); + if( !MWNamespace::hasSubpages( $nt->getNamespace() ) ) + return array( 'namespace-nosubpages', + MWNamespace::getCanonicalName( $nt->getNamespace() ) ); + + $subpages = $this->getSubpages($wgMaximumMovedPages + 1); + $retval = array(); + $count = 0; + foreach( $subpages as $oldSubpage ) { + $count++; + if( $count > $wgMaximumMovedPages ) { + $retval[$oldSubpage->getPrefixedTitle()] = + array( 'movepage-max-pages', + $wgMaximumMovedPages ); + break; + } + + if( $oldSubpage->getArticleId() == $this->getArticleId() ) + // When moving a page to a subpage of itself, + // don't move it twice + continue; + $newPageName = preg_replace( + '#^'.preg_quote( $this->getDBkey(), '#' ).'#', + $nt->getDBkey(), $oldSubpage->getDBkey() ); + if( $oldSubpage->isTalkPage() ) { + $newNs = $nt->getTalkPage()->getNamespace(); + } else { + $newNs = $nt->getSubjectPage()->getNamespace(); + } + # Bug 14385: we need makeTitleSafe because the new page names may + # be longer than 255 characters. + $newSubpage = Title::makeTitleSafe( $newNs, $newPageName ); + + $success = $oldSubpage->moveTo( $newSubpage, $auth, $reason, $createRedirect ); + if( $success === true ) { + $retval[$oldSubpage->getPrefixedText()] = $newSubpage->getPrefixedText(); + } else { + $retval[$oldSubpage->getPrefixedText()] = $success; + } + } + return $retval; + } + /** * Checks if this page is just a one-rev redirect. * Adds lock, so don't use just for light purposes. @@ -2934,7 +3110,7 @@ class Title { array( 'page_is_redirect', 'page_latest', 'page_id' ), $this->pageCond(), __METHOD__, - 'FOR UPDATE' + array( 'FOR UPDATE' ) ); # Cache some fields we may want $this->mArticleID = $row ? intval($row->page_id) : 0; @@ -2952,7 +3128,7 @@ class Title { 'page_latest != rev_id' ), __METHOD__, - 'FOR UPDATE' + array( 'FOR UPDATE' ) ); # Return true if there was no history return ($row === false); @@ -3188,8 +3364,8 @@ class Title { 'rev_page = ' . intval( $this->getArticleId() ) . ' AND rev_id > ' . intval( $old ) . ' AND rev_id < ' . intval( $new ), - __METHOD__, - array( 'USE INDEX' => 'PRIMARY' ) ); + __METHOD__ + ); } /** @@ -3208,7 +3384,7 @@ class Title { /** * Callback for usort() to do title sorts by (namespace, title) */ - static function compare( $a, $b ) { + public static function compare( $a, $b ) { if( $a->getNamespace() == $b->getNamespace() ) { return strcmp( $a->getText(), $b->getText() ); } else { @@ -3258,12 +3434,12 @@ class Title { if( $this->mInterwiki != '' ) { return true; // any interwiki link might be viewable, for all we know } - switch( $this->mNamespace ) { + switch( $this->mNamespace ) { case NS_MEDIA: case NS_FILE: return wfFindFile( $this ); // file exists, possibly in a foreign repo case NS_SPECIAL: - return SpecialPage::exists( $this->getDBKey() ); // valid special page + return SpecialPage::exists( $this->getDBkey() ); // valid special page case NS_MAIN: return $this->mDbkeyform == ''; // selflink, possibly with fragment case NS_MEDIAWIKI: @@ -3364,9 +3540,9 @@ class Title { * @return \type{\string} Trackback URL */ public function trackbackURL() { - global $wgScriptPath, $wgServer; + global $wgScriptPath, $wgServer, $wgScriptExtension; - return "$wgServer$wgScriptPath/trackback.php?article=" + return "$wgServer$wgScriptPath/trackback$wgScriptExtension?article=" . htmlspecialchars(urlencode($this->getPrefixedDBkey())); } @@ -3401,40 +3577,29 @@ class Title { * Generate strings used for xml 'id' names in monobook tabs * @return \type{\string} XML 'id' name */ - public function getNamespaceKey() { - global $wgContLang; - switch ($this->getNamespace()) { - case NS_MAIN: - case NS_TALK: - return 'nstab-main'; - case NS_USER: - case NS_USER_TALK: - return 'nstab-user'; - case NS_MEDIA: - return 'nstab-media'; - case NS_SPECIAL: - return 'nstab-special'; - case NS_PROJECT: - case NS_PROJECT_TALK: - return 'nstab-project'; - case NS_FILE: - case NS_FILE_TALK: - return 'nstab-image'; - case NS_MEDIAWIKI: - case NS_MEDIAWIKI_TALK: - return 'nstab-mediawiki'; - case NS_TEMPLATE: - case NS_TEMPLATE_TALK: - return 'nstab-template'; - case NS_HELP: - case NS_HELP_TALK: - return 'nstab-help'; - case NS_CATEGORY: - case NS_CATEGORY_TALK: - return 'nstab-category'; - default: - return 'nstab-' . $wgContLang->lc( $this->getSubjectNsText() ); + public function getNamespaceKey( $prepend = 'nstab-' ) { + global $wgContLang, $wgCanonicalNamespaceNames; + // Gets the subject namespace if this title + $namespace = MWNamespace::getSubject( $this->getNamespace() ); + // Checks if cononical namespace name exists for namespace + if ( isset( $wgCanonicalNamespaceNames[$namespace] ) ) { + // Uses canonical namespace name + $namespaceKey = $wgCanonicalNamespaceNames[$namespace]; + } else { + // Uses text of namespace + $namespaceKey = $this->getSubjectNsText(); + } + // Makes namespace key lowercase + $namespaceKey = $wgContLang->lc( $namespaceKey ); + // Uses main + if ( $namespaceKey == '' ) { + $namespaceKey = 'main'; } + // Changes file to image for backwards compatibility + if ( $namespaceKey == 'file' ) { + $namespaceKey = 'image'; + } + return $prepend . $namespaceKey; } /** @@ -3532,4 +3697,14 @@ class Title { return true; } + + /** + * Get a backlink cache object + */ + function getBacklinkCache() { + if ( is_null( $this->mBacklinkCache ) ) { + $this->mBacklinkCache = new BacklinkCache( $this ); + } + return $this->mBacklinkCache; + } }