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)) { |
|||
1 ignored issue
–
show
Bug
introduced
by
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); |
|||
1 ignored issue
–
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
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); |
|||
1 ignored issue
–
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
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); |
|||
1 ignored issue
–
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
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 |