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 $domains = self::getAllowedHosts();
75 if ( !count( $domains ) ) {
76 return true;
77 }
78 $parsedUrl = wfGetUrlUtils()->parse( $url );
79 if ( !$parsedUrl ) {
80 return false;
81 }
82 $valid = false;
83 foreach ( $domains as $domain ) {
84 // See if the domain for the upload matches this allowed domain
85 $domainPieces = explode( '.', $domain );
86 $uploadDomainPieces = explode( '.', $parsedUrl['host'] );
87 if ( count( $domainPieces ) === count( $uploadDomainPieces ) ) {
88 $valid = true;
89 // See if all the pieces match or not (excluding wildcards)
90 foreach ( $domainPieces as $index => $piece ) {
91 if ( $piece !== '*' && $piece !== $uploadDomainPieces[$index] ) {
92 $valid = false;
93 }
94 }
95 if ( $valid ) {
96 // We found a match, so quit comparing against the list
97 break;
98 }
99 }
100 /* Non-wildcard test
101 if ( $parsedUrl['host'] === $domain ) {
102 $valid = true;
103 break;
104 }
105 */
106 }
107
108 return $valid;
109 }
110
120 public static function getCacheKey( $params ) {
121 if ( !isset( $params['filename'] ) || !isset( $params['url'] ) ) {
122 return "";
123 } else {
124 // We use sha1 here to ensure we have a fixed-length string of printable
125 // characters. There is no cryptography involved, so we just need a
126 // relatively fast function.
127 return sha1( sprintf( "%s|||%s", $params['filename'], $params['url'] ) );
128 }
129 }
130
137 public static function getCacheKeyFromRequest( &$request ) {
138 $uploadCacheKey = $request->getText( 'wpCacheKey', $request->getText( 'key', '' ) );
139 if ( $uploadCacheKey !== '' ) {
140 return $uploadCacheKey;
141 }
142 $desiredDestName = $request->getText( 'wpDestFile' );
143 if ( !$desiredDestName ) {
144 $desiredDestName = $request->getText( 'wpUploadFileURL' );
145 }
146 return self::getCacheKey(
147 [
148 'filename' => $desiredDestName,
149 'url' => trim( $request->getVal( 'wpUploadFileURL' ) )
150 ]
151 );
152 }
153
158 public static function getAllowedHosts(): array {
159 $config = MediaWikiServices::getInstance()->getMainConfig();
160 $domains = $config->get( MainConfigNames::CopyUploadsDomains );
161
163 $page = wfMessage( 'copyupload-allowed-domains' )->inContentLanguage()->plain();
164
165 foreach ( explode( "\n", $page ) as $line ) {
166 // Strip comments
167 $line = preg_replace( "/^\\s*([^#]*)\\s*((.*)?)$/", "\\1", $line );
168 // Trim whitespace
169 $line = trim( $line );
170
171 if ( $line !== '' ) {
172 $domains[] = $line;
173 }
174 }
175 }
176
177 return $domains;
178 }
179
186 public static function isAllowedUrl( $url ) {
187 if ( !isset( self::$allowedUrls[$url] ) ) {
188 $allowed = true;
189 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
190 ->onIsUploadAllowedFromUrl( $url, $allowed );
191 self::$allowedUrls[$url] = $allowed;
192 }
193
194 return self::$allowedUrls[$url];
195 }
196
201 public function getUrl() {
202 return $this->mUrl;
203 }
204
212 public function initialize( $name, $url, $initTempFile = true ) {
213 $this->mUrl = $url;
214
215 $tempPath = $initTempFile ? $this->makeTemporaryFile() : null;
216 $fileSize = $initTempFile ? 0 : null;
217 # File size and removeTempFile will be filled in later
218 $this->initializePathInfo( $name, $tempPath, $fileSize, false );
219 }
220
225 public function initializeFromRequest( &$request ) {
226 $desiredDestName = $request->getText( 'wpDestFile' );
227 if ( !$desiredDestName ) {
228 $desiredDestName = $request->getText( 'wpUploadFileURL' );
229 }
230 $this->initialize(
231 $desiredDestName,
232 trim( $request->getVal( 'wpUploadFileURL' ) )
233 );
234 }
235
240 public static function isValidRequest( $request ) {
241 $user = RequestContext::getMain()->getUser();
242
243 $url = $request->getVal( 'wpUploadFileURL' );
244
245 return $url
247 ->getPermissionManager()
248 ->userHasRight( $user, 'upload_by_url' );
249 }
250
254 public function getSourceType() {
255 return 'url';
256 }
257
265 public function fetchFile( $httpOptions = [] ) {
266 $status = $this->canFetchFile();
267 if ( !$status->isGood() ) {
268 return $status;
269 }
270 return $this->reallyFetchFile( $httpOptions );
271 }
272
278 public function canFetchFile() {
279 if ( !MWHttpRequest::isValidURI( $this->mUrl ) ) {
280 return Status::newFatal( 'http-invalid-url', $this->mUrl );
281 }
282
283 if ( !self::isAllowedHost( $this->mUrl ) ) {
284 return Status::newFatal( 'upload-copy-upload-invalid-domain' );
285 }
286 if ( !self::isAllowedUrl( $this->mUrl ) ) {
287 return Status::newFatal( 'upload-copy-upload-invalid-url' );
288 }
289 return Status::newGood();
290 }
291
297 protected function makeTemporaryFile() {
298 $tmpFile = MediaWikiServices::getInstance()->getTempFSFileFactory()
299 ->newTempFSFile( 'URL', 'urlupload_' );
300 $tmpFile->bind( $this );
301
302 return $tmpFile->getPath();
303 }
304
312 public function saveTempFileChunk( $req, $buffer ) {
313 wfDebugLog( 'fileupload', 'Received chunk of ' . strlen( $buffer ) . ' bytes' );
314 $nbytes = fwrite( $this->mTmpHandle, $buffer );
315
316 if ( $nbytes == strlen( $buffer ) ) {
317 $this->mFileSize += $nbytes;
318 } else {
319 // Well... that's not good!
321 'fileupload',
322 'Short write ' . $nbytes . '/' . strlen( $buffer ) .
323 ' bytes, aborting with ' . $this->mFileSize . ' uploaded so far'
324 );
325 fclose( $this->mTmpHandle );
326 $this->mTmpHandle = false;
327 }
328
329 return $nbytes;
330 }
331
339 protected function reallyFetchFile( $httpOptions = [] ) {
340 $copyUploadProxy = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::CopyUploadProxy );
341 $copyUploadTimeout = MediaWikiServices::getInstance()->getMainConfig()
343
344 // Note the temporary file should already be created by makeTemporaryFile()
345 $this->mTmpHandle = fopen( $this->mTempPath, 'wb' );
346 if ( !$this->mTmpHandle ) {
347 return Status::newFatal( 'tmp-create-error' );
348 }
349 wfDebugLog( 'fileupload', 'Temporary file created "' . $this->mTempPath . '"' );
350
351 $this->mRemoveTempFile = true;
352 $this->mFileSize = 0;
353
354 $options = $httpOptions + [ 'followRedirects' => false ];
355
356 if ( $copyUploadProxy !== false ) {
357 $options['proxy'] = $copyUploadProxy;
358 }
359
360 if ( $copyUploadTimeout && !isset( $options['timeout'] ) ) {
361 $options['timeout'] = $copyUploadTimeout;
362 }
364 'fileupload',
365 'Starting download from "' . $this->mUrl . '" ' .
366 '<' . implode( ',', array_keys( array_filter( $options ) ) ) . '>'
367 );
368
369 // Manually follow any redirects up to the limit and reset the output file before each new request to prevent
370 // capturing the redirect response as part of the file.
371 $attemptsLeft = $options['maxRedirects'] ?? 5;
372 $targetUrl = $this->mUrl;
373 $requestFactory = MediaWikiServices::getInstance()->getHttpRequestFactory();
374 while ( $attemptsLeft > 0 ) {
375 $req = $requestFactory->create( $targetUrl, $options, __METHOD__ );
376 $req->setCallback( $this->saveTempFileChunk( ... ) );
377 $status = $req->execute();
378 if ( !$req->isRedirect() ) {
379 break;
380 }
381 $targetUrl = $req->getFinalUrl();
382 // Remove redirect response content from file.
383 ftruncate( $this->mTmpHandle, 0 );
384 rewind( $this->mTmpHandle );
385 $attemptsLeft--;
386 }
387
388 if ( $attemptsLeft == 0 ) {
389 return Status::newFatal( 'upload-too-many-redirects' );
390 }
391
392 if ( $this->mTmpHandle ) {
393 // File got written ok...
394 fclose( $this->mTmpHandle );
395 $this->mTmpHandle = null;
396 } else {
397 // We encountered a write error during the download...
398 return Status::newFatal( 'tmp-write-error' );
399 }
400
401 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable Always set after loop
402 if ( $status->isOK() ) {
403 wfDebugLog( 'fileupload', 'Download by URL completed successfully.' );
404 } else {
405 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable Always set after loop
406 wfDebugLog( 'fileupload', $status->getWikiText( false, false, 'en' ) );
408 'fileupload',
409 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable Always set after loop
410 'Download by URL completed with HTTP status ' . $req->getStatus()
411 );
412 }
413
414 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable Always set after loop
415 return $status;
416 }
417}
418
420class_alias( UploadFromUrl::class, 'UploadFromUrl' );
wfGetUrlUtils()
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.