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

engine/classes/ElggFile.php (4 issues)

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();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->detectMimeType() could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
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);
0 ignored issues
show
Bug Best Practice introduced by
The expression return _elgg_services()-... 'file', $params, $url) could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
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);
0 ignored issues
show
Bug Best Practice introduced by
The expression return _elgg_services()-... 'file', $params, $url) could also return false which is incompatible with the documented return type string. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
536
	}
537
538
}
539