Merge "Move Database and subclasses to Rdbms namespace"
[lhc/web/wiklou.git] / includes / Title.php
index 4d496e4..dd6aaef 100644 (file)
@@ -21,6 +21,9 @@
  *
  * @file
  */
+
+use Wikimedia\Rdbms\Database;
+use Wikimedia\Rdbms\IDatabase;
 use MediaWiki\Linker\LinkTarget;
 use MediaWiki\Interwiki\InterwikiLookup;
 use MediaWiki\MediaWikiServices;
@@ -134,7 +137,7 @@ class Title implements LinkTarget {
 
        /**
         * @var int Namespace index when there is no namespace. Don't change the
-        *   following default, NS_MAIN is hardcoded in several places. See bug 696.
+        *   following default, NS_MAIN is hardcoded in several places. See T2696.
         *   Zero except in {{transclusion}} tags.
         */
        public $mDefaultNamespace = NS_MAIN;
@@ -307,7 +310,7 @@ class Title implements LinkTarget {
                        }
                }
 
-               // Convert things like é ā or 〗 into normalized (bug 14952) text
+               // Convert things like é ā or 〗 into normalized (T16952) text
                $filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text );
 
                $t = new Title();
@@ -1233,12 +1236,6 @@ class Title implements LinkTarget {
                        && ( $this->hasContentModel( CONTENT_MODEL_CSS )
                                || $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) );
 
-               # @note This hook is also called in ContentHandler::getDefaultModel.
-               #   It's called here again to make sure hook functions can force this
-               #   method to return true even outside the MediaWiki namespace.
-
-               Hooks::run( 'TitleIsCssOrJsPage', [ $this, &$isCssOrJsPage ], '1.25' );
-
                return $isCssOrJsPage;
        }
 
@@ -1687,6 +1684,33 @@ class Title implements LinkTarget {
                return $url;
        }
 
