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