Passed
Push — master ( 03ce1e...93bb93 )
by Jeroen
23:30 queued 15s
created

ElggFile   F

Complexity

Total Complexity 61

Size/Duplication

Total Lines 504
Duplicated Lines 0 %

Test Coverage

Coverage 92.17%

Importance

Changes 0
Metric Value
eloc 140
c 0
b 0
f 0
dl 0
loc 504
ccs 153
cts 166
cp 0.9217
rs 3.52
wmc 61

30 Methods

Rating   Name   Duplication   Size   Complexity  
A initializeAttributes() 0 4 1
A setFilename() 0 4 1
A getFilenameOnFilestore() 0 2 1
A getFilename() 0 7 2
A __set() 0 9 2
A __get() 0 8 2
A write() 0 2 1
A read() 0 2 1
A grabFile() 0 2 1
A getMimeType() 0 13 3
A open() 0 13 3
A getSimpleType() 0 6 3
A setMimeType() 0 2 1
A close() 0 8 3
C acceptUploadedFile() 0 60 12
A setModifiedTime() 0 11 2
A getFilestore() 0 2 1
A transfer() 0 21 4
A delete() 0 6 2
A getInlineURL() 0 16 2
A tell() 0 2 1
A exists() 0 2 1
A getModifiedTime() 0 2 1
A seek() 0 2 1
A getDownloadURL() 0 16 2
A __sleep() 0 4 1
A canDownload() 0 2 1
A getSize() 0 2 1
A eof() 0 2 1
A persistentDelete() 0 11 3

How to fix   Complexity   

Complex Class

Complex classes like ElggFile often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ElggFile, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
use Elgg\Exceptions\Filesystem\IOException;
4
use Elgg\Exceptions\DomainException as ElggDomainException;
5
use Elgg\Exceptions\InvalidArgumentException as ElggInvalidArgumentException;
6
use Elgg\Project\Paths;
7
use Symfony\Component\HttpFoundation\File\Exception\FileException;
8
use Symfony\Component\HttpFoundation\File\UploadedFile;
9
10
/**
11
 * This class represents a physical file.
12
 *
13
 * Create a new \ElggFile object and specify a filename
14
 *
15
 * Open the file using the appropriate mode, and you will be able to
16
 * read and write to the file.
17
 *
18
 * Optionally, you can also call the file's save() method, this will
19
 * turn the file into an entity in the system and permit you to do
20
 * things like attach tags to the file. If you do not save the file, no
21
 * entity is created in the database. This is because there are occasions
22
 * when you may want access to file data on datastores using the \ElggFile
23
 * interface without a need to persist information such as temporary files.
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 252
	protected function initializeAttributes() {
46 252
		parent::initializeAttributes();
47
48 252
		$this->attributes['subtype'] = 'file';
49
	}
50
	
51
	/**
52
	 * {@inheritDoc}
53
	 */
54 241
	public function __set($name, $value) {
55
		switch ($name) {
56 241
			case 'filename':
57
				// ensure sanitization
58 17
				$this->setFilename($value);
59 17
				return;
60
		}
61
		
62 241
		parent::__set($name, $value);
63
	}
64
	
65
	/**
66
	 * {@inheritDoc}
67
	 */
68 252
	public function __get($name) {
69
		switch ($name) {
70 252
			case 'filename':
71
				// ensure sanitization
72 244
				return $this->getFilename();
73
		}
74
		
75 252
		return parent::__get($name);
76
	}
77
78
	/**
79
	 * Set the filename of this file. This filename will be sanitized to prevent path traversal
80
	 *
81
	 * @param string $filename The filename
82
	 *
83
	 * @return void
84
	 */
85 244
	public function setFilename(string $filename): void {
86 244
		$filename = ltrim(Paths::sanitize($filename, false), '/');
87
		
88 244
		parent::__set('filename', $filename);
89
	}
90
91
	/**
92
	 * Return the filename. This filename will be sanitized to prevent path traversal
93
	 *
94
	 * @return string
95
	 */
96 252
	public function getFilename(): string {
97 252
		$filename = parent::__get('filename');
98 252
		if (empty($filename)) {
99 252
			return '';
100
		}
101
		
102 227
		return ltrim(Paths::sanitize($filename, false), '/');
103
	}
104
105
	/**
106
	 * Return the filename of this file as it is/will be stored on the
107
	 * filestore, which may be different to the filename.
108
	 *
109
	 * @return string
110
	 */
111 126
	public function getFilenameOnFilestore(): string {
112 126
		return $this->getFilestore()->getFilenameOnFilestore($this);
113
	}
