Passed
Push — master ( c0a3a7...3b84a4 )
by Jeroen
58:51
created

engine/classes/ElggFile.php (1 issue)

loose comparison of strings.

Best Practice Bug Major
1
<?php
2
3
use Elgg\Filesystem\MimeTypeDetector;
4
use Symfony\Component\HttpFoundation\File\Exception\FileException;
5
use Symfony\Component\HttpFoundation\File\UploadedFile;
6
7
/**
8
 * This class represents a physical file.
9
 *
10
 * Create a new \ElggFile object and specify a filename
11
 *
12
 * Open the file using the appropriate mode, and you will be able to
13
 * read and write to the file.
14
 *
15
 * Optionally, you can also call the file's save() method, this will
16
 * turn the file into an entity in the system and permit you to do
17
 * things like attach tags to the file. If you do not save the file, no
18
 * entity is created in the database. This is because there are occasions
19
 * when you may want access to file data on datastores using the \ElggFile
20
 * interface without a need to persist information such as temporary files.
21
 *
22
 * @package    Elgg.Core
23
 * @subpackage DataModel.File
24
 *
25
 * @property string $mimetype         MIME type of the file
26
 * @property string $simpletype       Category of the file
27
 * @property string $originalfilename Filename of the original upload
28
 * @property int    $upload_time      Timestamp of the upload action, used as a filename prefix
29
 * @property string $filestore_prefix Prefix (directory) on user's filestore where the file is saved
30
 */
