Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
57.78% |
683 / 1182 |
|
53.94% |
89 / 165 |
CRAP | |
0.00% |
0 / 1 |
Title | |
57.78% |
683 / 1182 |
|
53.94% |
89 / 165 |
16662.89 | |
0.00% |
0 / 1 |
getLanguageConverter | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getPageLanguageConverter | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDbProvider | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTitleFormatter | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getInterwikiLookup | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
newFromDBkey | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
newFromLinkTarget | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
castFromLinkTarget | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
newFromPageIdentity | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
castFromPageIdentity | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
newFromPageReference | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
castFromPageReference | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
newFromText | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
56 | |||
newFromTextThrow | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
42 | |||
uncache | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
newFromURL | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
getTitleCache | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
newFromID | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
2 | |||
newFromRow | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
loadFromRow | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
72 | |||
makeTitle | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
makeTitleSafe | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
newMainPage | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
legalChars | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
convertByteClassToUnicodeClass | |
91.23% |
52 / 57 |
|
0.00% |
0 / 1 |
20.27 | |||
makeName | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
6 | |||
compare | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
isValid | |
100.00% |
17 / 17 |
|
100.00% |
1 / 1 |
8 | |||
isLocal | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
getInterwiki | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
wasLocalInterwiki | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
isTrans | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getTransWikiID | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getTitleValue | |
72.73% |
8 / 11 |
|
0.00% |
0 / 1 |
3.18 | |||
getText | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getPartialURL | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDBkey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getNamespace | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
shouldReadLatest | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getContentModel | |
90.91% |
10 / 11 |
|
0.00% |
0 / 1 |
6.03 | |||
hasContentModel | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setContentModel | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
2.03 | |||
lazyFillContentModel | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
12 | |||
getNsText | |
66.67% |
8 / 12 |
|
0.00% |
0 / 1 |
5.93 | |||
getSubjectNsText | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getTalkNsText | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
canHaveTalkPage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
canExist | |
81.82% |
9 / 11 |
|
0.00% |
0 / 1 |
6.22 | |||
isSpecialPage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isSpecial | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
fixSpecialName | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
4.03 | |||
inNamespace | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
inNamespaces | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
5 | |||
hasSubjectNamespace | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
isContentPage | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
isMovable | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
isMainPage | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
isSubpage | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
isConversionTable | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
isWikitextPage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isSiteConfigPage | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
isUserConfigPage | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
getSkinFromConfigSubpage | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
3 | |||
isUserCssConfigPage | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
isUserJsonConfigPage | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
isUserJsConfigPage | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
3 | |||
isSiteCssConfigPage | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
isSiteJsonConfigPage | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
isSiteJsConfigPage | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
3 | |||
isRawHtmlMessage | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
isTalkPage | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getTalkPage | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
getTalkPageIfDefined | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getSubjectPage | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
warnIfPageCannotExist | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
getOtherPage | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
4 | |||
getFragment | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFragmentForURL | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
5 | |||
setFragment | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
createFragmentTarget | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
normalizeFragment | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
prefix | |
80.00% |
8 / 10 |
|
0.00% |
0 / 1 |
4.13 | |||
getPrefixedDBkey | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getPrefixedText | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
__toString | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getFullText | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
findSubpageDivider | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
hasSubpagesEnabled | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getRootText | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
3.03 | |||
getRootTitle | |
50.00% |
5 / 10 |
|
0.00% |
0 / 1 |
4.12 | |||
getBaseText | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
3.03 | |||
getBaseTitle | |
50.00% |
5 / 10 |
|
0.00% |
0 / 1 |
4.12 | |||
getSubpageText | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getSubpage | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
1 | |||
getSubpageUrlForm | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getPrefixedURL | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getFullURL | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
getFullUrlForRedirect | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
getLocalURL | |
95.45% |
42 / 44 |
|
0.00% |
0 / 1 |
20 | |||
getLinkURL | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
5 | |||
getInternalURL | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
getCanonicalURL | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
2 | |||
getEditURL | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getTitleProtection | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
deleteTitleProtection | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
loadRestrictions | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
flushRestrictions | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
purgeExpiredRestrictions | |
0.00% |
0 / 24 |
|
0.00% |
0 / 1 |
12 | |||
hasSubpages | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
getSubpages | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
isDeleted | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDeletedEditsCount | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
12 | |||
isDeletedQuick | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
hasDeletedEdits | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
20 | |||
getArticleID | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
5 | |||
isRedirect | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
getLength | |
80.00% |
4 / 5 |
|
0.00% |
0 / 1 |
4.13 | |||
getLatestRevID | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
4 | |||
resetArticleID | |
0.00% |
0 / 15 |
|
0.00% |
0 / 1 |
6 | |||
clearCaches | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
capitalize | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
secureAndSplit | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
4 | |||
getLinksTo | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
42 | |||
getTemplateLinksTo | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getLinksFrom | |
80.65% |
25 / 31 |
|
0.00% |
0 / 1 |
5.18 | |||
getTemplateLinksFrom | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isSingleRevRedirect | |
0.00% |
0 / 19 |
|
0.00% |
0 / 1 |
12 | |||
getParentCategories | |
82.61% |
19 / 23 |
|
0.00% |
0 / 1 |
5.13 | |||
getParentCategoryTree | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
30 | |||
pageCond | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
isNewPage | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
isBigDeletion | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
12 | |||
estimateRevisionCount | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
equals | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
4 | |||
isSamePageAs | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
isSubpageOf | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
exists | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
isAlwaysKnown | |
83.33% |
15 / 18 |
|
0.00% |
0 / 1 |
9.37 | |||
isKnown | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
hasSourceText | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
12 | |||
getDefaultMessageText | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getDefaultSystemMessage | |
87.50% |
7 / 8 |
|
0.00% |
0 / 1 |
3.02 | |||
invalidateCache | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
30 | |||
touchLinks | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
6 | |||
getTouched | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
getNamespaceKey | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
20 | |||
getRedirectsHere | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
20 | |||
isValidRedirectTarget | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
5 | |||
canUseNoindex | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getCategorySortkey | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
getDbPageLanguageCode | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
30 | |||
getDbPageLanguage | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
getPageLanguage | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
30 | |||
getPageViewLanguage | |
65.00% |
13 / 20 |
|
0.00% |
0 / 1 |
6.07 | |||
getEditNotices | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
42 | |||
getFieldFromPageStore | |
83.33% |
10 / 12 |
|
0.00% |
0 / 1 |
6.17 | |||
__sleep | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
2 | |||
__wakeup | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
__clone | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getWikiId | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getId | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
assertProperPage | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
toPageIdentity | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
toPageRecord | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | /** |
3 | * Representation of a title within MediaWiki. |
4 | * |
5 | * See Title.md |
6 | * |
7 | * This program is free software; you can redistribute it and/or modify |
8 | * it under the terms of the GNU General Public License as published by |
9 | * the Free Software Foundation; either version 2 of the License, or |
10 | * (at your option) any later version. |
11 | * |
12 | * This program is distributed in the hope that it will be useful, |
13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
15 | * GNU General Public License for more details. |
16 | * |
17 | * You should have received a copy of the GNU General Public License along |
18 | * with this program; if not, write to the Free Software Foundation, Inc., |
19 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
20 | * http://www.gnu.org/copyleft/gpl.html |
21 | * |
22 | * @file |
23 | */ |
24 | |
25 | namespace MediaWiki\Title; |
26 | |
27 | use HTMLCacheUpdateJob; |
28 | use InvalidArgumentException; |
29 | use MapCacheLRU; |
30 | use MediaWiki\Cache\LinkCache; |
31 | use MediaWiki\Context\RequestContext; |
32 | use MediaWiki\DAO\WikiAwareEntityTrait; |
33 | use MediaWiki\Deferred\AutoCommitUpdate; |
34 | use MediaWiki\Deferred\DeferredUpdates; |
35 | use MediaWiki\HookContainer\HookRunner; |
36 | use MediaWiki\Html\Html; |
37 | use MediaWiki\Interwiki\InterwikiLookup; |
38 | use MediaWiki\Language\ILanguageConverter; |
39 | use MediaWiki\Language\Language; |
40 | use MediaWiki\Linker\LinkTarget; |
41 | use MediaWiki\MainConfigNames; |
42 | use MediaWiki\MediaWikiServices; |
43 | use MediaWiki\Message\Message; |
44 | use MediaWiki\Page\ExistingPageRecord; |
45 | use MediaWiki\Page\PageIdentity; |
46 | use MediaWiki\Page\PageIdentityValue; |
47 | use MediaWiki\Page\PageReference; |
48 | use MediaWiki\Page\PageStoreRecord; |
49 | use MediaWiki\Page\ProperPageIdentity; |
50 | use MediaWiki\Page\WikiPage; |
51 | use MediaWiki\Parser\Sanitizer; |
52 | use MediaWiki\Request\PathRouter; |
53 | use MediaWiki\ResourceLoader\WikiModule; |
54 | use MediaWiki\SpecialPage\SpecialPage; |
55 | use MediaWiki\Utils\MWTimestamp; |
56 | use MessageLocalizer; |
57 | use MWException; |
58 | use RuntimeException; |
59 | use stdClass; |
60 | use Stringable; |
61 | use Wikimedia\Assert\Assert; |
62 | use Wikimedia\Assert\PreconditionException; |
63 | use Wikimedia\Parsoid\Core\LinkTarget as ParsoidLinkTarget; |
64 | use Wikimedia\Parsoid\Core\LinkTargetTrait; |
65 | use Wikimedia\Rdbms\DBAccessObjectUtils; |
66 | use Wikimedia\Rdbms\IConnectionProvider; |
67 | use Wikimedia\Rdbms\IDatabase; |
68 | use Wikimedia\Rdbms\IDBAccessObject; |
69 | |
70 | /** |
71 | * Represents a title within MediaWiki. |
72 | * Optionally may contain an interwiki designation or namespace. |
73 | * @note This class can fetch various kinds of data from the database; |
74 | * however, it does so inefficiently. |
75 | * @note Consider using a TitleValue object instead. TitleValue is more lightweight |
76 | * and does not rely on global state or the database. |
77 | */ |
78 | class Title implements Stringable, LinkTarget, PageIdentity { |
79 | use WikiAwareEntityTrait; |
80 | use LinkTargetTrait; |
81 | |
82 | /** @var MapCacheLRU|null */ |
83 | private static $titleCache = null; |
84 | |
85 | /** |
86 | * Title::newFromText maintains a cache to avoid expensive re-normalization of |
87 | * commonly used titles. On a batch operation this can become a memory leak |
88 | * if not bounded. |
89 | */ |
90 | private const CACHE_MAX = 1000; |
91 | |
92 | /** |
93 | * Flag for use with factory methods like newFromLinkTarget() that have |
94 | * a $forceClone parameter. If set, the method must return a new instance. |
95 | * Without this flag, some factory methods may return existing instances.as |
96 | * |
97 | * @since 1.33 |
98 | */ |
99 | public const NEW_CLONE = 'clone'; |
100 | |
101 | /** @var string Text form (spaces not underscores) of the main part */ |
102 | private $mTextform = ''; |
103 | |
104 | /** @var string URL-encoded form of the main part */ |
105 | private $mUrlform = ''; |
106 | |
107 | /** @var string Main part with underscores */ |
108 | private $mDbkeyform = ''; |
109 | |
110 | /** @var int Namespace index, i.e. one of the NS_xxxx constants */ |
111 | private $mNamespace = NS_MAIN; |
112 | |
113 | /** @var string Interwiki prefix */ |
114 | private $mInterwiki = ''; |
115 | |
116 | /** @var bool Was this Title created from a string with a local interwiki prefix? */ |
117 | private $mLocalInterwiki = false; |
118 | |
119 | /** @var string Title fragment (i.e. the bit after the #) */ |
120 | private $mFragment = ''; |
121 | |
122 | /***************************************************************************/ |
123 | // region Private member variables |
124 | /** @name Private member variables |
125 | * Please use the accessor functions instead. |
126 | * @internal |
127 | * @{ |
128 | */ |
129 | |
130 | /** @var int Article ID, fetched from the link cache on demand */ |
131 | public $mArticleID = -1; |
132 | |
133 | /** @var int|false ID of most recent revision */ |
134 | protected $mLatestID = false; |
135 | |
136 | /** |
137 | * @var string|false ID of the page's content model, i.e. one of the |
138 | * CONTENT_MODEL_XXX constants |
139 | */ |
140 | private $mContentModel = false; |
141 | |
142 | /** |
143 | * @var bool If a content model was forced via setContentModel() |
144 | * this will be true to avoid having other code paths reset it |
145 | */ |
146 | private $mForcedContentModel = false; |
147 | |
148 | /** @var int|null Estimated number of revisions; null of not loaded */ |
149 | private $mEstimateRevisions; |
150 | |
151 | /** |
152 | * Text form including namespace/interwiki, initialised on demand |
153 | * |
154 | * Only public to share cache with TitleFormatter |
155 | * |
156 | * @internal |
157 | * @var string|null |
158 | */ |
159 | public $prefixedText = null; |
160 | |
161 | /** |
162 | * Namespace to assume when no namespace was passed to factory methods. |
163 | * This must be NS_MAIN, as it's hardcoded in several places. See T2696. |
164 | * Used primarily for {{transclusion}} tags. |
165 | */ |
166 | private const DEFAULT_NAMESPACE = NS_MAIN; |
167 | |
168 | /** @var int The page length, 0 for special pages */ |
169 | protected $mLength = -1; |
170 | |
171 | /** @var null|bool Is the article at this title a redirect? */ |
172 | public $mRedirect = null; |
173 | |
174 | /** @var bool Whether a page has any subpages */ |
175 | private $mHasSubpages; |
176 | |
177 | /** @var array|null The (string) language code of the page's language and content code. */ |
178 | private $mPageLanguage; |
179 | |
180 | /** @var string|false|null The page language code from the database, null if not saved in |
181 | * the database or false if not loaded, yet. |
182 | */ |
183 | private $mDbPageLanguage = false; |
184 | |
185 | /** @var TitleValue|null */ |
186 | private $mTitleValue = null; |
187 | |
188 | /** @var bool|null Would deleting this page be a big deletion? */ |
189 | private $mIsBigDeletion = null; |
190 | |
191 | /** @var bool|null Is the title known to be valid? */ |
192 | private $mIsValid = null; |
193 | |
194 | /** @var string|null The key of this instance in the internal Title instance cache */ |
195 | private $mInstanceCacheKey = null; |
196 | |
197 | // endregion -- end of private member variables |
198 | /** @} */ |
199 | /***************************************************************************/ |
200 | |
201 | /** |
202 | * Shorthand for getting a Language Converter for specific language |
203 | * @param Language $language Language of converter |
204 | * @return ILanguageConverter |
205 | */ |
206 | private function getLanguageConverter( $language ): ILanguageConverter { |
207 | return MediaWikiServices::getInstance()->getLanguageConverterFactory() |
208 | ->getLanguageConverter( $language ); |
209 | } |
210 | |
211 | /** |
212 | * Shorthand for getting a Language Converter for page's language |
213 | */ |
214 | private function getPageLanguageConverter(): ILanguageConverter { |
215 | return $this->getLanguageConverter( $this->getPageLanguage() ); |
216 | } |
217 | |
218 | /** |
219 | * Shorthand for getting a database connection provider |
220 | */ |
221 | private function getDbProvider(): IConnectionProvider { |
222 | return MediaWikiServices::getInstance()->getConnectionProvider(); |
223 | } |
224 | |
225 | /** |
226 | * B/C kludge: provide a TitleParser for use by Title. |
227 | * Ideally, Title would have no methods that need this. |
228 | * Avoid usage of this singleton by using TitleValue |
229 | * and the associated services when possible. |
230 | * |
231 | * @return TitleFormatter |
232 | */ |
233 | private static function getTitleFormatter() { |
234 | return MediaWikiServices::getInstance()->getTitleFormatter(); |
235 | } |
236 | |
237 | /** |
238 | * B/C kludge: provide an InterwikiLookup for use by Title. |
239 | * Ideally, Title would have no methods that need this. |
240 | * Avoid usage of this singleton by using TitleValue |
241 | * and the associated services when possible. |
242 | * |
243 | * @return InterwikiLookup |
244 | */ |
245 | private static function getInterwikiLookup() { |
246 | return MediaWikiServices::getInstance()->getInterwikiLookup(); |
247 | } |
248 | |
249 | private function __construct() { |
250 | } |
251 | |
252 | /** |
253 | * Create a new Title from a prefixed DB key |
254 | * |
255 | * @param string $key The database key, which has underscores |
256 | * instead of spaces, possibly including namespace and |
257 | * interwiki prefixes |
258 | * @return Title|null Title, or null on an error |
259 | */ |
260 | public static function newFromDBkey( $key ) { |
261 | $t = new self(); |
262 | |
263 | try { |
264 | $t->secureAndSplit( $key ); |
265 | return $t; |
266 | } catch ( MalformedTitleException $ex ) { |
267 | return null; |
268 | } |
269 | } |
270 | |
271 | /** |
272 | * Returns a Title given a LinkTarget. |
273 | * If the given LinkTarget is already a Title instance, that instance is returned, |
274 | * unless $forceClone is "clone". If $forceClone is "clone" and the given LinkTarget |
275 | * is already a Title instance, that instance is copied using the clone operator. |
276 | * |
277 | * @since 1.27 |
278 | * @param ParsoidLinkTarget $linkTarget Assumed to be safe. |
279 | * @param string $forceClone set to NEW_CLONE to ensure a fresh instance is returned. |
280 | * @return Title |
281 | */ |
282 | public static function newFromLinkTarget( ParsoidLinkTarget $linkTarget, $forceClone = '' ) { |
283 | if ( $linkTarget instanceof Title ) { |
284 | // Special case if it's already a Title object |
285 | if ( $forceClone === self::NEW_CLONE ) { |
286 | return clone $linkTarget; |
287 | } else { |
288 | return $linkTarget; |
289 | } |
290 | } |
291 | return self::makeTitle( |
292 | $linkTarget->getNamespace(), |
293 | $linkTarget->getText(), |
294 | $linkTarget->getFragment(), |
295 | $linkTarget->getInterwiki() |
296 | ); |
297 | } |
298 | |
299 | /** |
300 | * Same as newFromLinkTarget(), but if passed null, returns null. |
301 | * |
302 | * @since 1.34 |
303 | * @param ParsoidLinkTarget|null $linkTarget Assumed to be safe (if not null). |
304 | * @return Title|null |
305 | */ |
306 | public static function castFromLinkTarget( ?ParsoidLinkTarget $linkTarget ) { |
307 | if ( !$linkTarget ) { |
308 | return null; |
309 | } |
310 | return self::newFromLinkTarget( $linkTarget ); |
311 | } |
312 | |
313 | /** |
314 | * Return a Title for a given PageIdentity. If $pageIdentity is a Title, |
315 | * that Title is returned unchanged. |
316 | * |
317 | * @since 1.41 |
318 | * @param PageIdentity $pageIdentity |
319 | * @return Title |
320 | */ |
321 | public static function newFromPageIdentity( PageIdentity $pageIdentity ): Title { |
322 | return self::newFromPageReference( $pageIdentity ); |
323 | } |
324 | |
325 | /** |
326 | * Same as newFromPageIdentity(), but if passed null, returns null. |
327 | * |
328 | * @since 1.36 |
329 | * @param PageIdentity|null $pageIdentity |
330 | * @return Title|null |
331 | */ |
332 | public static function castFromPageIdentity( ?PageIdentity $pageIdentity ): ?Title { |
333 | return self::castFromPageReference( $pageIdentity ); |
334 | } |
335 | |
336 | /** |
337 | * Return a Title for a given Reference. If $pageReference is a Title, |
338 | * that Title is returned unchanged. |
339 | * |
340 | * @since 1.41 |
341 | * @param PageReference $pageReference |
342 | * @return Title |
343 | */ |
344 | public static function newFromPageReference( PageReference $pageReference ): Title { |
345 | if ( $pageReference instanceof Title ) { |
346 | return $pageReference; |
347 | } |
348 | |
349 | $pageReference->assertWiki( self::LOCAL ); |
350 | $title = self::makeTitle( $pageReference->getNamespace(), $pageReference->getDBkey() ); |
351 | |
352 | if ( $pageReference instanceof PageIdentity ) { |
353 | $title->mArticleID = $pageReference->getId(); |
354 | } |
355 | return $title; |
356 | } |
357 | |
358 | /** |
359 | * Same as newFromPageReference(), but if passed null, returns null. |
360 | * |
361 | * @since 1.37 |
362 | * @param PageReference|null $pageReference |
363 | * @return Title|null |
364 | */ |
365 | public static function castFromPageReference( ?PageReference $pageReference ): ?Title { |
366 | if ( !$pageReference ) { |
367 | return null; |
368 | } |
369 | return self::newFromPageReference( $pageReference ); |
370 | } |
371 | |
372 | /** |
373 | * Create a new Title from text, such as what one would find in a link. |
374 | * Decodes any HTML entities in the text. |
375 | * Titles returned by this method are guaranteed to be valid. |
376 | * Call canExist() to check if the Title represents an editable page. |
377 | * |
378 | * @note The Title instance returned by this method is not guaranteed to be a fresh instance. |
379 | * It may instead be a cached instance created previously, with references to it remaining |
380 | * elsewhere. |
381 | * |
382 | * @param string|int|null $text The link text; spaces, prefixes, and an |
383 | * initial ':' indicating the main namespace are accepted. |
384 | * @param int $defaultNamespace The namespace to use if none is specified |
385 | * by a prefix. If you want to force a specific namespace even if |
386 | * $text might begin with a namespace prefix, use makeTitle() or |
387 | * makeTitleSafe(). |
388 | * @return Title|null Title or null if the Title could not be parsed because |
389 | * it is invalid. |
390 | */ |
391 | public static function newFromText( $text, $defaultNamespace = NS_MAIN ) { |
392 | // DWIM: Integers can be passed in here when page titles are used as array keys. |
393 | if ( $text !== null && !is_string( $text ) && !is_int( $text ) ) { |
394 | throw new InvalidArgumentException( '$text must be a string.' ); |
395 | } |
396 | if ( $text === null || $text === '' ) { |
397 | return null; |
398 | } |
399 | |
400 | try { |
401 | return self::newFromTextThrow( (string)$text, (int)$defaultNamespace ); |
402 | } catch ( MalformedTitleException $ex ) { |
403 | return null; |
404 | } |
405 | } |
406 | |
407 | /** |
408 | * Like Title::newFromText(), but throws MalformedTitleException when the title is invalid, |
409 | * rather than returning null. |
410 | * |
411 | * Titles returned by this method are guaranteed to be valid. |
412 | * Call canExist() to check if the Title represents an editable page. |
413 | * |
414 | * @note The Title instance returned by this method is not guaranteed to be a fresh instance. |
415 | * It may instead be a cached instance created previously, with references to it remaining |
416 | * elsewhere. |
417 | * |
418 | * @see Title::newFromText |
419 | * |
420 | * @since 1.25 |
421 | * @param string $text Title text to check |
422 | * @param int $defaultNamespace |
423 | * @throws MalformedTitleException If the title is invalid. |
424 | * @return Title |
425 | */ |
426 | public static function newFromTextThrow( $text, $defaultNamespace = NS_MAIN ) { |
427 | if ( is_object( $text ) ) { |
428 | throw new InvalidArgumentException( '$text must be a string, given an object' ); |
429 | } elseif ( $text === null ) { |
430 | // Legacy code relies on MalformedTitleException being thrown in this case |
431 | // TODO: fix(happens when URL with no title in it is parsed). |
432 | throw new MalformedTitleException( 'title-invalid-empty' ); |
433 | } |
434 | |
435 | $titleCache = self::getTitleCache(); |
436 | |
437 | // Wiki pages often contain multiple links to the same page. |
438 | // Title normalization and parsing can become expensive on pages with many |
439 | // links, so we can save a little time by caching them. |
440 | if ( $defaultNamespace === NS_MAIN ) { |
441 | $t = $titleCache->get( $text ); |
442 | if ( $t ) { |
443 | return $t; |
444 | } |
445 | } |
446 | |
447 | // Convert things like é ā or 〗 into normalized (T16952) text |
448 | $filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text ); |
449 | |
450 | $t = new Title(); |
451 | $dbKeyForm = strtr( $filteredText, ' ', '_' ); |
452 | |
453 | $t->secureAndSplit( $dbKeyForm, (int)$defaultNamespace ); |
454 | if ( $defaultNamespace === NS_MAIN ) { |
455 | $t->mInstanceCacheKey = $text; |
456 | $titleCache->set( $text, $t ); |
457 | } |
458 | return $t; |
459 | } |
460 | |
461 | /** |
462 | * Removes this instance from the internal title cache, so it can be modified in-place |
463 | * without polluting the cache (see T281337). |
464 | */ |
465 | private function uncache() { |
466 | if ( $this->mInstanceCacheKey !== null ) { |
467 | $titleCache = self::getTitleCache(); |
468 | $titleCache->clear( $this->mInstanceCacheKey ); |
469 | $this->mInstanceCacheKey = null; |
470 | } |
471 | } |
472 | |
473 | /** |
474 | * THIS IS NOT THE FUNCTION YOU WANT. Use Title::newFromText(). |
475 | * |
476 | * Example of wrong and broken code: |
477 | * $title = Title::newFromURL( $request->getText( 'title' ) ); |
478 | * |
479 | * Example of right code: |
480 | * $title = Title::newFromText( $request->getText( 'title' ) ); |
481 | * |
482 | * Create a new Title from URL-encoded text. Ensures that |
483 | * the given title's length does not exceed the maximum. |
484 | * |
485 | * @param string $url The title, as might be taken from a URL |
486 | * @return Title|null The new object, or null on an error |
487 | */ |
488 | public static function newFromURL( $url ) { |
489 | $t = new Title(); |
490 | |
491 | # For compatibility with old buggy URLs. "+" is usually not valid in titles, |
492 | # but some URLs used it as a space replacement and they still come |
493 | # from some external search tools. |
494 | if ( !str_contains( self::legalChars(), '+' ) ) { |
495 | $url = strtr( $url, '+', ' ' ); |
496 | } |
497 | |
498 | $dbKeyForm = strtr( $url, ' ', '_' ); |
499 | |
500 | try { |
501 | $t->secureAndSplit( $dbKeyForm ); |
502 | return $t; |
503 | } catch ( MalformedTitleException $ex ) { |
504 | return null; |
505 | } |
506 | } |
507 | |
508 | /** |
509 | * @return MapCacheLRU |
510 | */ |
511 | private static function getTitleCache() { |
512 | if ( self::$titleCache === null ) { |
513 | self::$titleCache = new MapCacheLRU( self::CACHE_MAX ); |
514 | } |
515 | return self::$titleCache; |
516 | } |
517 | |
518 | /** |
519 | * Create a new Title from an article ID |
520 | * |
521 | * @param int $id The page_id corresponding to the Title to create |
522 | * @param int $flags Bitfield of IDBAccessObject::READ_* constants |
523 | * @return Title|null The new object, or null on an error |
524 | */ |
525 | public static function newFromID( $id, $flags = 0 ) { |
526 | $pageStore = MediaWikiServices::getInstance()->getPageStore(); |
527 | $dbr = DBAccessObjectUtils::getDBFromRecency( |
528 | MediaWikiServices::getInstance()->getConnectionProvider(), |
529 | $flags |
530 | ); |
531 | $row = $dbr->newSelectQueryBuilder() |
532 | ->select( $pageStore->getSelectFields() ) |
533 | ->from( 'page' ) |
534 | ->where( [ 'page_id' => $id ] ) |
535 | ->recency( $flags ) |
536 | ->caller( __METHOD__ )->fetchRow(); |
537 | if ( $row !== false ) { |
538 | $title = self::newFromRow( $row ); |
539 | } else { |
540 | $title = null; |
541 | } |
542 | |
543 | return $title; |
544 | } |
545 | |
546 | /** |
547 | * Make a Title object from a DB row |
548 | * |
549 | * @param stdClass $row Object database row (needs at least page_title,page_namespace) |
550 | * @return Title |
551 | */ |
552 | public static function newFromRow( $row ) { |
553 | $t = self::makeTitle( $row->page_namespace, $row->page_title ); |
554 | $t->loadFromRow( $row ); |
555 | return $t; |
556 | } |
557 | |
558 | /** |
559 | * Load Title object fields from a DB row. |
560 | * If false is given, the title will be treated as non-existing. |
561 | * |
562 | * @param stdClass|false $row Database row |
563 | */ |
564 | public function loadFromRow( $row ) { |
565 | if ( $row ) { // page found |
566 | if ( isset( $row->page_id ) ) { |
567 | $this->mArticleID = (int)$row->page_id; |
568 | } |
569 | if ( isset( $row->page_len ) ) { |
570 | $this->mLength = (int)$row->page_len; |
571 | } |
572 | if ( isset( $row->page_is_redirect ) ) { |
573 | $this->mRedirect = (bool)$row->page_is_redirect; |
574 | } |
575 | if ( isset( $row->page_latest ) ) { |
576 | $this->mLatestID = (int)$row->page_latest; |
577 | } |
578 | if ( isset( $row->page_content_model ) ) { |
579 | $this->lazyFillContentModel( $row->page_content_model ); |
580 | } else { |
581 | $this->lazyFillContentModel( false ); // lazily-load getContentModel() |
582 | } |
583 | if ( isset( $row->page_lang ) ) { |
584 | $this->mDbPageLanguage = (string)$row->page_lang; |
585 | } |
586 | } else { // page not found |
587 | $this->mArticleID = 0; |
588 | $this->mLength = 0; |
589 | $this->mRedirect = false; |
590 | $this->mLatestID = 0; |
591 | $this->lazyFillContentModel( false ); // lazily-load getContentModel() |
592 | } |
593 | } |
594 | |
595 | /** |
596 | * Create a new Title from a namespace index and a DB key. |
597 | * |
598 | * It's assumed that $ns and $title are safe, for instance when |
599 | * they came directly from the database or a special page name, |
600 | * not from user input. |
601 | * |
602 | * No validation is applied. For convenience, spaces are normalized |
603 | * to underscores, so that e.g. user_text fields can be used directly. |
604 | * |
605 | * @note This method may return Title objects that are "invalid" |
606 | * according to the isValid() method. This is usually caused by |
607 | * configuration changes: e.g. a namespace that was once defined is |
608 | * no longer configured, or a character that was once allowed in |
609 | * titles is now forbidden. |
610 | * |
611 | * @param int $ns The namespace of the article |
612 | * @param string $title The unprefixed database key form |
613 | * @param string $fragment The link fragment (after the "#") |
614 | * @param string $interwiki The interwiki prefix |
615 | * @return Title The new object |
616 | */ |
617 | public static function makeTitle( $ns, $title, $fragment = '', $interwiki = '' ) { |
618 | $t = new Title(); |
619 | $t->mInterwiki = $interwiki; |
620 | $t->mFragment = self::normalizeFragment( $fragment ); |
621 | $t->mNamespace = $ns = (int)$ns; |
622 | $t->mDbkeyform = strtr( $title, ' ', '_' ); |
623 | $t->mArticleID = ( $ns >= 0 ) ? -1 : 0; |
624 | $t->mUrlform = wfUrlencode( $t->mDbkeyform ); |
625 | $t->mTextform = strtr( $title, '_', ' ' ); |
626 | return $t; |
627 | } |
628 | |
629 | /** |
630 | * Create a new Title from a namespace index and a DB key. |
631 | * The parameters will be checked for validity, which is a bit slower |
632 | * than makeTitle() but safer for user-provided data. |
633 | * |
634 | * The Title object returned by this method is guaranteed to be valid. |
635 | * Call canExist() to check if the Title represents an editable page. |
636 | * |
637 | * @param int $ns The namespace of the article |
638 | * @param string $title Database key form |
639 | * @param string $fragment The link fragment (after the "#") |
640 | * @param string $interwiki Interwiki prefix |
641 | * @return Title|null The new object, or null on an error |
642 | */ |
643 | public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) { |
644 | // NOTE: ideally, this would just call makeTitle() and then isValid(), |
645 | // but presently, that means more overhead on a potential performance hotspot. |
646 | |
647 | if ( !MediaWikiServices::getInstance()->getNamespaceInfo()->exists( $ns ) ) { |
648 | return null; |
649 | } |
650 | |
651 | $t = new Title(); |
652 | $dbKeyForm = self::makeName( $ns, $title, $fragment, $interwiki, true ); |
653 | |
654 | try { |
655 | $t->secureAndSplit( $dbKeyForm ); |
656 | return $t; |
657 | } catch ( MalformedTitleException $ex ) { |
658 | return null; |
659 | } |
660 | } |
661 | |
662 | /** |
663 | * Create a new Title for the Main Page |
664 | * |
665 | * This uses the 'mainpage' interface message, which could be specified in |
666 | * `$wgForceUIMsgAsContentMsg`. If that is the case, then calling this method |
667 | * will use the user language, which would involve initialising the session |
668 | * via `RequestContext::getMain()->getLanguage()`. For session-less endpoints, |
669 | * be sure to pass in a MessageLocalizer (such as your own RequestContext or |
670 | * ResourceLoader Context) to prevent an error. |
671 | * |
672 | * @note The Title instance returned by this method is not guaranteed to be a fresh instance. |
673 | * It may instead be a cached instance created previously, with references to it remaining |
674 | * elsewhere. |
675 | * |
676 | * @param MessageLocalizer|null $localizer An optional context to use (since 1.34) |
677 | * @return Title |
678 | */ |
679 | public static function newMainPage( ?MessageLocalizer $localizer = null ) { |
680 | static $recursionGuard = false; |
681 | |
682 | $title = null; |
683 | |
684 | if ( !$recursionGuard ) { |
685 | $msg = $localizer ? $localizer->msg( 'mainpage' ) : wfMessage( 'mainpage' ); |
686 | |
687 | $recursionGuard = true; |
688 | $title = self::newFromText( $msg->inContentLanguage()->text() ); |
689 | $recursionGuard = false; |
690 | } |
691 | |
692 | // Every page renders at least one link to the Main Page (e.g. sidebar). |
693 | // Don't produce fatal errors that would make the wiki inaccessible, and hard to fix the |
694 | // invalid message. |
695 | // |
696 | // Fallback scenarios: |
697 | // * Recursion guard |
698 | // If the message contains a bare local interwiki (T297571), then |
699 | // Title::newFromText via TitleParser::splitTitleString can get back here. |
700 | // * Invalid title |
701 | // If the 'mainpage' message contains something that is invalid, Title::newFromText |
702 | // will return null. |
703 | |
704 | return $title ?? self::makeTitle( NS_MAIN, 'Main Page' ); |
705 | } |
706 | |
707 | /** |
708 | * Get a regex character class describing the legal characters in a link |
709 | * |
710 | * @return string The list of characters, not delimited |
711 | */ |
712 | public static function legalChars() { |
713 | global $wgLegalTitleChars; |
714 | return $wgLegalTitleChars; |
715 | } |
716 | |
717 | /** |
718 | * Utility method for converting a character sequence from bytes to Unicode. |
719 | * |
720 | * Primary usecase being converting $wgLegalTitleChars to a sequence usable in |
721 | * javascript, as PHP uses UTF-8 bytes where javascript uses Unicode code units. |
722 | * |
723 | * @param string $byteClass |
724 | * @return string |
725 | */ |
726 | public static function convertByteClassToUnicodeClass( $byteClass ) { |
727 | $length = strlen( $byteClass ); |
728 | // Input token queue |
729 | $x0 = $x1 = $x2 = ''; |
730 | // Decoded queue |
731 | $d0 = $d1 = ''; |
732 | // Decoded integer codepoints |
733 | $ord0 = $ord1 = $ord2 = 0; |
734 | // Re-encoded queue |
735 | $r0 = $r1 = $r2 = ''; |
736 | // Output |
737 | $out = ''; |
738 | // Flags |
739 | $allowUnicode = false; |
740 | for ( $pos = 0; $pos < $length; $pos++ ) { |
741 | // Shift the queues down |
742 | $x2 = $x1; |
743 | $x1 = $x0; |
744 | $d1 = $d0; |
745 | $ord2 = $ord1; |
746 | $ord1 = $ord0; |
747 | $r2 = $r1; |
748 | $r1 = $r0; |
749 | // Load the current input token and decoded values |
750 | $inChar = $byteClass[$pos]; |
751 | if ( $inChar === '\\' ) { |
752 | if ( preg_match( '/x([0-9a-fA-F]{2})/A', $byteClass, $m, 0, $pos + 1 ) ) { |
753 | $x0 = $inChar . $m[0]; |
754 | $d0 = chr( hexdec( $m[1] ) ); |
755 | $pos += strlen( $m[0] ); |
756 | } elseif ( preg_match( '/[0-7]{3}/A', $byteClass, $m, 0, $pos + 1 ) ) { |
757 | $x0 = $inChar . $m[0]; |
758 | $d0 = chr( octdec( $m[0] ) ); |
759 | $pos += strlen( $m[0] ); |
760 | } elseif ( $pos + 1 >= $length ) { |
761 | $x0 = $d0 = '\\'; |
762 | } else { |
763 | $d0 = $byteClass[$pos + 1]; |
764 | $x0 = $inChar . $d0; |
765 | $pos++; |
766 | } |
767 | } else { |
768 | $x0 = $d0 = $inChar; |
769 | } |
770 | $ord0 = ord( $d0 ); |
771 | // Load the current re-encoded value |
772 | if ( $ord0 < 32 || $ord0 == 0x7f ) { |
773 | $r0 = sprintf( '\x%02x', $ord0 ); |
774 | } elseif ( $ord0 >= 0x80 ) { |
775 | // Allow unicode if a single high-bit character appears |
776 | $r0 = sprintf( '\x%02x', $ord0 ); |
777 | $allowUnicode = true; |
778 | // @phan-suppress-next-line PhanParamSuspiciousOrder false positive |
779 | } elseif ( strpos( '-\\[]^', $d0 ) !== false ) { |
780 | $r0 = '\\' . $d0; |
781 | } else { |
782 | $r0 = $d0; |
783 | } |
784 | // Do the output |
785 | if ( $x0 !== '' && $x1 === '-' && $x2 !== '' ) { |
786 | // Range |
787 | if ( $ord2 > $ord0 ) { |
788 | // Empty range |
789 | } elseif ( $ord0 >= 0x80 ) { |
790 | // Unicode range |
791 | $allowUnicode = true; |
792 | if ( $ord2 < 0x80 ) { |
793 | // Keep the non-unicode section of the range |
794 | $out .= "$r2-\\x7F"; |
795 | } |
796 | } else { |
797 | // Normal range |
798 | $out .= "$r2-$r0"; |
799 | } |
800 | // Reset state to the initial value |
801 | // @phan-suppress-next-line PhanPluginRedundantAssignmentInLoop |
802 | $x0 = $x1 = $d0 = $d1 = $r0 = $r1 = ''; |
803 | } elseif ( $ord2 < 0x80 ) { |
804 | // ASCII character |
805 | $out .= $r2; |
806 | } |
807 | } |
808 | // @phan-suppress-next-line PhanSuspiciousValueComparison |
809 | if ( $ord1 < 0x80 ) { |
810 | $out .= $r1; |
811 | } |
812 | if ( $ord0 < 0x80 ) { |
813 | $out .= $r0; |
814 | } |
815 | if ( $allowUnicode ) { |
816 | $out .= '\u0080-\uFFFF'; |
817 | } |
818 | return $out; |
819 | } |
820 | |
821 | /** |
822 | * Make a prefixed DB key from a DB key and a namespace index |
823 | * |
824 | * @param int $ns Numerical representation of the namespace |
825 | * @param string $title The DB key form the title |
826 | * @param string $fragment The link fragment (after the "#") |
827 | * @param string $interwiki The interwiki prefix |
828 | * @param bool $canonicalNamespace If true, use the canonical name for |
829 | * $ns instead of the localized version. |
830 | * @return string The prefixed form of the title |
831 | */ |
832 | public static function makeName( $ns, $title, $fragment = '', $interwiki = '', |
833 | $canonicalNamespace = false |
834 | ) { |
835 | if ( $canonicalNamespace ) { |
836 | $namespace = MediaWikiServices::getInstance()->getNamespaceInfo()-> |
837 | getCanonicalName( $ns ); |
838 | } else { |
839 | $namespace = MediaWikiServices::getInstance()->getContentLanguage()->getNsText( $ns ); |
840 | } |
841 | if ( $namespace === false ) { |
842 | // See T165149. Awkward, but better than erroneously linking to the main namespace. |
843 | $namespace = self::makeName( NS_SPECIAL, "Badtitle/NS$ns", '', '', $canonicalNamespace ); |
844 | } |
845 | $name = $namespace === '' ? $title : "$namespace:$title"; |
846 | if ( strval( $interwiki ) != '' ) { |
847 | $name = "$interwiki:$name"; |
848 | } |
849 | if ( strval( $fragment ) != '' ) { |
850 | $name .= '#' . $fragment; |
851 | } |
852 | return $name; |
853 | } |
854 | |
855 | /** |
856 | * Callback for usort() to do title sorts by (namespace, title) |
857 | * |
858 | * @param LinkTarget|PageReference $a |
859 | * @param LinkTarget|PageReference $b |
860 | * |
861 | * @return int Result of string comparison, or namespace comparison |
862 | */ |
863 | public static function compare( $a, $b ) { |
864 | return $a->getNamespace() <=> $b->getNamespace() |
865 | ?: strcmp( $a->getDBkey(), $b->getDBkey() ); |
866 | } |
867 | |
868 | /** |
869 | * Returns true if the title is a valid link target, and that it has been |
870 | * properly normalized. This method checks that the title is syntactically valid, |
871 | * and that the namespace it refers to exists. |
872 | * |
873 | * Titles constructed using newFromText() or makeTitleSafe() are always valid. |
874 | * |
875 | * @note Code that wants to check whether the title can represent a page that can |
876 | * be created and edited should use canExist() instead. Examples of valid titles |
877 | * that cannot "exist" are Special pages, interwiki links, and on-page section links |
878 | * that only have the fragment part set. |
879 | * |
880 | * @see canExist() |
881 | * |
882 | * @return bool |
883 | */ |
884 | public function isValid() { |
885 | if ( $this->mIsValid !== null ) { |
886 | return $this->mIsValid; |
887 | } |
888 | |
889 | try { |
890 | // Optimization: Avoid Title::getFullText because that involves GenderCache |
891 | // and (unbatched) database queries. For validation, canonical namespace suffices. |
892 | $text = self::makeName( $this->mNamespace, $this->mDbkeyform, $this->mFragment, $this->mInterwiki, true ); |
893 | $titleParser = MediaWikiServices::getInstance()->getTitleParser(); |
894 | |
895 | $parts = $titleParser->splitTitleString( $text, $this->mNamespace ); |
896 | |
897 | // Check that nothing changed! |
898 | // This ensures that $text was already properly normalized. |
899 | if ( $parts['fragment'] !== $this->mFragment |
900 | || $parts['interwiki'] !== $this->mInterwiki |
901 | || $parts['local_interwiki'] !== $this->mLocalInterwiki |
902 | || $parts['namespace'] !== $this->mNamespace |
903 | || $parts['dbkey'] !== $this->mDbkeyform |
904 | ) { |
905 | $this->mIsValid = false; |
906 | return $this->mIsValid; |
907 | } |
908 | } catch ( MalformedTitleException $ex ) { |
909 | $this->mIsValid = false; |
910 | return $this->mIsValid; |
911 | } |
912 | |
913 | $this->mIsValid = true; |
914 | return $this->mIsValid; |
915 | } |
916 | |
917 | /** |
918 | * Determine whether the object refers to a page within |
919 | * this project (either this wiki or a wiki with a local |
920 | * interwiki, see https://www.mediawiki.org/wiki/Manual:Interwiki_table#iw_local ) |
921 | * |
922 | * @return bool True if this is an in-project interwiki link or a wikilink, false otherwise |
923 | */ |
924 | public function isLocal() { |
925 | if ( $this->isExternal() ) { |
926 | $iw = self::getInterwikiLookup()->fetch( $this->mInterwiki ); |
927 | if ( $iw ) { |
928 | return $iw->isLocal(); |
929 | } |
930 | } |
931 | return true; |
932 | } |
933 | |
934 | /** |
935 | * Get the interwiki prefix |
936 | * |
937 | * Use Title::isExternal to check if a interwiki is set |
938 | * |
939 | * @return string Interwiki prefix |
940 | */ |
941 | public function getInterwiki(): string { |
942 | return $this->mInterwiki; |
943 | } |
944 | |
945 | /** |
946 | * Was this a local interwiki link? |
947 | * |
948 | * @return bool |
949 | */ |
950 | public function wasLocalInterwiki() { |
951 | return $this->mLocalInterwiki; |
952 | } |
953 | |
954 | /** |
955 | * Determine whether the object refers to a page within |
956 | * this project and is transcludable. |
957 | * |
958 | * @return bool True if this is transcludable |
959 | */ |
960 | public function isTrans() { |
961 | if ( !$this->isExternal() ) { |
962 | return false; |
963 | } |
964 | |
965 | return self::getInterwikiLookup()->fetch( $this->mInterwiki )->isTranscludable(); |
966 | } |
967 | |
968 | /** |
969 | * Returns the DB name of the distant wiki which owns the object. |
970 | * |
971 | * @return string|false The DB name |
972 | */ |
973 | public function getTransWikiID() { |
974 | if ( !$this->isExternal() ) { |
975 | return false; |
976 | } |
977 | |
978 | return self::getInterwikiLookup()->fetch( $this->mInterwiki )->getWikiID(); |
979 | } |
980 | |
981 | /** |
982 | * Get a TitleValue object representing this Title. |
983 | * |
984 | * @note Not all valid Titles have a corresponding valid TitleValue |
985 | * (e.g. TitleValues cannot represent page-local links that have a |
986 | * fragment but no title text). |
987 | * |
988 | * @return TitleValue|null |
989 | */ |
990 | public function getTitleValue() { |
991 | if ( $this->mTitleValue === null ) { |
992 | try { |
993 | $this->mTitleValue = new TitleValue( |
994 | $this->mNamespace, |
995 | $this->mDbkeyform, |
996 | $this->mFragment, |
997 | $this->mInterwiki |
998 | ); |
999 | } catch ( InvalidArgumentException $ex ) { |
1000 | wfDebug( __METHOD__ . ': Can\'t create a TitleValue for [[' . |
1001 | $this->getPrefixedText() . ']]: ' . $ex->getMessage() ); |
1002 | } |
1003 | } |
1004 | |
1005 | return $this->mTitleValue; |
1006 | } |
1007 | |
1008 | /** |
1009 | * Get the text form (spaces not underscores) of the main part |
1010 | * |
1011 | * @return string Main part of the title |
1012 | */ |
1013 | public function getText(): string { |
1014 | return $this->mTextform; |
1015 | } |
1016 | |
1017 | /** |
1018 | * Get the URL-encoded form of the main part |
1019 | * |
1020 | * @return string Main part of the title, URL-encoded |
1021 | */ |
1022 | public function getPartialURL() { |
1023 | return $this->mUrlform; |
1024 | } |
1025 | |
1026 | /** |
1027 | * Get the main part with underscores |
1028 | * |
1029 | * @return string Main part of the title, with underscores |
1030 | */ |
1031 | public function getDBkey(): string { |
1032 | return $this->mDbkeyform; |
1033 | } |
1034 | |
1035 | /** |
1036 | * Get the namespace index, i.e. one of the NS_xxxx constants. |
1037 | * |
1038 | * @return int Namespace index |
1039 | */ |
1040 | public function getNamespace(): int { |
1041 | return $this->mNamespace; |
1042 | } |
1043 | |
1044 | /** |
1045 | * @param int $flags |
1046 | * |
1047 | * @return bool Whether $flags indicates that the latest information should be |
1048 | * read from the primary database, bypassing caches. |
1049 | */ |
1050 | private function shouldReadLatest( int $flags ) { |
1051 | return ( $flags & ( IDBAccessObject::READ_LATEST ) ) > 0; |
1052 | } |
1053 | |
1054 | /** |
1055 | * Get the page's content model id, see the CONTENT_MODEL_XXX constants. |
1056 | * |
1057 | * @todo Deprecate this in favor of SlotRecord::getModel() |
1058 | * |
1059 | * @param int $flags A bitfield of IDBAccessObject::READ_* constants |
1060 | * @return string Content model id |
1061 | */ |
1062 | public function getContentModel( $flags = 0 ) { |
1063 | if ( $this->mForcedContentModel ) { |
1064 | if ( !$this->mContentModel ) { |
1065 | throw new RuntimeException( 'Got out of sync; an empty model is being forced' ); |
1066 | } |
1067 | // Content model is locked to the currently loaded one |
1068 | return $this->mContentModel; |
1069 | } |
1070 | |
1071 | if ( $this->shouldReadLatest( $flags ) || !$this->mContentModel ) { |
1072 | $this->lazyFillContentModel( $this->getFieldFromPageStore( 'page_content_model', $flags ) ); |
1073 | } |
1074 | |
1075 | if ( !$this->mContentModel ) { |
1076 | $slotRoleregistry = MediaWikiServices::getInstance()->getSlotRoleRegistry(); |
1077 | $mainSlotHandler = $slotRoleregistry->getRoleHandler( 'main' ); |
1078 | $this->lazyFillContentModel( $mainSlotHandler->getDefaultModel( $this ) ); |
1079 | } |
1080 | |
1081 | return $this->mContentModel; |
1082 | } |
1083 | |
1084 | /** |
1085 | * Convenience method for checking a title's content model name |
1086 | * |
1087 | * @param string $id The content model ID (use the CONTENT_MODEL_XXX constants). |
1088 | * @return bool True if $this->getContentModel() == $id |
1089 | */ |
1090 | public function hasContentModel( $id ) { |
1091 | return $this->getContentModel() == $id; |
1092 | } |
1093 | |
1094 | /** |
1095 | * Set a proposed content model for the page for permissions checking |
1096 | * |
1097 | * This does not actually change the content model of a title in the DB. |
1098 | * It only affects this particular Title instance. The content model is |
1099 | * forced to remain this value until another setContentModel() call. |
1100 | * |
1101 | * ContentHandler::canBeUsedOn() should be checked before calling this |
1102 | * if there is any doubt regarding the applicability of the content model |
1103 | * |
1104 | * @warning This must only be used if the caller controls the further use of |
1105 | * this Title object, to avoid other code unexpectedly using the new value. |
1106 | * |
1107 | * @since 1.28 |
1108 | * @param string $model CONTENT_MODEL_XXX constant |
1109 | */ |
1110 | public function setContentModel( $model ) { |
1111 | if ( (string)$model === '' ) { |
1112 | throw new InvalidArgumentException( "Missing CONTENT_MODEL_* constant" ); |
1113 | } |
1114 | |
1115 | $this->uncache(); |
1116 | $this->mContentModel = $model; |
1117 | $this->mForcedContentModel = true; |
1118 | } |
1119 | |
1120 | /** |
1121 | * If the content model field is not frozen then update it with a retrieved value |
1122 | * |
1123 | * @param string|bool $model CONTENT_MODEL_XXX constant or false |
1124 | */ |
1125 | private function lazyFillContentModel( $model ) { |
1126 | if ( !$this->mForcedContentModel ) { |
1127 | $this->mContentModel = ( $model === false ) ? false : (string)$model; |
1128 | } |
1129 | } |
1130 | |
1131 | /** |
1132 | * Get the namespace text |
1133 | * |
1134 | * @return string|false Namespace name with underscores (not spaces), e.g. 'User_talk' |
1135 | */ |
1136 | public function getNsText() { |
1137 | if ( $this->isExternal() ) { |
1138 | // This probably shouldn't even happen, except for interwiki transclusion. |
1139 | // If possible, use the canonical name for the foreign namespace. |
1140 | if ( $this->mNamespace === NS_MAIN ) { |
1141 | // Optimisation |
1142 | return ''; |
1143 | } else { |
1144 | $nsText = MediaWikiServices::getInstance()->getNamespaceInfo()-> |
1145 | getCanonicalName( $this->mNamespace ); |
1146 | if ( $nsText !== false ) { |
1147 | return $nsText; |
1148 | } |
1149 | } |
1150 | } |
1151 | |
1152 | try { |
1153 | $formatter = self::getTitleFormatter(); |
1154 | return $formatter->getNamespaceName( $this->mNamespace, $this->mDbkeyform ); |
1155 | } catch ( InvalidArgumentException $ex ) { |
1156 | wfDebug( __METHOD__ . ': ' . $ex->getMessage() ); |
1157 | return false; |
1158 | } |
1159 | } |
1160 | |
1161 | /** |
1162 | * Get the namespace text of the subject (rather than talk) page |
1163 | * |
1164 | * @return string Namespace name with underscores (not spaces) |
1165 | */ |
1166 | public function getSubjectNsText() { |
1167 | $services = MediaWikiServices::getInstance(); |
1168 | return $services->getContentLanguage()-> |
1169 | getNsText( $services->getNamespaceInfo()->getSubject( $this->mNamespace ) ); |
1170 | } |
1171 | |
1172 | /** |
1173 | * Get the namespace text of the talk page |
1174 | * |
1175 | * @return string Namespace name with underscores (not spaces) |
1176 | */ |
1177 | public function getTalkNsText() { |
1178 | $services = MediaWikiServices::getInstance(); |
1179 | return $services->getContentLanguage()-> |
1180 | getNsText( $services->getNamespaceInfo()->getTalk( $this->mNamespace ) ); |
1181 | } |
1182 | |
1183 | /** |
1184 | * Can this title have a corresponding talk page? |
1185 | * |
1186 | * False for relative section links (with getText() === ''), |
1187 | * interwiki links (with getInterwiki() !== ''), and pages in NS_SPECIAL. |
1188 | * |
1189 | * @see NamespaceInfo::canHaveTalkPage |
1190 | * @since 1.30 |
1191 | * |
1192 | * @return bool True if this title either is a talk page or can have a talk page associated. |
1193 | */ |
1194 | public function canHaveTalkPage() { |
1195 | return MediaWikiServices::getInstance()->getNamespaceInfo()->canHaveTalkPage( $this ); |
1196 | } |
1197 | |
1198 | /** |
1199 | * Can this title represent a page in the wiki's database? |
1200 | * |
1201 | * Titles can exist as pages in the database if they are valid, and they |
1202 | * are not Special pages, interwiki links, or fragment-only links. |
1203 | * |
1204 | * @see isValid() |
1205 | * |
1206 | * @return bool true if and only if this title can be used to perform an edit. |
1207 | */ |
1208 | public function canExist(): bool { |
1209 | // NOTE: Don't use getArticleID(), we don't want to |
1210 | // trigger a database query here. This check is supposed to |
1211 | // act as an optimization, not add extra cost. |
1212 | if ( $this->mArticleID > 0 ) { |
1213 | // It exists, so it can exist. |
1214 | return true; |
1215 | } |
1216 | |
1217 | // NOTE: we call the relatively expensive isValid() method further down, |
1218 | // but we can bail out early if we already know the title is invalid. |
1219 | if ( $this->mIsValid === false ) { |
1220 | // It's invalid, so it can't exist. |
1221 | return false; |
1222 | } |
1223 | |
1224 | if ( $this->getNamespace() < NS_MAIN ) { |
1225 | // It's a special page, so it can't exist in the database. |
1226 | return false; |
1227 | } |
1228 | |
1229 | if ( $this->isExternal() ) { |
1230 | // If it's external, it's not local, so it can't exist. |
1231 | return false; |
1232 | } |
1233 | |
1234 | if ( $this->getText() === '' ) { |
1235 | // The title has no text, so it can't exist in the database. |
1236 | // It's probably an on-page section link, like "#something". |
1237 | return false; |
1238 | } |
1239 | |
1240 | // Double check that the title is valid. |
1241 | return $this->isValid(); |
1242 | } |
1243 | |
1244 | /** |
1245 | * Returns true if this is a special page. |
1246 | * |
1247 | * @return bool |
1248 | */ |
1249 | public function isSpecialPage() { |
1250 | return $this->mNamespace === NS_SPECIAL; |
1251 | } |
1252 | |
1253 | /** |
1254 | * Returns true if this title resolves to the named special page |
1255 | * |
1256 | * @param string $name The special page name |
1257 | * @return bool |
1258 | */ |
1259 | public function isSpecial( $name ) { |
1260 | if ( $this->isSpecialPage() ) { |
1261 | [ $thisName, /* $subpage */ ] = |
1262 | MediaWikiServices::getInstance()->getSpecialPageFactory()-> |
1263 | resolveAlias( $this->mDbkeyform ); |
1264 | if ( $name == $thisName ) { |
1265 | return true; |
1266 | } |
1267 | } |
1268 | return false; |
1269 | } |
1270 | |
1271 | /** |
1272 | * If the Title refers to a special page alias which is not the local default, resolve |
1273 | * the alias, and localise the name as necessary. Otherwise, return $this |
1274 | * |
1275 | * @return Title |
1276 | */ |
1277 | public function fixSpecialName() { |
1278 | if ( $this->isSpecialPage() ) { |
1279 | $spFactory = MediaWikiServices::getInstance()->getSpecialPageFactory(); |
1280 | [ $canonicalName, $par ] = $spFactory->resolveAlias( $this->mDbkeyform ); |
1281 | if ( $canonicalName ) { |
1282 | $localName = $spFactory->getLocalNameFor( $canonicalName, $par ); |
1283 | if ( $localName != $this->mDbkeyform ) { |
1284 | return self::makeTitle( NS_SPECIAL, $localName ); |
1285 | } |
1286 | } |
1287 | } |
1288 | return $this; |
1289 | } |
1290 | |
1291 | /** |
1292 | * Returns true if the title is inside the specified namespace. |
1293 | * |
1294 | * @param int $ns The namespace |
1295 | * @return bool |
1296 | * @since 1.19 |
1297 | */ |
1298 | public function inNamespace( int $ns ): bool { |
1299 | return MediaWikiServices::getInstance()->getNamespaceInfo()-> |
1300 | equals( $this->mNamespace, $ns ); |
1301 | } |
1302 | |
1303 | /** |
1304 | * Returns true if the title is inside one of the specified namespaces. |
1305 | * |
1306 | * @param int|int[] ...$namespaces The namespaces to check for |
1307 | * @return bool |
1308 | * @since 1.19 |
1309 | */ |
1310 | public function inNamespaces( ...$namespaces ) { |
1311 | if ( count( $namespaces ) > 0 && is_array( $namespaces[0] ) ) { |
1312 | $namespaces = $namespaces[0]; |
1313 | } |
1314 | |
1315 | foreach ( $namespaces as $ns ) { |
1316 | if ( $this->inNamespace( $ns ) ) { |
1317 | return true; |
1318 | } |
1319 | } |
1320 | |
1321 | return false; |
1322 | } |
1323 | |
1324 | /** |
1325 | * Returns true if the title has the same subject namespace as the |
1326 | * namespace specified. |
1327 | * For example this method will take NS_USER and return true if namespace |
1328 | * is either NS_USER or NS_USER_TALK since both of them have NS_USER |
1329 | * as their subject namespace. |
1330 | * |
1331 | * This is MUCH simpler than individually testing for equivalence |
1332 | * against both NS_USER and NS_USER_TALK, and is also forward compatible. |
1333 | * @since 1.19 |
1334 | * @param int $ns |
1335 | * @return bool |
1336 | */ |
1337 | public function hasSubjectNamespace( $ns ) { |
1338 | return MediaWikiServices::getInstance()->getNamespaceInfo()-> |
1339 | subjectEquals( $this->mNamespace, $ns ); |
1340 | } |
1341 | |
1342 | /** |
1343 | * Is this Title in a namespace which contains content? |
1344 | * In other words, is this a content page, for the purposes of calculating |
1345 | * statistics, etc? |
1346 | * |
1347 | * @return bool |
1348 | */ |
1349 | public function isContentPage() { |
1350 | return MediaWikiServices::getInstance()->getNamespaceInfo()-> |
1351 | isContent( $this->mNamespace ); |
1352 | } |
1353 | |
1354 | /** |
1355 | * Would anybody with sufficient privileges be able to move this page? |
1356 | * Some pages just aren't movable. |
1357 | * |
1358 | * @return bool |
1359 | */ |
1360 | public function isMovable() { |
1361 | $services = MediaWikiServices::getInstance(); |
1362 | if ( |
1363 | !$services->getNamespaceInfo()-> |
1364 | isMovable( $this->mNamespace ) || $this->isExternal() |
1365 | ) { |
1366 | // Interwiki title or immovable namespace. Hooks don't get to override here |
1367 | return false; |
1368 | } |
1369 | |
1370 | $result = true; |
1371 | ( new HookRunner( $services->getHookContainer() ) )->onTitleIsMovable( $this, $result ); |
1372 | return $result; |
1373 | } |
1374 | |
1375 | /** |
1376 | * Is this the mainpage? |
1377 | * @see T302186 |
1378 | * |
1379 | * @since 1.18 |
1380 | * @return bool |
1381 | */ |
1382 | public function isMainPage() { |
1383 | /** @var Title|null */ |
1384 | static $cachedMainPage; |
1385 | $cachedMainPage ??= self::newMainPage(); |
1386 | return $this->equals( $cachedMainPage ); |
1387 | } |
1388 | |
1389 | /** |
1390 | * Is this a subpage? |
1391 | * |
1392 | * @return bool |
1393 | */ |
1394 | public function isSubpage() { |
1395 | return MediaWikiServices::getInstance() |
1396 | ->getNamespaceInfo() |
1397 | ->hasSubpages( $this->mNamespace ) |
1398 | && str_contains( $this->getText(), '/' ); |
1399 | } |
1400 | |
1401 | /** |
1402 | * Is this a conversion table for the LanguageConverter? |
1403 | * |
1404 | * @return bool |
1405 | */ |
1406 | public function isConversionTable() { |
1407 | // @todo ConversionTable should become a separate content model. |
1408 | // @todo And the prefix should be localized, too! |
1409 | |
1410 | return $this->mNamespace === NS_MEDIAWIKI && |
1411 | str_starts_with( $this->getText(), 'Conversiontable/' ); |
1412 | } |
1413 | |
1414 | /** |
1415 | * Does that page contain wikitext, or it is JS, CSS or whatever? |
1416 | * |
1417 | * @return bool |
1418 | */ |
1419 | public function isWikitextPage() { |
1420 | return $this->hasContentModel( CONTENT_MODEL_WIKITEXT ); |
1421 | } |
1422 | |
1423 | /** |
1424 | * Could this MediaWiki namespace page contain custom CSS, JSON, or JavaScript for the |
1425 | * global UI. This is generally true for pages in the MediaWiki namespace having |
1426 | * CONTENT_MODEL_CSS, CONTENT_MODEL_JSON, or CONTENT_MODEL_JAVASCRIPT. |
1427 | * |
1428 | * This method does *not* return true for per-user JS/JSON/CSS. Use isUserConfigPage() |
1429 | * for that! |
1430 | * |
1431 | * Note that this method should not return true for pages that contain and show |
1432 | * "inactive" CSS, JSON, or JS. |
1433 | * |
1434 | * @return bool |
1435 | * @since 1.31 |
1436 | */ |
1437 | public function isSiteConfigPage() { |
1438 | return ( |
1439 | $this->isSiteCssConfigPage() |
1440 | || $this->isSiteJsonConfigPage() |
1441 | || $this->isSiteJsConfigPage() |
1442 | ); |
1443 | } |
1444 | |
1445 | /** |
1446 | * Is this a "config" (.css, .json, or .js) subpage of a user page? |
1447 | * |
1448 | * @return bool |
1449 | * @since 1.31 |
1450 | */ |
1451 | public function isUserConfigPage() { |
1452 | return ( |
1453 | $this->isUserCssConfigPage() |
1454 | || $this->isUserJsonConfigPage() |
1455 | || $this->isUserJsConfigPage() |
1456 | ); |
1457 | } |
1458 | |
1459 | /** |
1460 | * Trim down a .css, .json, or .js subpage title to get the corresponding skin name |
1461 | * |
1462 | * @return string Containing skin name from .css, .json, or .js subpage title |
1463 | * @since 1.31 |
1464 | */ |
1465 | public function getSkinFromConfigSubpage() { |
1466 | $text = $this->getText(); |
1467 | $lastSlashPos = $this->findSubpageDivider( $text, -1 ); |
1468 | if ( $lastSlashPos === false ) { |
1469 | return ''; |
1470 | } |
1471 | |
1472 | $lastDot = strrpos( $text, '.', $lastSlashPos ); |
1473 | if ( $lastDot === false ) { |
1474 | return ''; |
1475 | } |
1476 | |
1477 | return substr( $text, $lastSlashPos + 1, $lastDot - $lastSlashPos - 1 ); |
1478 | } |
1479 | |
1480 | /** |
1481 | * Is this a CSS "config" subpage of a user page? |
1482 | * |
1483 | * @return bool |
1484 | * @since 1.31 |
1485 | */ |
1486 | public function isUserCssConfigPage() { |
1487 | return ( |
1488 | $this->mNamespace === NS_USER |
1489 | && $this->isSubpage() |
1490 | && $this->hasContentModel( CONTENT_MODEL_CSS ) |
1491 | ); |
1492 | } |
1493 | |
1494 | /** |
1495 | * Is this a JSON "config" subpage of a user page? |
1496 | * |
1497 | * @return bool |
1498 | * @since 1.31 |
1499 | */ |
1500 | public function isUserJsonConfigPage() { |
1501 | return ( |
1502 | $this->mNamespace === NS_USER |
1503 | && $this->isSubpage() |
1504 | && $this->hasContentModel( CONTENT_MODEL_JSON ) |
1505 | ); |
1506 | } |
1507 | |
1508 | /** |
1509 | * Is this a JS "config" subpage of a user page? |
1510 | * |
1511 | * @return bool |
1512 | * @since 1.31 |
1513 | */ |
1514 | public function isUserJsConfigPage() { |
1515 | return ( |
1516 | $this->mNamespace === NS_USER |
1517 | && $this->isSubpage() |
1518 | && $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) |
1519 | ); |
1520 | } |
1521 | |
1522 | /** |
1523 | * Is this a sitewide CSS "config" page? |
1524 | * |
1525 | * @return bool |
1526 | * @since 1.32 |
1527 | */ |
1528 | public function isSiteCssConfigPage() { |
1529 | return ( |
1530 | $this->mNamespace === NS_MEDIAWIKI |
1531 | && ( |
1532 | $this->hasContentModel( CONTENT_MODEL_CSS ) |
1533 | // paranoia - a MediaWiki: namespace page with mismatching extension and content |
1534 | // model is probably by mistake and might get handled incorrectly (see e.g. T112937) |
1535 | || str_ends_with( $this->mDbkeyform, '.css' ) |
1536 | ) |
1537 | ); |
1538 | } |
1539 | |
1540 | /** |
1541 | * Is this a sitewide JSON "config" page? |
1542 | * |
1543 | * @return bool |
1544 | * @since 1.32 |
1545 | */ |
1546 | public function isSiteJsonConfigPage() { |
1547 | return ( |
1548 | $this->mNamespace === NS_MEDIAWIKI |
1549 | && ( |
1550 | $this->hasContentModel( CONTENT_MODEL_JSON ) |
1551 | // paranoia - a MediaWiki: namespace page with mismatching extension and content |
1552 | // model is probably by mistake and might get handled incorrectly (see e.g. T112937) |
1553 | || str_ends_with( $this->mDbkeyform, '.json' ) |
1554 | ) |
1555 | ); |
1556 | } |
1557 | |
1558 | /** |
1559 | * Is this a sitewide JS "config" page? |
1560 | * |
1561 | * @return bool |
1562 | * @since 1.31 |
1563 | */ |
1564 | public function isSiteJsConfigPage() { |
1565 | return ( |
1566 | $this->mNamespace === NS_MEDIAWIKI |
1567 | && ( |
1568 | $this->hasContentModel( CONTENT_MODEL_JAVASCRIPT ) |
1569 | // paranoia - a MediaWiki: namespace page with mismatching extension and content |
1570 | // model is probably by mistake and might get handled incorrectly (see e.g. T112937) |
1571 | || str_ends_with( $this->mDbkeyform, '.js' ) |
1572 | ) |
1573 | ); |
1574 | } |
1575 | |
1576 | /** |
1577 | * Is this a message which can contain raw HTML? |
1578 | * |
1579 | * @return bool |
1580 | * @since 1.32 |
1581 | */ |
1582 | public function isRawHtmlMessage() { |
1583 | global $wgRawHtmlMessages; |
1584 | |
1585 | if ( !$this->inNamespace( NS_MEDIAWIKI ) ) { |
1586 | return false; |
1587 | } |
1588 | $message = lcfirst( $this->getRootTitle()->getDBkey() ); |
1589 | return in_array( $message, $wgRawHtmlMessages, true ); |
1590 | } |
1591 | |
1592 | /** |
1593 | * Is this a talk page of some sort? |
1594 | * |
1595 | * @return bool |
1596 | */ |
1597 | public function isTalkPage() { |
1598 | return MediaWikiServices::getInstance()->getNamespaceInfo()-> |
1599 | isTalk( $this->mNamespace ); |
1600 | } |
1601 | |
1602 | /** |
1603 | * Get a Title object associated with the talk page of this article |
1604 | * |
1605 | * @deprecated since 1.34, use getTalkPageIfDefined() or NamespaceInfo::getTalkPage() |
1606 | * with NamespaceInfo::canHaveTalkPage(). Note that the new method will |
1607 | * throw if asked for the talk page of a section-only link, or of an interwiki |
1608 | * link. |
1609 | * @return Title The object for the talk page |
1610 | * @throws MWException if $target doesn't have talk pages, e.g. because it's in NS_SPECIAL |
1611 | * or because it's a relative link, or an interwiki link. |
1612 | */ |
1613 | public function getTalkPage() { |
1614 | // NOTE: The equivalent code in NamespaceInfo is less lenient about producing invalid titles. |
1615 | // Instead of failing on invalid titles, let's just log the issue for now. |
1616 | // See the discussion on T227817. |
1617 | |
1618 | // Is this the same title? |
1619 | $talkNS = MediaWikiServices::getInstance()->getNamespaceInfo()->getTalk( $this->mNamespace ); |
1620 | if ( $this->mNamespace == $talkNS ) { |
1621 | return $this; |
1622 | } |
1623 | |
1624 | $title = self::makeTitle( $talkNS, $this->mDbkeyform ); |
1625 | |
1626 | $this->warnIfPageCannotExist( $title, __METHOD__ ); |
1627 | |
1628 | return $title; |
1629 | // TODO: replace the above with the code below: |
1630 | // return self::castFromLinkTarget( |
1631 | // MediaWikiServices::getInstance()->getNamespaceInfo()->getTalkPage( $this ) ); |
1632 | } |
1633 | |
1634 | /** |
1635 | * Get a Title object associated with the talk page of this article, |
1636 | * if such a talk page can exist. |
1637 | * |
1638 | * @since 1.30 |
1639 | * |
1640 | * @return Title|null The object for the talk page, |
1641 | * or null if no associated talk page can exist, according to canHaveTalkPage(). |
1642 | */ |
1643 | public function getTalkPageIfDefined() { |
1644 | if ( !$this->canHaveTalkPage() ) { |
1645 | return null; |
1646 | } |
1647 | |
1648 | return $this->getTalkPage(); |
1649 | } |
1650 | |
1651 | /** |
1652 | * Get a title object associated with the subject page of this |
1653 | * talk page |
1654 | * |
1655 | * @deprecated since 1.34, use NamespaceInfo::getSubjectPage |
1656 | * @return Title The object for the subject page |
1657 | */ |
1658 | public function getSubjectPage() { |
1659 | // Is this the same title? |
1660 | $subjectNS = MediaWikiServices::getInstance()->getNamespaceInfo() |
1661 | ->getSubject( $this->mNamespace ); |
1662 | if ( $this->mNamespace == $subjectNS ) { |
1663 | return $this; |
1664 | } |
1665 | // NOTE: The equivalent code in NamespaceInfo is less lenient about producing invalid titles. |
1666 | // Instead of failing on invalid titles, let's just log the issue for now. |
1667 | // See the discussion on T227817. |
1668 | $title = self::makeTitle( $subjectNS, $this->mDbkeyform ); |
1669 | |
1670 | $this->warnIfPageCannotExist( $title, __METHOD__ ); |
1671 | |
1672 | return $title; |
1673 | // TODO: replace the above with the code below: |
1674 | // return self::castFromLinkTarget( |
1675 | // MediaWikiServices::getInstance()->getNamespaceInfo()->getSubjectPage( $this ) ); |
1676 | } |
1677 | |
1678 | /** |
1679 | * @param Title $title |
1680 | * @param string $method |
1681 | * |
1682 | * @return bool whether a warning was issued |
1683 | */ |
1684 | private function warnIfPageCannotExist( Title $title, $method ) { |
1685 | if ( $this->getText() == '' ) { |
1686 | wfLogWarning( |
1687 | $method . ': called on empty title ' . $this->getFullText() . ', returning ' |
1688 | . $title->getFullText() |
1689 | ); |
1690 | |
1691 | return true; |
1692 | } |
1693 | |
1694 | if ( $this->getInterwiki() !== '' ) { |
1695 | wfLogWarning( |
1696 | $method . ': called on interwiki title ' . $this->getFullText() . ', returning ' |
1697 | . $title->getFullText() |
1698 | ); |
1699 | |
1700 | return true; |
1701 | } |
1702 | |
1703 | return false; |
1704 | } |
1705 | |
1706 | /** |
1707 | * Get the other title for this page, if this is a subject page |
1708 | * get the talk page, if it is a subject page get the talk page |
1709 | * |
1710 | * @deprecated since 1.34, use NamespaceInfo::getAssociatedPage |
1711 | * @since 1.25 |
1712 | * @throws MWException If the page doesn't have an other page |
1713 | * @return Title |
1714 | */ |
1715 | public function getOtherPage() { |
1716 | // NOTE: Depend on the methods in this class instead of their equivalent in NamespaceInfo, |
1717 | // until their semantics has become exactly the same. |
1718 | // See the discussion on T227817. |
1719 | if ( $this->isSpecialPage() ) { |
1720 | throw new MWException( 'Special pages cannot have other pages' ); |
1721 | } |
1722 | if ( $this->isTalkPage() ) { |
1723 | return $this->getSubjectPage(); |
1724 | } else { |
1725 | if ( !$this->canHaveTalkPage() ) { |
1726 | throw new MWException( "{$this->getPrefixedText()} does not have an other page" ); |
1727 | } |
1728 | return $this->getTalkPage(); |
1729 | } |
1730 | // TODO: replace the above with the code below: |
1731 | // return self::castFromLinkTarget( |
1732 | // MediaWikiServices::getInstance()->getNamespaceInfo()->getAssociatedPage( $this ) ); |
1733 | } |
1734 | |
1735 | /** |
1736 | * Get the Title fragment (i.e.\ the bit after the #) in text form |
1737 | * |
1738 | * Use Title::hasFragment to check for a fragment |
1739 | * |
1740 | * @return string Title fragment |
1741 | */ |
1742 | public function getFragment(): string { |
1743 | return $this->mFragment; |
1744 | } |
1745 | |
1746 | /** |
1747 | * Get the fragment in URL form, including the "#" character if there is one |
1748 | * |
1749 | * @return string Fragment in URL form |
1750 | */ |
1751 | public function getFragmentForURL() { |
1752 | if ( !$this->hasFragment() ) { |
1753 | return ''; |
1754 | } elseif ( $this->isExternal() ) { |
1755 | // Note: If the interwiki is unknown, it's treated as a namespace on the local wiki, |
1756 | // so we treat it like a local interwiki. |
1757 | $interwiki = self::getInterwikiLookup()->fetch( $this->mInterwiki ); |
1758 | if ( $interwiki && !$interwiki->isLocal() ) { |
1759 | return '#' . Sanitizer::escapeIdForExternalInterwiki( $this->mFragment ); |
1760 | } |
1761 | } |
1762 | |
1763 | return '#' . Sanitizer::escapeIdForLink( $this->mFragment ); |
1764 | } |
1765 | |
1766 | /** |
1767 | * Set the fragment for this title. Removes the first character from the |
1768 | * specified fragment before setting, so it assumes you're passing it with |
1769 | * an initial "#". |
1770 | * |
1771 | * @warning This must only be used if the caller controls the further use of |
1772 | * this Title object, to avoid other code unexpectedly using the new value. |
1773 | * |
1774 | * @param string $fragment Text |
1775 | */ |
1776 | public function setFragment( $fragment ) { |
1777 | $this->uncache(); |
1778 | $this->mFragment = self::normalizeFragment( $fragment ); |
1779 | } |
1780 | |
1781 | /** |
1782 | * Creates a new Title for a different fragment of the same page. |
1783 | * |
1784 | * @since 1.27 |
1785 | * @param string $fragment |
1786 | * @return Title |
1787 | */ |
1788 | public function createFragmentTarget( string $fragment ): self { |
1789 | return self::makeTitle( |
1790 | $this->mNamespace, |
1791 | $this->getText(), |
1792 | $fragment, |
1793 | $this->mInterwiki |
1794 | ); |
1795 | } |
1796 | |
1797 | /** |
1798 | * Normalizes fragment part of the title. |
1799 | * |
1800 | * @param string $fragment |
1801 | * @return string |
1802 | */ |
1803 | private static function normalizeFragment( $fragment ) { |
1804 | if ( str_starts_with( $fragment, '#' ) ) { |
1805 | $fragment = substr( $fragment, 1 ); |
1806 | } |
1807 | return strtr( $fragment, '_', ' ' ); |
1808 | } |
1809 | |
1810 | /** |
1811 | * Prefix some arbitrary text with the namespace or interwiki prefix |
1812 | * of this object |
1813 | * |
1814 | * @param string $name The text |
1815 | * @return string The prefixed text |
1816 | */ |
1817 | private function prefix( $name ) { |
1818 | $p = ''; |
1819 | if ( $this->isExternal() ) { |
1820 | $p = $this->mInterwiki . ':'; |
1821 | } |
1822 | |
1823 | if ( $this->mNamespace != 0 ) { |
1824 | $nsText = $this->getNsText(); |
1825 | |
1826 | if ( $nsText === false ) { |
1827 | // See T165149. Awkward, but better than erroneously linking to the main namespace. |
1828 | $nsText = MediaWikiServices::getInstance()->getContentLanguage()-> |
1829 | getNsText( NS_SPECIAL ) . ":Badtitle/NS{$this->mNamespace}"; |
1830 | } |
1831 | |
1832 | $p .= $nsText . ':'; |
1833 | } |
1834 | return $p . $name; |
1835 | } |
1836 | |
1837 | /** |
1838 | * Get the prefixed database key form |
1839 | * |
1840 | * @return string The prefixed title, with underscores and |
1841 | * any interwiki and namespace prefixes |
1842 | */ |
1843 | public function getPrefixedDBkey() { |
1844 | $s = $this->prefix( $this->mDbkeyform ); |
1845 | $s = strtr( $s, ' ', '_' ); |
1846 | return $s; |
1847 | } |
1848 | |
1849 | /** |
1850 | * Get the prefixed title with spaces. |
1851 | * This is the form usually used for display |
1852 | * |
1853 | * @return string The prefixed title, with spaces |
1854 | */ |
1855 | public function getPrefixedText() { |
1856 | if ( $this->prefixedText === null ) { |
1857 | $s = $this->prefix( $this->mTextform ); |
1858 | $s = strtr( $s, '_', ' ' ); |
1859 | $this->prefixedText = $s; |
1860 | } |
1861 | return $this->prefixedText; |
1862 | } |
1863 | |
1864 | /** |
1865 | * Return a string representation of this title |
1866 | * |
1867 | * @return string Representation of this title |
1868 | */ |
1869 | public function __toString(): string { |
1870 | return $this->getPrefixedText(); |
1871 | } |
1872 | |
1873 | /** |
1874 | * Get the prefixed title with spaces, plus any fragment |
1875 | * (part beginning with '#') |
1876 | * |
1877 | * @return string The prefixed title, with spaces and the fragment, including '#' |
1878 | */ |
1879 | public function getFullText() { |
1880 | $text = $this->getPrefixedText(); |
1881 | if ( $this->hasFragment() ) { |
1882 | $text .= '#' . $this->mFragment; |
1883 | } |
1884 | return $text; |
1885 | } |
1886 | |
1887 | /** |
1888 | * Finds the first or last subpage divider (slash) in the string. |
1889 | * Any leading sequence of slashes is ignored, since it does not divide |
1890 | * two parts of the string. Considering leading slashes dividers would |
1891 | * result in empty root title or base title (T229443). |
1892 | * |
1893 | * Note that trailing slashes are considered dividers, and empty subpage |
1894 | * names are allowed. |
1895 | * |
1896 | * @param string $text |
1897 | * @param int $dir -1 for the last or +1 for the first divider. |
1898 | * |
1899 | * @return false|int |
1900 | */ |
1901 | private function findSubpageDivider( $text, $dir ) { |
1902 | if ( $dir > 0 ) { |
1903 | // Skip leading slashes, but keep the last one when there is nothing but slashes |
1904 | $bottom = strspn( $text, '/', 0, -1 ); |
1905 | $idx = strpos( $text, '/', $bottom ); |
1906 | } else { |
1907 | // Any slash from the end can be a divider, as subpage names can be empty |
1908 | $idx = strrpos( $text, '/' ); |
1909 | } |
1910 | |
1911 | // The first character can never be a divider, as that would result in an empty base |
1912 | return $idx === 0 ? false : $idx; |
1913 | } |
1914 | |
1915 | /** |
1916 | * Whether this Title's namespace has subpages enabled. |
1917 | * @return bool |
1918 | */ |
1919 | private function hasSubpagesEnabled() { |
1920 | return MediaWikiServices::getInstance()->getNamespaceInfo()-> |
1921 | hasSubpages( $this->mNamespace ); |
1922 | } |
1923 | |
1924 | /** |
1925 | * Get the root page name text without a namespace, i.e. the leftmost part before any slashes |
1926 | * |
1927 | * @note the return value may contain trailing whitespace and is thus |
1928 | * not safe for use with makeTitle or TitleValue. |
1929 | * |
1930 | * @par Example: |
1931 | * @code |
1932 | * Title::newFromText('User:Foo/Bar/Baz')->getRootText(); |
1933 | * # returns: 'Foo' |
1934 | * @endcode |
1935 | * |
1936 | * @return string Root name |
1937 | * @since 1.20 |
1938 | */ |
1939 | public function getRootText() { |
1940 | $text = $this->getText(); |
1941 | if ( !$this->hasSubpagesEnabled() ) { |
1942 | return $text; |
1943 | } |
1944 | |
1945 | $firstSlashPos = $this->findSubpageDivider( $text, +1 ); |
1946 | // Don't discard the real title if there's no subpage involved |
1947 | if ( $firstSlashPos === false ) { |
1948 | return $text; |
1949 | } |
1950 | |
1951 | return substr( $text, 0, $firstSlashPos ); |
1952 | } |
1953 | |
1954 | /** |
1955 | * Get the root page name title, i.e. the leftmost part before any slashes |
1956 | * |
1957 | * @par Example: |
1958 | * @code |
1959 | * Title::newFromText('User:Foo/Bar/Baz')->getRootTitle(); |
1960 | * # returns: Title{User:Foo} |
1961 | * @endcode |
1962 | * |
1963 | * @return Title |
1964 | * @since 1.20 |
1965 | */ |
1966 | public function getRootTitle() { |
1967 | $title = self::makeTitleSafe( $this->mNamespace, $this->getRootText() ); |
1968 | |
1969 | if ( !$title ) { |
1970 | if ( !$this->isValid() ) { |
1971 | // If the title wasn't valid in the first place, we can't expect |
1972 | // to successfully parse it. T290194 |
1973 | return $this; |
1974 | } |
1975 | |
1976 | Assert::postcondition( |
1977 | $title !== null, |
1978 | 'makeTitleSafe() should always return a Title for the text ' . |
1979 | 'returned by getRootText().' |
1980 | ); |
1981 | } |
1982 | |
1983 | return $title; |
1984 | } |
1985 | |
1986 | /** |
1987 | * Get the base page name without a namespace, i.e. the part before the subpage name |
1988 | * |
1989 | * @note the return value may contain trailing whitespace and is thus |
1990 | * not safe for use with makeTitle or TitleValue. |
1991 | * |
1992 | * @par Example: |
1993 | * @code |
1994 | * Title::newFromText('User:Foo/Bar/Baz')->getBaseText(); |
1995 | * # returns: 'Foo/Bar' |
1996 | * @endcode |
1997 | * |
1998 | * @return string Base name |
1999 | */ |
2000 | public function getBaseText() { |
2001 | $text = $this->getText(); |
2002 | if ( !$this->hasSubpagesEnabled() ) { |
2003 | return $text; |
2004 | } |
2005 | |
2006 | $lastSlashPos = $this->findSubpageDivider( $text, -1 ); |
2007 | // Don't discard the real title if there's no subpage involved |
2008 | if ( $lastSlashPos === false ) { |
2009 | return $text; |
2010 | } |
2011 | |
2012 | return substr( $text, 0, $lastSlashPos ); |
2013 | } |
2014 | |
2015 | /** |
2016 | * Get the base page name title, i.e. the part before the subpage name. |
2017 | * |
2018 | * @par Example: |
2019 | * @code |
2020 | * Title::newFromText('User:Foo/Bar/Baz')->getBaseTitle(); |
2021 | * # returns: Title{User:Foo/Bar} |
2022 | * @endcode |
2023 | * |
2024 | * @return Title |
2025 | * @since 1.20 |
2026 | */ |
2027 | public function getBaseTitle() { |
2028 | $title = self::makeTitleSafe( $this->mNamespace, $this->getBaseText() ); |
2029 | |
2030 | if ( !$title ) { |
2031 | if ( !$this->isValid() ) { |
2032 | // If the title wasn't valid in the first place, we can't expect |
2033 | // to successfully parse it. T290194 |
2034 | return $this; |
2035 | } |
2036 | |
2037 | Assert::postcondition( |
2038 | $title !== null, |
2039 | 'makeTitleSafe() should always return a Title for the text ' . |
2040 | 'returned by getBaseText().' |
2041 | ); |
2042 | } |
2043 | |
2044 | return $title; |
2045 | } |
2046 | |
2047 | /** |
2048 | * Get the lowest-level subpage name, i.e. the rightmost part after any slashes |
2049 | * |
2050 | * @par Example: |
2051 | * @code |
2052 | * Title::newFromText('User:Foo/Bar/Baz')->getSubpageText(); |
2053 | * # returns: "Baz" |
2054 | * @endcode |
2055 | * |
2056 | * @return string Subpage name |
2057 | */ |
2058 | public function getSubpageText() { |
2059 | $text = $this->getText(); |
2060 | if ( !$this->hasSubpagesEnabled() ) { |
2061 | return $text; |
2062 | } |
2063 | |
2064 | $lastSlashPos = $this->findSubpageDivider( $text, -1 ); |
2065 | if ( $lastSlashPos === false ) { |
2066 | // T256922 - Return the title text if no subpages |
2067 | return $text; |
2068 | } |
2069 | return substr( $text, $lastSlashPos + 1 ); |
2070 | } |
2071 | |
2072 | /** |
2073 | * Get the title for a subpage of the current page |
2074 | * |
2075 | * @par Example: |
2076 | * @code |
2077 | * Title::newFromText('User:Foo/Bar/Baz')->getSubpage("Asdf"); |
2078 | * # returns: Title{User:Foo/Bar/Baz/Asdf} |
2079 | * @endcode |
2080 | * |
2081 | * @param string $text The subpage name to add to the title |
2082 | * @return Title|null Subpage title, or null on an error |
2083 | * @since 1.20 |
2084 | */ |
2085 | public function getSubpage( $text ) { |
2086 | return self::makeTitleSafe( |
2087 | $this->mNamespace, |
2088 | $this->getText() . '/' . $text, |
2089 | '', |
2090 | $this->mInterwiki |
2091 | ); |
2092 | } |
2093 | |
2094 | /** |
2095 | * Get a URL-encoded form of the subpage text |
2096 | * |
2097 | * @return string URL-encoded subpage name |
2098 | */ |
2099 | public function getSubpageUrlForm() { |
2100 | $text = $this->getSubpageText(); |
2101 | $text = wfUrlencode( strtr( $text, ' ', '_' ) ); |
2102 | return $text; |
2103 | } |
2104 | |
2105 | /** |
2106 | * Get a URL-encoded title (not an actual URL) including interwiki |
2107 | * |
2108 | * @return string The URL-encoded form |
2109 | */ |
2110 | public function getPrefixedURL() { |
2111 | $s = $this->prefix( $this->mDbkeyform ); |
2112 | $s = wfUrlencode( strtr( $s, ' ', '_' ) ); |
2113 | return $s; |
2114 | } |
2115 | |
2116 | /** |
2117 | * Get a real URL referring to this title, with interwiki link and |
2118 | * fragment |
2119 | * |
2120 | * @see self::getLocalURL for the arguments. |
2121 | * @see \MediaWiki\Utils\UrlUtils::expand() |
2122 | * @param string|array $query |
2123 | * @param false $query2 deprecated since MW 1.19; ignored since MW 1.41 |
2124 | * @param string|int|null $proto Protocol type to use in URL |
2125 | * @return string The URL |
2126 | */ |
2127 | public function getFullURL( $query = '', $query2 = false, $proto = PROTO_RELATIVE ) { |
2128 | $services = MediaWikiServices::getInstance(); |
2129 | |
2130 | $query = is_array( $query ) ? wfArrayToCgi( $query ) : $query; |
2131 | |
2132 | # Hand off all the decisions on urls to getLocalURL |
2133 | $url = $this->getLocalURL( $query ); |
2134 | |
2135 | # Expand the url to make it a full url. Note that getLocalURL has the |
2136 | # potential to output full urls for a variety of reasons, so we use |
2137 | # UrlUtils::expand() instead of simply prepending $wgServer |
2138 | $url = (string)$services->getUrlUtils()->expand( $url, $proto ); |
2139 | |
2140 | # Finally, add the fragment. |
2141 | $url .= $this->getFragmentForURL(); |
2142 | ( new HookRunner( $services->getHookContainer() ) )->onGetFullURL( $this, $url, $query ); |
2143 | return $url; |
2144 | } |
2145 | |
2146 | /** |
2147 | * Get a url appropriate for making redirects based on an untrusted url arg |
2148 | * |
2149 | * This is basically the same as getFullUrl(), but in the case of external |
2150 | * interwikis, we send the user to a landing page, to prevent possible |
2151 | * phishing attacks and the like. |
2152 | * |
2153 | * @note Uses current protocol by default, since technically relative urls |
2154 | * aren't allowed in redirects per HTTP spec, so this is not suitable for |
2155 | * places where the url gets cached, as might pollute between |
2156 | * https and non-https users. |
2157 | * @see self::getLocalURL for the arguments. |
2158 | * @param array|string $query |
2159 | * @param string $proto Protocol type to use in URL |
2160 | * @return string A url suitable to use in an HTTP location header. |
2161 | */ |
2162 | public function getFullUrlForRedirect( $query = '', $proto = PROTO_CURRENT ) { |
2163 | $target = $this; |
2164 | if ( $this->isExternal() ) { |
2165 | $target = SpecialPage::getTitleFor( |
2166 | 'GoToInterwiki', |
2167 | $this->getPrefixedDBkey() |
2168 | ); |
2169 | } |
2170 | return $target->getFullURL( $query, false, $proto ); |
2171 | } |
2172 | |
2173 | /** |
2174 | * Get a URL with no fragment or server name (relative URL) from a Title object. |
2175 | * If this page is generated with action=render, however, |
2176 | * $wgServer is prepended to make an absolute URL. |
2177 | * |
2178 | * @see self::getFullURL to always get an absolute URL. |
2179 | * @see self::getLinkURL to always get a URL that's the simplest URL that will be |
2180 | * valid to link, locally, to the current Title. |
2181 | * @see self::newFromText to produce a Title object. |
2182 | * |
2183 | * @param string|array $query An optional query string, |
2184 | * not used for interwiki links. Can be specified as an associative array as well, |
2185 | * e.g., [ 'action' => 'edit' ] (keys and values will be URL-escaped). |
2186 | * Some query patterns will trigger various shorturl path replacements. |
2187 | * |
2188 | * @return string |
2189 | */ |
2190 | public function getLocalURL( $query = '' ) { |
2191 | global $wgArticlePath, $wgScript, $wgMainPageIsDomainRoot; |
2192 | |
2193 | $query = is_array( $query ) ? wfArrayToCgi( $query ) : $query; |
2194 | |
2195 | $services = MediaWikiServices::getInstance(); |
2196 | $hookRunner = new HookRunner( $services->getHookContainer() ); |
2197 | $interwiki = self::getInterwikiLookup()->fetch( $this->mInterwiki ); |
2198 | if ( $interwiki ) { |
2199 | $namespace = $this->getNsText(); |
2200 | if ( $namespace != '' ) { |
2201 | # Can this actually happen? Interwikis shouldn't be parsed. |
2202 | # Yes! It can in interwiki transclusion. But... it probably shouldn't. |
2203 | $namespace .= ':'; |
2204 | } |
2205 | $url = $interwiki->getURL( $namespace . $this->mDbkeyform ); |
2206 | $url = wfAppendQuery( $url, $query ); |
2207 | } else { |
2208 | $dbkey = wfUrlencode( $this->getPrefixedDBkey() ); |
2209 | if ( $query == '' ) { |
2210 | if ( $wgMainPageIsDomainRoot && $this->isMainPage() ) { |
2211 | $url = '/'; |
2212 | } else { |
2213 | $url = str_replace( '$1', $dbkey, $wgArticlePath ); |
2214 | } |
2215 | $hookRunner->onGetLocalURL__Article( $this, $url ); |
2216 | } else { |
2217 | global $wgVariantArticlePath, $wgActionPaths; |
2218 | $url = false; |
2219 | $matches = []; |
2220 | |
2221 | $articlePaths = PathRouter::getActionPaths( $wgActionPaths, $wgArticlePath ); |
2222 | |
2223 | if ( $articlePaths |
2224 | && preg_match( '/^(.*&|)action=([^&]*)(&(.*)|)$/', $query, $matches ) |
2225 | ) { |
2226 | $action = urldecode( $matches[2] ); |
2227 | if ( isset( $articlePaths[$action] ) ) { |
2228 | $query = $matches[1]; |
2229 | if ( isset( $matches[4] ) ) { |
2230 | $query .= $matches[4]; |
2231 | } |
2232 | $url = str_replace( '$1', $dbkey, $articlePaths[$action] ); |
2233 | if ( $query != '' ) { |
2234 | $url = wfAppendQuery( $url, $query ); |
2235 | } |
2236 | } |
2237 | } |
2238 | |
2239 | if ( $url === false |
2240 | && $wgVariantArticlePath |
2241 | && preg_match( '/^variant=([^&]*)$/', $query, $matches ) |
2242 | && $this->getPageLanguage()->equals( $services->getContentLanguage() ) |
2243 | && $this->getPageLanguageConverter()->hasVariants() |
2244 | ) { |
2245 | $variant = urldecode( $matches[1] ); |
2246 | if ( $this->getPageLanguageConverter()->hasVariant( $variant ) ) { |
2247 | // Only do the variant replacement if the given variant is a valid |
2248 | // variant for the page's language. |
2249 | $url = str_replace( '$2', urlencode( $variant ), $wgVariantArticlePath ); |
2250 | $url = str_replace( '$1', $dbkey, $url ); |
2251 | } |
2252 | } |
2253 | |
2254 | if ( $url === false ) { |
2255 | if ( $query == '-' ) { |
2256 | $query = ''; |
2257 | } |
2258 | $url = "{$wgScript}?title={$dbkey}&{$query}"; |
2259 | } |
2260 | } |
2261 | $hookRunner->onGetLocalURL__Internal( $this, $url, $query ); |
2262 | } |
2263 | |
2264 | $hookRunner->onGetLocalURL( $this, $url, $query ); |
2265 | return $url; |
2266 | } |
2267 | |
2268 | /** |
2269 | * Get a URL that's the simplest URL that will be valid to link, locally, |
2270 | * to the current Title. It includes the fragment, but does not include |
2271 | * the server unless action=render is used (or the link is external). If |
2272 | * there's a fragment but the prefixed text is empty, we just return a link |
2273 | * to the fragment. |
2274 | * |
2275 | * The result obviously should not be URL-escaped, but does need to be |
2276 | * HTML-escaped if it's being output in HTML. |
2277 | * |
2278 | * @param string|array $query |
2279 | * @param string|string[]|false $query2 deprecated since MW 1.19; ignored since MW 1.41 |
2280 | * @param string|int|false $proto A PROTO_* constant on how the URL should be expanded, |
2281 | * or false (default) for no expansion |
2282 | * @see self::getLocalURL for the arguments. |
2283 | * @return string The URL |
2284 | */ |
2285 | public function getLinkURL( $query = '', $query2 = false, $proto = false ) { |
2286 | if ( $this->isExternal() || $proto !== false ) { |
2287 | $ret = $this->getFullURL( $query, false, $proto ); |
2288 | } elseif ( $this->getPrefixedText() === '' && $this->hasFragment() ) { |
2289 | $ret = $this->getFragmentForURL(); |
2290 | } else { |
2291 | $ret = $this->getLocalURL( $query ) . $this->getFragmentForURL(); |
2292 | } |
2293 | return $ret; |
2294 | } |
2295 | |
2296 | /** |
2297 | * Get the URL form for an internal link. |
2298 | * - Used in various CDN-related code, in case we have a different |
2299 | * internal hostname for the server from the exposed one. |
2300 | * |
2301 | * This uses $wgInternalServer to qualify the path, or $wgServer |
2302 | * if $wgInternalServer is not set. If the server variable used is |
2303 | * protocol-relative, the URL will be expanded to http:// |
2304 | * |
2305 | * @see self::getLocalURL for the arguments. |
2306 | * @param string|array $query |
2307 | * @return string The URL |
2308 | */ |
2309 | public function getInternalURL( $query = '' ) { |
2310 | global $wgInternalServer, $wgServer; |
2311 | $services = MediaWikiServices::getInstance(); |
2312 | |
2313 | $query = is_array( $query ) ? wfArrayToCgi( $query ) : $query; |
2314 | |
2315 | $server = $wgInternalServer !== false ? $wgInternalServer : $wgServer; |
2316 | $url = (string)$services->getUrlUtils()->expand( $server . $this->getLocalURL( $query ), PROTO_HTTP ); |
2317 | ( new HookRunner( $services->getHookContainer() ) ) |
2318 | ->onGetInternalURL( $this, $url, $query ); |
2319 | return $url; |
2320 | } |
2321 | |
2322 | /** |
2323 | * Get the URL for a canonical link, for use in things like IRC and |
2324 | * e-mail notifications. Uses $wgCanonicalServer and the |
2325 | * GetCanonicalURL hook. |
2326 | * |
2327 | * NOTE: Unlike getInternalURL(), the canonical URL includes the fragment |
2328 | * |
2329 | * @see self::getLocalURL for the arguments. |
2330 | * @param string|array $query |
2331 | * @return string The URL |
2332 | * @since 1.18 |
2333 | */ |
2334 | public function getCanonicalURL( $query = '' ) { |
2335 | $services = MediaWikiServices::getInstance(); |
2336 | |
2337 | $query = is_array( $query ) ? wfArrayToCgi( $query ) : $query; |
2338 | |
2339 | $url = (string)$services->getUrlUtils()->expand( |
2340 | $this->getLocalURL( $query ) . $this->getFragmentForURL(), |
2341 | PROTO_CANONICAL |
2342 | ); |
2343 | ( new HookRunner( $services->getHookContainer() ) ) |
2344 | ->onGetCanonicalURL( $this, $url, $query ); |
2345 | return $url; |
2346 | } |
2347 | |
2348 | /** |
2349 | * Get the edit URL for this Title |
2350 | * |
2351 | * @return string The URL, or a null string if this is an interwiki link |
2352 | */ |
2353 | public function getEditURL() { |
2354 | if ( $this->isExternal() ) { |
2355 | return ''; |
2356 | } |
2357 | $s = $this->getLocalURL( 'action=edit' ); |
2358 | |
2359 | return $s; |
2360 | } |
2361 | |
2362 | /** |
2363 | * Is this title subject to title protection? |
2364 | * Title protection is the one applied against creation of such title. |
2365 | * |
2366 | * @deprecated since 1.37, use RestrictionStore::getCreateProtection() instead; |
2367 | * hard-deprecated since 1.43 |
2368 | * |
2369 | * @return array|bool An associative array representing any existent title |
2370 | * protection, or false if there's none. |
2371 | */ |
2372 | public function getTitleProtection() { |
2373 | wfDeprecated( __METHOD__, '1.37' ); |
2374 | return MediaWikiServices::getInstance()->getRestrictionStore()->getCreateProtection( $this ) |
2375 | ?: false; |
2376 | } |
2377 | |
2378 | /** |
2379 | * Remove any title protection due to page existing |
2380 | * |
2381 | * @deprecated since 1.37, do not use (this is only for WikiPage::onArticleCreate) |
2382 | */ |
2383 | public function deleteTitleProtection() { |
2384 | MediaWikiServices::getInstance()->getRestrictionStore()->deleteCreateProtection( $this ); |
2385 | } |
2386 | |
2387 | /** |
2388 | * Load restrictions from the page_restrictions table |
2389 | * |
2390 | * @deprecated since 1.37, no public replacement; hard-deprecated since 1.43 |
2391 | * |
2392 | * @param int $flags A bit field. If IDBAccessObject::READ_LATEST is set, skip replicas and read |
2393 | * from the primary DB. |
2394 | */ |
2395 | public function loadRestrictions( $flags = 0 ) { |
2396 | wfDeprecated( __METHOD__, '1.37' ); |
2397 | MediaWikiServices::getInstance()->getRestrictionStore()->loadRestrictions( $this, $flags ); |
2398 | } |
2399 | |
2400 | /** |
2401 | * Flush the protection cache in this object and force reload from the database. |
2402 | * This is used when updating protection from WikiPage::doUpdateRestrictions(). |
2403 | * |
2404 | * @deprecated since 1.37, now internal; hard-deprecated since 1.43 |
2405 | */ |
2406 | public function flushRestrictions() { |
2407 | wfDeprecated( __METHOD__, '1.37' ); |
2408 | MediaWikiServices::getInstance()->getRestrictionStore()->flushRestrictions( $this ); |
2409 | } |
2410 | |
2411 | /** |
2412 | * Purge expired restrictions from the page_restrictions table |
2413 | * |
2414 | * This will purge no more than $wgUpdateRowsPerQuery page_restrictions rows |
2415 | */ |
2416 | public static function purgeExpiredRestrictions() { |
2417 | if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ) { |
2418 | return; |
2419 | } |
2420 | |
2421 | DeferredUpdates::addUpdate( new AutoCommitUpdate( |
2422 | MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(), |
2423 | __METHOD__, |
2424 | static function ( IDatabase $dbw, $fname ) { |
2425 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
2426 | $ids = $dbw->newSelectQueryBuilder() |
2427 | ->select( 'pr_id' ) |
2428 | ->from( 'page_restrictions' ) |
2429 | ->where( $dbw->expr( 'pr_expiry', '<', $dbw->timestamp() ) ) |
2430 | ->limit( $config->get( MainConfigNames::UpdateRowsPerQuery ) ) // T135470 |
2431 | ->caller( $fname )->fetchFieldValues(); |
2432 | if ( $ids ) { |
2433 | $dbw->newDeleteQueryBuilder() |
2434 | ->deleteFrom( 'page_restrictions' ) |
2435 | ->where( [ 'pr_id' => $ids ] ) |
2436 | ->caller( $fname )->execute(); |
2437 | } |
2438 | |
2439 | $dbw->newDeleteQueryBuilder() |
2440 | ->deleteFrom( 'protected_titles' ) |
2441 | ->where( $dbw->expr( 'pt_expiry', '<', $dbw->timestamp() ) ) |
2442 | ->caller( $fname )->execute(); |
2443 | } |
2444 | ) ); |
2445 | } |
2446 | |
2447 | /** |
2448 | * Does this have subpages? (Warning, usually requires an extra DB query.) |
2449 | * |
2450 | * @return bool |
2451 | */ |
2452 | public function hasSubpages() { |
2453 | if ( |
2454 | !MediaWikiServices::getInstance()->getNamespaceInfo()-> |
2455 | hasSubpages( $this->mNamespace ) |
2456 | ) { |
2457 | # Duh |
2458 | return false; |
2459 | } |
2460 | |
2461 | # We dynamically add a member variable for the purpose of this method |
2462 | # alone to cache the result. There's no point in having it hanging |
2463 | # around uninitialized in every Title object; therefore we only add it |
2464 | # if needed and don't declare it statically. |
2465 | if ( $this->mHasSubpages === null ) { |
2466 | $subpages = $this->getSubpages( 1 ); |
2467 | $this->mHasSubpages = $subpages instanceof TitleArrayFromResult && $subpages->count(); |
2468 | } |
2469 | |
2470 | return $this->mHasSubpages; |
2471 | } |
2472 | |
2473 | /** |
2474 | * Get all subpages of this page. |
2475 | * |
2476 | * @param int $limit Maximum number of subpages to fetch; -1 for no limit |
2477 | * @return TitleArrayFromResult|array TitleArrayFromResult, or empty array if this page's namespace |
2478 | * doesn't allow subpages |
2479 | */ |
2480 | public function getSubpages( $limit = -1 ) { |
2481 | if ( |
2482 | !MediaWikiServices::getInstance()->getNamespaceInfo()-> |
2483 | hasSubpages( $this->mNamespace ) |
2484 | ) { |
2485 | return []; |
2486 | } |
2487 | |
2488 | $services = MediaWikiServices::getInstance(); |
2489 | $pageStore = $services->getPageStore(); |
2490 | $titleFactory = $services->getTitleFactory(); |
2491 | $query = $pageStore->newSelectQueryBuilder() |
2492 | ->fields( $pageStore->getSelectFields() ) |
2493 | ->whereTitlePrefix( $this->getNamespace(), $this->getDBkey() . '/' ) |
2494 | ->caller( __METHOD__ ); |
2495 | if ( $limit > -1 ) { |
2496 | $query->limit( $limit ); |
2497 | } |
2498 | |
2499 | return $titleFactory->newTitleArrayFromResult( $query->fetchResultSet() ); |
2500 | } |
2501 | |
2502 | /** |
2503 | * Is there a version of this page in the deletion archive? |
2504 | * |
2505 | * @deprecated since 1.36. Use self::getDeletedEditsCount() |
2506 | * @return int The number of archived revisions |
2507 | */ |
2508 | public function isDeleted() { |
2509 | return $this->getDeletedEditsCount(); |
2510 | } |
2511 | |
2512 | /** |
2513 | * Is there a version of this page in the deletion archive? |
2514 | * |
2515 | * @since 1.36 |
2516 | * @return int The number of archived revisions |
2517 | */ |
2518 | public function getDeletedEditsCount() { |
2519 | if ( $this->mNamespace < 0 ) { |
2520 | return 0; |
2521 | } |
2522 | |
2523 | $dbr = $this->getDbProvider()->getReplicaDatabase(); |
2524 | $n = (int)$dbr->newSelectQueryBuilder() |
2525 | ->select( 'COUNT(*)' ) |
2526 | ->from( 'archive' ) |
2527 | ->where( [ 'ar_namespace' => $this->mNamespace, 'ar_title' => $this->mDbkeyform ] ) |
2528 | ->caller( __METHOD__ )->fetchField(); |
2529 | if ( $this->mNamespace === NS_FILE ) { |
2530 | $n += $dbr->newSelectQueryBuilder() |
2531 | ->select( 'COUNT(*)' ) |
2532 | ->from( 'filearchive' ) |
2533 | ->where( [ 'fa_name' => $this->mDbkeyform ] ) |
2534 | ->caller( __METHOD__ )->fetchField(); |
2535 | } |
2536 | return $n; |
2537 | } |
2538 | |
2539 | /** |
2540 | * Is there a version of this page in the deletion archive? |
2541 | * |
2542 | * @deprecated since 1.36, Use self::hasDeletedEdits() |
2543 | * @return bool |
2544 | */ |
2545 | public function isDeletedQuick() { |
2546 | return $this->hasDeletedEdits(); |
2547 | } |
2548 | |
2549 | /** |
2550 | * Is there a version of this page in the deletion archive? |
2551 | * |
2552 | * @since 1.36 |
2553 | * @return bool |
2554 | */ |
2555 | public function hasDeletedEdits() { |
2556 | if ( $this->mNamespace < 0 ) { |
2557 | return false; |
2558 | } |
2559 | $dbr = $this->getDbProvider()->getReplicaDatabase(); |
2560 | $deleted = (bool)$dbr->newSelectQueryBuilder() |
2561 | ->select( '1' ) |
2562 | ->from( 'archive' ) |
2563 | ->where( [ 'ar_namespace' => $this->mNamespace, 'ar_title' => $this->mDbkeyform ] ) |
2564 | ->caller( __METHOD__ )->fetchField(); |
2565 | if ( !$deleted && $this->mNamespace === NS_FILE ) { |
2566 | $deleted = (bool)$dbr->newSelectQueryBuilder() |
2567 | ->select( '1' ) |
2568 | ->from( 'filearchive' ) |
2569 | ->where( [ 'fa_name' => $this->mDbkeyform ] ) |
2570 | ->caller( __METHOD__ )->fetchField(); |
2571 | } |
2572 | return $deleted; |
2573 | } |
2574 | |
2575 | /** |
2576 | * Get the article ID for this Title from the link cache, |
2577 | * adding it if necessary |
2578 | * |
2579 | * @param int $flags A bitfield of IDBAccessObject::READ_* constants |
2580 | * @return int The ID |
2581 | */ |
2582 | public function getArticleID( $flags = 0 ) { |
2583 | if ( $this->mArticleID === -1 && !$this->canExist() ) { |
2584 | $this->mArticleID = 0; |
2585 | |
2586 | return $this->mArticleID; |
2587 | } |
2588 | |
2589 | if ( $this->mArticleID === -1 || $this->shouldReadLatest( $flags ) ) { |
2590 | $this->mArticleID = (int)$this->getFieldFromPageStore( 'page_id', $flags ); |
2591 | } |
2592 | |
2593 | return $this->mArticleID; |
2594 | } |
2595 | |
2596 | /** |
2597 | * Is this an article that is a redirect page? |
2598 | * Uses link cache, adding it if necessary. |
2599 | * |
2600 | * This is intended to provide fast access to page_is_redirect for linking. |
2601 | * In rare cases, there might not be a valid target in the redirect table |
2602 | * even though this function returns true. |
2603 | * |
2604 | * To find a redirect target, just call WikiPage::getRedirectTarget() and |
2605 | * check if it returns null, there's no need to call this first. |
2606 | * |
2607 | * @param int $flags A bitfield of IDBAccessObject::READ_* constants |
2608 | * @return bool |
2609 | */ |
2610 | public function isRedirect( $flags = 0 ) { |
2611 | if ( $this->shouldReadLatest( $flags ) || $this->mRedirect === null ) { |
2612 | $this->mRedirect = (bool)$this->getFieldFromPageStore( 'page_is_redirect', $flags ); |
2613 | } |
2614 | |
2615 | return $this->mRedirect; |
2616 | } |
2617 | |
2618 | /** |
2619 | * What is the length of this page? |
2620 | * Uses link cache, adding it if necessary |
2621 | * |
2622 | * @param int $flags A bitfield of IDBAccessObject::READ_* constants |
2623 | * @return int |
2624 | */ |
2625 | public function getLength( $flags = 0 ) { |
2626 | if ( $this->shouldReadLatest( $flags ) || $this->mLength < 0 ) { |
2627 | $this->mLength = (int)$this->getFieldFromPageStore( 'page_len', $flags ); |
2628 | } |
2629 | |
2630 | if ( $this->mLength < 0 ) { |
2631 | $this->mLength = 0; |
2632 | } |
2633 | |
2634 | return $this->mLength; |
2635 | } |
2636 | |
2637 | /** |
2638 | * What is the page_latest field for this page? |
2639 | * |
2640 | * @param int $flags A bitfield of IDBAccessObject::READ_* constants |
2641 | * @return int Int or 0 if the page doesn't exist |
2642 | */ |
2643 | public function getLatestRevID( $flags = 0 ) { |
2644 | if ( $this->shouldReadLatest( $flags ) || $this->mLatestID === false ) { |
2645 | $this->mLatestID = (int)$this->getFieldFromPageStore( 'page_latest', $flags ); |
2646 | } |
2647 | |
2648 | if ( !$this->mLatestID ) { |
2649 | $this->mLatestID = 0; |
2650 | } |
2651 | |
2652 | return $this->mLatestID; |
2653 | } |
2654 | |
2655 | /** |
2656 | * Inject a page ID, reset DB-loaded fields, and clear the link cache for this title |
2657 | * |
2658 | * This can be called on page insertion to allow loading of the new page_id without |
2659 | * having to create a new Title instance. Likewise with deletion. |
2660 | * |
2661 | * This is also used during page moves, to reflect the change in the relationship |
2662 | * between article ID and title text. |
2663 | * |
2664 | * @note This overrides Title::setContentModel() |
2665 | * |
2666 | * @param int|bool $id Page ID, 0 for non-existent, or false for "unknown" (lazy-load) |
2667 | */ |
2668 | public function resetArticleID( $id ) { |
2669 | if ( $id === false ) { |
2670 | $this->mArticleID = -1; |
2671 | } else { |
2672 | $this->mArticleID = (int)$id; |
2673 | } |
2674 | $this->mRedirect = null; |
2675 | $this->mLength = -1; |
2676 | $this->mLatestID = false; |
2677 | $this->mContentModel = false; |
2678 | $this->mForcedContentModel = false; |
2679 | $this->mEstimateRevisions = null; |
2680 | $this->mPageLanguage = null; |
2681 | $this->mDbPageLanguage = false; |
2682 | $this->mIsBigDeletion = null; |
2683 | |
2684 | $this->uncache(); |
2685 | MediaWikiServices::getInstance()->getLinkCache()->clearLink( $this ); |
2686 | MediaWikiServices::getInstance()->getRestrictionStore()->flushRestrictions( $this ); |
2687 | } |
2688 | |
2689 | public static function clearCaches() { |
2690 | if ( MediaWikiServices::hasInstance() ) { |
2691 | $linkCache = MediaWikiServices::getInstance()->getLinkCache(); |
2692 | $linkCache->clear(); |
2693 | } |
2694 | |
2695 | $titleCache = self::getTitleCache(); |
2696 | $titleCache->clear(); |
2697 | } |
2698 | |
2699 | /** |
2700 | * Capitalize a text string for a title if it belongs to a namespace that capitalizes |
2701 | * |
2702 | * @param string $text Containing title to capitalize |
2703 | * @param int $ns Namespace index, defaults to NS_MAIN |
2704 | * @return string Containing capitalized title |
2705 | */ |
2706 | public static function capitalize( $text, $ns = NS_MAIN ) { |
2707 | $services = MediaWikiServices::getInstance(); |
2708 | if ( $services->getNamespaceInfo()->isCapitalized( $ns ) ) { |
2709 | return $services->getContentLanguage()->ucfirst( $text ); |
2710 | } else { |
2711 | return $text; |
2712 | } |
2713 | } |
2714 | |
2715 | /** |
2716 | * Secure and split - main initialisation function for this object |
2717 | * |
2718 | * Assumes that $text is urldecoded |
2719 | * and uses underscores, but not otherwise munged. This function |
2720 | * removes illegal characters, splits off the interwiki and |
2721 | * namespace prefixes, sets the other forms, and canonicalizes |
2722 | * everything. |
2723 | * |
2724 | * If this method returns normally, the Title is valid. |
2725 | * |
2726 | * @param string $text |
2727 | * @param int|null $defaultNamespace |
2728 | * |
2729 | * @throws MalformedTitleException On malformed titles |
2730 | */ |
2731 | private function secureAndSplit( $text, $defaultNamespace = null ) { |
2732 | $defaultNamespace ??= self::DEFAULT_NAMESPACE; |
2733 | |
2734 | // @note: splitTitleString() is a temporary hack to allow TitleParser to share |
2735 | // the parsing code with Title, while avoiding massive refactoring. |
2736 | // @todo: get rid of secureAndSplit, refactor parsing code. |
2737 | $titleParser = MediaWikiServices::getInstance()->getTitleParser(); |
2738 | // MalformedTitleException can be thrown here |
2739 | $parts = $titleParser->splitTitleString( $text, $defaultNamespace ); |
2740 | |
2741 | # Fill fields |
2742 | $this->setFragment( '#' . $parts['fragment'] ); |
2743 | $this->mInterwiki = $parts['interwiki']; |
2744 | $this->mLocalInterwiki = $parts['local_interwiki']; |
2745 | $this->mNamespace = $parts['namespace']; |
2746 | |
2747 | $this->mDbkeyform = $parts['dbkey']; |
2748 | $this->mUrlform = wfUrlencode( $this->mDbkeyform ); |
2749 | $this->mTextform = strtr( $this->mDbkeyform, '_', ' ' ); |
2750 | |
2751 | // splitTitleString() guarantees that this title is valid. |
2752 | $this->mIsValid = true; |
2753 | |
2754 | # We already know that some pages won't be in the database! |
2755 | if ( $this->isExternal() || $this->isSpecialPage() || $this->mTextform === '' ) { |
2756 | $this->mArticleID = 0; |
2757 | } |
2758 | } |
2759 | |
2760 | /** |
2761 | * Get an array of Title objects linking to this Title |
2762 | * Also stores the IDs in the link cache. |
2763 | * |
2764 | * WARNING: do not use this function on arbitrary user-supplied titles! |
2765 | * On heavily-used templates it will max out the memory. |
2766 | * |
2767 | * @param array $options May be FOR UPDATE |
2768 | * @param string $table Table name |
2769 | * @param string $prefix Fields prefix |
2770 | * @return Title[] |
2771 | */ |
2772 | public function getLinksTo( $options = [], $table = 'pagelinks', $prefix = 'pl' ) { |
2773 | if ( count( $options ) > 0 ) { |
2774 | $db = $this->getDbProvider()->getPrimaryDatabase(); |
2775 | } else { |
2776 | $db = $this->getDbProvider()->getReplicaDatabase(); |
2777 | } |
2778 | |
2779 | $linksMigration = MediaWikiServices::getInstance()->getLinksMigration(); |
2780 | if ( isset( $linksMigration::$mapping[$table] ) ) { |
2781 | $titleConds = $linksMigration->getLinksConditions( $table, $this ); |
2782 | } else { |
2783 | $titleConds = [ |
2784 | "{$prefix}_namespace" => $this->mNamespace, |
2785 | "{$prefix}_title" => $this->mDbkeyform |
2786 | ]; |
2787 | } |
2788 | |
2789 | $res = $db->newSelectQueryBuilder() |
2790 | ->select( LinkCache::getSelectFields() ) |
2791 | ->from( $table ) |
2792 | ->join( 'page', null, "{$prefix}_from=page_id" ) |
2793 | ->where( $titleConds ) |
2794 | ->options( $options ) |
2795 | ->caller( __METHOD__ ) |
2796 | ->fetchResultSet(); |
2797 | |
2798 | $retVal = []; |
2799 | if ( $res->numRows() ) { |
2800 | $linkCache = MediaWikiServices::getInstance()->getLinkCache(); |
2801 | foreach ( $res as $row ) { |
2802 | $titleObj = self::makeTitle( $row->page_namespace, $row->page_title ); |
2803 | if ( $titleObj ) { |
2804 | $linkCache->addGoodLinkObjFromRow( $titleObj, $row ); |
2805 | $retVal[] = $titleObj; |
2806 | } |
2807 | } |
2808 | } |
2809 | return $retVal; |
2810 | } |
2811 | |
2812 | /** |
2813 | * Get an array of Title objects using this Title as a template |
2814 | * Also stores the IDs in the link cache. |
2815 | * |
2816 | * WARNING: do not use this function on arbitrary user-supplied titles! |
2817 | * On heavily-used templates it will max out the memory. |
2818 | * |
2819 | * @param array $options Query option to Database::select() |
2820 | * @return Title[] |
2821 | */ |
2822 | public function getTemplateLinksTo( $options = [] ) { |
2823 | return $this->getLinksTo( $options, 'templatelinks', 'tl' ); |
2824 | } |
2825 | |
2826 | /** |
2827 | * Get an array of Title objects linked from this Title |
2828 | * Also stores the IDs in the link cache. |
2829 | * |
2830 | * WARNING: do not use this function on arbitrary user-supplied titles! |
2831 | * On heavily-used templates it will max out the memory. |
2832 | * |
2833 | * @param array $options Query option to Database::select() |
2834 | * @param string $table Table name |
2835 | * @param string $prefix Fields prefix |
2836 | * @return Title[] List of Titles linking here |
2837 | */ |
2838 | public function getLinksFrom( $options = [], $table = 'pagelinks', $prefix = 'pl' ) { |
2839 | $id = $this->getArticleID(); |
2840 | |
2841 | # If the page doesn't exist; there can't be any link from this page |
2842 | if ( !$id ) { |
2843 | return []; |
2844 | } |
2845 | |
2846 | $db = $this->getDbProvider()->getReplicaDatabase(); |
2847 | $linksMigration = MediaWikiServices::getInstance()->getLinksMigration(); |
2848 | |
2849 | $queryBuilder = $db->newSelectQueryBuilder(); |
2850 | if ( isset( $linksMigration::$mapping[$table] ) ) { |
2851 | [ $blNamespace, $blTitle ] = $linksMigration->getTitleFields( $table ); |
2852 | $linktargetQueryInfo = $linksMigration->getQueryInfo( $table ); |
2853 | $queryBuilder->queryInfo( $linktargetQueryInfo ); |
2854 | } else { |
2855 | $blNamespace = "{$prefix}_namespace"; |
2856 | $blTitle = "{$prefix}_title"; |
2857 | $queryBuilder->select( [ $blNamespace, $blTitle ] ) |
2858 | ->from( $table ); |
2859 | } |
2860 | |
2861 | $pageQuery = WikiPage::getQueryInfo(); |
2862 | $res = $queryBuilder |
2863 | ->where( [ "{$prefix}_from" => $id ] ) |
2864 | ->leftJoin( 'page', null, [ "page_namespace=$blNamespace", "page_title=$blTitle" ] ) |
2865 | ->fields( $pageQuery['fields'] ) |
2866 | ->options( $options ) |
2867 | ->caller( __METHOD__ ) |
2868 | ->fetchResultSet(); |
2869 | |
2870 | $retVal = []; |
2871 | $linkCache = MediaWikiServices::getInstance()->getLinkCache(); |
2872 | foreach ( $res as $row ) { |
2873 | if ( $row->page_id ) { |
2874 | $titleObj = self::newFromRow( $row ); |
2875 | } else { |
2876 | $titleObj = self::makeTitle( $row->$blNamespace, $row->$blTitle ); |
2877 | $linkCache->addBadLinkObj( $titleObj ); |
2878 | } |
2879 | $retVal[] = $titleObj; |
2880 | } |
2881 | |
2882 | return $retVal; |
2883 | } |
2884 | |
2885 | /** |
2886 | * Get an array of Title objects used on this Title as a template |
2887 | * Also stores the IDs in the link cache. |
2888 | * |
2889 | * WARNING: do not use this function on arbitrary user-supplied titles! |
2890 | * On heavily-used templates it will max out the memory. |
2891 | * |
2892 | * @param array $options May be FOR UPDATE |
2893 | * @return Title[] |
2894 | */ |
2895 | public function getTemplateLinksFrom( $options = [] ) { |
2896 | return $this->getLinksFrom( $options, 'templatelinks', 'tl' ); |
2897 | } |
2898 | |
2899 | /** |
2900 | * Locks the page row and check if this page is single revision redirect |
2901 | * |
2902 | * This updates the cached fields of this instance via Title::loadFromRow() |
2903 | * |
2904 | * @return bool |
2905 | */ |
2906 | public function isSingleRevRedirect() { |
2907 | $dbw = $this->getDbProvider()->getPrimaryDatabase(); |
2908 | $dbw->startAtomic( __METHOD__ ); |
2909 | $pageStore = MediaWikiServices::getInstance()->getPageStore(); |
2910 | |
2911 | $row = $dbw->newSelectQueryBuilder() |
2912 | ->select( $pageStore->getSelectFields() ) |
2913 | ->from( 'page' ) |
2914 | ->where( $this->pageCond() ) |
2915 | ->caller( __METHOD__ )->fetchRow(); |
2916 | // Update the cached fields |
2917 | $this->loadFromRow( $row ); |
2918 | |
2919 | if ( $this->mRedirect && $this->mLatestID ) { |
2920 | $isSingleRevRedirect = !$dbw->newSelectQueryBuilder() |
2921 | ->select( '1' ) |
2922 | ->forUpdate() |
2923 | ->from( 'revision' ) |
2924 | ->where( [ 'rev_page' => $this->mArticleID, $dbw->expr( 'rev_id', '!=', (int)$this->mLatestID ) ] ) |
2925 | ->caller( __METHOD__ )->fetchField(); |
2926 | } else { |
2927 | $isSingleRevRedirect = false; |
2928 | } |
2929 | |
2930 | $dbw->endAtomic( __METHOD__ ); |
2931 | |
2932 | return $isSingleRevRedirect; |
2933 | } |
2934 | |
2935 | /** |
2936 | * Get categories to which this Title belongs and return an array of |
2937 | * categories' names. |
2938 | * |
2939 | * @return string[] Array of parents in the form: |
2940 | * $parent => $currentarticle |
2941 | */ |
2942 | public function getParentCategories() { |
2943 | $data = []; |
2944 | |
2945 | $titleKey = $this->getArticleID(); |
2946 | |
2947 | if ( $titleKey === 0 ) { |
2948 | return $data; |
2949 | } |
2950 | |
2951 | $migrationStage = MediaWikiServices::getInstance()->getMainConfig()->get( |
2952 | MainConfigNames::CategoryLinksSchemaMigrationStage |
2953 | ); |
2954 | |
2955 | $dbr = $this->getDbProvider()->getReplicaDatabase(); |
2956 | $queryBuilder = $dbr->newSelectQueryBuilder() |
2957 | ->from( 'categorylinks' ) |
2958 | ->where( [ 'cl_from' => $titleKey ] ); |
2959 | |
2960 | if ( $migrationStage & SCHEMA_COMPAT_READ_OLD ) { |
2961 | $queryBuilder->select( 'cl_to' ); |
2962 | } else { |
2963 | $queryBuilder->field( 'lt_title', 'cl_to' ) |
2964 | ->join( 'linktarget', null, 'cl_target_id = lt_id' ) |
2965 | ->where( [ 'lt_namespace' => NS_CATEGORY ] ); |
2966 | } |
2967 | $res = $queryBuilder->caller( __METHOD__ )->fetchResultSet(); |
2968 | |
2969 | if ( $res->numRows() > 0 ) { |
2970 | $contLang = MediaWikiServices::getInstance()->getContentLanguage(); |
2971 | foreach ( $res as $row ) { |
2972 | // $data[] = Title::newFromText( $contLang->getNsText ( NS_CATEGORY ).':'.$row->cl_to); |
2973 | $data[$contLang->getNsText( NS_CATEGORY ) . ':' . $row->cl_to] = |
2974 | $this->getFullText(); |
2975 | } |
2976 | } |
2977 | return $data; |
2978 | } |
2979 | |
2980 | /** |
2981 | * Get a tree of parent categories |
2982 | * |
2983 | * @param array $children Array with the children in the keys, to check for circular refs |
2984 | * @return array Tree of parent categories |
2985 | */ |
2986 | public function getParentCategoryTree( $children = [] ) { |
2987 | $stack = []; |
2988 | $parents = $this->getParentCategories(); |
2989 | |
2990 | if ( $parents ) { |
2991 | foreach ( $parents as $parent => $current ) { |
2992 | if ( array_key_exists( $parent, $children ) ) { |
2993 | # Circular reference |
2994 | $stack[$parent] = []; |
2995 | } else { |
2996 | $nt = self::newFromText( $parent ); |
2997 | if ( $nt ) { |
2998 | $stack[$parent] = $nt->getParentCategoryTree( $children + [ $parent => 1 ] ); |
2999 | } |
3000 | } |
3001 | } |
3002 | } |
3003 | |
3004 | return $stack; |
3005 | } |
3006 | |
3007 | /** |
3008 | * Get an associative array for selecting this title from |
3009 | * the "page" table |
3010 | * |
3011 | * @return array Array suitable for the $where parameter of DB::select() |
3012 | */ |
3013 | public function pageCond() { |
3014 | if ( $this->mArticleID > 0 ) { |
3015 | // PK avoids secondary lookups in InnoDB, shouldn't hurt other DBs |
3016 | return [ 'page_id' => $this->mArticleID ]; |
3017 | } else { |
3018 | return [ 'page_namespace' => $this->mNamespace, 'page_title' => $this->mDbkeyform ]; |
3019 | } |
3020 | } |
3021 | |
3022 | /** |
3023 | * Check if this is a new page. |
3024 | * |
3025 | * @note This returns false if the page does not exist. |
3026 | * @param int $flags one of the READ_XXX constants. |
3027 | * |
3028 | * @return bool |
3029 | */ |
3030 | public function isNewPage( $flags = IDBAccessObject::READ_NORMAL ) { |
3031 | // NOTE: we rely on PHP casting "0" to false here. |
3032 | return (bool)$this->getFieldFromPageStore( 'page_is_new', $flags ); |
3033 | } |
3034 | |
3035 | /** |
3036 | * Check whether the number of revisions of this page surpasses $wgDeleteRevisionsLimit |
3037 | * @deprecated since 1.37 External callers shouldn't need to know about this. |
3038 | * |
3039 | * @return bool |
3040 | */ |
3041 | public function isBigDeletion() { |
3042 | global $wgDeleteRevisionsLimit; |
3043 | |
3044 | if ( !$wgDeleteRevisionsLimit ) { |
3045 | return false; |
3046 | } |
3047 | |
3048 | if ( $this->mIsBigDeletion === null ) { |
3049 | $dbr = $this->getDbProvider()->getReplicaDatabase(); |
3050 | $revCount = $dbr->newSelectQueryBuilder() |
3051 | ->select( '1' ) |
3052 | ->from( 'revision' ) |
3053 | ->where( [ 'rev_page' => $this->getArticleID() ] ) |
3054 | ->limit( $wgDeleteRevisionsLimit + 1 ) |
3055 | ->caller( __METHOD__ )->fetchRowCount(); |
3056 | |
3057 | $this->mIsBigDeletion = $revCount > $wgDeleteRevisionsLimit; |
3058 | } |
3059 | |
3060 | return $this->mIsBigDeletion; |
3061 | } |
3062 | |
3063 | /** |
3064 | * Get the approximate revision count of this page. |
3065 | * |
3066 | * @return int |
3067 | */ |
3068 | public function estimateRevisionCount() { |
3069 | if ( !$this->exists() ) { |
3070 | return 0; |
3071 | } |
3072 | |
3073 | if ( $this->mEstimateRevisions === null ) { |
3074 | $dbr = $this->getDbProvider()->getReplicaDatabase(); |
3075 | $this->mEstimateRevisions = $dbr->newSelectQueryBuilder() |
3076 | ->select( '*' ) |
3077 | ->from( 'revision' ) |
3078 | ->where( [ 'rev_page' => $this->getArticleID() ] ) |
3079 | ->caller( __METHOD__ ) |
3080 | ->estimateRowCount(); |
3081 | } |
3082 | |
3083 | return $this->mEstimateRevisions; |
3084 | } |
3085 | |
3086 | /** |
3087 | * Compares with another Title. |
3088 | * |
3089 | * A Title object is considered equal to another Title if it has the same text, |
3090 | * the same interwiki prefix, and the same namespace. |
3091 | * |
3092 | * @note This is different from {@see LinkTarget::isSameLinkAs} which also compares the fragment |
3093 | * part. |
3094 | * |
3095 | * @phpcs:disable MediaWiki.Commenting.FunctionComment.ObjectTypeHintParam |
3096 | * @param Title|object $other |
3097 | * |
3098 | * @return bool |
3099 | */ |
3100 | public function equals( object $other ) { |
3101 | // NOTE: In contrast to isSameLinkAs(), this ignores the fragment part! |
3102 | // NOTE: In contrast to isSamePageAs(), this ignores the page ID! |
3103 | // NOTE: === is necessary for proper matching of number-like titles |
3104 | return $other instanceof Title |
3105 | && $this->getInterwiki() === $other->getInterwiki() |
3106 | && $this->getNamespace() === $other->getNamespace() |
3107 | && $this->getDBkey() === $other->getDBkey(); |
3108 | } |
3109 | |
3110 | /** |
3111 | * @inheritDoc |
3112 | * @since 1.36 |
3113 | */ |
3114 | public function isSamePageAs( PageReference $other ): bool { |
3115 | // NOTE: keep in sync with PageReferenceValue::isSamePageAs()! |
3116 | return $this->getWikiId() === $other->getWikiId() |
3117 | && $this->getNamespace() === $other->getNamespace() |
3118 | && $this->getDBkey() === $other->getDBkey(); |
3119 | } |
3120 | |
3121 | /** |
3122 | * Check if this title is a subpage of another title |
3123 | * |
3124 | * @param Title $title |
3125 | * @return bool |
3126 | */ |
3127 | public function isSubpageOf( Title $title ) { |
3128 | return $this->mInterwiki === $title->mInterwiki |
3129 | && $this->mNamespace == $title->mNamespace |
3130 | && str_starts_with( $this->mDbkeyform, $title->mDbkeyform . '/' ); |
3131 | } |
3132 | |
3133 | /** |
3134 | * Check if page exists. For historical reasons, this function simply |
3135 | * checks for the existence of the title in the page table, and will |
3136 | * thus return false for interwiki links, special pages and the like. |
3137 | * If you want to know if a title can be meaningfully viewed, you should |
3138 | * probably call the isKnown() method instead. |
3139 | * |
3140 | * @param int $flags A bitfield of IDBAccessObject::READ_* constants |
3141 | * @return bool |
3142 | */ |
3143 | public function exists( $flags = 0 ): bool { |
3144 | $exists = $this->getArticleID( $flags ) != 0; |
3145 | ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )->onTitleExists( $this, $exists ); |
3146 | return $exists; |
3147 | } |
3148 | |
3149 | /** |
3150 | * Should links to this title be shown as potentially viewable (i.e. as |
3151 | * "bluelinks"), even if there's no record by this title in the page |
3152 | * table? |
3153 | * |
3154 | * This function is semi-deprecated for public use, as well as somewhat |
3155 | * misleadingly named. You probably just want to call isKnown(), which |
3156 | * calls this function internally. |
3157 | * |
3158 | * (ISSUE: Most of these checks are cheap, but the file existence check |
3159 | * can potentially be quite expensive. Including it here fixes a lot of |
3160 | * existing code, but we might want to add an optional parameter to skip |
3161 | * it and any other expensive checks.) |
3162 | * |
3163 | * @return bool |
3164 | */ |
3165 | public function isAlwaysKnown() { |
3166 | $isKnown = null; |
3167 | |
3168 | $services = MediaWikiServices::getInstance(); |
3169 | ( new HookRunner( $services->getHookContainer() ) )->onTitleIsAlwaysKnown( $this, $isKnown ); |
3170 | |
3171 | if ( $isKnown !== null ) { |
3172 | return $isKnown; |
3173 | } |
3174 | |
3175 | if ( $this->isExternal() ) { |
3176 | return true; // any interwiki link might be viewable, for all we know |
3177 | } |
3178 | |
3179 | switch ( $this->mNamespace ) { |
3180 | case NS_MEDIA: |
3181 | case NS_FILE: |
3182 | // file exists, possibly in a foreign repo |
3183 | return (bool)$services->getRepoGroup()->findFile( $this ); |
3184 | case NS_SPECIAL: |
3185 | // valid special page |
3186 | return $services->getSpecialPageFactory()->exists( $this->mDbkeyform ); |
3187 | case NS_MAIN: |
3188 | // selflink, possibly with fragment |
3189 | return $this->mDbkeyform == ''; |
3190 | case NS_MEDIAWIKI: |
3191 | // known system message |
3192 | return $this->hasSourceText() !== false; |
3193 | default: |
3194 | return false; |
3195 | } |
3196 | } |
3197 | |
3198 | /** |
3199 | * Does this title refer to a page that can (or might) be meaningfully |
3200 | * viewed? In particular, this function may be used to determine if |
3201 | * links to the title should be rendered as "bluelinks" (as opposed to |
3202 | * "redlinks" to non-existent pages). |
3203 | * Adding something else to this function will cause inconsistency |
3204 | * since LinkHolderArray calls isAlwaysKnown() and does its own |
3205 | * page existence check. |
3206 | * |
3207 | * @return bool |
3208 | */ |
3209 | public function isKnown() { |
3210 | return $this->isAlwaysKnown() || $this->exists(); |
3211 | } |
3212 | |
3213 | /** |
3214 | * Does this page have source text? |
3215 | * |
3216 | * @return bool |
3217 | */ |
3218 | public function hasSourceText() { |
3219 | if ( $this->exists() ) { |
3220 | return true; |
3221 | } |
3222 | |
3223 | if ( $this->mNamespace === NS_MEDIAWIKI ) { |
3224 | $services = MediaWikiServices::getInstance(); |
3225 | // If the page doesn't exist but is a known system message, default |
3226 | // message content will be displayed, same for language subpages- |
3227 | // Use always content language to avoid loading hundreds of languages |
3228 | // to get the link color. |
3229 | $contLang = $services->getContentLanguage(); |
3230 | [ $name, ] = $services->getMessageCache()->figureMessage( |
3231 | $contLang->lcfirst( $this->getText() ) |
3232 | ); |
3233 | $message = wfMessage( $name )->inLanguage( $contLang )->useDatabase( false ); |
3234 | return $message->exists(); |
3235 | } |
3236 | |
3237 | return false; |
3238 | } |
3239 | |
3240 | /** |
3241 | * Get the default (plain) message contents for a page that overrides an |
3242 | * interface message key. |
3243 | * |
3244 | * Primary use cases: |
3245 | * |
3246 | * - Article: |
3247 | * - Show default when viewing the page. The Article::getSubstituteContent |
3248 | * method displays the default message content, instead of the |
3249 | * 'noarticletext' placeholder message normally used. |
3250 | * |
3251 | * - EditPage: |
3252 | * - Title of edit page. When creating an interface message override, |
3253 | * the editor is told they are "Editing the page", instead of |
3254 | * "Creating the page". (EditPage::setHeaders) |
3255 | * - Edit notice. The 'translateinterface' edit notice is shown when creating |
3256 | * or editing an interface message override. (EditPage::showIntro) |
3257 | * - Opening the editor. The contents of the localisation message are used |
3258 | * as contents of the editor when creating a new page in the MediaWiki |
3259 | * namespace. This simplifies the process for editors when "changing" |
3260 | * an interface message by creating an override. (EditPage::getContentObject) |
3261 | * - Showing a diff. The left-hand side of a diff when an editor is |
3262 | * previewing their changes before saving the creation of a page in the |
3263 | * MediaWiki namespace. (EditPage::showDiff) |
3264 | * - Disallowing a save. When attempting to create a MediaWiki-namespace |
3265 | * page with the proposed content matching the interface message default, |
3266 | * the save is rejected, the same way we disallow blank pages from being |
3267 | * created. (EditPage using DefaultTextConstraint) |
3268 | * |
3269 | * - ApiEditPage: |
3270 | * - Default content, when using the 'prepend' or 'append' feature. |
3271 | * |
3272 | * - SkinTemplate: |
3273 | * - Label the create action as "Edit", if the page can be an override. |
3274 | * |
3275 | * @return string|false |
3276 | */ |
3277 | public function getDefaultMessageText() { |
3278 | $message = $this->getDefaultSystemMessage(); |
3279 | |
3280 | return $message ? $message->plain() : false; |
3281 | } |
3282 | |
3283 | /** |
3284 | * Same as getDefaultMessageText, but returns a Message object. |
3285 | * |
3286 | * @see ::getDefaultMessageText |
3287 | * |
3288 | * @return ?Message |
3289 | */ |
3290 | public function getDefaultSystemMessage(): ?Message { |
3291 | if ( $this->mNamespace !== NS_MEDIAWIKI ) { // Just in case |
3292 | return null; |
3293 | } |
3294 | |
3295 | [ $name, $lang ] = MediaWikiServices::getInstance()->getMessageCache()->figureMessage( |
3296 | MediaWikiServices::getInstance()->getContentLanguage()->lcfirst( $this->getText() ) |
3297 | ); |
3298 | |
3299 | if ( wfMessage( $name )->inLanguage( $lang )->useDatabase( false )->exists() ) { |
3300 | return wfMessage( $name )->inLanguage( $lang ); |
3301 | } else { |
3302 | return null; |
3303 | } |
3304 | } |
3305 | |
3306 | /** |
3307 | * Updates page_touched for this page; called from LinksUpdate.php |
3308 | * |
3309 | * @param string|null $purgeTime [optional] TS_MW timestamp |
3310 | * @return bool True if the update succeeded |
3311 | */ |
3312 | public function invalidateCache( $purgeTime = null ) { |
3313 | if ( MediaWikiServices::getInstance()->getReadOnlyMode()->isReadOnly() ) { |
3314 | return false; |
3315 | } |
3316 | if ( $this->mArticleID === 0 ) { |
3317 | // avoid gap locking if we know it's not there |
3318 | return true; |
3319 | } |
3320 | |
3321 | $conds = $this->pageCond(); |
3322 | |
3323 | // Periodically recompute page_random (T309477). This mitigates bias on |
3324 | // Special:Random due deleted pages leaving "gaps" in the distribution. |
3325 | // |
3326 | // Optimization: Update page_random only for 10% of updates. |
3327 | // Optimization: Do this outside the main transaction to avoid locking for too long. |
3328 | // Optimization: Update page_random alongside page_touched to avoid extra database writes. |
3329 | DeferredUpdates::addUpdate( |
3330 | new AutoCommitUpdate( |
3331 | $this->getDbProvider()->getPrimaryDatabase(), |
3332 | __METHOD__, |
3333 | function ( IDatabase $dbw, $fname ) use ( $conds, $purgeTime ) { |
3334 | $dbTimestamp = $dbw->timestamp( $purgeTime ?: time() ); |
3335 | $update = $dbw->newUpdateQueryBuilder() |
3336 | ->update( 'page' ) |
3337 | ->set( [ 'page_touched' => $dbTimestamp ] ) |
3338 | ->where( $conds ) |
3339 | ->andWhere( $dbw->expr( 'page_touched', '<', $dbTimestamp ) ); |
3340 | |
3341 | if ( mt_rand( 1, 10 ) === 1 ) { |
3342 | $update->andSet( [ 'page_random' => wfRandom() ] ); |
3343 | } |
3344 | |
3345 | $update->caller( $fname )->execute(); |
3346 | |
3347 | MediaWikiServices::getInstance()->getLinkCache()->invalidateTitle( $this ); |
3348 | |
3349 | WikiModule::invalidateModuleCache( |
3350 | $this, null, null, $dbw->getDomainID() ); |
3351 | } |
3352 | ), |
3353 | DeferredUpdates::PRESEND |
3354 | ); |
3355 | |
3356 | return true; |
3357 | } |
3358 | |
3359 | /** |
3360 | * Update page_touched timestamps and send CDN purge messages for |
3361 | * pages linking to this title. May be sent to the job queue depending |
3362 | * on the number of links. Typically called on create and delete. |
3363 | */ |
3364 | public function touchLinks() { |
3365 | $jobs = []; |
3366 | $jobs[] = HTMLCacheUpdateJob::newForBacklinks( |
3367 | $this, |
3368 | 'pagelinks', |
3369 | [ 'causeAction' => 'page-touch' ] |
3370 | ); |
3371 | if ( $this->mNamespace === NS_CATEGORY ) { |
3372 | $jobs[] = HTMLCacheUpdateJob::newForBacklinks( |
3373 | $this, |
3374 | 'categorylinks', |
3375 | [ 'causeAction' => 'category-touch' ] |
3376 | ); |
3377 | } |
3378 | |
3379 | MediaWikiServices::getInstance()->getJobQueueGroup()->lazyPush( $jobs ); |
3380 | } |
3381 | |
3382 | /** |
3383 | * Get the last touched timestamp |
3384 | * |
3385 | * @param int $flags one of the READ_XXX constants. |
3386 | * @return string|false Last-touched timestamp |
3387 | */ |
3388 | public function getTouched( int $flags = IDBAccessObject::READ_NORMAL ) { |
3389 | $touched = $this->getFieldFromPageStore( 'page_touched', $flags ); |
3390 | return $touched ? MWTimestamp::convert( TS_MW, $touched ) : false; |
3391 | } |
3392 | |
3393 | /** |
3394 | * Generate strings used for xml 'id' names in monobook tabs |
3395 | * |
3396 | * @param string $prepend Defaults to 'nstab-' |
3397 | * @return string XML 'id' name |
3398 | */ |
3399 | public function getNamespaceKey( $prepend = 'nstab-' ) { |
3400 | // Gets the subject namespace of this title |
3401 | $nsInfo = MediaWikiServices::getInstance()->getNamespaceInfo(); |
3402 | $subjectNS = $nsInfo->getSubject( $this->mNamespace ); |
3403 | // Prefer canonical namespace name for HTML IDs |
3404 | $namespaceKey = $nsInfo->getCanonicalName( $subjectNS ); |
3405 | if ( $namespaceKey === false ) { |
3406 | // Fallback to localised text |
3407 | $namespaceKey = $this->getSubjectNsText(); |
3408 | } |
3409 | // Makes namespace key lowercase |
3410 | $namespaceKey = MediaWikiServices::getInstance()->getContentLanguage()->lc( $namespaceKey ); |
3411 | // Uses main |
3412 | if ( $namespaceKey == '' ) { |
3413 | $namespaceKey = 'main'; |
3414 | } |
3415 | // Changes file to image for backwards compatibility |
3416 | if ( $namespaceKey == 'file' ) { |
3417 | $namespaceKey = 'image'; |
3418 | } |
3419 | return $prepend . $namespaceKey; |
3420 | } |
3421 | |
3422 | /** |
3423 | * Get all extant redirects to this Title |
3424 | * |
3425 | * @param int|null $ns Single namespace to consider; null to consider all namespaces |
3426 | * @return Title[] |
3427 | */ |
3428 | public function getRedirectsHere( $ns = null ) { |
3429 | $redirs = []; |
3430 | |
3431 | $queryBuilder = $this->getDbProvider()->getReplicaDatabase()->newSelectQueryBuilder() |
3432 | ->select( [ 'page_namespace', 'page_title' ] ) |
3433 | ->from( 'redirect' ) |
3434 | ->join( 'page', null, 'rd_from = page_id' ) |
3435 | ->where( [ |
3436 | 'rd_namespace' => $this->mNamespace, |
3437 | 'rd_title' => $this->mDbkeyform, |
3438 | 'rd_interwiki' => $this->isExternal() ? $this->mInterwiki : '', |
3439 | ] ); |
3440 | |
3441 | if ( $ns !== null ) { |
3442 | $queryBuilder->andWhere( [ 'page_namespace' => $ns ] ); |
3443 | } |
3444 | |
3445 | $res = $queryBuilder->caller( __METHOD__ )->fetchResultSet(); |
3446 | |
3447 | foreach ( $res as $row ) { |
3448 | $redirs[] = self::newFromRow( $row ); |
3449 | } |
3450 | return $redirs; |
3451 | } |
3452 | |
3453 | /** |
3454 | * Check if this Title is a valid redirect target |
3455 | * |
3456 | * @return bool |
3457 | */ |
3458 | public function isValidRedirectTarget() { |
3459 | global $wgInvalidRedirectTargets; |
3460 | |
3461 | if ( $this->isSpecialPage() ) { |
3462 | // invalid redirect targets are stored in a global array, but explicitly disallow Userlogout here |
3463 | foreach ( [ 'Userlogout', ...$wgInvalidRedirectTargets ] as $target ) { |
3464 | if ( $this->isSpecial( $target ) ) { |
3465 | return false; |
3466 | } |
3467 | } |
3468 | return true; |
3469 | } |
3470 | |
3471 | // relative section links are not valid redirect targets (T278367) |
3472 | return $this->getDBkey() !== '' && $this->isValid(); |
3473 | } |
3474 | |
3475 | /** |
3476 | * Whether the magic words __INDEX__ and __NOINDEX__ function for this page. |
3477 | * |
3478 | * @return bool |
3479 | */ |
3480 | public function canUseNoindex() { |
3481 | global $wgExemptFromUserRobotsControl; |
3482 | |
3483 | $bannedNamespaces = $wgExemptFromUserRobotsControl ?? |
3484 | MediaWikiServices::getInstance()->getNamespaceInfo()->getContentNamespaces(); |
3485 | |
3486 | return !in_array( $this->mNamespace, $bannedNamespaces ); |
3487 | } |
3488 | |
3489 | /** |
3490 | * Returns the raw sort key to be used for categories, with the specified |
3491 | * prefix. This will be fed to Collation::getSortKey() to get a |
3492 | * binary sortkey that can be used for actual sorting. |
3493 | * |
3494 | * @param string $prefix The prefix to be used, specified using |
3495 | * {{defaultsort:}} or like [[Category:Foo|prefix]]. Empty for no |
3496 | * prefix. |
3497 | * @return string |
3498 | */ |
3499 | public function getCategorySortkey( $prefix = '' ) { |
3500 | $unprefixed = $this->getText(); |
3501 | |
3502 | // Anything that uses this hook should only depend |
3503 | // on the Title object passed in, and should probably |
3504 | // tell the users to run updateCollations.php --force |
3505 | // in order to re-sort existing category relations. |
3506 | ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) ) |
3507 | ->onGetDefaultSortkey( $this, $unprefixed ); |
3508 | if ( $prefix !== '' ) { |
3509 | # Separate with a line feed, so the unprefixed part is only used as |
3510 | # a tiebreaker when two pages have the exact same prefix. |
3511 | # In UCA, tab is the only character that can sort above LF |
3512 | # so we strip both of them from the original prefix. |
3513 | $prefix = strtr( $prefix, "\n\t", ' ' ); |
3514 | return "$prefix\n$unprefixed"; |
3515 | } |
3516 | return $unprefixed; |
3517 | } |
3518 | |
3519 | /** |
3520 | * Returns the page language code saved in the database, if $wgPageLanguageUseDB is set |
3521 | * to true in LocalSettings.php, otherwise returns null. If there is no language saved in |
3522 | * the database, it will return null. |
3523 | * |
3524 | * @param int $flags |
3525 | * |
3526 | * @return ?string |
3527 | */ |
3528 | private function getDbPageLanguageCode( int $flags = 0 ): ?string { |
3529 | global $wgPageLanguageUseDB; |
3530 | |
3531 | // check, if the page language could be saved in the database, and if so and |
3532 | // the value is not requested already, lookup the page language using PageStore |
3533 | if ( $wgPageLanguageUseDB && $this->mDbPageLanguage === false ) { |
3534 | $this->mDbPageLanguage = $this->getFieldFromPageStore( 'page_lang', $flags ) ?: null; |
3535 | } |
3536 | |
3537 | return $this->mDbPageLanguage ?: null; |
3538 | } |
3539 | |
3540 | /** |
3541 | * Returns the Language object from the page language code saved in the database. |
3542 | * If $wgPageLanguageUseDB is set to false or there is no language saved in the database |
3543 | * or the language code in the database is invalid or unsupported, it will return null. |
3544 | * |
3545 | * @return Language|null |
3546 | */ |
3547 | private function getDbPageLanguage(): ?Language { |
3548 | $languageCode = $this->getDbPageLanguageCode(); |
3549 | if ( $languageCode === null ) { |
3550 | return null; |
3551 | } |
3552 | $services = MediaWikiServices::getInstance(); |
3553 | if ( !$services->getLanguageNameUtils()->isKnownLanguageTag( $languageCode ) ) { |
3554 | return null; |
3555 | } |
3556 | return $services->getLanguageFactory()->getLanguage( $languageCode ); |
3557 | } |
3558 | |
3559 | /** |
3560 | * Get the language in which the content of this page is written in |
3561 | * wikitext. Defaults to content language, but in certain cases it can be |
3562 | * e.g. $wgLang (such as special pages, which are in the user language). |
3563 | * |
3564 | * @since 1.18 |
3565 | * @return Language |
3566 | */ |
3567 | public function getPageLanguage() { |
3568 | global $wgLanguageCode; |
3569 | if ( $this->isSpecialPage() ) { |
3570 | // special pages are in the user language |
3571 | return RequestContext::getMain()->getLanguage(); |
3572 | } |
3573 | |
3574 | // Checking if DB language is set |
3575 | $dbPageLanguage = $this->getDbPageLanguage(); |
3576 | if ( $dbPageLanguage ) { |
3577 | return $dbPageLanguage; |
3578 | } |
3579 | |
3580 | $services = MediaWikiServices::getInstance(); |
3581 | if ( !$this->mPageLanguage || $this->mPageLanguage[1] !== $wgLanguageCode ) { |
3582 | // Note that this may depend on user settings, so the cache should |
3583 | // be only per-request. |
3584 | // NOTE: ContentHandler::getPageLanguage() may need to load the |
3585 | // content to determine the page language! |
3586 | // Checking $wgLanguageCode hasn't changed for the benefit of unit |
3587 | // tests. |
3588 | $contentHandler = $services->getContentHandlerFactory() |
3589 | ->getContentHandler( $this->getContentModel() ); |
3590 | $langObj = $contentHandler->getPageLanguage( $this ); |
3591 | $this->mPageLanguage = [ $langObj->getCode(), $wgLanguageCode ]; |
3592 | } else { |
3593 | $langObj = $services->getLanguageFactory() |
3594 | ->getLanguage( $this->mPageLanguage[0] ); |
3595 | } |
3596 | |
3597 | return $langObj; |
3598 | } |
3599 | |
3600 | /** |
3601 | * Get the language in which the content of this page is written when |
3602 | * viewed by user. Defaults to content language, but in certain cases it can be |
3603 | * e.g. the user language (such as special pages). |
3604 | * |
3605 | * @deprecated since 1.42 Use ParserOutput::getLanguage instead. See also OutputPage::getContLangForJS. |
3606 | * Hard-deprecated since 1.43. |
3607 | * @since 1.20 |
3608 | * @return Language |
3609 | */ |
3610 | public function getPageViewLanguage() { |
3611 | wfDeprecated( __METHOD__, '1.42' ); |
3612 | $services = MediaWikiServices::getInstance(); |
3613 | |
3614 | if ( $this->isSpecialPage() ) { |
3615 | // If the user chooses a variant, the content is actually |
3616 | // in a language whose code is the variant code. |
3617 | $userLang = RequestContext::getMain()->getLanguage(); |
3618 | $variant = $this->getLanguageConverter( $userLang )->getPreferredVariant(); |
3619 | if ( $userLang->getCode() !== $variant ) { |
3620 | return $services->getLanguageFactory() |
3621 | ->getLanguage( $variant ); |
3622 | } |
3623 | |
3624 | return $userLang; |
3625 | } |
3626 | |
3627 | // Checking if DB language is set |
3628 | $pageLang = $this->getDbPageLanguage(); |
3629 | if ( $pageLang ) { |
3630 | $variant = $this->getLanguageConverter( $pageLang )->getPreferredVariant(); |
3631 | if ( $pageLang->getCode() !== $variant ) { |
3632 | return $services->getLanguageFactory() |
3633 | ->getLanguage( $variant ); |
3634 | } |
3635 | |
3636 | return $pageLang; |
3637 | } |
3638 | |
3639 | // @note Can't be cached persistently, depends on user settings. |
3640 | // @note ContentHandler::getPageViewLanguage() may need to load the |
3641 | // content to determine the page language! |
3642 | $contentHandler = $services->getContentHandlerFactory() |
3643 | ->getContentHandler( $this->getContentModel() ); |
3644 | $pageLang = $contentHandler->getPageViewLanguage( $this ); |
3645 | return $pageLang; |
3646 | } |
3647 | |
3648 | /** |
3649 | * Get a list of rendered edit notices for this page. |
3650 | * |
3651 | * Array is keyed by the original message key, and values are rendered using parseAsBlock, so |
3652 | * they will already be wrapped in paragraphs. |
3653 | * |
3654 | * @since 1.21 |
3655 | * @param int $oldid Revision ID that's being edited |
3656 | * @return string[] |
3657 | */ |
3658 | public function getEditNotices( $oldid = 0 ) { |
3659 | $notices = []; |
3660 | |
3661 | $editnotice_base = 'editnotice-' . $this->mNamespace; |
3662 | // Optional notice for the entire namespace |
3663 | $messages = [ $editnotice_base => 'namespace' ]; |
3664 | |
3665 | if ( |
3666 | MediaWikiServices::getInstance()->getNamespaceInfo()-> |
3667 | hasSubpages( $this->mNamespace ) |
3668 | ) { |
3669 | // Optional notice for page itself and any parent page |
3670 | foreach ( explode( '/', $this->mDbkeyform ) as $part ) { |
3671 | $editnotice_base .= '-' . $part; |
3672 | $messages[$editnotice_base] = 'base'; |
3673 | } |
3674 | } else { |
3675 | // Even if there are no subpages in namespace, we still don't want "/" in MediaWiki message keys |
3676 | $messages[$editnotice_base . '-' . strtr( $this->mDbkeyform, '/', '-' )] = 'page'; |
3677 | } |
3678 | |
3679 | foreach ( $messages as $editnoticeText => $class ) { |
3680 | // The following messages are used here: |
3681 | // * editnotice-0 |
3682 | // * editnotice-0-Title |
3683 | // * editnotice-0-Title-Subpage |
3684 | // * editnotice-… |
3685 | $msg = wfMessage( $editnoticeText )->page( $this ); |
3686 | if ( $msg->exists() ) { |
3687 | $html = $msg->parseAsBlock(); |
3688 | // Edit notices may have complex logic, but output nothing (T91715) |
3689 | if ( trim( $html ) !== '' ) { |
3690 | $notices[$editnoticeText] = Html::rawElement( |
3691 | 'div', |
3692 | [ 'class' => [ |
3693 | 'mw-editnotice', |
3694 | // The following classes are used here: |
3695 | // * mw-editnotice-namespace |
3696 | // * mw-editnotice-base |
3697 | // * mw-editnotice-page |
3698 | "mw-editnotice-$class", |
3699 | // The following classes are used here: |
3700 | // * mw-editnotice-0 |
3701 | // * mw-editnotice-… |
3702 | Sanitizer::escapeClass( "mw-$editnoticeText" ) |
3703 | ] ], |
3704 | $html |
3705 | ); |
3706 | } |
3707 | } |
3708 | } |
3709 | |
3710 | ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) ) |
3711 | ->onTitleGetEditNotices( $this, $oldid, $notices ); |
3712 | return $notices; |
3713 | } |
3714 | |
3715 | /** |
3716 | * @param string $field |
3717 | * @param int $flags Bitfield of IDBAccessObject::READ_* constants |
3718 | * @return string|false |
3719 | */ |
3720 | private function getFieldFromPageStore( $field, $flags ) { |
3721 | $pageStore = MediaWikiServices::getInstance()->getPageStore(); |
3722 | |
3723 | if ( !in_array( $field, $pageStore->getSelectFields(), true ) ) { |
3724 | throw new InvalidArgumentException( "Unknown field: $field" ); |
3725 | } |
3726 | |
3727 | if ( $flags === IDBAccessObject::READ_NORMAL && $this->mArticleID === 0 ) { |
3728 | // page does not exist |
3729 | return false; |
3730 | } |
3731 | |
3732 | if ( !$this->canExist() ) { |
3733 | return false; |
3734 | } |
3735 | |
3736 | $page = $pageStore->getPageByReference( $this, $flags ); |
3737 | |
3738 | if ( $page instanceof PageStoreRecord ) { |
3739 | return $page->getField( $field ); |
3740 | } else { |
3741 | // The page record failed to load, remember the page as non-existing. |
3742 | // Note that this can happen even if a page ID was known before under some |
3743 | // rare circumstances, if this method is called with the READ_LATEST bit set |
3744 | // and the page has been deleted since the ID had initially been determined. |
3745 | $this->mArticleID = 0; |
3746 | return false; |
3747 | } |
3748 | } |
3749 | |
3750 | /** |
3751 | * @return array |
3752 | */ |
3753 | public function __sleep() { |
3754 | return [ |
3755 | 'mNamespace', |
3756 | 'mDbkeyform', |
3757 | 'mFragment', |
3758 | 'mInterwiki', |
3759 | 'mLocalInterwiki', |
3760 | ]; |
3761 | } |
3762 | |
3763 | public function __wakeup() { |
3764 | $this->mArticleID = ( $this->mNamespace >= 0 ) ? -1 : 0; |
3765 | $this->mUrlform = wfUrlencode( $this->mDbkeyform ); |
3766 | $this->mTextform = strtr( $this->mDbkeyform, '_', ' ' ); |
3767 | } |
3768 | |
3769 | public function __clone() { |
3770 | $this->mInstanceCacheKey = null; |
3771 | } |
3772 | |
3773 | /** |
3774 | * Returns false to indicate that this Title belongs to the local wiki. |
3775 | * |
3776 | * @note The behavior of this method is considered undefined for interwiki links. |
3777 | * At the moment, this method always returns false. But this may change in the future. |
3778 | * |
3779 | * @since 1.36 |
3780 | * @return string|false Always self::LOCAL |
3781 | */ |
3782 | public function getWikiId() { |
3783 | return self::LOCAL; |
3784 | } |
3785 | |
3786 | /** |
3787 | * Returns the page ID. |
3788 | * |
3789 | * If this ID is 0, this means the page does not exist. |
3790 | * |
3791 | * @see getArticleID() |
3792 | * @since 1.36, since 1.35.6 as an alias of getArticleID() |
3793 | * |
3794 | * @param string|false $wikiId The wiki ID expected by the caller. |
3795 | * |
3796 | * @throws PreconditionException if this Title instance does not represent a proper page, |
3797 | * that is, if it is a section link, interwiki link, link to a special page, or such. |
3798 | * @throws PreconditionException if $wikiId is not false. |
3799 | * |
3800 | * @return int |
3801 | */ |
3802 | public function getId( $wikiId = self::LOCAL ): int { |
3803 | $this->assertWiki( $wikiId ); |
3804 | $this->assertProperPage(); |
3805 | return $this->getArticleID(); |
3806 | } |
3807 | |
3808 | /** |
3809 | * Code that requires this Title to be a "proper page" in the sense |
3810 | * defined by PageIdentity should call this method. |
3811 | * |
3812 | * For the purpose of the Title class, a proper page is one that can |
3813 | * exist in the page table. That is, a Title represents a proper page |
3814 | * if canExist() returns true. |
3815 | * |
3816 | * @see canExist() |
3817 | * |
3818 | * @throws PreconditionException |
3819 | */ |
3820 | private function assertProperPage() { |
3821 | Assert::precondition( |
3822 | $this->canExist(), |
3823 | 'This Title instance does not represent a proper page, but merely a link target.' |
3824 | ); |
3825 | } |
3826 | |
3827 | /** |
3828 | * Returns the page represented by this Title as a ProperPageIdentity. |
3829 | * The ProperPageIdentity returned by this method is guaranteed to be immutable. |
3830 | * If this Title does not represent a proper page, an exception is thrown. |
3831 | * |
3832 | * It is preferred to use this method rather than using the Title as a PageIdentity directly. |
3833 | * |
3834 | * @return ProperPageIdentity |
3835 | * @throws PreconditionException if the page is not a proper page, that is, if it is a section |
3836 | * link, interwiki link, link to a special page, or such. |
3837 | * @since 1.36 |
3838 | */ |
3839 | public function toPageIdentity(): ProperPageIdentity { |
3840 | // TODO: replace individual member fields with a PageIdentityValue that is always present |
3841 | |
3842 | $this->assertProperPage(); |
3843 | |
3844 | return new PageIdentityValue( |
3845 | $this->getId(), |
3846 | $this->getNamespace(), |
3847 | $this->getDBkey(), |
3848 | $this->getWikiId() |
3849 | ); |
3850 | } |
3851 | |
3852 | /** |
3853 | * Returns the page represented by this Title as a ProperPageRecord. |
3854 | * The PageRecord returned by this method is guaranteed to be immutable, |
3855 | * the page is guaranteed to exist. |
3856 | * |
3857 | * @note For now, this method queries the database on every call. |
3858 | * @since 1.36 |
3859 | * |
3860 | * @param int $flags A bitfield of IDBAccessObject::READ_* constants |
3861 | * |
3862 | * @return ExistingPageRecord |
3863 | * @throws PreconditionException if the page does not exist, or is not a proper page, |
3864 | * that is, if it is a section link, interwiki link, link to a special page, or such. |
3865 | */ |
3866 | public function toPageRecord( $flags = 0 ): ExistingPageRecord { |
3867 | // TODO: Cache this? Construct is more efficiently? |
3868 | |
3869 | $this->assertProperPage(); |
3870 | |
3871 | Assert::precondition( |
3872 | $this->exists( $flags ), |
3873 | 'This Title instance does not represent an existing page: ' . $this |
3874 | ); |
3875 | |
3876 | return new PageStoreRecord( |
3877 | (object)[ |
3878 | 'page_id' => $this->getArticleID( $flags ), |
3879 | 'page_namespace' => $this->getNamespace(), |
3880 | 'page_title' => $this->getDBkey(), |
3881 | 'page_wiki_id' => $this->getWikiId(), |
3882 | 'page_latest' => $this->getLatestRevID( $flags ), |
3883 | 'page_is_new' => $this->isNewPage( $flags ), |
3884 | 'page_is_redirect' => $this->isRedirect( $flags ), |
3885 | 'page_touched' => $this->getTouched( $flags ), |
3886 | 'page_lang' => $this->getDbPageLanguageCode( $flags ), |
3887 | ], |
3888 | PageIdentity::LOCAL |
3889 | ); |
3890 | } |
3891 | |
3892 | } |