Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 175 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
SpecialGlobalUsage | |
0.00% |
0 / 175 |
|
0.00% |
0 / 8 |
812 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
execute | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
20 | |||
showForm | |
0.00% |
0 / 73 |
|
0.00% |
0 / 1 |
20 | |||
showResult | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
42 | |||
formatItem | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
12 | |||
getNavBar | |
0.00% |
0 / 36 |
|
0.00% |
0 / 1 |
30 | |||
prefixSearchSubpages | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Special page to show global file usage. Also contains hook functions for |
4 | * showing usage on an image page. |
5 | */ |
6 | |
7 | namespace MediaWiki\Extension\GlobalUsage; |
8 | |
9 | use MediaWiki\Html\Html; |
10 | use MediaWiki\Linker\Linker; |
11 | use MediaWiki\MainConfigNames; |
12 | use MediaWiki\Navigation\PagerNavigationBuilder; |
13 | use MediaWiki\SpecialPage\SpecialPage; |
14 | use MediaWiki\Title\Title; |
15 | use MediaWiki\WikiMap\WikiMap; |
16 | use OOUI\ButtonInputWidget; |
17 | use OOUI\CheckboxInputWidget; |
18 | use OOUI\FieldLayout; |
19 | use OOUI\FieldsetLayout; |
20 | use OOUI\FormLayout; |
21 | use OOUI\HtmlSnippet; |
22 | use OOUI\PanelLayout; |
23 | use OOUI\TextInputWidget; |
24 | use RepoGroup; |
25 | use SearchEngineFactory; |
26 | |
27 | class SpecialGlobalUsage extends SpecialPage { |
28 | /** |
29 | * @var Title |
30 | */ |
31 | protected $target; |
32 | |
33 | /** |
34 | * @var bool |
35 | */ |
36 | protected $filterLocal; |
37 | |
38 | private RepoGroup $repoGroup; |
39 | private SearchEngineFactory $searchEngineFactory; |
40 | |
41 | public function __construct( |
42 | RepoGroup $repoGroup, |
43 | SearchEngineFactory $searchEngineFactory |
44 | ) { |
45 | parent::__construct( 'GlobalUsage' ); |
46 | $this->repoGroup = $repoGroup; |
47 | $this->searchEngineFactory = $searchEngineFactory; |
48 | } |
49 | |
50 | /** |
51 | * Entry point |
52 | * @param string $par |
53 | */ |
54 | public function execute( $par ) { |
55 | $target = $par ?: $this->getRequest()->getVal( 'target' ); |
56 | $this->target = Title::makeTitleSafe( NS_FILE, $target ); |
57 | |
58 | $this->filterLocal = $this->getRequest()->getCheck( 'filterlocal' ); |
59 | |
60 | $this->setHeaders(); |
61 | $this->getOutput()->addWikiMsg( 'globalusage-header' ); |
62 | if ( $this->target !== null ) { |
63 | $this->getOutput()->addWikiMsg( 'globalusage-header-image', $this->target->getText() ); |
64 | } |
65 | $this->showForm(); |
66 | |
67 | if ( $this->target === null ) { |
68 | $this->getOutput()->setPageTitleMsg( $this->msg( 'globalusage' ) ); |
69 | return; |
70 | } |
71 | |
72 | $this->getOutput()->setPageTitleMsg( |
73 | $this->msg( 'globalusage-for', $this->target->getPrefixedText() ) ); |
74 | |
75 | $this->showResult(); |
76 | } |
77 | |
78 | /** |
79 | * Shows the search form |
80 | */ |
81 | private function showForm() { |
82 | $this->getOutput()->enableOOUI(); |
83 | /* Build form */ |
84 | $form = new FormLayout( [ |
85 | 'method' => 'get', |
86 | 'action' => $this->getConfig()->get( MainConfigNames::Script ), |
87 | ] ); |
88 | |
89 | $fields = []; |
90 | $fields[] = new FieldLayout( |
91 | new TextInputWidget( [ |
92 | 'name' => 'target', |
93 | 'id' => 'target', |
94 | 'autosize' => true, |
95 | 'infusable' => true, |
96 | 'value' => $this->target === null ? '' : $this->target->getText(), |
97 | ] ), |
98 | [ |
99 | 'label' => $this->msg( 'globalusage-filename' )->text(), |
100 | 'align' => 'top', |
101 | ] |
102 | ); |
103 | |
104 | // Filter local checkbox |
105 | $fields[] = new FieldLayout( |
106 | new CheckboxInputWidget( [ |
107 | 'name' => 'filterlocal', |
108 | 'id' => 'mw-filterlocal', |
109 | 'value' => '1', |
110 | 'selected' => $this->filterLocal, |
111 | ] ), |
112 | [ |
113 | 'align' => 'inline', |
114 | 'label' => $this->msg( 'globalusage-filterlocal' )->text(), |
115 | ] |
116 | ); |
117 | |
118 | // Submit button |
119 | $fields[] = new FieldLayout( |
120 | new ButtonInputWidget( [ |
121 | 'value' => $this->msg( 'globalusage-ok' )->text(), |
122 | 'label' => $this->msg( 'globalusage-ok' )->text(), |
123 | 'flags' => [ 'primary', 'progressive' ], |
124 | 'type' => 'submit', |
125 | ] ), |
126 | [ |
127 | 'align' => 'top', |
128 | ] |
129 | ); |
130 | |
131 | $fieldset = new FieldsetLayout( [ |
132 | 'label' => $this->msg( 'globalusage-text' )->text(), |
133 | 'id' => 'globalusage-text', |
134 | 'items' => $fields, |
135 | ] ); |
136 | |
137 | $form->appendContent( |
138 | $fieldset, |
139 | new HtmlSnippet( |
140 | Html::hidden( 'title', $this->getPageTitle()->getPrefixedText() ) . |
141 | Html::hidden( 'limit', $this->getRequest()->getInt( 'limit', 50 ) ) |
142 | ) |
143 | ); |
144 | |
145 | $this->getOutput()->addHTML( |
146 | new PanelLayout( [ |
147 | 'expanded' => false, |
148 | 'padded' => true, |
149 | 'framed' => true, |
150 | 'content' => $form, |
151 | ] ) |
152 | ); |
153 | |
154 | if ( $this->target !== null ) { |
155 | $file = $this->repoGroup->findFile( $this->target ); |
156 | if ( $file ) { |
157 | // Show the image if it exists |
158 | $html = Linker::makeThumbLinkObj( |
159 | $this->target, |
160 | $file, |
161 | $this->target->getPrefixedText(), |
162 | '', |
163 | $this->getLanguage()->alignEnd() |
164 | ); |
165 | $this->getOutput()->addHtml( $html ); |
166 | } |
167 | } |
168 | } |
169 | |
170 | /** |
171 | * Creates as queryer and executes it based on $this->getRequest() |
172 | */ |
173 | private function showResult() { |
174 | $query = new GlobalUsageQuery( $this->target ); |
175 | $request = $this->getRequest(); |
176 | |
177 | // Extract params from $request. |
178 | if ( $request->getText( 'from' ) ) { |
179 | $query->setOffset( $request->getText( 'from' ) ); |
180 | } elseif ( $request->getText( 'to' ) ) { |
181 | $query->setOffset( $request->getText( 'to' ), true ); |
182 | } |
183 | $query->setLimit( $request->getInt( 'limit', 50 ) ); |
184 | $query->filterLocal( $this->filterLocal ); |
185 | |
186 | // Perform query |
187 | $query->execute(); |
188 | |
189 | // Don't show form element if there is no data |
190 | if ( $query->count() == 0 ) { |
191 | $this->getOutput()->addWikiMsg( 'globalusage-no-results', $this->target->getPrefixedText() ); |
192 | return; |
193 | } |
194 | |
195 | $navbar = $this->getNavBar( $query ); |
196 | $targetName = $this->target->getText(); |
197 | $out = $this->getOutput(); |
198 | |
199 | // Top navbar |
200 | $out->addHtml( $navbar ); |
201 | |
202 | $out->addHtml( '<div id="mw-globalusage-result">' ); |
203 | foreach ( $query->getSingleImageResult() as $wiki => $result ) { |
204 | $out->addHtml( |
205 | '<h2>' . $this->msg( |
206 | 'globalusage-on-wiki', |
207 | $targetName, WikiMap::getWikiName( $wiki ) )->parse() |
208 | . "</h2><ul>\n" ); |
209 | foreach ( $result as $item ) { |
210 | $out->addHtml( "\t<li>" . self::formatItem( $item ) . "</li>\n" ); |
211 | } |
212 | $out->addHtml( "</ul>\n" ); |
213 | } |
214 | $out->addHtml( '</div>' ); |
215 | |
216 | // Bottom navbar |
217 | $out->addHtml( $navbar ); |
218 | } |
219 | |
220 | /** |
221 | * Helper to format a specific item |
222 | * @param array $item |
223 | * @return string |
224 | */ |
225 | public static function formatItem( $item ) { |
226 | if ( !$item['namespace'] ) { |
227 | $page = $item['title']; |
228 | } else { |
229 | $page = "{$item['namespace']}:{$item['title']}"; |
230 | } |
231 | |
232 | $link = WikiMap::makeForeignLink( |
233 | $item['wiki'], $page, |
234 | str_replace( '_', ' ', $page ) |
235 | ); |
236 | // Return only the title if no link can be constructed |
237 | return $link === false ? htmlspecialchars( $page ) : $link; |
238 | } |
239 | |
240 | /** |
241 | * Helper function to create the navbar |
242 | * |
243 | * @param GlobalUsageQuery $query An executed GlobalUsageQuery object |
244 | * @return string Navbar HTML |
245 | */ |
246 | protected function getNavBar( $query ) { |
247 | $target = $this->target->getText(); |
248 | $limit = $query->getLimit(); |
249 | |
250 | // Find out which strings are for the prev and which for the next links |
251 | $offset = $query->getOffsetString(); |
252 | $continue = $query->getContinueString(); |
253 | if ( $query->isReversed() ) { |
254 | $from = $offset; |
255 | $to = $continue; |
256 | } else { |
257 | $from = $continue; |
258 | $to = $offset; |
259 | } |
260 | |
261 | // Fetch the title object |
262 | $title = $this->getPageTitle(); |
263 | |
264 | $navBuilder = new PagerNavigationBuilder( $this ); |
265 | $navBuilder |
266 | ->setPage( $title ) |
267 | ->setPrevTooltipMsg( 'prevn-title' ) |
268 | ->setNextTooltipMsg( 'nextn-title' ) |
269 | ->setLimitTooltipMsg( 'shown-title' ); |
270 | |
271 | // Default query for all links, including nulls to ensure consistent order of parameters. |
272 | // 'from'/'to' parameters are overridden for the 'previous'/'next' links below. |
273 | $q = [ |
274 | 'target' => $target, |
275 | 'filterlocal' => null, |
276 | 'from' => $to, |
277 | 'to' => null, |
278 | 'limit' => (string)$limit, |
279 | ]; |
280 | if ( $this->filterLocal ) { |
281 | $q['filterlocal'] = '1'; |
282 | } |
283 | $navBuilder->setLinkQuery( $q ); |
284 | |
285 | // Make 'previous' link |
286 | if ( $to ) { |
287 | $q = [ 'from' => null, 'to' => $to ]; |
288 | $navBuilder->setPrevLinkQuery( $q ); |
289 | } |
290 | // Make 'next' link |
291 | if ( $from ) { |
292 | $q = [ 'from' => $from, 'to' => null ]; |
293 | $navBuilder->setNextLinkQuery( $q ); |
294 | } |
295 | // Make links to set number of items per page |
296 | $navBuilder |
297 | ->setLimitLinkQueryParam( 'limit' ) |
298 | ->setCurrentLimit( $limit ); |
299 | |
300 | return $navBuilder->getHtml(); |
301 | } |
302 | |
303 | /** |
304 | * Return an array of subpages beginning with $search that this special page will accept. |
305 | * |
306 | * @param string $search Prefix to search for |
307 | * @param int $limit Maximum number of results to return (usually 10) |
308 | * @param int $offset Number of results to skip (usually 0) |
309 | * @return string[] Matching subpages |
310 | */ |
311 | public function prefixSearchSubpages( $search, $limit, $offset ) { |
312 | if ( !GlobalUsage::onSharedRepo() ) { |
313 | // Local files on non-shared wikis are not useful as suggestion |
314 | return []; |
315 | } |
316 | $title = Title::newFromText( $search, NS_FILE ); |
317 | if ( !$title || $title->getNamespace() !== NS_FILE ) { |
318 | // No prefix suggestion outside of file namespace |
319 | return []; |
320 | } |
321 | $searchEngine = $this->searchEngineFactory->create(); |
322 | $searchEngine->setLimitOffset( $limit, $offset ); |
323 | // Autocomplete subpage the same as a normal search, but just for (local) files |
324 | $searchEngine->setNamespaces( [ NS_FILE ] ); |
325 | $result = $searchEngine->defaultPrefixSearch( $search ); |
326 | |
327 | return array_map( static function ( Title $t ) { |
328 | // Remove namespace in search suggestion |
329 | return $t->getText(); |
330 | }, $result ); |
331 | } |
332 | |
333 | /** @inheritDoc */ |
334 | protected function getGroupName() { |
335 | return 'media'; |
336 | } |
337 | } |