MediaWiki fundraising/REL1_35
ImageListPager.php
Go to the documentation of this file.
1<?php
29
31
32 protected $mFieldNames = null;
33
34 // Subclasses should override buildQueryConds instead of using $mQueryConds variable.
35 protected $mQueryConds = [];
36
37 protected $mUserName = null;
38
44 protected $mUser = null;
45
46 protected $mSearch = '';
47
48 protected $mIncluding = false;
49
50 protected $mShowAll = false;
51
52 protected $mTableName = 'image';
53
57 private const INDEX_FIELDS = [
58 'img_timestamp' => [ 'img_timestamp', 'img_name' ],
59 'img_name' => [ 'img_name' ],
60 'img_size' => [ 'img_size', 'img_name' ],
61 ];
62
63 public function __construct( IContextSource $context, $userName, $search,
64 $including, $showAll, LinkRenderer $linkRenderer
65 ) {
66 $this->setContext( $context );
67
68 $this->mIncluding = $including;
69 $this->mShowAll = $showAll;
70
71 if ( $userName !== null && $userName !== '' ) {
72 $nt = Title::makeTitleSafe( NS_USER, $userName );
73 if ( $nt === null ) {
74 $this->outputUserDoesNotExist( $userName );
75 } else {
76 $this->mUserName = $nt->getText();
77 $user = User::newFromName( $this->mUserName, false );
78 if ( $user ) {
79 $this->mUser = $user;
80 }
81 if ( !$user || ( $user->isAnon() && !User::isIP( $user->getName() ) ) ) {
82 $this->outputUserDoesNotExist( $userName );
83 }
84 }
85 }
86
87 if ( $search !== '' && !$this->getConfig()->get( 'MiserMode' ) ) {
88 $this->mSearch = $search;
89 $nt = Title::newFromText( $this->mSearch );
90
91 if ( $nt ) {
93 $this->mQueryConds[] = 'LOWER(img_name)' .
94 $dbr->buildLike( $dbr->anyString(),
95 strtolower( $nt->getDBkey() ), $dbr->anyString() );
96 }
97 }
98
99 if ( !$including ) {
100 if ( $this->getRequest()->getText( 'sort', 'img_date' ) == 'img_date' ) {
101 $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
102 } else {
103 $this->mDefaultDirection = IndexPager::DIR_ASCENDING;
104 }
105 } else {
106 $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
107 }
108
109 parent::__construct( $context, $linkRenderer );
110 }
111
117 public function getRelevantUser() {
118 return $this->mUser;
119 }
120
126 protected function outputUserDoesNotExist( $userName ) {
127 $this->getOutput()->wrapWikiMsg(
128 "<div class=\"mw-userpage-userdoesnotexist error\">\n$1\n</div>",
129 [
130 'listfiles-userdoesnotexist',
131 wfEscapeWikiText( $userName ),
132 ]
133 );
134 }
135
143 protected function buildQueryConds( $table ) {
144 $prefix = $table === 'image' ? 'img' : 'oi';
145 $conds = [];
146
147 if ( $this->mUserName !== null ) {
148 // getQueryInfoReal() should have handled the tables and joins.
150 $actorWhere = ActorMigration::newMigration()->getWhere(
151 $dbr,
152 $prefix . '_user',
153 User::newFromName( $this->mUserName, false ),
154 // oldimage doesn't have an index on oi_user, while image does. Set $useId accordingly.
155 $prefix === 'img'
156 );
157 $conds[] = $actorWhere['conds'];
158 }
159
160 if ( $this->mSearch !== '' ) {
161 $nt = Title::newFromText( $this->mSearch );
162 if ( $nt ) {
164 $conds[] = 'LOWER(' . $prefix . '_name)' .
165 $dbr->buildLike( $dbr->anyString(),
166 strtolower( $nt->getDBkey() ), $dbr->anyString() );
167 }
168 }
169
170 if ( $table === 'oldimage' ) {
171 // Don't want to deal with revdel.
172 // Future fixme: Show partial information as appropriate.
173 // Would have to be careful about filtering by username when username is deleted.
174 $conds['oi_deleted'] = 0;
175 }
176
177 // Add mQueryConds in case anyone was subclassing and using the old variable.
178 return $conds + $this->mQueryConds;
179 }
180
187 protected function getFieldNames() {
188 if ( !$this->mFieldNames ) {
189 $this->mFieldNames = [
190 'img_timestamp' => $this->msg( 'listfiles_date' )->text(),
191 'img_name' => $this->msg( 'listfiles_name' )->text(),
192 'thumb' => $this->msg( 'listfiles_thumb' )->text(),
193 'img_size' => $this->msg( 'listfiles_size' )->text(),
194 ];
195 if ( $this->mUserName === null ) {
196 // Do not show username if filtering by username
197 $this->mFieldNames['img_user_text'] = $this->msg( 'listfiles_user' )->text();
198 }
199 // img_description down here, in order so that its still after the username field.
200 $this->mFieldNames['img_description'] = $this->msg( 'listfiles_description' )->text();
201
202 if ( !$this->getConfig()->get( 'MiserMode' ) && !$this->mShowAll ) {
203 $this->mFieldNames['count'] = $this->msg( 'listfiles_count' )->text();
204 }
205 if ( $this->mShowAll ) {
206 $this->mFieldNames['top'] = $this->msg( 'listfiles-latestversion' )->text();
207 }
208 }
209
210 return $this->mFieldNames;
211 }
212
213 protected function isFieldSortable( $field ) {
214 if ( $this->mIncluding ) {
215 return false;
216 }
217 $sortable = array_keys( self::INDEX_FIELDS );
218 /* For reference, the indicies we can use for sorting are:
219 * On the image table: img_user_timestamp/img_usertext_timestamp/img_actor_timestamp,
220 * img_size, img_timestamp
221 * On oldimage: oi_usertext_timestamp/oi_actor_timestamp, oi_name_timestamp
222 *
223 * In particular that means we cannot sort by timestamp when not filtering
224 * by user and including old images in the results. Which is sad.
225 */
226 if ( $this->getConfig()->get( 'MiserMode' ) && $this->mUserName !== null ) {
227 // If we're sorting by user, the index only supports sorting by time.
228 if ( $field === 'img_timestamp' ) {
229 return true;
230 } else {
231 return false;
232 }
233 } elseif ( $this->getConfig()->get( 'MiserMode' )
234 && $this->mShowAll /* && mUserName === null */
235 ) {
236 // no oi_timestamp index, so only alphabetical sorting in this case.
237 if ( $field === 'img_name' ) {
238 return true;
239 } else {
240 return false;
241 }
242 }
243
244 return in_array( $field, $sortable );
245 }
246
247 public function getQueryInfo() {
248 // Hacky Hacky Hacky - I want to get query info
249 // for two different tables, without reimplementing
250 // the pager class.
251 $qi = $this->getQueryInfoReal( $this->mTableName );
252
253 return $qi;
254 }
255
266 protected function getQueryInfoReal( $table ) {
268 $prefix = $table === 'oldimage' ? 'oi' : 'img';
269
270 $tables = [ $table ];
271 $fields = array_keys( $this->getFieldNames() );
272 $fields = array_combine( $fields, $fields );
273 unset( $fields['img_description'] );
274 unset( $fields['img_user_text'] );
275
276 if ( $table === 'oldimage' ) {
277 foreach ( $fields as $id => $field ) {
278 if ( substr( $id, 0, 4 ) === 'img_' ) {
279 $fields[$id] = $prefix . substr( $field, 3 );
280 }
281 }
282 $fields['top'] = $dbr->addQuotes( 'no' );
283 } elseif ( $this->mShowAll ) {
284 $fields['top'] = $dbr->addQuotes( 'yes' );
285 }
286 $fields['thumb'] = $prefix . '_name';
287
288 $options = $join_conds = [];
289
290 # Description field
291 $commentQuery = CommentStore::getStore()->getJoin( $prefix . '_description' );
292 $tables += $commentQuery['tables'];
293 $fields += $commentQuery['fields'];
294 $join_conds += $commentQuery['joins'];
295 $fields['description_field'] = $dbr->addQuotes( "{$prefix}_description" );
296
297 # User fields
298 $actorQuery = ActorMigration::newMigration()->getJoin( $prefix . '_user' );
299 $tables += $actorQuery['tables'];
300 $join_conds += $actorQuery['joins'];
301 $fields['img_user'] = $actorQuery['fields'][$prefix . '_user'];
302 $fields['img_user_text'] = $actorQuery['fields'][$prefix . '_user_text'];
303 $fields['img_actor'] = $actorQuery['fields'][$prefix . '_actor'];
304
305 # Depends on $wgMiserMode
306 # Will also not happen if mShowAll is true.
307 if ( isset( $fields['count'] ) ) {
308 $fields['count'] = $dbr->buildSelectSubquery(
309 'oldimage',
310 'COUNT(oi_archive_name)',
311 'oi_name = img_name',
312 __METHOD__
313 );
314 }
315
316 return [
317 'tables' => $tables,
318 'fields' => $fields,
319 'conds' => $this->buildQueryConds( $table ),
320 'options' => $options,
321 'join_conds' => $join_conds
322 ];
323 }
324
337 public function reallyDoQuery( $offset, $limit, $order ) {
338 $prevTableName = $this->mTableName;
339 $this->mTableName = 'image';
340 list( $tables, $fields, $conds, $fname, $options, $join_conds ) =
341 $this->buildQueryInfo( $offset, $limit, $order );
342 $imageRes = $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds );
343 $this->mTableName = $prevTableName;
344
345 if ( !$this->mShowAll ) {
346 return $imageRes;
347 }
348
349 $this->mTableName = 'oldimage';
350
351 # Hacky...
352 $oldIndex = $this->mIndexField;
353 foreach ( $this->mIndexField as &$index ) {
354 if ( substr( $index, 0, 4 ) !== 'img_' ) {
355 throw new MWException( "Expected to be sorting on an image table field" );
356 }
357 $index = 'oi_' . substr( $index, 4 );
358 }
359
360 list( $tables, $fields, $conds, $fname, $options, $join_conds ) =
361 $this->buildQueryInfo( $offset, $limit, $order );
362 $oldimageRes = $this->mDb->select( $tables, $fields, $conds, $fname, $options, $join_conds );
363
364 $this->mTableName = $prevTableName;
365 $this->mIndexField = $oldIndex;
366
367 return $this->combineResult( $imageRes, $oldimageRes, $limit, $order );
368 }
369
381 protected function combineResult( $res1, $res2, $limit, $ascending ) {
382 $res1->rewind();
383 $res2->rewind();
384 $topRes1 = $res1->next();
385 $topRes2 = $res2->next();
386 $resultArray = [];
387 for ( $i = 0; $i < $limit && $topRes1 && $topRes2; $i++ ) {
388 if ( strcmp( $topRes1->{$this->mIndexField[0]}, $topRes2->{$this->mIndexField[0]} ) > 0 ) {
389 if ( !$ascending ) {
390 $resultArray[] = $topRes1;
391 $topRes1 = $res1->next();
392 } else {
393 $resultArray[] = $topRes2;
394 $topRes2 = $res2->next();
395 }
396 } elseif ( !$ascending ) {
397 $resultArray[] = $topRes2;
398 $topRes2 = $res2->next();
399 } else {
400 $resultArray[] = $topRes1;
401 $topRes1 = $res1->next();
402 }
403 }
404
405 for ( ; $i < $limit && $topRes1; $i++ ) {
406 $resultArray[] = $topRes1;
407 $topRes1 = $res1->next();
408 }
409
410 for ( ; $i < $limit && $topRes2; $i++ ) {
411 $resultArray[] = $topRes2;
412 $topRes2 = $res2->next();
413 }
414
415 return new FakeResultWrapper( $resultArray );
416 }
417
418 public function getIndexField() {
419 return [ self::INDEX_FIELDS[$this->mSort] ];
420 }
421
422 public function getDefaultSort() {
423 if ( $this->mShowAll && $this->getConfig()->get( 'MiserMode' ) && $this->mUserName === null ) {
424 // Unfortunately no index on oi_timestamp.
425 return 'img_name';
426 } else {
427 return 'img_timestamp';
428 }
429 }
430
431 protected function doBatchLookups() {
432 $userIds = [];
433 $this->mResult->seek( 0 );
434 foreach ( $this->mResult as $row ) {
435 $userIds[] = $row->img_user;
436 }
437 # Do a link batch query for names and userpages
438 UserCache::singleton()->doQuery( $userIds, [ 'userpage' ], __METHOD__ );
439 }
440
455 public function formatValue( $field, $value ) {
456 $services = MediaWikiServices::getInstance();
457 $linkRenderer = $this->getLinkRenderer();
458 switch ( $field ) {
459 case 'thumb':
460 $opt = [ 'time' => wfTimestamp( TS_MW, $this->mCurrentRow->img_timestamp ) ];
461 $file = $services->getRepoGroup()->getLocalRepo()->findFile( $value, $opt );
462 // If statement for paranoia
463 if ( $file ) {
464 $thumb = $file->transform( [ 'width' => 180, 'height' => 360 ] );
465 if ( $thumb ) {
466 return $thumb->toHtml( [ 'desc-link' => true ] );
467 } else {
468 return $this->msg( 'thumbnail_error', '' )->escaped();
469 }
470 } else {
471 return htmlspecialchars( $value );
472 }
473 case 'img_timestamp':
474 // We may want to make this a link to the "old" version when displaying old files
475 return htmlspecialchars( $this->getLanguage()->userTimeAndDate( $value, $this->getUser() ) );
476 case 'img_name':
477 static $imgfile = null;
478 if ( $imgfile === null ) {
479 $imgfile = $this->msg( 'imgfile' )->text();
480 }
481
482 // Weird files can maybe exist? T24227
483 $filePage = Title::makeTitleSafe( NS_FILE, $value );
484 if ( $filePage ) {
485 $link = $linkRenderer->makeKnownLink(
486 $filePage,
487 $filePage->getText()
488 );
489 $download = Xml::element(
490 'a',
491 [ 'href' => $services->getRepoGroup()->getLocalRepo()->newFile( $filePage )->getUrl() ],
492 $imgfile
493 );
494 $download = $this->msg( 'parentheses' )->rawParams( $download )->escaped();
495
496 // Add delete links if allowed
497 // From https://github.com/Wikia/app/pull/3859
498 $permissionManager = $services->getPermissionManager();
499
500 if ( $permissionManager->userCan( 'delete', $this->getUser(), $filePage ) ) {
501 $deleteMsg = $this->msg( 'listfiles-delete' )->text();
502
503 $delete = $linkRenderer->makeKnownLink(
504 $filePage, $deleteMsg, [], [ 'action' => 'delete' ]
505 );
506 $delete = $this->msg( 'parentheses' )->rawParams( $delete )->escaped();
507
508 return "$link $download $delete";
509 }
510
511 return "$link $download";
512 } else {
513 return htmlspecialchars( $value );
514 }
515 case 'img_user_text':
516 if ( $this->mCurrentRow->img_user ) {
517 $name = User::whoIs( $this->mCurrentRow->img_user );
518 $link = $linkRenderer->makeLink(
519 Title::makeTitle( NS_USER, $name ),
520 $name
521 );
522 } else {
523 $link = $value !== null ? htmlspecialchars( $value ) : '';
524 }
525
526 return $link;
527 case 'img_size':
528 return htmlspecialchars( $this->getLanguage()->formatSize( $value ) );
529 case 'img_description':
530 $field = $this->mCurrentRow->description_field;
531 $value = CommentStore::getStore()->getComment( $field, $this->mCurrentRow )->text;
532 return Linker::formatComment( $value );
533 case 'count':
534 return $this->getLanguage()->formatNum( intval( $value ) + 1 );
535 case 'top':
536 // Messages: listfiles-latestversion-yes, listfiles-latestversion-no
537 return $this->msg( 'listfiles-latestversion-' . $value )->escaped();
538 default:
539 throw new MWException( "Unknown field '$field'" );
540 }
541 }
542
543 public function getForm() {
544 $formDescriptor = [];
545 $formDescriptor['limit'] = [
546 'type' => 'select',
547 'name' => 'limit',
548 'label-message' => 'table_pager_limit_label',
549 'options' => $this->getLimitSelectList(),
550 'default' => $this->mLimit,
551 ];
552
553 if ( !$this->getConfig()->get( 'MiserMode' ) ) {
554 $formDescriptor['ilsearch'] = [
555 'type' => 'text',
556 'name' => 'ilsearch',
557 'id' => 'mw-ilsearch',
558 'label-message' => 'listfiles_search_for',
559 'default' => $this->mSearch,
560 'size' => '40',
561 'maxlength' => '255',
562 ];
563 }
564
565 $formDescriptor['user'] = [
566 'type' => 'user',
567 'name' => 'user',
568 'id' => 'mw-listfiles-user',
569 'label-message' => 'username',
570 'default' => $this->mUserName,
571 'size' => '40',
572 'maxlength' => '255',
573 ];
574
575 $formDescriptor['ilshowall'] = [
576 'type' => 'check',
577 'name' => 'ilshowall',
578 'id' => 'mw-listfiles-show-all',
579 'label-message' => 'listfiles-show-all',
580 'default' => $this->mShowAll,
581 ];
582
583 $query = $this->getRequest()->getQueryValues();
584 unset( $query['title'] );
585 unset( $query['limit'] );
586 unset( $query['ilsearch'] );
587 unset( $query['ilshowall'] );
588 unset( $query['user'] );
589
590 $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() );
591 $htmlForm
592 ->setMethod( 'get' )
593 ->setId( 'mw-listfiles-form' )
594 ->setTitle( $this->getTitle() )
595 ->setSubmitTextMsg( 'table_pager_limit_submit' )
596 ->setWrapperLegendMsg( 'listfiles' )
597 ->addHiddenFields( $query )
598 ->prepareForm()
599 ->displayForm( '' );
600 }
601
602 protected function getTableClass() {
603 return parent::getTableClass() . ' listfiles';
604 }
605
606 protected function getNavClass() {
607 return parent::getNavClass() . ' listfiles_nav';
608 }
609
610 protected function getSortHeaderClass() {
611 return parent::getSortHeaderClass() . ' listfiles_sort';
612 }
613
614 public function getPagingQueries() {
615 $queries = parent::getPagingQueries();
616 if ( $this->mUserName !== null ) {
617 # Append the username to the query string
618 foreach ( $queries as &$query ) {
619 if ( $query !== false ) {
620 $query['user'] = $this->mUserName;
621 }
622 }
623 }
624
625 return $queries;
626 }
627
628 public function getDefaultQuery() {
629 $queries = parent::getDefaultQuery();
630 if ( !isset( $queries['user'] ) && $this->mUserName !== null ) {
631 $queries['user'] = $this->mUserName;
632 }
633
634 return $queries;
635 }
636
637 public function getTitle() {
638 return SpecialPage::getTitleFor( 'Listfiles' );
639 }
640}
getUser()
wfGetDB( $db, $groups=[], $wiki=false)
Get a Database object.
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()
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()
The array keys (but not the array values) are used in sql.
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.
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.
getQueryInfo()
Provides all parameters needed for the main paged query.
reallyDoQuery( $offset, $limit, $order)
Override reallyDoQuery to mix together two queries.
getNavClass()
Stable to override.
getSortHeaderClass()
Stable to override.
const INDEX_FIELDS
The unique sort fields for the sort options for unique pagniate.
getDefaultQuery()
Get an array of query parameters that should be put into self-links.
__construct(IContextSource $context, $userName, $search, $including, $showAll, LinkRenderer $linkRenderer)
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.
LinkRenderer $linkRenderer
const DIR_DESCENDING
Backwards-compatible constant for $mDefaultDirection field (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:1199
MediaWiki exception.
Class that generates HTML links for pages.
MediaWikiServices is the service locator for the application scope of MediaWiki.
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 Stable to extend.
static singleton()
Definition UserCache.php:34
The User object encapsulates all of the user-specific settings (user_id, name, rights,...
Definition User.php:60
Overloads the relevant methods of the real ResultsWrapper so it doesn't go anywhere near an actual da...
const NS_USER
Definition Defines.php:72
const NS_FILE
Definition Defines.php:76
Interface for objects which can provide a MediaWiki context on request.
Result wrapper for grabbing data queried from an IDatabase object.
const DB_REPLICA
Definition defines.php:25
if(PHP_SAPI !='cli-server') if(!isset( $_SERVER['SCRIPT_FILENAME'])) $file
Item class for a filearchive table row.
Definition router.php:42