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); |
||
0 ignored issues
–
show
|
|||
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
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
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 For '' == 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
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
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 |
If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.