MediaWiki master
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
142 private static function getAllowedHosts(): array {
143 $config = MediaWikiServices::getInstance()->getMainConfig();
144 $domains = $config->get( MainConfigNames::CopyUploadsDomains );
145
146 if ( $config->get( MainConfigNames::CopyUploadAllowOnWikiDomainConfig ) ) {
147 $page = wfMessage( 'copyupload-allowed-domains' )->inContentLanguage()->plain();
148
149 foreach ( explode( "\n", $page ) as $line ) {
150 // Strip comments
151 $line = preg_replace( "/^\\s*([^#]*)\\s*((.*)?)$/", "\\1", $line );
152 // Trim whitespace
153 $line = trim( $line );
154
155 if ( $line !== '' ) {
156 $domains[] = $line;
157 }
158 }
159 }
160
161 return $domains;
162 }
163
170 public static function isAllowedUrl( $url ) {
171 if ( !isset( self::$allowedUrls[$url] ) ) {
172 $allowed = true;
173 ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
174 ->onIsUploadAllowedFromUrl( $url, $allowed );
175 self::$allowedUrls[$url] = $allowed;
176 }
177
178 return self::$allowedUrls[$url];
179 }
180
187 public function initialize( $name, $url ) {
188 $this->mUrl = $url;
189
190 $tempPath = $this->makeTemporaryFile();
191 # File size and removeTempFile will be filled in later
192 $this->initializePathInfo( $name, $tempPath, 0, false );
193 }
194
199 public function initializeFromRequest( &$request ) {
200 $desiredDestName = $request->getText( 'wpDestFile' );
201 if ( !$desiredDestName ) {
202 $desiredDestName = $request->getText( 'wpUploadFileURL' );
203 }
204 $this->initialize(
205 $desiredDestName,
206 trim( $request->getVal( 'wpUploadFileURL' ) )
207 );
208 }
209
214 public static function isValidRequest( $request ) {
215 $user = RequestContext::getMain()->getUser();
216
217 $url = $request->getVal( 'wpUploadFileURL' );
218
219 return $url
220 && MediaWikiServices::getInstance()
221 ->getPermissionManager()
222 ->userHasRight( $user, 'upload_by_url' );
223 }
224
228 public function getSourceType() {
229 return 'url';
230 }
231
239 public function fetchFile( $httpOptions = [] ) {
240 if ( !MWHttpRequest::isValidURI( $this->mUrl ) ) {
241 return Status::newFatal( 'http-invalid-url', $this->mUrl );
242 }
243
244 if ( !self::isAllowedHost( $this->mUrl ) ) {
245 return Status::newFatal( 'upload-copy-upload-invalid-domain' );
246 }
247 if ( !self::isAllowedUrl( $this->mUrl ) ) {
248 return Status::newFatal( 'upload-copy-upload-invalid-url' );
249 }
250 return $this->reallyFetchFile( $httpOptions );
251 }
252
258 protected function makeTemporaryFile() {
259 $tmpFile = MediaWikiServices::getInstance()->getTempFSFileFactory()
260 ->newTempFSFile( 'URL', 'urlupload_' );
261 $tmpFile->bind( $this );
262
263 return $tmpFile->getPath();
264 }
265
273 public function saveTempFileChunk( $req, $buffer ) {
274 wfDebugLog( 'fileupload', 'Received chunk of ' . strlen( $buffer ) . ' bytes' );
275 $nbytes = fwrite( $this->mTmpHandle, $buffer );
276
277 if ( $nbytes == strlen( $buffer ) ) {
278 $this->mFileSize += $nbytes;
279 } else {
280 // Well... that's not good!
282 'fileupload',
283 'Short write ' . $nbytes . '/' . strlen( $buffer ) .
284 ' bytes, aborting with ' . $this->mFileSize . ' uploaded so far'
285 );
286 fclose( $this->mTmpHandle );
287 $this->mTmpHandle = false;
288 }
289
290 return $nbytes;
291 }
292
300 protected function reallyFetchFile( $httpOptions = [] ) {
301 $copyUploadProxy = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::CopyUploadProxy );
302 $copyUploadTimeout = MediaWikiServices::getInstance()->getMainConfig()
303 ->get( MainConfigNames::CopyUploadTimeout );
304 if ( $this->mTempPath === false ) {
305 return Status::newFatal( 'tmp-create-error' );
306 }
307
308 // Note the temporary file should already be created by makeTemporaryFile()
309 $this->mTmpHandle = fopen( $this->mTempPath, 'wb' );
310 if ( !$this->mTmpHandle ) {
311 return Status::newFatal( 'tmp-create-error' );
312 }
313 wfDebugLog( 'fileupload', 'Temporary file created "' . $this->mTempPath . '"' );
314
315 $this->mRemoveTempFile = true;
316 $this->mFileSize = 0;
317
318 $options = $httpOptions + [ 'followRedirects' => false ];
319
320 if ( $copyUploadProxy !== false ) {
321 $options['proxy'] = $copyUploadProxy;
322 }
323
324 if ( $copyUploadTimeout && !isset( $options['timeout'] ) ) {
325 $options['timeout'] = $copyUploadTimeout;
326 }
328 'fileupload',
329 'Starting download from "' . $this->mUrl . '" ' .
330 '<' . implode( ',', array_keys( array_filter( $options ) ) ) . '>'
331 );
332
333 // Manually follow any redirects up to the limit and reset the output file before each new request to prevent
334 // capturing the redirect response as part of the file.
335 $attemptsLeft = $options['maxRedirects'] ?? 5;
336 $targetUrl = $this->mUrl;
337 $requestFactory = MediaWikiServices::getInstance()->getHttpRequestFactory();
338 while ( $attemptsLeft > 0 ) {
339 $req = $requestFactory->create( $targetUrl, $options, __METHOD__ );
340 $req->setCallback( [ $this, 'saveTempFileChunk' ] );
341 $status = $req->execute();
342 if ( !$req->isRedirect() ) {
343 break;
344 }
345 $targetUrl = $req->getFinalUrl();
346 // Remove redirect response content from file.
347 ftruncate( $this->mTmpHandle, 0 );
348 rewind( $this->mTmpHandle );
349 $attemptsLeft--;
350 }
351
352 if ( $attemptsLeft == 0 ) {
353 return Status::newFatal( 'upload-too-many-redirects' );
354 }
355
356 if ( $this->mTmpHandle ) {
357 // File got written ok...
358 fclose( $this->mTmpHandle );
359 $this->mTmpHandle = null;
360 } else {
361 // We encountered a write error during the download...
362 return Status::newFatal( 'tmp-write-error' );
363 }
364
365 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable Always set after loop
366 if ( $status->isOK() ) {
367 wfDebugLog( 'fileupload', 'Download by URL completed successfully.' );
368 } else {
369 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable Always set after loop
370 wfDebugLog( 'fileupload', $status->getWikiText( false, false, 'en' ) );
372 'fileupload',
373 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable Always set after loop
374 'Download by URL completed with HTTP status ' . $req->getStatus()
375 );
376 }
377
378 // @phan-suppress-next-line PhanTypeMismatchReturnNullable,PhanPossiblyUndeclaredVariable Always set after loop
379 return $status;
380 }
381}
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.
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 stripping il...
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.
makeTemporaryFile()
Create a new temporary file in the URL subdirectory of wfTempDir().
static isValidRequest( $request)
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.
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.