114
115
	/**
116
	 * Get the mime type of the file.
117
	 * Returns mimetype metadata value if set, otherwise attempts to detect it.
118
	 *
119
	 * @return string|false
120
	 */
121 64
	public function getMimeType(): string|false {
122 64
		if ($this->mimetype) {
123 51
			return $this->mimetype;
124
		}
125
		
126
		try {
127 44
			return _elgg_services()->mimetype->getMimeType($this->getFilenameOnFilestore());
128 1
		} catch (ElggInvalidArgumentException $e) {
129
			// the file has no file on the filesystem
130
			// can happen in tests etc.
131
		}
132
		
133 1
		return false;
134
	}
135
136
	/**
137
	 * Set the mime type of the file.
138
	 *
139
	 * @param string $mimetype The mimetype
140
	 *
141
	 * @return void
142
	 */
143 2
	public function setMimeType(string $mimetype): void {
144 2
		$this->mimetype = $mimetype;
145
	}
146
147
	/**
148
	 * Get the simple type of the file.
149
	 * Returns simpletype metadata value if set, otherwise parses it from mimetype
150
	 *
151
	 * @return string 'document', 'audio', 'video', or 'general' if the MIME type was unrecognized
152
	 */
153 60
	public function getSimpleType(): string {
154 60
		if (isset($this->simpletype)) {
155 46
			return $this->simpletype;
156
		}
157
		
158 14
		return _elgg_services()->mimetype->getSimpleType($this->getMimeType() ?: '');
159
	}
160
161
	/**
162
	 * Open the file with the given mode
163
	 *
164
	 * @param string $mode Either read/write/append
165
	 *
166
	 * @return false|resource File handler
167
	 *
168
	 * @throws IOException
169
	 * @throws \Elgg\Exceptions\DomainException
170
	 */
171 100
	public function open(string $mode) {
172 100
		if (!$this->getFilename()) {
173 1
			throw new IOException('You must specify a name before opening a file.');
174
		}
175
176 99
		if (!in_array($mode, ['read', 'write', 'append'])) {
177 1
			throw new ElggDomainException("Unrecognized file mode '{$mode}'");
178
		}
179
180
		// Open the file handle
181 98
		$this->handle = $this->getFilestore()->open($this, $mode);
182
183 98
		return $this->handle;
184
	}
185
186
	/**
187
	 * Write data.
188
	 *
189
	 * @param string $data The data
190
	 *
191
	 * @return false|int
192
	 */
193 47
	public function write(string $data): int|false {
194 47
		return $this->getFilestore()->write($this->handle, $data);
195
	}
196
197
	/**
198
	 * Read data.
199
	 *
200
	 * @param int $length Amount to read.
201
	 * @param int $offset The offset to start from.
202
	 *
203
	 * @return mixed Data or false
204
	 */
205 1
	public function read(int $length, int $offset = 0) {
206 1
		return $this->getFilestore()->read($this->handle, $length, $offset);
207
	}
208
209
	/**
210
	 * Gets the full contents of this file.
211
	 *
212
	 * @return false|string The file contents.
213
	 */
214 11
	public function grabFile(): string|false {
215 11
		return $this->getFilestore()->grabFile($this);
216
	}
217
218
	/**
219
	 * Close the file and commit changes
220
	 *
221
	 * @return bool
222
	 */
223 98
	public function close(): bool {
224 98
		if (is_resource($this->handle) && $this->getFilestore()->close($this->handle)) {
225 98
			$this->handle = null;
226
227 98
			return true;
228
		}
229
230
		return false;
231
	}
232
233
	/**
234
	 * {@inheritdoc}
235
	 */
236 96
	public function delete(bool $recursive = true, bool $persistent = null): bool {
237 96
		if (!$this->guid) {
238 86
			return $this->persistentDelete($recursive);
239
		}
240
		
241 10
		return parent::delete($recursive, $persistent);
242
	}
243
	
244
	/**
245
	 * {@inheritdoc}
246
	 */
247 96
	protected function persistentDelete(bool $recursive = true): bool {
248 96
		if ($this->guid) {
249 10
			$result = parent::persistentDelete($recursive);
250 10
			if ($result) {
251 10
				$this->getFilestore()->delete($this);
252
			}
253
			
254 10
			return $result;
255
		}
256
		
257 86
		return $this->getFilestore()->delete($this);
258
	}
259
260
	/**
261
	 * Seek a position in the file.
262
	 *
263
	 * @param int $position Position in bytes
264
	 *
265
	 * @return void
266
	 */
267 1
	public function seek(int $position): void {
268 1
		$this->getFilestore()->seek($this->handle, $position);
269
	}
270
271
	/**
272
	 * Return the current position of the file.
273
	 *
274
	 * @return int The file position
275
	 */
