Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
61.42% covered (warning)
61.42%
78 / 127
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
ApiQueryPageImages
61.42% covered (warning)
61.42%
78 / 127
50.00% covered (danger)
50.00%
4 / 8
86.20
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTitles
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 execute
81.82% covered (warning)
81.82%
36 / 44
0.00% covered (danger)
0.00%
0 / 1
14.02
 getCacheMode
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setResultValues
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
132
 getAllowedParams
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
1 / 1
1
 getExamplesMessages
0.00% covered (danger)
0.00%
0 / 4
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
3namespace PageImages;
4
5use MediaWiki\Api\ApiBase;
6use MediaWiki\Api\ApiQuery;
7use MediaWiki\Api\ApiQueryBase;
8use MediaWiki\FileRepo\RepoGroup;
9use MediaWiki\Page\PageReference;
10use MediaWiki\Page\PageReferenceValue;
11use MediaWiki\Utils\UrlUtils;
12use Wikimedia\ParamValidator\ParamValidator;
13use Wikimedia\ParamValidator\TypeDef\IntegerDef;
14
15/**
16 * Expose image information for a page via a new prop=pageimages API.
17 *
18 * @see https://www.mediawiki.org/wiki/Extension:PageImages#API
19 *
20 * @license WTFPL
21 * @author Max Semenik
22 * @author Ryan Kaldari
23 * @author Yuvi Panda
24 * @author Sam Smith
25 */
26class ApiQueryPageImages extends ApiQueryBase {
27
28    public function __construct(
29        ApiQuery $query,
30        string $moduleName,
31        private readonly RepoGroup $repoGroup,
32        private readonly UrlUtils $urlUtils,
33    ) {
34        parent::__construct( $query, $moduleName, 'pi' );
35    }
36
37    /**
38     * Gets the set of titles to get page images for.
39     *
40     * Note well that the set of titles comprises the set of "good" titles
41     * (see {@see ApiPageSet::getGoodPages}) union the set of "missing"
42     * titles in the File namespace that might correspond to foreign files.
43     * The latter are included because titles in the File namespace are
44     * expected to be found with {@see \RepoGroup::findFile}.
45     *
46     * @return PageReference[] A map of page ID, which will be negative in the case
47     *  of missing titles in the File namespace, to PageReference object
48     */
49    protected function getTitles() {
50        $pageSet = $this->getPageSet();
51        $titles = $pageSet->getGoodPages();
52
53        // T98791: We want foreign files to be treated like local files
54        // in #execute, so include the set of missing filespace pages,
55        // which were initially rejected in ApiPageSet#execute.
56        $missingTitles = $pageSet->getMissingTitlesByNamespace();
57        $missingFileTitles = $missingTitles[NS_FILE] ?? [];
58
59        // $titles is a map of ID to title object, which is ideal,
60        // whereas $missingFileTitles is a map of title text to ID.
61        // Do not use array_merge here as it doesn't preserve keys.
62        foreach ( $missingFileTitles as $dbkey => $id ) {
63            $titles[$id] = PageReferenceValue::localReference( NS_FILE, $dbkey );
64        }
65
66        return $titles;
67    }
68
69    /**
70     * Evaluates the parameters, performs the requested retrieval of page images,
71     * and sets up the result
72     */
73    public function execute(): void {
74        $params = $this->extractRequestParams();
75        $prop = array_flip( $params['prop'] );
76        if ( !count( $prop ) ) {
77            $this->dieWithError(
78                [ 'apierror-paramempty', $this->encodeParamName( 'prop' ) ], 'noprop'
79            );
80        }
81
82        $allTitles = $this->getTitles();
83
84        if ( count( $allTitles ) === 0 ) {
85            return;
86        }
87
88        // Find the offset based on the continue param
89        $offset = 0;
90        if ( isset( $params['continue'] ) ) {
91            // Get the position (not the key) of the 'continue' page within the
92            // array of titles. Set this as the offset.
93            $pageIds = array_keys( $allTitles );
94            $offset = array_search( intval( $params['continue'] ), $pageIds );
95            // If the 'continue' page wasn't found, die with error
96            $this->dieContinueUsageIf( !$offset );
97        }
98
99        $limit = $params['limit'];
100        // Slice the part of the array we want to find images for
101        $titles = array_slice( $allTitles, $offset, $limit, true );
102
103        // Get the next item in the title array and use it to set the continue value
104        $nextItemArray = array_slice( $allTitles, $offset + $limit, 1, true );
105        if ( $nextItemArray ) {
106            $this->setContinueEnumParameter( 'continue', key( $nextItemArray ) );
107        }
108
109        // Find any titles in the file namespace so we can handle those separately
110        $filePageTitles = [];
111        foreach ( $titles as $id => $title ) {
112            if ( $title->getNamespace() === NS_FILE ) {
113                $filePageTitles[$id] = $title;
114                unset( $titles[$id] );
115            }
116        }
117
118        $size = $params['thumbsize'];
119        $lang = $params['langcode'];
120        // Extract page images from the page_props table
121        if ( count( $titles ) > 0 ) {
122            $this->addTables( 'page_props' );
123            $this->addFields( [ 'pp_page', 'pp_propname', 'pp_value' ] );
124            $this->addWhere( [ 'pp_page' => array_keys( $titles ),
125                'pp_propname' => PageImages::getPropNames( $params['license'] ) ] );
126
127            $res = $this->select( __METHOD__ );
128
129            $buffer = [];
130            $propNameAny = PageImages::getPropName( false );
131            foreach ( $res as $row ) {
132                $pageId = $row->pp_page;
133                if ( !array_key_exists( $pageId, $buffer ) || $row->pp_propname === $propNameAny ) {
134                    $buffer[$pageId] = $row;
135                }
136            }
137
138            foreach ( $buffer as $pageId => $row ) {
139                $fileName = $row->pp_value;
140                $this->setResultValues( $prop, $pageId, $fileName, $size, $lang );
141            }
142        // End page props image extraction
143        }
144
145        // Extract images from file namespace pages. In this case we just use
146        // the file itself rather than searching for a page_image. (Bug 50252)
147        foreach ( $filePageTitles as $pageId => $title ) {
148            $fileName = $title->getDBkey();
149            $this->setResultValues( $prop, $pageId, $fileName, $size, $lang );
150        }
151    }
152
153    /**
154     * Get the cache mode for the data generated by this module
155     *
156     * @param array $params Ignored parameters
157     * @return string Always returns "public"
158     */
159    public function getCacheMode( $params ): string {
160        return 'public';
161    }
162
163    /**
164     * For a given page, set API return values for thumbnail and pageimage as needed
165     *
166     * @param array $prop The prop values from the API request
167     * @param int $pageId The ID of the page
168     * @param string $fileName The name of the file to transform
169     * @param int $size The thumbsize value from the API request
170     * @param string|null $lang The language code from the API request
171     */
172    protected function setResultValues(
173        array $prop,
174        int $pageId,
175        string $fileName,
176        int $size,
177        ?string $lang
178    ): void {
179        $vals = [];
180        if ( isset( $prop['thumbnail'] ) || isset( $prop['original'] ) ) {
181            $file = $this->repoGroup->findFile( $fileName );
182            if ( $file ) {
183                if ( isset( $prop['thumbnail'] ) ) {
184                    $thumb = $file->transform( [
185                        'width' => $size,
186                        'lang' => $lang
187                    ] );
188                    if ( $thumb && $thumb->getUrl() ) {
189                        // You can request a thumb 1000x larger than the original
190                        // which (in case of bitmap original) will return a Thumb object
191                        // that will lie about its size but have the original as an image.
192                        $reportedSize = $thumb->fileIsSource() ? $file : $thumb;
193                        $vals['thumbnail'] = [
194                            'source' => $this->urlUtils->expand( $thumb->getUrl(), PROTO_CURRENT ),
195                            'width' => $reportedSize->getWidth(),
196                            'height' => $reportedSize->getHeight(),
197                        ];
198                    }
199                }
200
201                if ( isset( $prop['original'] ) ) {
202                    $originalSize = [
203                        'width' => $file->getWidth(),
204                        'height' => $file->getHeight()
205                    ];
206                    if ( $lang ) {
207                        $file = $file->transform( [
208                            'lang' => $lang,
209                            'width' => $originalSize['width'],
210                            'height' => $originalSize['height']
211                        ] );
212                    }
213                    $original_url = $this->urlUtils->expand( $file->getUrl(), PROTO_CURRENT );
214
215                    $vals['original'] = [
216                        'source' => $original_url,
217                        'width' => $originalSize['width'],
218                        'height' => $originalSize['height']
219                    ];
220                }
221            }
222        }
223
224        if ( isset( $prop['name'] ) ) {
225            $vals['pageimage'] = $fileName;
226        }
227
228        $this->getResult()->addValue( [ 'query', 'pages' ], $pageId, $vals );
229    }
230
231    /**
232     * Return an array describing all possible parameters to this module
233     */
234    public function getAllowedParams(): array {
235        return [
236            'prop' => [
237                ParamValidator::PARAM_TYPE => [ 'thumbnail', 'name', 'original' ],
238                ParamValidator::PARAM_ISMULTI => true,
239                ParamValidator::PARAM_DEFAULT => 'thumbnail|name',
240                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
241            ],
242            'thumbsize' => [
243                ParamValidator::PARAM_TYPE => 'integer',
244                ParamValidator::PARAM_DEFAULT => 50,
245            ],
246            'limit' => [
247                ParamValidator::PARAM_DEFAULT => 50,
248                ParamValidator::PARAM_TYPE => 'limit',
249                IntegerDef::PARAM_MIN => 1,
250                IntegerDef::PARAM_MAX => 50,
251                IntegerDef::PARAM_MAX2 => 100,
252            ],
253            'license' => [
254                ParamValidator::PARAM_TYPE => [ PageImages::LICENSE_FREE, PageImages::LICENSE_ANY ],
255                ParamValidator::PARAM_ISMULTI => false,
256                ParamValidator::PARAM_DEFAULT => $this->getConfig()->get( 'PageImagesAPIDefaultLicense' ),
257                ApiBase::PARAM_HELP_MSG_PER_VALUE => [],
258            ],
259            'continue' => [
260                ParamValidator::PARAM_TYPE => 'integer',
261                ApiBase::PARAM_HELP_MSG => 'api-help-param-continue',
262            ],
263            'langcode' => [
264                ParamValidator::PARAM_TYPE => 'string',
265                ParamValidator::PARAM_DEFAULT => null
266            ]
267        ];
268    }
269
270    /**
271     * @inheritDoc
272     */
273    protected function getExamplesMessages(): array {
274        return [
275            'action=query&prop=pageimages&titles=Albert%20Einstein&pithumbsize=100' =>
276                'apihelp-query+pageimages-example-1',
277        ];
278    }
279
280    /**
281     * @see ApiBase::getHelpUrls()
282     */
283    public function getHelpUrls(): string {
284        return "https://www.mediawiki.org/wiki/Special:MyLanguage/Extension:PageImages#API";
285    }
286
287}