Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 240 |
|
0.00% |
0 / 20 |
CRAP | |
0.00% |
0 / 1 |
ForeignAPIRepo | |
0.00% |
0 / 239 |
|
0.00% |
0 / 20 |
6806 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
90 | |||
newFile | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
fileExistsBatch | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
156 | |||
getFileProps | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
fetchImageQuery | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
12 | |||
getImageInfo | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
42 | |||
findBySha1 | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
getThumbUrl | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
getThumbError | |
0.00% |
0 / 20 |
|
0.00% |
0 / 1 |
20 | |||
getThumbUrlFromCache | |
0.00% |
0 / 53 |
|
0.00% |
0 / 1 |
182 | |||
getZoneUrl | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
20 | |||
getZonePath | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
canCacheThumbs | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getUserAgent | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
getInfo | |
0.00% |
0 / 17 |
|
0.00% |
0 / 1 |
20 | |||
httpGet | |
0.00% |
0 / 23 |
|
0.00% |
0 / 1 |
30 | |||
getIIProps | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
httpGetCached | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
20 | |||
enumFiles | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
assertWritableRepo | |
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 | namespace MediaWiki\FileRepo; |
22 | |
23 | use LogicException; |
24 | use MediaTransformError; |
25 | use MediaWiki\FileRepo\File\File; |
26 | use MediaWiki\FileRepo\File\ForeignAPIFile; |
27 | use MediaWiki\Json\FormatJson; |
28 | use MediaWiki\Linker\LinkTarget; |
29 | use MediaWiki\Logger\LoggerFactory; |
30 | use MediaWiki\MainConfigNames; |
31 | use MediaWiki\MediaWikiServices; |
32 | use MediaWiki\Page\PageIdentity; |
33 | use MediaWiki\Title\Title; |
34 | use RuntimeException; |
35 | use Wikimedia\FileBackend\FileBackend; |
36 | use Wikimedia\ObjectCache\WANObjectCache; |
37 | |
38 | /** |
39 | * A foreign repository for a remote MediaWiki accessible through api.php requests. |
40 | * |
41 | * @par Example config: |
42 | * @code |
43 | * $wgForeignFileRepos[] = [ |
44 | * 'class' => ForeignAPIRepo::class, |
45 | * 'name' => 'shared', |
46 | * 'apibase' => 'https://en.wikipedia.org/w/api.php', |
47 | * 'fetchDescription' => true, // Optional |
48 | * 'descriptionCacheExpiry' => 3600, |
49 | * ]; |
50 | * @endcode |
51 | * |
52 | * @ingroup FileRepo |
53 | */ |
54 | class ForeignAPIRepo extends FileRepo implements IForeignRepoWithMWApi { |
55 | /* This version string is used in the user agent for requests and will help |
56 | * server maintainers in identify ForeignAPI usage. |
57 | * Update the version every time you make breaking or significant changes. */ |
58 | private const VERSION = "2.1"; |
59 | |
60 | /** |
61 | * List of iiprop values for the thumbnail fetch queries. |
62 | */ |
63 | private const IMAGE_INFO_PROPS = [ |
64 | 'url', |
65 | 'timestamp', |
66 | ]; |
67 | |
68 | /** @var callable */ |
69 | protected $fileFactory = [ ForeignAPIFile::class, 'newFromTitle' ]; |
70 | /** @var int Check back with Commons after this expiry */ |
71 | protected $apiThumbCacheExpiry = 24 * 3600; // 1 day |
72 | |
73 | /** @var int Redownload thumbnail files after this expiry */ |
74 | protected $fileCacheExpiry = 30 * 24 * 3600; // 1 month |
75 | |
76 | /** |
77 | * @var int API metadata cache time. |
78 | * @since 1.38 |
79 | * |
80 | * This is often the performance bottleneck for ForeignAPIRepo. For |
81 | * each file used, we must fetch file metadata for it and every high-DPI |
82 | * variant, in serial, during the parse. This is slow if a page has many |
83 | * files, with RTT of the handshake often being significant. The metadata |
84 | * rarely changes, but if a new version of the file was uploaded, it might |
85 | * be displayed incorrectly until its metadata entry falls out of cache. |
86 | */ |
87 | protected $apiMetadataExpiry = 4 * 3600; // 4 hours |
88 | |
89 | /** @var array */ |
90 | protected $mFileExists = []; |
91 | |
92 | /** @var string */ |
93 | private $mApiBase; |
94 | |
95 | /** |
96 | * @param array|null $info |
97 | */ |
98 | public function __construct( $info ) { |
99 | $localFileRepo = MediaWikiServices::getInstance()->getMainConfig() |
100 | ->get( MainConfigNames::LocalFileRepo ); |
101 | parent::__construct( $info ); |
102 | |
103 | // https://commons.wikimedia.org/w/api.php |
104 | $this->mApiBase = $info['apibase'] ?? null; |
105 | |
106 | if ( isset( $info['apiThumbCacheExpiry'] ) ) { |
107 | $this->apiThumbCacheExpiry = $info['apiThumbCacheExpiry']; |
108 | } |
109 | if ( isset( $info['fileCacheExpiry'] ) ) { |
110 | $this->fileCacheExpiry = $info['fileCacheExpiry']; |
111 | } |
112 | if ( isset( $info['apiMetadataExpiry'] ) ) { |
113 | $this->apiMetadataExpiry = $info['apiMetadataExpiry']; |
114 | } |
115 | if ( !$this->scriptDirUrl ) { |
116 | // hack for description fetches |
117 | $this->scriptDirUrl = dirname( $this->mApiBase ); |
118 | } |
119 | // If we can cache thumbs we can guess sensible defaults for these |
120 | if ( $this->canCacheThumbs() && !$this->url ) { |
121 | $this->url = $localFileRepo['url']; |
122 | } |
123 | if ( $this->canCacheThumbs() && !$this->thumbUrl ) { |
124 | $this->thumbUrl = $this->url . '/thumb'; |
125 | } |
126 | } |
127 | |
128 | /** |
129 | * Per docs in FileRepo, this needs to return false if we don't support versioned |
130 | * files. Well, we don't. |
131 | * |
132 | * @param PageIdentity|LinkTarget|string $title |
133 | * @param string|false $time |
134 | * @return File|false |
135 | */ |
136 | public function newFile( $title, $time = false ) { |
137 | if ( $time ) { |
138 | return false; |
139 | } |
140 | |
141 | return parent::newFile( $title, $time ); |
142 | } |
143 | |
144 | /** |
145 | * @param string[] $files |
146 | * @return array |
147 | */ |
148 | public function fileExistsBatch( array $files ) { |
149 | $results = []; |
150 | foreach ( $files as $k => $f ) { |
151 | if ( isset( $this->mFileExists[$f] ) ) { |
152 | $results[$k] = $this->mFileExists[$f]; |
153 | unset( $files[$k] ); |
154 | } elseif ( self::isVirtualUrl( $f ) ) { |
155 | # @todo FIXME: We need to be able to handle virtual |
156 | # URLs better, at least when we know they refer to the |
157 | # same repo. |
158 | $results[$k] = false; |
159 | unset( $files[$k] ); |
160 | } elseif ( FileBackend::isStoragePath( $f ) ) { |
161 | $results[$k] = false; |
162 | unset( $files[$k] ); |
163 | wfWarn( "Got mwstore:// path '$f'." ); |
164 | } |
165 | } |
166 | |
167 | $data = $this->fetchImageQuery( [ |
168 | 'titles' => implode( '|', $files ), |
169 | 'prop' => 'imageinfo' ] |
170 | ); |
171 | |
172 | if ( isset( $data['query']['pages'] ) ) { |
173 | # First, get results from the query. Note we only care whether the image exists, |
174 | # not whether it has a description page. |
175 | foreach ( $data['query']['pages'] as $p ) { |
176 | $this->mFileExists[$p['title']] = ( $p['imagerepository'] !== '' ); |
177 | } |
178 | # Second, copy the results to any redirects that were queried |
179 | if ( isset( $data['query']['redirects'] ) ) { |
180 | foreach ( $data['query']['redirects'] as $r ) { |
181 | $this->mFileExists[$r['from']] = $this->mFileExists[$r['to']]; |
182 | } |
183 | } |
184 | # Third, copy the results to any non-normalized titles that were queried |
185 | if ( isset( $data['query']['normalized'] ) ) { |
186 | foreach ( $data['query']['normalized'] as $n ) { |
187 | $this->mFileExists[$n['from']] = $this->mFileExists[$n['to']]; |
188 | } |
189 | } |
190 | # Finally, copy the results to the output |
191 | foreach ( $files as $key => $file ) { |
192 | $results[$key] = $this->mFileExists[$file]; |
193 | } |
194 | } |
195 | |
196 | return $results; |
197 | } |
198 | |
199 | /** |
200 | * @param string $virtualUrl |
201 | * @return array |
202 | */ |
203 | public function getFileProps( $virtualUrl ) { |
204 | return []; |
205 | } |
206 | |
207 | /** |
208 | * Make an API query in the foreign repo, caching results |
209 | * |
210 | * @param array $query |
211 | * @return array|null |
212 | */ |
213 | public function fetchImageQuery( $query ) { |
214 | $languageCode = MediaWikiServices::getInstance()->getMainConfig() |
215 | ->get( MainConfigNames::LanguageCode ); |
216 | |
217 | $query = array_merge( $query, |
218 | [ |
219 | 'format' => 'json', |
220 | 'action' => 'query', |
221 | 'redirects' => 'true' |
222 | ] ); |
223 | |
224 | if ( !isset( $query['uselang'] ) ) { // uselang is unset or null |
225 | $query['uselang'] = $languageCode; |
226 | } |
227 | |
228 | $data = $this->httpGetCached( 'Metadata', $query, $this->apiMetadataExpiry ); |
229 | |
230 | if ( $data ) { |
231 | return FormatJson::decode( $data, true ); |
232 | } else { |
233 | return null; |
234 | } |
235 | } |
236 | |
237 | /** |
238 | * @param array $data |
239 | * @return array|false |
240 | */ |
241 | public function getImageInfo( $data ) { |
242 | if ( $data && isset( $data['query']['pages'] ) ) { |
243 | foreach ( $data['query']['pages'] as $info ) { |
244 | if ( isset( $info['imageinfo'][0] ) ) { |
245 | $return = $info['imageinfo'][0]; |
246 | if ( isset( $info['pageid'] ) ) { |
247 | $return['pageid'] = $info['pageid']; |
248 | } |
249 | return $return; |
250 | } |
251 | } |
252 | } |
253 | |
254 | return false; |
255 | } |
256 | |
257 | /** |
258 | * @param string $hash |
259 | * @return ForeignAPIFile[] |
260 | */ |
261 | public function findBySha1( $hash ) { |
262 | $results = $this->fetchImageQuery( [ |
263 | 'aisha1base36' => $hash, |
264 | 'aiprop' => ForeignAPIFile::getProps(), |
265 | 'list' => 'allimages', |
266 | ] ); |
267 | $ret = []; |
268 | if ( isset( $results['query']['allimages'] ) ) { |
269 | foreach ( $results['query']['allimages'] as $img ) { |
270 | // 1.14 was broken, doesn't return name attribute |
271 | if ( !isset( $img['name'] ) ) { |
272 | continue; |
273 | } |
274 | $ret[] = new ForeignAPIFile( Title::makeTitle( NS_FILE, $img['name'] ), $this, $img ); |
275 | } |
276 | } |
277 | |
278 | return $ret; |
279 | } |
280 | |
281 | /** |
282 | * @param string $name |
283 | * @param int $width |
284 | * @param int $height |
285 | * @param array|null &$result Output-only parameter, guaranteed to become an array |
286 | * @param string $otherParams |
287 | * |
288 | * @return string|false |
289 | */ |
290 | private function getThumbUrl( |
291 | $name, $width = -1, $height = -1, &$result = null, $otherParams = '' |
292 | ) { |
293 | $data = $this->fetchImageQuery( [ |
294 | 'titles' => 'File:' . $name, |
295 | 'iiprop' => self::getIIProps(), |
296 | 'iiurlwidth' => $width, |
297 | 'iiurlheight' => $height, |
298 | 'iiurlparam' => $otherParams, |
299 | 'prop' => 'imageinfo' ] ); |
300 | $info = $this->getImageInfo( $data ); |
301 | |
302 | if ( $data && $info && isset( $info['thumburl'] ) ) { |
303 | wfDebug( __METHOD__ . " got remote thumb " . $info['thumburl'] ); |
304 | $result = $info; |
305 | |
306 | return $info['thumburl']; |
307 | } else { |
308 | return false; |
309 | } |
310 | } |
311 | |
312 | /** |
313 | * @param string $name |
314 | * @param int $width |
315 | * @param int $height |
316 | * @param string $otherParams |
317 | * @param string|null $lang Language code for language of error |
318 | * @return MediaTransformError|false |
319 | * @since 1.22 |
320 | */ |
321 | public function getThumbError( |
322 | $name, $width = -1, $height = -1, $otherParams = '', $lang = null |
323 | ) { |
324 | $data = $this->fetchImageQuery( [ |
325 | 'titles' => 'File:' . $name, |
326 | 'iiprop' => self::getIIProps(), |
327 | 'iiurlwidth' => $width, |
328 | 'iiurlheight' => $height, |
329 | 'iiurlparam' => $otherParams, |
330 | 'prop' => 'imageinfo', |
331 | 'uselang' => $lang, |
332 | ] ); |
333 | $info = $this->getImageInfo( $data ); |
334 | |
335 | if ( $data && $info && isset( $info['thumberror'] ) ) { |
336 | wfDebug( __METHOD__ . " got remote thumb error " . $info['thumberror'] ); |
337 | |
338 | return new MediaTransformError( |
339 | 'thumbnail_error_remote', |
340 | $width, |
341 | $height, |
342 | $this->getDisplayName(), |
343 | $info['thumberror'] // already parsed message from foreign repo |
344 | ); |
345 | } else { |
346 | return false; |
347 | } |
348 | } |
349 | |
350 | /** |
351 | * Return the imageurl from cache if possible |
352 | * |
353 | * If the url has been requested today, get it from cache |
354 | * Otherwise retrieve remote thumb url, check for local file. |
355 | * |
356 | * @param string $name Is a dbkey form of a title |
357 | * @param int $width |
358 | * @param int $height |
359 | * @param string $params Other rendering parameters (page number, etc) |
360 | * from handler's makeParamString. |
361 | * @return string|false |
362 | */ |
363 | public function getThumbUrlFromCache( $name, $width, $height, $params = "" ) { |
364 | // We can't check the local cache using FileRepo functions because |
365 | // we override fileExistsBatch(). We have to use the FileBackend directly. |
366 | $backend = $this->getBackend(); // convenience |
367 | |
368 | if ( !$this->canCacheThumbs() ) { |
369 | $result = null; // can't pass "null" by reference, but it's ok as default value |
370 | |
371 | return $this->getThumbUrl( $name, $width, $height, $result, $params ); |
372 | } |
373 | |
374 | $key = $this->getLocalCacheKey( 'file-thumb-url', sha1( $name ) ); |
375 | $sizekey = "$width:$height:$params"; |
376 | |
377 | /* Get the array of urls that we already know */ |
378 | $knownThumbUrls = $this->wanCache->get( $key ); |
379 | if ( !$knownThumbUrls ) { |
380 | /* No knownThumbUrls for this file */ |
381 | $knownThumbUrls = []; |
382 | } elseif ( isset( $knownThumbUrls[$sizekey] ) ) { |
383 | wfDebug( __METHOD__ . ': Got thumburl from local cache: ' . |
384 | "{$knownThumbUrls[$sizekey]}" ); |
385 | |
386 | return $knownThumbUrls[$sizekey]; |
387 | } |
388 | |
389 | $metadata = null; |
390 | $foreignUrl = $this->getThumbUrl( $name, $width, $height, $metadata, $params ); |
391 | |
392 | if ( !$foreignUrl ) { |
393 | wfDebug( __METHOD__ . " Could not find thumburl" ); |
394 | |
395 | return false; |
396 | } |
397 | |
398 | // We need the same filename as the remote one :) |
399 | $fileName = rawurldecode( pathinfo( $foreignUrl, PATHINFO_BASENAME ) ); |
400 | if ( !$this->validateFilename( $fileName ) ) { |
401 | wfDebug( __METHOD__ . " The deduced filename $fileName is not safe" ); |
402 | |
403 | return false; |
404 | } |
405 | $localPath = $this->getZonePath( 'thumb' ) . "/" . $this->getHashPath( $name ) . $name; |
406 | $localFilename = $localPath . "/" . $fileName; |
407 | $localUrl = $this->getZoneUrl( 'thumb' ) . "/" . $this->getHashPath( $name ) . |
408 | rawurlencode( $name ) . "/" . rawurlencode( $fileName ); |
409 | |
410 | if ( $backend->fileExists( [ 'src' => $localFilename ] ) |
411 | && isset( $metadata['timestamp'] ) |
412 | ) { |
413 | wfDebug( __METHOD__ . " Thumbnail was already downloaded before" ); |
414 | $modified = (int)wfTimestamp( TS_UNIX, $backend->getFileTimestamp( [ 'src' => $localFilename ] ) ); |
415 | $remoteModified = (int)wfTimestamp( TS_UNIX, $metadata['timestamp'] ); |
416 | $current = (int)wfTimestamp( TS_UNIX ); |
417 | $diff = abs( $modified - $current ); |
418 | if ( $remoteModified < $modified && $diff < $this->fileCacheExpiry ) { |
419 | /* Use our current and already downloaded thumbnail */ |
420 | $knownThumbUrls[$sizekey] = $localUrl; |
421 | $this->wanCache->set( $key, $knownThumbUrls, $this->apiThumbCacheExpiry ); |
422 | |
423 | return $localUrl; |
424 | } |
425 | /* There is a new Commons file, or existing thumbnail older than a month */ |
426 | } |
427 | |
428 | $thumb = self::httpGet( $foreignUrl, 'default', [], $mtime ); |
429 | if ( !$thumb ) { |
430 | wfDebug( __METHOD__ . " Could not download thumb" ); |
431 | |
432 | return false; |
433 | } |
434 | |
435 | # @todo FIXME: Delete old thumbs that aren't being used. Maintenance script? |
436 | $backend->prepare( [ 'dir' => dirname( $localFilename ) ] ); |
437 | $params = [ 'dst' => $localFilename, 'content' => $thumb ]; |
438 | if ( !$backend->quickCreate( $params )->isOK() ) { |
439 | wfDebug( __METHOD__ . " could not write to thumb path '$localFilename'" ); |
440 | |
441 | return $foreignUrl; |
442 | } |
443 | $knownThumbUrls[$sizekey] = $localUrl; |
444 | |
445 | $ttl = $mtime |
446 | ? $this->wanCache->adaptiveTTL( $mtime, $this->apiThumbCacheExpiry ) |
447 | : $this->apiThumbCacheExpiry; |
448 | $this->wanCache->set( $key, $knownThumbUrls, $ttl ); |
449 | wfDebug( __METHOD__ . " got local thumb $localUrl, saving to cache" ); |
450 | |
451 | return $localUrl; |
452 | } |
453 | |
454 | /** |
455 | * @see FileRepo::getZoneUrl() |
456 | * @param string $zone |
457 | * @param string|null $ext Optional file extension |
458 | * @return string |
459 | */ |
460 | public function getZoneUrl( $zone, $ext = null ) { |
461 | switch ( $zone ) { |
462 | case 'public': |
463 | return $this->url; |
464 | case 'thumb': |
465 | return $this->thumbUrl; |
466 | default: |
467 | return parent::getZoneUrl( $zone, $ext ); |
468 | } |
469 | } |
470 | |
471 | /** |
472 | * Get the local directory corresponding to one of the basic zones |
473 | * @param string $zone |
474 | * @return null|string|false |
475 | */ |
476 | public function getZonePath( $zone ) { |
477 | $supported = [ 'public', 'thumb' ]; |
478 | if ( in_array( $zone, $supported ) ) { |
479 | return parent::getZonePath( $zone ); |
480 | } |
481 | |
482 | return false; |
483 | } |
484 | |
485 | /** |
486 | * Are we locally caching the thumbnails? |
487 | * @return bool |
488 | */ |
489 | public function canCacheThumbs() { |
490 | return ( $this->apiThumbCacheExpiry > 0 ); |
491 | } |
492 | |
493 | /** |
494 | * The user agent the ForeignAPIRepo will use. |
495 | * @return string |
496 | */ |
497 | public static function getUserAgent() { |
498 | return MediaWikiServices::getInstance()->getHttpRequestFactory()->getUserAgent() . |
499 | " ForeignAPIRepo/" . self::VERSION; |
500 | } |
501 | |
502 | /** |
503 | * Get information about the repo - overrides/extends the parent |
504 | * class's information. |
505 | * @return array |
506 | * @since 1.22 |
507 | */ |
508 | public function getInfo() { |
509 | $info = parent::getInfo(); |
510 | $info['apiurl'] = $this->mApiBase; |
511 | |
512 | $query = [ |
513 | 'format' => 'json', |
514 | 'action' => 'query', |
515 | 'meta' => 'siteinfo', |
516 | 'siprop' => 'general', |
517 | ]; |
518 | |
519 | $data = $this->httpGetCached( 'SiteInfo', $query, 7200 ); |
520 | |
521 | if ( $data ) { |
522 | $siteInfo = FormatJson::decode( $data, true ); |
523 | $general = $siteInfo['query']['general']; |
524 | |
525 | $info['articlepath'] = $general['articlepath']; |
526 | $info['server'] = $general['server']; |
527 | if ( !isset( $info['favicon'] ) && isset( $general['favicon'] ) ) { |
528 | $info['favicon'] = $general['favicon']; |
529 | } |
530 | } |
531 | |
532 | return $info; |
533 | } |
534 | |
535 | /** |
536 | * @param string $url |
537 | * @param string $timeout |
538 | * @param array $options |
539 | * @param int|false &$mtime Resulting Last-Modified UNIX timestamp if received |
540 | * @return string|false |
541 | */ |
542 | public static function httpGet( |
543 | $url, $timeout = 'default', $options = [], &$mtime = false |
544 | ) { |
545 | $options['timeout'] = $timeout; |
546 | $url = MediaWikiServices::getInstance()->getUrlUtils() |
547 | ->expand( $url, PROTO_HTTP ); |
548 | wfDebug( "ForeignAPIRepo: HTTP GET: $url" ); |
549 | if ( !$url ) { |
550 | return false; |
551 | } |
552 | $options['method'] = "GET"; |
553 | |
554 | if ( !isset( $options['timeout'] ) ) { |
555 | $options['timeout'] = 'default'; |
556 | } |
557 | |
558 | $options['userAgent'] = self::getUserAgent(); |
559 | |
560 | $req = MediaWikiServices::getInstance()->getHttpRequestFactory() |
561 | ->create( $url, $options, __METHOD__ ); |
562 | $status = $req->execute(); |
563 | |
564 | if ( $status->isOK() ) { |
565 | $lmod = $req->getResponseHeader( 'Last-Modified' ); |
566 | $mtime = $lmod ? (int)wfTimestamp( TS_UNIX, $lmod ) : false; |
567 | |
568 | return $req->getContent(); |
569 | } else { |
570 | $logger = LoggerFactory::getInstance( 'http' ); |
571 | $logger->warning( |
572 | $status->getWikiText( false, false, 'en' ), |
573 | [ 'caller' => 'ForeignAPIRepo::httpGet' ] |
574 | ); |
575 | |
576 | return false; |
577 | } |
578 | } |
579 | |
580 | /** |
581 | * @return string |
582 | * @since 1.23 |
583 | */ |
584 | protected static function getIIProps() { |
585 | return implode( '|', self::IMAGE_INFO_PROPS ); |
586 | } |
587 | |
588 | /** |
589 | * HTTP GET request to a mediawiki API (with caching) |
590 | * @param string $attribute Used in cache key creation, mostly |
591 | * @param array $query The query parameters for the API request |
592 | * @param int $cacheTTL Time to live for the memcached caching |
593 | * @return string|null |
594 | */ |
595 | public function httpGetCached( $attribute, $query, $cacheTTL = 3600 ) { |
596 | if ( $this->mApiBase ) { |
597 | $url = wfAppendQuery( $this->mApiBase, $query ); |
598 | } else { |
599 | $url = $this->makeUrl( $query, 'api' ); |
600 | } |
601 | |
602 | return $this->wanCache->getWithSetCallback( |
603 | // Allow reusing the same cached data across wikis (T285271). |
604 | // This does not use getSharedCacheKey() because caching here |
605 | // is transparent to client wikis (which are not expected to issue purges). |
606 | $this->wanCache->makeGlobalKey( "filerepo-$attribute", sha1( $url ) ), |
607 | $cacheTTL, |
608 | function ( $curValue, &$ttl ) use ( $url ) { |
609 | $html = self::httpGet( $url, 'default', [], $mtime ); |
610 | // FIXME: This should use the mtime from the api response body |
611 | // not the mtime from the last-modified header which usually is not set. |
612 | if ( $html !== false ) { |
613 | $ttl = $mtime ? $this->wanCache->adaptiveTTL( $mtime, $ttl ) : $ttl; |
614 | } else { |
615 | $ttl = $this->wanCache->adaptiveTTL( $mtime, $ttl ); |
616 | $html = null; // caches negatives |
617 | } |
618 | |
619 | return $html; |
620 | }, |
621 | [ 'pcGroup' => 'http-get:3', 'pcTTL' => WANObjectCache::TTL_PROC_LONG ] |
622 | ); |
623 | } |
624 | |
625 | /** |
626 | * @param callable $callback |
627 | * @return never |
628 | */ |
629 | public function enumFiles( $callback ) { |
630 | throw new RuntimeException( 'enumFiles is not supported by ' . static::class ); |
631 | } |
632 | |
633 | /** |
634 | * @return never |
635 | */ |
636 | protected function assertWritableRepo() { |
637 | throw new LogicException( static::class . ': write operations are not supported.' ); |
638 | } |
639 | } |
640 | |
641 | /** @deprecated class alias since 1.44 */ |
642 | class_alias( ForeignAPIRepo::class, 'ForeignAPIRepo' ); |