276 1
	public function tell(): int {
277 1
		return $this->getFilestore()->tell($this->handle);
278
	}
279
280
	/**
281
	 * Updates modification time of the file and clears stats cache for the file
282
	 *
283
	 * @return bool
284
	 */
285 5
	public function setModifiedTime(): bool {
286 5
		$filestorename = $this->getFilenameOnFilestore();
287
		
288 5
		$modified = touch($filestorename);
289 5
		if ($modified) {
290 5
			clearstatcache(true, $filestorename);
291
		} else {
292
			elgg_log("Unable to update modified time for {$filestorename}", 'ERROR');
293
		}
294
		
295 5
		return $modified;
296
	}
297
298
	/**
299
	 * Returns file modification time
300
	 *
301
	 * @return int
302
	 */
303 18
	public function getModifiedTime(): int {
304 18
		return filemtime($this->getFilenameOnFilestore());
305
	}
306
307
	/**
308
	 * Return the size of the file in bytes.
309
	 *
310
	 * @return int
311
	 * @since 1.9
312
	 */
313 14
	public function getSize(): int {
314 14
		return $this->getFilestore()->getFileSize($this);
315
	}
316
317
	/**
318
	 * Return a boolean value whether the file handle is at the end of the file
319
	 *
320
	 * @return bool
321
	 */
322 1
	public function eof(): bool {
323 1
		return $this->getFilestore()->eof($this->handle);
324
	}
325
326
	/**
327
	 * Returns if the file exists
328
	 *
329
	 * @return bool
330
	 */
331 185
	public function exists(): bool {
332 185
		return $this->getFilestore()->exists($this);
333
	}
334
335
	/**
336
	 * Return the system filestore based on dataroot.
337
	 *
338
	 * @return \Elgg\Filesystem\Filestore\DiskFilestore
339
	 */
340 224
	protected function getFilestore(): \Elgg\Filesystem\Filestore\DiskFilestore {
341 224
		return _elgg_services()->filestore;
342
	}
343
344
	/**
345
	 * Transfer a file to a new owner and sets a new filename,
346
	 * copies file contents to a new location.
347
	 *
348
	 * This is an alternative to using rename() which fails to move files to
349
	 * a non-existent directory under new owner's filestore directory
350
	 *
351
	 * @param int    $owner_guid New owner's guid
352
	 * @param string $filename   New filename (uses old filename if not set)
353
	 *
354
	 * @return bool
355
	 */
356 1
	public function transfer(int $owner_guid, string $filename = null): bool {
357 1
		if ($owner_guid < 1) {
358
			return false;
359
		}
360
361 1
		if (!$this->exists()) {
362 1
			return false;
363
		}
364
365 1
		if (empty($filename)) {
366 1
			$filename = $this->getFilename();
367
		}
368
		
369 1
		$filestorename = $this->getFilenameOnFilestore();
370
371 1
		$this->owner_guid = $owner_guid;
372 1
		$this->setFilename($filename);
373 1
		$this->open('write');
374 1
		$this->close();
375
376 1
		return rename($filestorename, $this->getFilenameOnFilestore());
377
	}
378
379
	/**
380
	 * Writes contents of the uploaded file to an instance of ElggFile
381
	 *
382
	 * @note Note that this function moves the file and populates properties,
383
	 * but does not call ElggFile::save().
384
	 *
385
	 * @note This method will automatically assign a filename on filestore based
386
	 * on the upload time and filename. By default, the file will be written
387
	 * to /file directory on owner's filestore. You can change this directory,
388
	 * by setting 'filestore_prefix' property of the ElggFile instance before
389
	 * calling this method.
390
	 *
391
	 * @param UploadedFile $upload Uploaded file object
392
	 *
393
	 * @return bool
394
	 */
