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