Issues (946)

engine/classes/ElggFile.php (4 issues)

Labels
Severity
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
 * @property-read string $filename         The filename of the file
31
 */
32
class ElggFile extends ElggObject {
33
34
	/**
35
	 * @var resource|null|false File handle used to identify this file in a filestore
36
	 * @see \ElggFile::open()
37
	 */
38
	private $handle;
39
40
	/**
41
	 * Set subtype to 'file'.
42
	 *
43
	 * @return void
44
	 */
45 165
	protected function initializeAttributes() {
46 165
		parent::initializeAttributes();
47
48 165
		$this->attributes['subtype'] = "file";
49 165
	}
50
51
	/**
52
	 * Set the filename of this file.
53
	 *
54
	 * @param string $name The filename.
55
	 *
56
	 * @return void
57
	 */
58 155
	public function setFilename($name) {
59 155
		$this->filename = $name;
60 155
	}
61
62
	/**
63
	 * Return the filename.
64
	 *
65
	 * @return string
66
	 */
67 141
	public function getFilename() {
68 141
		return $this->filename;
69
	}
70
71
	/**
72
	 * Return the filename of this file as it is/will be stored on the
73
	 * filestore, which may be different to the filename.
74
	 *
75
	 * @return string
76
	 */
77 98
	public function getFilenameOnFilestore() {
78 98
		return $this->getFilestore()->getFilenameOnFilestore($this);
79
	}
80
81
	/**
82
	 * Return the size of the filestore associated with this file
83
	 *
84
	 * @param string $prefix         Storage prefix
85
	 * @param int    $container_guid The container GUID of the checked filestore
86
	 *
87
	 * @return int
88
	 */
89
	public function getFilestoreSize($prefix = '', $container_guid = 0) {
90
		if (!$container_guid) {
91
			$container_guid = $this->container_guid;
92
		}
93
		// @todo add getSize() to \ElggFilestore
94
		return (int) $this->getFilestore()->getSize($prefix, $container_guid);
95
	}
96
97
	/**
98
	 * Get the mime type of the file.
99
	 * Returns mimetype metadata value if set, otherwise attempts to detect it.
100
	 *
101
	 * @return string|false
102
	 */
103 62
	public function getMimeType() {
104 62
		if ($this->mimetype) {
105 51
			return $this->mimetype;
106
		}
107 42
		return $this->detectMimeType();
108
	}
109
110
	/**
111
	 * Set the mime type of the file.
112
	 *
113
	 * @param string $mimetype The mimetype
114
	 *
115
	 * @return string
116
	 */
117 3
	public function setMimeType($mimetype) {
118 3
		return $this->mimetype = $mimetype;
119
	}
120
121
	/**
122
	 * Detects mime types based on filename or actual file.
123
	 *
124
	 * @note This method can be called both dynamically and statically
125
	 *
126
	 * @param mixed $file    The full path of the file to check. For uploaded files, use tmp_name.
127
	 * @param mixed $default A default. Useful to pass what the browser thinks it is.
128
	 * @since 1.7.12
129
	 *
130
	 * @return mixed Detected type on success, false on failure.
131
	 * @todo Move this out into a utility class
132
	 */
133 45
	public function detectMimeType($file = null, $default = null) {
134 45
		$class = __CLASS__;
135 45
		if (!$file && isset($this) && $this instanceof $class) {
136 45
			$file = $this->getFilenameOnFilestore();
137
		}
138
139 45
		if (!is_readable($file)) {
140 2
			return false;
141
		}
142
143 43
		$mime = $default;
144
145 43
		$detected = (new MimeTypeDetector())->tryStrategies($file);
146 43
		if ($detected) {
147 43
			$mime = $detected;
148
		}
149
150 43
		$original_filename = isset($this) && $this instanceof $class ? $this->originalfilename : basename($file);
151
		$params = [
152 43
			'filename' => $file,
153 43
			'original_filename' => $original_filename, // @see file upload action
154 43
			'default' => $default,
155
		];
156 43
		return _elgg_services()->hooks->trigger('mime_type', 'file', $params, $mime);
157
	}
158
159
	/**
160
	 * Get the simple type of the file.
161
	 * Returns simpletype metadata value if set, otherwise parses it from mimetype
162
	 * @see elgg_get_file_simple_type()
163
	 *
164
	 * @return string 'document', 'audio', 'video', or 'general' if the MIME type was unrecognized
165
	 */
166 60
	public function getSimpleType() {
167 60
		if (isset($this->simpletype)) {
168 46
			return $this->simpletype;
169
		}
170 14
		$mime_type = $this->getMimeType();
171 14
		return elgg_get_file_simple_type($mime_type);
172
	}
173
174
	/**
175
	 * Set the optional file description.
176
	 *
177
	 * @param string $description The description.
178
	 *
179
	 * @return bool
180
	 */
181
	public function setDescription($description) {
182
		$this->description = $description;
183
	}
184
185
	/**
186
	 * Open the file with the given mode
187
	 *
188
	 * @param string $mode Either read/write/append
189
	 *
190
	 * @return false|resource File handler
191
	 *
192
	 * @throws IOException
193
	 * @throws InvalidParameterException
194
	 */
195 100
	public function open($mode) {
196 100
		if (!$this->getFilename()) {
197 1
			throw new IOException("You must specify a name before opening a file.");
198
		}
199
200
		// See if file has already been saved
201
		// seek on datastore, parameters and name?
202
		// Sanity check
203
		if (
204 99
				($mode != "read") &&
205 99
				($mode != "write") &&
206 99
				($mode != "append")
207
		) {
208 1
			$msg = "Unrecognized file mode '" . $mode . "'";
209 1
			throw new InvalidParameterException($msg);
210
		}
211
212
		// Open the file handle
213 98
		$this->handle = $this->getFilestore()->open($this, $mode);
214
215 98
		return $this->handle;
216
	}
217
218
	/**
219
	 * Write data.
220
	 *
221
	 * @param string $data The data
222
	 *
223
	 * @return false|int
224
	 */
225 46
	public function write($data) {
226 46
		return $this->getFilestore()->write($this->handle, $data);
227
	}
228
229
	/**
230
	 * Read data.
231
	 *
232
	 * @param int $length Amount to read.
233
	 * @param int $offset The offset to start from.
234
	 *
235
	 * @return mixed Data or false
236
	 */
237 1
	public function read($length, $offset = 0) {
238 1
		return $this->getFilestore()->read($this->handle, $length, $offset);
239
	}
240
241
	/**
242
	 * Gets the full contents of this file.
243
	 *
244
	 * @return false|string The file contents.
245
	 */
246 9
	public function grabFile() {
247 9
		return $this->getFilestore()->grabFile($this);
248
	}
249
250
	/**
251
	 * Close the file and commit changes
252
	 *
253
	 * @return bool
254
	 */
255 98
	public function close() {
256 98
		if ($this->getFilestore()->close($this->handle)) {
0 ignored issues
show
It seems like $this->handle can also be of type boolean; however, parameter $f of ElggDiskFilestore::close() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

256
		if ($this->getFilestore()->close(/** @scrutinizer ignore-type */ $this->handle)) {
Loading history...
257 98
			$this->handle = null;
258
259 98
			return true;
260
		}
261
262
		return false;
263
	}
264
265
	/**
266
	 * Delete this file.
267
	 *
268
	 * @param bool $follow_symlinks If true, will also delete the target file if the current file is a symlink
269
	 * @return bool
270
	 */
271 87
	public function delete($follow_symlinks = true) {
272 87
		$result = $this->getFilestore()->delete($this, $follow_symlinks);
273
274 87
		if ($this->getGUID() && $result) {
275 1
			$result = parent::delete();
276
		}
277
278 87
		return $result;
279
	}
280
281
	/**
282
	 * Seek a position in the file.
283
	 *
284
	 * @param int $position Position in bytes
285
	 *
286
	 * @return void
287
	 */
288 1
	public function seek($position) {
289
		// @todo add seek() to \ElggFilestore
290 1
		$this->getFilestore()->seek($this->handle, $position);
0 ignored issues
show
It seems like $this->handle can also be of type boolean; however, parameter $f of ElggDiskFilestore::seek() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

290
		$this->getFilestore()->seek(/** @scrutinizer ignore-type */ $this->handle, $position);
Loading history...
291 1
	}
292
293
	/**
294
	 * Return the current position of the file.
295
	 *
296
	 * @return int The file position
297
	 */
298 1
	public function tell() {
299 1
		return $this->getFilestore()->tell($this->handle);
0 ignored issues
show
It seems like $this->handle can also be of type boolean; however, parameter $f of ElggDiskFilestore::tell() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

299
		return $this->getFilestore()->tell(/** @scrutinizer ignore-type */ $this->handle);
Loading history...
300
	}
301
302
	/**
303
	 * Updates modification time of the file and clears stats cache for the file
304
	 * @return bool
305
	 */
306 5
	public function setModifiedTime() {
307 5
		$filestorename = $this->getFilenameOnFilestore();
308 5
		$modified = touch($filestorename);
309 5
		if ($modified) {
310 5
			clearstatcache(true, $filestorename);
311
		} else {
312
			elgg_log("Unable to update modified time for $filestorename", 'ERROR');
313
		}
314 5
		return $modified;
315
	}
316
317
	/**
318
	 * Returns file modification time
319
	 * @return int
320
	 */
321 18
	public function getModifiedTime() {
322 18
		return filemtime($this->getFilenameOnFilestore());
323
	}
324
325
	/**
326
	 * Return the size of the file in bytes.
327
	 *
328
	 * @return int
329
	 * @since 1.9
330
	 */
331 3
	public function getSize() {
332 3
		return $this->getFilestore()->getFileSize($this);
333
	}
334
335
	/**
336
	 * Return a boolean value whether the file handle is at the end of the file
337
	 *
338
	 * @return bool
339
	 */
340 1
	public function eof() {
341 1
		return $this->getFilestore()->eof($this->handle);
0 ignored issues
show
It seems like $this->handle can also be of type boolean; however, parameter $f of ElggDiskFilestore::eof() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

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