Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 253
0.00% covered (danger)
0.00%
0 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryAllImages
0.00% covered (danger)
0.00%
0 / 252
0.00% covered (danger)
0.00%
0 / 9
3306
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getDB
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 execute
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCacheMode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 executeGenerator
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 run
0.00% covered (danger)
0.00%
0 / 160
0.00% covered (danger)
0.00%
0 / 1
2256
 getAllowedParams
0.00% covered (danger)
0.00%
0 / 71
0.00% covered (danger)
0.00%
0 / 1
6
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
2
 getHelpUrls
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * API for MediaWiki 1.12+
5 *
6 * Copyright © 2008 Vasiliev Victor vasilvv@gmail.com,
7 * based on ApiQueryAllPages.php
8 *
9 * @license GPL-2.0-or-later
10 * @file
11 */
12
13namespace MediaWiki\Api;
14
15use MediaWiki\FileRepo\File\File;
16use MediaWiki\FileRepo\File\FileSelectQueryBuilder;
17use MediaWiki\FileRepo\LocalRepo;
18use MediaWiki\FileRepo\RepoGroup;
19use MediaWiki\MainConfigNames;
20use MediaWiki\ParamValidator\TypeDef\UserDef;
21use MediaWiki\Permissions\GroupPermissionsLookup;
22use MediaWiki\Title\Title;
23use Wikimedia\ParamValidator\ParamValidator;
24use Wikimedia\ParamValidator\TypeDef\IntegerDef;
25use Wikimedia\Rdbms\IExpression;
26use Wikimedia\Rdbms\IReadableDatabase;
27use Wikimedia\Rdbms\LikeValue;
28
29/**
30 * Query module to enumerate all images.
31 *
32 * @ingroup API
33 */
34class ApiQueryAllImages extends ApiQueryGeneratorBase {
35
36    /**
37     * @var LocalRepo
38     */
39    protected $mRepo;
40
41    public function __construct(
42        ApiQuery $query,
43        string $moduleName,
44        RepoGroup $repoGroup,
45        private readonly GroupPermissionsLookup $groupPermissionsLookup,
46    ) {
47        parent::__construct( $query, $moduleName, 'ai' );
48        $this->mRepo = $repoGroup->getLocalRepo();
49    }
50
51    /**
52     * Override parent method to make sure the repo's DB is used
53     * which may not necessarily be the same as the local DB.
54     *
55     * TODO: allow querying non-local repos.
56     * @return IReadableDatabase
57     */
58    protected function getDB() {
59        return $this->mRepo->getReplicaDB();
60    }
61
62    public function execute() {
63        $this->run();
64    }
65
66    /** @inheritDoc */
67    public function getCacheMode( $params ) {
68        return 'public';
69    }
70
71    /**
72     * @param ApiPageSet $resultPageSet
73     * @return void
74     */
75    public function executeGenerator( $resultPageSet ) {
76        if ( $resultPageSet->isResolvingRedirects() ) {
77            $this->dieWithError( 'apierror-allimages-redirect', 'invalidparammix' );
78        }
79
80        $this->run( $resultPageSet );
81    }
82
83    /**
84     * @param ApiPageSet|null $resultPageSet
85     * @return void
86     */
87    private function run( $resultPageSet = null ) {
88        $repo = $this->mRepo;
89        if ( !$repo instanceof LocalRepo ) {
90            $this->dieWithError( 'apierror-unsupportedrepo' );
91        }
92
93        $prefix = $this->getModulePrefix();
94
95        $db = $this->getDB();
96
97        $params = $this->extractRequestParams();
98
99        // Table and return fields
100        $prop = array_fill_keys( $params['prop'], true );
101
102        $migrationStage = $this->getConfig()->get( MainConfigNames::FileSchemaMigrationStage );
103        $fileOptions = [];
104        if ( ( $migrationStage & SCHEMA_COMPAT_READ_NEW ) && $params['sort'] == 'timestamp' ) {
105            // Force fr_timestamp index to optimize query for timestamp sorting
106            $fileOptions['force-index'] = 'fr_timestamp';
107        }
108
109        $fileQuery = FileSelectQueryBuilder::newForFile( $db, $fileOptions )->getQueryInfo();
110        $this->addTables( $fileQuery['tables'] );
111        $this->addFields( $fileQuery['fields'] );
112        $this->addJoinConds( $fileQuery['join_conds'] );
113
114        $secondarySortField = ( $migrationStage & SCHEMA_COMPAT_READ_NEW )
115            ? 'img_filerevision_id'
116            : 'img_name';
117
118        $ascendingOrder = true;
119        if ( $params['dir'] == 'descending' || $params['dir'] == 'older' ) {
120            $ascendingOrder = false;
121        }
122
123        if ( $params['sort'] == 'name' ) {
124            // Check mutually exclusive params
125            $disallowed = [ 'start', 'end', 'user' ];
126            foreach ( $disallowed as $pname ) {
127                if ( isset( $params[$pname] ) ) {
128                    $this->dieWithError(
129                        [
130                            'apierror-invalidparammix-mustusewith',
131                            "{$prefix}{$pname}",
132                            "{$prefix}sort=timestamp"
133                        ],
134                        'invalidparammix'
135                    );
136                }
137            }
138            if ( $params['filterbots'] != 'all' ) {
139                $this->dieWithError(
140                    [
141                        'apierror-invalidparammix-mustusewith',
142                        "{$prefix}filterbots",
143                        "{$prefix}sort=timestamp"
144                    ],
145                    'invalidparammix'
146                );
147            }
148
149            // Pagination
150            if ( $params['continue'] !== null ) {
151                $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'string' ] );
152                $op = $ascendingOrder ? '>=' : '<=';
153                $this->addWhere( $db->expr( 'img_name', $op, $cont[0] ) );
154            }
155
156            // Image filters
157            $from = $params['from'] === null ? null : $this->titlePartToKey( $params['from'], NS_FILE );
158            $to = $params['to'] === null ? null : $this->titlePartToKey( $params['to'], NS_FILE );
159            $this->addWhereRange( 'img_name', $ascendingOrder ? 'newer' : 'older', $from, $to );
160
161            if ( isset( $params['prefix'] ) ) {
162                $this->addWhere(
163                    $db->expr(
164                        'img_name',
165                        IExpression::LIKE,
166                        new LikeValue( $this->titlePartToKey( $params['prefix'], NS_FILE ), $db->anyString() )
167                    )
168                );
169            }
170        } else {
171            // Check mutually exclusive params
172            $disallowed = [ 'from', 'to', 'prefix' ];
173            foreach ( $disallowed as $pname ) {
174                if ( isset( $params[$pname] ) ) {
175                    $this->dieWithError(
176                        [
177                            'apierror-invalidparammix-mustusewith',
178                            "{$prefix}{$pname}",
179                            "{$prefix}sort=name"
180                        ],
181                        'invalidparammix'
182                    );
183                }
184            }
185            if ( $params['user'] !== null && $params['filterbots'] != 'all' ) {
186                // Since filterbots checks if each user has the bot right, it
187                // doesn't make sense to use it with user
188                $this->dieWithError(
189                    [ 'apierror-invalidparammix-cannotusewith', "{$prefix}user", "{$prefix}filterbots" ]
190                );
191            }
192
193            // Pagination
194            $this->addTimestampWhereRange(
195                'img_timestamp',
196                $ascendingOrder ? 'newer' : 'older',
197                $params['start'],
198                $params['end']
199            );
200            // Include in ORDER BY for uniqueness
201            $this->addWhereRange( $secondarySortField, $ascendingOrder ? 'newer' : 'older', null, null );
202
203            if ( $params['continue'] !== null ) {
204                $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'timestamp', 'string' ] );
205                $op = ( $ascendingOrder ? '>=' : '<=' );
206                $this->addWhere( $db->buildComparison( $op, [
207                    'img_timestamp' => $db->timestamp( $cont[0] ),
208                    $secondarySortField => $cont[1],
209                ] ) );
210            }
211
212            // Image filters
213            if ( $params['user'] !== null ) {
214                if ( isset( $fileQuery['fields']['img_user_text'] ) ) {
215                    $this->addWhereFld( $fileQuery['fields']['img_user_text'], $params['user'] );
216                } else {
217                    // file read new
218                    $this->addWhereFld( 'img_user_text', $params['user'] );
219                }
220
221            }
222            if ( $params['filterbots'] != 'all' ) {
223                $this->addTables( 'user_groups' );
224                $this->addJoinConds( [ 'user_groups' => [
225                    'LEFT JOIN',
226                    [
227                        'ug_group' => $this->groupPermissionsLookup->getGroupsWithPermission( 'bot' ),
228                        'ug_user = actor_user',
229                        $db->expr( 'ug_expiry', '=', null )->or( 'ug_expiry', '>=', $db->timestamp() )
230                    ]
231                ] ] );
232                $groupCond = $params['filterbots'] == 'nobots' ? 'NULL' : 'NOT NULL';
233                $this->addWhere( "ug_group IS $groupCond" );
234            }
235        }
236
237        // Filters not depending on sort
238        if ( isset( $params['minsize'] ) ) {
239            $this->addWhere( 'img_size>=' . (int)$params['minsize'] );
240        }
241
242        if ( isset( $params['maxsize'] ) ) {
243            $this->addWhere( 'img_size<=' . (int)$params['maxsize'] );
244        }
245
246        $sha1 = false;
247        if ( isset( $params['sha1'] ) ) {
248            $sha1 = strtolower( $params['sha1'] );
249            if ( !$this->validateSha1Hash( $sha1 ) ) {
250                $this->dieWithError( 'apierror-invalidsha1hash' );
251            }
252            $sha1 = \Wikimedia\base_convert( $sha1, 16, 36, 31 );
253        } elseif ( isset( $params['sha1base36'] ) ) {
254            $sha1 = strtolower( $params['sha1base36'] );
255            if ( !$this->validateSha1Base36Hash( $sha1 ) ) {
256                $this->dieWithError( 'apierror-invalidsha1base36hash' );
257            }
258        }
259        if ( $sha1 ) {
260            $this->addWhereFld( 'img_sha1', $sha1 );
261        }
262
263        if ( $params['mime'] !== null ) {
264            if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) {
265                $this->dieWithError( 'apierror-mimesearchdisabled' );
266            }
267
268            $mimeConds = [];
269            foreach ( $params['mime'] as $mime ) {
270                [ $major, $minor ] = File::splitMime( $mime );
271                $mimeConds[] =
272                    $db->expr( 'img_major_mime', '=', $major )
273                        ->and( 'img_minor_mime', '=', $minor );
274            }
275            if ( count( $mimeConds ) > 0 ) {
276                $this->addWhere( $db->orExpr( $mimeConds ) );
277            } else {
278                // no MIME types, no files
279                $this->getResult()->addValue( 'query', $this->getModuleName(), [] );
280                return;
281            }
282        }
283
284        $limit = $params['limit'];
285        $this->addOption( 'LIMIT', $limit + 1 );
286
287        $res = $this->select( __METHOD__ );
288
289        $titles = [];
290        $count = 0;
291        $result = $this->getResult();
292        foreach ( $res as $row ) {
293            if ( ++$count > $limit ) {
294                // We've reached the one extra which shows that there are
295                // additional pages to be had. Stop here...
296                if ( $params['sort'] == 'name' ) {
297                    $this->setContinueEnumParameter( 'continue', $row->img_name );
298                } else {
299                    $secondaryValue = $row->$secondarySortField;
300                    $this->setContinueEnumParameter( 'continue', "$row->img_timestamp|$secondaryValue" );
301                }
302                break;
303            }
304
305            if ( $resultPageSet === null ) {
306                $file = $repo->newFileFromRow( $row );
307                $info = ApiQueryImageInfo::getInfo( $file, $prop, $result ) +
308                    [ 'name' => $row->img_name ];
309                self::addTitleInfo( $info, $file->getTitle() );
310
311                $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $info );
312                if ( !$fit ) {
313                    if ( $params['sort'] == 'name' ) {
314                        $this->setContinueEnumParameter( 'continue', $row->img_name );
315                    } else {
316                        $secondaryValue = $row->$secondarySortField;
317                        $this->setContinueEnumParameter( 'continue', "$row->img_timestamp|$secondaryValue" );
318                    }
319                    break;
320                }
321            } else {
322                $titles[] = Title::makeTitle( NS_FILE, $row->img_name );
323            }
324        }
325
326        if ( $resultPageSet === null ) {
327            $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'img' );
328        } else {
329            $resultPageSet->populateFromTitles( $titles );
330        }
331    }
332
333    /** @inheritDoc */
334    public function getAllowedParams() {
335        $ret = [
336            'sort' => [
337                ParamValidator::PARAM_DEFAULT => 'name',
338                ParamValidator::PARAM_TYPE => [
339                    'name',
340                    'timestamp'
341                ]
342            ],
343            'dir' => [
344                ParamValidator::PARAM_DEFAULT => 'ascending',
345                ParamValidator::PARAM_TYPE => [
346                    // sort=name
347                    'ascending',
348                    'descending',
349                    // sort=timestamp
350                    'newer',
351                    'older'
352                ]
353            ],
354            'from' => null,
355            'to' => null,
356            'continue' => [
357                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
358            ],
359            'start' => [
360                ParamValidator::PARAM_TYPE => 'timestamp'
361            ],
362            'end' => [
363                ParamValidator::PARAM_TYPE => 'timestamp'
364            ],
365            'prop' => [
366                ParamValidator::PARAM_TYPE => ApiQueryImageInfo::getPropertyNames( self::PROPERTY_FILTER ),
367                ParamValidator::PARAM_DEFAULT => 'timestamp|url',
368                ParamValidator::PARAM_ISMULTI => true,
369                ApiBase::PARAM_HELP_MSG => 'apihelp-query+imageinfo-param-prop',
370                ApiBase::PARAM_HELP_MSG_PER_VALUE =>
371                    ApiQueryImageInfo::getPropertyMessages( self::PROPERTY_FILTER ),
372            ],
373            'prefix' => null,
374            'minsize' => [
375                ParamValidator::PARAM_TYPE => 'integer',
376            ],
377            'maxsize' => [
378                ParamValidator::PARAM_TYPE => 'integer',
379            ],
380            'sha1' => null,
381            'sha1base36' => null,
382            'user' => [
383                ParamValidator::PARAM_TYPE => 'user',
384                UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ],
385            ],
386            'filterbots' => [
387                ParamValidator::PARAM_DEFAULT => 'all',
388                ParamValidator::PARAM_TYPE => [
389                    'all',
390                    'bots',
391                    'nobots'
392                ]
393            ],
394            'mime' => [
395                ParamValidator::PARAM_ISMULTI => true,
396            ],
397            'limit' => [
398                ParamValidator::PARAM_DEFAULT => 10,
399                ParamValidator::PARAM_TYPE => 'limit',
400                IntegerDef::PARAM_MIN => 1,
401                IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
402                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
403            ],
404        ];
405
406        if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) {
407            $ret['mime'][ApiBase::PARAM_HELP_MSG] = 'api-help-param-disabled-in-miser-mode';
408        }
409
410        return $ret;
411    }
412
413    private const PROPERTY_FILTER = [ 'archivename', 'thumbmime', 'uploadwarning' ];
414
415    /** @inheritDoc */
416    protected function getExamplesMessages() {
417        return [
418            'action=query&list=allimages&aifrom=B'
419                => 'apihelp-query+allimages-example-b',
420            'action=query&list=allimages&aiprop=user|timestamp|url&' .
421                'aisort=timestamp&aidir=older'
422                => 'apihelp-query+allimages-example-recent',
423            'action=query&list=allimages&aimime=image/png|image/gif'
424                => 'apihelp-query+allimages-example-mimetypes',
425            'action=query&generator=allimages&gailimit=4&' .
426                'gaifrom=T&prop=imageinfo'
427                => 'apihelp-query+allimages-example-generator',
428        ];
429    }
430
431    /** @inheritDoc */
432    public function getHelpUrls() {
433        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Allimages';
434    }
435}
436
437/** @deprecated class alias since 1.43 */
438class_alias( ApiQueryAllImages::class, 'ApiQueryAllImages' );