MediaWiki master
UploadFromUrl.php
Go to the documentation of this file.
1<?php
31
40 protected $mUrl;
41
42 protected $mTempPath;
43 protected $mTmpHandle;
44
45 protected static $allowedUrls = [];
46
56 public static function isAllowed( Authority $performer ) {
57 if ( !$performer->isAllowed( 'upload_by_url' ) ) {
58 return 'upload_by_url';
59 }
60
61 return parent::isAllowed( $performer );
62 }
63
68 public static function isEnabled() {
69 $allowCopyUploads = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::AllowCopyUploads );
70
71 return $allowCopyUploads && parent::isEnabled();
72 }
73
82 public static function isAllowedHost( $url ) {
83 $domains = self::getAllowedHosts();
84 if ( !count( $domains ) ) {
85 return true;
86 }
87 $parsedUrl = wfParseUrl( $url );
88 if ( !$parsedUrl ) {
89 return false;
90 }
91 $valid = false;
92 foreach ( $domains as $domain ) {
93 // See if the domain for the upload matches this allowed domain
94 $domainPieces = explode( '.', $domain );
95 $uploadDomainPieces = explode( '.', $parsedUrl['host'] );
96 if ( count( $domainPieces ) === count( $uploadDomainPieces ) ) {
97 $valid = true;
98 // See if all the pieces match or not (excluding wildcards)
99 foreach ( $domainPieces as $index => $piece ) {
100 if ( $piece !== '*' && $piece !== $uploadDomainPieces[$index] ) {
101 $valid = false;
102 }
103 }
104 if ( $valid ) {
105 // We found a match, so quit comparing against the list
106 break;
107 }
108 }
109 /* Non-wildcard test
110 if ( $parsedUrl['host'] === $domain ) {
111 $valid = true;
112 break;
113 }
114 */
115 }
116
117 return $valid;
118 }
119
129 public static function getCacheKey( $params ) {
130 if ( !isset( $params['filename'] ) || !isset( $params['url'] ) ) {
131 return "";
132 } else {
133 // We use sha1 here to ensure we have a fixed-length string of printable
134 // characters. There is no cryptography involved, so we just need a
135 // relatively fast function.
136 return sha1( sprintf( "%s|||%s", $params['filename'], $params['url'] ) );
137 }
138 }
139
146 public static function getCacheKeyFromRequest( &$request ) {
147 $uploadCacheKey = $request->getText( 'wpCacheKey', $request->getText( 'key', '' ) );
148 if ( $uploadCacheKey !== '' ) {
149 return $uploadCacheKey;
150 }
151 $desiredDestName = $request->getText( 'wpDestFile' );
152 if ( !$desiredDestName ) {
153 $desiredDestName = $request->getText( 'wpUploadFileURL' );
154 }
155 return self::getCacheKey(
156 [
157 'filename' => $desiredDestName,
158 'url' => trim( $request->getVal( 'wpUploadFileURL' ) )
159 ]
160 );
161 }
162
166 private static function getAllowedHosts(): array {
167 $config = MediaWikiServices::getInstance()->getMainConfig();
168 $domains = $config->get( MainConfigNames::CopyUploadsDomains );
169
170 if ( $config->get( MainConfigNames::CopyUploadAllowOnWikiDomainConfig ) ) {
171 $page = wfMessage( 'copyupload-allowed-domains' )->inContentLanguage()->plain();
172
173 foreach ( explode( "\n", $page ) as $line ) {
174 // Strip comments
175 $line = preg_replace( "/^\\s*([^#]*)\\s*((.*)?)$/", "\\1", $line );
176 // Trim whitespace
177 $line = trim( $line );
178
179 if ( $line !== '' ) {
180 $domains[] = $line;
181 }
182 }
183 }
184
185 return $domains;
186 }
187
194 public static function isAllowedUrl( $url ) {
195 if ( !isset( self::$allowedUrls[$url] ) ) {
196 $allowed = true;
197 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
198 ->onIsUploadAllowedFromUrl( $url, $allowed );
199 self::$allowedUrls[$url] = $allowed;
200 }
201
202 return self::$allowedUrls[$url];
203 }
204
209 public function getUrl() {
210 return $this->mUrl;
211 }
212
219 public function initialize( $name, $url ) {
220 $this->mUrl = $url;
221
222 $tempPath = $this->makeTemporaryFile();
223 # File size and removeTempFile will be filled in later
224 $this->initializePathInfo( $name, $tempPath, 0, false );
225 }
226
231 public function initializeFromRequest( &$request ) {
232 $desiredDestName = $request->getText( 'wpDestFile' );
233 if ( !$desiredDestName ) {
234 $desiredDestName = $request->getText( 'wpUploadFileURL' );
235 }
236 $this->initialize(
237 $desiredDestName,
238 trim( $request->getVal( 'wpUploadFileURL' ) )
239 );
240 }
241
246 public static function isValidRequest( $request ) {
247 $user = RequestContext::getMain()->getUser();
248
249 $url = $request->getVal( 'wpUploadFileURL' );
250
251 return $url
252 && MediaWikiServices::getInstance()
253 ->getPermissionManager()
254 ->userHasRight( $user, 'upload_by_url' );
255 }
256
260 public function getSourceType() {
261 return 'url';
262 }
263
271 public function fetchFile( $httpOptions = [] ) {
272 $status = $this->canFetchFile();
273 if ( !$status->isGood() ) {
274 return $status;
275 }
276 return $this->reallyFetchFile( $httpOptions );
277 }
278
284 public function canFetchFile() {
285 if ( !MWHttpRequest::isValidURI( $this->mUrl ) ) {
286 return Status::newFatal( 'http-invalid-url', $this->mUrl );
287 }
288
289 if ( !self::isAllowedHost( $this->mUrl ) ) {
290 return Status::newFatal( 'upload-copy-upload-invalid-domain' );
291 }
292 if ( !self::isAllowedUrl( $this->mUrl ) ) {
293 return Status::newFatal( 'upload-copy-upload-invalid-url' );
294 }
295 return Status::newGood();
296 }
297
303 protected function makeTemporaryFile() {
304 $tmpFile = MediaWikiServices::getInstance()->getTempFSFileFactory()
305 ->newTempFSFile( 'URL', 'urlupload_' );
306 $tmpFile->bind( $this );
307
308 return $tmpFile->getPath();
309 }
310
318 public function saveTempFileChunk( $req, $buffer ) {
319 wfDebugLog( 'fileupload', 'Received chunk of ' . strlen( $buffer ) . ' bytes' );
320 $nbytes = fwrite( $this->mTmpHandle, $buffer );
321
322 if ( $nbytes == strlen( $buffer ) ) {
323 $this->mFileSize += $nbytes;
324 } else {
325 // Well... that's not good!
327 'fileupload',
328 'Short write ' . $nbytes . '/' . strlen( $buffer ) .
329 ' bytes, aborting with ' . $this->mFileSize . ' uploaded so far'
330 );
331 fclose( $this->mTmpHandle );
332 $this->mTmpHandle = false;
333 }
334
335 return $nbytes;
336 }
337
345 protected function reallyFetchFile( $httpOptions = [] ) {
346 $copyUploadProxy = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::CopyUploadProxy );
347 $copyUploadTimeout = MediaWikiServices::getInstance()->getMainConfig()
348 ->get( MainConfigNames::CopyUploadTimeout );
349 if ( $this->mTempPath === false ) {
350 return Status::newFatal( 'tmp-create-error' );
351 }
352
353 // Note the temporary file should already be created by makeTemporaryFile()
354 $this->mTmpHandle = fopen( $this->mTempPath, 'wb' );
355 if ( !$this->mTmpHandle ) {
356 return Status::newFatal( 'tmp-create-error' );
357 }
358 wfDebugLog( 'fileupload', 'Temporary file created "' . $this->mTempPath . '"' );
359
360 $this->mRemoveTempFile = true;
361 $this->mFileSize = 0;
362
363 $options = $httpOptions + [ 'followRedirects' => false ];
364
365 if ( $copyUploadProxy !== false ) {
366 $options['proxy'] = $copyUploadProxy;
367 }
368
369 if ( $copyUploadTimeout && !isset( $options['timeout'] ) ) {
370 $options['timeout'] = $copyUploadTimeout;
371 }
373 'fileupload',
374 'Starting download from "' . $this->mUrl . '" ' .
375 '<' . implode( ',', array_keys( array_filter( $options ) ) ) . '>'
376 );
377
378 // Manually follow any redirects up to the limit and reset the output file before each new request to prevent
379 // capturing the redirect response as part of the file.
380 $attemptsLeft = $options['maxRedirects'] ?? 5;
381 $targetUrl = $this->mUrl;
382 $requestFactory = MediaWikiServices::getInstance()->getHttpRequestFactory();
383 while ( $attemptsLeft > 0 ) {
384 $req = $requestFactory->create( $targetUrl, $options, __METHOD__ );
385 $req->setCallback( [ $this, 'saveTempFileChunk' ] );
386 $status = $req->execute();
387 if ( !$req->isRedirect() ) {
388 break;
389 }
390 $targetUrl = $req->getFinalUrl();
391 // Remove redirect response content from file.
392 ftruncate( $this->mTmpHandle, 0 );
393 rewind( $this->mTmpHandle );
394 $attemptsLeft--;
395 }
396
397 if ( $attemptsLeft == 0 ) {
398 return Status::newFatal( 'upload-too-many-redirects' );
399 }
400
401 if ( $this->mTmpHandle ) {
402 // File got written ok...
403 fclose( $this->mTmpHandle );
404 $this->mTmpHandle = null;
405 } else {
406 // We encountered a write error during the download...
407 return Status::newFatal( 'tmp-write-error' );
408 }
409
410 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable Always set after loop
411 if ( $status->isOK() ) {
412 wfDebugLog( 'fileupload', 'Download by URL completed successfully.' );
413 } else {
414 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable Always set after loop
415 wfDebugLog( 'fileupload', $status->getWikiText( false, false, 'en' ) );
417 'fileupload',
418 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable Always set after loop
419 'Download by URL completed with HTTP status ' . $req->getStatus()
420 );
421 }
422
423 // @phan-suppress-next-line PhanTypeMismatchReturnNullable,PhanPossiblyUndeclaredVariable Always set after loop
424 return $status;
425 }
426}
wfParseUrl( $url)
parse_url() work-alike, but non-broken.
wfDebugLog( $logGroup, $text, $dest='all', array $context=[])
Send a line to a supplementary debug log file, if configured, or main debug log if not.
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
array $params
The job parameters.
getCacheKey()
Get the cache key used to store status.
Group all the pieces relevant to the context of a request into one instance.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
A class containing constants representing the names of configuration variables.
Service locator for MediaWiki core services.
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form,...
Generic operation result class Has warning/error list, boolean status and arbitrary value.
Definition Status.php:54
UploadBase and subclasses are the backend of MediaWiki's file uploads.
Implements uploading from a HTTP resource.
canFetchFile()
verify we can actually download the file
makeTemporaryFile()
Create a new temporary file in the URL subdirectory of wfTempDir().
static isValidRequest( $request)
getUrl()
Get the URL of the file to be uploaded.
static isAllowed(Authority $performer)
Checks if the user is allowed to use the upload-by-URL feature.
initializeFromRequest(&$request)
Entry point for SpecialUpload.
reallyFetchFile( $httpOptions=[])
Download the file, save it to the temporary file and update the file size and set $mRemoveTempFile to...
initialize( $name, $url)
Entry point for API upload.
static getCacheKeyFromRequest(&$request)
Get the caching key from a web request.
fetchFile( $httpOptions=[])
Download the file.
static getCacheKey( $params)
Provides a caching key for an upload from url set of parameters Used to set the status of an async jo...
saveTempFileChunk( $req, $buffer)
Callback: save a chunk of the result of a HTTP request to the temporary file.
static isAllowedHost( $url)
Checks whether the URL is for an allowed host The domains in the allowlist can include wildcard chara...
static isAllowedUrl( $url)
Checks whether the URL is not allowed.
static isEnabled()
Checks if the upload from URL feature is enabled.
This interface represents the authority associated with the current execution context,...
Definition Authority.php:37
isAllowed(string $permission, PermissionStatus $status=null)
Checks whether this authority has the given permission in general.