MediaWiki master
UploadFromUrl.php
Go to the documentation of this file.
1<?php
31
41 protected $mUrl;
42
43 protected $mTempPath;
45 protected $mTmpHandle;
46
48 protected static $allowedUrls = [];
49
59 public static function isAllowed( Authority $performer ) {
60 if ( !$performer->isAllowed( 'upload_by_url' ) ) {
61 return 'upload_by_url';
62 }
63
64 return parent::isAllowed( $performer );
65 }
66
71 public static function isEnabled() {
72 $allowCopyUploads = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::AllowCopyUploads );
73
74 return $allowCopyUploads && parent::isEnabled();
75 }
76
85 public static function isAllowedHost( $url ) {
86 $domains = self::getAllowedHosts();
87 if ( !count( $domains ) ) {
88 return true;
89 }
90 $parsedUrl = wfParseUrl( $url );
91 if ( !$parsedUrl ) {
92 return false;
93 }
94 $valid = false;
95 foreach ( $domains as $domain ) {
96 // See if the domain for the upload matches this allowed domain
97 $domainPieces = explode( '.', $domain );
98 $uploadDomainPieces = explode( '.', $parsedUrl['host'] );
99 if ( count( $domainPieces ) === count( $uploadDomainPieces ) ) {
100 $valid = true;
101 // See if all the pieces match or not (excluding wildcards)
102 foreach ( $domainPieces as $index => $piece ) {
103 if ( $piece !== '*' && $piece !== $uploadDomainPieces[$index] ) {
104 $valid = false;
105 }
106 }
107 if ( $valid ) {
108 // We found a match, so quit comparing against the list
109 break;
110 }
111 }
112 /* Non-wildcard test
113 if ( $parsedUrl['host'] === $domain ) {
114 $valid = true;
115 break;
116 }
117 */
118 }
119
120 return $valid;
121 }
122
132 public static function getCacheKey( $params ) {
133 if ( !isset( $params['filename'] ) || !isset( $params['url'] ) ) {
134 return "";
135 } else {
136 // We use sha1 here to ensure we have a fixed-length string of printable
137 // characters. There is no cryptography involved, so we just need a
138 // relatively fast function.
139 return sha1( sprintf( "%s|||%s", $params['filename'], $params['url'] ) );
140 }
141 }
142
149 public static function getCacheKeyFromRequest( &$request ) {
150 $uploadCacheKey = $request->getText( 'wpCacheKey', $request->getText( 'key', '' ) );
151 if ( $uploadCacheKey !== '' ) {
152 return $uploadCacheKey;
153 }
154 $desiredDestName = $request->getText( 'wpDestFile' );
155 if ( !$desiredDestName ) {
156 $desiredDestName = $request->getText( 'wpUploadFileURL' );
157 }
158 return self::getCacheKey(
159 [
160 'filename' => $desiredDestName,
161 'url' => trim( $request->getVal( 'wpUploadFileURL' ) )
162 ]
163 );
164 }
165
169 private static function getAllowedHosts(): array {
170 $config = MediaWikiServices::getInstance()->getMainConfig();
171 $domains = $config->get( MainConfigNames::CopyUploadsDomains );
172
173 if ( $config->get( MainConfigNames::CopyUploadAllowOnWikiDomainConfig ) ) {
174 $page = wfMessage( 'copyupload-allowed-domains' )->inContentLanguage()->plain();
175
176 foreach ( explode( "\n", $page ) as $line ) {
177 // Strip comments
178 $line = preg_replace( "/^\\s*([^#]*)\\s*((.*)?)$/", "\\1", $line );
179 // Trim whitespace
180 $line = trim( $line );
181
182 if ( $line !== '' ) {
183 $domains[] = $line;
184 }
185 }
186 }
187
188 return $domains;
189 }
190
197 public static function isAllowedUrl( $url ) {
198 if ( !isset( self::$allowedUrls[$url] ) ) {
199 $allowed = true;
200 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
201 ->onIsUploadAllowedFromUrl( $url, $allowed );
202 self::$allowedUrls[$url] = $allowed;
203 }
204
205 return self::$allowedUrls[$url];
206 }
207
212 public function getUrl() {
213 return $this->mUrl;
214 }
215
222 public function initialize( $name, $url ) {
223 $this->mUrl = $url;
224
225 $tempPath = $this->makeTemporaryFile();
226 # File size and removeTempFile will be filled in later
227 $this->initializePathInfo( $name, $tempPath, 0, false );
228 }
229
234 public function initializeFromRequest( &$request ) {
235 $desiredDestName = $request->getText( 'wpDestFile' );
236 if ( !$desiredDestName ) {
237 $desiredDestName = $request->getText( 'wpUploadFileURL' );
238 }
239 $this->initialize(
240 $desiredDestName,
241 trim( $request->getVal( 'wpUploadFileURL' ) )
242 );
243 }
244
249 public static function isValidRequest( $request ) {
250 $user = RequestContext::getMain()->getUser();
251
252 $url = $request->getVal( 'wpUploadFileURL' );
253
254 return $url
255 && MediaWikiServices::getInstance()
256 ->getPermissionManager()
257 ->userHasRight( $user, 'upload_by_url' );
258 }
259
263 public function getSourceType() {
264 return 'url';
265 }
266
274 public function fetchFile( $httpOptions = [] ) {
275 $status = $this->canFetchFile();
276 if ( !$status->isGood() ) {
277 return $status;
278 }
279 return $this->reallyFetchFile( $httpOptions );
280 }
281
287 public function canFetchFile() {
288 if ( !MWHttpRequest::isValidURI( $this->mUrl ) ) {
289 return Status::newFatal( 'http-invalid-url', $this->mUrl );
290 }
291
292 if ( !self::isAllowedHost( $this->mUrl ) ) {
293 return Status::newFatal( 'upload-copy-upload-invalid-domain' );
294 }
295 if ( !self::isAllowedUrl( $this->mUrl ) ) {
296 return Status::newFatal( 'upload-copy-upload-invalid-url' );
297 }
298 return Status::newGood();
299 }
300
306 protected function makeTemporaryFile() {
307 $tmpFile = MediaWikiServices::getInstance()->getTempFSFileFactory()
308 ->newTempFSFile( 'URL', 'urlupload_' );
309 $tmpFile->bind( $this );
310
311 return $tmpFile->getPath();
312 }
313
321 public function saveTempFileChunk( $req, $buffer ) {
322 wfDebugLog( 'fileupload', 'Received chunk of ' . strlen( $buffer ) . ' bytes' );
323 $nbytes = fwrite( $this->mTmpHandle, $buffer );
324
325 if ( $nbytes == strlen( $buffer ) ) {
326 $this->mFileSize += $nbytes;
327 } else {
328 // Well... that's not good!
330 'fileupload',
331 'Short write ' . $nbytes . '/' . strlen( $buffer ) .
332 ' bytes, aborting with ' . $this->mFileSize . ' uploaded so far'
333 );
334 fclose( $this->mTmpHandle );
335 $this->mTmpHandle = false;
336 }
337
338 return $nbytes;
339 }
340
348 protected function reallyFetchFile( $httpOptions = [] ) {
349 $copyUploadProxy = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::CopyUploadProxy );
350 $copyUploadTimeout = MediaWikiServices::getInstance()->getMainConfig()
351 ->get( MainConfigNames::CopyUploadTimeout );
352 if ( $this->mTempPath === false ) {
353 return Status::newFatal( 'tmp-create-error' );
354 }
355
356 // Note the temporary file should already be created by makeTemporaryFile()
357 $this->mTmpHandle = fopen( $this->mTempPath, 'wb' );
358 if ( !$this->mTmpHandle ) {
359 return Status::newFatal( 'tmp-create-error' );
360 }
361 wfDebugLog( 'fileupload', 'Temporary file created "' . $this->mTempPath . '"' );
362
363 $this->mRemoveTempFile = true;
364 $this->mFileSize = 0;
365
366 $options = $httpOptions + [ 'followRedirects' => false ];
367
368 if ( $copyUploadProxy !== false ) {
369 $options['proxy'] = $copyUploadProxy;
370 }
371
372 if ( $copyUploadTimeout && !isset( $options['timeout'] ) ) {
373 $options['timeout'] = $copyUploadTimeout;
374 }
376 'fileupload',
377 'Starting download from "' . $this->mUrl . '" ' .
378 '<' . implode( ',', array_keys( array_filter( $options ) ) ) . '>'
379 );
380
381 // Manually follow any redirects up to the limit and reset the output file before each new request to prevent
382 // capturing the redirect response as part of the file.
383 $attemptsLeft = $options['maxRedirects'] ?? 5;
384 $targetUrl = $this->mUrl;
385 $requestFactory = MediaWikiServices::getInstance()->getHttpRequestFactory();
386 while ( $attemptsLeft > 0 ) {
387 $req = $requestFactory->create( $targetUrl, $options, __METHOD__ );
388 $req->setCallback( [ $this, 'saveTempFileChunk' ] );
389 $status = $req->execute();
390 if ( !$req->isRedirect() ) {
391 break;
392 }
393 $targetUrl = $req->getFinalUrl();
394 // Remove redirect response content from file.
395 ftruncate( $this->mTmpHandle, 0 );
396 rewind( $this->mTmpHandle );
397 $attemptsLeft--;
398 }
399
400 if ( $attemptsLeft == 0 ) {
401 return Status::newFatal( 'upload-too-many-redirects' );
402 }
403
404 if ( $this->mTmpHandle ) {
405 // File got written ok...
406 fclose( $this->mTmpHandle );
407 $this->mTmpHandle = null;
408 } else {
409 // We encountered a write error during the download...
410 return Status::newFatal( 'tmp-write-error' );
411 }
412
413 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable Always set after loop
414 if ( $status->isOK() ) {
415 wfDebugLog( 'fileupload', 'Download by URL completed successfully.' );
416 } else {
417 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable Always set after loop
418 wfDebugLog( 'fileupload', $status->getWikiText( false, false, 'en' ) );
420 'fileupload',
421 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable Always set after loop
422 'Download by URL completed with HTTP status ' . $req->getStatus()
423 );
424 }
425
426 // @phan-suppress-next-line PhanTypeMismatchReturnNullable,PhanPossiblyUndeclaredVariable Always set after loop
427 return $status;
428 }
429}
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
static array< string, bool > $allowedUrls
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...
resource null false $mTmpHandle
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.