Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
57.40% covered (warning)
57.40%
679 / 1183
53.33% covered (warning)
53.33%
88 / 165
CRAP
0.00% covered (danger)
0.00%
0 / 1
Title
57.45% covered (warning)
57.45%
679 / 1182
53.33% covered (warning)
53.33%
88 / 165
17055.57
0.00% covered (danger)
0.00%
0 / 1
 getLanguageConverter
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getPageLanguageConverter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDbProvider
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTitleFormatter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getInterwikiLookup
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newFromDBkey
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 newFromLinkTarget
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 castFromLinkTarget
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 newFromPageIdentity
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 castFromPageIdentity
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 newFromPageReference
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 castFromPageReference
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 newFromText
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
56
 newFromTextThrow
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
42
 uncache
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 newFromURL
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 getTitleCache
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 newFromID
100.00% covered (success)
100.00%
15 / 15
100.00% covered (success)
100.00%
1 / 1
2
 newFromRow
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 loadFromRow
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
72
 makeTitle
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 makeTitleSafe
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 newMainPage
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 legalChars
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 convertByteClassToUnicodeClass
91.23% covered (success)
91.23%
52 / 57
0.00% covered (danger)
0.00%
0 / 1
20.27
 makeName
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
6
 compare
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 isValid
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
8
 isLocal
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getInterwiki
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 wasLocalInterwiki
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isTrans
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getTransWikiID
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getTitleValue
72.73% covered (warning)
72.73%
8 / 11
0.00% covered (danger)
0.00%
0 / 1
3.18
 getText
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPartialURL
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDBkey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getNamespace
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 shouldReadLatest
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getContentModel
90.91% covered (success)
90.91%
10 / 11
0.00% covered (danger)
0.00%
0 / 1
6.03
 hasContentModel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setContentModel
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 lazyFillContentModel
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
12
 getNsText
66.67% covered (warning)
66.67%
8 / 12
0.00% covered (danger)
0.00%
0 / 1
5.93
 getSubjectNsText
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getTalkNsText
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 canHaveTalkPage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 canExist
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
6.22
 isSpecialPage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isSpecial
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 fixSpecialName
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 inNamespace
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 inNamespaces
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 hasSubjectNamespace
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 isContentPage
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 isMovable
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 isMainPage
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 isSubpage
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 isConversionTable
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 isWikitextPage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isSiteConfigPage
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 isUserConfigPage
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 getSkinFromConfigSubpage
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 isUserCssConfigPage
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 isUserJsonConfigPage
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 isUserJsConfigPage
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 isSiteCssConfigPage
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 isSiteJsonConfigPage
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 isSiteJsConfigPage
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
3
 isRawHtmlMessage
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 isTalkPage
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getTalkPage
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 getTalkPageIfDefined
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getSubjectPage
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 warnIfPageCannotExist
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 getOtherPage
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 getFragment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFragmentForURL
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 setFragment
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 createFragmentTarget
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 normalizeFragment
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 prefix
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
4.13
 getPrefixedDBkey
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getPrefixedText
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 __toString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFullText
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 findSubpageDivider
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 hasSubpagesEnabled
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getRootText
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 getRootTitle
50.00% covered (danger)
50.00%
5 / 10
0.00% covered (danger)
0.00%
0 / 1
4.12
 getBaseText
85.71% covered (warning)
85.71%
6 / 7
0.00% covered (danger)
0.00%
0 / 1
3.03
 getBaseTitle
50.00% covered (danger)
50.00%
5 / 10
0.00% covered (danger)
0.00%
0 / 1
4.12
 getSubpageText
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getSubpage
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getSubpageUrlForm
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getPrefixedURL
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getFullURL
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getFullUrlForRedirect
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getLocalURL
95.45% covered (success)
95.45%
42 / 44
0.00% covered (danger)
0.00%
0 / 1
20
 getLinkURL
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 getInternalURL
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getCanonicalURL
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
2
 getEditURL
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getTitleProtection
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 deleteTitleProtection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 loadRestrictions
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 flushRestrictions
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 purgeExpiredRestrictions
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
12
 hasSubpages
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 getSubpages
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 isDeleted
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDeletedEditsCount
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 isDeletedQuick
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 hasDeletedEdits
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 getArticleID
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 isRedirect
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getLength
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
 getLatestRevID
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 resetArticleID
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 clearCaches
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 capitalize
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 secureAndSplit
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
5
 getLinksTo
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
42
 getTemplateLinksTo
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLinksFrom
80.65% covered (warning)
80.65%
25 / 31
0.00% covered (danger)
0.00%
0 / 1
5.18
 getTemplateLinksFrom
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isSingleRevRedirect
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
12
 getParentCategories