31
class ElggFile extends ElggObject {
32
33
	/**
34
	 * @var resource|null File handle used to identify this file in a filestore. Created by open.
35
	 */
36
	private $handle;
37
38
	/**
39
	 * Set subtype to 'file'.
40
	 *
41
	 * @return void
42
	 */
43 105
	protected function initializeAttributes() {
44 105
		parent::initializeAttributes();
45
46 105
		$this->attributes['subtype'] = "file";
47 105
	}
48
49
	/**
50
	 * Set the filename of this file.
51
	 *
52
	 * @param string $name The filename.
53
	 *
54
	 * @return void
55
	 */
56 101
	public function setFilename($name) {
57 101
		$this->filename = $name;
58 101
	}
59
60
	/**
61
	 * Return the filename.
62
	 *
63
	 * @return string
64
	 */
65 101
	public function getFilename() {
66 101
		return $this->filename;
67
	}
68
69
	/**
70
	 * Return the filename of this file as it is/will be stored on the
71
	 * filestore, which may be different to the filename.
72
	 *
73
	 * @return string
74
	 */
75 69
	public function getFilenameOnFilestore() {
76 69
		return $this->getFilestore()->getFilenameOnFilestore($this);
77
	}
78
79
	/**
80
	 * Return the size of the filestore associated with this file
81
	 *
82
	 * @param string $prefix         Storage prefix
83
	 * @param int    $container_guid The container GUID of the checked filestore
84
	 *
85
	 * @return int
86
	 */
87
	public function getFilestoreSize($prefix = '', $container_guid = 0) {
88
		if (!$container_guid) {
89
			$container_guid = $this->container_guid;
90
		}
91
		// @todo add getSize() to \ElggFilestore
92
		return $this->getFilestore()->getSize($prefix, $container_guid);
93
	}
94
95
	/**
96
	 * Get the mime type of the file.
97
	 * Returns mimetype metadata value if set, otherwise attempts to detect it.
98
	 * @return string
99
	 */
100 46
	public function getMimeType() {
101 46
		if ($this->mimetype) {
102 36
			return $this->mimetype;
103
		}
104 41
		return $this->detectMimeType();
105
	}
106
107
	/**
108
	 * Set the mime type of the file.
109
	 *
110
	 * @param string $mimetype The mimetype
111
	 * @return bool
112
	 */
113 2
	public function setMimeType($mimetype) {
114 2
		return $this->mimetype = $mimetype;
115
	}
116
117
	/**
118
	 * Detects mime types based on filename or actual file.
119
	 *
120
	 * @note This method can be called both dynamically and statically
121
	 *
122
	 * @param mixed $file    The full path of the file to check. For uploaded files, use tmp_name.
123
	 * @param mixed $default A default. Useful to pass what the browser thinks it is.
124
	 * @since 1.7.12
125
	 *
126
	 * @return mixed Detected type on success, false on failure.
127
	 * @todo Move this out into a utility class
128
	 */
129 43
	public function detectMimeType($file = null, $default = null) {
130 43
		$class = __CLASS__;
131 43
		if (!$file && isset($this) && $this instanceof $class) {
132 43
			$file = $this->getFilenameOnFilestore();
133
		}
134
135 43
		if (!is_readable($file)) {
136 1
			return false;
137
		}
138
139 42
		$mime = $default;
140
141 42
		$detected = (new MimeTypeDetector())->tryStrategies($file);
142 42
		if ($detected) {
143 42
			$mime = $detected;
144
		}
145
146 42
		$original_filename = isset($this) && $this instanceof $class ? $this->originalfilename : basename($file);
147
		$params = [
148 42
			'filename' => $file,
149 42
			'original_filename' => $original_filename, // @see file upload action
150 42
			'default' => $default,
151
		];
152 42
		return _elgg_services()->hooks->trigger('mime_type', 'file', $params, $mime);
153
	}
154
155
	/**
156
	 * Get the simple type of the file.
157
	 * Returns simpletype metadata value if set, otherwise parses it from mimetype
158
	 * @see elgg_get_file_simple_type
159
	 *
160
	 * @return string 'document', 'audio', 'video', or 'general' if the MIME type was unrecognized
161
	 */
162 46
	public function getSimpleType() {
163 46
		if (isset($this->simpletype)) {
164 46
			return $this->simpletype;
165
		}
166
		$mime_type = $this->getMimeType();
167
		return elgg_get_file_simple_type($mime_type);
168
	}
169
170
	/**
171
	 * Set the optional file description.
172
	 *
173
	 * @param string $description The description.
174
	 *
175
	 * @return bool
176
	 */
177
	public function setDescription($description) {
178
		$this->description = $description;
179
	}
180
181
	/**
182
	 * Open the file with the given mode
183
	 *
184
	 * @param string $mode Either read/write/append
185
	 *
186
	 * @return resource File handler
187
	 *
188
	 * @throws IOException
189
	 * @throws InvalidParameterException
190
	 */
191 63
	public function open($mode) {
192 63
		if (!$this->getFilename()) {
193
			throw new IOException("You must specify a name before opening a file.");
194
		}
195
196
		// See if file has already been saved
197
		// seek on datastore, parameters and name?
198
		// Sanity check
199
		if (
200 63
				($mode != "read") &&
201 63
				($mode != "write") &&
202 63
				($mode != "append")
203
		) {
204
			$msg = "Unrecognized file mode '" . $mode . "'";
205
			throw new InvalidParameterException($msg);
206
		}
207
208
		// Open the file handle
209 63
		$this->handle = $this->getFilestore()->open($this, $mode);
210
211 63
		return $this->handle;
212
	}
213
214
	/**
215
	 * Write data.
216
	 *
217
	 * @param string $data The data
218
	 *
219
	 * @return false|int
220
	 */
221 19
	public function write($data) {
222 19
		return $this->getFilestore()->write($this->handle, $data);
223
	}
224
225
	/**
226
	 * Read data.
227
	 *
228
	 * @param int $length Amount to read.
229
	 * @param int $offset The offset to start from.
230
	 *
231
	 * @return mixed Data or false
232
	 */
233
	public function read($length, $offset = 0) {
234
		return $this->getFilestore()->read($this->handle, $length, $offset);
235
	}
236
237
	/**
238
	 * Gets the full contents of this file.
239
	 *
240
	 * @return mixed The file contents.
241
	 */
242 7
	public function grabFile() {
243 7
		return $this->getFilestore()->grabFile($this);
244
	}
245
246
	/**
247
	 * Close the file and commit changes
248
	 *
249
	 * @return bool
250
	 */
251 63
	public function close() {
252 63
		if ($this->getFilestore()->close($this->handle)) {
253 63
			$this->handle = null;
254
255 63
			return true;
256
		}
257
258
		return false;
259
	}
260
261
	/**
262
	 * Delete this file.
263
	 *
264
	 * @param bool $follow_symlinks If true, will also delete the target file if the current file is a symlink
265
	 * @return bool
266
	 */
267 64
	public function delete($follow_symlinks = true) {
268 64
		$result = $this->getFilestore()->delete($this, $follow_symlinks);
269
270 64
		if ($this->getGUID() && $result) {
271 1
			$result = parent::delete();
272
		}
273
274 64
		return $result;
275
	}
276
277
	/**
278
	 * Seek a position in the file.
279
	 *
280
	 * @param int $position Position in bytes
281
	 *
282
	 * @return void
283
	 */
284
	public function seek($position) {
285
		// @todo add seek() to \ElggFilestore
286
		$this->getFilestore()->seek($this->handle, $position);
287
	}
288
289
	/**
290
	 * Return the current position of the file.
291
	 *
292
	 * @return int The file position
293
	 */
294
	public function tell() {
295
		return $this->getFilestore()->tell($this->handle);
296
	}
297
298
	/**
299
	 * Updates modification time of the file and clears stats cache for the file
300
	 * @return bool
301
	 */
302 2
	public function setModifiedTime() {
303 2
		$filestorename = $this->getFilenameOnFilestore();
304 2
		$modified = touch($filestorename);
305 2
		if ($modified) {
306 2
			clearstatcache(true, $filestorename);
307
		} else {
308
			elgg_log("Unable to update modified time for $filestorename", 'ERROR');
309
		}
310 2
		return $modified;
311
	}
312
313
	/**
314
	 * Returns file modification time
315
	 * @return int
316
	 */
317 7
	public function getModifiedTime() {
318 7
		return filemtime($this->getFilenameOnFilestore());
319
	}
320
321
	/**
322
	 * Return the size of the file in bytes.
323
	 *
324
	 * @return int
325
	 * @since 1.9
326
	 */
327 1
	public function getSize() {
328 1
		return $this->getFilestore()->getFileSize($this);
329
	}
330
331
	/**
332
	 * Return a boolean value whether the file handle is at the end of the file
333
	 *
334
	 * @return bool
335
	 */
336
	public function eof() {
337
		return $this->getFilestore()->eof($this->handle);
338
	}
339
340
	/**
341
	 * Returns if the file exists
342
	 *
343
	 * @return bool
344
	 */
345 89
	public function exists() {
346 89
		return $this->getFilestore()->exists($this);
347
	}
348
349
	/**
350
	 * Return the system filestore based on dataroot.
351
	 *
352
	 * @return \ElggDiskFilestore
353
	 */
354 92
	protected function getFilestore() {
355 92
		return _elgg_services()->filestore;
356
	}
357
358
	/**
359
	 * Transfer a file to a new owner and sets a new filename,
360
	 * copies file contents to a new location.
361
	 *
362
	 * This is an alternative to using rename() which fails to move files to
363
	 * a non-existent directory under new owner's filestore directory
364
	 *
365
	 * @param int    $owner_guid New owner's guid
366
	 * @param string $filename   New filename (uses old filename if not set)
367
	 * @return bool
368
	 */
369
	public function transfer($owner_guid, $filename = null) {
370
		if (!$owner_guid) {
371
			return false;
372
		}
373
374
		if (!$this->exists()) {
375
			return false;
376
		}
377
378
		if (!$filename) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $filename of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
379
			$filename = $this->getFilename();
380
		}
381
		$filestorename = $this->getFilenameOnFilestore();
382
383
		$this->owner_guid = $owner_guid;
384
		$this->setFilename($filename);
385
		$this->open('write');
386
		$this->close();
387
388
		return rename($filestorename, $this->getFilenameOnFilestore());
389
	}