+       /**
+        * Get a url appropriate for making redirects based on an untrusted url arg
+        *
+        * This is basically the same as getFullUrl(), but in the case of external
+        * interwikis, we send the user to a landing page, to prevent possible
+        * phishing attacks and the like.
+        *
+        * @note Uses current protocol by default, since technically relative urls
+        *   aren't allowed in redirects per HTTP spec, so this is not suitable for
+        *   places where the url gets cached, as might pollute between
+        *   https and non-https users.
+        * @see self::getLocalURL for the arguments.
+        * @param array|string $query
+        * @param string $proto Protocol type to use in URL
+        * @return String. A url suitable to use in an HTTP location header.
+        */
+       public function getFullUrlForRedirect( $query = '', $proto = PROTO_CURRENT ) {
+               $target = $this;
+               if ( $this->isExternal() ) {
+                       $target = SpecialPage::getTitleFor(
+                               'GoToInterwiki',
+                               $this->getPrefixedDBKey()
+                       );
+               }
+               return $target->getFullUrl( $query, false, $proto );
+       }
+
        /**
         * Get a URL with no fragment or server name (relative URL) from a Title object.
         * If this page is generated with action=render, however,
@@ -2128,8 +2152,7 @@ class Title implements LinkTarget {
        private function checkCSSandJSPermissions( $action, $user, $errors, $rigor, $short ) {
                # Protect css/js subpages of user pages
                # XXX: this might be better using restrictions
-               # XXX: right 'editusercssjs' is deprecated, for backward compatibility only
-               if ( $action != 'patrol' && !$user->isAllowed( 'editusercssjs' ) ) {
+               if ( $action != 'patrol' ) {
                        if ( preg_match( '/^' . preg_quote( $user->getName(), '/' ) . '\//', $this->mTextform ) ) {
                                if ( $this->isCssSubpage() && !$user->isAllowedAny( 'editmyusercss', 'editusercss' ) ) {
                                        $errors[] = [ 'mycustomcssprotected', $action ];
@@ -2293,6 +2316,17 @@ class Title implements LinkTarget {
                        ) {
                                $errors[] = [ 'delete-toobig', $wgLang->formatNum( $wgDeleteRevisionsLimit ) ];
                        }
+               } elseif ( $action === 'undelete' ) {
+                       if ( count( $this->getUserPermissionsErrorsInternal( 'edit', $user, $rigor, true ) ) ) {
+                               // Undeleting implies editing
+                               $errors[] = [ 'undelete-cantedit' ];
+                       }
+                       if ( !$this->exists()
+                               && count( $this->getUserPermissionsErrorsInternal( 'create', $user, $rigor, true ) )
+                       ) {
+                               // Undeleting where nothing currently exists implies creating
+                               $errors[] = [ 'undelete-cantcreate' ];
+                       }
                }
                return $errors;
        }
@@ -2423,7 +2457,7 @@ class Title implements LinkTarget {
         *
         * @param string $action The action to check
         * @param bool $short Short circuit on first error
-        * @return array List of errors
+        * @return array Array containing an error message key and any parameters
         */
        private function missingPermissionError( $action, $short ) {
                // We avoid expensive display logic for quickUserCan's and such
@@ -2431,19 +2465,7 @@ class Title implements LinkTarget {
                        return [ 'badaccess-group0' ];
                }
 
-               $groups = array_map( [ 'User', 'makeGroupLinkWiki' ],
-                       User::getGroupsWithPermission( $action ) );
-
-               if ( count( $groups ) ) {
-                       global $wgLang;
-                       return [
-                               'badaccess-groups',
-                               $wgLang->commaList( $groups ),
-                               count( $groups )
-                       ];
-               } else {
-                       return [ 'badaccess-group0' ];
-               }
+               return User::newFatalPermissionDeniedStatus( $action )->getErrorsArray()[0];
        }
 
        /**
@@ -2567,6 +2589,29 @@ class Title implements LinkTarget {
         *   protection, or false if there's none.
         */
        public function getTitleProtection() {
+               $protection = $this->getTitleProtectionInternal();
+               if ( $protection ) {
+                       if ( $protection['permission'] == 'sysop' ) {
+                               $protection['permission'] = 'editprotected'; // B/C
+                       }
+                       if ( $protection['permission'] == 'autoconfirmed' ) {
+                               $protection['permission'] = 'editsemiprotected'; // B/C
+                       }
+               }
+               return $protection;
+       }
+
+       /**
+        * Fetch title protection settings
+        *
+        * To work correctly, $this->loadRestrictions() needs to have access to the
+        * actual protections in the database without munging 'sysop' =>
+        * 'editprotected' and 'autoconfirmed' => 'editsemiprotected'. Other
+        * callers probably want $this->getTitleProtection() instead.
+        *
+        * @return array|bool
+        */
+       protected function getTitleProtectionInternal() {
                // Can't protect pages in special namespaces
                if ( $this->getNamespace() < 0 ) {
                        return false;
@@ -2594,12 +2639,6 @@ class Title implements LinkTarget {
                        // fetchRow returns false if there are no rows.
                        $row = $dbr->fetchRow( $res );
                        if ( $row ) {
-                               if ( $row['permission'] == 'sysop' ) {
-                                       $row['permission'] = 'editprotected'; // B/C
-                               }
-                               if ( $row['permission'] == 'autoconfirmed' ) {
-                                       $row['permission'] = 'editsemiprotected'; // B/C
-                               }
                                $row['expiry'] = $dbr->decodeExpiry( $row['expiry'] );
                        }
                        $this->mTitleProtection = $row;
@@ -2944,8 +2983,6 @@ class Title implements LinkTarget {
                                        continue;
                                }
 
-                               // This code should be refactored, now that it's being used more generally,
-                               // But I don't really see any harm in leaving it in Block for now -werdna
                                $expiry = $dbr->decodeExpiry( $row->pr_expiry );
 
                                // Only apply the restrictions if they haven't expired!
@@ -2997,7 +3034,7 @@ class Title implements LinkTarget {
 
                        $this->loadRestrictionsFromRows( $rows, $oldFashionedRestrictions );
                } else {
-                       $title_protection = $this->getTitleProtection();
+                       $title_protection = $this->getTitleProtectionInternal();
 
                        if ( $title_protection ) {
                                $now = wfTimestampNow();
@@ -3672,9 +3709,12 @@ class Title implements LinkTarget {
         * @param string $reason The reason for the move
         * @param bool $createRedirect Whether to create a redirect from the old title to the new title.
         *  Ignored if the user doesn't have the suppressredirect right.
+        * @param array $changeTags Applied to the entry in the move log and redirect page revision
         * @return array|bool True on success, getUserPermissionsErrors()-like array on failure
         */
-       public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true ) {
+       public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true,
+               array $changeTags = [] ) {
+
                global $wgUser;
                $err = $this->isValidMoveOperation( $nt, $auth, $reason );
                if ( is_array( $err ) ) {
@@ -3688,7 +3728,7 @@ class Title implements LinkTarget {
                }
 
                $mp = new MovePage( $this, $nt );
-               $status = $mp->move( $wgUser, $reason, $createRedirect );
+               $status = $mp->move( $wgUser, $reason, $createRedirect, $changeTags );
                if ( $status->isOK() ) {
                        return true;
                } else {
@@ -3704,12 +3744,15 @@ class Title implements LinkTarget {
         * @param string $reason The reason for the move
         * @param bool $createRedirect Whether to create redirects from the old subpages to
         *     the new ones Ignored if the user doesn't have the 'suppressredirect' right
+        * @param array $changeTags Applied to the entry in the move log and redirect page revision
         * @return array Array with old page titles as keys, and strings (new page titles) or
         *     getUserPermissionsErrors()-like arrays (errors) as values, or a
         *     getUserPermissionsErrors()-like error array with numeric indices if
         *     no pages were moved
         */
-       public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true ) {
+       public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true,
+               array $changeTags = [] ) {
+
                global $wgMaximumMovedPages;
                // Check permissions
                if ( !$this->userCan( 'move-subpages' ) ) {
@@ -3753,18 +3796,18 @@ class Title implements LinkTarget {
                        }
                        $newPageName = preg_replace(
                                        '#^' . preg_quote( $this->getDBkey(), '#' ) . '#',
-                                       StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # bug 21234
+                                       StringUtils::escapeRegexReplacement( $nt->getDBkey() ), # T23234
                                        $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
+                       # T16385: 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 );
+                       $success = $oldSubpage->moveTo( $newSubpage, $auth, $reason, $createRedirect, $changeTags );
                        if ( $success === true ) {
                                $retval[$oldSubpage->getPrefixedText()] = $newSubpage->getPrefixedText();
                        } else {
@@ -3877,7 +3920,7 @@ class Title implements LinkTarget {
         * categories' names.
         *
         * @return array Array of parents in the form:
-        *        $parent => $currentarticle
+        *     $parent => $currentarticle
         */
        public function getParentCategories() {
                global $wgContLang;
@@ -3958,14 +4001,22 @@ class Title implements LinkTarget {
         * @return int|bool Old revision ID, or false if none exists
         */
        public function getPreviousRevisionID( $revId, $flags = 0 ) {
-               $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_REPLICA );
+               /* This function and getNextRevisionID have bad performance when
+                  used on a page with many revisions on mysql. An explicit extended
+                  primary key may help in some cases, if the PRIMARY KEY is banned:
+                  T159319 */
+               if ( $flags & self::GAID_FOR_UPDATE ) {
+                       $db = wfGetDB( DB_MASTER );
+               } else {
+                       $db = wfGetDB( DB_REPLICA, 'contributions' );
+               }
                $revId = $db->selectField( 'revision', 'rev_id',
                        [
                                'rev_page' => $this->getArticleID( $flags ),
                                'rev_id < ' . intval( $revId )
                        ],
                        __METHOD__,
-                       [ 'ORDER BY' => 'rev_id DESC' ]
+                       [ 'ORDER BY' => 'rev_id DESC', 'IGNORE INDEX' => 'PRIMARY' ]
                );
 
                if ( $revId === false ) {
@@ -3983,14 +4034,18 @@ class Title implements LinkTarget {
         * @return int|bool Next revision ID, or false if none exists
         */
        public function getNextRevisionID( $revId, $flags = 0 ) {
-               $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_REPLICA );
+               if ( $flags & self::GAID_FOR_UPDATE ) {
+                       $db = wfGetDB( DB_MASTER );
+               } else {
+                       $db = wfGetDB( DB_REPLICA, 'contributions' );
+               }
                $revId = $db->selectField( 'revision', 'rev_id',
                        [
                                'rev_page' => $this->getArticleID( $flags ),
                                'rev_id > ' . intval( $revId )
                        ],
                        __METHOD__,
-                       [ 'ORDER BY' => 'rev_id' ]
+                       [ 'ORDER BY' => 'rev_id', 'IGNORE INDEX' => 'PRIMARY' ]
                );
 
                if ( $revId === false ) {
@@ -4013,7 +4068,10 @@ class Title implements LinkTarget {
                        $row = $db->selectRow( 'revision', Revision::selectFields(),
                                [ 'rev_page' => $pageId ],
                                __METHOD__,
-                               [ 'ORDER BY' => 'rev_timestamp ASC', 'LIMIT' => 1 ]
+                               [
+                                       'ORDER BY' => 'rev_timestamp ASC',
+                                       'IGNORE INDEX' => 'rev_timestamp'
+                               ]
                        );
                        if ( $row ) {
                                return new Revision( $row );
@@ -4593,7 +4651,7 @@ class Title implements LinkTarget {
        }
 
        /**
-        * Whether the magic words __INDEX__ and __NOINDEX__ function for  this page.
+        * Whether the magic words __INDEX__ and __NOINDEX__ function for this page.
         *
         * @return bool
         */