Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/filerepo/ForeignAPIRepo.php (7 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Foreign repository accessible through api.php requests.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup FileRepo
22
 */
23
24
use MediaWiki\Logger\LoggerFactory;
25
26
/**
27
 * A foreign repository with a remote MediaWiki with an API thingy
28
 *
29
 * Example config:
30
 *
31
 * $wgForeignFileRepos[] = [
32
 *   'class'                  => 'ForeignAPIRepo',
33
 *   'name'                   => 'shared',
34
 *   'apibase'                => 'https://en.wikipedia.org/w/api.php',
35
 *   'fetchDescription'       => true, // Optional
36
 *   'descriptionCacheExpiry' => 3600,
37
 * ];
38
 *
39
 * @ingroup FileRepo
40
 */
41
class ForeignAPIRepo extends FileRepo {
42
	/* This version string is used in the user agent for requests and will help
43
	 * server maintainers in identify ForeignAPI usage.
44
	 * Update the version every time you make breaking or significant changes. */
45
	const VERSION = "2.1";
46
47
	/**
48
	 * List of iiprop values for the thumbnail fetch queries.
49
	 * @since 1.23
50
	 */
51
	protected static $imageInfoProps = [
52
		'url',
53
		'timestamp',
54
	];
55
56
	protected $fileFactory = [ 'ForeignAPIFile', 'newFromTitle' ];
57
	/** @var int Check back with Commons after this expiry */
58
	protected $apiThumbCacheExpiry = 86400; // 1 day (24*3600)
59
60
	/** @var int Redownload thumbnail files after this expiry */
61
	protected $fileCacheExpiry = 2592000; // 1 month (30*24*3600)
62
63
	/** @var array */
64
	protected $mFileExists = [];
65
66
	/** @var string */
67
	private $mApiBase;
68
69
	/**
70
	 * @param array|null $info
71
	 */
72
	function __construct( $info ) {
73
		global $wgLocalFileRepo;
74
		parent::__construct( $info );
75
76
		// https://commons.wikimedia.org/w/api.php
77
		$this->mApiBase = isset( $info['apibase'] ) ? $info['apibase'] : null;
78
79
		if ( isset( $info['apiThumbCacheExpiry'] ) ) {
80
			$this->apiThumbCacheExpiry = $info['apiThumbCacheExpiry'];
81
		}
82
		if ( isset( $info['fileCacheExpiry'] ) ) {
83
			$this->fileCacheExpiry = $info['fileCacheExpiry'];
84
		}
85
		if ( !$this->scriptDirUrl ) {
86
			// hack for description fetches
87
			$this->scriptDirUrl = dirname( $this->mApiBase );
88
		}
89
		// If we can cache thumbs we can guess sane defaults for these
90
		if ( $this->canCacheThumbs() && !$this->url ) {
91
			$this->url = $wgLocalFileRepo['url'];
92
		}
93
		if ( $this->canCacheThumbs() && !$this->thumbUrl ) {
94
			$this->thumbUrl = $this->url . '/thumb';
95
		}
96
	}
97
98
	/**
99
	 * @return string
100
	 * @since 1.22
101
	 */
102
	function getApiUrl() {
103
		return $this->mApiBase;
104
	}
105
106
	/**
107
	 * Per docs in FileRepo, this needs to return false if we don't support versioned
108
	 * files. Well, we don't.
109
	 *
110
	 * @param Title $title
111
	 * @param string|bool $time
112
	 * @return File
113
	 */
114
	function newFile( $title, $time = false ) {
115
		if ( $time ) {
116
			return false;
117
		}
118
119
		return parent::newFile( $title, $time );
120
	}
121
122
	/**
123
	 * @param array $files
124
	 * @return array
125
	 */
126
	function fileExistsBatch( array $files ) {
127
		$results = [];
128
		foreach ( $files as $k => $f ) {
129
			if ( isset( $this->mFileExists[$f] ) ) {
130
				$results[$k] = $this->mFileExists[$f];
131
				unset( $files[$k] );
132
			} elseif ( self::isVirtualUrl( $f ) ) {
133
				# @todo FIXME: We need to be able to handle virtual
134
				# URLs better, at least when we know they refer to the
135
				# same repo.
136
				$results[$k] = false;
137
				unset( $files[$k] );
138
			} elseif ( FileBackend::isStoragePath( $f ) ) {
139
				$results[$k] = false;
140
				unset( $files[$k] );
141
				wfWarn( "Got mwstore:// path '$f'." );
142
			}
143
		}
144
145
		$data = $this->fetchImageQuery( [
146
			'titles' => implode( $files, '|' ),
147
			'prop' => 'imageinfo' ]
148
		);
149
150
		if ( isset( $data['query']['pages'] ) ) {
151
			# First, get results from the query. Note we only care whether the image exists,
152
			# not whether it has a description page.
153
			foreach ( $data['query']['pages'] as $p ) {
0 ignored issues
show
The expression $data['query']['pages'] of type string is not traversable.
Loading history...
154
				$this->mFileExists[$p['title']] = ( $p['imagerepository'] !== '' );
155
			}
156
			# Second, copy the results to any redirects that were queried
157 View Code Duplication
			if ( isset( $data['query']['redirects'] ) ) {
158
				foreach ( $data['query']['redirects'] as $r ) {
0 ignored issues
show
The expression $data['query']['redirects'] of type string is not traversable.
Loading history...
159
					$this->mFileExists[$r['from']] = $this->mFileExists[$r['to']];
160
				}
161
			}
162
			# Third, copy the results to any non-normalized titles that were queried
163 View Code Duplication
			if ( isset( $data['query']['normalized'] ) ) {
164
				foreach ( $data['query']['normalized'] as $n ) {
0 ignored issues
show
The expression $data['query']['normalized'] of type string is not traversable.
Loading history...
165
					$this->mFileExists[$n['from']] = $this->mFileExists[$n['to']];
166
				}
167
			}
168
			# Finally, copy the results to the output
169
			foreach ( $files as $key => $file ) {
170
				$results[$key] = $this->mFileExists[$file];
171
			}
172
		}
173
174
		return $results;
175
	}
176
177
	/**
178
	 * @param string $virtualUrl
179
	 * @return bool
180
	 */
181
	function getFileProps( $virtualUrl ) {
182
		return false;
183
	}
184
185
	/**
186
	 * @param array $query
187
	 * @return string
188
	 */
189
	function fetchImageQuery( $query ) {
190
		global $wgLanguageCode;
191
192
		$query = array_merge( $query,
193
			[
194
				'format' => 'json',
195
				'action' => 'query',
196
				'redirects' => 'true'
197
			] );
198
199
		if ( !isset( $query['uselang'] ) ) { // uselang is unset or null
200
			$query['uselang'] = $wgLanguageCode;
201
		}
202
203
		$data = $this->httpGetCached( 'Metadata', $query );
204
205
		if ( $data ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
206
			return FormatJson::decode( $data, true );
207
		} else {
208
			return null;
209
		}
210
	}
211
212
	/**
213
	 * @param array $data
214
	 * @return bool|array
215
	 */
216
	function getImageInfo( $data ) {
217
		if ( $data && isset( $data['query']['pages'] ) ) {
218
			foreach ( $data['query']['pages'] as $info ) {
219
				if ( isset( $info['imageinfo'][0] ) ) {
220
					$return = $info['imageinfo'][0];
221
					if ( isset( $info['pageid'] ) ) {
222
						$return['pageid'] = $info['pageid'];
223
					}
224
					return $return;
225
				}
226
			}
227
		}
228
229
		return false;
230
	}
231
232
	/**
233
	 * @param string $hash
234
	 * @return array
235
	 */
236
	function findBySha1( $hash ) {
237
		$results = $this->fetchImageQuery( [
238
			'aisha1base36' => $hash,
239
			'aiprop' => ForeignAPIFile::getProps(),
240
			'list' => 'allimages',
241
		] );
242
		$ret = [];
243
		if ( isset( $results['query']['allimages'] ) ) {
244
			foreach ( $results['query']['allimages'] as $img ) {
0 ignored issues
show
The expression $results['query']['allimages'] of type string is not traversable.
Loading history...
245
				// 1.14 was broken, doesn't return name attribute
246
				if ( !isset( $img['name'] ) ) {
247
					continue;
248
				}
249
				$ret[] = new ForeignAPIFile( Title::makeTitle( NS_FILE, $img['name'] ), $this, $img );
250
			}
251
		}
252
253
		return $ret;
254
	}
255
256
	/**
257
	 * @param string $name
258
	 * @param int $width
259
	 * @param int $height
260
	 * @param array $result Out parameter that will be changed by the function.
261
	 * @param string $otherParams
262
	 *
263
	 * @return bool
264
	 */
265
	function getThumbUrl( $name, $width = -1, $height = -1, &$result = null, $otherParams = '' ) {
266
		$data = $this->fetchImageQuery( [
267
			'titles' => 'File:' . $name,
268
			'iiprop' => self::getIIProps(),
269
			'iiurlwidth' => $width,
270
			'iiurlheight' => $height,
271
			'iiurlparam' => $otherParams,
272
			'prop' => 'imageinfo' ] );
273
		$info = $this->getImageInfo( $data );
274
275 View Code Duplication
		if ( $data && $info && isset( $info['thumburl'] ) ) {
276
			wfDebug( __METHOD__ . " got remote thumb " . $info['thumburl'] . "\n" );
277
			$result = $info;
278
279
			return $info['thumburl'];
280
		} else {
281
			return false;
282
		}
283
	}
284
285
	/**
286
	 * @param string $name
287
	 * @param int $width
288
	 * @param int $height
289
	 * @param string $otherParams
290
	 * @param string $lang Language code for language of error
291
	 * @return bool|MediaTransformError
292
	 * @since 1.22
293
	 */
294
	function getThumbError( $name, $width = -1, $height = -1, $otherParams = '', $lang = null ) {
295
		$data = $this->fetchImageQuery( [
296
			'titles' => 'File:' . $name,
297
			'iiprop' => self::getIIProps(),
298
			'iiurlwidth' => $width,
299
			'iiurlheight' => $height,
300
			'iiurlparam' => $otherParams,
301
			'prop' => 'imageinfo',
302
			'uselang' => $lang,
303
		] );
304
		$info = $this->getImageInfo( $data );
305
306 View Code Duplication
		if ( $data && $info && isset( $info['thumberror'] ) ) {
307
			wfDebug( __METHOD__ . " got remote thumb error " . $info['thumberror'] . "\n" );
308
309
			return new MediaTransformError(
310
				'thumbnail_error_remote',
311
				$width,
312
				$height,
313
				$this->getDisplayName(),
314
				$info['thumberror'] // already parsed message from foreign repo
315
			);
316
		} else {
317
			return false;
318
		}
319
	}
320
321
	/**
322
	 * Return the imageurl from cache if possible
323
	 *
324
	 * If the url has been requested today, get it from cache
325
	 * Otherwise retrieve remote thumb url, check for local file.
326
	 *
327
	 * @param string $name Is a dbkey form of a title
328
	 * @param int $width
329
	 * @param int $height
330
	 * @param string $params Other rendering parameters (page number, etc)
331
	 *   from handler's makeParamString.
332
	 * @return bool|string
333
	 */
334
	function getThumbUrlFromCache( $name, $width, $height, $params = "" ) {
335
		$cache = ObjectCache::getMainWANInstance();
336
		// We can't check the local cache using FileRepo functions because
337
		// we override fileExistsBatch(). We have to use the FileBackend directly.
338
		$backend = $this->getBackend(); // convenience
339
340
		if ( !$this->canCacheThumbs() ) {
341
			$result = null; // can't pass "null" by reference, but it's ok as default value
342
			return $this->getThumbUrl( $name, $width, $height, $result, $params );
343
		}
344
		$key = $this->getLocalCacheKey( 'ForeignAPIRepo', 'ThumbUrl', $name );
345
		$sizekey = "$width:$height:$params";
346
347
		/* Get the array of urls that we already know */
348
		$knownThumbUrls = $cache->get( $key );
349
		if ( !$knownThumbUrls ) {
350
			/* No knownThumbUrls for this file */
351
			$knownThumbUrls = [];
352
		} else {
353
			if ( isset( $knownThumbUrls[$sizekey] ) ) {
354
				wfDebug( __METHOD__ . ': Got thumburl from local cache: ' .
355
					"{$knownThumbUrls[$sizekey]} \n" );
356
357
				return $knownThumbUrls[$sizekey];
358
			}
359
			/* This size is not yet known */
360
		}
361
362
		$metadata = null;
363
		$foreignUrl = $this->getThumbUrl( $name, $width, $height, $metadata, $params );
364
365
		if ( !$foreignUrl ) {
366
			wfDebug( __METHOD__ . " Could not find thumburl\n" );
367
368
			return false;
369
		}
370
371
		// We need the same filename as the remote one :)
372
		$fileName = rawurldecode( pathinfo( $foreignUrl, PATHINFO_BASENAME ) );
373
		if ( !$this->validateFilename( $fileName ) ) {
374
			wfDebug( __METHOD__ . " The deduced filename $fileName is not safe\n" );
375
376
			return false;
377
		}
378
		$localPath = $this->getZonePath( 'thumb' ) . "/" . $this->getHashPath( $name ) . $name;
379
		$localFilename = $localPath . "/" . $fileName;
380
		$localUrl = $this->getZoneUrl( 'thumb' ) . "/" . $this->getHashPath( $name ) .
381
			rawurlencode( $name ) . "/" . rawurlencode( $fileName );
382
383
		if ( $backend->fileExists( [ 'src' => $localFilename ] )
384
			&& isset( $metadata['timestamp'] )
385
		) {
386
			wfDebug( __METHOD__ . " Thumbnail was already downloaded before\n" );
387
			$modified = $backend->getFileTimestamp( [ 'src' => $localFilename ] );
388
			$remoteModified = strtotime( $metadata['timestamp'] );
389
			$current = time();
390
			$diff = abs( $modified - $current );
391
			if ( $remoteModified < $modified && $diff < $this->fileCacheExpiry ) {
392
				/* Use our current and already downloaded thumbnail */
393
				$knownThumbUrls[$sizekey] = $localUrl;
394
				$cache->set( $key, $knownThumbUrls, $this->apiThumbCacheExpiry );
395
396
				return $localUrl;
397
			}
398
			/* There is a new Commons file, or existing thumbnail older than a month */
399
		}
400
401
		$thumb = self::httpGet( $foreignUrl, 'default', [], $mtime );
402
		if ( !$thumb ) {
403
			wfDebug( __METHOD__ . " Could not download thumb\n" );
404
405
			return false;
406
		}
407
408
		# @todo FIXME: Delete old thumbs that aren't being used. Maintenance script?
409
		$backend->prepare( [ 'dir' => dirname( $localFilename ) ] );
410
		$params = [ 'dst' => $localFilename, 'content' => $thumb ];
411
		if ( !$backend->quickCreate( $params )->isOK() ) {
412
			wfDebug( __METHOD__ . " could not write to thumb path '$localFilename'\n" );
413
414
			return $foreignUrl;
415
		}
416
		$knownThumbUrls[$sizekey] = $localUrl;
417
418
		$ttl = $mtime
419
			? $cache->adaptiveTTL( $mtime, $this->apiThumbCacheExpiry )
420
			: $this->apiThumbCacheExpiry;
421
		$cache->set( $key, $knownThumbUrls, $ttl );
422
		wfDebug( __METHOD__ . " got local thumb $localUrl, saving to cache \n" );
423
424
		return $localUrl;
425
	}
426
427
	/**
428
	 * @see FileRepo::getZoneUrl()
429
	 * @param string $zone
430
	 * @param string|null $ext Optional file extension
431
	 * @return string
432
	 */
433
	function getZoneUrl( $zone, $ext = null ) {
434
		switch ( $zone ) {
435
			case 'public':
436
				return $this->url;
437
			case 'thumb':
438
				return $this->thumbUrl;
439
			default:
440
				return parent::getZoneUrl( $zone, $ext );
441
		}
442
	}
443
444
	/**
445
	 * Get the local directory corresponding to one of the basic zones
446
	 * @param string $zone
447
	 * @return bool|null|string
448
	 */
449
	function getZonePath( $zone ) {
450
		$supported = [ 'public', 'thumb' ];
451
		if ( in_array( $zone, $supported ) ) {
452
			return parent::getZonePath( $zone );
453
		}
454
455
		return false;
456
	}
457
458
	/**
459
	 * Are we locally caching the thumbnails?
460
	 * @return bool
461
	 */
462
	public function canCacheThumbs() {
463
		return ( $this->apiThumbCacheExpiry > 0 );
464
	}
465
466
	/**
467
	 * The user agent the ForeignAPIRepo will use.
468
	 * @return string
469
	 */
470
	public static function getUserAgent() {
471
		return Http::userAgent() . " ForeignAPIRepo/" . self::VERSION;
472
	}
473
474
	/**
475
	 * Get information about the repo - overrides/extends the parent
476
	 * class's information.
477
	 * @return array
478
	 * @since 1.22
479
	 */
480
	function getInfo() {
481
		$info = parent::getInfo();
482
		$info['apiurl'] = $this->getApiUrl();
483
484
		$query = [
485
			'format' => 'json',
486
			'action' => 'query',
487
			'meta' => 'siteinfo',
488
			'siprop' => 'general',
489
		];
490
491
		$data = $this->httpGetCached( 'SiteInfo', $query, 7200 );
492
493
		if ( $data ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
494
			$siteInfo = FormatJson::decode( $data, true );
495
			$general = $siteInfo['query']['general'];
496
497
			$info['articlepath'] = $general['articlepath'];
498
			$info['server'] = $general['server'];
499
500
			if ( isset( $general['favicon'] ) ) {
501
				$info['favicon'] = $general['favicon'];
502
			}
503
		}
504
505
		return $info;
506
	}
507
508
	/**
509
	 * Like a Http:get request, but with custom User-Agent.
510
	 * @see Http::get
511
	 * @param string $url
512
	 * @param string $timeout
513
	 * @param array $options
514
	 * @param integer|bool &$mtime Resulting Last-Modified UNIX timestamp if received
515
	 * @return bool|string
516
	 */
517
	public static function httpGet(
518
		$url, $timeout = 'default', $options = [], &$mtime = false
519
	) {
520
		$options['timeout'] = $timeout;
521
		/* Http::get */
522
		$url = wfExpandUrl( $url, PROTO_HTTP );
523
		wfDebug( "ForeignAPIRepo: HTTP GET: $url\n" );
524
		$options['method'] = "GET";
525
526
		if ( !isset( $options['timeout'] ) ) {
527
			$options['timeout'] = 'default';
528
		}
529
530
		$req = MWHttpRequest::factory( $url, $options, __METHOD__ );
0 ignored issues
show
It seems like $url defined by wfExpandUrl($url, PROTO_HTTP) on line 522 can also be of type false; however, MWHttpRequest::factory() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
531
		$req->setUserAgent( ForeignAPIRepo::getUserAgent() );
532
		$status = $req->execute();
533
534
		if ( $status->isOK() ) {
535
			$lmod = $req->getResponseHeader( 'Last-Modified' );
536
			$mtime = $lmod ? wfTimestamp( TS_UNIX, $lmod ) : false;
537
538
			return $req->getContent();
539
		} else {
540
			$logger = LoggerFactory::getInstance( 'http' );
541
			$logger->warning(
542
				$status->getWikiText( false, false, 'en' ),
543
				[ 'caller' => 'ForeignAPIRepo::httpGet' ]
544
			);
545
546
			return false;
547
		}
548
	}
549
550
	/**
551
	 * @return string
552
	 * @since 1.23
553
	 */
554
	protected static function getIIProps() {
555
		return implode( '|', self::$imageInfoProps );
556
	}
557
558
	/**
559
	 * HTTP GET request to a mediawiki API (with caching)
560
	 * @param string $target Used in cache key creation, mostly
561
	 * @param array $query The query parameters for the API request
562
	 * @param int $cacheTTL Time to live for the memcached caching
563
	 * @return string|null
564
	 */
565
	public function httpGetCached( $target, $query, $cacheTTL = 3600 ) {
566
		if ( $this->mApiBase ) {
567
			$url = wfAppendQuery( $this->mApiBase, $query );
568
		} else {
569
			$url = $this->makeUrl( $query, 'api' );
570
		}
571
572
		$cache = ObjectCache::getMainWANInstance();
573
		return $cache->getWithSetCallback(
574
			$this->getLocalCacheKey( get_class( $this ), $target, md5( $url ) ),
575
			$cacheTTL,
576
			function ( $curValue, &$ttl ) use ( $url, $cache ) {
577
				$html = self::httpGet( $url, 'default', [], $mtime );
578
				if ( $html !== false ) {
579
					$ttl = $mtime ? $cache->adaptiveTTL( $mtime, $ttl ) : $ttl;
580
				} else {
581
					$ttl = $cache->adaptiveTTL( $mtime, $ttl );
582
					$html = null; // caches negatives
583
				}
584
585
				return $html;
586
			},
587
			[ 'pcTTL' => $cache::TTL_PROC_LONG ]
588
		);
589
	}
590
591
	/**
592
	 * @param callable $callback
593
	 * @throws MWException
594
	 */
595
	function enumFiles( $callback ) {
596
		throw new MWException( 'enumFiles is not supported by ' . get_class( $this ) );
597
	}
598
599
	/**
600
	 * @throws MWException
601
	 */
602
	protected function assertWritableRepo() {
603
		throw new MWException( get_class( $this ) . ': write operations are not supported.' );
604
	}
605
}
606