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