Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
97.14% |
68 / 70 |
|
75.00% |
3 / 4 |
CRAP | |
0.00% |
0 / 1 |
TitleMatcher | |
97.14% |
68 / 70 |
|
75.00% |
3 / 4 |
35 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
getNearMatch | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getNearMatchResultSet | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getNearMatchInternal | |
96.49% |
55 / 57 |
|
0.00% |
0 / 1 |
32 |
1 | <?php |
2 | namespace MediaWiki\Search; |
3 | |
4 | use ISearchResultSet; |
5 | use MediaWiki\Config\ServiceOptions; |
6 | use MediaWiki\HookContainer\HookContainer; |
7 | use MediaWiki\HookContainer\HookRunner; |
8 | use MediaWiki\Language\ILanguageConverter; |
9 | use MediaWiki\Language\Language; |
10 | use MediaWiki\Languages\LanguageConverterFactory; |
11 | use MediaWiki\MainConfigNames; |
12 | use MediaWiki\Page\WikiPageFactory; |
13 | use MediaWiki\SpecialPage\SpecialPage; |
14 | use MediaWiki\Title\Title; |
15 | use MediaWiki\Title\TitleFactory; |
16 | use MediaWiki\User\UserNameUtils; |
17 | use RepoGroup; |
18 | use SearchNearMatchResultSet; |
19 | |
20 | /** |
21 | * Service implementation of near match title search. |
22 | */ |
23 | class TitleMatcher { |
24 | /** |
25 | * @internal For use by ServiceWiring. |
26 | */ |
27 | public const CONSTRUCTOR_OPTIONS = [ |
28 | MainConfigNames::EnableSearchContributorsByIP, |
29 | ]; |
30 | |
31 | private ServiceOptions $options; |
32 | private Language $language; |
33 | private ILanguageConverter $languageConverter; |
34 | private HookRunner $hookRunner; |
35 | private WikiPageFactory $wikiPageFactory; |
36 | private UserNameUtils $userNameUtils; |
37 | private RepoGroup $repoGroup; |
38 | private TitleFactory $titleFactory; |
39 | |
40 | public function __construct( |
41 | ServiceOptions $options, |
42 | Language $contentLanguage, |
43 | LanguageConverterFactory $languageConverterFactory, |
44 | HookContainer $hookContainer, |
45 | WikiPageFactory $wikiPageFactory, |
46 | UserNameUtils $userNameUtils, |
47 | RepoGroup $repoGroup, |
48 | TitleFactory $titleFactory |
49 | ) { |
50 | $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS ); |
51 | $this->options = $options; |
52 | |
53 | $this->language = $contentLanguage; |
54 | $this->languageConverter = $languageConverterFactory->getLanguageConverter( $contentLanguage ); |
55 | $this->hookRunner = new HookRunner( $hookContainer ); |
56 | $this->wikiPageFactory = $wikiPageFactory; |
57 | $this->userNameUtils = $userNameUtils; |
58 | $this->repoGroup = $repoGroup; |
59 | $this->titleFactory = $titleFactory; |
60 | } |
61 | |
62 | /** |
63 | * If an exact title match can be found, or a very slightly close match, |
64 | * return the title. If no match, returns NULL. |
65 | * |
66 | * @param string $searchterm |
67 | * @return Title |
68 | */ |
69 | public function getNearMatch( $searchterm ) { |
70 | $title = $this->getNearMatchInternal( $searchterm ); |
71 | |
72 | $this->hookRunner->onSearchGetNearMatchComplete( $searchterm, $title ); |
73 | return $title; |
74 | } |
75 | |
76 | /** |
77 | * Do a near match (see SearchEngine::getNearMatch) and wrap it into a |
78 | * ISearchResultSet. |
79 | * |
80 | * @param string $searchterm |
81 | * @return ISearchResultSet |
82 | */ |
83 | public function getNearMatchResultSet( $searchterm ) { |
84 | return new SearchNearMatchResultSet( $this->getNearMatch( $searchterm ) ); |
85 | } |
86 | |
87 | /** |
88 | * Really find the title match. |
89 | * @param string $searchterm |
90 | * @return null|Title |
91 | */ |
92 | protected function getNearMatchInternal( $searchterm ) { |
93 | $allSearchTerms = [ $searchterm ]; |
94 | |
95 | if ( $this->languageConverter->hasVariants() ) { |
96 | $allSearchTerms = array_unique( array_merge( |
97 | $allSearchTerms, |
98 | $this->languageConverter->autoConvertToAllVariants( $searchterm ) |
99 | ) ); |
100 | } |
101 | |
102 | $titleResult = null; |
103 | if ( !$this->hookRunner->onSearchGetNearMatchBefore( $allSearchTerms, $titleResult ) ) { |
104 | return $titleResult; |
105 | } |
106 | |
107 | // Most of our handling here deals with finding a valid title for the search term, |
108 | // but almost anything starting with '#' is "valid" and points to Main_Page#searchterm. |
109 | // Rather than doing something completely wrong, do nothing. |
110 | if ( $searchterm === '' || $searchterm[0] === '#' ) { |
111 | return null; |
112 | } |
113 | |
114 | foreach ( $allSearchTerms as $term ) { |
115 | # Exact match? No need to look further. |
116 | $title = $this->titleFactory->newFromText( $term ); |
117 | if ( $title === null ) { |
118 | return null; |
119 | } |
120 | |
121 | # Try files if searching in the Media: namespace |
122 | if ( $title->getNamespace() === NS_MEDIA ) { |
123 | $title = Title::makeTitle( NS_FILE, $title->getText() ); |
124 | } |
125 | |
126 | if ( $title->isSpecialPage() || $title->isExternal() || $title->exists() ) { |
127 | return $title; |
128 | } |
129 | |
130 | # See if it still otherwise has content is some sensible sense |
131 | if ( $title->canExist() ) { |
132 | $page = $this->wikiPageFactory->newFromTitle( $title ); |
133 | if ( $page->hasViewableContent() ) { |
134 | return $title; |
135 | } |
136 | } |
137 | |
138 | if ( !$this->hookRunner->onSearchAfterNoDirectMatch( $term, $title ) ) { |
139 | return $title; |
140 | } |
141 | |
142 | # Now try all lower case (i.e. first letter capitalized) |
143 | $title = $this->titleFactory->newFromText( $this->language->lc( $term ) ); |
144 | if ( $title && $title->exists() ) { |
145 | return $title; |
146 | } |
147 | |
148 | # Now try capitalized string |
149 | $title = $this->titleFactory->newFromText( $this->language->ucwords( $term ) ); |
150 | if ( $title && $title->exists() ) { |
151 | return $title; |
152 | } |
153 | |
154 | # Now try all upper case |
155 | $title = $this->titleFactory->newFromText( $this->language->uc( $term ) ); |
156 | if ( $title && $title->exists() ) { |
157 | return $title; |
158 | } |
159 | |
160 | # Now try Word-Caps-Breaking-At-Word-Breaks, for hyphenated names etc |
161 | $title = $this->titleFactory->newFromText( $this->language->ucwordbreaks( $term ) ); |
162 | if ( $title && $title->exists() ) { |
163 | return $title; |
164 | } |
165 | |
166 | // Give hooks a chance at better match variants |
167 | $title = null; |
168 | // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args |
169 | if ( !$this->hookRunner->onSearchGetNearMatch( $term, $title ) ) { |
170 | return $title; |
171 | } |
172 | } |
173 | |
174 | $title = $this->titleFactory->newFromTextThrow( $searchterm ); |
175 | |
176 | # Entering an IP address goes to the contributions page |
177 | if ( $this->options->get( MainConfigNames::EnableSearchContributorsByIP ) ) { |
178 | if ( ( $title->getNamespace() === NS_USER && $this->userNameUtils->isIP( $title->getText() ) ) |
179 | || $this->userNameUtils->isIP( trim( $searchterm ) ) ) { |
180 | return SpecialPage::getTitleFor( 'Contributions', $title->getDBkey() ); |
181 | } |
182 | } |
183 | |
184 | # Entering a user goes to the user page whether it's there or not |
185 | if ( $title->getNamespace() === NS_USER ) { |
186 | return $title; |
187 | } |
188 | |
189 | # Go to images that exist even if there's no local page. |
190 | # There may have been a funny upload, or it may be on a shared |
191 | # file repository such as Wikimedia Commons. |
192 | if ( $title->getNamespace() === NS_FILE ) { |
193 | $image = $this->repoGroup->findFile( $title ); |
194 | if ( $image ) { |
195 | return $title; |
196 | } |
197 | } |
198 | |
199 | # MediaWiki namespace? Page may be "implied" if not customized. |
200 | # Just return it, with caps forced as the message system likes it. |
201 | if ( $title->getNamespace() === NS_MEDIAWIKI ) { |
202 | return Title::makeTitle( NS_MEDIAWIKI, $this->language->ucfirst( $title->getText() ) ); |
203 | } |
204 | |
205 | # Quoted term? Try without the quotes... |
206 | $matches = []; |
207 | if ( preg_match( '/^"([^"]+)"$/', $searchterm, $matches ) ) { |
208 | return $this->getNearMatch( $matches[1] ); |
209 | } |
210 | |
211 | return null; |
212 | } |
213 | } |