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