Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 167
0.00% covered (danger)
0.00%
0 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
SpecialMediaStatistics
0.00% covered (danger)
0.00%
0 / 166
0.00% covered (danger)
0.00%
0 / 16
756
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 isExpensive
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getQueryInfo
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
2
 getOrderFields
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 outputResults
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
42
 outputTableEnd
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
2
 outputTableRow
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
2
 makePercentPretty
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getExtensionList
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 outputTableStart
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
2
 getTableHeaderRow
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 outputMediaType
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 splitFakeTitle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getGroupName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 formatResult
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 preprocessResults
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Implements Special:MediaStatistics
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; if not, write to the Free Software Foundation, Inc.,
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 * http://www.gnu.org/copyleft/gpl.html
19 *
20 * @file
21 * @ingroup SpecialPage
22 * @author Brian Wolff
23 */
24
25namespace MediaWiki\Specials;
26
27use MediaWiki\Cache\LinkBatchFactory;
28use MediaWiki\Html\Html;
29use MediaWiki\Output\OutputPage;
30use MediaWiki\SpecialPage\QueryPage;
31use MediaWiki\SpecialPage\SpecialPage;
32use MimeAnalyzer;
33use Skin;
34use Wikimedia\Rdbms\IConnectionProvider;
35use Wikimedia\Rdbms\IDatabase;
36use Wikimedia\Rdbms\IResultWrapper;
37
38/**
39 * @ingroup SpecialPage
40 */
41class SpecialMediaStatistics extends QueryPage {
42
43    public const MAX_LIMIT = 5000;
44
45    protected $totalCount = 0, $totalBytes = 0;
46
47    /**
48     * @var int Combined file size of all files in a section
49     */
50    protected $totalPerType = 0;
51
52    /**
53     * @var int Combined file count of all files in a section
54     */
55    protected $countPerType = 0;
56
57    /**
58     * @var int Combined file size of all files
59     */
60    protected $totalSize = 0;
61
62    private MimeAnalyzer $mimeAnalyzer;
63
64    /**
65     * @param MimeAnalyzer $mimeAnalyzer
66     * @param IConnectionProvider $dbProvider
67     * @param LinkBatchFactory $linkBatchFactory
68     */
69    public function __construct(
70        MimeAnalyzer $mimeAnalyzer,
71        IConnectionProvider $dbProvider,
72        LinkBatchFactory $linkBatchFactory
73    ) {
74        parent::__construct( 'MediaStatistics' );
75        // Generally speaking there is only a small number of file types,
76        // so just show all of them.
77        $this->limit = self::MAX_LIMIT;
78        $this->shownavigation = false;
79        $this->mimeAnalyzer = $mimeAnalyzer;
80        $this->setDatabaseProvider( $dbProvider );
81        $this->setLinkBatchFactory( $linkBatchFactory );
82    }
83
84    public function isExpensive() {
85        return true;
86    }
87
88    /**
89     * Query to do.
90     *
91     * This abuses the query cache table by storing mime types as "titles".
92     *
93     * This will store entries like [[Media:BITMAP;image/jpeg;200;20000]]
94     * where the form is Media type;mime type;count;bytes.
95     *
96     * This relies on the behaviour that when value is tied, the order things
97     * come out of querycache table is the order they went in. Which is hacky.
98     * However, other special pages like Special:Deadendpages and
99     * Special:BrokenRedirects also rely on this.
100     * @return array
101     */
102    public function getQueryInfo() {
103        $dbr = $this->getDatabaseProvider()->getReplicaDatabase();
104        $fakeTitle = $dbr->buildConcat( [
105            'img_media_type',
106            $dbr->addQuotes( ';' ),
107            'img_major_mime',
108            $dbr->addQuotes( '/' ),
109            'img_minor_mime',
110            $dbr->addQuotes( ';' ),
111            $dbr->buildStringCast( 'COUNT(*)' ),
112            $dbr->addQuotes( ';' ),
113            $dbr->buildStringCast( 'SUM( img_size )' )
114        ] );
115        return [
116            'tables' => [ 'image' ],
117            'fields' => [
118                'title' => $fakeTitle,
119                'namespace' => NS_MEDIA, /* needs to be something */
120                'value' => '1'
121            ],
122            'options' => [
123                'GROUP BY' => [
124                    'img_media_type',
125                    'img_major_mime',
126                    'img_minor_mime',
127                ]
128            ]
129        ];
130    }
131
132    /**
133     * How to sort the results
134     *
135     * It's important that img_media_type come first, otherwise the
136     * tables will be fragmented.
137     * @return array Fields to sort by
138     */
139    protected function getOrderFields() {
140        return [ 'img_media_type', 'count(*)', 'img_major_mime', 'img_minor_mime' ];
141    }
142
143    /**
144     * Output the results of the query.
145     *
146     * @param OutputPage $out
147     * @param Skin $skin (deprecated presumably)
148     * @param IDatabase $dbr
149     * @param IResultWrapper $res Results from query
150     * @param int $num Number of results
151     * @param int $offset Paging offset (Should always be 0 in our case)
152     */
153    protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
154        $prevMediaType = null;
155        foreach ( $res as $row ) {
156            $mediaStats = $this->splitFakeTitle( $row->title );
157            if ( count( $mediaStats ) < 4 ) {
158                continue;
159            }
160            [ $mediaType, $mime, $totalCount, $totalBytes ] = $mediaStats;
161            if ( $prevMediaType !== $mediaType ) {
162                if ( $prevMediaType !== null ) {
163                    // We're not at beginning, so we have to
164                    // close the previous table.
165                    $this->outputTableEnd();
166                }
167                $this->outputMediaType( $mediaType );
168                $this->totalPerType = 0;
169                $this->countPerType = 0;
170                $this->outputTableStart( $mediaType );
171                $prevMediaType = $mediaType;
172            }
173            $this->outputTableRow( $mime, intval( $totalCount ), intval( $totalBytes ) );
174        }
175        if ( $prevMediaType !== null ) {
176            $this->outputTableEnd();
177            // add total size of all files
178            $this->outputMediaType( 'total' );
179            $this->getOutput()->addWikiTextAsInterface(
180                $this->msg( 'mediastatistics-allbytes' )
181                    ->numParams( $this->totalSize )
182                    ->sizeParams( $this->totalSize )
183                    ->numParams( $this->totalCount )
184                    ->text()
185            );
186        }
187    }
188
189    /**
190     * Output closing </table>
191     */
192    protected function outputTableEnd() {
193        $this->getOutput()->addHTML(
194            Html::closeElement( 'tbody' ) .
195            Html::closeElement( 'table' )
196        );
197        $this->getOutput()->addWikiTextAsInterface(
198                $this->msg( 'mediastatistics-bytespertype' )
199                    ->numParams( $this->totalPerType )
200                    ->sizeParams( $this->totalPerType )
201                    ->numParams( $this->makePercentPretty( $this->totalPerType / $this->totalBytes ) )
202                    ->numParams( $this->countPerType )
203                    ->numParams( $this->makePercentPretty( $this->countPerType / $this->totalCount ) )
204                    ->text()
205        );
206        $this->totalSize += $this->totalPerType;
207    }
208
209    /**
210     * Output a row of the stats table
211     *
212     * @param string $mime mime type (e.g. image/jpeg)
213     * @param int $count Number of images of this type
214     * @param int $bytes Total space for images of this type
215     */
216    protected function outputTableRow( $mime, $count, $bytes ) {
217        $mimeSearch = SpecialPage::getTitleFor( 'MIMEsearch', $mime );
218        $linkRenderer = $this->getLinkRenderer();
219        $row = Html::rawElement(
220            'td',
221            [],
222            $linkRenderer->makeLink( $mimeSearch, $mime )
223        );
224        $row .= Html::rawElement(
225            'td',
226            [],
227            $this->getExtensionList( $mime )
228        );
229        $row .= Html::rawElement(
230            'td',
231            // Make sure js sorts it in numeric order
232            [ 'data-sort-value' => $count ],
233            $this->msg( 'mediastatistics-nfiles' )
234                ->numParams( $count )
235                /** @todo Check to be sure this really should have number formatting */
236                ->numParams( $this->makePercentPretty( $count / $this->totalCount ) )
237                ->parse()
238        );
239        $row .= Html::rawElement(
240            'td',
241            // Make sure js sorts it in numeric order
242            [ 'data-sort-value' => $bytes ],
243            $this->msg( 'mediastatistics-nbytes' )
244                ->numParams( $bytes )
245                ->sizeParams( $bytes )
246                /** @todo Check to be sure this really should have number formatting */
247                ->numParams( $this->makePercentPretty( $bytes / $this->totalBytes ) )
248                ->parse()
249        );
250        $this->totalPerType += $bytes;
251        $this->countPerType += $count;
252        $this->getOutput()->addHTML( Html::rawElement( 'tr', [], $row ) );
253    }
254
255    /**
256     * @param float $decimal A decimal percentage (ie for 12.3%, this would be 0.123)
257     * @return string The percentage formatted so that 3 significant digits are shown.
258     */
259    protected function makePercentPretty( $decimal ) {
260        $decimal *= 100;
261        // Always show three useful digits
262        if ( $decimal == 0 ) {
263            return '0';
264        }
265        if ( $decimal >= 100 ) {
266            return '100';
267        }
268        $percent = sprintf( "%." . max( 0, 2 - floor( log10( $decimal ) ) ) . "f", $decimal );
269        // Then remove any trailing 0's
270        return preg_replace( '/\.?0*$/', '', $percent );
271    }
272
273    /**
274     * Given a mime type, return a comma separated list of allowed extensions.
275     *
276     * @param string $mime mime type
277     * @return string Comma separated list of allowed extensions (e.g. ".ogg, .oga")
278     */
279    private function getExtensionList( $mime ) {
280        $exts = $this->mimeAnalyzer->getExtensionsFromMimeType( $mime );
281        if ( !$exts ) {
282            return '';
283        }
284        foreach ( $exts as &$ext ) {
285            $ext = htmlspecialchars( '.' . $ext );
286        }
287
288        return $this->getLanguage()->commaList( $exts );
289    }
290
291    /**
292     * Output the start of the table
293     *
294     * Including opening <table>, and first <tr> with column headers.
295     * @param string $mediaType
296     */
297    protected function outputTableStart( $mediaType ) {
298        $out = $this->getOutput();
299        $out->addModuleStyles( 'jquery.tablesorter.styles' );
300        $out->addModules( 'jquery.tablesorter' );
301        $out->addHTML(
302            Html::openElement(
303                'table',
304                [ 'class' => [
305                    'mw-mediastats-table',
306                    'mw-mediastats-table-' . strtolower( $mediaType ),
307                    'sortable',
308                    'wikitable'
309                ] ]
310            ) .
311            Html::rawElement( 'thead', [], $this->getTableHeaderRow() ) .
312            Html::openElement( 'tbody' )
313        );
314    }
315
316    /**
317     * Get (not output) the header row for the table
318     *
319     * @return string The header row of the table
320     */
321    protected function getTableHeaderRow() {
322        $headers = [ 'mimetype', 'extensions', 'count', 'totalbytes' ];
323        $ths = '';
324        foreach ( $headers as $header ) {
325            $ths .= Html::rawElement(
326                'th',
327                [],
328                // for grep:
329                // mediastatistics-table-mimetype, mediastatistics-table-extensions
330                // mediastatistics-table-count, mediastatistics-table-totalbytes
331                $this->msg( 'mediastatistics-table-' . $header )->parse()
332            );
333        }
334        return Html::rawElement( 'tr', [], $ths );
335    }
336
337    /**
338     * Output a header for a new media type section
339     *
340     * @param string $mediaType A media type (e.g. from the MEDIATYPE_xxx constants)
341     */
342    protected function outputMediaType( $mediaType ) {
343        $this->getOutput()->addHTML(
344            Html::element(
345                'h2',
346                [ 'class' => [
347                    'mw-mediastats-mediatype',
348                    'mw-mediastats-mediatype-' . strtolower( $mediaType )
349                ] ],
350                // for grep
351                // mediastatistics-header-unknown, mediastatistics-header-bitmap,
352                // mediastatistics-header-drawing, mediastatistics-header-audio,
353                // mediastatistics-header-video, mediastatistics-header-multimedia,
354                // mediastatistics-header-office, mediastatistics-header-text,
355                // mediastatistics-header-executable, mediastatistics-header-archive,
356                // mediastatistics-header-3d,
357                $this->msg( 'mediastatistics-header-' . strtolower( $mediaType ) )->text()
358            )
359        );
360        /** @todo Possibly could add a message here explaining what the different types are.
361         *    not sure if it is needed though.
362         */
363    }
364
365    /**
366     * parse the fake title format that this special page abuses querycache with.
367     *
368     * @param string $fakeTitle A string formatted as <media type>;<mime type>;<count>;<bytes>
369     * @return array The constituent parts of $fakeTitle
370     */
371    private function splitFakeTitle( $fakeTitle ) {
372        return explode( ';', $fakeTitle, 4 );
373    }
374
375    /**
376     * What group to put the page in
377     * @return string
378     */
379    protected function getGroupName() {
380        return 'media';
381    }
382
383    /** @inheritDoc */
384    public function formatResult( $skin, $result ) {
385        return false;
386    }
387
388    /**
389     * Initialize total values so we can figure out percentages later.
390     *
391     * @param IDatabase $dbr
392     * @param IResultWrapper $res
393     */
394    public function preprocessResults( $dbr, $res ) {
395        $this->executeLBFromResultWrapper( $res );
396        $this->totalCount = $this->totalBytes = 0;
397        foreach ( $res as $row ) {
398            $mediaStats = $this->splitFakeTitle( $row->title );
399            $this->totalCount += $mediaStats[2] ?? 0;
400            $this->totalBytes += $mediaStats[3] ?? 0;
401        }
402        $res->seek( 0 );
403    }
404}
405
406/**
407 * Retain the old class name for backwards compatibility.
408 * @deprecated since 1.41
409 */
410class_alias( SpecialMediaStatistics::class, 'SpecialMediaStatistics' );