93.75% covered (success)
93.75%
15 / 16
0.00% covered (danger)
0.00%
0 / 1
4.00
 getParentCategoryTree
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 pageCond
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 isNewPage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isBigDeletion
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 estimateRevisionCount
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 equals
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 isSamePageAs
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 isSubpageOf
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 exists
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 isAlwaysKnown
83.33% covered (warning)
83.33%
15 / 18
0.00% covered (danger)
0.00%
0 / 1
9.37
 isKnown
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 hasSourceText
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 getDefaultMessageText
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getDefaultSystemMessage
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
3.02
 invalidateCache
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
30
 touchLinks
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
6
 getTouched
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getNamespaceKey
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 getRedirectsHere
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 isValidRedirectTarget
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
5
 canUseNoindex
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getCategorySortkey
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getDbPageLanguageCode
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
30
 getDbPageLanguage
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getPageLanguage
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
30
 getPageViewLanguage
65.00% covered (warning)
65.00%
13 / 20
0.00% covered (danger)
0.00%
0 / 1
6.07
 getEditNotices
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
42
 getFieldFromPageStore
83.33% covered (warning)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
6.17
 __sleep
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 __wakeup
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 __clone
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWikiId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 assertProperPage
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 toPageIdentity
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 toPageRecord
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
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
25namespace MediaWiki\Title;
26
27use DBAccessObjectUtils;
28use HTMLCacheUpdateJob;
29use IDBAccessObject;
30use ILanguageConverter;
31use InvalidArgumentException;
32use Language;
33use MapCacheLRU;
34use MediaWiki\Cache\LinkCache;
35use MediaWiki\Context\RequestContext;
36use MediaWiki\DAO\WikiAwareEntityTrait;
37use MediaWiki\Deferred\AtomicSectionUpdate;
38use MediaWiki\Deferred\AutoCommitUpdate;
39use MediaWiki\Deferred\DeferredUpdates;
40use MediaWiki\HookContainer\HookRunner;
41use MediaWiki\Html\Html;
42use MediaWiki\Interwiki\InterwikiLookup;
43use MediaWiki\Linker\LinkTarget;
44use MediaWiki\MainConfigNames;
45use MediaWiki\MediaWikiServices;
46use MediaWiki\Message\Message;
47use MediaWiki\Page\ExistingPageRecord;
48use MediaWiki\Page\PageIdentity;
49use MediaWiki\Page\PageIdentityValue;
50use MediaWiki\Page\PageReference;
51use MediaWiki\Page\PageStoreRecord;
52use MediaWiki\Page\ProperPageIdentity;
53use MediaWiki\Parser\Sanitizer;
54use MediaWiki\Request\PathRouter;
55use MediaWiki\ResourceLoader\WikiModule;
56use MediaWiki\SpecialPage\SpecialPage;
57use MediaWiki\Utils\MWTimestamp;
58use MessageLocalizer;
59use MWException;
60use RuntimeException;
61use stdClass;
62use Stringable;
63use Wikimedia\Assert\Assert;
64use Wikimedia\Assert\PreconditionException;
65use Wikimedia\Parsoid\Core\LinkTarget as ParsoidLinkTarget;
66use Wikimedia\Parsoid\Core\LinkTargetTrait;
67use Wikimedia\Rdbms\IConnectionProvider;
68use Wikimedia\Rdbms\IDatabase;
69use WikiPage;
70
71/**
72 * Represents a title within MediaWiki.
73 * Optionally may contain an interwiki designation or namespace.
74 * @note This class can fetch various kinds of data from the database;
75 *       however, it does so inefficiently.
76 * @note Consider using a TitleValue object instead. TitleValue is more lightweight
77 *       and does not rely on global state or the database.
78 */
79class Title implements Stringable, LinkTarget, PageIdentity {
80    use WikiAwareEntityTrait;
81    use LinkTargetTrait;
82
83    /** @var MapCacheLRU|null */
84    private static $titleCache = null;
85
86    /**
87     * Title::newFromText maintains a cache to avoid expensive re-normalization of
88     * commonly used titles. On a batch operation this can become a memory leak
89     * if not bounded.
90     */
91    private const CACHE_MAX = 1000;
92
93    /**
94     * Flag for use with factory methods like newFromLinkTarget() that have
95     * a $forceClone parameter. If set, the method must return a new instance.
96     * Without this flag, some factory methods may return existing instances.as
97     *
98     * @since 1.33
99     */
100    public const NEW_CLONE = 'clone';
101
102    /** @var string Text form (spaces not underscores) of the main part */
103    private $mTextform = '';
104
105    /** @var string URL-encoded form of the main part */
106    private $mUrlform = '';
107
108    /** @var string Main part with underscores */
109    private $mDbkeyform = '';
110
111    /** @var int Namespace index, i.e. one of the NS_xxxx constants */
112    private $mNamespace = NS_MAIN;
113
114    /** @var string Interwiki prefix */
115    private $mInterwiki = '';
116
117    /** @var bool Was this Title created from a string with a local interwiki prefix? */
118    private $mLocalInterwiki = false;
119
120    /** @var string Title fragment (i.e. the bit after the #) */
121    private $mFragment = '';
122
123    /***************************************************************************/
124    // region   Private member variables
125    /** @name   Private member variables
126     * Please use the accessor functions instead.
127     * @internal
128     * @{
129     */
130
131    /** @var int Article ID, fetched from the link cache on demand */
132    public $mArticleID = -1;
133
134    /** @var int|false ID of most recent revision */
135    protected $mLatestID = false;
136
137    /**
138     * @var string|false ID of the page's content model, i.e. one of the
139     *   CONTENT_MODEL_XXX constants
140     */
141    private $mContentModel = false;
142
143    /**
144     * @var bool If a content model was forced via setContentModel()
145     *   this will be true to avoid having other code paths reset it
146     */
147    private $mForcedContentModel = false;
148
149    /** @var int|null Estimated number of revisions; null of not loaded */
150    private $mEstimateRevisions;
151
152    /**
153     * Text form including namespace/interwiki, initialised on demand
154     *
155     * Only public to share cache with TitleFormatter
156     *
157     * @internal
158     * @var string|null
159     */
160    public $prefixedText = null;
161
162    /**
163     * Namespace to assume when no namespace was passed to factory methods.
164     * This must be NS_MAIN, as it's hardcoded in several places. See T2696.
165     * Used primarily for {{transclusion}} tags.
166     */
167    private const DEFAULT_NAMESPACE = NS_MAIN;
168
169    /** @var int The page length, 0 for special pages */
170    protected $mLength = -1;
171
172    /** @var null|bool Is the article at this title a redirect? */
173    public $mRedirect = null;
174
175    /** @var bool Whether a page has any subpages */
176    private $mHasSubpages;
177
178    /** @var array|null The (string) language code of the page's language and content code. */
179    private $mPageLanguage;
180
181    /** @var string|false|null The page language code from the database, null if not saved in
182     * the database or false if not loaded, yet.
183     */
184    private $mDbPageLanguage = false;
185
186    /** @var TitleValue|null */
187    private $mTitleValue = null;
188
189    /** @var bool|null Would deleting this page be a big deletion? */
190    private $mIsBigDeletion = null;
191
192    /** @var bool|null Is the title known to be valid? */
193    private $mIsValid = null;
194
195    /** @var string|null The key of this instance in the internal Title instance cache */
196    private $mInstanceCacheKey = null;
197
198    // endregion -- end of private member variables
199    /** @} */
200    /***************************************************************************/
201
202    /**
203     * Shorthand for getting a Language Converter for specific language
204     * @param Language $language Language of converter
205     * @return ILanguageConverter
206     */
207    private function getLanguageConverter( $language ): ILanguageConverter {
208        return MediaWikiServices::getInstance()->getLanguageConverterFactory()
209            ->getLanguageConverter( $language );
210    }
211
212    /**
213     * Shorthand for getting a Language Converter for page's language
214     * @return ILanguageConverter
215     */
216    private function getPageLanguageConverter(): ILanguageConverter {
217        return $this->getLanguageConverter( $this->getPageLanguage() );
218    }
219
220    /**
221     * Shorthand for getting a database connection provider
222     * @return IConnectionProvider
223     */
224    private function getDbProvider(): IConnectionProvider {
225        return MediaWikiServices::getInstance()->getConnectionProvider();
226    }
227
228    /**
229     * B/C kludge: provide a TitleParser for use by Title.
230     * Ideally, Title would have no methods that need this.
231     * Avoid usage of this singleton by using TitleValue
232     * and the associated services when possible.
233     *
234     * @return TitleFormatter
235     */
236    private static function getTitleFormatter() {
237        return MediaWikiServices::getInstance()->getTitleFormatter();
238    }
239
240    /**
241     * B/C kludge: provide an InterwikiLookup for use by Title.
242     * Ideally, Title would have no methods that need this.
243     * Avoid usage of this singleton by using TitleValue
244     * and the associated services when possible.
245     *
246     * @return InterwikiLookup
247     */
248    private static function getInterwikiLookup() {
249        return MediaWikiServices::getInstance()->getInterwikiLookup();
250    }
251
252    private function __construct() {
253    }
254
255    /**
256     * Create a new Title from a prefixed DB key
257     *
258     * @param string $key The database key, which has underscores
259     *     instead of spaces, possibly including namespace and
260     *     interwiki prefixes
261     * @return Title|null Title, or null on an error
262     */
263    public static function newFromDBkey( $key ) {
264        $t = new self();
265
266        try {
267            $t->secureAndSplit( $key );
268            return $t;
269        } catch ( MalformedTitleException $ex ) {
270            return null;
271        }
272    }
273
274    /**
275     * Returns a Title given a LinkTarget.
276     * If the given LinkTarget is already a Title instance, that instance is returned,
277     * unless $forceClone is "clone". If $forceClone is "clone" and the given LinkTarget
278     * is already a Title instance, that instance is copied using the clone operator.
279     *
280     * @since 1.27
281     * @param ParsoidLinkTarget $linkTarget Assumed to be safe.
282     * @param string $forceClone set to NEW_CLONE to ensure a fresh instance is returned.
283     * @return Title
284     */
285    public static function newFromLinkTarget( ParsoidLinkTarget $linkTarget, $forceClone = '' ) {
286        if ( $linkTarget instanceof Title ) {
287            // Special case if it's already a Title object
288            if ( $forceClone === self::NEW_CLONE ) {
289                return clone $linkTarget;
290            } else {
291                return $linkTarget;
292            }
293        }
294        return self::makeTitle(
295            $linkTarget->getNamespace(),
296            $linkTarget->getText(),
297            $linkTarget->getFragment(),
298            $linkTarget->getInterwiki()
299        );
300    }
301
302    /**
303     * Same as newFromLinkTarget(), but if passed null, returns null.
304     *
305     * @since 1.34
306     * @param ParsoidLinkTarget|null $linkTarget Assumed to be safe (if not null).
307     * @return Title|null
308     */
309    public static function castFromLinkTarget( ?ParsoidLinkTarget $linkTarget ) {
310        if ( !$linkTarget ) {
311            return null;
312        }
313        return self::newFromLinkTarget( $linkTarget );
314    }
315
316    /**
317     * Return a Title for a given PageIdentity. If $pageIdentity is a Title,
318     * that Title is returned unchanged.
319     *
320     * @since 1.41
321     * @param PageIdentity $pageIdentity
322     * @return Title
323     */
324    public static function newFromPageIdentity( PageIdentity $pageIdentity ): Title {
325        return self::newFromPageReference( $pageIdentity );
326    }
327
328    /**
329     * Same as newFromPageIdentity(), but if passed null, returns null.
330     *
331     * @since 1.36
332     * @param PageIdentity|null $pageIdentity
333     * @return Title|null
334     */
335    public static function castFromPageIdentity( ?PageIdentity $pageIdentity ): ?Title {
336        return self::castFromPageReference( $pageIdentity );
337    }
338
339    /**
340     * Return a Title for a given Reference. If $pageReference is a Title,
341     * that Title is returned unchanged.
342     *
343     * @since 1.41
344     * @param PageReference $pageReference
345     * @return Title
346     */
347    public static function newFromPageReference( PageReference $pageReference ): Title {
348        if ( $pageReference instanceof Title ) {
349            return $pageReference;
350        }
351
352        $pageReference->assertWiki( self::LOCAL );
353        $title = self::makeTitle( $pageReference->getNamespace(), $pageReference->getDBkey() );
354
355        if ( $pageReference instanceof PageIdentity ) {
356            $title->mArticleID = $pageReference->getId();
357        }
358        return $title;
359    }
360
361    /**
362     * Same as newFromPageReference(), but if passed null, returns null.
363     *
364     * @since 1.37
365     * @param PageReference|null $pageReference
366     * @return Title|null
367     */
368    public static function castFromPageReference( ?PageReference $pageReference ): ?Title {
369        if ( !$pageReference ) {
370            return null;
371        }
372        return self::newFromPageReference( $pageReference );
373    }
374
375    /**
376     * Create a new Title from text, such as what one would find in a link.
377     * Decodes any HTML entities in the text.
378     * Titles returned by this method are guaranteed to be valid.
379     * Call canExist() to check if the Title represents an editable page.
380     *
381     * @note The Title instance returned by this method is not guaranteed to be a fresh instance.
382     * It may instead be a cached instance created previously, with references to it remaining
383     * elsewhere.
384     *
385     * @param string|int|null $text The link text; spaces, prefixes, and an
386     *   initial ':' indicating the main namespace are accepted.
387     * @param int $defaultNamespace The namespace to use if none is specified
388     *   by a prefix.  If you want to force a specific namespace even if
389     *   $text might begin with a namespace prefix, use makeTitle() or
390     *   makeTitleSafe().
391     * @throws InvalidArgumentException
392     * @return Title|null Title or null if the Title could not be parsed because
393     *         it is invalid.
394     */
395    public static function newFromText( $text, $defaultNamespace = NS_MAIN ) {
396        // DWIM: Integers can be passed in here when page titles are used as array keys.
397        if ( $text !== null && !is_string( $text ) && !is_int( $text ) ) {
398            throw new InvalidArgumentException( '$text must be a string.' );
399        }
400        if ( $text === null || $text === '' ) {
401            return null;
402        }
403
404        try {
405            return self::newFromTextThrow( (string)$text, (int)$defaultNamespace );
406        } catch ( MalformedTitleException $ex ) {
407            return null;
408        }
409    }
410
411    /**
412     * Like Title::newFromText(), but throws MalformedTitleException when the title is invalid,
413     * rather than returning null.
414     *
415     * Titles returned by this method are guaranteed to be valid.
416     * Call canExist() to check if the Title represents an editable page.
417     *
418     * @note The Title instance returned by this method is not guaranteed to be a fresh instance.
419     * It may instead be a cached instance created previously, with references to it remaining
420     * elsewhere.
421     *
422     * @see Title::newFromText
423     *
424     * @since 1.25
425     * @param string $text Title text to check
426     * @param int $defaultNamespace
427     * @throws MalformedTitleException If the title is invalid.
428     * @return Title
429     */
430    public static function newFromTextThrow( $text, $defaultNamespace = NS_MAIN ) {
431        if ( is_object( $text ) ) {
432            throw new InvalidArgumentException( '$text must be a string, given an object' );
433        } elseif ( $text === null ) {
434            // Legacy code relies on MalformedTitleException being thrown in this case
435            //  TODO: fix(happens when URL with no title in it is parsed).
436            throw new MalformedTitleException( 'title-invalid-empty' );
437        }
438
439        $titleCache = self::getTitleCache();
440
441        // Wiki pages often contain multiple links to the same page.
442        // Title normalization and parsing can become expensive on pages with many
443        // links, so we can save a little time by caching them.
444        if ( $defaultNamespace === NS_MAIN ) {
445            $t = $titleCache->get( $text );
446            if ( $t ) {
447                return $t;
448            }
449        }
450
451        // Convert things like &eacute; &#257; or &#x3017; into normalized (T16952) text
452        $filteredText = Sanitizer::decodeCharReferencesAndNormalize( $text );
453
454        $t = new Title();
455        $dbKeyForm = strtr( $filteredText, ' ', '_' );
456
457        $t->secureAndSplit( $dbKeyForm, (int)$defaultNamespace );
458        if ( $defaultNamespace === NS_MAIN ) {
459            $t->mInstanceCacheKey = $text;
460            $titleCache->set( $text, $t );
461        }
462        return $t;
463    }
464
465    /**
466     * Removes this instance from the internal title cache, so it can be modified in-place
467     * without polluting the cache (see T281337).
468     */
469    private function uncache() {
470        if ( $this->mInstanceCacheKey !== null ) {
471            $titleCache = self::getTitleCache();
472            $titleCache->clear( $this->mInstanceCacheKey );
473            $this->mInstanceCacheKey = null;
474        }
475    }
476
477    /**
478     * THIS IS NOT THE FUNCTION YOU WANT. Use Title::newFromText().
479     *
480     * Example of wrong and broken code:
481     * $title = Title::newFromURL( $request->getText( 'title' ) );
482     *
483     * Example of right code:
484     * $title = Title::newFromText( $request->getText( 'title' ) );
485     *
486     * Create a new Title from URL-encoded text. Ensures that
487     * the given title's length does not exceed the maximum.
488     *
489     * @param string $url The title, as might be taken from a URL
490     * @return Title|null The new object, or null on an error
491     */
492    public static function newFromURL( $url ) {
493        $t = new Title();
494
495        # For compatibility with old buggy URLs. "+" is usually not valid in titles,
496        # but some URLs used it as a space replacement and they still come
497        # from some external search tools.
498        if ( !str_contains( self::legalChars(), '+' ) ) {
499            $url = strtr( $url, '+', ' ' );
500        }
501
502        $dbKeyForm = strtr( $url, ' ', '_' );
503
504        try {
505            $t->secureAndSplit( $dbKeyForm );
506            return $t;
507        } catch ( MalformedTitleException $ex ) {
508            return null;
509        }
510    }
511
512    /**
513     * @return MapCacheLRU
514     */
515    private static function getTitleCache() {
516        if ( self::$titleCache === null ) {
517            self::$titleCache = new MapCacheLRU( self::CACHE_MAX );
518        }
519        return self::$titleCache;
520    }
521
522    /**
523     * Create a new Title from an article ID
524     *
525     * @param int $id The page_id corresponding to the Title to create
526     * @param int $flags Bitfield of IDBAccessObject::READ_* constants
527     * @return Title|null The new object, or null on an error
528     */
529    public static function newFromID( $id, $flags = 0 ) {
530        $pageStore = MediaWikiServices::getInstance()->getPageStore();
531        $dbr = DBAccessObjectUtils::getDBFromRecency(
532            MediaWikiServices::getInstance()->getConnectionProvider(),
533            $flags
534        );
535        $row = $dbr->newSelectQueryBuilder()
536            ->select( $pageStore->getSelectFields() )
537            ->from( 'page' )
538            ->where( [ 'page_id' => $id ] )
539            ->recency( $flags )
540            ->caller( __METHOD__ )->fetchRow();
541        if ( $row !== false ) {
542            $title = self::newFromRow( $row );
543        } else {
544            $title = null;
545        }
546
547        return $title;
548    }
549
550    /**
551     * Make a Title object from a DB row
552     *
553     * @param stdClass $row Object database row (needs at least page_title,page_namespace)
554     * @return Title
555     */
556    public static function newFromRow( $row ) {
557        $t = self::makeTitle( $row->page_namespace, $row->page_title );
558        $t->loadFromRow( $row );
559        return $t;
560    }
561
562    /**
563     * Load Title object fields from a DB row.
564     * If false is given, the title will be treated as non-existing.
565     *
566     * @param stdClass|false $row Database row
567     */
568    public function loadFromRow( $row ) {
569        if ( $row ) { // page found
570            if ( isset( $row->page_id ) ) {
571                $this->mArticleID = (int)$row->page_id;
572            }
573            if ( isset( $row->page_len ) ) {
574                $this->mLength = (int)$row->page_len;
575            }
576            if ( isset( $row->page_is_redirect ) ) {
577                $this->mRedirect = (bool)$row->page_is_redirect;
578            }
579            if ( isset( $row->page_latest ) ) {
580                $this->mLatestID = (int)$row->page_latest;
581            }
582            if ( isset( $row->page_content_model ) ) {
583                $this->lazyFillContentModel( $row->page_content_model );
584            } else {
585                $this->lazyFillContentModel( false ); // lazily-load getContentModel()
586            }
587            if ( isset( $row->page_lang ) ) {
588                $this->mDbPageLanguage = (string)$row->page_lang;
589            }
590        } else { // page not found
591            $this->mArticleID = 0;
592            $this->mLength = 0;
593            $this->mRedirect = false;
594            $this->mLatestID = 0;
595            $this->lazyFillContentModel( false ); // lazily-load getContentModel()
596        }
597    }
598
599    /**
600     * Create a new Title from a namespace index and a DB key.
601     *
602     * It's assumed that $ns and $title are safe, for instance when
603     * they came directly from the database or a special page name,
604     * not from user input.
605     *
606     * No validation is applied. For convenience, spaces are normalized
607     * to underscores, so that e.g. user_text fields can be used directly.
608     *
609     * @note This method may return Title objects that are "invalid"
610     * according to the isValid() method. This is usually caused by
611     * configuration changes: e.g. a namespace that was once defined is
612     * no longer configured, or a character that was once allowed in
613     * titles is now forbidden.
614     *
615     * @param int $ns The namespace of the article
616     * @param string $title The unprefixed database key form
617     * @param string $fragment The link fragment (after the "#")
618     * @param string $interwiki The interwiki prefix
619     * @return Title The new object
620     */
621    public static function makeTitle( $ns, $title, $fragment = '', $interwiki = '' ) {
622        $t = new Title();
623        $t->mInterwiki = $interwiki;
624        $t->mFragment = self::normalizeFragment( $fragment );
625        $t->mNamespace = $ns = (int)$ns;
626        $t->mDbkeyform = strtr( $title, ' ', '_' );
627        $t->mArticleID = ( $ns >= 0 ) ? -1 : 0;
628        $t->mUrlform = wfUrlencode( $t->mDbkeyform );
629        $t->mTextform = strtr( $title, '_', ' ' );
630        return $t;
631    }
632
633    /**
634     * Create a new Title from a namespace index and a DB key.
635     * The parameters will be checked for validity, which is a bit slower
636     * than makeTitle() but safer for user-provided data.
637     *
638     * The Title object returned by this method is guaranteed to be valid.
639     * Call canExist() to check if the Title represents an editable page.
640     *
641     * @param int $ns The namespace of the article
642     * @param string $title Database key form
643     * @param string $fragment The link fragment (after the "#")
644     * @param string $interwiki Interwiki prefix
645     * @return Title|null The new object, or null on an error
646     */
647    public static function makeTitleSafe( $ns, $title, $fragment = '', $interwiki = '' ) {
648        // NOTE: ideally, this would just call makeTitle() and then isValid(),
649        // but presently, that means more overhead on a potential performance hotspot.
650
651        if ( !MediaWikiServices::getInstance()->getNamespaceInfo()->exists( $ns ) ) {
652            return null;
653        }
654
655        $t = new Title();
656        $dbKeyForm = self::makeName( $ns, $title, $fragment, $interwiki, true );
657
658        try {
659            $t->secureAndSplit( $dbKeyForm );
660            return $t;
661        } catch ( MalformedTitleException $ex ) {
662            return null;
663        }
664    }
665
666    /**
667     * Create a new Title for the Main Page
668     *
669     * This uses the 'mainpage' interface message, which could be specified in
670     * `$wgForceUIMsgAsContentMsg`. If that is the case, then calling this method
671     * will use the user language, which would involve initialising the session
672     * via `RequestContext::getMain()->getLanguage()`. For session-less endpoints,
673     * be sure to pass in a MessageLocalizer (such as your own RequestContext or
674     * ResourceLoader Context) to prevent an error.
675     *
676     * @note The Title instance returned by this method is not guaranteed to be a fresh instance.
677     * It may instead be a cached instance created previously, with references to it remaining
678     * elsewhere.
679     *
680     * @param MessageLocalizer|null $localizer An optional context to use (since 1.34)
681     * @return Title
682     */
683    public static function newMainPage( MessageLocalizer $localizer = null ) {
684        static $recursionGuard = false;
685
686        $title = null;
687
688        if ( !$recursionGuard ) {
689            $msg = $localizer ? $localizer->msg( 'mainpage' ) : wfMessage( 'mainpage' );
690
691            $recursionGuard = true;
692            $title = self::newFromText( $msg->inContentLanguage()->text() );
693            $recursionGuard = false;
694        }
695
696        // Every page renders at least one link to the Main Page (e.g. sidebar).
697        // Don't produce fatal errors that would make the wiki inaccessible, and hard to fix the
698        // invalid message.
699        //
700        // Fallback scenarios:
701        // * Recursion guard
702        //   If the message contains a bare local interwiki (T297571), then
703        //   Title::newFromText via MediaWikiTitleCodec::splitTitleString can get back here.
704        // * Invalid title
705        //   If the 'mainpage' message contains something that is invalid,  Title::newFromText
706        //   will return null.
707
708        return $title ?? self::makeTitle( NS_MAIN, 'Main Page' );
709    }
710
711    /**
712     * Get a regex character class describing the legal characters in a link
713     *
714     * @return string The list of characters, not delimited
715     */
716    public static function legalChars() {
717        global $wgLegalTitleChars;
718        return $wgLegalTitleChars;
719    }
720
721    /**
722     * Utility method for converting a character sequence from bytes to Unicode.
723     *
724     * Primary usecase being converting $wgLegalTitleChars to a sequence usable in
725     * javascript, as PHP uses UTF-8 bytes where javascript uses Unicode code units.
726     *
727     * @param string $byteClass
728     * @return string
729     */
730    public static function convertByteClassToUnicodeClass( $byteClass ) {
731        $length = strlen( $byteClass );
732        // Input token queue
733        $x0 = $x1 = $x2 = '';
734        // Decoded queue
735        $d0 = $d1 = '';
736        // Decoded integer codepoints
737        $ord0 = $ord1 = $ord2 = 0;
738        // Re-encoded queue
739        $r0 = $r1 = $r2 = '';
740        // Output
741        $out = '';
742        // Flags
743        $allowUnicode = false;
744        for ( $pos = 0; $pos < $length; $pos++ ) {
745            // Shift the queues down
746            $x2 = $x1;
747            $x1 = $x0;
748            $d1 = $d0;
749            $ord2 = $ord1;
750            $ord1 = $ord0;
751            $r2 = $r1;
752            $r1 = $r0;
753            // Load the current input token and decoded values
754            $inChar = $byteClass[$pos];
755            if ( $inChar === '\\' ) {
756                if ( preg_match( '/x([0-9a-fA-F]{2})/A', $byteClass, $m, 0, $pos + 1 ) ) {
757                    $x0 = $inChar . $m[0];
758                    $d0 = chr( hexdec( $m[1] ) );
759                    $pos += strlen( $m[0] );
760                } elseif ( preg_match( '/[0-7]{3}/A', $byteClass, $m, 0, $pos + 1 ) ) {
761                    $x0 = $inChar . $m[0];
762                    $d0 = chr( octdec( $m[0] ) );
763                    $pos += strlen( $m[0] );
764                } elseif ( $pos + 1 >= $length ) {
765                    $x0 = $d0 = '\\';
766                } else {
767                    $d0 = $byteClass[$pos + 1];
768                    $x0 = $inChar . $d0;
769                    $pos++;
770                }
771            } else {
772                $x0 = $d0 = $inChar;
773            }
774            $ord0 = ord( $d0 );
775            // Load the current re-encoded value
776            if ( $ord0 < 32 || $ord0 == 0x7f ) {
777                $r0 = sprintf( '\x%02x', $ord0 );
778            } elseif ( $ord0 >= 0x80 ) {
779                // Allow unicode if a single high-bit character appears
780                $r0 = sprintf( '\x%02x', $ord0 );
781                $allowUnicode = true;
782                // @phan-suppress-next-line PhanParamSuspiciousOrder false positive
783            } elseif ( strpos( '-\\[]^', $d0 ) !== false ) {
784                $r0 = '\\' . $d0;
785            } else {
786                $r0 = $d0;
787            }
788            // Do the output
789            if ( $x0 !== '' && $x1 === '-' && $x2 !== '' ) {
790                // Range
791                if ( $ord2 > $ord0 ) {
792                    // Empty range
793                } elseif ( $ord0 >= 0x80 ) {
794                    // Unicode range
795                    $allowUnicode = true;
796                    if ( $ord2 < 0x80 ) {
797                        // Keep the non-unicode section of the range
798                        $out .= "$r2-\\x7F";
799                    }
800                } else {
801                    // Normal range
802                    $out .= "$r2-$r0";
803                }
804                // Reset state to the initial value
805                // @phan-suppress-next-line PhanPluginRedundantAssignmentInLoop
806                $x0 = $x1 = $d0 = $d1 = $r0 = $r1 = '';
807            } elseif ( $ord2 < 0x80 ) {
808                // ASCII character
809                $out .= $r2;
810            }
811        }
812        // @phan-suppress-next-line PhanSuspiciousValueComparison
813        if ( $ord1 < 0x80 ) {
814            $out .= $r1;
815        }
816        if ( $ord0 < 0x80 ) {
817            $out .= $r0;
818        }
819        if ( $allowUnicode ) {
820            $out .= '\u0080-\uFFFF';
821        }
822        return $out;
823    }
824
825    /**
826     * Make a prefixed DB key from a DB key and a namespace index
827     *
828     * @param int $ns Numerical representation of the namespace
829     * @param string $title The DB key form the title
830     * @param string $fragment The link fragment (after the "#")
831     * @param string $interwiki The interwiki prefix
832     * @param bool $canonicalNamespace If true, use the canonical name for
833     *   $ns instead of the localized version.
834     * @return string The prefixed form of the title
835     */
836    public static function makeName( $ns, $title, $fragment = '', $interwiki = '',
837        $canonicalNamespace = false
838    ) {
839        if ( $canonicalNamespace ) {
840            $namespace = MediaWikiServices::getInstance()->getNamespaceInfo()->
841                getCanonicalName( $ns );
842        } else {
843            $namespace = MediaWikiServices::getInstance()->getContentLanguage()->getNsText( $ns );
844        }
845        if ( $namespace === false ) {
846            // See T165149. Awkward, but better than erroneously linking to the main namespace.
847            $namespace = self::makeName( NS_SPECIAL, "Badtitle/NS$ns", '', '', $canonicalNamespace );
848        }
849        $name = $namespace === '' ? $title : "$namespace:$title";
850        if ( strval( $interwiki ) != '' ) {
851            $name = "$interwiki:$name";
852        }