SpecialMediaStatistics.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  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. use Wikimedia\Rdbms\IResultWrapper;
  25. use Wikimedia\Rdbms\IDatabase;
  26. /**
  27. * @ingroup SpecialPage
  28. */
  29. class SpecialMediaStatistics extends QueryPage {
  30. protected $totalCount = 0, $totalBytes = 0;
  31. /**
  32. * @var int $totalPerType Combined file size of all files in a section
  33. */
  34. protected $totalPerType = 0;
  35. /**
  36. * @var int $totalSize Combined file size of all files
  37. */
  38. protected $totalSize = 0;
  39. function __construct( $name = 'MediaStatistics' ) {
  40. parent::__construct( $name );
  41. // Generally speaking there is only a small number of file types,
  42. // so just show all of them.
  43. $this->limit = 5000;
  44. $this->shownavigation = false;
  45. }
  46. public function isExpensive() {
  47. return true;
  48. }
  49. /**
  50. * Query to do.
  51. *
  52. * This abuses the query cache table by storing mime types as "titles".
  53. *
  54. * This will store entries like [[Media:BITMAP;image/jpeg;200;20000]]
  55. * where the form is Media type;mime type;count;bytes.
  56. *
  57. * This relies on the behaviour that when value is tied, the order things
  58. * come out of querycache table is the order they went in. Which is hacky.
  59. * However, other special pages like Special:Deadendpages and
  60. * Special:BrokenRedirects also rely on this.
  61. * @return array
  62. */
  63. public function getQueryInfo() {
  64. $dbr = wfGetDB( DB_REPLICA );
  65. $fakeTitle = $dbr->buildConcat( [
  66. 'img_media_type',
  67. $dbr->addQuotes( ';' ),
  68. 'img_major_mime',
  69. $dbr->addQuotes( '/' ),
  70. 'img_minor_mime',
  71. $dbr->addQuotes( ';' ),
  72. $dbr->buildStringCast( 'COUNT(*)' ),
  73. $dbr->addQuotes( ';' ),
  74. $dbr->buildStringCast( 'SUM( img_size )' )
  75. ] );
  76. return [
  77. 'tables' => [ 'image' ],
  78. 'fields' => [
  79. 'title' => $fakeTitle,
  80. 'namespace' => NS_MEDIA, /* needs to be something */
  81. 'value' => '1'
  82. ],
  83. 'options' => [
  84. 'GROUP BY' => [
  85. 'img_media_type',
  86. 'img_major_mime',
  87. 'img_minor_mime',
  88. ]
  89. ]
  90. ];
  91. }
  92. /**
  93. * How to sort the results
  94. *
  95. * It's important that img_media_type come first, otherwise the
  96. * tables will be fragmented.
  97. * @return array Fields to sort by
  98. */
  99. function getOrderFields() {
  100. return [ 'img_media_type', 'count(*)', 'img_major_mime', 'img_minor_mime' ];
  101. }
  102. /**
  103. * Output the results of the query.
  104. *
  105. * @param OutputPage $out
  106. * @param Skin $skin (deprecated presumably)
  107. * @param IDatabase $dbr
  108. * @param IResultWrapper $res Results from query
  109. * @param int $num Number of results
  110. * @param int $offset Paging offset (Should always be 0 in our case)
  111. */
  112. protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
  113. $prevMediaType = null;
  114. foreach ( $res as $row ) {
  115. $mediaStats = $this->splitFakeTitle( $row->title );
  116. if ( count( $mediaStats ) < 4 ) {
  117. continue;
  118. }
  119. list( $mediaType, $mime, $totalCount, $totalBytes ) = $mediaStats;
  120. if ( $prevMediaType !== $mediaType ) {
  121. if ( $prevMediaType !== null ) {
  122. // We're not at beginning, so we have to
  123. // close the previous table.
  124. $this->outputTableEnd();
  125. }
  126. $this->outputMediaType( $mediaType );
  127. $this->totalPerType = 0;
  128. $this->outputTableStart( $mediaType );
  129. $prevMediaType = $mediaType;
  130. }
  131. $this->outputTableRow( $mime, intval( $totalCount ), intval( $totalBytes ) );
  132. }
  133. if ( $prevMediaType !== null ) {
  134. $this->outputTableEnd();
  135. // add total size of all files
  136. $this->outputMediaType( 'total' );
  137. $this->getOutput()->addWikiTextAsInterface(
  138. $this->msg( 'mediastatistics-allbytes' )
  139. ->numParams( $this->totalSize )
  140. ->sizeParams( $this->totalSize )
  141. ->text()
  142. );
  143. }
  144. }
  145. /**
  146. * Output closing </table>
  147. */
  148. protected function outputTableEnd() {
  149. $this->getOutput()->addHTML(
  150. Html::closeElement( 'tbody' ) .
  151. Html::closeElement( 'table' )
  152. );
  153. $this->getOutput()->addWikiTextAsInterface(
  154. $this->msg( 'mediastatistics-bytespertype' )
  155. ->numParams( $this->totalPerType )
  156. ->sizeParams( $this->totalPerType )
  157. ->numParams( $this->makePercentPretty( $this->totalPerType / $this->totalBytes ) )
  158. ->text()
  159. );
  160. $this->totalSize += $this->totalPerType;
  161. }
  162. /**
  163. * Output a row of the stats table
  164. *
  165. * @param string $mime mime type (e.g. image/jpeg)
  166. * @param int $count Number of images of this type
  167. * @param int $bytes Total space for images of this type
  168. */
  169. protected function outputTableRow( $mime, $count, $bytes ) {
  170. $mimeSearch = SpecialPage::getTitleFor( 'MIMEsearch', $mime );
  171. $linkRenderer = $this->getLinkRenderer();
  172. $row = Html::rawElement(
  173. 'td',
  174. [],
  175. $linkRenderer->makeLink( $mimeSearch, $mime )
  176. );
  177. $row .= Html::rawElement(
  178. 'td',
  179. [],
  180. $this->getExtensionList( $mime )
  181. );
  182. $row .= Html::rawElement(
  183. 'td',
  184. // Make sure js sorts it in numeric order
  185. [ 'data-sort-value' => $count ],
  186. $this->msg( 'mediastatistics-nfiles' )
  187. ->numParams( $count )
  188. /** @todo Check to be sure this really should have number formatting */
  189. ->numParams( $this->makePercentPretty( $count / $this->totalCount ) )
  190. ->parse()
  191. );
  192. $row .= Html::rawElement(
  193. 'td',
  194. // Make sure js sorts it in numeric order
  195. [ 'data-sort-value' => $bytes ],
  196. $this->msg( 'mediastatistics-nbytes' )
  197. ->numParams( $bytes )
  198. ->sizeParams( $bytes )
  199. /** @todo Check to be sure this really should have number formatting */
  200. ->numParams( $this->makePercentPretty( $bytes / $this->totalBytes ) )
  201. ->parse()
  202. );
  203. $this->totalPerType += $bytes;
  204. $this->getOutput()->addHTML( Html::rawElement( 'tr', [], $row ) );
  205. }
  206. /**
  207. * @param float $decimal A decimal percentage (ie for 12.3%, this would be 0.123)
  208. * @return string The percentage formatted so that 3 significant digits are shown.
  209. */
  210. protected function makePercentPretty( $decimal ) {
  211. $decimal *= 100;
  212. // Always show three useful digits
  213. if ( $decimal == 0 ) {
  214. return '0';
  215. }
  216. if ( $decimal >= 100 ) {
  217. return '100';
  218. }
  219. $percent = sprintf( "%." . max( 0, 2 - floor( log10( $decimal ) ) ) . "f", $decimal );
  220. // Then remove any trailing 0's
  221. return preg_replace( '/\.?0*$/', '', $percent );
  222. }
  223. /**
  224. * Given a mime type, return a comma separated list of allowed extensions.
  225. *
  226. * @param string $mime mime type
  227. * @return string Comma separated list of allowed extensions (e.g. ".ogg, .oga")
  228. */
  229. private function getExtensionList( $mime ) {
  230. $exts = MediaWiki\MediaWikiServices::getInstance()->getMimeAnalyzer()
  231. ->getExtensionsForType( $mime );
  232. if ( $exts === null ) {
  233. return '';
  234. }
  235. $extArray = explode( ' ', $exts );
  236. $extArray = array_unique( $extArray );
  237. foreach ( $extArray as &$ext ) {
  238. $ext = htmlspecialchars( '.' . $ext );
  239. }
  240. return $this->getLanguage()->commaList( $extArray );
  241. }
  242. /**
  243. * Output the start of the table
  244. *
  245. * Including opening <table>, and first <tr> with column headers.
  246. * @param string $mediaType
  247. */
  248. protected function outputTableStart( $mediaType ) {
  249. $out = $this->getOutput();
  250. $out->addModuleStyles( 'jquery.tablesorter.styles' );
  251. $out->addModules( 'jquery.tablesorter' );
  252. $out->addHTML(
  253. Html::openElement(
  254. 'table',
  255. [ 'class' => [
  256. 'mw-mediastats-table',
  257. 'mw-mediastats-table-' . strtolower( $mediaType ),
  258. 'sortable',
  259. 'wikitable'
  260. ] ]
  261. ) .
  262. Html::rawElement( 'thead', [], $this->getTableHeaderRow() ) .
  263. Html::openElement( 'tbody' )
  264. );
  265. }
  266. /**
  267. * Get (not output) the header row for the table
  268. *
  269. * @return string The header row of the table
  270. */
  271. protected function getTableHeaderRow() {
  272. $headers = [ 'mimetype', 'extensions', 'count', 'totalbytes' ];
  273. $ths = '';
  274. foreach ( $headers as $header ) {
  275. $ths .= Html::rawElement(
  276. 'th',
  277. [],
  278. // for grep:
  279. // mediastatistics-table-mimetype, mediastatistics-table-extensions
  280. // tatistics-table-count, mediastatistics-table-totalbytes
  281. $this->msg( 'mediastatistics-table-' . $header )->parse()
  282. );
  283. }
  284. return Html::rawElement( 'tr', [], $ths );
  285. }
  286. /**
  287. * Output a header for a new media type section
  288. *
  289. * @param string $mediaType A media type (e.g. from the MEDIATYPE_xxx constants)
  290. */
  291. protected function outputMediaType( $mediaType ) {
  292. $this->getOutput()->addHTML(
  293. Html::element(
  294. 'h2',
  295. [ 'class' => [
  296. 'mw-mediastats-mediatype',
  297. 'mw-mediastats-mediatype-' . strtolower( $mediaType )
  298. ] ],
  299. // for grep
  300. // mediastatistics-header-unknown, mediastatistics-header-bitmap,
  301. // mediastatistics-header-drawing, mediastatistics-header-audio,
  302. // mediastatistics-header-video, mediastatistics-header-multimedia,
  303. // mediastatistics-header-office, mediastatistics-header-text,
  304. // mediastatistics-header-executable, mediastatistics-header-archive,
  305. // mediastatistics-header-3d,
  306. $this->msg( 'mediastatistics-header-' . strtolower( $mediaType ) )->text()
  307. )
  308. );
  309. /** @todo Possibly could add a message here explaining what the different types are.
  310. * not sure if it is needed though.
  311. */
  312. }
  313. /**
  314. * parse the fake title format that this special page abuses querycache with.
  315. *
  316. * @param string $fakeTitle A string formatted as <media type>;<mime type>;<count>;<bytes>
  317. * @return array The constituant parts of $fakeTitle
  318. */
  319. private function splitFakeTitle( $fakeTitle ) {
  320. return explode( ';', $fakeTitle, 4 );
  321. }
  322. /**
  323. * What group to put the page in
  324. * @return string
  325. */
  326. protected function getGroupName() {
  327. return 'media';
  328. }
  329. /**
  330. * This method isn't used, since we override outputResults, but
  331. * we need to implement since abstract in parent class.
  332. *
  333. * @param Skin $skin
  334. * @param stdClass $result Result row
  335. * @return bool|string|void
  336. * @throws MWException
  337. */
  338. public function formatResult( $skin, $result ) {
  339. throw new MWException( "unimplemented" );
  340. }
  341. /**
  342. * Initialize total values so we can figure out percentages later.
  343. *
  344. * @param IDatabase $dbr
  345. * @param IResultWrapper $res
  346. */
  347. public function preprocessResults( $dbr, $res ) {
  348. $this->executeLBFromResultWrapper( $res );
  349. $this->totalCount = $this->totalBytes = 0;
  350. foreach ( $res as $row ) {
  351. $mediaStats = $this->splitFakeTitle( $row->title );
  352. $this->totalCount += $mediaStats[2] ?? 0;
  353. $this->totalBytes += $mediaStats[3] ?? 0;
  354. }
  355. $res->seek( 0 );
  356. }
  357. }