MediaWiki 1.42.0
UploadFromUrl.php
Go to the documentation of this file.
1<?php
31
40 protected $mUrl;
41
43
44 protected static $allowedUrls = [];
45
55 public static function isAllowed( Authority $performer ) {
56 if ( !$performer->isAllowed( 'upload_by_url' ) ) {
57 return 'upload_by_url';
58 }
59
60 return parent::isAllowed( $performer );
61 }
62
67 public static function isEnabled() {
68 $allowCopyUploads = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::AllowCopyUploads );
69
70 return $allowCopyUploads && parent::isEnabled();
71 }
72
81 public static function isAllowedHost( $url ) {
82 $domains = self::getAllowedHosts();
83 if ( !count( $domains ) ) {
84 return true;
85 }
86 $parsedUrl = wfParseUrl( $url );
87 if ( !$parsedUrl ) {
88 return false;
89 }
90 $valid = false;
91 foreach ( $domains as $domain ) {
92 // See if the domain for the upload matches this allowed domain
93 $domainPieces = explode( '.', $domain );
94 $uploadDomainPieces = explode( '.', $parsedUrl['host'] );
95 if ( count( $domainPieces ) === count( $uploadDomainPieces ) ) {
96 $valid = true;
97 // See if all the pieces match or not (excluding wildcards)
98 foreach ( $domainPieces as $index => $piece ) {
99 if ( $piece !== '*' && $piece !== $uploadDomainPieces[$index] ) {
100 $valid = false;
101 }
102 }
103 if ( $valid ) {
104 // We found a match, so quit comparing against the list
105 break;
106 }
107 }
108 /* Non-wildcard test
109 if ( $parsedUrl['host'] === $domain ) {
110 $valid = true;
111 break;
112 }
113 */
114 }
115
116 return $valid;
117 }
118
128 public static function getCacheKey( $params ) {
129 if ( !isset( $params['filename'] ) || !isset( $params['url'] ) ) {
130 return "";
131 } else {
132 // We use sha1 here to ensure we have a fixed-length string of printable
133 // characters. There is no cryptography involved, so we just need a
134 // relatively fast function.
135 return sha1( sprintf( "%s|||%s", $params['filename'], $params['url'] ) );
136 }
137 }
138
145 public static function getCacheKeyFromRequest( &$request ) {
146 $uploadCacheKey = $request->getText( 'wpCacheKey', $request->getText( 'key', '' ) );
147 if ( $uploadCacheKey !== '' ) {
148 return $uploadCacheKey;
149 }
150 $desiredDestName = $request->getText( 'wpDestFile' );
151 if ( !$desiredDestName ) {
152 $desiredDestName = $request->getText( 'wpUploadFileURL' );
153 }
154 return self::getCacheKey(
155 [
156 'filename' => $desiredDestName,
157 'url' => trim( $request->getVal( 'wpUploadFileURL' ) )
158 ]
159 );
160 }
161
165 private static function getAllowedHosts(): array {
166 $config = MediaWikiServices::getInstance()->getMainConfig();
167 $domains = $config->get( MainConfigNames::CopyUploadsDomains );
168
169 if ( $config->get( MainConfigNames::CopyUploadAllowOnWikiDomainConfig ) ) {
170 $page = wfMessage( 'copyupload-allowed-domains' )->inContentLanguage()->plain();
171
172 foreach ( explode( "\n", $page ) as $line ) {
173 // Strip comments
174 $line = preg_replace( "/^\\s*([^#]*)\\s*((.*)?)$/", "\\1", $line );
175 // Trim whitespace
176 $line = trim( $line );
177
178 if ( $line !== '' ) {
179 $domains[] = $line;
180 }
181 }
182 }
183
184 return $domains;
185 }
186
193 public static function isAllowedUrl( $url ) {
194 if ( !isset( self::$allowedUrls[$url] ) ) {
195 $allowed = true;
196 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
197 ->onIsUploadAllowedFromUrl( $url, $allowed );
198 self::$allowedUrls[$url] = $allowed;
199 }
200
201 return self::$allowedUrls[$url];
202 }
203
208 public function getUrl() {
209 return $this->mUrl;
210 }
211
218 public function initialize( $name, $url ) {
219 $this->mUrl = $url;
220
221 $tempPath = $this->makeTemporaryFile();
222 # File size and removeTempFile will be filled in later
223 $this->initializePathInfo( $name, $tempPath, 0, false );
224 }
225
230 public function initializeFromRequest( &$request ) {
231 $desiredDestName = $request->getText( 'wpDestFile' );
232 if ( !$desiredDestName ) {
233 $desiredDestName = $request->getText( 'wpUploadFileURL' );
234 }
235 $this->initialize(
236 $desiredDestName,
237 trim( $request->getVal( 'wpUploadFileURL' ) )
238 );
239 }
240
245 public static function isValidRequest( $request ) {
246 $user = RequestContext::getMain()->getUser();
247
248 $url = $request->getVal( 'wpUploadFileURL' );
249
250 return $url
251 && MediaWikiServices::getInstance()
252 ->getPermissionManager()
253 ->userHasRight( $user, 'upload_by_url' );
254 }
255
259 public function getSourceType() {
260 return 'url';
261 }
262
270 public function fetchFile( $httpOptions = [] ) {
271 $status = $this->canFetchFile();
272 if ( !$status->isGood() ) {
273 return $status;
274 }
275 return $this->reallyFetchFile( $httpOptions );
276 }
277
283 public function canFetchFile() {
284 if ( !MWHttpRequest::isValidURI( $this->mUrl ) ) {
285 return Status::newFatal( 'http-invalid-url', $this->mUrl );
286 }
287
288 if ( !self::isAllowedHost( $this->mUrl ) ) {
289 return Status::newFatal( 'upload-copy-upload-invalid-domain' );
290 }
291 if ( !self::isAllowedUrl( $this->mUrl ) ) {
292 return Status::newFatal( 'upload-copy-upload-invalid-url' );
293 }
294 return Status::newGood();
295 }
296
302 protected function makeTemporaryFile() {
303 $tmpFile = MediaWikiServices::getInstance()->getTempFSFileFactory()
304 ->newTempFSFile( 'URL', 'urlupload_' );
305 $tmpFile->bind( $this );
306
307 return $tmpFile->getPath();
308 }
309
317 public function saveTempFileChunk( $req, $buffer ) {
318 wfDebugLog( 'fileupload', 'Received chunk of ' . strlen( $buffer ) . ' bytes' );
319 $nbytes = fwrite( $this->mTmpHandle, $buffer );
320
321 if ( $nbytes == strlen( $buffer ) ) {
322 $this->mFileSize += $nbytes;
323 } else {
324 // Well... that's not good!
326 'fileupload',
327 'Short write ' . $nbytes . '/' . strlen( $buffer ) .
328 ' bytes, aborting with ' . $this->mFileSize . ' uploaded so far'
329 );
330 fclose( $this->mTmpHandle );
331 $this->mTmpHandle = false;
332 }
333
334 return $nbytes;
335 }
336
344 protected function reallyFetchFile( $httpOptions = [] ) {
345 $copyUploadProxy = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::CopyUploadProxy );
346 $copyUploadTimeout = MediaWikiServices::getInstance()->getMainConfig()
347 ->get( MainConfigNames::CopyUploadTimeout );
348 if ( $this->mTempPath === false ) {
349 return Status::newFatal( 'tmp-create-error' );
350 }
351
352 // Note the temporary file should already be created by makeTemporaryFile()
353 $this->mTmpHandle = fopen( $this->mTempPath, 'wb' );
354 if ( !$this->mTmpHandle ) {
355 return Status::newFatal( 'tmp-create-error' );
356 }
357 wfDebugLog( 'fileupload', 'Temporary file created "' . $this->mTempPath . '"' );
358
359 $this->mRemoveTempFile = true;
360 $this->mFileSize = 0;
361
362 $options = $httpOptions + [ 'followRedirects' => false ];
363
364 if ( $copyUploadProxy !== false ) {
365 $options['proxy'] = $copyUploadProxy;
366 }
367
368 if ( $copyUploadTimeout && !isset( $options['timeout'] ) ) {
369 $options['timeout'] = $copyUploadTimeout;
370 }
372 'fileupload',
373 'Starting download from "' . $this->mUrl . '" ' .
374 '<' . implode( ',', array_keys( array_filter( $options ) ) ) . '>'
375 );
376
377 // Manually follow any redirects up to the limit and reset the output file before each new request to prevent
378 // capturing the redirect response as part of the file.
379 $attemptsLeft = $options['maxRedirects'] ?? 5;
380 $targetUrl = $this->mUrl;
381 $requestFactory = MediaWikiServices::getInstance()->getHttpRequestFactory();
382 while ( $attemptsLeft > 0 ) {
383 $req = $requestFactory->create( $targetUrl, $options, __METHOD__ );
384 $req->setCallback( [ $this, 'saveTempFileChunk' ] );
385 $status = $req->execute();
386 if ( !$req->isRedirect() ) {
387 break;
388 }
389 $targetUrl = $req->getFinalUrl();
390 // Remove redirect response content from file.
391 ftruncate( $this->mTmpHandle, 0 );
392 rewind( $this->mTmpHandle );
393 $attemptsLeft--;
394 }
395
396 if ( $attemptsLeft == 0 ) {
397 return Status::newFatal( 'upload-too-many-redirects' );
398 }
399
400 if ( $this->mTmpHandle ) {
401 // File got written ok...
402 fclose( $this->mTmpHandle );
403 $this->mTmpHandle = null;
404 } else {
405 // We encountered a write error during the download...
406 return Status::newFatal( 'tmp-write-error' );
407 }
408
409 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable Always set after loop
410 if ( $status->isOK() ) {
411 wfDebugLog( 'fileupload', 'Download by URL completed successfully.' );
412 } else {
413 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable Always set after loop
414 wfDebugLog( 'fileupload', $status->getWikiText( false, false, 'en' ) );
416 'fileupload',
417 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable Always set after loop
418 'Download by URL completed with HTTP status ' . $req->getStatus()
419 );
420 }
421
422 // @phan-suppress-next-line PhanTypeMismatchReturnNullable,PhanPossiblyUndeclaredVariable Always set after loop
423 return $status;
424 }
425}
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.