MediaWiki master
UploadFromUrl.php
Go to the documentation of this file.
1<?php
10namespace MediaWiki\Upload;
11
20
30 protected $mUrl;
31
33 protected $mTmpHandle;
34
36 protected static $allowedUrls = [];
37
47 public static function isAllowed( Authority $performer ) {
48 if ( !$performer->isAllowed( 'upload_by_url' ) ) {
49 return 'upload_by_url';
50 }
51
52 return parent::isAllowed( $performer );
53 }
54
59 public static function isEnabled() {
60 $allowCopyUploads = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::AllowCopyUploads );
61
62 return $allowCopyUploads && parent::isEnabled();
63 }
64
73 public static function isAllowedHost( $url ) {
74 $urlUtils = MediaWikiServices::getInstance()->getURLUtils();
75 $domains = self::getAllowedHosts();
76 if ( !count( $domains ) ) {
77 return true;
78 }
79 $parsedUrl = $urlUtils->parse( $url );
80 if ( !$parsedUrl ) {
81 return false;
82 }
83 $valid = false;
84 foreach ( $domains as $domain ) {
85 // See if the domain for the upload matches this allowed domain
86 $domainPieces = explode( '.', $domain );
87 $uploadDomainPieces = explode( '.', $parsedUrl['host'] );
88 if ( count( $domainPieces ) === count( $uploadDomainPieces ) ) {
89 $valid = true;
90 // See if all the pieces match or not (excluding wildcards)
91 foreach ( $domainPieces as $index => $piece ) {
92 if ( $piece !== '*' && $piece !== $uploadDomainPieces[$index] ) {
93 $valid = false;
94 }
95 }
96 if ( $valid ) {
97 // We found a match, so quit comparing against the list
98 break;
99 }
100 }
101 /* Non-wildcard test
102 if ( $parsedUrl['host'] === $domain ) {
103 $valid = true;
104 break;
105 }
106 */
107 }
108
109 return $valid;
110 }
111
121 public static function getCacheKey( $params ) {
122 if ( !isset( $params['filename'] ) || !isset( $params['url'] ) ) {
123 return "";
124 } else {
125 // We use sha1 here to ensure we have a fixed-length string of printable
126 // characters. There is no cryptography involved, so we just need a
127 // relatively fast function.
128 return sha1( sprintf( "%s|||%s", $params['filename'], $params['url'] ) );
129 }
130 }
131
138 public static function getCacheKeyFromRequest( &$request ) {
139 $uploadCacheKey = $request->getText( 'wpCacheKey', $request->getText( 'key', '' ) );
140 if ( $uploadCacheKey !== '' ) {
141 return $uploadCacheKey;
142 }
143 $desiredDestName = $request->getText( 'wpDestFile' );
144 if ( !$desiredDestName ) {
145 $desiredDestName = $request->getText( 'wpUploadFileURL' );
146 }
147 return self::getCacheKey(
148 [
149 'filename' => $desiredDestName,
150 'url' => trim( $request->getVal( 'wpUploadFileURL' ) )
151 ]
152 );
153 }
154
159 public static function getAllowedHosts(): array {
160 $config = MediaWikiServices::getInstance()->getMainConfig();
161 $domains = $config->get( MainConfigNames::CopyUploadsDomains );
162
164 $page = wfMessage( 'copyupload-allowed-domains' )->inContentLanguage()->plain();
165
166 foreach ( explode( "\n", $page ) as $line ) {
167 // Strip comments
168 $line = preg_replace( "/^\\s*([^#]*)\\s*((.*)?)$/", "\\1", $line );
169 // Trim whitespace
170 $line = trim( $line );
171
172 if ( $line !== '' ) {
173 $domains[] = $line;
174 }
175 }
176 }
177
178 return $domains;
179 }
180
187 public static function isAllowedUrl( $url ) {
188 if ( !isset( self::$allowedUrls[$url] ) ) {
189 $allowed = true;
190 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
191 ->onIsUploadAllowedFromUrl( $url, $allowed );
192 self::$allowedUrls[$url] = $allowed;
193 }
194
195 return self::$allowedUrls[$url];
196 }
197
202 public function getUrl() {
203 return $this->mUrl;
204 }
205
213 public function initialize( $name, $url, $initTempFile = true ) {
214 $this->mUrl = $url;
215
216 $tempPath = $initTempFile ? $this->makeTemporaryFile() : null;
217 $fileSize = $initTempFile ? 0 : null;
218 # File size and removeTempFile will be filled in later
219 $this->initializePathInfo( $name, $tempPath, $fileSize, false );
220 }
221
226 public function initializeFromRequest( &$request ) {
227 $desiredDestName = $request->getText( 'wpDestFile' );
228 if ( !$desiredDestName ) {
229 $desiredDestName = $request->getText( 'wpUploadFileURL' );
230 }
231 $this->initialize(
232 $desiredDestName,
233 trim( $request->getVal( 'wpUploadFileURL' ) )
234 );
235 }
236
241 public static function isValidRequest( $request ) {
242 $user = RequestContext::getMain()->getUser();
243
244 $url = $request->getVal( 'wpUploadFileURL' );
245
246 return $url
248 ->getPermissionManager()
249 ->userHasRight( $user, 'upload_by_url' );
250 }
251
255 public function getSourceType() {
256 return 'url';
257 }
258
266 public function fetchFile( $httpOptions = [] ) {
267 $status = $this->canFetchFile();
268 if ( !$status->isGood() ) {
269 return $status;
270 }
271 return $this->reallyFetchFile( $httpOptions );
272 }
273
279 public function canFetchFile() {
280 if ( !MWHttpRequest::isValidURI( $this->mUrl ) ) {
281 return Status::newFatal( 'http-invalid-url', $this->mUrl );
282 }
283
284 if ( !self::isAllowedHost( $this->mUrl ) ) {
285 return Status::newFatal( 'upload-copy-upload-invalid-domain' );
286 }
287 if ( !self::isAllowedUrl( $this->mUrl ) ) {
288 return Status::newFatal( 'upload-copy-upload-invalid-url' );
289 }
290 return Status::newGood();
291 }
292
298 protected function makeTemporaryFile() {
299 $tmpFile = MediaWikiServices::getInstance()->getTempFSFileFactory()
300 ->newTempFSFile( 'URL', 'urlupload_' );
301 $tmpFile->bind( $this );
302
303 return $tmpFile->getPath();
304 }
305
313 public function saveTempFileChunk( $req, $buffer ) {
314 wfDebugLog( 'fileupload', 'Received chunk of ' . strlen( $buffer ) . ' bytes' );
315 $nbytes = fwrite( $this->mTmpHandle, $buffer );
316
317 if ( $nbytes == strlen( $buffer ) ) {
318 $this->mFileSize += $nbytes;
319 } else {
320 // Well... that's not good!
322 'fileupload',
323 'Short write ' . $nbytes . '/' . strlen( $buffer ) .
324 ' bytes, aborting with ' . $this->mFileSize . ' uploaded so far'
325 );
326 fclose( $this->mTmpHandle );
327 $this->mTmpHandle = false;
328 }
329
330 return $nbytes;
331 }
332
340 protected function reallyFetchFile( $httpOptions = [] ) {
341 $copyUploadProxy = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::CopyUploadProxy );
342 $copyUploadTimeout = MediaWikiServices::getInstance()->getMainConfig()
344
345 // Note the temporary file should already be created by makeTemporaryFile()
346 $this->mTmpHandle = fopen( $this->mTempPath, 'wb' );
347 if ( !$this->mTmpHandle ) {
348 return Status::newFatal( 'tmp-create-error' );
349 }
350 wfDebugLog( 'fileupload', 'Temporary file created "' . $this->mTempPath . '"' );
351
352 $this->mRemoveTempFile = true;
353 $this->mFileSize = 0;
354
355 $options = $httpOptions + [ 'followRedirects' => false ];
356
357 if ( $copyUploadProxy !== false ) {
358 $options['proxy'] = $copyUploadProxy;
359 }
360
361 if ( $copyUploadTimeout && !isset( $options['timeout'] ) ) {
362 $options['timeout'] = $copyUploadTimeout;
363 }
365 'fileupload',
366 'Starting download from "' . $this->mUrl . '" ' .
367 '<' . implode( ',', array_keys( array_filter( $options ) ) ) . '>'
368 );
369
370 // Manually follow any redirects up to the limit and reset the output file before each new request to prevent
371 // capturing the redirect response as part of the file.
372 $attemptsLeft = $options['maxRedirects'] ?? 5;
373 $targetUrl = $this->mUrl;
374 $requestFactory = MediaWikiServices::getInstance()->getHttpRequestFactory();
375 while ( $attemptsLeft > 0 ) {
376 $req = $requestFactory->create( $targetUrl, $options, __METHOD__ );
377 $req->setCallback( $this->saveTempFileChunk( ... ) );
378 $status = $req->execute();
379 if ( !$req->isRedirect() ) {
380 break;
381 }
382 $targetUrl = $req->getFinalUrl();
383 // Remove redirect response content from file.
384 ftruncate( $this->mTmpHandle, 0 );
385 rewind( $this->mTmpHandle );
386 $attemptsLeft--;
387 }
388
389 if ( $attemptsLeft == 0 ) {
390 return Status::newFatal( 'upload-too-many-redirects' );
391 }
392
393 if ( $this->mTmpHandle ) {
394 // File got written ok...
395 fclose( $this->mTmpHandle );
396 $this->mTmpHandle = null;
397 } else {
398 // We encountered a write error during the download...
399 return Status::newFatal( 'tmp-write-error' );
400 }
401
402 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable Always set after loop
403 if ( $status->isOK() ) {
404 wfDebugLog( 'fileupload', 'Download by URL completed successfully.' );
405 } else {
406 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable Always set after loop
407 wfDebugLog( 'fileupload', $status->getWikiText( false, false, 'en' ) );
409 'fileupload',
410 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable Always set after loop
411 'Download by URL completed with HTTP status ' . $req->getStatus()
412 );
413 }
414
415 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable Always set after loop
416 return $status;
417 }
418}
419
421class_alias( UploadFromUrl::class, 'UploadFromUrl' );
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.
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...
This wrapper class will call out to curl (if available) or fallback to regular PHP if necessary for h...
static isValidURI( $uri)
Check that the given URI is a valid one.
A class containing constants representing the names of configuration variables.
const CopyUploadTimeout
Name constant for the CopyUploadTimeout setting, for use with Config::get()
const AllowCopyUploads
Name constant for the AllowCopyUploads setting, for use with Config::get()
const CopyUploadProxy
Name constant for the CopyUploadProxy setting, for use with Config::get()
const CopyUploadAllowOnWikiDomainConfig
Name constant for the CopyUploadAllowOnWikiDomainConfig setting, for use with Config::get()
const CopyUploadsDomains
Name constant for the CopyUploadsDomains setting, for use with Config::get()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
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:44
UploadBase and subclasses are the backend of MediaWiki's file uploads.
Implements uploading from a HTTP resource.
reallyFetchFile( $httpOptions=[])
Download the file, save it to the temporary file and update the file size and set $mRemoveTempFile to...
initializeFromRequest(&$request)
Entry point for SpecialUpload.
fetchFile( $httpOptions=[])
Download the file.
saveTempFileChunk( $req, $buffer)
Callback: save a chunk of the result of a HTTP request to the temporary file.
static isAllowedUrl( $url)
Checks whether the URL is not allowed.
static array< string, bool > $allowedUrls
static getCacheKeyFromRequest(&$request)
Get the caching key from a web request.
static isEnabled()
Checks if the upload from URL feature is enabled.
canFetchFile()
verify we can actually download the file
getUrl()
Get the URL of the file to be uploaded.
resource null false $mTmpHandle
makeTemporaryFile()
Create a new temporary file in the URL subdirectory of wfTempDir().
static isAllowedHost( $url)
Checks whether the URL is for an allowed host The domains in the allowlist can include wildcard chara...
static isAllowed(Authority $performer)
Checks if the user is allowed to use the upload-by-URL feature.
initialize( $name, $url, $initTempFile=true)
Entry point for API upload.
static getCacheKey( $params)
Provides a caching key for an upload from url set of parameters Used to set the status of an async jo...
This interface represents the authority associated with the current execution context,...
Definition Authority.php:23
isAllowed(string $permission, ?PermissionStatus $status=null)
Checks whether this authority has the given permission in general.