Merge "Show a warning in edit preview when a template loop is detected"
[lhc/web/wiklou.git] / includes / Title.php
index c9f09f7..3da6ab9 100644 (file)
@@ -253,6 +253,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
@@ -272,7 +275,7 @@ class Title implements LinkTarget {
                }
 
                try {
-                       return Title::newFromTextThrow( strval( $text ), $defaultNamespace );
+                       return self::newFromTextThrow( strval( $text ), $defaultNamespace );
                } catch ( MalformedTitleException $ex ) {
                        return null;
                }
@@ -284,6 +287,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
@@ -411,7 +417,7 @@ class Title implements LinkTarget {
                        __METHOD__
                );
                if ( $row !== false ) {
-                       $title = Title::newFromRow( $row );
+                       $title = self::newFromRow( $row );
                } else {
                        $title = null;
                }
@@ -439,7 +445,7 @@ class Title implements LinkTarget {
 
                $titles = [];
                foreach ( $res as $row ) {
-                       $titles[] = Title::newFromRow( $row );
+                       $titles[] = self::newFromRow( $row );
                }
                return $titles;
        }
@@ -500,10 +506,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 +544,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,12 +555,15 @@ 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;
                }
 
                $t = new Title();
-               $t->mDbkeyform = Title::makeName( $ns, $title, $fragment, $interwiki, true );
+               $t->mDbkeyform = self::makeName( $ns, $title, $fragment, $interwiki, true );
 
                try {
                        $t->secureAndSplit();
@@ -557,10 +579,10 @@ class Title implements LinkTarget {
         * @return Title The new object
         */
        public static function newMainPage() {
-               $title = Title::newFromText( wfMessage( 'mainpage' )->inContentLanguage()->text() );
+               $title = self::newFromText( wfMessage( 'mainpage' )->inContentLanguage()->text() );
                // Don't give fatal errors if the message is broken
                if ( !$title ) {
-                       $title = Title::newFromText( 'Main Page' );
+                       $title = self::newFromText( 'Main Page' );
                }
                return $title;
        }
@@ -748,6 +770,8 @@ class Title implements LinkTarget {
        /**
         * Escape a text fragment, say from a link, for a URL
         *
+        * @deprecated since 1.30, use Sanitizer::escapeIdForLink() or escapeIdForExternalInterwiki()
+        *
         * @param string $fragment Containing a URL or link fragment (after the "#")
         * @return string Escaped string
         */
@@ -775,6 +799,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
@@ -933,7 +987,7 @@ class Title implements LinkTarget {
         */
        public function getContentModel( $flags = 0 ) {
                if ( !$this->mForcedContentModel
-                       && ( !$this->mContentModel || $flags === Title::GAID_FOR_UPDATE )
+                       && ( !$this->mContentModel || $flags === self::GAID_FOR_UPDATE )
                        && $this->getArticleID( $flags )
                ) {
                        $linkCache = LinkCache::singleton();
@@ -1020,12 +1074,26 @@ class Title implements LinkTarget {
        }
 
        /**
-        * Could this title have a corresponding talk page?
+        * Can this title have a corresponding talk page?
         *
-        * @return bool
+        * @deprecated since 1.30, use canHaveTalkPage() instead.
+        *
+        * @return bool True if this title either is a talk page or can have a talk page associated.
         */
        public function canTalk() {
-               return MWNamespace::canTalk( $this->mNamespace );
+               return $this->canHaveTalkPage();
+       }
+
+       /**
+        * 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.
+        */
+       public function canHaveTalkPage() {
+               return MWNamespace::hasTalkNamespace( $this->mNamespace );
        }
 
        /**
@@ -1083,7 +1151,7 @@ class Title implements LinkTarget {
                        if ( $canonicalName ) {
                                $localName = SpecialPageFactory::getLocalNameFor( $canonicalName, $par );
                                if ( $localName != $this->mDbkeyform ) {
-                                       return Title::makeTitle( NS_SPECIAL, $localName );
+                                       return self::makeTitle( NS_SPECIAL, $localName );
                                }
                        }
                }
@@ -1182,7 +1250,7 @@ class Title implements LinkTarget {
         * @return bool
         */
        public function isMainPage() {
-               return $this->equals( Title::newMainPage() );
+               return $this->equals( self::newMainPage() );
        }
 
        /**
@@ -1300,7 +1368,24 @@ class Title implements LinkTarget {
         * @return Title The object for the talk page
         */
        public function getTalkPage() {
-               return Title::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() );
+               return self::makeTitle( MWNamespace::getTalk( $this->getNamespace() ), $this->getDBkey() );
+       }
+
+       /**
+        * Get a Title object associated with the talk page of this article,
+        * if such a talk page can exist.
+        *
+        * @since 1.30
+        *
+        * @return Title|null The object for the talk page,
+        *         or null if no associated talk page can exist, according to canHaveTalkPage().
+        */
+       public function getTalkPageIfDefined() {
+               if ( !$this->canHaveTalkPage() ) {
+                       return null;
+               }
+
+               return $this->getTalkPage();
        }
 
        /**
@@ -1315,7 +1400,7 @@ class Title implements LinkTarget {
                if ( $this->getNamespace() == $subjectNS ) {
                        return $this;
                }
-               return Title::makeTitle( $subjectNS, $this->getDBkey() );
+               return self::makeTitle( $subjectNS, $this->getDBkey() );
        }
 
        /**
@@ -1323,7 +1408,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() {
@@ -1333,6 +1418,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();
                }
        }
@@ -1369,14 +1457,16 @@ class Title implements LinkTarget {
 
        /**
         * Get the fragment in URL form, including the "#" character if there is one
+        *
         * @return string Fragment in URL form
         */
        public function getFragmentForURL() {
                if ( !$this->hasFragment() ) {
                        return '';
-               } else {
-                       return '#' . Title::escapeFragmentForURL( $this->getFragment() );
+               } elseif ( $this->isExternal() && !$this->getTransWikiID() ) {
+                       return '#' . Sanitizer::escapeIdForExternalInterwiki( $this->getFragment() );
                }
+               return '#' . Sanitizer::escapeIdForLink( $this->getFragment() );
        }
 
        /**
@@ -1522,7 +1612,7 @@ class Title implements LinkTarget {
         * @since 1.20
         */
        public function getRootTitle() {
-               return Title::makeTitle( $this->getNamespace(), $this->getRootText() );
+               return self::makeTitle( $this->getNamespace(), $this->getRootText() );
        }
 
        /**
@@ -1562,7 +1652,7 @@ class Title implements LinkTarget {
         * @since 1.20
         */
        public function getBaseTitle() {
-               return Title::makeTitle( $this->getNamespace(), $this->getBaseText() );
+               return self::makeTitle( $this->getNamespace(), $this->getBaseText() );
        }
 
        /**
@@ -1598,7 +1688,7 @@ class Title implements LinkTarget {
         * @since 1.20
         */
        public function getSubpage( $text ) {
-               return Title::makeTitleSafe( $this->getNamespace(), $this->getText() . '/' . $text );
+               return self::makeTitleSafe( $this->getNamespace(), $this->getText() . '/' . $text );
        }
 
        /**
@@ -1707,7 +1797,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;
@@ -1862,6 +1952,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 ) {
@@ -1883,6 +1975,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
         */
@@ -2633,24 +2727,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;
        }
@@ -2834,7 +2937,7 @@ class Title implements LinkTarget {
                                        $page_id = $row->pr_page;
                                        $page_ns = $row->page_namespace;
                                        $page_title = $row->page_title;
-                                       $sources[$page_id] = Title::makeTitle( $page_ns, $page_title );
+                                       $sources[$page_id] = self::makeTitle( $page_ns, $page_title );
                                        # Add groups needed for each restriction type if its not already there
                                        # Make sure this restriction type still exists
 
@@ -3159,7 +3262,7 @@ class Title implements LinkTarget {
                if ( $limit > -1 ) {
                        $options['LIMIT'] = $limit;
                }
-               $this->mSubpages = TitleArray::newFromResult(
+               return TitleArray::newFromResult(
                        $dbr->select( 'page',
                                [ 'page_id', 'page_namespace', 'page_title', 'page_is_redirect' ],
                                $conds,
@@ -3167,7 +3270,6 @@ class Title implements LinkTarget {
                                $options
                        )
                );
-               return $this->mSubpages;
        }
 
        /**
@@ -3316,7 +3418,7 @@ class Title implements LinkTarget {
         * @return int Int or 0 if the page doesn't exist
         */
        public function getLatestRevID( $flags = 0 ) {
-               if ( !( $flags & Title::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) {
+               if ( !( $flags & self::GAID_FOR_UPDATE ) && $this->mLatestID !== false ) {
                        return intval( $this->mLatestID );
                }
                if ( !$this->getArticleID( $flags ) ) {
@@ -3476,7 +3578,7 @@ class Title implements LinkTarget {
                if ( $res->numRows() ) {
                        $linkCache = LinkCache::singleton();
                        foreach ( $res as $row ) {
-                               $titleObj = Title::makeTitle( $row->page_namespace, $row->page_title );
+                               $titleObj = self::makeTitle( $row->page_namespace, $row->page_title );
                                if ( $titleObj ) {
                                        $linkCache->addGoodLinkObjFromRow( $titleObj, $row );
                                        $retVal[] = $titleObj;
@@ -3544,9 +3646,9 @@ class Title implements LinkTarget {
                $linkCache = LinkCache::singleton();
                foreach ( $res as $row ) {
                        if ( $row->page_id ) {
-                               $titleObj = Title::newFromRow( $row );
+                               $titleObj = self::newFromRow( $row );
                        } else {
-                               $titleObj = Title::makeTitle( $row->$blNamespace, $row->$blTitle );
+                               $titleObj = self::makeTitle( $row->$blNamespace, $row->$blTitle );
                                $linkCache->addBadLinkObj( $titleObj );
                        }
                        $retVal[] = $titleObj;
@@ -3602,7 +3704,7 @@ class Title implements LinkTarget {
 
                $retVal = [];
                foreach ( $res as $row ) {
-                       $retVal[] = Title::makeTitle( $row->pl_namespace, $row->pl_title );
+                       $retVal[] = self::makeTitle( $row->pl_namespace, $row->pl_title );
                }
                return $retVal;
        }
@@ -3660,7 +3762,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
@@ -3712,7 +3814,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
@@ -3722,8 +3824,8 @@ class Title implements LinkTarget {
         * @return array|bool True on success, getUserPermissionsErrors()-like array on failure
         */
        public function moveTo( &$nt, $auth = true, $reason = '', $createRedirect = true,
-               array $changeTags = [] ) {
-
+               array $changeTags = []
+       ) {
                global $wgUser;
                $err = $this->isValidMoveOperation( $nt, $auth, $reason );
                if ( is_array( $err ) ) {
@@ -3760,8 +3862,8 @@ class Title implements LinkTarget {
         *     no pages were moved
         */
        public function moveSubpages( $nt, $auth = true, $reason = '', $createRedirect = true,
-               array $changeTags = [] ) {
-
+               array $changeTags = []
+       ) {
                global $wgMaximumMovedPages;
                // Check permissions
                if ( !$this->userCan( 'move-subpages' ) ) {
@@ -3814,7 +3916,7 @@ class Title implements LinkTarget {
                        }
                        # T16385: we need makeTitleSafe because the new page names may
                        # be longer than 255 characters.
-                       $newSubpage = Title::makeTitleSafe( $newNs, $newPageName );
+                       $newSubpage = self::makeTitleSafe( $newNs, $newPageName );
 
                        $success = $oldSubpage->moveTo( $newSubpage, $auth, $reason, $createRedirect, $changeTags );
                        if ( $success === true ) {
@@ -3976,7 +4078,7 @@ class Title implements LinkTarget {
                                        # Circular reference
                                        $stack[$parent] = [];
                                } else {
-                                       $nt = Title::newFromText( $parent );
+                                       $nt = self::newFromText( $parent );
                                        if ( $nt ) {
                                                $stack[$parent] = $nt->getParentCategoryTree( $children + [ $parent => 1 ] );
                                        }