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            // We don't know if the foreign repo will have a real interwiki prefix,
284            // treat this user as a foreign imported user. Maybe we can do better?
285            return UserIdentityValue::newExternal( $this->getRepoName(), $this->mInfo['user'] );
286        }
287        return null;
288    }
289
290    /**
291     * @param int $audience
292     * @param Authority|null $performer
293     * @return null|string
294     */
295    public function getDescription( $audience = self::FOR_PUBLIC, Authority $performer = null ) {
296        return isset( $this->mInfo['comment'] ) ? strval( $this->mInfo['comment'] ) : null;
297    }
298
299    /**
300     * @return null|string
301     */
302    public function getSha1() {
303        return isset( $this->mInfo['sha1'] )
304            ? Wikimedia\base_convert( strval( $this->mInfo['sha1'] ), 16, 36, 31 )
305            : null;
306    }
307
308    /**
309     * @return string|false
310     */
311    public function getTimestamp() {
312        return wfTimestamp( TS_MW,
313            isset( $this->mInfo['timestamp'] )
314                ? strval( $this->mInfo['timestamp'] )
315                : null
316        );
317    }
318
319    /**
320     * @return string
321     */
322    public function getMimeType() {
323        if ( !isset( $this->mInfo['mime'] ) ) {
324            $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
325            $this->mInfo['mime'] = $magic->getMimeTypeFromExtensionOrNull( $this->getExtension() );
326        }
327
328        return $this->mInfo['mime'];
329    }
330
331    /**
332     * @return int|string
333     */
334    public function getMediaType() {
335        if ( isset( $this->mInfo['mediatype'] ) ) {
336            return $this->mInfo['mediatype'];
337        }
338        $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
339
340        return $magic->getMediaType( null, $this->getMimeType() );
341    }
342
343    /**
344     * @return string|false
345     */
346    public function getDescriptionUrl() {
347        return $this->mInfo['descriptionurl'] ?? false;
348    }
349
350    /**
351     * Only useful if we're locally caching thumbs anyway...
352     * @param string $suffix
353     * @return null|string
354     */
355    public function getThumbPath( $suffix = '' ) {
356        if ( !$this->repo->canCacheThumbs() ) {
357            return null;
358        }
359
360        $path = $this->repo->getZonePath( 'thumb' ) . '/' . $this->getHashPath();
361        if ( $suffix ) {
362            $path .= $suffix . '/';
363        }
364        return $path;
365    }
366
367    /**
368     * @return string[]
369     */
370    protected function getThumbnails() {
371        $dir = $this->getThumbPath( $this->getName() );
372        $iter = $this->repo->getBackend()->getFileList( [ 'dir' => $dir ] );
373
374        $files = [];
375        if ( $iter ) {
376            foreach ( $iter as $file ) {
377                $files[] = $file;
378            }
379        }
380
381        return $files;
382    }
383
384    public function purgeCache( $options = [] ) {
385        $this->purgeThumbnails( $options );
386        $this->purgeDescriptionPage();
387    }
388
389    private function purgeDescriptionPage() {
390        $services = MediaWikiServices::getInstance();
391        $langCode = $services->getContentLanguage()->getCode();
392
393        // Key must match File::getDescriptionText
394        $key = $this->repo->getLocalCacheKey( 'file-remote-description', $langCode, md5( $this->getName() ) );
395        $services->getMainWANObjectCache()->delete( $key );
396    }
397
398    /**
399     * @param array $options
400     */
401    public function purgeThumbnails( $options = [] ) {
402        $key = $this->repo->getLocalCacheKey( 'file-thumb-url', sha1( $this->getName() ) );
403        MediaWikiServices::getInstance()->getMainWANObjectCache()->delete( $key );
404
405        $files = $this->getThumbnails();
406        // Give media handler a chance to filter the purge list
407        $handler = $this->getHandler();
408        if ( $handler ) {
409            $handler->filterThumbnailPurgeList( $files, $options );
410        }
411
412        $dir = $this->getThumbPath( $this->getName() );
413        $purgeList = [];
414        foreach ( $files as $file ) {
415            $purgeList[] = "{$dir}{$file}";
416        }
417
418        # Delete the thumbnails
419        $this->repo->quickPurgeBatch( $purgeList );
420        # Clear out the thumbnail directory if empty
421        $this->repo->quickCleanDir( $dir );
422    }
423
424    /**
425     * The thumbnail is created on the foreign server and fetched over internet
426     * @since 1.25
427     * @return bool
428     */
429    public function isTransformedLocally() {
430        return false;
431    }
432}