390
391
	/**
392
	 * Writes contents of the uploaded file to an instance of ElggFile
393
	 *
394
	 * @note Note that this function moves the file and populates properties,
395
	 * but does not call ElggFile::save().
396
	 *
397
	 * @note This method will automatically assign a filename on filestore based
398
	 * on the upload time and filename. By default, the file will be written
399
	 * to /file directory on owner's filestore. You can change this directory,
400
	 * by setting 'filestore_prefix' property of the ElggFile instance before
401
	 * calling this method.
402
	 *
403
	 * @param UploadedFile $upload Uploaded file object
404
	 * @return bool
405
	 */
406 2
	public function acceptUploadedFile(UploadedFile $upload) {
407 2
		if (!$upload->isValid()) {
408
			return false;
409
		}
410
411 2
		$old_filestorename = '';
412 2
		if ($this->exists()) {
413
			$old_filestorename = $this->getFilenameOnFilestore();
414
		}
415
416 2
		$originalfilename = $upload->getClientOriginalName();
417 2
		$this->originalfilename = $originalfilename;
418 2
		if (empty($this->title)) {
419 2
			$this->title = htmlspecialchars($this->originalfilename, ENT_QUOTES, 'UTF-8');
420
		}
421
422 2
		$this->upload_time = time();
423 2
		$prefix = $this->filestore_prefix ?: 'file';
424 2
		$prefix = trim($prefix, '/');
425 2
		$filename = elgg_strtolower("$prefix/{$this->upload_time}{$this->originalfilename}");
426 2
		$this->setFilename($filename);
427 2
		$this->filestore_prefix = $prefix;
428
429
		$hook_params = [
430 2
			'file' => $this,
431 2
			'upload' => $upload,
432
		];
433
434 2
		$uploaded = _elgg_services()->hooks->trigger('upload', 'file', $hook_params);
435 2
		if ($uploaded !== true && $uploaded !== false) {
436 1
			$filestorename = $this->getFilenameOnFilestore();
437
			try {
438 1
				$uploaded = $upload->move(pathinfo($filestorename, PATHINFO_DIRNAME), pathinfo($filestorename, PATHINFO_BASENAME));
439
			} catch (FileException $ex) {
440
				_elgg_services()->logger->error($ex->getMessage());
441
				$uploaded = false;
442
			}
443
		}
444
445 2
		if ($uploaded) {
446 2
			if ($old_filestorename && $old_filestorename != $this->getFilenameOnFilestore()) {
447
				// remove old file
448
				unlink($old_filestorename);
449
			}
450 2
			$mime_type = $this->detectMimeType(null, $upload->getClientMimeType());
451 2
			$this->setMimeType($mime_type);
452 2
			$this->simpletype = elgg_get_file_simple_type($mime_type);
453 2
			_elgg_services()->hooks->getEvents()->triggerAfter('upload', 'file', $this);
454 2
			return true;
455
		}
456
457 1
		return false;
458
	}
