Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 242
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 / 242
0.00% covered (danger)
0.00%
0 / 9
2862
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 / 149
0.00% covered (danger)
0.00%
0 / 1
1892
 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 * This program is free software; you can redistribute it and/or modify
10 * it under the terms of the GNU General Public License as published by
11 * the Free Software Foundation; either version 2 of the License, or
12 * (at your option) any later version.
13 *
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU General Public License for more details.
18 *
19 * You should have received a copy of the GNU General Public License along
20 * with this program; if not, write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
22 * http://www.gnu.org/copyleft/gpl.html
23 *
24 * @file
25 */
26
27use MediaWiki\MainConfigNames;
28use MediaWiki\ParamValidator\TypeDef\UserDef;
29use MediaWiki\Permissions\GroupPermissionsLookup;
30use MediaWiki\Title\Title;
31use Wikimedia\ParamValidator\ParamValidator;
32use Wikimedia\ParamValidator\TypeDef\IntegerDef;
33use Wikimedia\Rdbms\IExpression;
34use Wikimedia\Rdbms\IReadableDatabase;
35use Wikimedia\Rdbms\LikeValue;
36use Wikimedia\Rdbms\OrExpressionGroup;
37
38/**
39 * Query module to enumerate all images.
40 *
41 * @ingroup API
42 */
43class ApiQueryAllImages extends ApiQueryGeneratorBase {
44
45    /**
46     * @var LocalRepo
47     */
48    protected $mRepo;
49
50    private GroupPermissionsLookup $groupPermissionsLookup;
51
52    /**
53     * @param ApiQuery $query
54     * @param string $moduleName
55     * @param RepoGroup $repoGroup
56     * @param GroupPermissionsLookup $groupPermissionsLookup
57     */
58    public function __construct(
59        ApiQuery $query,
60        $moduleName,
61        RepoGroup $repoGroup,
62        GroupPermissionsLookup $groupPermissionsLookup
63    ) {
64        parent::__construct( $query, $moduleName, 'ai' );
65        $this->mRepo = $repoGroup->getLocalRepo();
66        $this->groupPermissionsLookup = $groupPermissionsLookup;
67    }
68
69    /**
70     * Override parent method to make sure the repo's DB is used
71     * which may not necessarily be the same as the local DB.
72     *
73     * TODO: allow querying non-local repos.
74     * @return IReadableDatabase
75     */
76    protected function getDB() {
77        return $this->mRepo->getReplicaDB();
78    }
79
80    public function execute() {
81        $this->run();
82    }
83
84    public function getCacheMode( $params ) {
85        return 'public';
86    }
87
88    /**
89     * @param ApiPageSet $resultPageSet
90     * @return void
91     */
92    public function executeGenerator( $resultPageSet ) {
93        if ( $resultPageSet->isResolvingRedirects() ) {
94            $this->dieWithError( 'apierror-allimages-redirect', 'invalidparammix' );
95        }
96
97        $this->run( $resultPageSet );
98    }
99
100    /**
101     * @param ApiPageSet|null $resultPageSet
102     * @return void
103     */
104    private function run( $resultPageSet = null ) {
105        $repo = $this->mRepo;
106        if ( !$repo instanceof LocalRepo ) {
107            $this->dieWithError( 'apierror-unsupportedrepo' );
108        }
109
110        $prefix = $this->getModulePrefix();
111
112        $db = $this->getDB();
113
114        $params = $this->extractRequestParams();
115
116        // Table and return fields
117        $prop = array_fill_keys( $params['prop'], true );
118
119        $fileQuery = LocalFile::getQueryInfo();
120        $this->addTables( $fileQuery['tables'] );
121        $this->addFields( $fileQuery['fields'] );
122        $this->addJoinConds( $fileQuery['joins'] );
123
124        $ascendingOrder = true;
125        if ( $params['dir'] == 'descending' || $params['dir'] == 'older' ) {
126            $ascendingOrder = false;
127        }
128
129        if ( $params['sort'] == 'name' ) {
130            // Check mutually exclusive params
131            $disallowed = [ 'start', 'end', 'user' ];
132            foreach ( $disallowed as $pname ) {
133                if ( isset( $params[$pname] ) ) {
134                    $this->dieWithError(
135                        [
136                            'apierror-invalidparammix-mustusewith',
137                            "{$prefix}{$pname}",
138                            "{$prefix}sort=timestamp"
139                        ],
140                        'invalidparammix'
141                    );
142                }
143            }
144            if ( $params['filterbots'] != 'all' ) {
145                $this->dieWithError(
146                    [
147                        'apierror-invalidparammix-mustusewith',
148                        "{$prefix}filterbots",
149                        "{$prefix}sort=timestamp"
150                    ],
151                    'invalidparammix'
152                );
153            }
154
155            // Pagination
156            if ( $params['continue'] !== null ) {
157                $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'string' ] );
158                $op = $ascendingOrder ? '>=' : '<=';
159                $this->addWhere( $db->expr( 'img_name', $op, $cont[0] ) );
160            }
161
162            // Image filters
163            $from = $params['from'] === null ? null : $this->titlePartToKey( $params['from'], NS_FILE );
164            $to = $params['to'] === null ? null : $this->titlePartToKey( $params['to'], NS_FILE );
165            $this->addWhereRange( 'img_name', $ascendingOrder ? 'newer' : 'older', $from, $to );
166
167            if ( isset( $params['prefix'] ) ) {
168                $this->addWhere(
169                    $db->expr(
170                        'img_name',
171                        IExpression::LIKE,
172                        new LikeValue( $this->titlePartToKey( $params['prefix'], NS_FILE ), $db->anyString() )
173                    )
174                );
175            }
176        } else {
177            // Check mutually exclusive params
178            $disallowed = [ 'from', 'to', 'prefix' ];
179            foreach ( $disallowed as $pname ) {
180                if ( isset( $params[$pname] ) ) {
181                    $this->dieWithError(
182                        [
183                            'apierror-invalidparammix-mustusewith',
184                            "{$prefix}{$pname}",
185                            "{$prefix}sort=name"
186                        ],
187                        'invalidparammix'
188                    );
189                }
190            }
191            if ( $params['user'] !== null && $params['filterbots'] != 'all' ) {
192                // Since filterbots checks if each user has the bot right, it
193                // doesn't make sense to use it with user
194                $this->dieWithError(
195                    [ 'apierror-invalidparammix-cannotusewith', "{$prefix}user", "{$prefix}filterbots" ]
196                );
197            }
198
199            // Pagination
200            $this->addTimestampWhereRange(
201                'img_timestamp',
202                $ascendingOrder ? 'newer' : 'older',
203                $params['start'],
204                $params['end']
205            );
206            // Include in ORDER BY for uniqueness
207            $this->addWhereRange( 'img_name', $ascendingOrder ? 'newer' : 'older', null, null );
208
209            if ( $params['continue'] !== null ) {
210                $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'timestamp', 'string' ] );
211                $op = ( $ascendingOrder ? '>=' : '<=' );
212                $this->addWhere( $db->buildComparison( $op, [
213                    'img_timestamp' => $db->timestamp( $cont[0] ),
214                    'img_name' => $cont[1],
215                ] ) );
216            }
217
218            // Image filters
219            if ( $params['user'] !== null ) {
220                $this->addWhereFld( $fileQuery['fields']['img_user_text'], $params['user'] );
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( new OrExpressionGroup( ...$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                    $this->setContinueEnumParameter( 'continue', "$row->img_timestamp|$row->img_name" );
300                }
301                break;
302            }
303
304            if ( $resultPageSet === null ) {
305                $file = $repo->newFileFromRow( $row );
306                $info = array_merge( [ 'name' => $row->img_name ],
307                    ApiQueryImageInfo::getInfo( $file, $prop, $result ) );
308                self::addTitleInfo( $info, $file->getTitle() );
309
310                $fit = $result->addValue( [ 'query', $this->getModuleName() ], null, $info );
311                if ( !$fit ) {
312                    if ( $params['sort'] == 'name' ) {
313                        $this->setContinueEnumParameter( 'continue', $row->img_name );
314                    } else {
315                        $this->setContinueEnumParameter( 'continue', "$row->img_timestamp|$row->img_name" );
316                    }
317                    break;
318                }
319            } else {
320                $titles[] = Title::makeTitle( NS_FILE, $row->img_name );
321            }
322        }
323
324        if ( $resultPageSet === null ) {
325            $result->addIndexedTagName( [ 'query', $this->getModuleName() ], 'img' );
326        } else {
327            $resultPageSet->populateFromTitles( $titles );
328        }
329    }
330
331    public function getAllowedParams() {
332        $ret = [
333            'sort' => [
334                ParamValidator::PARAM_DEFAULT => 'name',
335                ParamValidator::PARAM_TYPE => [
336                    'name',
337                    'timestamp'
338                ]
339            ],
340            'dir' => [
341                ParamValidator::PARAM_DEFAULT => 'ascending',
342                ParamValidator::PARAM_TYPE => [
343                    // sort=name
344                    'ascending',
345                    'descending',
346                    // sort=timestamp
347                    'newer',
348                    'older'
349                ]
350            ],
351            'from' => null,
352            'to' => null,
353            'continue' => [
354                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
355            ],
356            'start' => [
357                ParamValidator::PARAM_TYPE => 'timestamp'
358            ],
359            'end' => [
360                ParamValidator::PARAM_TYPE => 'timestamp'
361            ],
362            'prop' => [
363                ParamValidator::PARAM_TYPE => ApiQueryImageInfo::getPropertyNames( $this->propertyFilter ),
364                ParamValidator::PARAM_DEFAULT => 'timestamp|url',
365                ParamValidator::PARAM_ISMULTI => true,
366                ApiBase::PARAM_HELP_MSG => 'apihelp-query+imageinfo-param-prop',
367                ApiBase::PARAM_HELP_MSG_PER_VALUE =>
368                    ApiQueryImageInfo::getPropertyMessages( $this->propertyFilter ),
369            ],
370            'prefix' => null,
371            'minsize' => [
372                ParamValidator::PARAM_TYPE => 'integer',
373            ],
374            'maxsize' => [
375                ParamValidator::PARAM_TYPE => 'integer',
376            ],
377            'sha1' => null,
378            'sha1base36' => null,
379            'user' => [
380                ParamValidator::PARAM_TYPE => 'user',
381                UserDef::PARAM_ALLOWED_USER_TYPES => [ 'name', 'ip', 'temp', 'id', 'interwiki' ],
382            ],
383            'filterbots' => [
384                ParamValidator::PARAM_DEFAULT => 'all',
385                ParamValidator::PARAM_TYPE => [
386                    'all',
387                    'bots',
388                    'nobots'
389                ]
390            ],
391            'mime' => [
392                ParamValidator::PARAM_ISMULTI => true,
393            ],
394            'limit' => [
395                ParamValidator::PARAM_DEFAULT => 10,
396                ParamValidator::PARAM_TYPE => 'limit',
397                IntegerDef::PARAM_MIN => 1,
398                IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
399                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
400            ],
401        ];
402
403        if ( $this->getConfig()->get( MainConfigNames::MiserMode ) ) {
404            $ret['mime'][ApiBase::PARAM_HELP_MSG] = 'api-help-param-disabled-in-miser-mode';
405        }
406
407        return $ret;
408    }
409
410    private $propertyFilter = [ 'archivename', 'thumbmime', 'uploadwarning' ];
411
412    protected function getExamplesMessages() {
413        return [
414            'action=query&list=allimages&aifrom=B'
415                => 'apihelp-query+allimages-example-b',
416            'action=query&list=allimages&aiprop=user|timestamp|url&' .
417                'aisort=timestamp&aidir=older'
418                => 'apihelp-query+allimages-example-recent',
419            'action=query&list=allimages&aimime=image/png|image/gif'
420                => 'apihelp-query+allimages-example-mimetypes',
421            'action=query&generator=allimages&gailimit=4&' .
422                'gaifrom=T&prop=imageinfo'
423                => 'apihelp-query+allimages-example-generator',
424        ];
425    }
426
427    public function getHelpUrls() {
428        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Allimages';
429    }
430}