Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
9.40% covered (danger)
9.40%
55 / 585
0.00% covered (danger)
0.00%
0 / 64
CRAP
0.00% covered (danger)
0.00%
0 / 1
UploadBase
9.42% covered (danger)
9.42%
55 / 584
0.00% covered (danger)
0.00%
0 / 64
34571.22
0.00% covered (danger)
0.00%
0 / 1
 getVerificationErrorCode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isEnabled
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 isAllowed
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 isThrottled
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 createFromRequest
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
56
 isValidRequest
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDesiredDestName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSourceType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 initializePathInfo
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 initializeFromRequest
n/a
0 / 0
n/a
0 / 0
0
 setTempFile
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 fetchFile
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 canFetchFile
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isEmptyFile
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFileSize
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTempFileSha1Base36
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 getRealPath
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 verifyUpload
50.00% covered (danger)
50.00%
9 / 18
0.00% covered (danger)
0.00%
0 / 1
8.12
 validateName
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 verifyMimeType
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 verifyFile
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
30
 getFileProps
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 verifyPartialFile
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 authorizeUpload
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 checkWarnings
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
90
 makeWarningsSerializable
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 unserializeWarnings
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 checkBadFileName
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 checkUnwantedFileExtensions
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 checkFileSize
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 checkLocalFileExists
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
42
 checkLocalFileWasDeleted
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 checkAgainstExistingDupes
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
42
 checkAgainstArchiveDupes
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 performUpload
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
42
 postProcessUpload
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTitle
67.65% covered (warning)
67.65%
46 / 68
0.00% covered (danger)
0.00%
0 / 1
35.93
 getLocalFile
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 getStashFile
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 tryStashFile
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 skipStashFileAttempt
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 runUploadStashFileHook
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 doStashFile
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 cleanupTempFile
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 getTempPath
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 splitExtensions
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 checkFileExtension
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 checkFileExtensionList
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 verifyExtension
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 detectScript
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 detectVirus
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 checkOverwrite
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
30
 userCanReUpload
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
20
 getExistsWarning
