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 }
102
103 return $valid;
104 }
105
115 public static function getCacheKey( $params ) {
116 if ( !isset( $params['filename'] ) || !isset( $params['url'] ) ) {
117 return "";
118 } else {
119 // We use sha1 here to ensure we have a fixed-length string of printable
120 // characters. There is no cryptography involved, so we just need a
121 // relatively fast function.
122 return sha1( sprintf( "%s|||%s", $params['filename'], $params['url'] ) );
123 }
124 }
125
132 public static function getCacheKeyFromRequest( &$request ) {
133 $uploadCacheKey = $request->getText( 'wpCacheKey', $request->getText( 'key', '' ) );
134 if ( $uploadCacheKey !== '' ) {
135 return $uploadCacheKey;
136 }
137 $desiredDestName = $request->getText( 'wpDestFile' );
138 if ( !$desiredDestName ) {
139 $desiredDestName = $request->getText( 'wpUploadFileURL' );
140 }
141 return self::getCacheKey(
142 [
143 'filename' => $desiredDestName,
144 'url' => trim( $request->getVal( 'wpUploadFileURL' ) )
145 ]
146 );
147 }
148
153 public static function getAllowedHosts(): array {
154 $config = MediaWikiServices::getInstance()->getMainConfig();
155 $domains = $config->get( MainConfigNames::CopyUploadsDomains );
156
158 $page = wfMessage( 'copyupload-allowed-domains' )->inContentLanguage()->plain();
159
160 foreach ( explode( "\n", $page ) as $line ) {
161 // Strip comments
162 $line = preg_replace( "/^\\s*([^#]*)\\s*((.*)?)$/", "\\1", $line );
163 // Trim whitespace
164 $line = trim( $line );
165
166 if ( $line !== '' ) {
167 $domains[] = $line;
168 }
169 }
170 }
171
172 return $domains;
173 }
174
181 public static function isAllowedUrl( $url ) {
182 if ( !isset( self::$allowedUrls[$url] ) ) {
183 $allowed = true;
184 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
185 ->onIsUploadAllowedFromUrl( $url, $allowed );
186 self::$allowedUrls[$url] = $allowed;
187 }
188
189 return self::$allowedUrls[$url];
190 }
191
196 public function getUrl() {
197 return $this->mUrl;
198 }
199
207 public function initialize( $name, $url, $initTempFile = true ) {
208 $this->mUrl = $url;
209
210 $tempPath = $initTempFile ? $this->makeTemporaryFile() : null;
211 $fileSize = $initTempFile ? 0 : null;
212 // File size and removeTempFile will be filled in later
213 $this->initializePathInfo( $name, $tempPath, $fileSize, false );
214 }
215
220 public function initializeFromRequest( &$request ) {
221 $desiredDestName = $request->getText( 'wpDestFile' );
222 if ( !$desiredDestName ) {
223 $desiredDestName = $request->getText( 'wpUploadFileURL' );
224 }
225 $this->initialize(
226 $desiredDestName,
227 trim( $request->getVal( 'wpUploadFileURL' ) )
228 );
229 }
230
235 public static function isValidRequest( $request ) {
236 $user = RequestContext::getMain()->getUser();
237
238 $url = $request->getVal( 'wpUploadFileURL' );
239
240 return $url
242 ->getPermissionManager()
243 ->userHasRight( $user, 'upload_by_url' );
244 }
245
249 public function getSourceType() {
250 return 'url';
251 }
252
260 public function fetchFile( $httpOptions = [] ) {
261 $status = $this->canFetchFile();
262 if ( !$status->isGood() ) {
263 return $status;
264 }
265 return $this->reallyFetchFile( $httpOptions );
266 }
267
273 public function canFetchFile() {
274 return $this->canFetchFileFromUrl( $this->mUrl );
275 }
276
277 private function canFetchFileFromUrl( string $url ): Status {
278 if ( !MWHttpRequest::isValidURI( $url ) ) {
279 return Status::newFatal( 'http-invalid-url', $url );
280 }
281 if ( !self::isAllowedHost( $url ) ) {
282 return Status::newFatal( 'upload-copy-upload-invalid-domain' );
283 }
284 if ( !self::isAllowedUrl( $url ) ) {
285 return Status::newFatal( 'upload-copy-upload-invalid-url' );
286 }
287 return Status::newGood();
288 }
289
295 protected function makeTemporaryFile() {
296 $tmpFile = MediaWikiServices::getInstance()->getTempFSFileFactory()
297 ->newTempFSFile( 'URL', 'urlupload_' );
298 $tmpFile->bind( $this );
299
300 return $tmpFile->getPath();
301 }
302
310 public function saveTempFileChunk( $req, $buffer ) {
311 wfDebugLog( 'fileupload', 'Received chunk of ' . strlen( $buffer ) . ' bytes' );
312 $nbytes = fwrite( $this->mTmpHandle, $buffer );
313
314 if ( $nbytes == strlen( $buffer ) ) {
315 $this->mFileSize += $nbytes;
316 } else {
317 // Well... that's not good!
319 'fileupload',
320 'Short write ' . $nbytes . '/' . strlen( $buffer ) .
321 ' bytes, aborting with ' . $this->mFileSize . ' uploaded so far'
322 );
323 fclose( $this->mTmpHandle );
324 $this->mTmpHandle = false;
325 }
326
327 return $nbytes;
328 }
329
336 protected function reallyFetchFile( $httpOptions = [] ) {
337 $copyUploadProxy = MediaWikiServices::getInstance()->getMainConfig()
338 ->get( MainConfigNames::CopyUploadProxy );
339 $copyUploadTimeout = MediaWikiServices::getInstance()->getMainConfig()
340 ->get( MainConfigNames::CopyUploadTimeout );
341
342 // Note the temporary file should already be created by makeTemporaryFile()
343 $this->mTmpHandle = fopen( $this->mTempPath, 'wb' );
344 if ( !$this->mTmpHandle ) {
345 return Status::newFatal( 'tmp-create-error' );
346 }
347 wfDebugLog( 'fileupload', 'Temporary file created "' . $this->mTempPath . '"' );
348
349 $this->mRemoveTempFile = true;
350 $this->mFileSize = 0;
351
352 $options = $httpOptions + [ 'followRedirects' => false ];
353
354 if ( $copyUploadProxy !== false ) {
355 $options['proxy'] = $copyUploadProxy;
356 }
357
358 if ( $copyUploadTimeout && !isset( $options['timeout'] ) ) {
359 $options['timeout'] = $copyUploadTimeout;
360 }
362 'fileupload',
363 'Starting download from "' . $this->mUrl . '" ' .
364 '<' . implode( ',', array_keys( array_filter( $options ) ) ) . '>'
365 );
366
367 // Manually follow any redirects up to the limit and reset the output
368 // file before each new request to prevent capturing the redirect
369 // response as part of the file.
370 $attemptsLeft = $options['maxRedirects'] ?? 5;
371 $targetUrl = $this->mUrl;
372 $requestFactory = MediaWikiServices::getInstance()->getHttpRequestFactory();
373 do {
374 $req = $requestFactory->create( $targetUrl, $options, __METHOD__ );
375 $req->setCallback( $this->saveTempFileChunk( ... ) );
376 $status = $req->execute();
377 if ( !$req->isRedirect() ) {
378 break;
379 }
380 $targetUrl = $req->getFinalUrl();
381 $redirectedUrlFetchable = $this->canFetchFileFromUrl( $targetUrl );
382 if ( !$redirectedUrlFetchable->isGood() ) {
383 return $redirectedUrlFetchable;
384 }
385 // Remove redirect response content from file.
386 ftruncate( $this->mTmpHandle, 0 );
387 rewind( $this->mTmpHandle );
388 $attemptsLeft--;
389 } while ( $attemptsLeft > 0 );
390
391 if ( $attemptsLeft == 0 ) {
392 return Status::newFatal( 'upload-too-many-redirects' );
393 }
394
395 if ( $this->mTmpHandle ) {
396 // File got written OK
397 fclose( $this->mTmpHandle );
398 $this->mTmpHandle = null;
399 } else {
400 // We encountered a write error during the download
401 return Status::newFatal( 'tmp-write-error' );
402 }
403
404 if ( $status->isOK() ) {
405 wfDebugLog( 'fileupload', 'Download by URL completed successfully.' );
406 } else {
407 wfDebugLog( 'fileupload', $status->getWikiText( false, false, 'en' ) );
409 'fileupload',
410 'Download by URL completed with HTTP status ' . $req->getStatus()
411 );
412 }
413
414 return $status;
415 }
416}
417
419class_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.
if(!defined('MW_SETUP_CALLBACK'))
Definition WebStart.php:71
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...
A class containing constants representing the names of configuration variables.
const AllowCopyUploads
Name constant for the AllowCopyUploads 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 with no domain name validation.
initializeFromRequest(&$request)
Entry point for SpecialUpload.
fetchFile( $httpOptions=[])
Download the file if allowed.
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.
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.
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.