MediaWiki REL1_37
ImageListPager.php
Go to the documentation of this file.
1<?php
27
32
33 protected $mFieldNames = null;
34
35 // Subclasses should override buildQueryConds instead of using $mQueryConds variable.
36 protected $mQueryConds = [];
37
38 protected $mUserName = null;
39
45 protected $mUser = null;
46
47 protected $mSearch = '';
48
49 protected $mIncluding = false;
50
51 protected $mShowAll = false;
52
53 protected $mTableName = 'image';
54
56 private $localRepo;
57
60
62 private $userCache;
63
67 private const INDEX_FIELDS = [
68 'img_timestamp' => [ 'img_timestamp', 'img_name' ],
69 'img_name' => [ 'img_name' ],
70 'img_size' => [ 'img_size', 'img_name' ],
71 ];
72
86 public function __construct(
88 $userName,
89 $search,
90 $including,
91 $showAll,
93 RepoGroup $repoGroup,
94 ILoadBalancer $loadBalancer,
97 UserNameUtils $userNameUtils
98 ) {
99 $this->setContext( $context );
100
101 $this->mIncluding = $including;
102 $this->mShowAll = $showAll;
103 $dbr = $loadBalancer->getConnectionRef( ILoadBalancer::DB_REPLICA );
104
105 if ( $userName !== null && $userName !== '' ) {
106 $nt = Title::makeTitleSafe( NS_USER, $userName );
107 if ( $nt === null ) {
108 $this->outputUserDoesNotExist( $userName );
109 } else {
110 $this->mUserName = $nt->getText();
111 $user = User::newFromName( $this->mUserName, false );
112 if ( $user ) {
113 $this->mUser = $user;
114 }
115 if ( !$user || ( $user->isAnon() && !$userNameUtils->isIP( $user->getName() ) ) ) {
116 $this->outputUserDoesNotExist( $userName );
117 }
118 }
119 }
120
121 if ( $search !== '' && !$this->getConfig()->get( 'MiserMode' ) ) {
122 $this->mSearch = $search;
123 $nt = Title::newFromText( $this->mSearch );
124
125 if ( $nt ) {
126 $this->mQueryConds[] = 'LOWER(img_name)' .
127 $dbr->buildLike( $dbr->anyString(),
128 strtolower( $nt->getDBkey() ), $dbr->anyString() );
129 }
130 }
131
132 if ( !$including ) {
133 if ( $this->getRequest()->getText( 'sort', 'img_date' ) == 'img_date' ) {
134 $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
135 } else {
136 $this->mDefaultDirection = IndexPager::DIR_ASCENDING;
137 }
138 } else {
139 $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
140 }
141 // Set database before parent constructor to avoid setting it there with wfGetDB
142 $this->mDb = $dbr;
143
144 parent::__construct( $context, $linkRenderer );
145 $this->localRepo = $repoGroup->getLocalRepo();
146 $this->commentStore = $commentStore;
147 $this->userCache = $userCache;
148 }
149
155 public function getRelevantUser() {
156 return $this->mUser;
157 }
158
164 protected function outputUserDoesNotExist( $userName ) {
165 $this->getOutput()->wrapWikiMsg(
166 "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
167 [
168 'listfiles-userdoesnotexist',
169 wfEscapeWikiText( $userName ),
170 ]
171 );
172 }
173
181 protected function buildQueryConds( $table ) {
182 $prefix = $table === 'image' ? 'img' : 'oi';
183 $conds = [];
184
185 if ( $this->mUserName !== null ) {
186 // getQueryInfoReal() should have handled the tables and joins.
187 $conds['actor_name'] = $this->mUserName;
188 }
189
190 if ( $this->mSearch !== '' ) {
191 $nt = Title::newFromText( $this->mSearch );
192 if ( $nt ) {
193 $dbr = $this->getDatabase();
194 $conds[] = 'LOWER(' . $prefix . '_name)' .
195 $dbr->buildLike( $dbr->anyString(),
196 strtolower( $nt->getDBkey() ), $dbr->anyString() );
197 }
198 }
199
200 if ( $table === 'oldimage' ) {
201 // Don't want to deal with revdel.
202 // Future fixme: Show partial information as appropriate.
203 // Would have to be careful about filtering by username when username is deleted.
204 $conds['oi_deleted'] = 0;
205 }
206
207 // Add mQueryConds in case anyone was subclassing and using the old variable.
208 return $conds + $this->mQueryConds;
209 }
210
211 protected function getFieldNames() {
212 if ( !$this->mFieldNames ) {
213 $this->mFieldNames = [
214 'img_timestamp' => $this->msg( 'listfiles_date' )->text(),
215 'img_name' => $this->msg( 'listfiles_name' )->text(),
216 'thumb' => $this->msg( 'listfiles_thumb' )->text(),
217 'img_size' => $this->msg( 'listfiles_size' )->text(),
218 ];
219 if ( $this->mUserName === null ) {
220 // Do not show username if filtering by username
221 $this->mFieldNames['img_actor'] = $this->msg( 'listfiles_user' )->text();
222 }
223 // img_description down here, in order so that its still after the username field.
224 $this->mFieldNames['img_description'] = $this->msg( 'listfiles_description' )->text();
225
226 if ( !$this->getConfig()->get( 'MiserMode' ) && !$this->mShowAll ) {
227 $this->mFieldNames['count'] = $this->msg( 'listfiles_count' )->text();
228 }
229 if ( $this->mShowAll ) {
230 $this->mFieldNames['top'] = $this->msg( 'listfiles-latestversion' )->text();
231 }
232 }
233
234 return $this->mFieldNames;
235 }
236
237 protected function isFieldSortable( $field ) {
238 if ( $this->mIncluding ) {
239 return false;
240 }
241 $sortable = array_keys( self::INDEX_FIELDS );
242 /* For reference, the indicies we can use for sorting are:
243 * On the image table: img_actor_timestamp, img_size, img_timestamp
244 * On oldimage: oi_actor_timestamp, oi_name_timestamp
245 *
246 * In particular that means we cannot sort by timestamp when not filtering
247 * by user and including old images in the results. Which is sad. (T279982)
248 */
249 if ( $this->getConfig()->get( 'MiserMode' ) && $this->mUserName !== null ) {
250 // If we're sorting by user, the index only supports sorting by time.
251 return $field === 'img_timestamp';
252 } elseif ( $this->getConfig()->get( 'MiserMode' )
253 && $this->mShowAll /* && mUserName === null */
254 ) {
255 // no oi_timestamp index, so only alphabetical sorting in this case.
256 return $field === 'img_name';
257 }
258
259 return in_array( $field, $sortable );
260 }
261
262 public function getQueryInfo() {
263 // Hacky Hacky Hacky - I want to get query info
264 // for two different tables, without reimplementing
265 // the pager class.
266 $qi = $this->getQueryInfoReal( $this->mTableName );
267
268 return $qi;
269 }
270
281 protected function getQueryInfoReal( $table ) {
282 $dbr = $this->getDatabase();
283 $prefix = $table === 'oldimage' ? 'oi' : 'img';
284
285 $tables = [ $table, 'actor' ];
286 $join_conds = [];
287
288 if ( $table === 'oldimage' ) {
289 $fields = [
290 'img_timestamp' => 'oi_timestamp',
291 'img_name' => 'oi_name',
292 'img_size' => 'oi_size',
293 'top' => $dbr->addQuotes( 'no' )
294 ];
295 $join_conds['actor'] = [ 'JOIN', 'actor_id=oi_actor' ];
296 } else {
297 $fields = [
298 'img_timestamp',
299 'img_name',
300 'img_size',
301 'top' => $dbr->addQuotes( 'yes' )
302 ];
303 $join_conds['actor'] = [ 'JOIN', 'actor_id=img_actor' ];
304 }
305
306 $options = [];
307
308 # Description field
309 $commentQuery = $this->commentStore->getJoin( $prefix . '_description' );
310 $tables += $commentQuery['tables'];
311 $fields += $commentQuery['fields'];
312 $join_conds += $commentQuery['joins'];
313 $fields['description_field'] = $dbr->addQuotes( "{$prefix}_description" );
314
315 # Actor fields
316 $fields[] = 'actor_user';
317 $fields[] = 'actor_name';
318
319 # Depends on $wgMiserMode
320 # Will also not happen if mShowAll is true.
321 if ( array_key_exists( 'count', $this->getFieldNames() ) ) {
322 $fields['count'] = $dbr->buildSelectSubquery(
323 'oldimage',
324 'COUNT(oi_archive_name)',
325 'oi_name = img_name',
326 __METHOD__
327 );
328 }
329
330 return [
331 'tables' => $tables,
332 'fields' => $fields,
333 'conds' => $this->buildQueryConds( $table ),
334 'options' => $options,
335 'join_conds' => $join_conds
336 ];
337 }
338
348 public function reallyDoQuery( $offset, $limit, $order ) {
349 $dbr = $this->getDatabase();
350 $prevTableName = $this->mTableName;
351 $this->mTableName = 'image';
352 list( $tables, $fields, $conds, $fname, $options, $join_conds ) =
353 $this->buildQueryInfo( $offset, $limit, $order );
354 $imageRes = $dbr->select( $tables, $fields, $conds, $fname, $options, $join_conds );
355 $this->mTableName = $prevTableName;
356
357 if ( !$this->mShowAll ) {
358 return $imageRes;
359 }
360
361 $this->mTableName = 'oldimage';
362
363 # Hacky...
364 $oldIndex = $this->mIndexField;
365 foreach ( $this->mIndexField as &$index ) {
366 if ( substr( $index, 0, 4 ) !== 'img_' ) {
367 throw new MWException( "Expected to be sorting on an image table field" );
368 }
369 $index = 'oi_' . substr( $index, 4 );
370 }
371
372 list( $tables, $fields, $conds, $fname, $options, $join_conds ) =
373 $this->buildQueryInfo( $offset, $limit, $order );
374 $oldimageRes = $dbr->select( $tables, $fields, $conds, $fname, $options, $join_conds );
375
376 $this->mTableName = $prevTableName;
377 $this->mIndexField = $oldIndex;
378
379 return $this->combineResult( $imageRes, $oldimageRes, $limit, $order );
380 }
381
393 protected function combineResult( $res1, $res2, $limit, $order ) {
394 $res1->rewind();
395 $res2->rewind();
396 $topRes1 = $res1->next();
397 $topRes2 = $res2->next();
398 $resultArray = [];
399 for ( $i = 0; $i < $limit && $topRes1 && $topRes2; $i++ ) {
400 if ( strcmp( $topRes1->{$this->mIndexField[0]}, $topRes2->{$this->mIndexField[0]} ) > 0 ) {
401 if ( $order !== IndexPager::QUERY_ASCENDING ) {
402 $resultArray[] = $topRes1;
403 $topRes1 = $res1->next();
404 } else {
405 $resultArray[] = $topRes2;
406 $topRes2 = $res2->next();
407 }
408 } elseif ( $order !== IndexPager::QUERY_ASCENDING ) {
409 $resultArray[] = $topRes2;
410 $topRes2 = $res2->next();
411 } else {
412 $resultArray[] = $topRes1;
413 $topRes1 = $res1->next();
414 }
415 }
416
417 for ( ; $i < $limit && $topRes1; $i++ ) {
418 $resultArray[] = $topRes1;
419 $topRes1 = $res1->next();
420 }
421
422 for ( ; $i < $limit && $topRes2; $i++ ) {
423 $resultArray[] = $topRes2;
424 $topRes2 = $res2->next();
425 }
426
427 return new FakeResultWrapper( $resultArray );
428 }
429
430 public function getIndexField() {
431 return [ self::INDEX_FIELDS[$this->mSort] ];
432 }
433
434 public function getDefaultSort() {
435 if ( $this->mShowAll && $this->getConfig()->get( 'MiserMode' ) && $this->mUserName === null ) {
436 // Unfortunately no index on oi_timestamp.
437 return 'img_name';
438 } else {
439 return 'img_timestamp';
440 }
441 }
442
443 protected function doBatchLookups() {
444 $userIds = [];
445 $this->mResult->seek( 0 );
446 foreach ( $this->mResult as $row ) {
447 if ( $row->actor_user ) {
448 $userIds[] = $row->actor_user;
449 }
450 }
451 # Do a link batch query for names and userpages
452 $this->userCache->doQuery( $userIds, [ 'userpage' ], __METHOD__ );
453 }
454
469 public function formatValue( $field, $value ) {
470 $linkRenderer = $this->getLinkRenderer();
471 switch ( $field ) {
472 case 'thumb':
473 $opt = [ 'time' => wfTimestamp( TS_MW, $this->mCurrentRow->img_timestamp ) ];
474 $file = $this->localRepo->findFile( $this->getCurrentRow()->img_name, $opt );
475 // If statement for paranoia
476 if ( $file ) {
477 $thumb = $file->transform( [ 'width' => 180, 'height' => 360 ] );
478 if ( $thumb ) {
479 return $thumb->toHtml( [ 'desc-link' => true ] );
480 } else {
481 return $this->msg( 'thumbnail_error', '' )->escaped();
482 }
483 } else {
484 return htmlspecialchars( $this->getCurrentRow()->img_name );
485 }
486 case 'img_timestamp':
487 // We may want to make this a link to the "old" version when displaying old files
488 return htmlspecialchars( $this->getLanguage()->userTimeAndDate( $value, $this->getUser() ) );
489 case 'img_name':
490 static $imgfile = null;
491 if ( $imgfile === null ) {
492 $imgfile = $this->msg( 'imgfile' )->text();
493 }
494
495 // Weird files can maybe exist? T24227
496 $filePage = Title::makeTitleSafe( NS_FILE, $value );
497 if ( $filePage ) {
498 $link = $linkRenderer->makeKnownLink(
499 $filePage,
500 $filePage->getText()
501 );
502 $download = Xml::element(
503 'a',
504 [ 'href' => $this->localRepo->newFile( $filePage )->getUrl() ],
505 $imgfile
506 );
507 $download = $this->msg( 'parentheses' )->rawParams( $download )->escaped();
508
509 // Add delete links if allowed
510 // From https://github.com/Wikia/app/pull/3859
511 if ( $this->getAuthority()->probablyCan( 'delete', $filePage ) ) {
512 $deleteMsg = $this->msg( 'listfiles-delete' )->text();
513
514 $delete = $linkRenderer->makeKnownLink(
515 $filePage, $deleteMsg, [], [ 'action' => 'delete' ]
516 );
517 $delete = $this->msg( 'parentheses' )->rawParams( $delete )->escaped();
518
519 return "$link $download $delete";
520 }
521
522 return "$link $download";
523 } else {
524 return htmlspecialchars( $value );
525 }
526 case 'img_actor':
527 if ( $this->mCurrentRow->actor_user ) {
528 $name = $this->mCurrentRow->actor_name;
529 $link = $linkRenderer->makeLink(
530 Title::makeTitle( NS_USER, $name ),
531 $name
532 );
533 } else {
534 $link = $value !== null ? htmlspecialchars( $value ) : '';
535 }
536
537 return $link;
538 case 'img_size':
539 return htmlspecialchars( $this->getLanguage()->formatSize( $value ) );
540 case 'img_description':
541 $field = $this->mCurrentRow->description_field;
542 $value = $this->commentStore->getComment( $field, $this->mCurrentRow )->text;
543 return Linker::formatComment( $value );
544 case 'count':
545 return $this->getLanguage()->formatNum( intval( $value ) + 1 );
546 case 'top':
547 // Messages: listfiles-latestversion-yes, listfiles-latestversion-no
548 return $this->msg( 'listfiles-latestversion-' . $value )->escaped();
549 default:
550 throw new MWException( "Unknown field '$field'" );
551 }
552 }
553
554 public function getForm() {
555 $formDescriptor = [];
556 $formDescriptor['limit'] = [
557 'type' => 'select',
558 'name' => 'limit',
559 'label-message' => 'table_pager_limit_label',
560 'options' => $this->getLimitSelectList(),
561 'default' => $this->mLimit,
562 ];
563
564 if ( !$this->getConfig()->get( 'MiserMode' ) ) {
565 $formDescriptor['ilsearch'] = [
566 'type' => 'text',
567 'name' => 'ilsearch',
568 'id' => 'mw-ilsearch',
569 'label-message' => 'listfiles_search_for',
570 'default' => $this->mSearch,
571 'size' => '40',
572 'maxlength' => '255',
573 ];
574 }
575
576 $formDescriptor['user'] = [
577 'type' => 'user',
578 'name' => 'user',
579 'id' => 'mw-listfiles-user',
580 'label-message' => 'username',
581 'default' => $this->mUserName,
582 'size' => '40',
583 'maxlength' => '255',
584 ];
585
586 $formDescriptor['ilshowall'] = [
587 'type' => 'check',
588 'name' => 'ilshowall',
589 'id' => 'mw-listfiles-show-all',
590 'label-message' => 'listfiles-show-all',
591 'default' => $this->mShowAll,
592 ];
593
594 $query = $this->getRequest()->getQueryValues();
595 unset( $query['title'] );
596 unset( $query['limit'] );
597 unset( $query['ilsearch'] );
598 unset( $query['ilshowall'] );
599 unset( $query['user'] );
600
601 HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
602 ->setMethod( 'get' )
603 ->setId( 'mw-listfiles-form' )
604 ->setTitle( $this->getTitle() )
605 ->setSubmitTextMsg( 'table_pager_limit_submit' )
606 ->setWrapperLegendMsg( 'listfiles' )
607 ->addHiddenFields( $query )
608 ->prepareForm()
609 ->displayForm( '' );
610 }
611
612 protected function getTableClass() {
613 return parent::getTableClass() . ' listfiles';
614 }
615
616 protected function getNavClass() {
617 return parent::getNavClass() . ' listfiles_nav';
618 }
619
620 protected function getSortHeaderClass() {
621 return parent::getSortHeaderClass() . ' listfiles_sort';
622 }
623
624 public function getPagingQueries() {
625 $queries = parent::getPagingQueries();
626 if ( $this->mUserName !== null ) {
627 # Append the username to the query string
628 foreach ( $queries as &$query ) {
629 if ( $query !== false ) {
630 $query['user'] = $this->mUserName;
631 }
632 }
633 }
634
635 return $queries;
636 }
637
638 public function getDefaultQuery() {
639 $queries = parent::getDefaultQuery();
640 if ( !isset( $queries['user'] ) && $this->mUserName !== null ) {
641 $queries['user'] = $this->mUserName;
642 }
643
644 return $queries;
645 }
646
647 public function getTitle() {
648 return SpecialPage::getTitleFor( 'Listfiles' );
649 }
650}
getAuthority()
const NS_USER
Definition Defines.php:66
const NS_FILE
Definition Defines.php:70
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()
Handle database storage of comments such as edit summaries and log reasons.
msg( $key,... $params)
Get a Message object with context set Parameters are the same as wfMessage()
IContextSource $context
setContext(IContextSource $context)
getDefaultSort()
The database field name used as a default sort order.
getTableClass()
TablePager relies on mw-datatable for styling, see T214208.
formatValue( $field, $value)
outputUserDoesNotExist( $userName)
Add a message to the output stating that the user doesn't exist.
getFieldNames()
An array mapping database field names to a textual description of the field name, for use in the tabl...
combineResult( $res1, $res2, $limit, $order)
Combine results from 2 tables.
buildQueryConds( $table)
Build the where clause of the query.
doBatchLookups()
Called from getBody(), before getStartBody() is called and after doQuery() was called.
User null $mUser
The relevant user.
LocalRepo $localRepo
CommentStore $commentStore
getIndexField()
Returns the name of the index field.If the pager supports multiple orders, it may return an array of ...
getRelevantUser()
Get the user relevant to the ImageList.
__construct(IContextSource $context, $userName, $search, $including, $showAll, LinkRenderer $linkRenderer, RepoGroup $repoGroup, ILoadBalancer $loadBalancer, CommentStore $commentStore, UserCache $userCache, UserNameUtils $userNameUtils)
getQueryInfo()
Provides all parameters needed for the main paged query.
reallyDoQuery( $offset, $limit, $order)
Override reallyDoQuery to mix together two queries.
const INDEX_FIELDS
The unique sort fields for the sort options for unique pagniate.
UserCache $userCache
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.
isFieldSortable( $field)
Return true if the named field should be sortable by the UI, false otherwise.
const DIR_ASCENDING
Backwards-compatible constant for $mDefaultDirection field (do not change)
buildQueryInfo( $offset, $limit, $order)
Build variables to use by the database wrapper.
string string[] $mIndexField
The index to actually be used for ordering.
getDatabase()
Get the Database object in use.
LinkRenderer $linkRenderer
const DIR_DESCENDING
Backwards-compatible constant for $mDefaultDirection field (do not change)
const QUERY_ASCENDING
Backwards-compatible constant for reallyDoQuery() (do not change)
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:1372
A repository that stores files in the local filesystem and registers them in the wiki's own database.
Definition LocalRepo.php:41
MediaWiki exception.
Class that generates HTML links for pages.
UserNameUtils service.
isIP(string $name)
Does the string match an anonymous IP address?
Prioritized list of file repositories.
Definition RepoGroup.php:33
getLocalRepo()
Get the local repository, i.e.
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
Table-based display with a user-selectable sort order.
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:69
static newFromName( $name, $validate='valid')
Definition User.php:607
Overloads the relevant methods of the real ResultWrapper so it doesn't go anywhere near an actual dat...
Interface for objects which can provide a MediaWiki context on request.
Database cluster connection, tracking, load balancing, and transaction manager interface.
getConnectionRef( $i, $groups=[], $domain=false, $flags=0)
Get a live database handle reference for a server index.
Result wrapper for grabbing data queried from an IDatabase object.
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42