MediaWiki REL1_31
ImageListPager.php
Go to the documentation of this file.
1<?php
28
30
31 protected $mFieldNames = null;
32
33 // Subclasses should override buildQueryConds instead of using $mQueryConds variable.
34 protected $mQueryConds = [];
35
36 protected $mUserName = null;
37
43 protected $mUser = null;
44
45 protected $mSearch = '';
46
47 protected $mIncluding = false;
48
49 protected $mShowAll = false;
50
51 protected $mTableName = 'image';
52
53 function __construct( IContextSource $context, $userName = null, $search = '',
54 $including = false, $showAll = false
55 ) {
56 $this->setContext( $context );
57 $this->mIncluding = $including;
58 $this->mShowAll = $showAll;
59
60 if ( $userName !== null && $userName !== '' ) {
61 $nt = Title::makeTitleSafe( NS_USER, $userName );
62 if ( is_null( $nt ) ) {
63 $this->outputUserDoesNotExist( $userName );
64 } else {
65 $this->mUserName = $nt->getText();
66 $user = User::newFromName( $this->mUserName, false );
67 if ( $user ) {
68 $this->mUser = $user;
69 }
70 if ( !$user || ( $user->isAnon() && !User::isIP( $user->getName() ) ) ) {
71 $this->outputUserDoesNotExist( $userName );
72 }
73 }
74 }
75
76 if ( $search !== '' && !$this->getConfig()->get( 'MiserMode' ) ) {
77 $this->mSearch = $search;
78 $nt = Title::newFromText( $this->mSearch );
79
80 if ( $nt ) {
82 $this->mQueryConds[] = 'LOWER(img_name)' .
83 $dbr->buildLike( $dbr->anyString(),
84 strtolower( $nt->getDBkey() ), $dbr->anyString() );
85 }
86 }
87
88 if ( !$including ) {
89 if ( $this->getRequest()->getText( 'sort', 'img_date' ) == 'img_date' ) {
90 $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
91 } else {
92 $this->mDefaultDirection = IndexPager::DIR_ASCENDING;
93 }
94 } else {
95 $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
96 }
97
98 parent::__construct( $context );
99 }
100
106 function getRelevantUser() {
107 return $this->mUser;
108 }
109
115 protected function outputUserDoesNotExist( $userName ) {
116 $this->getOutput()->wrapWikiMsg(
117 "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
118 [
119 'listfiles-userdoesnotexist',
120 wfEscapeWikiText( $userName ),
121 ]
122 );
123 }
124
132 protected function buildQueryConds( $table ) {
133 $prefix = $table === 'image' ? 'img' : 'oi';
134 $conds = [];
135
136 if ( !is_null( $this->mUserName ) ) {
137 // getQueryInfoReal() should have handled the tables and joins.
139 $actorWhere = ActorMigration::newMigration()->getWhere(
140 $dbr,
141 $prefix . '_user',
142 User::newFromName( $this->mUserName, false )
143 );
144 $conds[] = $actorWhere['conds'];
145 }
146
147 if ( $this->mSearch !== '' ) {
148 $nt = Title::newFromText( $this->mSearch );
149 if ( $nt ) {
151 $conds[] = 'LOWER(' . $prefix . '_name)' .
152 $dbr->buildLike( $dbr->anyString(),
153 strtolower( $nt->getDBkey() ), $dbr->anyString() );
154 }
155 }
156
157 if ( $table === 'oldimage' ) {
158 // Don't want to deal with revdel.
159 // Future fixme: Show partial information as appropriate.
160 // Would have to be careful about filtering by username when username is deleted.
161 $conds['oi_deleted'] = 0;
162 }
163
164 // Add mQueryConds in case anyone was subclassing and using the old variable.
165 return $conds + $this->mQueryConds;
166 }
167
171 function getFieldNames() {
172 if ( !$this->mFieldNames ) {
173 $this->mFieldNames = [
174 'img_timestamp' => $this->msg( 'listfiles_date' )->text(),
175 'img_name' => $this->msg( 'listfiles_name' )->text(),
176 'thumb' => $this->msg( 'listfiles_thumb' )->text(),
177 'img_size' => $this->msg( 'listfiles_size' )->text(),
178 ];
179 if ( is_null( $this->mUserName ) ) {
180 // Do not show username if filtering by username
181 $this->mFieldNames['img_user_text'] = $this->msg( 'listfiles_user' )->text();
182 }
183 // img_description down here, in order so that its still after the username field.
184 $this->mFieldNames['img_description'] = $this->msg( 'listfiles_description' )->text();
185
186 if ( !$this->getConfig()->get( 'MiserMode' ) && !$this->mShowAll ) {
187 $this->mFieldNames['count'] = $this->msg( 'listfiles_count' )->text();
188 }
189 if ( $this->mShowAll ) {
190 $this->mFieldNames['top'] = $this->msg( 'listfiles-latestversion' )->text();
191 }
192 }
193
194 return $this->mFieldNames;
195 }
196
197 function isFieldSortable( $field ) {
198 if ( $this->mIncluding ) {
199 return false;
200 }
201 $sortable = [ 'img_timestamp', 'img_name', 'img_size' ];
202 /* For reference, the indicies we can use for sorting are:
203 * On the image table: img_user_timestamp/img_usertext_timestamp/img_actor_timestamp,
204 * img_size, img_timestamp
205 * On oldimage: oi_usertext_timestamp/oi_actor_timestamp, oi_name_timestamp
206 *
207 * In particular that means we cannot sort by timestamp when not filtering
208 * by user and including old images in the results. Which is sad.
209 */
210 if ( $this->getConfig()->get( 'MiserMode' ) && !is_null( $this->mUserName ) ) {
211 // If we're sorting by user, the index only supports sorting by time.
212 if ( $field === 'img_timestamp' ) {
213 return true;
214 } else {
215 return false;
216 }
217 } elseif ( $this->getConfig()->get( 'MiserMode' )
218 && $this->mShowAll /* && mUserName === null */
219 ) {
220 // no oi_timestamp index, so only alphabetical sorting in this case.
221 if ( $field === 'img_name' ) {
222 return true;
223 } else {
224 return false;
225 }
226 }
227
228 return in_array( $field, $sortable );
229 }
230
231 function getQueryInfo() {
232 // Hacky Hacky Hacky - I want to get query info
233 // for two different tables, without reimplementing
234 // the pager class.
235 $qi = $this->getQueryInfoReal( $this->mTableName );
236
237 return $qi;
238 }
239
250 protected function getQueryInfoReal( $table ) {
251 $prefix = $table === 'oldimage' ? 'oi' : 'img';
252
253 $tables = [ $table ];
254 $fields = $this->getFieldNames();
255 unset( $fields['img_description'] );
256 unset( $fields['img_user_text'] );
257 $fields = array_keys( $fields );
258
259 if ( $table === 'oldimage' ) {
260 foreach ( $fields as $id => &$field ) {
261 if ( substr( $field, 0, 4 ) !== 'img_' ) {
262 continue;
263 }
264 $field = $prefix . substr( $field, 3 ) . ' AS ' . $field;
265 }
266 $fields[array_search( 'top', $fields )] = "'no' AS top";
267 } else {
268 if ( $this->mShowAll ) {
269 $fields[array_search( 'top', $fields )] = "'yes' AS top";
270 }
271 }
272 $fields[array_search( 'thumb', $fields )] = $prefix . '_name AS thumb';
273
274 $options = $join_conds = [];
275
276 # Description field
277 $commentQuery = CommentStore::getStore()->getJoin( $prefix . '_description' );
278 $tables += $commentQuery['tables'];
279 $fields += $commentQuery['fields'];
280 $join_conds += $commentQuery['joins'];
281 $fields['description_field'] = "'{$prefix}_description'";
282
283 # User fields
284 $actorQuery = ActorMigration::newMigration()->getJoin( $prefix . '_user' );
285 $tables += $actorQuery['tables'];
286 $join_conds += $actorQuery['joins'];
287 $fields['img_user'] = $actorQuery['fields'][$prefix . '_user'];
288 $fields['img_user_text'] = $actorQuery['fields'][$prefix . '_user_text'];
289 $fields['img_actor'] = $actorQuery['fields'][$prefix . '_actor'];
290
291 # Depends on $wgMiserMode
292 # Will also not happen if mShowAll is true.
293 if ( isset( $this->mFieldNames['count'] ) ) {
294 $tables[] = 'oldimage';
295
296 # Need to rewrite this one
297 foreach ( $fields as &$field ) {
298 if ( $field == 'count' ) {
299 $field = 'COUNT(oi_archive_name) AS count';
300 }
301 }
302 unset( $field );
303
304 $columnlist = preg_grep( '/^img/', array_keys( $this->getFieldNames() ) );
305 $options = [ 'GROUP BY' => array_merge( [ $fields['img_user'] ], $columnlist ) ];
306 $join_conds['oldimage'] = [ 'LEFT JOIN', 'oi_name = img_name' ];
307 }
308
309 return [
310 'tables' => $tables,
311 'fields' => $fields,
312 'conds' => $this->buildQueryConds( $table ),
313 'options' => $options,
314 'join_conds' => $join_conds
315 ];
316 }
317
330 function reallyDoQuery( $offset, $limit, $asc ) {
331 $prevTableName = $this->mTableName;
332 $this->mTableName = 'image';
333 list( $tables, $fields, $conds, $fname, $options, $join_conds ) =
334 $this->buildQueryInfo( $offset, $limit, $asc );
335 $imageRes = $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds );
336 $this->mTableName = $prevTableName;
337
338 if ( !$this->mShowAll ) {
339 return $imageRes;
340 }
341
342 $this->mTableName = 'oldimage';
343
344 # Hacky...
345 $oldIndex = $this->mIndexField;
346 if ( substr( $this->mIndexField, 0, 4 ) !== 'img_' ) {
347 throw new MWException( "Expected to be sorting on an image table field" );
348 }
349 $this->mIndexField = 'oi_' . substr( $this->mIndexField, 4 );
350
351 list( $tables, $fields, $conds, $fname, $options, $join_conds ) =
352 $this->buildQueryInfo( $offset, $limit, $asc );
353 $oldimageRes = $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds );
354
355 $this->mTableName = $prevTableName;
356 $this->mIndexField = $oldIndex;
357
358 return $this->combineResult( $imageRes, $oldimageRes, $limit, $asc );
359 }
360
372 protected function combineResult( $res1, $res2, $limit, $ascending ) {
373 $res1->rewind();
374 $res2->rewind();
375 $topRes1 = $res1->next();
376 $topRes2 = $res2->next();
377 $resultArray = [];
378 for ( $i = 0; $i < $limit && $topRes1 && $topRes2; $i++ ) {
379 if ( strcmp( $topRes1->{$this->mIndexField}, $topRes2->{$this->mIndexField} ) > 0 ) {
380 if ( !$ascending ) {
381 $resultArray[] = $topRes1;
382 $topRes1 = $res1->next();
383 } else {
384 $resultArray[] = $topRes2;
385 $topRes2 = $res2->next();
386 }
387 } else {
388 if ( !$ascending ) {
389 $resultArray[] = $topRes2;
390 $topRes2 = $res2->next();
391 } else {
392 $resultArray[] = $topRes1;
393 $topRes1 = $res1->next();
394 }
395 }
396 }
397
398 for ( ; $i < $limit && $topRes1; $i++ ) {
399 $resultArray[] = $topRes1;
400 $topRes1 = $res1->next();
401 }
402
403 for ( ; $i < $limit && $topRes2; $i++ ) {
404 $resultArray[] = $topRes2;
405 $topRes2 = $res2->next();
406 }
407
408 return new FakeResultWrapper( $resultArray );
409 }
410
411 function getDefaultSort() {
412 if ( $this->mShowAll && $this->getConfig()->get( 'MiserMode' ) && is_null( $this->mUserName ) ) {
413 // Unfortunately no index on oi_timestamp.
414 return 'img_name';
415 } else {
416 return 'img_timestamp';
417 }
418 }
419
420 function doBatchLookups() {
421 $userIds = [];
422 $this->mResult->seek( 0 );
423 foreach ( $this->mResult as $row ) {
424 $userIds[] = $row->img_user;
425 }
426 # Do a link batch query for names and userpages
427 UserCache::singleton()->doQuery( $userIds, [ 'userpage' ], __METHOD__ );
428 }
429
444 function formatValue( $field, $value ) {
445 $linkRenderer = MediaWikiServices::getInstance()->getLinkRenderer();
446 switch ( $field ) {
447 case 'thumb':
448 $opt = [ 'time' => wfTimestamp( TS_MW, $this->mCurrentRow->img_timestamp ) ];
449 $file = RepoGroup::singleton()->getLocalRepo()->findFile( $value, $opt );
450 // If statement for paranoia
451 if ( $file ) {
452 $thumb = $file->transform( [ 'width' => 180, 'height' => 360 ] );
453 if ( $thumb ) {
454 return $thumb->toHtml( [ 'desc-link' => true ] );
455 } else {
456 return wfMessage( 'thumbnail_error', '' )->escaped();
457 }
458 } else {
459 return htmlspecialchars( $value );
460 }
461 case 'img_timestamp':
462 // We may want to make this a link to the "old" version when displaying old files
463 return htmlspecialchars( $this->getLanguage()->userTimeAndDate( $value, $this->getUser() ) );
464 case 'img_name':
465 static $imgfile = null;
466 if ( $imgfile === null ) {
467 $imgfile = $this->msg( 'imgfile' )->text();
468 }
469
470 // Weird files can maybe exist? T24227
471 $filePage = Title::makeTitleSafe( NS_FILE, $value );
472 if ( $filePage ) {
473 $link = $linkRenderer->makeKnownLink(
474 $filePage,
475 $filePage->getText()
476 );
477 $download = Xml::element( 'a',
478 [ 'href' => wfLocalFile( $filePage )->getUrl() ],
479 $imgfile
480 );
481 $download = $this->msg( 'parentheses' )->rawParams( $download )->escaped();
482
483 // Add delete links if allowed
484 // From https://github.com/Wikia/app/pull/3859
485 if ( $filePage->userCan( 'delete', $this->getUser() ) ) {
486 $deleteMsg = $this->msg( 'listfiles-delete' )->text();
487
488 $delete = $linkRenderer->makeKnownLink(
489 $filePage, $deleteMsg, [], [ 'action' => 'delete' ]
490 );
491 $delete = $this->msg( 'parentheses' )->rawParams( $delete )->escaped();
492
493 return "$link $download $delete";
494 }
495
496 return "$link $download";
497 } else {
498 return htmlspecialchars( $value );
499 }
500 case 'img_user_text':
501 if ( $this->mCurrentRow->img_user ) {
502 $name = User::whoIs( $this->mCurrentRow->img_user );
503 $link = $linkRenderer->makeLink(
504 Title::makeTitle( NS_USER, $name ),
505 $name
506 );
507 } else {
508 $link = htmlspecialchars( $value );
509 }
510
511 return $link;
512 case 'img_size':
513 return htmlspecialchars( $this->getLanguage()->formatSize( $value ) );
514 case 'img_description':
515 $field = $this->mCurrentRow->description_field;
516 $value = CommentStore::getStore()->getComment( $field, $this->mCurrentRow )->text;
518 case 'count':
519 return $this->getLanguage()->formatNum( intval( $value ) + 1 );
520 case 'top':
521 // Messages: listfiles-latestversion-yes, listfiles-latestversion-no
522 return $this->msg( 'listfiles-latestversion-' . $value );
523 default:
524 throw new MWException( "Unknown field '$field'" );
525 }
526 }
527
528 function getForm() {
529 $fields = [];
530 $fields['limit'] = [
531 'type' => 'select',
532 'name' => 'limit',
533 'label-message' => 'table_pager_limit_label',
534 'options' => $this->getLimitSelectList(),
535 'default' => $this->mLimit,
536 ];
537
538 if ( !$this->getConfig()->get( 'MiserMode' ) ) {
539 $fields['ilsearch'] = [
540 'type' => 'text',
541 'name' => 'ilsearch',
542 'id' => 'mw-ilsearch',
543 'label-message' => 'listfiles_search_for',
544 'default' => $this->mSearch,
545 'size' => '40',
546 'maxlength' => '255',
547 ];
548 }
549
550 $this->getOutput()->addModules( 'mediawiki.userSuggest' );
551 $fields['user'] = [
552 'type' => 'text',
553 'name' => 'user',
554 'id' => 'mw-listfiles-user',
555 'label-message' => 'username',
556 'default' => $this->mUserName,
557 'size' => '40',
558 'maxlength' => '255',
559 'cssclass' => 'mw-autocomplete-user', // used by mediawiki.userSuggest
560 ];
561
562 $fields['ilshowall'] = [
563 'type' => 'check',
564 'name' => 'ilshowall',
565 'id' => 'mw-listfiles-show-all',
566 'label-message' => 'listfiles-show-all',
567 'default' => $this->mShowAll,
568 ];
569
570 $query = $this->getRequest()->getQueryValues();
571 unset( $query['title'] );
572 unset( $query['limit'] );
573 unset( $query['ilsearch'] );
574 unset( $query['ilshowall'] );
575 unset( $query['user'] );
576
577 $form = new HTMLForm( $fields, $this->getContext() );
578
579 $form->setMethod( 'get' );
580 $form->setTitle( $this->getTitle() );
581 $form->setId( 'mw-listfiles-form' );
582 $form->setWrapperLegendMsg( 'listfiles' );
583 $form->setSubmitTextMsg( 'table_pager_limit_submit' );
584 $form->addHiddenFields( $query );
585
586 $form->prepareForm();
587 $form->displayForm( '' );
588 }
589
590 protected function getTableClass() {
591 return parent::getTableClass() . ' listfiles';
592 }
593
594 protected function getNavClass() {
595 return parent::getNavClass() . ' listfiles_nav';
596 }
597
598 protected function getSortHeaderClass() {
599 return parent::getSortHeaderClass() . ' listfiles_sort';
600 }
601
602 function getPagingQueries() {
603 $queries = parent::getPagingQueries();
604 if ( !is_null( $this->mUserName ) ) {
605 # Append the username to the query string
606 foreach ( $queries as &$query ) {
607 if ( $query !== false ) {
608 $query['user'] = $this->mUserName;
609 }
610 }
611 }
612
613 return $queries;
614 }
615
616 function getDefaultQuery() {
617 $queries = parent::getDefaultQuery();
618 if ( !isset( $queries['user'] ) && !is_null( $this->mUserName ) ) {
619 $queries['user'] = $this->mUserName;
620 }
621
622 return $queries;
623 }
624
625 function getTitle() {
626 return SpecialPage::getTitleFor( 'Listfiles' );
627 }
628}
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
wfLocalFile( $title)
Get an object referring to a locally registered file.
wfTimestamp( $outputtype=TS_UNIX, $ts=0)
Get a timestamp string in one of various formats.
wfEscapeWikiText( $text)
Escapes the given text so that it may be output using addWikiText() without any linking,...
getContext()
if(defined( 'MW_SETUP_CALLBACK')) $fname
Customization point after all loading (constants, functions, classes, DefaultSettings,...
Definition Setup.php:112
msg( $key)
Get a Message object with context set Parameters are the same as wfMessage()
IContextSource $context
setContext(IContextSource $context)
Object handling generic submission, CSRF protection, layout and other logic for UI forms.
Definition HTMLForm.php:130
getDefaultSort()
The database field name used as a default sort order.
formatValue( $field, $value)
outputUserDoesNotExist( $userName)
Add a message to the output stating that the user doesn't exist.
buildQueryConds( $table)
Build the where clause of the query.
doBatchLookups()
Called from getBody(), before getStartBody() is called and after doQuery() was called.
combineResult( $res1, $res2, $limit, $ascending)
Combine results from 2 tables.
User null $mUser
The relevant user.
reallyDoQuery( $offset, $limit, $asc)
Override reallyDoQuery to mix together two queries.
getRelevantUser()
Get the user relevant to the ImageList.
getQueryInfo()
This function should be overridden to provide all parameters needed for the main paged query.
getDefaultQuery()
Get an array of query parameters that should be put into self-links.
getPagingQueries()
Get a URL query array for the prev, next, first and last links.
getQueryInfoReal( $table)
Actually get the query info.
__construct(IContextSource $context, $userName=null, $search='', $including=false, $showAll=false)
isFieldSortable( $field)
Return true if the named field should be sortable by the UI, false otherwise.
$mIndexField
The index to actually be used for ordering.
const DIR_ASCENDING
Constants for the $mDefaultDirection field.
buildQueryInfo( $offset, $limit, $descending)
Build variables to use by the database wrapper.
const DIR_DESCENDING
static formatComment( $comment, $title=null, $local=false, $wikiId=null)
This function is called by all recent changes variants, by the page history, and by the user contribu...
Definition Linker.php:1109
MediaWiki exception.
MediaWikiServices is the service locator for the application scope of MediaWiki.
static singleton()
Get a RepoGroup instance.
Definition RepoGroup.php:59
Table-based display with a user-selectable sort order.
static singleton()
Definition UserCache.php:34
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:53
static newFromName( $name, $validate='valid')
Static factory method for creation from username.
Definition User.php:591
static whoIs( $id)
Get the username corresponding to a given user ID.
Definition User.php:863
static isIP( $name)
Does the string match an anonymous IP address?
Definition User.php:943
Overloads the relevant methods of the real ResultsWrapper so it doesn't go anywhere near an actual da...
deferred txt A few of the database updates required by various functions here can be deferred until after the result page is displayed to the user For updating the view updating the linked to tables after a etc PHP does not yet have any way to tell the server to actually return and disconnect while still running these but it might have such a feature in the future We handle these by creating a deferred update object and putting those objects on a global list
Definition deferred.txt:11
this hook is for auditing only RecentChangesLinked and Watchlist RecentChangesLinked and Watchlist Do not use this to implement individual filters if they are compatible with the ChangesListFilter and ChangesListFilterGroup structure use sub classes of those in conjunction with the ChangesListSpecialPageStructuredFilters hook This hook can be used to implement filters that do not implement that or custom behavior that is not an individual filter e g Watchlist & $tables
Definition hooks.txt:1015
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped & $options
Definition hooks.txt:2001
either a unescaped string or a HtmlArmor object after in associative array form externallinks including delete and has completed for all link tables whether this was an auto creation default is conds Array Extra conditions for the No matching items in log is displayed if loglist is empty msgKey Array If you want a nice box with a set this to the key of the message First element is the message additional optional elements are parameters for the key that are processed with wfMessage() -> params() ->parseAsBlock() - offset Set to overwrite offset parameter in $wgRequest set to '' to unset offset - wrap String Wrap the message in html(usually something like "&lt;div ...>$1&lt;/div>"). - flags Integer display flags(NO_ACTION_LINK, NO_EXTRA_USER_LINKS) 'LogException':Called before an exception(or PHP error) is logged. This is meant for integration with external error aggregation services
usually copyright or history_copyright This message must be in HTML not wikitext & $link
Definition hooks.txt:3021
null for the local wiki Added should default to null in handler for backwards compatibility add a value to it if you want to add a cookie that have to vary cache options can modify $query
Definition hooks.txt:1620
null means default in associative array with keys and values unescaped Should be merged with default with a value of false meaning to suppress the attribute in associative array with keys and values unescaped noclasses just before the function returns a value If you return an< a > element with HTML attributes $attribs and contents $html will be returned If you return $ret will be returned and may include noclasses after processing after in associative array form before processing starts Return false to skip default processing and return $ret $linkRenderer
Definition hooks.txt:2056
please add to it if you re going to add events to the MediaWiki code where normally authentication against an external auth plugin would be creating a local account $user
Definition hooks.txt:247
const NS_FILE
Definition Defines.php:80
Interface for objects which can provide a MediaWiki context on request.
Result wrapper for grabbing data queried from an IDatabase object.
$queries
const DB_REPLICA
Definition defines.php:25