Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 175
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialGlobalUsage
0.00% covered (danger)
0.00%
0 / 175
0.00% covered (danger)
0.00%
0 / 8
812
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
20
 showForm
0.00% covered (danger)
0.00%
0 / 73
0.00% covered (danger)
0.00%
0 / 1
20
 showResult
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
42
 formatItem
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 getNavBar
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
30
 prefixSearchSubpages
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
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
7namespace MediaWiki\Extension\GlobalUsage;
8
9use MediaWiki\Html\Html;
10use MediaWiki\Linker\Linker;
11use MediaWiki\MainConfigNames;
12use MediaWiki\Navigation\PagerNavigationBuilder;
13use MediaWiki\SpecialPage\SpecialPage;
14use MediaWiki\Title\Title;
15use MediaWiki\WikiMap\WikiMap;
16use OOUI\ButtonInputWidget;
17use OOUI\CheckboxInputWidget;
18use OOUI\FieldLayout;
19use OOUI\FieldsetLayout;
20use OOUI\FormLayout;
21use OOUI\HtmlSnippet;
22use OOUI\PanelLayout;
23use OOUI\TextInputWidget;
24use RepoGroup;
25use SearchEngineFactory;
26
27class 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}