Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 167 |
|
0.00% |
0 / 16 |
CRAP | |
0.00% |
0 / 1 |
SpecialMediaStatistics | |
0.00% |
0 / 166 |
|
0.00% |
0 / 16 |
756 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
2 | |||
isExpensive | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getQueryInfo | |
0.00% |
0 / 27 |
|
0.00% |
0 / 1 |
2 | |||
getOrderFields | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
outputResults | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
42 | |||
outputTableEnd | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
2 | |||
outputTableRow | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
2 | |||
makePercentPretty | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
getExtensionList | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
outputTableStart | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
2 | |||
getTableHeaderRow | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
6 | |||
outputMediaType | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
2 | |||
splitFakeTitle | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getGroupName | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
formatResult | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
preprocessResults | |
0.00% |
0 / 7 |
|
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 | |
25 | namespace MediaWiki\Specials; |
26 | |
27 | use MediaWiki\Cache\LinkBatchFactory; |
28 | use MediaWiki\Html\Html; |
29 | use MediaWiki\Output\OutputPage; |
30 | use MediaWiki\SpecialPage\QueryPage; |
31 | use MediaWiki\SpecialPage\SpecialPage; |
32 | use MimeAnalyzer; |
33 | use Skin; |
34 | use Wikimedia\Rdbms\IConnectionProvider; |
35 | use Wikimedia\Rdbms\IDatabase; |
36 | use Wikimedia\Rdbms\IResultWrapper; |
37 | |
38 | /** |
39 | * @ingroup SpecialPage |
40 | */ |
41 | class 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 | */ |
410 | class_alias( SpecialMediaStatistics::class, 'SpecialMediaStatistics' ); |