Merge "RevisionStore tests for current behaviour of getTitle"
[lhc/web/wiklou.git] / includes / Title.php
index 5decece..3de85e1 100644 (file)
@@ -108,7 +108,12 @@ class Title implements LinkTarget {
        /** @var array Array of groups allowed to edit this article */
        public $mRestrictions = [];
 
-       /** @var string|bool */
+       /**
+        * @var string|bool Comma-separated set of permission keys
+        * indicating who can move or edit the page from the page table, (pre 1.10) rows.
+        * Edit and move sections are separated by a colon
+        * Example: "edit=autoconfirmed,sysop:move=sysop"
+        */
        protected $mOldRestrictions = false;
 
        /** @var bool Cascade restrictions on this page to included templates and images? */
@@ -253,6 +258,9 @@ class Title implements LinkTarget {
         * Create a new Title from text, such as what one would find in a link. De-
         * codes any HTML entities in the text.
         *
+        * Title objects returned by this method are guaranteed to be valid, and
+        * thus return true from the isValid() method.
+        *
         * @param string|int|null $text The link text; spaces, prefixes, and an
         *   initial ':' indicating the main namespace are accepted.
         * @param int $defaultNamespace The namespace to use if none is specified
@@ -284,6 +292,9 @@ class Title implements LinkTarget {
         *
         * The exception subclasses encode detailed information about why the title is invalid.
         *
+        * Title objects returned by this method are guaranteed to be valid, and
+        * thus return true from the isValid() method.
+        *
         * @see Title::newFromText
         *
         * @since 1.25
@@ -500,10 +511,19 @@ class Title implements LinkTarget {
 
        /**
         * Create a new Title from a namespace index and a DB key.
-        * It's assumed that $ns and $title are *valid*, for instance when
-        * they came directly from the database or a special page name.
-        * For convenience, spaces are converted to underscores so that
-        * eg user_text fields can be used directly.
+        *
+        * It's assumed that $ns and $title are safe, for instance when
+        * they came directly from the database or a special page name,
+        * not from user input.
+        *
+        * No validation is applied. For convenience, spaces are normalized
+        * to underscores, so that e.g. user_text fields can be used directly.
+        *
+        * @note This method may return Title objects that are "invalid"
+        * according to the isValid() method. This is usually caused by
+        * configuration changes: e.g. a namespace that was once defined is
+        * no longer configured, or a character that was once allowed in
+        * titles is now forbidden.
         *
         * @param int $ns The namespace of the article
         * @param string $title The unprefixed database key form
@@ -529,6 +549,10 @@ class Title implements LinkTarget {
         * The parameters will be checked for validity, which is a bit slower
         * than makeTitle() but safer for user-provided data.
         *
+        * Title objects returned by makeTitleSafe() are guaranteed to be valid,
+        * that is, they return true from the isValid() method. If no valid Title
+        * can be constructed from the input, this method returns null.
+        *
         * @param int $ns The namespace of the article
         * @param string $title Database key form
         * @param string $fragment The link fragment (after the "#")
@@ -536,6 +560,9 @@ class Title implements LinkTarget {
         * @return Title|null The new object, or null on an error
         */
        public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) {
+               // NOTE: ideally, this would just call makeTitle() and then isValid(),
+               // but presently, that means more overhead on a potential performance hotspot.
+
                if ( !MWNamespace::exists( $ns ) ) {
                        return null;
                }
@@ -754,6 +781,7 @@ class Title implements LinkTarget {
         * @return string Escaped string
         */
        static function escapeFragmentForURL( $fragment ) {
+               wfDeprecated( __METHOD__, '1.30' );
                # Note that we don't urlencode the fragment.  urlencoded Unicode
                # fragments appear not to work in IE (at least up to 7) or in at least
                # one version of Opera 9.x.  The W3C validator, for one, doesn't seem
@@ -777,6 +805,36 @@ class Title implements LinkTarget {
                }
        }
 
+       /**
+        * Returns true if the title is valid, false if it is invalid.
+        *
+        * Valid titles can be round-tripped via makeTitleSafe() and newFromText().
+        * Invalid titles may get returned from makeTitle(), and it may be useful to
+        * allow them to exist, e.g. in order to process log entries about pages in
+        * namespaces that belong to extensions that are no longer installed.
+        *
+        * @note This method is relatively expensive. When constructing Title
+        * objects that need to be valid, use an instantiator method that is guaranteed
+        * to return valid titles, such as makeTitleSafe() or newFromText().
+        *
+        * @return bool
+        */
+       public function isValid() {
+               $ns = $this->getNamespace();
+
+               if ( !MWNamespace::exists( $ns ) ) {
+                       return false;
+               }
+
+               try {
+                       $parser = MediaWikiServices::getInstance()->getTitleParser();
+                       $parser->parseTitle( $this->getDBkey(), $ns );
+                       return true;
+               } catch ( MalformedTitleException $ex ) {
+                       return false;
+               }
+       }
+
        /**
         * Determine whether the object refers to a page within
         * this project (either this wiki or a wiki with a local
@@ -1036,6 +1094,7 @@ class Title implements LinkTarget {
         * Can this title have a corresponding talk page?
         *
         * @see MWNamespace::hasTalkNamespace
+        * @since 1.30
         *
         * @return bool True if this title either is a talk page or can have a talk page associated.
         */
@@ -1324,7 +1383,7 @@ class Title implements LinkTarget {
         *
         * @since 1.30
         *
-        * @return Title The object for the talk page,
+        * @return Title|null The object for the talk page,
         *         or null if no associated talk page can exist, according to canHaveTalkPage().
         */
        public function getTalkPageIfDefined() {
@@ -1355,7 +1414,7 @@ class Title implements LinkTarget {
         * get the talk page, if it is a subject page get the talk page
         *
         * @since 1.25
-        * @throws MWException
+        * @throws MWException If the page doesn't have an other page
         * @return Title
         */
        public function getOtherPage() {
@@ -1365,6 +1424,9 @@ class Title implements LinkTarget {
                if ( $this->isTalkPage() ) {
                        return $this->getSubjectPage();
                } else {
+                       if ( !$this->canHaveTalkPage() ) {
+                               throw new MWException( "{$this->getPrefixedText()} does not have an other page" );
+                       }
                        return $this->getTalkPage();
                }
        }
@@ -1407,7 +1469,9 @@ class Title implements LinkTarget {
        public function getFragmentForURL() {
                if ( !$this->hasFragment() ) {
                        return '';
-               } elseif ( $this->isExternal() && !$this->getTransWikiID() ) {
+               } elseif ( $this->isExternal()
+                       && !self::getInterwikiLookup()->fetch( $this->mInterwiki )->isLocal()
+               ) {
                        return '#' . Sanitizer::escapeIdForExternalInterwiki( $this->getFragment() );
                }
                return '#' . Sanitizer::escapeIdForLink( $this->getFragment() );
@@ -1741,7 +1805,7 @@ class Title implements LinkTarget {
         * @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.
+        * @return string A url suitable to use in an HTTP location header.
         */
        public function getFullUrlForRedirect( $query = '', $proto = PROTO_CURRENT ) {
                $target = $this;
@@ -1896,6 +1960,8 @@ class Title implements LinkTarget {
         * protocol-relative, the URL will be expanded to http://
         *
         * @see self::getLocalURL for the arguments.
+        * @param string $query
+        * @param string|bool $query2
         * @return string The URL
         */
        public function getInternalURL( $query = '', $query2 = false ) {
@@ -1917,6 +1983,8 @@ class Title implements LinkTarget {
         * NOTE: Unlike getInternalURL(), the canonical URL includes the fragment
         *
         * @see self::getLocalURL for the arguments.
+        * @param string $query
+        * @param string|bool $query2
         * @return string The URL
         * @since 1.18
         */
@@ -2667,24 +2735,33 @@ class Title implements LinkTarget {
 
                if ( $this->mTitleProtection === null ) {
                        $dbr = wfGetDB( DB_REPLICA );
+                       $commentStore = new CommentStore( 'pt_reason' );
+                       $commentQuery = $commentStore->getJoin();
                        $res = $dbr->select(
-                               'protected_titles',
+                               [ 'protected_titles' ] + $commentQuery['tables'],
                                [
                                        'user' => 'pt_user',
-                                       'reason' => 'pt_reason',
                                        'expiry' => 'pt_expiry',
                                        'permission' => 'pt_create_perm'
-                               ],
+                               ] + $commentQuery['fields'],
                                [ 'pt_namespace' => $this->getNamespace(), 'pt_title' => $this->getDBkey() ],
-                               __METHOD__
+                               __METHOD__,
+                               [],
+                               $commentQuery['joins']
                        );
 
                        // fetchRow returns false if there are no rows.
                        $row = $dbr->fetchRow( $res );
                        if ( $row ) {
-                               $row['expiry'] = $dbr->decodeExpiry( $row['expiry'] );
+                               $this->mTitleProtection = [
+                                       'user' => $row['user'],
+                                       'expiry' => $dbr->decodeExpiry( $row['expiry'] ),
+                                       'permission' => $row['permission'],
+                                       'reason' => $commentStore->getComment( $row )->text,
+                               ];
+                       } else {
+                               $this->mTitleProtection = false;
                        }
-                       $this->mTitleProtection = $row;
                }
                return $this->mTitleProtection;
        }
@@ -2974,8 +3051,10 @@ class Title implements LinkTarget {
         * Public for usage by LiquidThreads.
         *
         * @param array $rows Array of db result objects
-        * @param string $oldFashionedRestrictions Comma-separated list of page
-        *   restrictions from page table (pre 1.10)
+        * @param string $oldFashionedRestrictions Comma-separated set of permission keys
+        * indicating who can move or edit the page from the page table, (pre 1.10) rows.
+        * Edit and move sections are separated by a colon
+        * Example: "edit=autoconfirmed,sysop:move=sysop"
         */
        public function loadRestrictionsFromRows( $rows, $oldFashionedRestrictions = null ) {
                $dbr = wfGetDB( DB_REPLICA );
@@ -3044,8 +3123,10 @@ class Title implements LinkTarget {
        /**
         * Load restrictions from the page_restrictions table
         *
-        * @param string $oldFashionedRestrictions Comma-separated list of page
-        *   restrictions from page table (pre 1.10)
+        * @param string $oldFashionedRestrictions Comma-separated set of permission keys
+        * indicating who can move or edit the page from the page table, (pre 1.10) rows.
+        * Edit and move sections are separated by a colon
+        * Example: "edit=autoconfirmed,sysop:move=sysop"
         */
        public function loadRestrictions( $oldFashionedRestrictions = null ) {
                if ( $this->mRestrictionsLoaded ) {
@@ -3558,19 +3639,20 @@ class Title implements LinkTarget {
                $blNamespace = "{$prefix}_namespace";
                $blTitle = "{$prefix}_title";
 
+               $pageQuery = WikiPage::getQueryInfo();
                $res = $db->select(
-                       [ $table, 'page' ],
+                       [ $table, 'nestpage' => $pageQuery['tables'] ],
                        array_merge(
                                [ $blNamespace, $blTitle ],
-                               WikiPage::selectFields()
+                               $pageQuery['fields']
                        ),
                        [ "{$prefix}_from" => $id ],
                        __METHOD__,
                        $options,
-                       [ 'page' => [
+                       [ 'nestpage' => [
                                'LEFT JOIN',
                                [ "page_namespace=$blNamespace", "page_title=$blTitle" ]
-                       ] ]
+                       ] ] + $pageQuery['joins']
                );
 
                $retVal = [];
@@ -3693,7 +3775,7 @@ class Title implements LinkTarget {
         * Returns true if ok, or a getUserPermissionsErrors()-like array otherwise
         *
         * @deprecated since 1.25, use MovePage's methods instead
-        * @param Title $nt The new title
+        * @param Title &$nt The new title
         * @param bool $auth Whether to check user permissions (uses $wgUser)
         * @param string $reason Is the log summary of the move, used for spam checking
         * @return array|bool True on success, getUserPermissionsErrors()-like array on failure
@@ -3745,7 +3827,7 @@ class Title implements LinkTarget {
         * Move a title to a new location
         *
         * @deprecated since 1.25, use the MovePage class instead
-        * @param Title $nt The new 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
@@ -4123,13 +4205,15 @@ class Title implements LinkTarget {
                $pageId = $this->getArticleID( $flags );
                if ( $pageId ) {
                        $db = ( $flags & self::GAID_FOR_UPDATE ) ? wfGetDB( DB_MASTER ) : wfGetDB( DB_REPLICA );
-                       $row = $db->selectRow( 'revision', Revision::selectFields(),
+                       $revQuery = Revision::getQueryInfo();
+                       $row = $db->selectRow( $revQuery['tables'], $revQuery['fields'],
                                [ 'rev_page' => $pageId ],
                                __METHOD__,
                                [
                                        'ORDER BY' => 'rev_timestamp ASC, rev_id ASC',
-                                       'IGNORE INDEX' => 'rev_timestamp', // See T159319
-                               ]
+                                       'IGNORE INDEX' => [ 'revision' => 'rev_timestamp' ], // See T159319
+                               ],
+                               $revQuery['joins']
                        );
                        if ( $row ) {
                                return new Revision( $row );
@@ -4549,16 +4633,18 @@ class Title implements LinkTarget {
         * on the number of links. Typically called on create and delete.
         */
        public function touchLinks() {
-               DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'pagelinks' ) );
+               DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'pagelinks', 'page-touch' ) );
                if ( $this->getNamespace() == NS_CATEGORY ) {
-                       DeferredUpdates::addUpdate( new HTMLCacheUpdate( $this, 'categorylinks' ) );
+                       DeferredUpdates::addUpdate(
+                               new HTMLCacheUpdate( $this, 'categorylinks', 'category-touch' )
+                       );
                }
        }
 
        /**
         * Get the last touched timestamp
         *
-        * @param IDatabase $db Optional db
+        * @param IDatabase|null $db
         * @return string|false Last-touched timestamp
         */
        public function getTouched( $db = null ) {