Issues (4122)

Security Analysis    not enabled

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Header Injection
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

includes/upload/UploadStash.php (3 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
/**
3
 * Temporary storage for uploaded files.
4
 *
5
 * This program is free software; you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation; either version 2 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License along
16
 * with this program; if not, write to the Free Software Foundation, Inc.,
17
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18
 * http://www.gnu.org/copyleft/gpl.html
19
 *
20
 * @file
21
 * @ingroup Upload
22
 */
23
24
/**
25
 * UploadStash is intended to accomplish a few things:
26
 *   - Enable applications to temporarily stash files without publishing them to
27
 *     the wiki.
28
 *      - Several parts of MediaWiki do this in similar ways: UploadBase,
29
 *        UploadWizard, and FirefoggChunkedExtension.
30
 *        And there are several that reimplement stashing from scratch, in
31
 *        idiosyncratic ways. The idea is to unify them all here.
32
 *        Mostly all of them are the same except for storing some custom fields,
33
 *        which we subsume into the data array.
34
 *   - Enable applications to find said files later, as long as the db table or
35
 *     temp files haven't been purged.
36
 *   - Enable the uploading user (and *ONLY* the uploading user) to access said
37
 *     files, and thumbnails of said files, via a URL. We accomplish this using
38
 *     a database table, with ownership checking as you might expect. See
39
 *     SpecialUploadStash, which implements a web interface to some files stored
40
 *     this way.
41
 *
42
 * UploadStash right now is *mostly* intended to show you one user's slice of
43
 * the entire stash. The user parameter is only optional because there are few
44
 * cases where we clean out the stash from an automated script. In the future we
45
 * might refactor this.
46
 *
47
 * UploadStash represents the entire stash of temporary files.
48
 * UploadStashFile is a filestore for the actual physical disk files.
49
 * UploadFromStash extends UploadBase, and represents a single stashed file as
50
 * it is moved from the stash to the regular file repository
51
 *
52
 * @ingroup Upload
53
 */
54
class UploadStash {
55
	// Format of the key for files -- has to be suitable as a filename itself (e.g. ab12cd34ef.jpg)
56
	const KEY_FORMAT_REGEX = '/^[\w-\.]+\.\w*$/';
57
	const MAX_US_PROPS_SIZE = 65535;
58
59
	/**
60
	 * repository that this uses to store temp files
61
	 * public because we sometimes need to get a LocalFile within the same repo.
62
	 *
63
	 * @var LocalRepo
64
	 */
65
	public $repo;
66
67
	// array of initialized repo objects
68
	protected $files = [];
69
70
	// cache of the file metadata that's stored in the database
71
	protected $fileMetadata = [];
72
73
	// fileprops cache
74
	protected $fileProps = [];
75
76
	// current user
77
	protected $user, $userId, $isLoggedIn;
0 ignored issues
show
It is generally advisable to only define one property per statement.

Only declaring a single property per statement allows you to later on add doc comments more easily.

It is also recommended by PSR2, so it is a common style that many people expect.

Loading history...
78
79
	/**
80
	 * Represents a temporary filestore, with metadata in the database.
81
	 * Designed to be compatible with the session stashing code in UploadBase
82
	 * (should replace it eventually).
83
	 *
84
	 * @param FileRepo $repo
85
	 * @param User $user (default null)
86
	 */
87
	public function __construct( FileRepo $repo, $user = null ) {
88
		// this might change based on wiki's configuration.
89
		$this->repo = $repo;
0 ignored issues
show
Documentation Bug introduced by
$repo is of type object<FileRepo>, but the property $repo was declared to be of type object<LocalRepo>. Are you sure that you always receive this specific sub-class here, or does it make sense to add an instanceof check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a given class or a super-class is assigned to a property that is type hinted more strictly.

Either this assignment is in error or an instanceof check should be added for that assignment.

class Alien {}

class Dalek extends Alien {}

class Plot
{
    /** @var  Dalek */
    public $villain;
}

$alien = new Alien();
$plot = new Plot();
if ($alien instanceof Dalek) {
    $plot->villain = $alien;
}
Loading history...
90
91
		// if a user was passed, use it. otherwise, attempt to use the global.
92
		// this keeps FileRepo from breaking when it creates an UploadStash object
93
		if ( $user ) {
94
			$this->user = $user;
95
		} else {
96
			global $wgUser;
97
			$this->user = $wgUser;
98
		}
99
100
		if ( is_object( $this->user ) ) {
101
			$this->userId = $this->user->getId();
102
			$this->isLoggedIn = $this->user->isLoggedIn();
103
		}
104
	}
105
106
	/**
107
	 * Get a file and its metadata from the stash.
108
	 * The noAuth param is a bit janky but is required for automated scripts
109
	 * which clean out the stash.
110
	 *
111
	 * @param string $key Key under which file information is stored
112
	 * @param bool $noAuth (optional) Don't check authentication. Used by maintenance scripts.
113
	 * @throws UploadStashFileNotFoundException
114
	 * @throws UploadStashNotLoggedInException
115
	 * @throws UploadStashWrongOwnerException
116
	 * @throws UploadStashBadPathException
117
	 * @return UploadStashFile
118
	 */
119
	public function getFile( $key, $noAuth = false ) {
120
		if ( !preg_match( self::KEY_FORMAT_REGEX, $key ) ) {
121
			throw new UploadStashBadPathException( "key '$key' is not in a proper format" );
122
		}
123
124
		if ( !$noAuth && !$this->isLoggedIn ) {
125
			throw new UploadStashNotLoggedInException( __METHOD__ .
126
				' No user is logged in, files must belong to users' );
127
		}
128
129
		if ( !isset( $this->fileMetadata[$key] ) ) {
130
			if ( !$this->fetchFileMetadata( $key ) ) {
131
				// If nothing was received, it's likely due to replication lag.
132
				// Check the master to see if the record is there.
133
				$this->fetchFileMetadata( $key, DB_MASTER );
134
			}
135
136
			if ( !isset( $this->fileMetadata[$key] ) ) {
137
				throw new UploadStashFileNotFoundException( "key '$key' not found in stash" );
138
			}
139
140
			// create $this->files[$key]
141
			$this->initFile( $key );
142
143
			// fetch fileprops
144
			if ( strlen( $this->fileMetadata[$key]['us_props'] ) ) {
145
				$this->fileProps[$key] = unserialize( $this->fileMetadata[$key]['us_props'] );
146
			} else { // b/c for rows with no us_props
147
				wfDebug( __METHOD__ . " fetched props for $key from file\n" );
148
				$path = $this->fileMetadata[$key]['us_path'];
149
				$this->fileProps[$key] = $this->repo->getFileProps( $path );
150
			}
151
		}
152
153
		if ( !$this->files[$key]->exists() ) {
154
			wfDebug( __METHOD__ . " tried to get file at $key, but it doesn't exist\n" );
155
			// @todo Is this not an UploadStashFileNotFoundException case?
156
			throw new UploadStashBadPathException( "path doesn't exist" );
157
		}
158
159
		if ( !$noAuth ) {
160
			if ( $this->fileMetadata[$key]['us_user'] != $this->userId ) {
161
				throw new UploadStashWrongOwnerException( "This file ($key) doesn't "
162
					. "belong to the current user." );
163
			}
164
		}
165
166
		return $this->files[$key];
167
	}
168
169
	/**
170
	 * Getter for file metadata.
171
	 *
172
	 * @param string $key Key under which file information is stored
173
	 * @return array
174
	 */
175
	public function getMetadata( $key ) {
176
		$this->getFile( $key );
177
178
		return $this->fileMetadata[$key];
179
	}
180
181
	/**
182
	 * Getter for fileProps
183
	 *
184
	 * @param string $key Key under which file information is stored
185
	 * @return array
186
	 */
187
	public function getFileProps( $key ) {
188
		$this->getFile( $key );
189
190
		return $this->fileProps[$key];
191
	}
192
193
	/**
194
	 * Stash a file in a temp directory and record that we did this in the
195
	 * database, along with other metadata.
196
	 *
197
	 * @param string $path Path to file you want stashed
198
	 * @param string $sourceType The type of upload that generated this file
199
	 *   (currently, I believe, 'file' or null)
200
	 * @throws UploadStashBadPathException
201
	 * @throws UploadStashFileException
202
	 * @throws UploadStashNotLoggedInException
203
	 * @return UploadStashFile|null File, or null on failure
204
	 */
205
	public function stashFile( $path, $sourceType = null ) {
206
		if ( !is_file( $path ) ) {
207
			wfDebug( __METHOD__ . " tried to stash file at '$path', but it doesn't exist\n" );
208
			throw new UploadStashBadPathException( "path doesn't exist" );
209
		}
210
211
		$mwProps = new MWFileProps( MimeMagic::singleton() );
212
		$fileProps = $mwProps->getPropsFromPath( $path, true );
213
		wfDebug( __METHOD__ . " stashing file at '$path'\n" );
214
215
		// we will be initializing from some tmpnam files that don't have extensions.
216
		// most of MediaWiki assumes all uploaded files have good extensions. So, we fix this.
217
		$extension = self::getExtensionForPath( $path );
218
		if ( !preg_match( "/\\.\\Q$extension\\E$/", $path ) ) {
219
			$pathWithGoodExtension = "$path.$extension";
220
		} else {
221
			$pathWithGoodExtension = $path;
222
		}
223
224
		// If no key was supplied, make one.  a mysql insertid would be totally
225
		// reasonable here, except that for historical reasons, the key is this
226
		// random thing instead.  At least it's not guessable.
227
		// Some things that when combined will make a suitably unique key.
228
		// see: http://www.jwz.org/doc/mid.html
229
		list( $usec, $sec ) = explode( ' ', microtime() );
230
		$usec = substr( $usec, 2 );
231
		$key = Wikimedia\base_convert( $sec . $usec, 10, 36 ) . '.' .
232
			Wikimedia\base_convert( mt_rand(), 10, 36 ) . '.' .
233
			$this->userId . '.' .
234
			$extension;
235
236
		$this->fileProps[$key] = $fileProps;
237
238
		if ( !preg_match( self::KEY_FORMAT_REGEX, $key ) ) {
239
			throw new UploadStashBadPathException( "key '$key' is not in a proper format" );
240
		}
241
242
		wfDebug( __METHOD__ . " key for '$path': $key\n" );
243
244
		// if not already in a temporary area, put it there
245
		$storeStatus = $this->repo->storeTemp( basename( $pathWithGoodExtension ), $path );
246
247
		if ( !$storeStatus->isOK() ) {
248
			// It is a convention in MediaWiki to only return one error per API
249
			// exception, even if multiple errors are available. We use reset()
250
			// to pick the "first" thing that was wrong, preferring errors to
251
			// warnings. This is a bit lame, as we may have more info in the
252
			// $storeStatus and we're throwing it away, but to fix it means
253
			// redesigning API errors significantly.
254
			// $storeStatus->value just contains the virtual URL (if anything)
255
			// which is probably useless to the caller.
256
			$error = $storeStatus->getErrorsArray();
257
			$error = reset( $error );
258 View Code Duplication
			if ( !count( $error ) ) {
259
				$error = $storeStatus->getWarningsArray();
260
				$error = reset( $error );
261
				if ( !count( $error ) ) {
262
					$error = [ 'unknown', 'no error recorded' ];
263
				}
264
			}
265
			// At this point, $error should contain the single "most important"
266
			// error, plus any parameters.
267
			$errorMsg = array_shift( $error );
268
			throw new UploadStashFileException( "Error storing file in '$path': "
269
				. wfMessage( $errorMsg, $error )->text() );
270
		}
271
		$stashPath = $storeStatus->value;
272
273
		// fetch the current user ID
274
		if ( !$this->isLoggedIn ) {
275
			throw new UploadStashNotLoggedInException( __METHOD__
276
				. ' No user is logged in, files must belong to users' );
277
		}
278
279
		// insert the file metadata into the db.
280
		wfDebug( __METHOD__ . " inserting $stashPath under $key\n" );
281
		$dbw = $this->repo->getMasterDB();
282
283
		$serializedFileProps = serialize( $fileProps );
284
		if ( strlen( $serializedFileProps ) > self::MAX_US_PROPS_SIZE ) {
285
			// Database is going to truncate this and make the field invalid.
286
			// Prioritize important metadata over file handler metadata.
287
			// File handler should be prepared to regenerate invalid metadata if needed.
288
			$fileProps['metadata'] = false;
289
			$serializedFileProps = serialize( $fileProps );
290
		}
291
292
		$this->fileMetadata[$key] = [
293
			'us_id' => $dbw->nextSequenceValue( 'uploadstash_us_id_seq' ),
294
			'us_user' => $this->userId,
295
			'us_key' => $key,
296
			'us_orig_path' => $path,
297
			'us_path' => $stashPath, // virtual URL
298
			'us_props' => $dbw->encodeBlob( $serializedFileProps ),
299
			'us_size' => $fileProps['size'],
300
			'us_sha1' => $fileProps['sha1'],
301
			'us_mime' => $fileProps['mime'],
302
			'us_media_type' => $fileProps['media_type'],
303
			'us_image_width' => $fileProps['width'],
304
			'us_image_height' => $fileProps['height'],
305
			'us_image_bits' => $fileProps['bits'],
306
			'us_source_type' => $sourceType,
307
			'us_timestamp' => $dbw->timestamp(),
308
			'us_status' => 'finished'
309
		];
310
311
		$dbw->insert(
312
			'uploadstash',
313
			$this->fileMetadata[$key],
314
			__METHOD__
315
		);
316
317
		// store the insertid in the class variable so immediate retrieval
318
		// (possibly laggy) isn't necesary.
319
		$this->fileMetadata[$key]['us_id'] = $dbw->insertId();
320
321
		# create the UploadStashFile object for this file.
322
		$this->initFile( $key );
323
324
		return $this->getFile( $key );
325
	}
326
327
	/**
328
	 * Remove all files from the stash.
329
	 * Does not clean up files in the repo, just the record of them.
330
	 *
331
	 * @throws UploadStashNotLoggedInException
332
	 * @return bool Success
333
	 */
334
	public function clear() {
335
		if ( !$this->isLoggedIn ) {
336
			throw new UploadStashNotLoggedInException( __METHOD__
337
				. ' No user is logged in, files must belong to users' );
338
		}
339
340
		wfDebug( __METHOD__ . ' clearing all rows for user ' . $this->userId . "\n" );
341
		$dbw = $this->repo->getMasterDB();
342
		$dbw->delete(
343
			'uploadstash',
344
			[ 'us_user' => $this->userId ],
345
			__METHOD__
346
		);
347
348
		# destroy objects.
349
		$this->files = [];
350
		$this->fileMetadata = [];
351
352
		return true;
353
	}
354
355
	/**
356
	 * Remove a particular file from the stash.  Also removes it from the repo.
357
	 *
358
	 * @param string $key
359
	 * @throws UploadStashNoSuchKeyException|UploadStashNotLoggedInException
360
	 * @throws UploadStashWrongOwnerException
361
	 * @return bool Success
362
	 */
363
	public function removeFile( $key ) {
364
		if ( !$this->isLoggedIn ) {
365
			throw new UploadStashNotLoggedInException( __METHOD__
366
				. ' No user is logged in, files must belong to users' );
367
		}
368
369
		$dbw = $this->repo->getMasterDB();
370
371
		// this is a cheap query. it runs on the master so that this function
372
		// still works when there's lag. It won't be called all that often.
373
		$row = $dbw->selectRow(
374
			'uploadstash',
375
			'us_user',
376
			[ 'us_key' => $key ],
377
			__METHOD__
378
		);
379
380
		if ( !$row ) {
381
			throw new UploadStashNoSuchKeyException( "No such key ($key), cannot remove" );
382
		}
383
384
		if ( $row->us_user != $this->userId ) {
385
			throw new UploadStashWrongOwnerException( "Can't delete: "
386
				. "the file ($key) doesn't belong to this user." );
387
		}
388
389
		return $this->removeFileNoAuth( $key );
390
	}
391
392
	/**
393
	 * Remove a file (see removeFile), but doesn't check ownership first.
394
	 *
395
	 * @param string $key
396
	 * @return bool Success
397
	 */
398
	public function removeFileNoAuth( $key ) {
399
		wfDebug( __METHOD__ . " clearing row $key\n" );
400
401
		// Ensure we have the UploadStashFile loaded for this key
402
		$this->getFile( $key, true );
403
404
		$dbw = $this->repo->getMasterDB();
405
406
		$dbw->delete(
407
			'uploadstash',
408
			[ 'us_key' => $key ],
409
			__METHOD__
410
		);
411
412
		/** @todo Look into UnregisteredLocalFile and find out why the rv here is
413
		 *  sometimes wrong (false when file was removed). For now, ignore.
414
		 */
415
		$this->files[$key]->remove();
416
417
		unset( $this->files[$key] );
418
		unset( $this->fileMetadata[$key] );
419
420
		return true;
421
	}
422
423
	/**
424
	 * List all files in the stash.
425
	 *
426
	 * @throws UploadStashNotLoggedInException
427
	 * @return array
428
	 */
429
	public function listFiles() {
430
		if ( !$this->isLoggedIn ) {
431
			throw new UploadStashNotLoggedInException( __METHOD__
432
				. ' No user is logged in, files must belong to users' );
433
		}
434
435
		$dbr = $this->repo->getSlaveDB();
436
		$res = $dbr->select(
437
			'uploadstash',
438
			'us_key',
439
			[ 'us_user' => $this->userId ],
440
			__METHOD__
441
		);
442
443
		if ( !is_object( $res ) || $res->numRows() == 0 ) {
444
			// nothing to do.
445
			return false;
446
		}
447
448
		// finish the read before starting writes.
449
		$keys = [];
450
		foreach ( $res as $row ) {
451
			array_push( $keys, $row->us_key );
452
		}
453
454
		return $keys;
455
	}
456
457
	/**
458
	 * Find or guess extension -- ensuring that our extension matches our MIME type.
459
	 * Since these files are constructed from php tempnames they may not start off
460
	 * with an extension.
461
	 * XXX this is somewhat redundant with the checks that ApiUpload.php does with incoming
462
	 * uploads versus the desired filename. Maybe we can get that passed to us...
463
	 * @param string $path
464
	 * @throws UploadStashFileException
465
	 * @return string
466
	 */
467
	public static function getExtensionForPath( $path ) {
468
		global $wgFileBlacklist;
469
		// Does this have an extension?
470
		$n = strrpos( $path, '.' );
471
		$extension = null;
472
		if ( $n !== false ) {
473
			$extension = $n ? substr( $path, $n + 1 ) : '';
474
		} else {
475
			// If not, assume that it should be related to the MIME type of the original file.
476
			$magic = MimeMagic::singleton();
477
			$mimeType = $magic->guessMimeType( $path );
478
			$extensions = explode( ' ', MimeMagic::singleton()->getExtensionsForType( $mimeType ) );
479
			if ( count( $extensions ) ) {
480
				$extension = $extensions[0];
481
			}
482
		}
483
484
		if ( is_null( $extension ) ) {
485
			throw new UploadStashFileException( "extension is null" );
486
		}
487
488
		$extension = File::normalizeExtension( $extension );
489
		if ( in_array( $extension, $wgFileBlacklist ) ) {
490
			// The file should already be checked for being evil.
491
			// However, if somehow we got here, we definitely
492
			// don't want to give it an extension of .php and
493
			// put it in a web accesible directory.
494
			return '';
495
		}
496
497
		return $extension;
498
	}
499
500
	/**
501
	 * Helper function: do the actual database query to fetch file metadata.
502
	 *
503
	 * @param string $key
504
	 * @param int $readFromDB Constant (default: DB_REPLICA)
505
	 * @return bool
506
	 */
507
	protected function fetchFileMetadata( $key, $readFromDB = DB_REPLICA ) {
508
		// populate $fileMetadata[$key]
509
		$dbr = null;
0 ignored issues
show
$dbr is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
510
		if ( $readFromDB === DB_MASTER ) {
511
			// sometimes reading from the master is necessary, if there's replication lag.
512
			$dbr = $this->repo->getMasterDB();
513
		} else {
514
			$dbr = $this->repo->getSlaveDB();
515
		}
516
517
		$row = $dbr->selectRow(
518
			'uploadstash',
519
			'*',
520
			[ 'us_key' => $key ],
521
			__METHOD__
522
		);
523
524
		if ( !is_object( $row ) ) {
525
			// key wasn't present in the database. this will happen sometimes.
526
			return false;
527
		}
528
529
		$this->fileMetadata[$key] = (array)$row;
530
		$this->fileMetadata[$key]['us_props'] = $dbr->decodeBlob( $row->us_props );
531
532
		return true;
533
	}
534
535
	/**
536
	 * Helper function: Initialize the UploadStashFile for a given file.
537
	 *
538
	 * @param string $key Key under which to store the object
539
	 * @throws UploadStashZeroLengthFileException
540
	 * @return bool
541
	 */
542
	protected function initFile( $key ) {
543
		$file = new UploadStashFile( $this->repo, $this->fileMetadata[$key]['us_path'], $key );
544
		if ( $file->getSize() === 0 ) {
545
			throw new UploadStashZeroLengthFileException( "File is zero length" );
546
		}
547
		$this->files[$key] = $file;
548
549
		return true;
550
	}
551
}
552
553
class UploadStashFile extends UnregisteredLocalFile {
554
	private $fileKey;
555
	private $urlName;
556
	protected $url;
557
558
	/**
559
	 * A LocalFile wrapper around a file that has been temporarily stashed,
560
	 * so we can do things like create thumbnails for it. Arguably
561
	 * UnregisteredLocalFile should be handling its own file repo but that
562
	 * class is a bit retarded currently.
563
	 *
564
	 * @param FileRepo $repo Repository where we should find the path
565
	 * @param string $path Path to file
566
	 * @param string $key Key to store the path and any stashed data under
567
	 * @throws UploadStashBadPathException
568
	 * @throws UploadStashFileNotFoundException
569
	 */
570
	public function __construct( $repo, $path, $key ) {
571
		$this->fileKey = $key;
572
573
		// resolve mwrepo:// urls
574
		if ( $repo->isVirtualUrl( $path ) ) {
575
			$path = $repo->resolveVirtualUrl( $path );
576
		} else {
577
			// check if path appears to be sane, no parent traversals,
578
			// and is in this repo's temp zone.
579
			$repoTempPath = $repo->getZonePath( 'temp' );
580
			if ( ( !$repo->validateFilename( $path ) ) ||
581
				( strpos( $path, $repoTempPath ) !== 0 )
582
			) {
583
				wfDebug( "UploadStash: tried to construct an UploadStashFile "
584
					. "from a file that should already exist at '$path', but path is not valid\n" );
585
				throw new UploadStashBadPathException( 'path is not valid' );
586
			}
587
588
			// check if path exists! and is a plain file.
589
			if ( !$repo->fileExists( $path ) ) {
590
				wfDebug( "UploadStash: tried to construct an UploadStashFile from "
591
					. "a file that should already exist at '$path', but path is not found\n" );
592
				throw new UploadStashFileNotFoundException( 'cannot find path, or not a plain file' );
593
			}
594
		}
595
596
		parent::__construct( false, $repo, $path, false );
597
598
		$this->name = basename( $this->path );
599
	}
600
601
	/**
602
	 * A method needed by the file transforming and scaling routines in File.php
603
	 * We do not necessarily care about doing the description at this point
604
	 * However, we also can't return the empty string, as the rest of MediaWiki
605
	 * demands this (and calls to imagemagick convert require it to be there)
606
	 *
607
	 * @return string Dummy value
608
	 */
609
	public function getDescriptionUrl() {
610
		return $this->getUrl();
611
	}
612
613
	/**
614
	 * Get the path for the thumbnail (actually any transformation of this file)
615
	 * The actual argument is the result of thumbName although we seem to have
616
	 * buggy code elsewhere that expects a boolean 'suffix'
617
	 *
618
	 * @param string $thumbName Name of thumbnail (e.g. "120px-123456.jpg" ),
619
	 *   or false to just get the path
620
	 * @return string Path thumbnail should take on filesystem, or containing
621
	 *   directory if thumbname is false
622
	 */
623
	public function getThumbPath( $thumbName = false ) {
624
		$path = dirname( $this->path );
625
		if ( $thumbName !== false ) {
626
			$path .= "/$thumbName";
627
		}
628
629
		return $path;
630
	}
631
632
	/**
633
	 * Return the file/url base name of a thumbnail with the specified parameters.
634
	 * We override this because we want to use the pretty url name instead of the
635
	 * ugly file name.
636
	 *
637
	 * @param array $params Handler-specific parameters
638
	 * @param int $flags Bitfield that supports THUMB_* constants
639
	 * @return string|null Base name for URL, like '120px-12345.jpg', or null if there is no handler
640
	 */
641
	function thumbName( $params, $flags = 0 ) {
642
		return $this->generateThumbName( $this->getUrlName(), $params );
643
	}
644
645
	/**
646
	 * Helper function -- given a 'subpage', return the local URL,
647
	 * e.g. /wiki/Special:UploadStash/subpage
648
	 * @param string $subPage
649
	 * @return string Local URL for this subpage in the Special:UploadStash space.
650
	 */
651
	private function getSpecialUrl( $subPage ) {
652
		return SpecialPage::getTitleFor( 'UploadStash', $subPage )->getLocalURL();
653
	}
654
655
	/**
656
	 * Get a URL to access the thumbnail
657
	 * This is required because the model of how files work requires that
658
	 * the thumbnail urls be predictable. However, in our model the URL is
659
	 * not based on the filename (that's hidden in the db)
660
	 *
661
	 * @param string $thumbName Basename of thumbnail file -- however, we don't
662
	 *   want to use the file exactly
663
	 * @return string URL to access thumbnail, or URL with partial path
664
	 */
665
	public function getThumbUrl( $thumbName = false ) {
666
		wfDebug( __METHOD__ . " getting for $thumbName \n" );
667
668
		return $this->getSpecialUrl( 'thumb/' . $this->getUrlName() . '/' . $thumbName );
669
	}
670
671
	/**
672
	 * The basename for the URL, which we want to not be related to the filename.
673
	 * Will also be used as the lookup key for a thumbnail file.
674
	 *
675
	 * @return string Base url name, like '120px-123456.jpg'
676
	 */
677
	public function getUrlName() {
678
		if ( !$this->urlName ) {
679
			$this->urlName = $this->fileKey;
680
		}
681
682
		return $this->urlName;
683
	}
684
685
	/**
686
	 * Return the URL of the file, if for some reason we wanted to download it
687
	 * We tend not to do this for the original file, but we do want thumb icons
688
	 *
689
	 * @return string Url
690
	 */
691
	public function getUrl() {
692
		if ( !isset( $this->url ) ) {
693
			$this->url = $this->getSpecialUrl( 'file/' . $this->getUrlName() );
694
		}
695
696
		return $this->url;
697
	}
698
699
	/**
700
	 * Parent classes use this method, for no obvious reason, to return the path
701
	 * (relative to wiki root, I assume). But with this class, the URL is
702
	 * unrelated to the path.
703
	 *
704
	 * @return string Url
705
	 */
706
	public function getFullUrl() {
707
		return $this->getUrl();
708
	}
709
710
	/**
711
	 * Getter for file key (the unique id by which this file's location &
712
	 * metadata is stored in the db)
713
	 *
714
	 * @return string File key
715
	 */
716
	public function getFileKey() {
717
		return $this->fileKey;
718
	}
719
720
	/**
721
	 * Remove the associated temporary file
722
	 * @return status Success
723
	 */
724
	public function remove() {
725
		if ( !$this->repo->fileExists( $this->path ) ) {
726
			// Maybe the file's already been removed? This could totally happen in UploadBase.
727
			return true;
728
		}
729
730
		return $this->repo->freeTemp( $this->path );
731
	}
732
733
	public function exists() {
734
		return $this->repo->fileExists( $this->path );
735
	}
736
}
737
738
class UploadStashException extends MWException {
739
}
740
741
class UploadStashFileNotFoundException extends UploadStashException {
742
}
743
744
class UploadStashBadPathException extends UploadStashException {
745
}
746
747
class UploadStashFileException extends UploadStashException {
748
}
749
750
class UploadStashZeroLengthFileException extends UploadStashException {
751
}
752
753
class UploadStashNotLoggedInException extends UploadStashException {
754
}
755
756
class UploadStashWrongOwnerException extends UploadStashException {
757
}
758
759
class UploadStashNoSuchKeyException extends UploadStashException {
760
}
761