From 2dd30d0c6ddfdd609cb2a64a78e8d09d7ca43574 Mon Sep 17 00:00:00 2001 From: Brian Wolff Date: Thu, 18 Apr 2013 21:16:41 -0300 Subject: [PATCH] Allow listing old files in Special:ListFiles. Add Special:AllMyUploads This solves the problem of new users on commons wanting a list of their files, but something like cropbot modifying it and taking it off Special:MyUploads. I'm worried this is a bit hacky to make TablePager work with two queries. If anyone has any suggestions on how to do this in a less hacky way, please say. Some notes: * This totally removes any revdeleted entries instead of dealing with them. Some future iteration can do selective deletion. * Should maybe add links to old versions of files description page somewhere. Not sure where in ui that would fit (The date maybe) (by old file description page I mean something like flagged revs filetimestamp parameter) * The latest version column should perhaps have "latest" and "old" instead of "yes" or "no" * This is slightly different from the suggestion on the bug report as it shows all revisions (instead of say just first revisions). I think showing all revisions makes more sense for the "where are my uploads" use case. Second of all, the checkbox is not on by default. Bug: 30607 Change-Id: I9e58db1f212e3bb361316c05ef32d4b9c31c6490 --- RELEASE-NOTES-1.22 | 3 + includes/AutoLoader.php | 1 + includes/SpecialPage.php | 18 +- includes/SpecialPageFactory.php | 1 + includes/specials/SpecialListfiles.php | 234 ++++++++++++++++++++++--- languages/messages/MessagesEn.php | 8 +- languages/messages/MessagesQqq.php | 4 + maintenance/language/messages.inc | 4 + 8 files changed, 250 insertions(+), 23 deletions(-) diff --git a/RELEASE-NOTES-1.22 b/RELEASE-NOTES-1.22 index dfc972a95f..c90fdbe06d 100644 --- a/RELEASE-NOTES-1.22 +++ b/RELEASE-NOTES-1.22 @@ -201,6 +201,9 @@ production. "nolines", "packed", "packed-overlay", or "packed-hover". * (bug 47399) A success message is now displayed after changing the password. * Make thumb.php give HTTP redirects for file redirects +* (bug 30607) Special:ListFiles can now show old versions of files. Additionally + Special:AllMyUploads was introduced so the user can get a list of all things + they have ever uploaded, even if it was subsequently overriden. === Bug fixes in 1.22 === * Disable Special:PasswordReset when $wgEnableEmail is false. Previously one diff --git a/includes/AutoLoader.php b/includes/AutoLoader.php index b830b16835..82d204b138 100644 --- a/includes/AutoLoader.php +++ b/includes/AutoLoader.php @@ -232,6 +232,7 @@ $wgAutoloadLocalClasses = array( 'SpecialMypage' => 'includes/SpecialPage.php', 'SpecialMytalk' => 'includes/SpecialPage.php', 'SpecialMyuploads' => 'includes/SpecialPage.php', + 'SpecialAllMyUploads' => 'includes/SpecialPage.php', 'SpecialPage' => 'includes/SpecialPage.php', 'SpecialPageFactory' => 'includes/SpecialPageFactory.php', 'SpecialRedirectToSpecial' => 'includes/SpecialPage.php', diff --git a/includes/SpecialPage.php b/includes/SpecialPage.php index d87f9107ac..94782db3df 100644 --- a/includes/SpecialPage.php +++ b/includes/SpecialPage.php @@ -1386,7 +1386,7 @@ class SpecialMycontributions extends RedirectSpecialPage { class SpecialMyuploads extends RedirectSpecialPage { function __construct() { parent::__construct( 'Myuploads' ); - $this->mAllowedRedirectParams = array( 'limit' ); + $this->mAllowedRedirectParams = array( 'limit', 'ilshowall', 'ilsearch' ); } function getRedirect( $subpage ) { @@ -1394,6 +1394,22 @@ class SpecialMyuploads extends RedirectSpecialPage { } } +/** + * Redirect Special:Listfiles?user=$wgUser&ilshowall=true + */ +class SpecialAllMyUploads extends RedirectSpecialPage { + function __construct() { + parent::__construct( 'AllMyUploads' ); + $this->mAllowedRedirectParams = array( 'limit', 'ilsearch' ); + } + + function getRedirect( $subpage ) { + $this->mAddedRedirectParams['ilshowall'] = 1; + return SpecialPage::getTitleFor( 'Listfiles', $this->getUser()->getName() ); + } +} + + /** * Redirect from Special:PermanentLink/### to index.php?oldid=### */ diff --git a/includes/SpecialPageFactory.php b/includes/SpecialPageFactory.php index 9f5d4ada10..a412fdb8dd 100644 --- a/includes/SpecialPageFactory.php +++ b/includes/SpecialPageFactory.php @@ -163,6 +163,7 @@ class SpecialPageFactory { 'Mypage' => 'SpecialMypage', 'Mytalk' => 'SpecialMytalk', 'Myuploads' => 'SpecialMyuploads', + 'AllMyUploads' => 'SpecialAllMyUploads', 'PermanentLink' => 'SpecialPermanentLink', 'Redirect' => 'SpecialRedirect', 'Revisiondelete' => 'SpecialRevisionDelete', diff --git a/includes/specials/SpecialListfiles.php b/includes/specials/SpecialListfiles.php index 24bd19fc88..9377628b75 100644 --- a/includes/specials/SpecialListfiles.php +++ b/includes/specials/SpecialListfiles.php @@ -36,13 +36,15 @@ class SpecialListFiles extends IncludableSpecialPage { } else { $userName = $this->getRequest()->getText( 'user', $par ); $search = $this->getRequest()->getText( 'ilsearch', '' ); + $showAll = $this->getRequest()->getBool( 'ilshowall', false ); } $pager = new ImageListPager( $this->getContext(), $userName, $search, - $this->including() + $this->including(), + $showAll ); if ( $this->including() ) { @@ -66,27 +68,30 @@ class SpecialListFiles extends IncludableSpecialPage { */ class ImageListPager extends TablePager { var $mFieldNames = null; + // Subclasses should override buildQueryConds instead of using $mQueryConds variable. var $mQueryConds = array(); var $mUserName = null; var $mSearch = ''; var $mIncluding = false; + var $mShowAll = false; + var $mTableName = 'image'; function __construct( IContextSource $context, $userName = null, $search = '', - $including = false + $including = false, $showAll = false ) { global $wgMiserMode; $this->mIncluding = $including; + $this->mShowAll = $showAll; if ( $userName ) { $nt = Title::newFromText( $userName, NS_USER ); if ( !is_null( $nt ) ) { $this->mUserName = $nt->getText(); - $this->mQueryConds['img_user_text'] = $this->mUserName; } } - if ( $search != '' && !$wgMiserMode ) { + if ( $search !== '' && !$wgMiserMode ) { $this->mSearch = $search; $nt = Title::newFromURL( $this->mSearch ); @@ -111,6 +116,42 @@ class ImageListPager extends TablePager { parent::__construct( $context ); } + /** + * Build the where clause of the query. + * + * Replaces the older mQueryConds member variable. + * @param $table String Either "image" or "oldimage" + * @return array The query conditions. + */ + protected function buildQueryConds( $table ) { + $prefix = $table === 'image' ? 'img' : 'oi'; + $conds = array(); + + if ( !is_null( $this->mUserName ) ) { + $conds[ $prefix . '_user_text' ] = $this->mUserName; + } + + if ( $this->mSearch !== '' ) { + $nt = Title::newFromURL( $this->mSearch ); + if ( $nt ) { + $dbr = wfGetDB( DB_SLAVE ); + $conds[] = 'LOWER(' . $prefix . '_name)' . + $dbr->buildLike( $dbr->anyString(), + strtolower( $nt->getDBkey() ), $dbr->anyString() ); + } + } + + if ( $table === 'oldimage' ) { + // Don't want to deal with revdel. + // Future fixme: Show partial information as appropriate. + // Would have to be careful about filtering by username when username is deleted. + $conds['oi_deleted'] = 0; + } + + // Add mQueryConds in case anyone was subclassing and using the old variable. + return $conds + $this->mQueryConds; + } + /** * @return Array */ @@ -125,9 +166,12 @@ class ImageListPager extends TablePager { 'img_user_text' => $this->msg( 'listfiles_user' )->text(), 'img_description' => $this->msg( 'listfiles_description' )->text(), ); - if ( !$wgMiserMode ) { + if ( !$wgMiserMode && !$this->mShowAll ) { $this->mFieldNames['count'] = $this->msg( 'listfiles_count' )->text(); } + if ( $this->mShowAll ) { + $this->mFieldNames['top'] = $this->msg( 'listfiles-latestversion' )->text(); + } } return $this->mFieldNames; @@ -139,26 +183,76 @@ class ImageListPager extends TablePager { return false; } $sortable = array( 'img_timestamp', 'img_name', 'img_size' ); - if ( $wgMiserMode && isset( $this->mQueryConds['img_user_text'] ) ) { - // If we're sorting by user, the index only supports sorting by time + /* For reference, the indicies we can use for sorting are: + * On the image table: img_usertext_timestamp, img_size, img_timestamp + * On oldimage: oi_usertext_timestamp, oi_name_timestamp + * + * In particular that means we cannot sort by timestamp when not filtering + * by user and including old images in the results. Which is sad. + */ + if ( $wgMiserMode && !is_null( $this->mUserName ) ) { + // If we're sorting by user, the index only supports sorting by time. if ( $field === 'img_timestamp' ) { return true; } else { return false; } + } elseif ( $wgMiserMode && $this->mShowAll /* && mUserName === null */ ) { + // no oi_timestamp index, so only alphabetical sorting in this case. + if ( $field === 'img_name' ) { + return true; + } else { + return false; + } } return in_array( $field, $sortable ); } function getQueryInfo() { - $tables = array( 'image' ); + // Hacky Hacky Hacky - I want to get query info + // for two different tables, without reimplementing + // the pager class. + $qi = $this->getQueryInfoReal( $this->mTableName ); + return $qi; + } + + /** + * Actually get the query info. + * + * This is to allow displaying both stuff from image and oldimage table. + * + * This is a bit hacky. + * + * @param $table String Either 'image' or 'oldimage' + * @return array Query info + */ + protected function getQueryInfoReal( $table ) { + $prefix = $table === 'oldimage' ? 'oi' : 'img'; + + $tables = array( $table ); $fields = array_keys( $this->getFieldNames() ); - $fields[] = 'img_user'; - $fields[array_search( 'thumb', $fields )] = 'img_name AS thumb'; + + if ( $table === 'oldimage' ) { + foreach ( $fields as $id => &$field ) { + if ( substr( $field, 0, 4 ) !== 'img_' ) { + continue; + } + $field = $prefix . substr( $field, 3 ) . ' AS ' . $field; + } + $fields[array_search('top', $fields)] = "'no' AS top"; + } else { + if ( $this->mShowAll ) { + $fields[array_search( 'top', $fields )] = "'yes' AS top"; + } + } + $fields[] = $prefix . '_user AS img_user'; + $fields[array_search( 'thumb', $fields )] = $prefix . '_name AS thumb'; + $options = $join_conds = array(); # Depends on $wgMiserMode + # Will also not happen if mShowAll is true. if ( isset( $this->mFieldNames['count'] ) ) { $tables[] = 'oldimage'; @@ -183,14 +277,103 @@ class ImageListPager extends TablePager { return array( 'tables' => $tables, 'fields' => $fields, - 'conds' => $this->mQueryConds, + 'conds' => $this->buildQueryConds( $table ), 'options' => $options, 'join_conds' => $join_conds ); } + /** + * Override reallyDoQuery to mix together two queries. + * + * @note $asc is named $descending in IndexPager base class. However + * it is true when the order is ascending, and false when the order + * is descending, so I renamed it to $asc here. + */ + function reallyDoQuery( $offset, $limit, $asc ) { + $prevTableName = $this->mTableName; + $this->mTableName = 'image'; + list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->buildQueryInfo( $offset, $limit, $asc ); + $imageRes = $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds ); + $this->mTableName = $prevTableName; + + if ( !$this->mShowAll ) { + return $imageRes; + } + + $this->mTableName = 'oldimage'; + + # Hacky... + $oldIndex = $this->mIndexField; + if ( substr( $this->mIndexField, 0, 4 ) !== 'img_' ) { + throw new MWException( "Expected to be sorting on an image table field" ); + } + $this->mIndexField = 'oi_' . substr( $this->mIndexField, 4 ); + + list( $tables, $fields, $conds, $fname, $options, $join_conds ) = $this->buildQueryInfo( $offset, $limit, $asc ); + $oldimageRes = $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds ); + + $this->mTableName = $prevTableName; + $this->mIndexField = $oldIndex; + + return $this->combineResult( $imageRes, $oldimageRes, $limit, $asc ); + } + + /** + * Combine results from 2 tables. + * + * Note: This will throw away some results + * + * @param $res1 ResultWrapper + * @param $res2 ResultWrapper + * @param $limit int + * @param $ascending boolean See note about $asc in $this->reallyDoQuery + * @return FakeResultWrapper $res1 and $res2 combined + */ + protected function combineResult( $res1, $res2, $limit, $ascending ) { + $res1->rewind(); + $res2->rewind(); + $topRes1 = $res1->next(); + $topRes2 = $res2->next(); + $resultArray = array(); + for ( $i = 0; $i < $limit && $topRes1 && $topRes2; $i++ ) { + if ( strcmp( $topRes1->{ $this->mIndexField }, $topRes2->{ $this->mIndexField } ) > 0 ) { + if ( !$ascending ) { + $resultArray[] = $topRes1; + $topRes1 = $res1->next(); + } else { + $resultArray[] = $topRes2; + $topRes2 = $res2->next(); + } + } else { + if ( !$ascending ) { + $resultArray[] = $topRes2; + $topRes2 = $res2->next(); + } else { + $resultArray[] = $topRes1; + $topRes1 = $res1->next(); + } + } + } + for ( ; $i < $limit && $topRes1; $i++ ) { + $resultArray[] = $topRes1; + $topRes1 = $res1->next(); + } + for ( ; $i < $limit && $topRes2; $i++ ) { + $resultArray[] = $topRes2; + $topRes2 = $res2->next(); + } + return new FakeResultWrapper( $resultArray ); + } + function getDefaultSort() { - return 'img_timestamp'; + global $wgMiserMode; + if ( $this->mShowAll && $wgMiserMode && is_null( $this->mUserName ) ) { + // Unfortunately no index on oi_timestamp. + return 'img_name'; + } else { + return 'img_timestamp'; + } } function doBatchLookups() { @@ -206,13 +389,18 @@ class ImageListPager extends TablePager { function formatValue( $field, $value ) { switch ( $field ) { case 'thumb': - $file = wfLocalFile( $value ); - $thumb = $file->transform( array( 'width' => 180, 'height' => 360 ) ); - - return $thumb->toHtml( array( 'desc-link' => true ) ); + $opt = array( 'time' => $this->mCurrentRow->img_timestamp ); + $file = RepoGroup::singleton()->getLocalRepo()->findFile( $value, $opt ); + // If statement for paranoia + if ( $file ) { + $thumb = $file->transform( array( 'width' => 180, 'height' => 360 ) ); + return $thumb->toHtml( array( 'desc-link' => true ) ); + } else { + return htmlspecialchars( $value ); + } case 'img_timestamp': - $timeAndDate = $this->getLanguage()->userTimeAndDate( $value, $this->getUser() ); - return htmlspecialchars( $timeAndDate ); + // We may want to make this a link to the "old" version when displaying old files + return htmlspecialchars( $this->getLanguage()->userTimeAndDate( $value, $this->getUser() ) ); case 'img_name': static $imgfile = null; if ( $imgfile === null ) { @@ -254,6 +442,8 @@ class ImageListPager extends TablePager { return Linker::formatComment( $value ); case 'count': return intval( $value ) + 1; + case 'top': + return $this->msg( 'listfiles-latestversion-' . $value ); } } @@ -281,13 +471,17 @@ class ImageListPager extends TablePager { 'tabindex' => 3, ) ); + $inputForm['listfiles-show-all'] = HTML::input( 'ilshowall', 1, 'checkbox', array( + 'checked' => $this->mShowAll, + 'tabindex' => 4, + ) ); return Html::openElement( 'form', array( 'method' => 'get', 'action' => $wgScript, 'id' => 'mw-listfiles-form' ) ) . Xml::fieldset( $this->msg( 'listfiles' )->text() ) . Html::hidden( 'title', $this->getTitle()->getPrefixedText() ) . - Xml::buildForm( $inputForm, 'table_pager_limit_submit', array( 'tabindex' => 4 ) ) . - $this->getHiddenFields( array( 'limit', 'ilsearch', 'user', 'title' ) ) . + Xml::buildForm( $inputForm, 'table_pager_limit_submit', array( 'tabindex' => 5 ) ) . + $this->getHiddenFields( array( 'limit', 'ilsearch', 'user', 'title', 'ilshowall' ) ) . Html::closeElement( 'fieldset' ) . Html::closeElement( 'form' ) . "\n"; } diff --git a/languages/messages/MessagesEn.php b/languages/messages/MessagesEn.php index 0ba7ccd9bc..a26839e392 100644 --- a/languages/messages/MessagesEn.php +++ b/languages/messages/MessagesEn.php @@ -385,6 +385,7 @@ $magicWords = array( $specialPageAliases = array( 'Activeusers' => array( 'ActiveUsers' ), 'Allmessages' => array( 'AllMessages' ), + 'AllMyUploads' => array( 'AllMyUploads' ), 'Allpages' => array( 'AllPages' ), 'Ancientpages' => array( 'AncientPages' ), 'Badtitle' => array( 'Badtitle' ), @@ -2514,8 +2515,7 @@ You may want to try at a less busy time.', 'upload_source_file' => '(a file on your computer)', # Special:ListFiles -'listfiles-summary' => 'This special page shows all uploaded files. -When filtered by user, only files where that user uploaded the most recent version of the file are shown.', +'listfiles-summary' => 'This special page shows all uploaded files.', 'listfiles_search_for' => 'Search for media name:', 'imgfile' => 'file', 'listfiles' => 'File list', @@ -2526,6 +2526,10 @@ When filtered by user, only files where that user uploaded the most recent versi 'listfiles_size' => 'Size', 'listfiles_description' => 'Description', 'listfiles_count' => 'Versions', +'listfiles-show-all' => 'Include old versions of images', +'listfiles-latestversion' => 'Current version', +'listfiles-latestversion-yes' => 'Yes', +'listfiles-latestversion-no' => 'No', # File description page 'file-anchor-link' => 'File', diff --git a/languages/messages/MessagesQqq.php b/languages/messages/MessagesQqq.php index 598efb448d..ef5859b2df 100644 --- a/languages/messages/MessagesQqq.php +++ b/languages/messages/MessagesQqq.php @@ -4218,6 +4218,10 @@ See also: {{Identical|Description}}', 'listfiles_count' => 'One of the table column headers in [[Special:Listfiles]] denoting the amount of saved versions of that file. {{Identical|Version}}', +'listfiles-show-all' => 'Label for a checkbox in the options section of [[Special:ListFiles]]. The checkbox controls if old versions of image files are shown in Special:Listfiles (as opposed to only the most recent version of an image).', +'listfiles-latestversion' => 'Column header for the result table on [[Special:Listfiles]]. The column is for if the entry is for a current version of the file, or if it is for a historic version of the file that has since been overriden', +'listfiles-latestversion-yes' => 'Text to display in latestversion column if the entry is the latest version of the file. See also {{msg-mw|listfiles-latestversion-no}} and {{msg-mw|listfiles-latestversion}}.', +'listfiles-latestversion-no' => 'Text to display in latestversion column if the entry is an old version of the file. See also {{msg-mw|listfiles-latestversion-yes}} and {{msg-mw|listfiles-latestversion}}.', # File description page 'file-anchor-link' => '{{Identical|File}}', diff --git a/maintenance/language/messages.inc b/maintenance/language/messages.inc index b9b8544b99..5d559f9744 100644 --- a/maintenance/language/messages.inc +++ b/maintenance/language/messages.inc @@ -1592,6 +1592,10 @@ $wgMessageStructure = array( 'listfiles_size', 'listfiles_description', 'listfiles_count', + 'listfiles-show-all', + 'listfiles-latestversion', + 'listfiles-latestversion-yes', + 'listfiles-latestversion-no', ), 'filedescription' => array( 'file-anchor-link', -- 2.20.1