Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
78.08% covered (warning)
78.08%
114 / 146
33.33% covered (danger)
33.33%
5 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
Title
78.08% covered (warning)
78.08%
114 / 146
33.33% covered (danger)
33.33%
5 / 15
109.49
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 newFromText
96.94% covered (success)
96.94%
95 / 98
0.00% covered (danger)
0.00%
0 / 1
42
 getInterwiki
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getKey
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getDBkey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPrefixedDBKey
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getPrefixedText
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getNamespace
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNamespaceName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFragment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 equals
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 isSpecialPage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 fixSpecialName
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 createFragmentTarget
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 newFromLinkTarget
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2declare( strict_types = 1 );
3
4namespace Wikimedia\Parsoid\Utils;
5
6use Wikimedia\Assert\Assert;
7use Wikimedia\IPUtils;
8use Wikimedia\Parsoid\Config\SiteConfig;
9use Wikimedia\Parsoid\Core\LinkTarget;
10use Wikimedia\Parsoid\Core\LinkTargetTrait;
11
12class Title implements LinkTarget {
13    use LinkTargetTrait;
14
15    /** @var string */
16    private $interwiki;
17
18    /** @var int */
19    private $namespaceId;
20
21    /** @var string */
22    private $namespaceName;
23
24    /** @var string */
25    private $dbkey;
26
27    /** @var string */
28    private $fragment;
29
30    // cached values of prefixed title/key
31    private ?string $prefixedDBKey = null;
32    private ?string $prefixedText = null;
33
34    /**
35     * @param string $interwiki Interwiki prefix, or empty string if none
36     * @param string $key Page DBkey (with underscores, not spaces)
37     * @param int $namespaceId
38     * @param string $namespaceName (with spaces, not underscores)
39     * @param ?string $fragment
40     */
41    private function __construct(
42        string $interwiki, string $key, int $namespaceId, string $namespaceName, ?string $fragment = null
43    ) {
44        $this->interwiki = $interwiki;
45        $this->dbkey = $key;
46        $this->namespaceId = $namespaceId;
47        $this->namespaceName = $namespaceName;
48        $this->fragment = $fragment ?? '';
49    }
50
51    public static function newFromText(
52        string $title, SiteConfig $siteConfig, ?int $defaultNs = null
53    ): Title {
54        if ( $defaultNs === null ) {
55            $defaultNs = 0;
56        }
57        $origTitle = $title;
58
59        if ( !mb_check_encoding( $title, 'UTF-8' ) ) {
60            throw new TitleException( "Bad UTF-8 in title \"$origTitle\"", 'title-invalid-utf8', $origTitle );
61        }
62
63        // Strip Unicode bidi override characters.
64        $title = preg_replace( '/[\x{200E}\x{200F}\x{202A}-\x{202E}]+/u', '', $title );
65        if ( $title === null ) {
66            throw new TitleException( "Bad UTF-8 in title \"$origTitle\"", 'title-invalid-utf8', $origTitle );
67        }
68
69        // Clean up whitespace
70        $title = preg_replace(
71            '/[ _\x{00A0}\x{1680}\x{180E}\x{2000}-\x{200A}\x{2028}\x{2029}\x{202F}\x{205F}\x{3000}]+/u',
72            '_', $title
73        );
74        // Trim _ from beginning and end
75        $title = trim( $title, '_' );
76
77        if ( str_contains( $title, \UtfNormal\Constants::UTF8_REPLACEMENT ) ) {
78            throw new TitleException( "Bad UTF-8 in title \"$title\"", 'title-invalid-utf8', $title );
79        }
80
81        // Initial colon indicates main namespace rather than specified default
82        // but should not create invalid {ns,title} pairs such as {0,Project:Foo}
83        if ( $title !== '' && $title[0] === ':' ) {
84            $title = ltrim( substr( $title, 1 ), '_' );
85            $defaultNs = 0;
86        }
87
88        if ( $title === '' ) {
89            throw new TitleException( 'Empty title', 'title-invalid-empty', $title );
90        }
91
92        $ns = $defaultNs;
93        $interwiki = null;
94
95        # Namespace or interwiki prefix
96        $prefixRegexp = "/^(.+?)_*:_*(.*)$/S";
97        // MediaWikiTitleCodec::splitTitleString wraps a loop around the
98        // next section, to allow it to repeat this prefix processing if
99        // an interwiki prefix is found which points at the local wiki.
100        $m = [];
101        if ( preg_match( $prefixRegexp, $title, $m ) ) {
102            $p = $m[1];
103            $nsId = $siteConfig->canonicalNamespaceId( $p ) ??
104                $siteConfig->namespaceId( $p );
105            if ( $nsId !== null ) {
106                $title = $m[2];
107                $ns = $nsId;
108                # For Talk:X pages, check if X has a "namespace" prefix
109                if (
110                    $nsId === $siteConfig->canonicalNamespaceId( 'talk' ) &&
111                    preg_match( $prefixRegexp, $title, $x )
112                ) {
113                    if ( $siteConfig->namespaceId( $x[1] ) ) {
114                        // Disallow Talk:File:x type titles.
115                        throw new TitleException(
116                            "Invalid Talk namespace title \"$origTitle\"", 'title-invalid-talk-namespace', $title
117                        );
118                    } elseif ( $siteConfig->interwikiMapNoNamespaces()[$x[1]] ?? null ) {
119                        // Disallow Talk:Interwiki:x type titles.
120                        throw new TitleException(
121                            "Invalid Talk namespace title \"$origTitle\"", 'title-invalid-talk-namespace', $title
122                        );
123                    }
124                }
125            } elseif ( $siteConfig->interwikiMapNoNamespaces()[$p] ?? null ) {
126                # Interwiki link
127                $title = $m[2];
128                $interwiki = strtolower( $p );
129
130                # We don't check for a redundant interwiki prefix to the
131                # local wiki, like core does here in
132                # MediaWikiTitleCodec::splitTitleString;
133                # core then does a `continue` to repeat the processing
134
135                // If there's an initial colon after the interwiki, that also
136                // resets the default namespace
137                if ( $title !== '' && $title[0] === ':' ) {
138                    $title = trim( substr( $title, 1 ), '_' );
139                    $ns = 0;
140                }
141            }
142            # If there's no recognized interwiki or namespace,
143            # then let the colon expression be part of the title
144        }
145
146        $fragment = null;
147        $fragmentIndex = strpos( $title, '#' );
148        if ( $fragmentIndex !== false ) {
149            $fragment = substr( $title, $fragmentIndex + 1 );
150            $title = rtrim( substr( $title, 0, $fragmentIndex ), '_' );
151        }
152
153        $illegalCharsRe = '/[^' . $siteConfig->legalTitleChars() . ']'
154            // URL percent encoding sequences interfere with the ability
155            // to round-trip titles -- you can't link to them consistently.
156            . '|%[0-9A-Fa-f]{2}'
157            // XML/HTML character references produce similar issues.
158            . '|&[A-Za-z0-9\x80-\xff]+;/S';
159        if ( preg_match( $illegalCharsRe, $title ) ) {
160            throw new TitleException(
161                "Invalid characters in title \"$origTitle\"", 'title-invalid-characters', $title
162            );
163        }
164
165        // Pages with "/./" or "/../" appearing in the URLs will often be
166        // unreachable due to the way web browsers deal with 'relative' URLs.
167        // Also, they conflict with subpage syntax. Forbid them explicitly.
168        if ( str_contains( $title, '.' ) && (
169            $title === '.' || $title === '..' ||
170            str_starts_with( $title, './' ) ||
171            str_starts_with( $title, '../' ) ||
172            str_contains( $title, '/./' ) ||
173            str_contains( $title, '/../' ) ||
174            str_ends_with( $title, '/.' ) ||
175            str_ends_with( $title, '/..' )
176        ) ) {
177            throw new TitleException(
178                "Title \"$origTitle\" contains relative path components", 'title-invalid-relative', $title
179            );
180        }
181
182        // Magic tilde sequences? Nu-uh!
183        if ( str_contains( $title, '~~~' ) ) {
184            throw new TitleException(
185                "Title \"$origTitle\" contains ~~~", 'title-invalid-magic-tilde', $title
186            );
187        }
188
189        $maxLength = $ns === $siteConfig->canonicalNamespaceId( 'special' ) ? 512 : 255;
190        if ( strlen( $title ) > $maxLength ) {
191            throw new TitleException(
192                "Title \"$origTitle\" is too long", 'title-invalid-too-long', $title
193            );
194        }
195
196        if ( $interwiki === null && $siteConfig->namespaceCase( $ns ) === 'first-letter' ) {
197            $title = $siteConfig->ucfirst( $title );
198        }
199
200        # Can't make a link to a namespace alone... "empty" local links can only be
201        # self-links with a fragment identifier.