0.00% covered (danger)
0.00%
0 / 53
0.00% covered (danger)
0.00%
0 / 1
132
 isThumbName
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 getFilenamePrefixBlacklist
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
42
 getImageInfo
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 convertVerifyErrorToStatus
0.00% covered (danger)
0.00%
0 / 47
0.00% covered (danger)
0.00%
0 / 1
182
 getMaxUploadSize
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getMaxPhpUploadSize
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getSessionStatus
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 setSessionStatus
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
30
 getUploadSessionKey
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getUploadSessionStore
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Base class for the backend of file upload.
4 *
5 * @license GPL-2.0-or-later
6 * @file
7 * @ingroup Upload
8 */
9
10namespace MediaWiki\Upload;
11
12use InvalidArgumentException;
13use LogicException;
14use MediaWiki\Api\ApiMessage;
15use MediaWiki\Api\ApiResult;
16use MediaWiki\Api\ApiUpload;
17use MediaWiki\Context\RequestContext;
18use MediaWiki\FileRepo\File\ArchivedFile;
19use MediaWiki\FileRepo\File\File;
20use MediaWiki\FileRepo\File\LocalFile;
21use MediaWiki\FileRepo\FileRepo;
22use MediaWiki\HookContainer\HookRunner;
23use MediaWiki\HookContainer\ProtectedHookAccessorTrait;
24use MediaWiki\Logger\LoggerFactory;
25use MediaWiki\MainConfigNames;
26use MediaWiki\MediaWikiServices;
27use MediaWiki\Message\Message;
28use MediaWiki\Permissions\Authority;
29use MediaWiki\Permissions\PermissionStatus;
30use MediaWiki\Request\WebRequest;
31use MediaWiki\Status\Status;
32use MediaWiki\Title\Title;
33use MediaWiki\Upload\Exception\UploadStashException;
34use MediaWiki\User\User;
35use MediaWiki\User\UserIdentity;
36use MWFileProps;
37use Wikimedia\FileBackend\FileBackend;
38use Wikimedia\FileBackend\FSFile\FSFile;
39use Wikimedia\FileBackend\FSFile\TempFSFile;
40use Wikimedia\Message\ListType;
41use Wikimedia\Message\MessageParam;
42use Wikimedia\Message\MessageSpecifier;
43use Wikimedia\ObjectCache\BagOStuff;
44use Wikimedia\Rdbms\IDBAccessObject;
45
46/**
47 * @defgroup Upload Upload related
48 */
49
50/**
51 * @ingroup Upload
52 *
53 * UploadBase and subclasses are the backend of MediaWiki's file uploads.
54 * The frontends are formed by ApiUpload and SpecialUpload.
55 *
56 * @stable to extend
57 *
58 * @author Brooke Vibber
59 * @author Bryan Tong Minh
60 * @author Michael Dale
61 */
62abstract class UploadBase {
63    use ProtectedHookAccessorTrait;
64
65    /** @var string|null Local file system path to the file to upload (or a local copy) */
66    protected $mTempPath;
67    /** @var TempFSFile|null Wrapper to handle deleting the temp file */
68    protected $tempFileObj;
69    /** @var string|null */
70    protected $mDesiredDestName;
71    /** @var string|null */
72    protected $mDestName;
73    /** @var bool|null */
74    protected $mRemoveTempFile;
75    /** @var string|null */
76    protected $mSourceType;
77    /** @var Title|false|null */
78    protected $mTitle = false;
79    /** @var int */
80    protected $mTitleError = 0;
81    /** @var string|null */
82    protected $mFilteredName;
83    /** @var string|null */
84    protected $mFinalExtension;
85    /** @var LocalFile|null */
86    protected $mLocalFile;
87    /** @var UploadStashFile|null */
88    protected $mStashFile;
89    /** @var int|null */
90    protected $mFileSize;
91    /** @var array|null */
92    protected $mFileProps;
93    /** @var string[] */
94    protected $mBlackListedExtensions;
95
96    private UploadVerification $uploadVerification;
97
98    public const SUCCESS = 0;
99    public const OK = 0;
100    public const EMPTY_FILE = 3;
101    public const MIN_LENGTH_PARTNAME = 4;
102    public const ILLEGAL_FILENAME = 5;
103    public const FILETYPE_MISSING = 8;
104    public const FILETYPE_BADTYPE = 9;
105    public const VERIFICATION_ERROR = 10;
106    public const FILE_TOO_LARGE = 12;
107    public const WINDOWS_NONASCII_FILENAME = 13;
108    public const FILENAME_TOO_LONG = 14;
109
110    private const CODE_TO_STATUS = [
111        self::EMPTY_FILE => 'empty-file',
112        self::FILE_TOO_LARGE => 'file-too-large',
113        self::FILETYPE_MISSING => 'filetype-missing',
114        self::FILETYPE_BADTYPE => 'filetype-banned',
115        self::MIN_LENGTH_PARTNAME => 'filename-tooshort',
116        self::ILLEGAL_FILENAME => 'illegal-filename',
117        self::VERIFICATION_ERROR => 'verification-error',
118        self::WINDOWS_NONASCII_FILENAME => 'windows-nonascii-filename',
119        self::FILENAME_TOO_LONG => 'filename-toolong',
120    ];
121
122    /**
123     * @param int $error
124     * @return string
125     */
126    public function getVerificationErrorCode( $error ) {
127        return self::CODE_TO_STATUS[$error] ?? 'unknown-error';
128    }
129
130    /**
131     * Returns true if uploads are enabled.
132     * Can be override by subclasses.
133     * @stable to override
134     * @return bool
135     */
136    public static function isEnabled() {
137        $enableUploads = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::EnableUploads );
138
139        return $enableUploads && wfIniGetBool( 'file_uploads' );
140    }
141
142    /**
143     * Returns true if the user can use this upload module or else a string
144     * identifying the missing permission.
145     * Can be overridden by subclasses.
146     *
147     * @param Authority $performer
148     * @return bool|string
149     */
150    public static function isAllowed( Authority $performer ) {
151        foreach ( [ 'upload', 'edit' ] as $permission ) {
152            if ( !$performer->isAllowed( $permission ) ) {
153                return $permission;
154            }
155        }
156
157        return true;
158    }
159
160    /**
161     * Returns true if the user has surpassed the upload rate limit, false otherwise.
162     *
163     * @deprecated since 1.41, use authorizeUpload() instead.
164     *  Rate limit checks are now implicit in permission checks.
165     *
166     * @param User $user
167     * @return bool
168     */
169    public static function isThrottled( $user ) {
170        wfDeprecated( __METHOD__, '1.41' );
171        return $user->pingLimiter( 'upload' );
172    }
173
174    /** @var string[] Upload handlers. Should probably just be a configuration variable. */
175    private static $uploadHandlers = [ 'Stash', 'File', 'Url' ];
176
177    /**
178     * Create a form of UploadBase depending on wpSourceType and initializes it.
179     *
180     * @param WebRequest &$request
181     * @param string|null $type
182     * @return null|self
183     */
184    public static function createFromRequest( &$request, $type = null ) {
185        $type = $type ?: $request->getVal( 'wpSourceType', 'File' );
186
187        if ( !$type ) {
188            return null;
189        }
190
191        // Get the upload class
192        $type = ucfirst( $type );
193
194        // Give hooks the chance to handle this request
195        /** @var class-string<self>|null $className */
196        $className = null;
197        ( new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ) )
198            // @phan-suppress-next-line PhanTypeMismatchArgument Type mismatch on pass-by-ref args
199            ->onUploadCreateFromRequest( $type, $className );
200        if ( $className === null ) {
201            $className = 'UploadFrom' . $type;
202            wfDebug( __METHOD__ . ": class name: $className" );
203            if ( !in_array( $type, self::$uploadHandlers ) ) {
204                return null;
205            }
206        }
207
208        if ( !$className::isEnabled() || !$className::isValidRequest( $request ) ) {
209            return null;
210        }
211
212        /** @var self $handler */
213        $handler = new $className;
214
215        $handler->initializeFromRequest( $request );
216
217        return $handler;
218    }
219
220    /**
221     * Check whether a request if valid for this handler.
222     * @param WebRequest $request
223     * @return bool
224     */
225    public static function isValidRequest( $request ) {
226        return false;
227    }
228
229    /**
230     * Get the desired destination name.
231     * @return string|null
232     */
233    public function getDesiredDestName() {
234        return $this->mDesiredDestName;
235    }
236
237    /**
238     * @stable to call
239     */
240    public function __construct() {
241        $this->uploadVerification = MediaWikiServices::getInstance()->getUploadVerification();
242    }
243
244    /**
245     * Returns the upload type. Should be overridden by child classes.
246     *
247     * @since 1.18
248     * @stable to override
249     * @return string|null
250     */
251    public function getSourceType() {
252        return null;
253    }
254
255    /**
256     * @param string $name The desired destination name
257     * @param string|null $tempPath Callers should make sure this is not a storage path
258     * @param int|null $fileSize
259     * @param bool $removeTempFile (false) remove the temporary file?
260     */
261    public function initializePathInfo( $name, $tempPath, $fileSize, $removeTempFile = false ) {
262        $this->mDesiredDestName = $name;
263        if ( FileBackend::isStoragePath( $tempPath ) ) {
264            throw new InvalidArgumentException( __METHOD__ . " given storage path `$tempPath`." );
265        }
266
267        $this->setTempFile( $tempPath, $fileSize );
268        $this->mRemoveTempFile = $removeTempFile;
269    }
270
271    /**
272     * Initialize from a WebRequest. Override this in a subclass.
273     *
274     * @param WebRequest &$request
275     */
276    abstract public function initializeFromRequest( &$request );
277
278    /**
279     * @param string|null $tempPath File system path to temporary file containing the upload
280     * @param int|null $fileSize
281     */
282    protected function setTempFile( $tempPath, $fileSize = null ) {
283        $this->mTempPath = $tempPath ?? '';
284        $this->mFileSize = $fileSize ?: null;
285        $this->mFileProps = null;
286        if ( $this->mTempPath !== '' && file_exists( $this->mTempPath ) ) {
287            $this->tempFileObj = new TempFSFile( $this->mTempPath );
288            if ( !$fileSize ) {
289                $this->mFileSize = filesize( $this->mTempPath );
290            }
291        } else {
292            $this->tempFileObj = null;
293        }
294    }
295
296    /**
297     * Fetch the file. Usually a no-op.
298     * @stable to override
299     * @return Status
300     */
301    public function fetchFile() {
302        return Status::newGood();
303    }
304
305    /**
306     * Perform checks to see if the file can be fetched. Usually a no-op.
307     * @stable to override
308     * @return Status
309     */
310    public function canFetchFile() {
311        return Status::newGood();
312    }
313
314    /**
315     * Return true if the file is empty.
316     * @return bool
317     */
318    public function isEmptyFile() {
319        return !$this->mFileSize;
320    }
321
322    /**
323     * Return the file size.
324     * @return int
325     */
326    public function getFileSize() {
327        return $this->mFileSize;
328    }
329
330    /**
331     * Get the base 36 SHA1 of the file.
332     * @stable to override
333     * @return string|false
334     */
335    public function getTempFileSha1Base36() {
336        // Use cached version if we already have it.
337        if ( $this->mFileProps && is_string( $this->mFileProps['sha1'] ) ) {
338            return $this->mFileProps['sha1'];
339        }
340        return FSFile::getSha1Base36FromPath( $this->mTempPath );
341    }
342
343    /**
344     * @param string $srcPath The source path
345     * @return string|false The real path if it was a virtual URL Returns false on failure
346     */
347    public function getRealPath( $srcPath ) {
348        $repo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
349        if ( FileRepo::isVirtualUrl( $srcPath ) ) {
350            /** @todo Just make uploads work with storage paths UploadFromStash
351             *  loads files via virtual URLs.
352             */
353            $tmpFile = $repo->getLocalCopy( $srcPath );
354            if ( $tmpFile ) {
355                $tmpFile->bind( $this ); // keep alive with $this
356            }
357            $path = $tmpFile ? $tmpFile->getPath() : false;
358        } else {
359            $path = $srcPath;
360        }
361
362        return $path;
363    }
364
365    /**
366     * Verify whether the upload is sensible.
367     *
368     * Return a status array representing the outcome of the verification.
369     * Possible keys are:
370     * - 'status': set to self::OK in case of success, or to one of the error constants defined in
371     *   this class in case of failure
372     * - 'max': set to the maximum allowed file size ($wgMaxUploadSize) if the upload is too large
373     * - 'details': set to error details if the file type is valid but contents are corrupt
374     * - 'filtered': set to the sanitized file name if the requested file name is invalid
375     * - 'finalExt': set to the file's file extension if it is not an allowed file extension
376     * - 'blacklistedExt': set to the list of disallowed file extensions if the current file extension
377     *    is not allowed for uploads and the list is not empty
378     *
379     * @stable to override
380     * @return mixed[] array representing the result of the verification
381     */
382    public function verifyUpload() {
383        /**
384         * If there was no filename or a zero size given, give up quick.
385         */
386        if ( $this->isEmptyFile() ) {
387            return [ 'status' => self::EMPTY_FILE ];
388        }
389
390        /**
391         * Honor $wgMaxUploadSize
392         */
393        $maxSize = self::getMaxUploadSize( $this->getSourceType() );
394        if ( $this->mFileSize > $maxSize ) {
395            return [
396                'status' => self::FILE_TOO_LARGE,
397                'max' => $maxSize,
398            ];
399        }
400
401        /**
402         * Look at the contents of the file; if we can recognize the
403         * type, but it's corrupt or data of the wrong type, we should
404         * probably not accept it.
405         */
406        $verification = $this->verifyFile();
407        if ( $verification !== true ) {
408            return [
409                'status' => self::VERIFICATION_ERROR,
410                'details' => $verification
411            ];
412        }
413
414        /**
415         * Make sure this file can be created
416         */
417        $result = $this->validateName();
418        if ( $result !== true ) {
419            return $result;
420        }
421
422        return [ 'status' => self::OK ];
423    }
424
425    /**
426     * Verify that the name is valid and, if necessary, that we can overwrite
427     *
428     * @return array|bool True if valid, otherwise an array with 'status'
429     * and other keys
430     */
431    public function validateName() {
432        $nt = $this->getTitle();
433        if ( $nt === null ) {
434            $result = [ 'status' => $this->mTitleError ];
435            if ( $this->mTitleError === self::ILLEGAL_FILENAME ) {
436                $result['filtered'] = $this->mFilteredName;
437            }
438            if ( $this->mTitleError === self::FILETYPE_BADTYPE ) {
439                $result['finalExt'] = $this->mFinalExtension;
440                if ( count( $this->mBlackListedExtensions ) ) {
441                    $result['blacklistedExt'] = $this->mBlackListedExtensions;
442                }
443            }
444
445            return $result;
446        }
447        $this->mDestName = $this->getLocalFile()->getName();
448
449        return true;
450    }
451
452    /**
453     * Verify the MIME type.
454     *
455     * @note Only checks that it is not an evil MIME.
456     *  The "does it have the correct file extension given its MIME type?" check is in verifyFile.
457     * @param string $mime Representing the MIME
458     * @return array|bool True if the file is verified, an array otherwise
459     */
460    protected function verifyMimeType( $mime ) {
461        $verifyMimeType = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::VerifyMimeType );
462        if ( $verifyMimeType ) {
463            wfDebug( "mime: <$mime> extension: <{$this->mFinalExtension}>" );
464            $mimeTypeExclusions = MediaWikiServices::getInstance()->getMainConfig()
465                ->get( MainConfigNames::MimeTypeExclusions );
466            if ( self::checkFileExtension( $mime, $mimeTypeExclusions ) ) {
467                return [ 'filetype-badmime', $mime ];
468            }
469        }
470
471        return true;
472    }
473
474    /**
475     * Verifies that it's ok to include the uploaded file
476     *
477     * @return array|true True of the file is verified, array otherwise.
478     */
479    protected function verifyFile() {
480        $status = $this->verifyPartialFile();
481        if ( $status !== true ) {
482            return $status;
483        }
484
485        $res = $this->uploadVerification->verifyFile(
486            $this->mTempPath,
487            (string)$this->mFinalExtension,
488            $this->getFileProps()
489        );
490        if ( $res !== true ) {
491            return $res;
492        }
493
494        $error = true;
495        $mime = $this->getFileProps()['mime'];
496        $this->getHookRunner()->onUploadVerifyFile( $this, $mime, $error );
497        if ( $error !== true ) {
498            if ( !is_array( $error ) ) {
499                $error = [ $error ];
500            }
501            return $error;
502        }
503
504        return true;
505    }
506
507    /**
508     * File props is very expensive on large files (due to sha1 calc)
509     * so it is important we save the result to reuse
510     *
511     * @return array List of file properties
512     */
513    protected function getFileProps(): array {
514        // Force $this->mFinalExtension to be populated.
515        $this->getTitle();
516
517        if ( !is_array( $this->mFileProps ) ) {
518            $mwProps = new MWFileProps( MediaWikiServices::getInstance()->getMimeAnalyzer() );
519            $this->mFileProps = $mwProps->getPropsFromPath( $this->mTempPath, $this->mFinalExtension );
520        }
521        return $this->mFileProps;
522    }
523
524    /**
525     * A verification routine suitable for partial files
526     *
527     * Runs the deny list checks, but not any checks that may
528     * assume the entire file is present.
529     *
530     * @return array|true True, if the file is valid, else an array with error message key.
531     * @phan-return non-empty-array|true
532     */
533    protected function verifyPartialFile() {
534        // Needed to set mFinalExtension.
535        $this->getTitle();
536        return $this->uploadVerification->verifyPartialFile(
537            $this->mTempPath,
538            (string)$this->mFinalExtension,
539            $this->getFileProps()
540        );
541    }
542
543    /**
544     * Check whether the user can upload the image. This method checks against the current title.
545     * Use verifyUpload() or validateName() first to check that the title is valid.
546     */
547    public function authorizeUpload( Authority $performer ): PermissionStatus {
548        $status = PermissionStatus::newEmpty();
549
550        $nt = $this->getTitle();
551        if ( $nt === null ) {
552            throw new LogicException( __METHOD__ . ' must only be called with valid title' );
553        }
554
555        $performer->authorizeWrite( 'edit', $nt, $status );
556        $performer->authorizeWrite( 'upload', $nt, $status );
557        if ( !$status->isGood() ) {
558            // If the user can't upload at all, don't display additional errors about re-uploading
559            return $status;
560        }
561
562        $overwriteError = $this->checkOverwrite( $performer );
563        if ( $overwriteError !== true ) {
564            $status->fatal( ...$overwriteError );
565        }
566
567        return $status;
568    }
569
570    /**
571     * Check for non fatal problems with the file.
572     *
573     * This should not assume that mTempPath is set.
574     *
575     * @param User|null $user Accepted since 1.35
576     *
577     * @return mixed[] Array of warnings
578     */
579    public function checkWarnings( $user = null ) {
580        if ( $user === null ) {
581            // TODO check uses and hard deprecate
582            $user = RequestContext::getMain()->getUser();
583        }
584
585        $warnings = [];
586
587        $localFile = $this->getLocalFile();
588        $localFile->load( IDBAccessObject::READ_LATEST );
589        $filename = $localFile->getName();
590        $hash = $this->getTempFileSha1Base36();
591
592        $badFileName = $this->checkBadFileName( $filename, $this->mDesiredDestName );
593        if ( $badFileName !== null ) {
594            $warnings['badfilename'] = $badFileName;
595        }
596
597        $unwantedFileExtensionDetails = $this->checkUnwantedFileExtensions( (string)$this->mFinalExtension );
598        if ( $unwantedFileExtensionDetails !== null ) {
599            $warnings['filetype-unwanted-type'] = $unwantedFileExtensionDetails;
600        }
601
602        $fileSizeWarnings = $this->checkFileSize( $this->mFileSize );
603        if ( $fileSizeWarnings ) {
604            $warnings = array_merge( $warnings, $fileSizeWarnings );
605        }
606
607        $localFileExistsWarnings = $this->checkLocalFileExists( $localFile, $hash );
608        if ( $localFileExistsWarnings ) {
609            $warnings = array_merge( $warnings, $localFileExistsWarnings );
610        }
611
612        if ( $this->checkLocalFileWasDeleted( $localFile ) ) {
613            $warnings['was-deleted'] = $filename;
614        }
615
616        // If a file with the same name exists locally then the local file has already been tested
617        // for duplication of content
618        $ignoreLocalDupes = isset( $warnings['exists'] );
619        $dupes = $this->checkAgainstExistingDupes( $hash, $ignoreLocalDupes );
620        if ( $dupes ) {
621            $warnings['duplicate'] = $dupes;
622        }
623
624        $archivedDupes = $this->checkAgainstArchiveDupes( $hash, $user );
625        if ( $archivedDupes !== null ) {
626            $warnings['duplicate-archive'] = $archivedDupes;
627        }
628
629        return $warnings;
630    }
631
632    /**
633     * Convert the warnings array returned by checkWarnings() to something that
634     * can be serialized, and that is suitable for inclusion directly in action API results.
635     *
636     * File objects will be converted to an associative array with the following keys:
637     *
638     *   - fileName: The name of the file
639     *   - timestamp: The upload timestamp
640     *
641     * @param mixed[] $warnings
642     * @return mixed[]
643     */
644    public static function makeWarningsSerializable( $warnings ) {
645        array_walk_recursive( $warnings, static function ( &$param, $key ) {
646            if ( $param instanceof File ) {
647                $param = [
648                    'fileName' => $param->getName(),
649                    'timestamp' => $param->getTimestamp()
650                ];
651            } elseif ( $param instanceof MessageParam ) {
652                // Do nothing (T390001)
653            } elseif ( is_object( $param ) ) {
654                throw new InvalidArgumentException(
655                    'UploadBase::makeWarningsSerializable: ' .
656                    'Unexpected object of class ' . get_class( $param ) );
657            }
658        } );
659        return $warnings;
660    }
661
662    /**
663     * Convert the serialized warnings array created by makeWarningsSerializable()
664     * back to the output of checkWarnings().
665     *
666     * @param mixed[] $warnings
667     * @return mixed[]
668     */
669    public static function unserializeWarnings( $warnings ) {
670        foreach ( $warnings as $key => $value ) {
671            if ( is_array( $value ) ) {
672                if ( isset( $value['fileName'] ) && isset( $value['timestamp'] ) ) {
673                    $warnings[$key] = MediaWikiServices::getInstance()->getRepoGroup()->findFile(
674                        $value['fileName'],
675                        [ 'time' => $value['timestamp'] ]
676                    );
677                } else {
678                    $warnings[$key] = self::unserializeWarnings( $value );
679                }
680            }
681        }
682        return $warnings;
683    }
684
685    /**
686     * Check whether the resulting filename is different from the desired one,
687     * but ignore things like ucfirst() and spaces/underscore things
688     *
689     * @param string $filename
690     * @param string $desiredFileName
691     *
692     * @return string|null String that was determined to be bad or null if the filename is okay
693     */
694    private function checkBadFileName( $filename, $desiredFileName ) {
695        $comparableName = str_replace( ' ', '_', $desiredFileName );
696        $comparableName = Title::capitalize( $comparableName, NS_FILE );
697
698        if ( $desiredFileName != $filename && $comparableName != $filename ) {
699            return $filename;
700        }
701
702        return null;
703    }
704
705    /**
706     * @param string $fileExtension The file extension to check
707     *
708     * @return array|null array with the following keys:
709     *                    0 => string The final extension being used
710     *                    1 => string[] The extensions that are allowed
711     *                    2 => int The number of extensions that are allowed.
712     */
713    private function checkUnwantedFileExtensions( $fileExtension ) {
714        $checkFileExtensions = MediaWikiServices::getInstance()->getMainConfig()
715            ->get( MainConfigNames::CheckFileExtensions );
716        $fileExtensions = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::FileExtensions );
717        if ( $checkFileExtensions ) {
718            $extensions = array_unique( $fileExtensions );
719            if ( !self::checkFileExtension( $fileExtension, $extensions ) ) {
720                return [
721                    $fileExtension,
722                    Message::listParam( $extensions, ListType::COMMA ),
723                    count( $extensions )
724                ];
725            }
726        }
727
728        return null;
729    }
730
731    /**
732     * @param int $fileSize
733     *
734     * @return array warnings
735     */
736    private function checkFileSize( $fileSize ) {
737        $uploadSizeWarning = MediaWikiServices::getInstance()->getMainConfig()
738            ->get( MainConfigNames::UploadSizeWarning );
739
740        $warnings = [];
741
742        if ( $uploadSizeWarning && ( $fileSize > $uploadSizeWarning ) ) {
743            $warnings['large-file'] = [
744                Message::sizeParam( $uploadSizeWarning ),
745                Message::sizeParam( $fileSize ),
746            ];
747        }
748
749        if ( $fileSize == 0 ) {
750            $warnings['empty-file'] = true;
751        }
752
753        return $warnings;
754    }
755
756    /**
757     * @param LocalFile $localFile
758     * @param string|false $hash sha1 hash of the file to check
759     *
760     * @return array warnings
761     */
762    private function checkLocalFileExists( LocalFile $localFile, $hash ) {
763        $warnings = [];
764
765        $exists = self::getExistsWarning( $localFile );
766        if ( $exists !== false ) {
767            $warnings['exists'] = $exists;
768
769            // check if file is an exact duplicate of current file version
770            if ( $hash !== false && $hash === $localFile->getSha1() ) {
771                $warnings['no-change'] = $localFile;
772            }
773
774            // check if file is an exact duplicate of older versions of this file
775            $history = $localFile->getHistory();
776            foreach ( $history as $oldFile ) {
777                if ( $hash === $oldFile->getSha1() ) {
778                    $warnings['duplicate-version'][] = $oldFile;
779                }
780            }
781        }
782
783        return $warnings;
784    }
785
786    private function checkLocalFileWasDeleted( LocalFile $localFile ): bool {
787        return $localFile->wasDeleted() && !$localFile->exists();
788    }
789
790    /**
791     * @param string|false $hash sha1 hash of the file to check
792     * @param bool $ignoreLocalDupes True to ignore local duplicates
793     *
794     * @return File[] Duplicate files, if found.
795     */
796    private function checkAgainstExistingDupes( $hash, $ignoreLocalDupes ) {
797        if ( $hash === false ) {
798            return [];
799        }
800        $dupes = MediaWikiServices::getInstance()->getRepoGroup()->findBySha1( $hash );
801        $title = $this->getTitle();
802        foreach ( $dupes as $key => $dupe ) {
803            if (
804                ( $dupe instanceof LocalFile ) &&
805                $ignoreLocalDupes &&
806                $title->equals( $dupe->getTitle() )
807            ) {
808                unset( $dupes[$key] );
809            }
810        }
811
812        return $dupes;
813    }
814
815    /**
816     * @param string|false $hash sha1 hash of the file to check
817     * @param Authority $performer
818     *
819     * @return string|null Name of the dupe or empty string if discovered (depending on visibility)
820     *                     null if the check discovered no dupes.
821     */
822    private function checkAgainstArchiveDupes( $hash, Authority $performer ) {
823        if ( $hash === false ) {
824            return null;
825        }
826        $archivedFile = new ArchivedFile( null, 0, '', $hash );
827        if ( $archivedFile->getID() > 0 ) {
828            if ( $archivedFile->userCan( File::DELETED_FILE, $performer ) ) {
829                return $archivedFile->getName();
830            }
831            return '';
832        }
833
834        return null;
835    }
836
837    /**
838     * Really perform the upload. Stores the file in the local repo, watches
839     * if necessary and runs the UploadComplete hook.
840     *
841     * @param string $comment
842     * @param string|false $pageText
843     * @param bool $watch Whether the file page should be added to user's watchlist.
844     *   (This doesn't check $user's permissions.)
845     * @param User $user
846     * @param string[] $tags Change tags to add to the log entry and page revision.
847     *   (This doesn't check $user's permissions.)
848     * @param string|null $watchlistExpiry Optional watchlist expiry timestamp in any format
849     *   acceptable to wfTimestamp().
850     * @return Status Indicating the whether the upload succeeded.
851     *
852     * @since 1.35 Accepts $watchlistExpiry parameter.
853     */
854    public function performUpload(
855        $comment, $pageText, $watch, $user, $tags = [], ?string $watchlistExpiry = null
856    ) {
857        $this->getLocalFile()->load( IDBAccessObject::READ_LATEST );
858        $props = $this->mFileProps;
859
860        $error = null;
861        $this->getHookRunner()->onUploadVerifyUpload( $this, $user, $props, $comment, $pageText, $error );
862        if ( $error ) {
863            if ( !is_array( $error ) ) {
864                $error = [ $error ];
865            }
866            return Status::newFatal( ...$error );
867        }
868
869        $status = $this->getLocalFile()->upload(
870            $this->mTempPath,
871            $comment,
872            $pageText !== false ? $pageText : '',
873            File::DELETE_SOURCE,
874            $props,
875            false,
876            $user,
877            $tags
878        );
879
880        if ( $status->isGood() ) {
881            if ( $watch ) {
882                MediaWikiServices::getInstance()->getWatchlistManager()->addWatchIgnoringRights(
883                    $user,
884                    $this->getLocalFile()->getTitle(),
885                    $watchlistExpiry
886                );
887            }
888            $this->getHookRunner()->onUploadComplete( $this );
889
890            $this->postProcessUpload();
891        }
892
893        return $status;
894    }
895
896    /**
897     * Perform extra steps after a successful upload.
898     *
899     * @stable to override
900     * @since  1.25
901     */
902    public function postProcessUpload() {
903    }
904
905    /**
906     * Returns the title of the file to be uploaded. Sets mTitleError in case
907     * the name was illegal.
908     *
909     * @return Title|null The title of the file or null in case the name was illegal
910     */
911    public function getTitle() {
912        if ( $this->mTitle !== false ) {
913            return $this->mTitle;
914        }
915        if ( !is_string( $this->mDesiredDestName ) ) {
916            $this->mTitleError = self::ILLEGAL_FILENAME;
917            $this->mTitle = null;
918
919            return $this->mTitle;
920        }
921        /* Assume that if a user specified File:Something.jpg, this is an error
922         * and that the namespace prefix needs to be stripped of.
923         */
924        $title = Title::newFromText( $this->mDesiredDestName );
925        if ( $title && $title->getNamespace() === NS_FILE ) {
926            $this->mFilteredName = $title->getDBkey();
927        } else {
928            $this->mFilteredName = $this->mDesiredDestName;
929        }
930
931        # oi_archive_name is max 255 bytes, which include a timestamp and an
932        # exclamation mark, so restrict file name to 240 bytes.
933        if ( strlen( $this->mFilteredName ) > 240 ) {
934            $this->mTitleError = self::FILENAME_TOO_LONG;
935            $this->mTitle = null;
936
937            return $this->mTitle;
938        }
939
940        /**
941         * Chop off any directories in the given filename. Then
942         * filter out illegal characters, and try to make a legible name
943         * out of it. We'll strip some silently that Title would die on.
944         */
945        $this->mFilteredName = wfStripIllegalFilenameChars( $this->mFilteredName );
946        /* Normalize to title form before we do any further processing */
947        $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
948        if ( $nt === null ) {
949            $this->mTitleError = self::ILLEGAL_FILENAME;
950            $this->mTitle = null;
951
952            return $this->mTitle;
953        }
954        $this->mFilteredName = $nt->getDBkey();
955
956        /**
957         * We'll want to prevent against *any* 'extension', and use
958         * only the final one for the allow list.
959         */
960        [ $partname, $ext ] = self::splitExtensions( $this->mFilteredName );
961
962        if ( $ext !== [] ) {
963            $this->mFinalExtension = trim( end( $ext ) );
964        } else {
965            $this->mFinalExtension = '';
966
967            // No extension, try guessing one from the temporary file
968            // FIXME: Sometimes we mTempPath isn't set yet here, possibly due to an unrealistic
969            // or incomplete test case in UploadBaseTest (T272328)
970            if ( $this->mTempPath !== null ) {
971                $magic = MediaWikiServices::getInstance()->getMimeAnalyzer();
972                $mime = $magic->guessMimeType( $this->mTempPath );
973                if ( $mime !== 'unknown/unknown' ) {
974                    # Get a space separated list of extensions
975                    $mimeExt = $magic->getExtensionFromMimeTypeOrNull( $mime );
976                    if ( $mimeExt !== null ) {
977                        # Set the extension to the canonical extension
978                        $this->mFinalExtension = $mimeExt;
979
980                        # Fix up the other variables
981                        $this->mFilteredName .= ".{$this->mFinalExtension}";
982                        $nt = Title::makeTitleSafe( NS_FILE, $this->mFilteredName );
983                        $ext = [ $this->mFinalExtension ];
984                    }
985                }
986            }
987        }
988
989        // Don't allow users to override the list of prohibited file extensions (check file extension)
990        $config = MediaWikiServices::getInstance()->getMainConfig();
991        $checkFileExtensions = $config->get( MainConfigNames::CheckFileExtensions );
992        $strictFileExtensions = $config->get( MainConfigNames::StrictFileExtensions );
993        $fileExtensions = $config->get( MainConfigNames::FileExtensions );
994        $prohibitedFileExtensions = $config->get( MainConfigNames::ProhibitedFileExtensions );
995
996        $badList = self::checkFileExtensionList( $ext, $prohibitedFileExtensions );
997
998        if ( $this->mFinalExtension == '' ) {
999            $this->mTitleError = self::FILETYPE_MISSING;
1000            $this->mTitle = null;
1001
1002            return $this->mTitle;
1003        }
1004
1005        if ( $badList ||
1006            ( $checkFileExtensions && $strictFileExtensions &&
1007                !self::checkFileExtension( $this->mFinalExtension, $fileExtensions ) )
1008        ) {
1009            $this->mBlackListedExtensions = $badList;
1010            $this->mTitleError = self::FILETYPE_BADTYPE;
1011            $this->mTitle = null;
1012
1013            return $this->mTitle;
1014        }
1015
1016        // Windows may be broken with special characters, see T3780
1017        if ( !preg_match( '/^[\x0-\x7f]*$/', $nt->getText() )
1018            && !MediaWikiServices::getInstance()->getRepoGroup()
1019                ->getLocalRepo()->backendSupportsUnicodePaths()
1020        ) {
1021            $this->mTitleError = self::WINDOWS_NONASCII_FILENAME;
1022            $this->mTitle = null;
1023
1024            return $this->mTitle;
1025        }
1026
1027        # If there was more than one file "extension", reassemble the base
1028        # filename to prevent bogus complaints about length
1029        if ( count( $ext ) > 1 ) {
1030            $iterations = count( $ext ) - 1;
1031            for ( $i = 0; $i < $iterations; $i++ ) {
1032                $partname .= '.' . $ext[$i];
1033            }
1034        }
1035
1036        if ( strlen( $partname ) < 1 ) {
1037            $this->mTitleError = self::MIN_LENGTH_PARTNAME;
1038            $this->mTitle = null;
1039
1040            return $this->mTitle;
1041        }
1042
1043        $this->mTitle = $nt;
1044
1045        return $this->mTitle;
1046    }
1047
1048    /**
1049     * Return the local file and initializes if necessary.
1050     *
1051     * @stable to override
1052     * @return LocalFile|null
1053     */
1054    public function getLocalFile() {
1055        if ( $this->mLocalFile === null ) {
1056            $nt = $this->getTitle();
1057            $this->mLocalFile = $nt === null
1058                ? null
1059                : MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo()->newFile( $nt );
1060        }
1061
1062        return $this->mLocalFile;
1063    }
1064
1065    /**
1066     * @return UploadStashFile|null
1067     */
1068    public function getStashFile() {
1069        return $this->mStashFile;
1070    }
1071
1072    /**
1073     * Like stashFile(), but respects extensions' wishes to prevent the stashing. verifyUpload() must
1074     * be called before calling this method (unless $isPartial is true).
1075     *
1076     * Upload stash exceptions are also caught and converted to an error status.
1077     *
1078     * @since 1.28
1079     * @stable to override
1080     * @param User $user
1081     * @param bool $isPartial Pass `true` if this is a part of a chunked upload (not a complete file).
1082     * @return Status If successful, value is an UploadStashFile instance
1083     */
1084    public function tryStashFile( User $user, $isPartial = false ) {
1085        if ( !$isPartial ) {
1086            $error = $this->runUploadStashFileHook( $user );
1087            if ( $error ) {
1088                return Status::newFatal( ...$error );
1089            }
1090        }
1091        try {
1092            $file = $this->doStashFile( $user );
1093            return Status::newGood( $file );
1094        } catch ( UploadStashException $e ) {
1095            return Status::newFatal( 'uploadstash-exception', get_class( $e ), $e->getMessage() );
1096        }
1097    }
1098
1099    /**
1100     * Check, if stash file attempt should be skipped,
1101     * for example when the file is already known to stash.
1102     *
1103     * @since 1.46
1104     * @stable to override
1105     */
1106    public function skipStashFileAttempt(): bool {
1107        return $this->getStashFile() !== null;
1108    }
1109
1110    /**
1111     * @param User $user
1112     * @return array|null Error message and parameters, null if there's no error
1113     */
1114    protected function runUploadStashFileHook( User $user ) {
1115        $props = $this->mFileProps;
1116        $error = null;
1117        $this->getHookRunner()->onUploadStashFile( $this, $user, $props, $error );
1118        if ( $error && !is_array( $error ) ) {
1119            $error = [ $error ];
1120        }
1121        return $error;
1122    }
1123
1124    /**
1125     * Implementation for stashFile() and tryStashFile().
1126     *
1127     * @stable to override
1128     * @param User|null $user
1129     * @return UploadStashFile Stashed file
1130     */
1131    protected function doStashFile( ?User $user = null ) {
1132        $stash = MediaWikiServices::getInstance()->getRepoGroup()
1133            ->getLocalRepo()->getUploadStash( $user );
1134        $file = $stash->stashFile( $this->mTempPath, $this->getSourceType(), $this->mFileProps );
1135        $this->mStashFile = $file;
1136
1137        return $file;
1138    }
1139
1140    /**
1141     * If we've modified the upload file, then we need to manually remove it
1142     * on exit to clean up.
1143     */
1144    public function cleanupTempFile() {
1145        if ( $this->mRemoveTempFile && $this->tempFileObj ) {
1146            // Delete when all relevant TempFSFile handles go out of scope
1147            wfDebug( __METHOD__ . ": Marked temporary file '{$this->mTempPath}' for removal" );
1148            $this->tempFileObj->autocollect();
1149        }
1150    }
1151
1152    /**
1153     * @return string|null
1154     */
1155    public function getTempPath() {
1156        return $this->mTempPath;
1157    }
1158
1159    /**
1160     * Split a file into a base name and all dot-delimited 'extensions'
1161     * on the end. Some web server configurations will fall back to
1162     * earlier pseudo-'extensions' to determine type and execute
1163     * scripts, so we need to check them all.
1164     *
1165     * @param string $filename
1166     * @return array [ string, string[] ]
1167     */
1168    public static function splitExtensions( $filename ) {
1169        $bits = explode( '.', $filename );
1170        $basename = array_shift( $bits );
1171
1172        return [ $basename, $bits ];
1173    }
1174
1175    /**
1176     * Perform case-insensitive match against a list of file extensions.
1177     *
1178     * @param string $ext File extension
1179     * @param array $list
1180     * @return bool Returns true if the extension is in the list.
1181     */
1182    public static function checkFileExtension( $ext, $list ) {
1183        return in_array( strtolower( $ext ?? '' ), $list, true );
1184    }
1185
1186    /**
1187     * Perform case-insensitive match against a list of file extensions.
1188     * Returns an array of matching extensions.
1189     *
1190     * @param string[] $ext File extensions
1191     * @param string[] $list
1192     * @return string[]
1193     */
1194    public static function checkFileExtensionList( $ext, $list ) {
1195        return array_intersect( array_map( 'strtolower', $ext ), $list );
1196    }
1197
1198    /**
1199     * Checks if the MIME type of the uploaded file matches the file extension.
1200     *
1201     * @deprecated 1.45
1202     * @param string $mime The MIME type of the uploaded file
1203     * @param string $extension The filename extension that the file is to be served with
1204     * @return bool
1205     */
1206    public static function verifyExtension( $mime, $extension ) {
1207        wfDeprecated( __METHOD__, '1.45' );
1208        // External callers should probably be using verifyFile and not this method.
1209        $verify = MediaWikiServices::getInstance()->getUploadVerification();
1210        return $verify->verifyExtension( $mime, $extension );
1211    }
1212
1213    /**
1214     * Heuristic for detecting files that *could* contain JavaScript instructions or
1215     * things that may look like HTML to a browser and are thus
1216     * potentially harmful. The present implementation will produce false
1217     * positives in some situations.
1218     *
1219     * @warning This only does some of the checks and should not be used to verify files by itself.
1220     *
1221     * @deprecated 1.45 use UploadVerification::verifyFile() instead
1222     * @param string|null $file Pathname to the temporary upload file
1223     * @param string $mime The MIME type of the file
1224     * @param string|null $extension The extension of the file
1225     * @return bool True if the file contains something looking like embedded scripts
1226     */
1227    public static function detectScript( $file, $mime, $extension ) {
1228        wfDeprecated( __METHOD__, '1.45' );
1229        // When replacing usage of this in extensions, use UploadVerification::verifyFile.
1230        // detectScript is unlikely to be the method you want.
1231        $verify = MediaWikiServices::getInstance()->getUploadVerification();
1232        return $verify->detectScript( $file, $mime, $extension );
1233    }
1234
1235    /**
1236     * Generic wrapper function for a virus scanner program.
1237     * This relies on the $wgAntivirus and $wgAntivirusSetup variables.
1238     * $wgAntivirusRequired may be used to deny upload if the scan fails.
1239     *
1240     * @param string $file Pathname to the temporary upload file
1241     * @return bool|null|string False if not virus is found, null if the scan fails or is disabled,
1242     *   or a string containing feedback from the virus scanner if a virus was found.
1243     *   If textual feedback is missing but a virus was found, this function returns true.
1244     * @deprecated 1.45 Use UploadVerification->detectVirus() directly.
1245     */
1246    public static function detectVirus( $file ) {
1247        wfDeprecated( __METHOD__, '1.45' );
1248        $uploadVerification = MediaWikiServices::getInstance()->getUploadVerification();
1249        return $uploadVerification->detectVirus( $file );
1250    }
1251
1252    /**
1253     * Check if there's a file overwrite conflict and, if so, if restrictions
1254     * forbid this user from performing the upload.
1255     *
1256     * @param Authority $performer
1257     *
1258     * @return bool|array
1259     * @phan-return true|non-empty-array
1260     */
1261    private function checkOverwrite( Authority $performer ) {
1262        // First check whether the local file can be overwritten
1263        $file = $this->getLocalFile();
1264        $file->load( IDBAccessObject::READ_LATEST );
1265        if ( $file->exists() ) {
1266            if ( !self::userCanReUpload( $performer, $file ) ) {
1267                return [ 'fileexists-forbidden', $file->getName() ];
1268            }
1269
1270            return true;
1271        }
1272
1273        $services = MediaWikiServices::getInstance();
1274
1275        /* Check shared conflicts: if the local file does not exist, but
1276         * RepoGroup::findFile finds a file, it exists in a shared repository.
1277         */
1278        $file = $services->getRepoGroup()->findFile( $this->getTitle(), [ 'latest' => true ] );
1279        if ( $file && !$performer->isAllowed( 'reupload-shared' ) ) {
1280            return [ 'fileexists-shared-forbidden', $file->getName() ];
1281        }
1282
1283        return true;
1284    }
1285
1286    /**
1287     * Check if a user is the last uploader
1288     *
1289     * @param Authority $performer
1290     * @param File $img
1291     * @return bool
1292     */
1293    public static function userCanReUpload( Authority $performer, File $img ) {
1294        if ( $performer->isAllowed( 'reupload' ) ) {
1295            return true; // non-conditional
1296        }
1297
1298        if ( !$performer->isAllowed( 'reupload-own' ) ) {
1299            return false;
1300        }
1301
1302        if ( !( $img instanceof LocalFile ) ) {
1303            return false;
1304        }
1305
1306        return $performer->getUser()->equals( $img->getUploader( File::RAW ) );
1307    }
1308
1309    /**
1310     * Helper function that does various existence checks for a file.
1311     * The following checks are performed:
1312     * - If the file exists
1313     * - If an article with the same name as the file exists
1314     * - If a file exists with normalized extension
1315     * - If the file looks like a thumbnail and the original exists
1316     *
1317     * @param File $file The File object to check
1318     * @return array|false False if the file does not exist, else an array
1319     */
1320    public static function getExistsWarning( $file ) {
1321        if ( $file->exists() ) {
1322            return [ 'warning' => 'exists', 'file' => $file ];
1323        }
1324
1325        if ( $file->getTitle()->getArticleID() ) {
1326            return [ 'warning' => 'page-exists', 'file' => $file ];
1327        }
1328
1329        $n = strrpos( $file->getName(), '.' );
1330        if ( $n > 0 ) {
1331            $partname = substr( $file->getName(), 0, $n );
1332            $extension = substr( $file->getName(), $n + 1 );
1333        } else {
1334            $partname = $file->getName();
1335            $extension = '';
1336        }
1337        $normalizedExtension = File::normalizeExtension( $extension );
1338        $localRepo = MediaWikiServices::getInstance()->getRepoGroup()->getLocalRepo();
1339
1340        if ( $normalizedExtension != $extension ) {
1341            // We're not using the normalized form of the extension.
1342            // Normal form is lowercase, using most common of alternate
1343            // extensions (e.g. 'jpg' rather than 'JPEG').
1344
1345            // Check for another file using the normalized form...
1346            $nt_lc = Title::makeTitle( NS_FILE, "{$partname}.{$normalizedExtension}" );
1347            $file_lc = $localRepo->newFile( $nt_lc );
1348
1349            if ( $file_lc->exists() ) {
1350                return [
1351                    'warning' => 'exists-normalized',
1352                    'file' => $file,
1353                    'normalizedFile' => $file_lc
1354                ];
1355            }
1356        }
1357
1358        // Check for files with the same name but a different extension
1359        $similarFiles = $localRepo->findFilesByPrefix( "{$partname}.", 1 );
1360        if ( count( $similarFiles ) ) {
1361            return [
1362                'warning' => 'exists-normalized',
1363                'file' => $file,
1364                'normalizedFile' => $similarFiles[0],
1365            ];
1366        }
1367
1368        if ( self::isThumbName( $file->getName() ) ) {
1369            // Check for filenames like 50px- or 180px-, these are mostly thumbnails
1370            $nt_thb = Title::newFromText(
1371                substr( $partname, strpos( $partname, '-' ) + 1 ) . '.' . $extension,
1372                NS_FILE
1373            );
1374            $file_thb = $localRepo->newFile( $nt_thb );
1375            if ( $file_thb->exists() ) {
1376                return [
1377                    'warning' => 'thumb',
1378                    'file' => $file,
1379                    'thumbFile' => $file_thb
1380                ];
1381            }
1382
1383            // The file does not exist, but we just don't like the name
1384            return [
1385                'warning' => 'thumb-name',
1386                'file' => $file,
1387                'thumbFile' => $file_thb
1388            ];
1389        }
1390
1391        foreach ( self::getFilenamePrefixBlacklist() as $prefix ) {
1392            if ( str_starts_with( $partname, $prefix ) ) {
1393                return [
1394                    'warning' => 'bad-prefix',
1395                    'file' => $file,
1396                    'prefix' => $prefix
1397                ];
1398            }
1399        }
1400
1401        return false;
1402    }
1403
1404    /**
1405     * Helper function that checks whether the filename looks like a thumbnail
1406     * @param string $filename
1407     * @return bool
1408     */
1409    public static function isThumbName( $filename ) {
1410        $n = strrpos( $filename, '.' );
1411        $partname = $n ? substr( $filename, 0, $n ) : $filename;
1412
1413        return (
1414                substr( $partname, 3, 3 ) === 'px-' ||
1415                substr( $partname, 2, 3 ) === 'px-'
1416            ) && preg_match( "/[0-9]{2}/", substr( $partname, 0, 2 ) );
1417    }
1418
1419    /**
1420     * Get a list of disallowed filename prefixes from [[MediaWiki:Filename-prefix-blacklist]]
1421     *
1422     * @return string[] List of prefixes
1423     */
1424    public static function getFilenamePrefixBlacklist() {
1425        $list = [];
1426        $message = wfMessage( 'filename-prefix-blacklist' )->inContentLanguage();
1427        if ( !$message->isDisabled() ) {
1428            $lines = explode( "\n", $message->plain() );
1429            foreach ( $lines as $line ) {
1430                // Remove comment lines
1431                $comment = substr( trim( $line ), 0, 1 );
1432                if ( $comment === '#' || $comment == '' ) {
1433                    continue;
1434                }
1435                // Remove additional comments after a prefix
1436                $comment = strpos( $line, '#' );
1437                if ( $comment > 0 ) {
1438                    $line = substr( $line, 0, $comment - 1 );
1439                }
1440                $list[] = trim( $line );
1441            }
1442        }
1443
1444        return $list;
1445    }
1446
1447    /**
1448     * Gets image info about the file just uploaded.
1449     *
1450     * @deprecated since 1.42, subclasses of ApiUpload can use
1451     * ApiUpload::getUploadImageInfo() instead.
1452     *
1453     * @param ?ApiResult $result unused since 1.42
1454     * @return array Image info
1455     */
1456    public function getImageInfo( $result = null ) {
1457        $apiUpload = ApiUpload::getDummyInstance();
1458        return $apiUpload->getUploadImageInfo( $this );
1459    }
1460
1461    public function convertVerifyErrorToStatus( array $error ): UploadVerificationStatus {
1462        switch ( $error['status'] ) {
1463            /** Statuses that only require name changing */
1464            case self::MIN_LENGTH_PARTNAME:
1465                return UploadVerificationStatus::newFatal( 'filename-tooshort' )
1466                    ->setRecoverableError( true )
1467                    ->setInvalidParameter( 'filename' );
1468
1469            case self::ILLEGAL_FILENAME:
1470                return UploadVerificationStatus::newFatal( 'illegal-filename' )
1471                    ->setRecoverableError( true )
1472                    ->setInvalidParameter( 'filename' )
1473                    ->setApiData( [ 'filename' => $error['filtered'] ] );
1474
1475            case self::FILENAME_TOO_LONG:
1476                return UploadVerificationStatus::newFatal( 'filename-toolong' )
1477                    ->setRecoverableError( true )
1478                    ->setInvalidParameter( 'filename' );
1479
1480            case self::FILETYPE_MISSING:
1481                return UploadVerificationStatus::newFatal( 'filetype-missing' )
1482                    ->setRecoverableError( true )
1483                    ->setInvalidParameter( 'filename' );
1484
1485            case self::WINDOWS_NONASCII_FILENAME:
1486                return UploadVerificationStatus::newFatal( 'windows-nonascii-filename' )
1487                    ->setRecoverableError( true )
1488                    ->setInvalidParameter( 'filename' );
1489
1490            /** Statuses that require reuploading */
1491            case self::EMPTY_FILE:
1492                return UploadVerificationStatus::newFatal( 'empty-file' );
1493
1494            case self::FILE_TOO_LARGE:
1495                return UploadVerificationStatus::newFatal( 'file-too-large' );
1496
1497            case self::FILETYPE_BADTYPE:
1498                $extensions = array_unique( MediaWikiServices::getInstance()
1499                    ->getMainConfig()->get( MainConfigNames::FileExtensions ) );
1500                $extradata = [
1501                    'filetype' => $error['finalExt'],
1502                    'allowed' => array_values( $extensions ),
1503                ];
1504                ApiResult::setIndexedTagName( $extradata['allowed'], 'ext' );
1505                if ( isset( $error['blacklistedExt'] ) ) {
1506                    $bannedTypes = $error['blacklistedExt'];
1507                    $extradata['blacklisted'] = array_values( $bannedTypes );
1508                    ApiResult::setIndexedTagName( $extradata['blacklisted'], 'ext' );
1509                } else {
1510                    $bannedTypes = [ $error['finalExt'] ];
1511                }
1512                '@phan-var string[] $bannedTypes';
1513                return UploadVerificationStatus::newFatal(
1514                    'filetype-banned-type',
1515                    Message::listParam( $bannedTypes, ListType::COMMA ),
1516                    Message::listParam( $extensions, ListType::COMMA ),
1517                    count( $extensions ),
1518                    // Add PLURAL support for the first parameter. This results
1519                    // in a bit unlogical parameter sequence, but does not break
1520                    // old translations
1521                    count( $bannedTypes )
1522                )
1523                    ->setApiCode( 'filetype-banned' )
1524                    ->setApiData( $extradata );
1525
1526            case self::VERIFICATION_ERROR:
1527                $msg = ApiMessage::create( $error['details'], 'verification-error' );
1528                if ( $error['details'][0] instanceof MessageSpecifier ) {
1529                    $apiDetails = [ $msg->getKey(), ...$msg->getParams() ];
1530                } else {
1531                    $apiDetails = $error['details'];
1532                }
1533                ApiResult::setIndexedTagName( $apiDetails, 'detail' );
1534                $msg->setApiData( $msg->getApiData() + [ 'details' => $apiDetails ] );
1535                return UploadVerificationStatus::newFatal( $msg );
1536
1537            default:
1538                // @codeCoverageIgnoreStart
1539                return UploadVerificationStatus::newFatal( 'upload-unknownerror-nocode' )
1540                    ->setApiCode( 'unknown-error' )
1541                    ->setApiData( [ 'details' => [ 'code' => $error['status'] ] ] );
1542                // @codeCoverageIgnoreEnd
1543        }
1544    }
1545
1546    /**
1547     * Get MediaWiki's maximum uploaded file size for a given type of upload, based on
1548     * $wgMaxUploadSize.
1549     *
1550     * @param null|string $forType
1551     * @return int
1552     */
1553    public static function getMaxUploadSize( $forType = null ) {
1554        $maxUploadSize = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::MaxUploadSize );
1555
1556        if ( is_array( $maxUploadSize ) ) {
1557            return $maxUploadSize[$forType] ?? $maxUploadSize['*'];
1558        }
1559        return intval( $maxUploadSize );
1560    }
1561
1562    /**
1563     * Get the PHP maximum uploaded file size, based on ini settings. If there is no limit or the
1564     * limit can't be guessed, return a very large number (PHP_INT_MAX) instead.
1565     *
1566     * @since 1.27
1567     * @return int
1568     */
1569    public static function getMaxPhpUploadSize() {
1570        $phpMaxFileSize = wfShorthandToInteger(
1571            ini_get( 'upload_max_filesize' ),
1572            PHP_INT_MAX
1573        );
1574        $phpMaxPostSize = wfShorthandToInteger(
1575            ini_get( 'post_max_size' ),
1576            PHP_INT_MAX
1577        ) ?: PHP_INT_MAX;
1578        return min( $phpMaxFileSize, $phpMaxPostSize );
1579    }
1580
1581    /**
1582     * Get the current status of a chunked upload (used for polling).
1583     *
1584     * This should only be called during POST requests since we
1585     * fetch from dc-local MainStash, and from a GET request we can't
1586     * know that the value is available or up-to-date.
1587     *
1588     * @param UserIdentity $user
1589     * @param string $statusKey
1590     * @return mixed[]|false
1591     */
1592    public static function getSessionStatus( UserIdentity $user, $statusKey ) {
1593        $store = self::getUploadSessionStore();
1594        $key = self::getUploadSessionKey( $store, $user, $statusKey );
1595
1596        return $store->get( $key );
1597    }
1598
1599    /**
1600     * Set the current status of a chunked upload (used for polling).
1601     *
1602     * The value will be set in cache for 1 day.
1603     *
1604     * This should only be called during POST requests.
1605     *
1606     * @param UserIdentity $user
1607     * @param string $statusKey
1608     * @param array|false $value
1609     * @return void
1610     */
1611    public static function setSessionStatus( UserIdentity $user, $statusKey, $value ) {
1612        $store = self::getUploadSessionStore();
1613        $key = self::getUploadSessionKey( $store, $user, $statusKey );
1614        $logger = LoggerFactory::getInstance( 'upload' );
1615
1616        if ( is_array( $value ) && ( $value['result'] ?? '' ) === 'Failure' ) {
1617            $logger->info( 'Upload session {key} for {user} set to failure {status} at {stage}',
1618                [
1619                    'result' => $value['result'] ?? '',
1620                    'stage' => $value['stage'] ?? 'unknown',
1621                    'user' => $user->getName(),
1622                    'status' => (string)( $value['status'] ?? '-' ),
1623                    'filekey' => $value['filekey'] ?? '',
1624                    'key' => $statusKey
1625                ]
1626            );
1627        } elseif ( is_array( $value ) ) {
1628            $logger->debug( 'Upload session {key} for {user} changed {status} at {stage}',
1629                [
1630                    'result' => $value['result'] ?? '',
1631                    'stage' => $value['stage'] ?? 'unknown',
1632                    'user' => $user->getName(),
1633                    'status' => (string)( $value['status'] ?? '-' ),
1634                    'filekey' => $value['filekey'] ?? '',
1635                    'key' => $statusKey
1636                ]
1637            );
1638        } else {
1639            $logger->debug( "Upload session {key} deleted for {user}",
1640                [
1641                    'value' => $value,
1642                    'key' => $statusKey,
1643                    'user' => $user->getName()
1644                ]
1645            );
1646        }
1647
1648        if ( $value === false ) {
1649            $store->delete( $key );
1650        } else {
1651            $store->set( $key, $value, $store::TTL_DAY );
1652        }
1653    }
1654
1655    /**
1656     * @param BagOStuff $store
1657     * @param UserIdentity $user
1658     * @param string $statusKey
1659     * @return string
1660     */
1661    private static function getUploadSessionKey( BagOStuff $store, UserIdentity $user, $statusKey ) {
1662        return $store->makeKey(
1663            'uploadstatus',
1664            $user->isRegistered() ? $user->getId() : md5( $user->getName() ),
1665            $statusKey
1666        );
1667    }
1668
1669    /**
1670     * @return BagOStuff
1671     */
1672    private static function getUploadSessionStore() {
1673        return MediaWikiServices::getInstance()->getMainObjectStash();
1674    }
1675}
1676
1677/** @deprecated class alias since 1.46 */
1678class_alias( UploadBase::class, 'UploadBase' );