Add Special:MediaStatistics page for file type stats
authorBrian Wolff <bawolff+wn@gmail.com>
Sun, 20 Jul 2014 21:29:38 +0000 (18:29 -0300)
committerBrian Wolff <bawolff+wn@gmail.com>
Thu, 18 Sep 2014 17:28:57 +0000 (14:28 -0300)
There used to be a bot on commons that did this, but it broke, and
this seems like the sort of thing that should be built in to
MediaWiki anyhow.

In order to do this, it abuses the querycache table somewhat
(Storing non-titles things in the title column).

Also changes the link on Special:Statistics for number of
uploads to link to new page. It seemed appropriate to link
to more detailed statistics instead of to Special:ListFiles.

Change-Id: I9ab768584b02a32b450d5f3981ff775ee07fecfa

includes/AutoLoader.php
includes/specialpage/QueryPage.php
includes/specialpage/SpecialPageFactory.php
includes/specials/SpecialMediaStatistics.php [new file with mode: 0644]
includes/specials/SpecialStatistics.php
languages/i18n/en.json
languages/i18n/qqq.json
languages/messages/MessagesEn.php

index 44c405d..a5c24e8 100644 (file)
@@ -997,6 +997,7 @@ $wgAutoloadLocalClasses = array(
        'LoginForm' => 'includes/specials/SpecialUserlogin.php',
        'LonelyPagesPage' => 'includes/specials/SpecialLonelypages.php',
        'LongPagesPage' => 'includes/specials/SpecialLongpages.php',
+       'MediaStatisticsPage' => 'includes/specials/SpecialMediaStatistics.php',
        'MergeHistoryPager' => 'includes/specials/SpecialMergeHistory.php',
        'MIMEsearchPage' => 'includes/specials/SpecialMIMEsearch.php',
        'MostcategoriesPage' => 'includes/specials/SpecialMostcategories.php',
index ae0003d..52e4299 100644 (file)
@@ -76,6 +76,7 @@ abstract class QueryPage extends SpecialPage {
                                array( 'ListredirectsPage', 'Listredirects' ),
                                array( 'LonelyPagesPage', 'Lonelypages' ),
                                array( 'LongPagesPage', 'Longpages' ),
+                               array( 'MediaStatisticsPage', 'MediaStatistics' ),
                                array( 'MIMEsearchPage', 'MIMEsearch' ),
                                array( 'MostcategoriesPage', 'Mostcategories' ),
                                array( 'MostimagesPage', 'Mostimages' ),
index e0737a0..86ddde7 100644 (file)
@@ -114,6 +114,7 @@ class SpecialPageFactory {
                // Media reports and uploads
                'Listfiles' => 'SpecialListFiles',
                'Filepath' => 'SpecialFilepath',
+               'MediaStatistics' => 'MediaStatisticsPage',
                'MIMEsearch' => 'MIMEsearchPage',
                'FileDuplicateSearch' => 'FileDuplicateSearchPage',
                'Upload' => 'SpecialUpload',
diff --git a/includes/specials/SpecialMediaStatistics.php b/includes/specials/SpecialMediaStatistics.php
new file mode 100644 (file)
index 0000000..681c332
--- /dev/null
@@ -0,0 +1,325 @@
+<?php
+/**
+ * Implements Special:MediaStatistics
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ * http://www.gnu.org/copyleft/gpl.html
+ *
+ * @file
+ * @ingroup SpecialPage
+ * @author Brian Wolff
+ */
+
+/**
+ * @ingroup SpecialPage
+ */
+class MediaStatisticsPage extends QueryPage {
+       protected $totalCount = 0, $totalBytes = 0;
+
+       function __construct( $name = 'MediaStatistics' ) {
+               parent::__construct( $name );
+               // Generally speaking there is only a small number of file types,
+               // so just show all of them.
+               $this->limit = 5000;
+               $this->shownavigation = false;
+       }
+
+       function isExpensive() {
+               return true;
+       }
+
+       /**
+        * Query to do.
+        *
+        * This abuses the query cache table by storing mime types as "titles".
+        *
+        * This will store entries like [[Media:BITMAP;image/jpeg;200;20000]]
+        * where the form is Media type;mime type;count;bytes.
+        *
+        * This relies on the behaviour that when value is tied, the order things
+        * come out of querycache table is the order they went in. Which is hacky.
+        * However, other special pages like Special:Deadendpages and
+        * Special:BrokenRedirects also rely on this.
+        */
+       public function getQueryInfo() {
+               $dbr = wfGetDB( DB_SLAVE );
+               $fakeTitle = $dbr->buildConcat( array(
+                       'img_media_type',
+                       $dbr->addQuotes( ';' ),
+                       'img_major_mime',
+                       $dbr->addQuotes( '/' ),
+                       'img_minor_mime',
+                       $dbr->addQuotes( ';' ),
+                       'COUNT(*)',
+                       $dbr->addQuotes( ';' ),
+                       'SUM( img_size )'
+               ) );
+               return array(
+                       'tables' => array( 'image' ),
+                       'fields' => array(
+                               'title' => $fakeTitle,
+                               'namespace' => NS_MEDIA, /* needs to be something */
+                               'value' => '1'
+                       ),
+                       'options' => array(
+                               'GROUP BY' => array(
+                                       'img_media_type',
+                                       'img_major_mime',
+                                       'img_minor_mime',
+                               )
+                       )
+               );
+       }
+
+       /**
+        * How to sort the results
+        *
+        * It's important that img_media_type come first, otherwise the
+        * tables will be fragmented.
+        * @return Array Fields to sort by
+        */
+       function getOrderFields() {
+               return array( 'img_media_type', 'count(*)', 'img_major_mime', 'img_minor_mime' );
+       }
+
+       /**
+        * Output the results of the query.
+        *
+        * @param $out OutputPage
+        * @param $skin Skin (deprecated presumably)
+        * @param $dbr DatabaseBase
+        * @param $res ResultWrapper Results from query
+        * @param $num integer Number of results
+        * @param $offset integer Paging offset (Should always be 0 in our case)
+        */
+       protected function outputResults( $out, $skin, $dbr, $res, $num, $offset ) {
+               $prevMediaType = null;
+               foreach ( $res as $row ) {
+                       list( $mediaType, $mime, $totalCount, $totalBytes ) = $this->splitFakeTitle( $row->title );
+                       if ( $prevMediaType !== $mediaType ) {
+                               if ( $prevMediaType !== null ) {
+                                       // We're not at beginning, so we have to
+                                       // close the previous table.
+                                       $this->outputTableEnd();
+                               }
+                               $this->outputMediaType( $mediaType );
+                               $this->outputTableStart( $mediaType );
+                               $prevMediaType = $mediaType;
+                       }
+                       $this->outputTableRow( $mime, intval( $totalCount ), intval( $totalBytes ) );
+               }
+               if ( $prevMediaType !== null ) {
+                       $this->outputTableEnd();
+               }
+       }
+
+       /**
+        * Output closing </table>
+        */
+       protected function outputTableEnd() {
+               $this->getOutput()->addHtml( Html::closeElement( 'table' ) );
+       }
+
+       /**
+        * Output a row of the stats table
+        *
+        * @param $mime String mime type (e.g. image/jpeg)
+        * @param $count integer Number of images of this type
+        * @param $totalBytes integer Total space for images of this type
+        */
+       protected function outputTableRow( $mime, $count, $bytes ) {
+               $mimeSearch = SpecialPage::getTitleFor( 'MIMEsearch', $mime );
+               $row = Html::rawElement(
+                       'td',
+                       array(),
+                       Linker::link( $mimeSearch, htmlspecialchars( $mime ) )
+               );
+               $row .= Html::element(
+                       'td',
+                       array(),
+                       $this->getExtensionList( $mime )
+               );
+               $row .= Html::rawElement(
+                       'td',
+                       array(),
+                       $this->msg( 'mediastatistics-nfiles' )
+                               ->numParams( $count )
+                               /** @todo Check to be sure this really should have number formatting */
+                               ->numParams( $this->makePercentPretty( $count / $this->totalCount ) )
+                               ->parse()
+               );
+               $row .= Html::rawElement(
+                       'td',
+                       // Make sure js sorts it in numeric order
+                       array( 'data-sort-value' =>  $bytes ),
+                       $this->msg( 'mediastatistics-nbytes' )
+                               ->numParams( $bytes )
+                               ->sizeParams( $bytes )
+                               /** @todo Check to be sure this really should have number formatting */
+                               ->numParams( $this->makePercentPretty( $bytes / $this->totalBytes ) )
+                               ->parse()
+               );
+
+               $this->getOutput()->addHTML( Html::rawElement( 'tr', array(), $row ) );
+       }
+
+       /**
+        * @param float $decimal A decimal percentage (ie for 12.3%, this would be 0.123)
+        * @return String The percentage formatted so that 3 significant digits are shown.
+        */
+       protected function makePercentPretty( $decimal ) {
+               $decimal *= 100;
+               // Always show three useful digits
+               if ( $decimal == 0 ) {
+                       return '0';
+               }
+               $percent = sprintf( "%." . max( 0, 2 - floor( log10( $decimal ) ) ) . "f", $decimal );
+               // Then remove any trailing 0's
+               return preg_replace( '/\.?0*$/', '', $percent );
+       }
+
+       /**
+        * Given a mime type, return a comma separated list of allowed extensions.
+        *
+        * @param $mime String mime type
+        * @return String Comma separated list of allowed extensions (e.g. ".ogg, .oga")
+        */
+       private function getExtensionList( $mime ) {
+               $exts = MimeMagic::singleton()->getExtensionsForType( $mime );
+               if ( $exts === null ) {
+                       return '';
+               }
+               $extArray = explode( ' ', $exts );
+               $extArray = array_unique( $extArray );
+               foreach ( $extArray as &$ext ) {
+                       $ext = '.' . $ext;
+               }
+
+               return $this->getLanguage()->commaList( $extArray );
+       }
+
+       /**
+        * Output the start of the table
+        *
+        * Including opening <table>, and first <tr> with column headers.
+        */
+       protected function outputTableStart( $mediaType ) {
+               $this->getOutput()->addHTML(
+                       Html::openElement(
+                               'table',
+                               array( 'class' => array(
+                                       'mw-mediastats-table',
+                                       'mw-mediastats-table-' . strtolower( $mediaType ),
+                                       'sortable',
+                                       'wikitable'
+                               ))
+                       )
+               );
+               $this->getOutput()->addHTML( $this->getTableHeaderRow() );
+       }
+
+       /**
+        * Get (not output) the header row for the table
+        *
+        * @return String the header row of the able
+        */
+       protected function getTableHeaderRow() {
+               $headers = array( 'mimetype', 'extensions', 'count', 'totalbytes' );
+               $ths = '';
+               foreach ( $headers as $header ) {
+                       $ths .= Html::rawElement(
+                               'th',
+                               array(),
+                               // for grep:
+                               // mediastatistics-table-mimetype, mediastatistics-table-extensions
+                               // tatistics-table-count, mediastatistics-table-totalbytes
+                               $this->msg( 'mediastatistics-table-' . $header )->parse()
+                       );
+               }
+               return Html::rawElement( 'tr', array(), $ths );
+       }
+
+       /**
+        * Output a header for a new media type section
+        *
+        * @param $mediaType string A media type (e.g. from the MEDIATYPE_xxx constants)
+        */
+       protected function outputMediaType( $mediaType ) {
+               $this->getOutput()->addHTML(
+                       Html::element(
+                               'h2',
+                               array( 'class' => array(
+                                       'mw-mediastats-mediatype',
+                                       'mw-mediastats-mediatype-' . strtolower( $mediaType )
+                               )),
+                               // for grep
+                               // mediastatistics-header-unknown, mediastatistics-header-bitmap,
+                               // mediastatistics-header-drawing, mediastatistics-header-audio,
+                               // mediastatistics-header-video, mediastatistics-header-multimedia,
+                               // mediastatistics-header-office, mediastatistics-header-text,
+                               // mediastatistics-header-executable, mediastatistics-header-archive,
+                               $this->msg( 'mediastatistics-header-' . strtolower( $mediaType ) )->text()
+                       )
+               );
+               /** @todo Possibly could add a message here explaining what the different types are.
+                *    not sure if it is needed though.
+                */
+       }
+
+       /**
+        * parse the fake title format that this special page abuses querycache with.
+        *
+        * @param $fakeTitle String A string formatted as <media type>;<mime type>;<count>;<bytes>
+        * @return Array The constituant parts of $fakeTitle
+        */
+       private function splitFakeTitle( $fakeTitle ) {
+               return explode( ';', $fakeTitle, 4 );
+       }
+
+       /**
+        * What group to put the page in
+        * @return string
+        */
+       protected function getGroupName() {
+               return 'media';
+       }
+
+       /**
+        * This method isn't used, since we override outputResults, but
+        * we need to implement since abstract in parent class.
+        *
+        * @param $skin Skin
+        * @param $result stdObject Result row
+        */
+       public function formatResult( $skin, $result ) {
+               throw new MWException( "unimplemented" );
+       }
+
+       /**
+        * Initialize total values so we can figure out percentages later.
+        *
+        * @param $dbr DatabaseBase
+        * @param $res ResultWrapper
+        */
+       public function preprocessResults( $dbr, $res ) {
+               $this->totalCount = $this->totalBytes = 0;
+               foreach ( $res as $row ) {
+                       list( , , $count, $bytes ) = $this->splitFakeTitle( $row->title );
+                       $this->totalCount += $count;
+                       $this->totalBytes += $bytes;
+               }
+               $res->seek( 0 );
+       }
+}
index 67c96a7..f0e360e 100644 (file)
@@ -151,7 +151,7 @@ class SpecialStatistics extends SpecialPage {
                                        $this->getLanguage()->formatNum( $this->total ),
                                        array( 'class' => 'mw-statistics-pages' ),
                                        'statistics-pages-desc' ) .
-                               $this->formatRow( Linker::linkKnown( SpecialPage::getTitleFor( 'Listfiles' ),
+                               $this->formatRow( Linker::linkKnown( SpecialPage::getTitleFor( 'MediaStatistics' ),
                                        $this->msg( 'statistics-files' )->parse() ),
                                        $this->getLanguage()->formatNum( $this->images ),
                                        array( 'class' => 'mw-statistics-files' ) );
index e56789a..77ea9ce 100644 (file)
        "default-skin-not-found": "Whoops! The default skin for your wiki, defined in <code dir=\"ltr\">$wgDefaultSkin</code> as <code>$1</code>, is not available.\n\nYour installation seems to include the following skins. See [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manual: Skin configuration] for information how to enable them and choose the default.\n\n$2\n\n; If you have just installed MediaWiki:\n: You probably installed from git, or directly from the source code using some other method. This is expected. Try installing some skins from [https://www.mediawiki.org/wiki/Category:All_skins mediawiki.org's skin directory], by:\n:* Downloading the [https://www.mediawiki.org/wiki/Download tarball installer], which comes with several skins and extensions. You can copy and paste the <code>skins/</code> directory from it.\n:* Cloning one of the <code>mediawiki/skins/*</code> repositories via git into the <code dir=\"ltr\">skins/</code> directory of your MediaWiki installation.\n: Doing this should not interfere with your git repository if you're a MediaWiki developer.\n\n; If you have just upgraded MediaWiki:\n: MediaWiki 1.24 and newer no longer automatically enables installed skins (see [https://www.mediawiki.org/wiki/Manual:Skin_autodiscovery Manual: Skin autodiscovery]). You can paste the following lines into <code>LocalSettings.php</code> to enable all currently installed skins:\n\n<pre dir=\"ltr\">$3</pre>\n\n; If you have just modified <code>LocalSettings.php</code>:\n: Double-check the skin names for typos.",
        "default-skin-not-found-no-skins": "Whoops! The default skin for your wiki, defined in <code>$wgDefaultSkin</code> as <code>$1</code>, is not available.\n\nYou have no installed skins.\n\n; If you have just installed or upgraded MediaWiki:\n: You probably installed from git, or directly from the source code using some other method. This is expected. MediaWiki 1.24 and newer doesn't include any skins in the main repository. Try installing some skins from [https://www.mediawiki.org/wiki/Category:All_skins mediawiki.org's skin directory], by:\n:* Downloading the [https://www.mediawiki.org/wiki/Download tarball installer], which comes with several skins and extensions. You can copy and paste the <code>skins/</code> directory from it.\n:* Cloning one of the <code>mediawiki/skins/*</code> repositories via git into the <code dir=\"ltr\">skins/</code> directory of your MediaWiki installation.\n: Doing this should not interfere with your git repository if you're a MediaWiki developer. See [https://www.mediawiki.org/wiki/Manual:Skin_configuration Manual: Skin configuration] for information how to enable skins and choose the default.\n",
        "default-skin-not-found-row-enabled": "* <code>$1</code> / $2 (enabled)",
-       "default-skin-not-found-row-disabled": "* <code>$1</code> / $2 ('''disabled''')"
+       "default-skin-not-found-row-disabled": "* <code>$1</code> / $2 ('''disabled''')",
+       "mediastatistics": "Media statistics",
+       "mediastatistics-summary": "Statistics about uploaded file types. This only includes the most recent version of a file. Old or deleted versions of files are excluded.",
+       "mediastatistics-nfiles": "$1 ($2%)",
+       "mediastatistics-nbytes": "{{PLURAL:$1|$1 byte|$1 bytes}} ($2; $3%)",
+       "mediastatistics-table-mimetype": "MIME type",
+       "mediastatistics-table-extensions": "Possible extensions",
+       "mediastatistics-table-count": "Number of files",
+       "mediastatistics-table-totalbytes": "Combined size",
+       "mediastatistics-header-unknown": "Unknown",
+       "mediastatistics-header-bitmap": "Bitmap images",
+       "mediastatistics-header-drawing": "Drawings (vector images)",
+       "mediastatistics-header-audio": "Audio",
+       "mediastatistics-header-video": "Videos",
+       "mediastatistics-header-multimedia": "Rich media",
+       "mediastatistics-header-office": "Office",
+       "mediastatistics-header-text": "Textual",
+       "mediastatistics-header-executable": "Executables",
+       "mediastatistics-header-archive": "Compressed formats"
 }
index 5c5683a..eebd4e8 100644 (file)
        "default-skin-not-found": "Message shown when the default skin for this MediaWiki installation can not be found.\n\nParameters:\n* $1: skin identifier for the default skin\n* $2: list of installed skins, composed using {{msg-mw|default-skin-not-found-row-enabled}} and {{msg-mw|default-skin-not-found-row-disabled}}\n* $3: code snippet to use to enable installed skins",
        "default-skin-not-found-no-skins": "Message shown when the default skin for this MediaWiki installation can not be found and the installation has no skins at all.\n\nParameters:\n* $1: name of the default skin",
        "default-skin-not-found-row-enabled": "One row of the list of installed skins shown as a part of the following message (for an enabled skin):\n* {{msg-mw|default-skin-not-found}}\n----\nParameters:\n* $1 - skin identifier\n* $2 - human-readable skin name\nSee also:\n* {{msg-mw|Default-skin-not-found-row-disabled}}",
-       "default-skin-not-found-row-disabled": "One row of the list of installed skins shown as a part of the following message (for a disabled skin):\n* {{msg-mw|default-skin-not-found}}\n----\nParameters:\n* $1 - skin identifier\n* $2 - human-readable skin name\nSee also:\n* {{msg-mw|Default-skin-not-found-row-enabled}}"
+       "default-skin-not-found-row-disabled": "One row of the list of installed skins shown as a part of the following message (for a disabled skin):\n* {{msg-mw|default-skin-not-found}}\n----\nParameters:\n* $1 - skin identifier\n* $2 - human-readable skin name\nSee also:\n* {{msg-mw|Default-skin-not-found-row-enabled}}",
+       "mediastatistics": "{{doc-special|MediaStatistics}}",
+       "mediastatistics-summary": "{{doc-specialpagesummary|MediaStatistics}}\nUsed to explain that this page only does statistics over current versions of files. \"Old\" versions of files and deleted files are not counted.",
+       "mediastatistics-nfiles": "{{optional}}\nEntry in table on [[Special:MediaStatistics]] that gives total number of files. $1 - number of files. $2 - percentage of total files that is this type (percent will be formatted to have about 3 interesting digits. e.g. 0.121 or 10.2)",
+       "mediastatistics-nbytes": "{{optional}}\nCombined space of this type of file. Bytes and \"human units\" are shown so that users can better get a sense of magnitude when making comparisions.\n*$1 - total space in bytes.\n*$2 - total space in \"human units\" (i.e. KB, MB, GB, etc)\n*$3 - What percentage of the space all uploads take up does this file take up. ",
+       "mediastatistics-table-mimetype": "Header for table on Special:MediaStatistics. Column that lists MIME types (The values in this column will look like 'image/jpeg', and be linked to Special:MIMESearch).",
+       "mediastatistics-table-extensions": "Header for column in tables on [[Special:MediaStatistics]] that lists possible extensions for a given file type. (The values in this column will be a comma separated list of file extensions, such as '.webm' or '.png, .apng').",
+       "mediastatistics-table-count": "Column header on Special:MediaStatistics for the number of files column. The headers in this column use {{msg-mw|mediastatistics-nfiles}}.",
+       "mediastatistics-table-totalbytes": "Column header on Special:MediaStatistics for the number of bytes that this file type takes up. Values for this column use {{msg-mw|mediastatistics-nbytes}}",
+       "mediastatistics-header-unknown": "Header on Special:MediaStatistics for file types that are in the unknown category",
+       "mediastatistics-header-bitmap": "Header on [[Special:MediaStatistics]] for file types that are in the bitmap category. This includes raster graphics like PNG, JPEG, XCF, GIF etc. Vector graphics like SVG are considered \"drawings\" ({{msg-mw|mediastatistics-header-drawing}})",
+       "mediastatistics-header-drawing": "Header on [[Special:MediaStatistics]] for file types that are in the drawing category. This includes vector images like SVGs. Some chemical markup formats may also be included here.",
+       "mediastatistics-header-audio": "Header on [[Special:MediaStatistics]] for file types that are in the audio category",
+       "mediastatistics-header-video": "Header on [[Special:MediaStatistics]] for file types that are in the video category",
+       "mediastatistics-header-multimedia": "Header on [[Special:MediaStatistics]] for file types that are in the multimedia category. This does not include plain audio or video files, but more complex multimedia such as flash or vrml. This especially includes scripted multimedia. Ogg files in which MediaWiki cannot determine if it is an audio or video file (or something else) are included here.",
+       "mediastatistics-header-office": "Header on [[Special:MediaStatistics]] for file types that are in the Office category. This includes PDFs, open office files, MSWord files, etc.",
+       "mediastatistics-header-text": "Header on [[Special:MediaStatistics]] for file types that are in the text category. This includes simple text formats, including plain text formats, json, csv, and xml. Source code of compiled programming languages may be included here in the future, but isn't currently.",
+       "mediastatistics-header-executable": "Header on [[Special:MediaStatistics]] for file types that are in the executable category. This includes things like source files for interpreted programming language (Shell scripts, javascript, etc).",
+       "mediastatistics-header-archive": "Header on [[Special:MediaStatistics]] for file types that are in the archive category. Includes things like tar, zip, gzip etc."
 }
index 7541f31..0a10279 100644 (file)
@@ -428,6 +428,7 @@ $specialPageAliases = array(
        'Log'                       => array( 'Log', 'Logs' ),
        'Lonelypages'               => array( 'LonelyPages', 'OrphanedPages' ),
        'Longpages'                 => array( 'LongPages' ),
+       'MediaStatistics'           => array( 'MediaStatistics' ),
        'MergeHistory'              => array( 'MergeHistory' ),
        'MIMEsearch'                => array( 'MIMESearch' ),
        'Mostcategories'            => array( 'MostCategories' ),