395 2
	public function acceptUploadedFile(UploadedFile $upload): bool {
396 2
		if (!$upload->isValid()) {
397
			return false;
398
		}
399
400 2
		$old_filestorename = '';
401 2
		if ($this->exists()) {
402
			$old_filestorename = $this->getFilenameOnFilestore();
403
		}
404
405 2
		$originalfilename = $upload->getClientOriginalName();
406 2
		$this->originalfilename = $originalfilename;
407 2
		if (empty($this->title)) {
408 2
			$this->title = htmlspecialchars($this->originalfilename, ENT_QUOTES, 'UTF-8');
409
		}
410
411 2
		$this->upload_time = time();
412 2
		$prefix = $this->filestore_prefix ?: 'file';
413 2
		$prefix = trim($prefix, '/');
414 2
		$filename = elgg_strtolower("{$prefix}/{$this->upload_time}{$this->originalfilename}");
415 2
		$this->setFilename($filename);
416 2
		$this->filestore_prefix = $prefix;
417
418 2
		$params = [
419 2
			'file' => $this,
420 2
			'upload' => $upload,
421 2
		];
422
423 2
		$uploaded = _elgg_services()->events->triggerResults('upload', 'file', $params);
424 2
		if ($uploaded !== true && $uploaded !== false) {
425 1
			$filestorename = $this->getFilenameOnFilestore();
426
			try {
427 1
				$uploaded = $upload->move(pathinfo($filestorename, PATHINFO_DIRNAME), pathinfo($filestorename, PATHINFO_BASENAME));
428
			} catch (FileException $ex) {
429
				_elgg_services()->logger->error($ex->getMessage());
430
				$uploaded = false;
431
			}
432
		}
433
434 2
		if ($uploaded) {
435 2
			if ($old_filestorename && $old_filestorename != $this->getFilenameOnFilestore()) {
436
				// remove old file
437
				unlink($old_filestorename);
438
			}
439
			
440
			try {
441
				// try to detect mimetype
442 2
				$mime_type = _elgg_services()->mimetype->getMimeType($this->getFilenameOnFilestore());
443 1
				$this->setMimeType($mime_type);
444 1
				$this->simpletype = _elgg_services()->mimetype->getSimpleType($mime_type);
445 1
			} catch (ElggInvalidArgumentException $e) {
446
				// this can fail if the upload events returns true, but the file is not present on the filestore
447
				// this happens in a unittest
448
			}
449
			
450 2
			_elgg_services()->events->triggerAfter('upload', 'file', $this);
451 2
			return true;
452
		}
453
454 1
		return false;
455
	}
456
457
	/**
458
	 * Get property names to serialize.
459
	 *
460
	 * @return string[]
461
	 */
462
	public function __sleep() {
463
		return array_diff(array_keys(get_object_vars($this)), [
464
			// a resource
465
			'handle',
466
		]);
467
	}
468
469
	/**
470
	 * Checks the download permissions for the file
471
	 *
472
	 * @param int  $user_guid GUID of the user (defaults to logged in user)
473
	 * @param bool $default   Default permission
474
	 *
475
	 * @return bool
476
	 */
477 3
	public function canDownload(int $user_guid = 0, bool $default = true): bool {
478 3
		return _elgg_services()->userCapabilities->canDownload($this, $user_guid, $default);
479
	}
480
481
	/**
482
	 * Returns file's download URL
483
	 *
484
	 * @note This does not work for files with custom filestores.
485
	 *
486
	 * @param bool   $use_cookie Limit URL validity to current session only
487
	 * @param string $expires    URL expiration, as a string suitable for strtotime()
488
	 *
489
	 * @return string|null
490
	 */
491 3
	public function getDownloadURL(bool $use_cookie = true, string $expires = '+2 hours'): ?string {
492 3
		$file_svc = new \Elgg\FileService\File();
493 3
		$file_svc->setFile($this);
494 3
		if (!empty($expires)) {
495 3
			$file_svc->setExpires($expires);
496
		}
497
		
498 3
		$file_svc->setDisposition('attachment');
499 3
		$file_svc->bindSession($use_cookie);
500
501 3
		$params = [
502 3
			'entity' => $this,
503 3
			'use_cookie' => $use_cookie,
504 3
			'expires' => $expires,
505 3
		];
506 3
		return _elgg_services()->events->triggerResults('download:url', 'file', $params, $file_svc->getURL());
507
	}
508
509
	/**
510
	 * Returns file's URL for inline display
511
	 * Suitable for displaying cacheable resources, such as user avatars
512
	 *
513
	 * @note This does not work for files with custom filestores.
514
	 *
515
	 * @param bool   $use_cookie Limit URL validity to current session only
516
	 * @param string $expires    URL expiration, as a string suitable for strtotime()
517
	 *
518
	 * @return string|null
519
	 */
520 2
	public function getInlineURL(bool $use_cookie = false, string $expires = ''): ?string {
521 2
		$file_svc = new \Elgg\FileService\File();
522 2
		$file_svc->setFile($this);
523 2
		if (!empty($expires)) {
524 1
			$file_svc->setExpires($expires);
525
		}
526
		
527 2
		$file_svc->setDisposition('inline');
528 2
		$file_svc->bindSession($use_cookie);
529
530 2
		$params = [
531 2
			'entity' => $this,
532 2
			'use_cookie' => $use_cookie,
533 2
			'expires' => $expires,
534 2
		];
535 2
		return _elgg_services()->events->triggerResults('inline:url', 'file', $params, $file_svc->getURL());
536
	}
537
}
538