Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
3.90% covered (danger)
3.90%
14 / 359
0.00% covered (danger)
0.00%
0 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
ImageListPager
3.92% covered (danger)
3.92%
14 / 357
0.00% covered (danger)
0.00%
0 / 24
7930.70
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
110
 getRelevantUser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 outputUserDoesNotExist
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 buildQueryCondsOld
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 buildQueryConds
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 getFieldNames
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
30
 isFieldSortable
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 getQueryInfo
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
12
 getQueryInfoReal
0.00% covered (danger)
0.00%
0 / 41
0.00% covered (danger)
0.00%
0 / 1
20
 reallyDoQuery
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 reallyDoQueryOld
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
42
 combineResult
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
132
 getIndexField
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDefaultSort
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 doBatchLookups
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
30
 formatValue
25.93% covered (danger)
25.93%
14 / 54
0.00% covered (danger)
0.00%
0 / 1
120.05
 getEscapedLimitSelectList
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getForm
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
2
 getTableClass
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getNavClass
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSortHeaderClass
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPagingQueries
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 getDefaultQuery
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 getTitle
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 * @ingroup Pager
6 */
7
8namespace MediaWiki\Specials\Pager;
9
10use MediaWiki\Cache\LinkBatchFactory;
11use MediaWiki\CommentFormatter\RowCommentFormatter;
12use MediaWiki\CommentStore\CommentStore;
13use MediaWiki\Context\IContextSource;
14use MediaWiki\FileRepo\LocalRepo;
15use MediaWiki\FileRepo\RepoGroup;
16use MediaWiki\Html\Html;
17use MediaWiki\HTMLForm\HTMLForm;
18use MediaWiki\Linker\Linker;
19use MediaWiki\Linker\LinkRenderer;
20use MediaWiki\MainConfigNames;
21use MediaWiki\Pager\IndexPager;
22use MediaWiki\Pager\TablePager;
23use MediaWiki\SpecialPage\SpecialPage;
24use MediaWiki\Title\Title;
25use MediaWiki\User\User;
26use MediaWiki\User\UserIdentityValue;
27use MediaWiki\User\UserNameUtils;
28use UnexpectedValueException;
29use Wikimedia\Rdbms\FakeResultWrapper;
30use Wikimedia\Rdbms\IConnectionProvider;
31use Wikimedia\Rdbms\IResultWrapper;
32use Wikimedia\Rdbms\Subquery;
33use Wikimedia\Timestamp\TimestampFormat as TS;
34
35/**
36 * @ingroup Pager
37 */
38class ImageListPager extends TablePager {
39
40    /** @var string[]|null */
41    protected ?array $mFieldNames = null;
42    /**
43     * @deprecated Subclasses should override {@see buildQueryConds} instead
44     * @var array
45     */
46    protected $mQueryConds = [];
47    protected ?string $mUserName = null;
48    /** The relevant user */
49    protected ?User $mUser = null;
50    protected ?bool $mIncluding = false;
51    protected bool $mShowAll = false;
52    protected string $mTableName = 'image';
53
54    private CommentStore $commentStore;
55    private LocalRepo $localRepo;
56    private RowCommentFormatter $rowCommentFormatter;
57    private LinkBatchFactory $linkBatchFactory;
58
59    /** @var string[] */
60    private array $formattedComments = [];
61
62    private int $migrationStage;
63
64    /**
65     * The unique sort fields for the sort options for unique paginate
66     */
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
73    public function __construct(
74        IContextSource $context,
75        CommentStore $commentStore,
76        LinkRenderer $linkRenderer,
77        IConnectionProvider $dbProvider,
78        RepoGroup $repoGroup,
79        UserNameUtils $userNameUtils,
80        RowCommentFormatter $rowCommentFormatter,
81        LinkBatchFactory $linkBatchFactory,
82        ?string $userName,
83        string $search,
84        ?bool $including,
85        bool $showAll
86    ) {
87        $this->setContext( $context );
88
89        $this->mIncluding = $including;
90        $this->mShowAll = $showAll;
91
92        if ( $userName !== null && $userName !== '' ) {
93            $nt = Title::makeTitleSafe( NS_USER, $userName );
94            if ( $nt === null ) {
95                $this->outputUserDoesNotExist( $userName );
96            } else {
97                $this->mUserName = $nt->getText();
98                $user = User::newFromName( $this->mUserName, false );
99                if ( $user ) {
100                    $this->mUser = $user;
101                }
102                if ( !$user || ( $user->isAnon() && !$userNameUtils->isIP( $user->getName() ) ) ) {
103                    $this->outputUserDoesNotExist( $userName );
104                }
105            }
106        }
107
108        if ( $including ||
109            $this->getRequest()->getText( 'sort', 'img_date' ) === 'img_date'
110        ) {
111            $this->mDefaultDirection = IndexPager::DIR_DESCENDING;
112        } else {
113            $this->mDefaultDirection = IndexPager::DIR_ASCENDING;
114        }
115        // Set database before parent constructor to avoid setting it there
116        $this->mDb = $dbProvider->getReplicaDatabase();
117
118        parent::__construct( $context, $linkRenderer );
119        $this->commentStore = $commentStore;
120        $this->localRepo = $repoGroup->getLocalRepo();
121        $this->rowCommentFormatter = $rowCommentFormatter;
122        $this->linkBatchFactory = $linkBatchFactory;
123        $this->migrationStage = $this->getConfig()->get(
124            MainConfigNames::FileSchemaMigrationStage
125        );
126    }
127
128    /**
129     * Get the user relevant to the ImageList
130     *
131     * @return User|null
132     */
133    public function getRelevantUser() {
134        return $this->mUser;
135    }
136
137    /**
138     * Add a message to the output stating that the user doesn't exist
139     *
140     * @param string $userName Unescaped user name
141     */
142    protected function outputUserDoesNotExist( $userName ) {
143        $out = $this->getOutput();
144        $out->addModuleStyles( 'mediawiki.codex.messagebox.styles' );
145        $out->addHTML(
146            Html::warningBox(
147                $out->msg(
148                    'listfiles-userdoesnotexist', wfEscapeWikiText( $userName )
149                )->parse(),
150                'mw-userpage-userdoesnotexist'
151            )
152        );
153    }
154
155    /**
156     * Build the where clause of the query.
157     *
158     * Replaces the older mQueryConds member variable.
159     * @param string $table Either "image" or "oldimage"
160     * @return array The query conditions.
161     */
162    protected function buildQueryCondsOld( $table ) {
163        $conds = [];
164
165        if ( $this->mUserName !== null ) {
166            // getQueryInfoReal() should have handled the tables and joins.
167            $conds['actor_name'] = $this->mUserName;
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
181    private function buildQueryConds(): array {
182        $conds = [
183            'file_deleted' => 0,
184            'fr_deleted' => 0,
185        ];
186
187        if ( $this->mUserName !== null ) {
188            // getQueryInfoReal() should have handled the tables and joins.
189            $conds['actor_name'] = $this->mUserName;
190        }
191
192        if ( !$this->mShowAll ) {
193            $conds[] = 'file_latest = fr_id';
194        }
195        return $conds;
196    }
197
198    /** @inheritDoc */
199    protected function getFieldNames() {
200        if ( !$this->mFieldNames ) {
201            $this->mFieldNames = [
202                'img_timestamp' => $this->msg( 'listfiles_date' )->text(),
203                'img_name' => $this->msg( 'listfiles_name' )->text(),
204                'thumb' => $this->msg( 'listfiles_thumb' )->text(),
205                'img_size' => $this->msg( 'listfiles_size' )->text(),
206            ];
207            if ( $this->mUserName === null ) {
208                // Do not show username if filtering by username
209                $this->mFieldNames['img_actor'] = $this->msg( 'listfiles_user' )->text();
210            }
211            // img_description down here, in order so that its still after the username field.
212            $this->mFieldNames['img_description'] = $this->msg( 'listfiles_description' )->text();
213
214            if ( $this->mShowAll ) {
215                $this->mFieldNames['top'] = $this->msg( 'listfiles-latestversion' )->text();
216            } elseif ( !$this->getConfig()->get( MainConfigNames::MiserMode ) ) {
217                $this->mFieldNames['count'] = $this->msg( 'listfiles_count' )->text();
218            }
219        }
220
221        return $this->mFieldNames;
222    }
223
224    /** @inheritDoc */
225    protected function isFieldSortable( $field ) {
226        if ( $this->mIncluding ) {
227            return false;
228        }
229        /* For reference, the indices we can use for sorting are:
230         * On the image table: img_actor_timestamp, img_size, img_timestamp
231         * On oldimage: oi_actor_timestamp, oi_name_timestamp
232         *
233         * In particular that means we cannot sort by timestamp when not filtering
234         * by user and including old images in the results. Which is sad. (T279982)
235         */
236        if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) {
237            if ( $this->mUserName !== null ) {
238                // If we're sorting by user, the index only supports sorting by time.
239                return $field === 'img_timestamp';
240            } elseif ( $this->mShowAll ) {
241                // no oi_timestamp index, so only alphabetical sorting in this case.
242                return $field === 'img_name';
243            }
244        }
245
246        return isset( self::INDEX_FIELDS[$field] );
247    }
248
249    /** @inheritDoc */
250    public function getQueryInfo() {
251        if ( $this->migrationStage & SCHEMA_COMPAT_READ_OLD ) {
252            // Hacky Hacky Hacky - I want to get query info
253            // for two different tables, without reimplementing
254            // the pager class.
255            return $this->getQueryInfoReal( $this->mTableName );
256        }
257        $dbr = $this->getDatabase();
258        $tables = [ 'filerevision', 'file', 'actor' ];
259        $fields = [
260            'img_timestamp' => 'fr_timestamp',
261            'img_name' => 'file_name',
262            'img_size' => 'fr_size',
263            'top' => 'CASE WHEN file_latest = fr_id THEN \'yes\' ELSE \'no\' END',
264        ];
265        $join_conds = [
266            'filerevision' => [ 'JOIN', 'fr_file=file_id' ],
267            'actor' => [ 'JOIN', 'actor_id=fr_actor' ]
268        ];
269
270        # Description field
271        $commentQuery = $this->commentStore->getJoin( 'fr_description' );
272        $tables += $commentQuery['tables'];
273        $fields += $commentQuery['fields'];
274        $join_conds += $commentQuery['joins'];
275        $fields['description_field'] = $dbr->addQuotes( "fr_description" );
276
277        # Actor fields
278        $fields[] = 'actor_user';
279        $fields[] = 'actor_name';
280
281        # Depends on $wgMiserMode
282        # Will also not happen if mShowAll is true.
283        if ( array_key_exists( 'count', $this->getFieldNames() ) ) {
284            $fields['count'] = new Subquery( $dbr->newSelectQueryBuilder()
285                ->select( 'COUNT(fr_archive_name)' )
286                ->from( 'filerevision' )
287                ->where( 'fr_file = file_id' )
288                ->caller( __METHOD__ )
289                ->getSQL()
290            );
291        }
292
293        return [
294            'tables' => $tables,
295            'fields' => $fields,
296            'conds' => $this->buildQueryConds(),
297            'options' => [],
298            'join_conds' => $join_conds
299        ];
300    }
301
302    /**
303     * Actually get the query info.
304     *
305     * This is to allow displaying both stuff from image and oldimage table.
306     *
307     * This is a bit hacky.
308     *
309     * @param string $table Either 'image' or 'oldimage'
310     * @return array Query info
311     */
312    protected function getQueryInfoReal( $table ) {
313        $dbr = $this->getDatabase();
314        $prefix = $table === 'oldimage' ? 'oi' : 'img';
315
316        $tables = [ $table, 'actor' ];
317        $join_conds = [];
318
319        if ( $table === 'oldimage' ) {
320            $fields = [
321                'img_timestamp' => 'oi_timestamp',
322                'img_name' => 'oi_name',
323                'img_size' => 'oi_size',
324                'top' => $dbr->addQuotes( 'no' )
325            ];
326            $join_conds['actor'] = [ 'JOIN', 'actor_id=oi_actor' ];
327        } else {
328            $fields = [
329                'img_timestamp',
330                'img_name',
331                'img_size',
332                'top' => $dbr->addQuotes( 'yes' )
333            ];
334            $join_conds['actor'] = [ 'JOIN', 'actor_id=img_actor' ];
335        }
336
337        # Description field
338        $commentQuery = $this->commentStore->getJoin( $prefix . '_description' );
339        $tables += $commentQuery['tables'];
340        $fields += $commentQuery['fields'];
341        $join_conds += $commentQuery['joins'];
342        $fields['description_field'] = $dbr->addQuotes( "{$prefix}_description" );
343
344        # Actor fields
345        $fields[] = 'actor_user';
346        $fields[] = 'actor_name';
347
348        # Depends on $wgMiserMode
349        # Will also not happen if mShowAll is true.
350        if ( array_key_exists( 'count', $this->getFieldNames() ) ) {
351            $fields['count'] = new Subquery( $dbr->newSelectQueryBuilder()
352                ->select( 'COUNT(oi_archive_name)' )
353                ->from( 'oldimage' )
354                ->where( 'oi_name = img_name' )
355                ->caller( __METHOD__ )
356                ->getSQL()
357            );
358        }
359
360        return [
361            'tables' => $tables,
362            'fields' => $fields,
363            'conds' => $this->buildQueryCondsOld( $table ),
364            'options' => [],
365            'join_conds' => $join_conds
366        ];
367    }
368
369    /** @inheritDoc */
370    public function reallyDoQuery( $offset, $limit, $order ) {
371        if ( $this->migrationStage & SCHEMA_COMPAT_READ_OLD ) {
372            return $this->reallyDoQueryOld( $offset, $limit, $order );
373        } else {
374            return parent::reallyDoQuery( $offset, $limit, $order );
375        }
376    }
377
378    /**
379     * Override reallyDoQuery to mix together two queries.
380     *
381     * @param string $offset
382     * @param int $limit
383     * @param bool $order IndexPager::QUERY_ASCENDING or IndexPager::QUERY_DESCENDING
384     * @return IResultWrapper
385     */
386    public function reallyDoQueryOld( $offset, $limit, $order ) {
387        $dbr = $this->getDatabase();
388        $prevTableName = $this->mTableName;
389        $this->mTableName = 'image';
390        [ $tables, $fields, $conds, $fname, $options, $join_conds ] =
391            $this->buildQueryInfo( $offset, $limit, $order );
392        $imageRes = $dbr->newSelectQueryBuilder()
393            ->tables( is_array( $tables ) ? $tables : [ $tables ] )
394            ->fields( $fields )
395            ->conds( $conds )
396            ->caller( $fname )
397            ->options( $options )
398            ->joinConds( $join_conds )
399            ->fetchResultSet();
400        $this->mTableName = $prevTableName;
401
402        if ( !$this->mShowAll ) {
403            return $imageRes;
404        }
405
406        $this->mTableName = 'oldimage';
407
408        # Hacky...
409        $oldIndex = $this->mIndexField;
410        foreach ( $this->mIndexField as &$index ) {
411            if ( !str_starts_with( $index, 'img_' ) ) {
412                throw new UnexpectedValueException( "Expected to be sorting on an image table field" );
413            }
414            $index = 'oi_' . substr( $index, 4 );
415        }
416        unset( $index );
417
418        [ $tables, $fields, $conds, $fname, $options, $join_conds ] =
419            $this->buildQueryInfo( $offset, $limit, $order );
420        $oldimageRes = $dbr->newSelectQueryBuilder()
421            ->tables( is_array( $tables ) ? $tables : [ $tables ] )
422            ->fields( $fields )
423            ->conds( $conds )
424            ->caller( $fname )
425            ->options( $options )
426            ->joinConds( $join_conds )
427            ->fetchResultSet();
428
429        $this->mTableName = $prevTableName;
430        $this->mIndexField = $oldIndex;
431
432        return $this->combineResult( $imageRes, $oldimageRes, $limit, $order );
433    }
434
435    /**
436     * Combine results from 2 tables.
437     *
438     * Note: This will throw away some results
439     *
440     * @param IResultWrapper $res1
441     * @param IResultWrapper $res2
442     * @param int $limit
443     * @param bool $order IndexPager::QUERY_ASCENDING or IndexPager::QUERY_DESCENDING
444     * @return IResultWrapper $res1 and $res2 combined
445     */
446    protected function combineResult( $res1, $res2, $limit, $order ) {
447        $res1->rewind();
448        $res2->rewind();
449        $topRes1 = $res1->fetchObject();
450        $topRes2 = $res2->fetchObject();
451        $resultArray = [];
452        for ( $i = 0; $i < $limit && $topRes1 && $topRes2; $i++ ) {
453            if ( strcmp( $topRes1->{$this->mIndexField[0]}, $topRes2->{$this->mIndexField[0]} ) > 0 ) {
454                if ( $order !== IndexPager::QUERY_ASCENDING ) {
455                    $resultArray[] = $topRes1;
456                    $topRes1 = $res1->fetchObject();
457                } else {
458                    $resultArray[] = $topRes2;
459                    $topRes2 = $res2->fetchObject();
460                }
461            } elseif ( $order !== IndexPager::QUERY_ASCENDING ) {
462                $resultArray[] = $topRes2;
463                $topRes2 = $res2->fetchObject();
464            } else {
465                $resultArray[] = $topRes1;
466                $topRes1 = $res1->fetchObject();
467            }
468        }
469
470        for ( ; $i < $limit && $topRes1; $i++ ) {
471            $resultArray[] = $topRes1;
472            $topRes1 = $res1->fetchObject();
473        }
474
475        for ( ; $i < $limit && $topRes2; $i++ ) {
476            $resultArray[] = $topRes2;
477            $topRes2 = $res2->fetchObject();
478        }
479
480        return new FakeResultWrapper( $resultArray );
481    }
482
483    /** @inheritDoc */
484    public function getIndexField() {
485        return [ self::INDEX_FIELDS[$this->mSort] ];
486    }
487
488    /** @inheritDoc */
489    public function getDefaultSort() {
490        if ( $this->mShowAll &&
491            $this->getConfig()->get( MainConfigNames::MiserMode ) &&
492            $this->mUserName === null
493        ) {
494            // Unfortunately no index on oi_timestamp.
495            return 'img_name';
496        } else {
497            return 'img_timestamp';
498        }
499    }
500
501    protected function doBatchLookups() {
502        $this->mResult->seek( 0 );
503        $batch = $this->linkBatchFactory->newLinkBatch()->setCaller( __METHOD__ );
504        $rowsWithComments = [ 'img_description' => [], 'oi_description' => [], 'fr_description' => [] ];
505        foreach ( $this->mResult as $i => $row ) {
506            $batch->addUser( new UserIdentityValue( $row->actor_user ?? 0, $row->actor_name ) );
507            $batch->add( NS_FILE, $row->img_name );
508            $rowsWithComments[$row->description_field][$i] = $row;
509        }
510        $batch->execute();
511
512        // Format the comments
513        if ( $rowsWithComments['img_description'] ) {
514            $this->formattedComments += $this->rowCommentFormatter->formatRows(
515                $rowsWithComments['img_description'],
516                'img_description'
517            );
518        }
519        if ( $rowsWithComments['oi_description'] ) {
520            $this->formattedComments += $this->rowCommentFormatter->formatRows(
521                $rowsWithComments['oi_description'],
522                'oi_description'
523            );
524        }
525        if ( $rowsWithComments['fr_description'] ) {
526            $this->formattedComments += $this->rowCommentFormatter->formatRows(
527                $rowsWithComments['fr_description'],
528                'fr_description'
529            );
530        }
531    }
532
533    /**
534     * @param string $field
535     * @param string|null $value
536     * @return string
537     */
538    public function formatValue( $field, $value ) {
539        $linkRenderer = $this->getLinkRenderer();
540        switch ( $field ) {
541            case 'thumb':
542                $opt = [ 'time' => wfTimestamp( TS::MW, $this->mCurrentRow->img_timestamp ) ];
543                $file = $this->localRepo->findFile( $this->getCurrentRow()->img_name, $opt );
544                // If statement for paranoia
545                if ( $file ) {
546                    $thumb = $file->transform( [ 'width' => 180, 'height' => 360 ] );
547                    if ( $thumb ) {
548                        return $thumb->toHtml( [ 'desc-link' => true, 'loading' => 'lazy' ] );
549                    }
550                    return $this->msg( 'thumbnail_error', '' )->escaped();
551                } else {
552                    return htmlspecialchars( $this->getCurrentRow()->img_name );
553                }
554            case 'img_timestamp':
555                // We may want to make this a link to the "old" version when displaying old files
556                return htmlspecialchars( $this->getLanguage()->userTimeAndDate( $value, $this->getUser() ) );
557            case 'img_name':
558                static $imgfile = null;
559                $imgfile ??= $this->msg( 'imgfile' )->text();
560
561                // Weird files can maybe exist? T24227
562                $filePage = Title::makeTitleSafe( NS_FILE, $value );
563                if ( $filePage ) {
564                    $html = $linkRenderer->makeKnownLink(
565                        $filePage,
566                        $filePage->getText()
567                    );
568                    $opt = [ 'time' => wfTimestamp( TS::MW, $this->mCurrentRow->img_timestamp ) ];
569                    $file = $this->localRepo->findFile( $value, $opt );
570                    if ( $file ) {
571                        $download = Html::element(
572                            'a',
573                            [ 'href' => $file->getUrl() ],
574                            $imgfile
575                        );
576                        $html .= ' ' . $this->msg( 'parentheses' )->rawParams( $download )->escaped();
577                    }
578
579                    // Add delete links if allowed
580                    // From https://github.com/Wikia/app/pull/3859
581                    if ( $this->getAuthority()->probablyCan( 'delete', $filePage ) ) {
582                        $deleteMsg = $this->msg( 'listfiles-delete' )->text();
583
584                        $delete = $linkRenderer->makeKnownLink(
585                            $filePage, $deleteMsg, [], [ 'action' => 'delete' ]
586                        );
587                        $html .= ' ' . $this->msg( 'parentheses' )->rawParams( $delete )->escaped();
588                    }
589
590                    return $html;
591                } else {
592                    return htmlspecialchars( $value );
593                }
594            case 'img_actor':
595                $userId = (int)$this->mCurrentRow->actor_user;
596                $userName = $this->mCurrentRow->actor_name;
597                return Linker::userLink( $userId, $userName )
598                    . Linker::userToolLinks( $userId, $userName );
599            case 'img_size':
600                return htmlspecialchars( $this->getLanguage()->formatSize( (int)$value ) );
601            case 'img_description':
602                return $this->formattedComments[$this->getResultOffset()];
603            case 'count':
604                if ( $this->migrationStage & SCHEMA_COMPAT_READ_OLD ) {
605                    return htmlspecialchars( $this->getLanguage()->formatNum( intval( $value ) + 1 ) );
606                } else {
607                    return htmlspecialchars( $this->getLanguage()->formatNum( intval( $value ) ) );
608                }
609            case 'top':
610                // Messages: listfiles-latestversion-yes, listfiles-latestversion-no
611                return $this->msg( 'listfiles-latestversion-' . $value )->escaped();
612            default:
613                throw new UnexpectedValueException( "Unknown field '$field'" );
614        }
615    }
616
617    /**
618     * Escape the options list
619     */
620    private function getEscapedLimitSelectList(): array {
621        $list = $this->getLimitSelectList();
622        $result = [];
623        foreach ( $list as $key => $value ) {
624            $result[htmlspecialchars( $key )] = $value;
625        }
626        return $result;
627    }
628
629    public function getForm() {
630        $formDescriptor = [];
631        $formDescriptor['limit'] = [
632            'type' => 'radio',
633            'name' => 'limit',
634            'label-message' => 'table_pager_limit_label',
635            'options' => $this->getEscapedLimitSelectList(),
636            'flatlist' => true,
637            'default' => $this->mLimit
638        ];
639
640        $formDescriptor['user'] = [
641            'type' => 'user',
642            'name' => 'user',
643            'id' => 'mw-listfiles-user',
644            'label-message' => 'username',
645            'default' => $this->mUserName,
646            'size' => '40',
647            'maxlength' => '255',
648        ];
649
650        $formDescriptor['ilshowall'] = [
651            'type' => 'check',
652            'name' => 'ilshowall',
653            'id' => 'mw-listfiles-show-all',
654            'label-message' => 'listfiles-show-all',
655            'default' => $this->mShowAll,
656        ];
657
658        $query = $this->getRequest()->getQueryValues();
659        unset( $query['title'] );
660        unset( $query['limit'] );
661        unset( $query['ilsearch'] );
662        unset( $query['ilshowall'] );
663        unset( $query['user'] );
664
665        HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() )
666            ->setMethod( 'get' )
667            ->setId( 'mw-listfiles-form' )
668            ->setTitle( $this->getTitle() )
669            ->setSubmitTextMsg( 'listfiles-pager-submit' )
670            ->setWrapperLegendMsg( 'listfiles' )
671            ->addHiddenFields( $query )
672            ->prepareForm()
673            ->displayForm( '' );
674    }
675
676    /** @inheritDoc */
677    protected function getTableClass() {
678        return parent::getTableClass() . ' listfiles';
679    }
680
681    /** @inheritDoc */
682    protected function getNavClass() {
683        return parent::getNavClass() . ' listfiles_nav';
684    }
685
686    /** @inheritDoc */
687    protected function getSortHeaderClass() {
688        return parent::getSortHeaderClass() . ' listfiles_sort';
689    }
690
691    /** @inheritDoc */
692    public function getPagingQueries() {
693        $queries = parent::getPagingQueries();
694        if ( $this->mUserName !== null ) {
695            # Append the username to the query string
696            foreach ( $queries as &$query ) {
697                if ( $query !== false ) {
698                    $query['user'] = $this->mUserName;
699                }
700            }
701        }
702
703        return $queries;
704    }
705
706    /** @inheritDoc */
707    public function getDefaultQuery() {
708        $queries = parent::getDefaultQuery();
709        if ( !isset( $queries['user'] ) && $this->mUserName !== null ) {
710            $queries['user'] = $this->mUserName;
711        }
712
713        return $queries;
714    }
715
716    public function getTitle(): Title {
717        return SpecialPage::getTitleFor( 'Listfiles' );
718    }
719}
720
721/**
722 * Retain the old class name for backwards compatibility.
723 * @deprecated since 1.41
724 */
725class_alias( ImageListPager::class, 'ImageListPager' );
726
727/** @deprecated class alias since 1.46 */
728class_alias( ImageListPager::class, 'MediaWiki\\Pager\\ImageListPager' );