Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 139 |
|
0.00% |
0 / 30 |
CRAP | |
0.00% |
0 / 1 |
ForeignAPIFile | |
0.00% |
0 / 139 |
|
0.00% |
0 / 30 |
3660 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
newFromTitle | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
12 | |||
getProps | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getRepo | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
exists | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getPath | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
transform | |
0.00% |
0 / 25 |
|
0.00% |
0 / 1 |
42 | |||
getWidth | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getHeight | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getMetadata | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getMetadataArray | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getExtendedMetadata | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
parseMetadata | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
parseMetadataValue | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
getSize | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
getUrl | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
getDescriptionShortUrl | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
20 | |||
getUploader | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getDescription | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
getSha1 | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getTimestamp | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getMimeType | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getMediaType | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getDescriptionUrl | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getThumbPath | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
12 | |||
getThumbnails | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
12 | |||
purgeCache | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
purgeDescriptionPage | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
purgeThumbnails | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
12 | |||
isTransformedLocally | |
0.00% |
0 / 1 |
|
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 | |
21 | use MediaWiki\MediaWikiServices; |
22 | use MediaWiki\Permissions\Authority; |
23 | use MediaWiki\Title\Title; |
24 | use MediaWiki\User\UserIdentity; |
25 | use MediaWiki\User\UserIdentityValue; |
26 | |
27 | /** |
28 | * Foreign file accessible through api.php requests. |
29 | * |
30 | * @ingroup FileAbstraction |
31 | */ |
32 | class 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 | } |