Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 98
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryDuplicateFiles
0.00% covered (danger)
0.00%
0 / 97
0.00% covered (danger)
0.00%
0 / 8
930
0.00% covered (danger)
0.00%
0 / 1
 __construct
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 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 run
0.00% covered (danger)
0.00%
0 / 66
0.00% covered (danger)
0.00%
0 / 1
552
 getAllowedParams
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
2
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 6
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 * Copyright © 2008 Roan Kattouw <roan.kattouw@gmail.com>
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 */
8
9namespace MediaWiki\Api;
10
11use MediaWiki\FileRepo\File\File;
12use MediaWiki\FileRepo\RepoGroup;
13use Wikimedia\ParamValidator\ParamValidator;
14use Wikimedia\ParamValidator\TypeDef\IntegerDef;
15use Wikimedia\Timestamp\TimestampFormat as TS;
16
17/**
18 * A query module to list duplicates of the given file(s)
19 *
20 * @ingroup API
21 */
22class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
23
24    public function __construct(
25        ApiQuery $query,
26        string $moduleName,
27        private readonly RepoGroup $repoGroup,
28    ) {
29        parent::__construct( $query, $moduleName, 'df' );
30    }
31
32    public function execute() {
33        $this->run();
34    }
35
36    /** @inheritDoc */
37    public function getCacheMode( $params ) {
38        return 'public';
39    }
40
41    /** @inheritDoc */
42    public function executeGenerator( $resultPageSet ) {
43        $this->run( $resultPageSet );
44    }
45
46    /**
47     * @param ApiPageSet|null $resultPageSet
48     */
49    private function run( $resultPageSet = null ) {
50        $params = $this->extractRequestParams();
51        $namespaces = $this->getPageSet()->getGoodAndMissingTitlesByNamespace();
52        if ( empty( $namespaces[NS_FILE] ) ) {
53            return;
54        }
55        $images = $namespaces[NS_FILE];
56
57        if ( $params['dir'] == 'descending' ) {
58            $images = array_reverse( $images );
59        }
60
61        $skipUntilThisDup = false;
62        if ( isset( $params['continue'] ) ) {
63            $cont = $this->parseContinueParamOrDie( $params['continue'], [ 'string', 'string' ] );
64            $fromImage = $cont[0];
65            $skipUntilThisDup = $cont[1];
66            // Filter out any images before $fromImage
67            foreach ( $images as $image => $pageId ) {
68                if ( $image < $fromImage ) {
69                    unset( $images[$image] );
70                } else {
71                    break;
72                }
73            }
74        }
75
76        $filesToFind = array_keys( $images );
77        if ( $params['localonly'] ) {
78            $files = $this->repoGroup->getLocalRepo()->findFiles( $filesToFind );
79        } else {
80            $files = $this->repoGroup->findFiles( $filesToFind );
81        }
82
83        $fit = true;
84        $count = 0;
85        $titles = [];
86
87        $sha1s = [];
88        foreach ( $files as $file ) {
89            /** @var File $file */
90            $sha1s[$file->getName()] = $file->getSha1();
91        }
92
93        // find all files with the hashes, result format is:
94        // [ hash => [ dup1, dup2 ], hash1 => ... ]
95        $filesToFindBySha1s = array_unique( array_values( $sha1s ) );
96        if ( $params['localonly'] ) {
97            $filesBySha1s = $this->repoGroup->getLocalRepo()->findBySha1s( $filesToFindBySha1s );
98        } else {
99            $filesBySha1s = $this->repoGroup->findBySha1s( $filesToFindBySha1s );
100        }
101
102        // iterate over $images to handle continue param correct
103        foreach ( $images as $image => $pageId ) {
104            if ( !isset( $sha1s[$image] ) ) {
105                continue; // file does not exist
106            }
107            $sha1 = $sha1s[$image];
108            $dupFiles = $filesBySha1s[$sha1];
109            if ( $params['dir'] == 'descending' ) {
110                $dupFiles = array_reverse( $dupFiles );
111            }
112            /** @var File $dupFile */
113            foreach ( $dupFiles as $dupFile ) {
114                $dupName = $dupFile->getName();
115                if ( $image == $dupName && $dupFile->isLocal() ) {
116                    continue; // ignore the local file itself
117                }
118                if ( $skipUntilThisDup !== false && $dupName < $skipUntilThisDup ) {
119                    continue; // skip to pos after the image from continue param
120                }
121                $skipUntilThisDup = false;
122                if ( ++$count > $params['limit'] ) {
123                    $fit = false; // break outer loop
124                    // We're one over limit which shows that
125                    // there are additional images to be had. Stop here...
126                    $this->setContinueEnumParameter( 'continue', $image . '|' . $dupName );
127                    break;
128                }
129                if ( $resultPageSet !== null ) {
130                    $titles[] = $dupFile->getTitle();
131                } else {
132                    $r = [
133                        'name' => $dupName,
134                        'timestamp' => wfTimestamp( TS::ISO_8601, $dupFile->getTimestamp() ),
135                        'shared' => !$dupFile->isLocal(),
136                    ];
137                    $uploader = $dupFile->getUploader( File::FOR_PUBLIC );
138                    if ( $uploader ) {
139                        $r['user'] = $uploader->getName();
140                    }
141                    $fit = $this->addPageSubItem( $pageId, $r );
142                    if ( !$fit ) {
143                        $this->setContinueEnumParameter( 'continue', $image . '|' . $dupName );
144                        break;
145                    }
146                }
147            }
148            if ( !$fit ) {
149                break;
150            }
151        }
152        if ( $resultPageSet !== null ) {
153            $resultPageSet->populateFromTitles( $titles );
154        }
155    }
156
157    /** @inheritDoc */
158    public function getAllowedParams() {
159        return [
160            'limit' => [
161                ParamValidator::PARAM_DEFAULT => 10,
162                ParamValidator::PARAM_TYPE => 'limit',
163                IntegerDef::PARAM_MIN => 1,
164                IntegerDef::PARAM_MAX => ApiBase::LIMIT_BIG1,
165                IntegerDef::PARAM_MAX2 => ApiBase::LIMIT_BIG2
166            ],
167            'continue' => [
168                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
169            ],
170            'dir' => [
171                ParamValidator::PARAM_DEFAULT => 'ascending',
172                ParamValidator::PARAM_TYPE => [
173                    'ascending',
174                    'descending'
175                ]
176            ],
177            'localonly' => false,
178        ];
179    }
180
181    /** @inheritDoc */
182    protected function getExamplesMessages() {
183        return [
184            'action=query&titles=File:Albert_Einstein_Head.jpg&prop=duplicatefiles'
185                => 'apihelp-query+duplicatefiles-example-simple',
186            'action=query&generator=allimages&prop=duplicatefiles'
187                => 'apihelp-query+duplicatefiles-example-generated',
188        ];
189    }
190
191    /** @inheritDoc */
192    public function getHelpUrls() {
193        return 'https://www.mediawiki.org/wiki/Special:MyLanguage/API:Duplicatefiles';
194    }
195}
196
197/** @deprecated class alias since 1.43 */
198class_alias( ApiQueryDuplicateFiles::class, 'ApiQueryDuplicateFiles' );