459
460
	/**
461
	 * Get property names to serialize.
462
	 *
463
	 * @return string[]
464
	 */
465 4
	public function __sleep() {
466 4
		return array_diff(array_keys(get_object_vars($this)), [
467
			// a resource
468 4
			'handle',
469
		]);
470
	}
471
472
	/**
473
	 * Checks the download permissions for the file
474
	 *
475
	 * @param int  $user_guid GUID of the user (defaults to logged in user)
476
	 * @param bool $default   Default permission
477
	 *
478
	 * @return bool
479
	 */
480 2
	public function canDownload($user_guid = 0, $default = true) {
481 2
		return _elgg_services()->userCapabilities->canDownload($this, $user_guid, $default);
482
	}
483
484
	/**
485
	 * Returns file's download URL
486
	 *
487
	 * @note This does not work for files with custom filestores.
488
	 *
489
	 * @param bool   $use_cookie Limit URL validity to current session only
490
	 * @param string $expires    URL expiration, as a string suitable for strtotime()
491
	 *
492
	 * @return string
493
	 */
494
	public function getDownloadURL($use_cookie = true, $expires = '+2 hours') {
495
496
		$file_svc = new \Elgg\FileService\File();
497
		$file_svc->setFile($this);
498
		$file_svc->setExpires($expires);
499
		$file_svc->setDisposition('attachment');
500
		$file_svc->bindSession($use_cookie);
501
		$url = $file_svc->getURL();
502
503
		$params = [
504
			'entity' => $this,
505
		];
506
507
		return _elgg_services()->hooks->trigger('download:url', 'file', $params, $url);
508
	}
509
510
	/**
511
	 * Returns file's URL for inline display
512
	 * Suitable for displaying cacheable resources, such as user avatars
513
	 *
514
	 * @note This does not work for files with custom filestores.
515
	 *
516
	 * @param bool   $use_cookie Limit URL validity to current session only
517
	 * @param string $expires    URL expiration, as a string suitable for strtotime()
518
	 *
519
	 * @return string
520
	 */
521 1
	public function getInlineURL($use_cookie = false, $expires = '') {
522 1
		$file_svc = new \Elgg\FileService\File();
523 1
		$file_svc->setFile($this);
524 1
		if ($expires) {
525
			$file_svc->setExpires($expires);
526
		}
527 1
		$file_svc->setDisposition('inline');
528 1
		$file_svc->bindSession($use_cookie);
529 1
		$url = $file_svc->getURL();
530
531
		$params = [
532 1
			'entity' => $this,
533
		];
534
535 1
		return _elgg_services()->hooks->trigger('inline:url', 'file', $params, $url);
536
	}
537
538
}
539