Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 139
0.00% covered (danger)
0.00%
0 / 30
CRAP
0.00% covered (danger)
0.00%
0 / 1
ForeignAPIFile
0.00% covered (danger)
0.00%
0 / 139
0.00% covered (danger)
0.00%
0 / 30
3660
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 newFromTitle
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 getProps
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRepo
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 exists
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getPath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 transform
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
42
 getWidth
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getHeight
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getMetadata
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getMetadataArray
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getExtendedMetadata
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 parseMetadata
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 parseMetadataValue
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getSize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 getUrl
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 getDescriptionShortUrl
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 getUploader
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getDescription
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getSha1
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getTimestamp
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getMimeType
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getMediaType
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getDescriptionUrl
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getThumbPath
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getThumbnails
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 purgeCache
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 purgeDescriptionPage
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 purgeThumbnails
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 isTransformedLocally
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * This program is free software; you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation; either version 2 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License along
14 * with this program; if not, write to the Free Software Foundation, Inc.,
15 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 * http://www.gnu.org/copyleft/gpl.html
17 *
18 * @file
19 */
20
21use MediaWiki\MediaWikiServices;
22use MediaWiki\Permissions\Authority;
23use MediaWiki\Title\Title;
24use MediaWiki\User\UserIdentity;
25use MediaWiki\User\UserIdentityValue;
26
27/**
28 * Foreign file accessible through api.php requests.
29 *
30 * @ingroup FileAbstraction
31 */
32class ForeignAPIFile extends File {
33    /** @var bool */
34    private $mExists;
35    /** @var array */
36    private $mInfo;
37
38    protected $repoClass = ForeignAPIRepo::class;
39
40    /**
41     * @param Title|string|false $title
42     * @param ForeignApiRepo $repo
43     * @param array $info
44     * @param bool $exists
45     */
46    public function __construct( $title, $repo, $info, $exists = false ) {
47        parent::__construct( $title, $repo );
48
49        $this->mInfo = $info;
50        $this->mExists = $exists;
51
52        $this->assertRepoDefined();
53    }
54
55    /**
56     * @param Title $title
57     * @param ForeignApiRepo $repo
58     * @return ForeignAPIFile|null
59     */
60    public static function newFromTitle( Title $title, $repo ) {
61        $data = $repo->fetchImageQuery( [
62            'titles' => 'File:' . $title->getDBkey(),
63            'iiprop' => self::getProps(),
64            'prop' => 'imageinfo',
65            'iimetadataversion' => MediaHandler::getMetadataVersion(),
66            // extmetadata is language-dependent, accessing the current language here
67            // would be problematic, so we just get them all
68            'iiextmetadatamultilang' => 1,
69        ] );
70
71        $info = $repo->getImageInfo( $data );
72
73        if ( $info ) {
74            $lastRedirect = count( $data['query']['redirects'] ?? [] ) - 1;
75            if ( $lastRedirect >= 0 ) {
76                // @phan-suppress-next-line PhanTypeArraySuspiciousNullable
77                $newtitle = Title::newFromText( $data['query']['redirects'][$lastRedirect]['to'] );
78                $img = new self( $newtitle, $repo, $info, true );
79                $img->redirectedFrom( $title->getDBkey() );
80            } else {
81                $img = new self( $title, $repo, $info, true );
82            }
83
84            return $img;
85        } else {
86            return null;
87        }
88    }
89
90    /**
91     * Get the property string for iiprop and aiprop
92     * @return string
93     */
94    public static function getProps() {
95        return 'timestamp|user|comment|url|size|sha1|metadata|mime|mediatype|extmetadata';
96    }
97
98    /**
99     * @return ForeignAPIRepo|false
100     */
101    public function getRepo() {
102        return $this->repo;
103    }
104
105    // Dummy functions...
106
107    /**
108     * @return bool
109     */
110    public function exists() {
111        return $this->mExists;
112    }
113
114    /**
115     * @return bool
116     */
117    public function getPath() {
118        return false;
119    }
120
121    /**
122     * @param array $params
123     * @param int $flags
124     * @return MediaTransformOutput|false
125     */
126    public function transform( $params, $flags = 0 ) {
127        if ( !$this->canRender() ) {
128            // show icon
129            return parent::transform( $params, $flags );
130        }
131
132        // Note, the this->canRender() check above implies
133        // that we have a handler, and it can do makeParamString.
134        $otherParams = $this->handler->makeParamString( $params );
135        $width = $params['width'] ?? -1;
136        $height = $params['height'] ?? -1;
137        $thumbUrl = false;
138
139        if ( $width > 0 || $height > 0 ) {
140            // Only query the remote if there are dimensions
141            $thumbUrl = $this->repo->getThumbUrlFromCache(
142                $this->getName(),
143                $width,
144                $height,
145                $otherParams
146            );
147        } elseif ( $this->getMediaType() === MEDIATYPE_AUDIO ) {
148            // This has no dimensions, but we still need to pass a value to getTransform()
149            $thumbUrl = '/';
150        }
151        if ( $thumbUrl === false ) {
152            global $wgLang;
153
154            return $this->repo->getThumbError(
155                $this->getName(),
156                $width,
157                $height,
158                $otherParams,
159                $wgLang->getCode()
160            );
161        }
162
163        return $this->handler->getTransform( $this, 'bogus', $thumbUrl, $params );
164    }
165
166    // Info we can get from API...
167
168    /**
169     * @param int $page
170     * @return int
171     */
172    public function getWidth( $page = 1 ) {
173        return (int)( $this->mInfo['width'] ?? 0 );
174    }
175
176    /**
177     * @param int $page
178     * @return int
179     */
180    public function getHeight( $page = 1 ) {
181        return (int)( $this->mInfo['height'] ?? 0 );
182    }
183
184    /**
185     * @return string|false
186     */
187    public function getMetadata() {
188        if ( isset( $this->mInfo['metadata'] ) ) {
189            return serialize( self::parseMetadata( $this->mInfo['metadata'] ) );
190        }
191
192        return false;
193    }
194
195    /**
196     * @return array
197     */
198    public function getMetadataArray(): array {
199        if ( isset( $this->mInfo['metadata'] ) ) {
200            return self::parseMetadata( $this->mInfo['metadata'] );
201        }
202
203        return [];
204    }
205
206    /**
207     * @return array|null Extended metadata (see imageinfo API for format) or
208     *   null on error
209     */
210    public function getExtendedMetadata() {
211        return $this->mInfo['extmetadata'] ?? null;
212    }
213
214    /**
215     * @param mixed $metadata
216     * @return array
217     */
218    public static function parseMetadata( $metadata ) {
219        if ( !is_array( $metadata ) ) {
220            return [ '_error' => $metadata ];
221        }
222        '@phan-var array[] $metadata';
223        $ret = [];
224        foreach ( $metadata as $meta ) {
225            $ret[$meta['name']] = self::parseMetadataValue( $meta['value'] );
226        }
227
228        return $ret;
229    }
230
231    /**
232     * @param mixed $metadata
233     * @return mixed
234     */
235    private static function parseMetadataValue( $metadata ) {
236        if ( !is_array( $metadata ) ) {
237            return $metadata;
238        }
239        '@phan-var array[] $metadata';
240        $ret = [];
241        foreach ( $metadata as $meta ) {
242            $ret[$meta['name']] = self::parseMetadataValue( $meta['value'] );
243        }
244
245        return $ret;
246    }
247
248    /**
249     * @return int|null|false
250     */
251    public function getSize() {
252        return isset( $this->mInfo['size'] ) ? intval( $this->mInfo['size'] ) : null;
253    }
254
255    /**
256     * @return null|string
257     */
258    public function getUrl() {
259        return isset( $this->mInfo['url'] ) ? strval( $this->mInfo['url'] ) : null;
260    }
261
262    /**
263     * Get short description URL for a file based on the foreign API response,
264     * or if unavailable, the short URL is constructed from the foreign page ID.
265     *
266     * @return null|string
267     * @since 1.27
268     */
269    public function getDescriptionShortUrl() {
270        if ( isset( $this->mInfo['descriptionshorturl'] ) ) {
271            return $this->mInfo['descriptionshorturl'];
272        } elseif ( isset( $this->mInfo['pageid'] ) ) {
273            $url = $this->repo->makeUrl( [ 'curid' => $this->mInfo['pageid'] ] );
274            if ( $url !== false ) {
275                return $url;
276            }
277        }
278        return null;
279    }
280
281    public function getUploader( int $audience = self::FOR_PUBLIC, Authority $performer = null ): ?UserIdentity {
282        if ( isset( $this->mInfo['user'] ) ) {
283            return UserIdentityValue::newExternal( $this->getRepoName(), $this->mInfo['user'] );
284        }
285        return null;
286    }
287
288    /**
289     * @param int $audience
290     * @param Authority|null $performer
291     * @return null|string
292     */
293    public function getDescription( $audience = self::FOR_PUBLIC, Authority $performer = null ) {
294        return isset( $this->mInfo['comment'] ) ? strval( $this->mInfo['comment'] ) : null;
295    }
296
297    /**
298     * @return null|string
299     */
300    public function getSha1() {
301        return isset( $this->mInfo['sha1'] )
302            ? Wikimedia\base_convert( strval( $this->mInfo['sha1'] ), 16, 36, 31 )
303            : null;
304    }
305
306    /**
307     * @return string|false
308     */
309    public function getTimestamp() {
310        return wfTimestamp( TS_MW,
311            isset( $this->mInfo['timestamp'] )
312                ? strval( $this->mInfo['timestamp'] )
313                : null
314        );
315    }
316
317    /**
318     * @return string
319     */
320    public function getMimeType() {
321        if ( !isset( $this->mInfo['mime'] ) ) {
322            $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
323            $this->mInfo['mime'] = $magic->getMimeTypeFromExtensionOrNull( $this->getExtension() );
324        }
325
326        return $this->mInfo['mime'];
327    }
328
329    /**
330     * @return int|string
331     */
332    public function getMediaType() {
333        if ( isset( $this->mInfo['mediatype'] ) ) {
334            return $this->mInfo['mediatype'];
335        }
336        $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
337
338        return $magic->getMediaType( null, $this->getMimeType() );
339    }
340
341    /**
342     * @return string|false
343     */
344    public function getDescriptionUrl() {
345        return $this->mInfo['descriptionurl'] ?? false;
346    }
347
348    /**
349     * Only useful if we're locally caching thumbs anyway...
350     * @param string $suffix
351     * @return null|string
352     */
353    public function getThumbPath( $suffix = '' ) {
354        if ( !$this->repo->canCacheThumbs() ) {
355            return null;
356        }
357
358        $path = $this->repo->getZonePath( 'thumb' ) . '/' . $this->getHashPath();
359        if ( $suffix ) {
360            $path .= $suffix . '/';
361        }
362        return $path;
363    }
364
365    /**
366     * @return string[]
367     */
368    protected function getThumbnails() {
369        $dir = $this->getThumbPath( $this->getName() );
370        $iter = $this->repo->getBackend()->getFileList( [ 'dir' => $dir ] );
371
372        $files = [];
373        if ( $iter ) {
374            foreach ( $iter as $file ) {
375                $files[] = $file;
376            }
377        }
378
379        return $files;
380    }
381
382    public function purgeCache( $options = [] ) {
383        $this->purgeThumbnails( $options );
384        $this->purgeDescriptionPage();
385    }
386
387    private function purgeDescriptionPage() {
388        $services = MediaWikiServices::getInstance();
389        $langCode = $services->getContentLanguage()->getCode();
390
391        // Key must match File::getDescriptionText
392        $key = $this->repo->getLocalCacheKey( 'file-remote-description', $langCode, md5( $this->getName() ) );
393        $services->getMainWANObjectCache()->delete( $key );
394    }
395
396    /**
397     * @param array $options
398     */
399    public function purgeThumbnails( $options = [] ) {
400        $key = $this->repo->getLocalCacheKey( 'file-thumb-url', sha1( $this->getName() ) );
401        MediaWikiServices::getInstance()->getMainWANObjectCache()->delete( $key );
402
403        $files = $this->getThumbnails();
404        // Give media handler a chance to filter the purge list
405        $handler = $this->getHandler();
406        if ( $handler ) {
407            $handler->filterThumbnailPurgeList( $files, $options );
408        }
409
410        $dir = $this->getThumbPath( $this->getName() );
411        $purgeList = [];
412        foreach ( $files as $file ) {
413            $purgeList[] = "{$dir}{$file}";
414        }
415
416        # Delete the thumbnails
417        $this->repo->quickPurgeBatch( $purgeList );
418        # Clear out the thumbnail directory if empty
419        $this->repo->quickCleanDir( $dir );
420    }
421
422    /**
423     * The thumbnail is created on the foreign server and fetched over internet
424     * @since 1.25
425     * @return bool
426     */
427    public function isTransformedLocally() {
428        return false;
429    }
430}