|
1
|
|
|
<?php |
|
2
|
|
|
/** |
|
3
|
|
|
* Elgg filestore. |
|
4
|
|
|
* This file contains functions for saving and retrieving data from files. |
|
5
|
|
|
* |
|
6
|
|
|
* @package Elgg.Core |
|
7
|
|
|
* @subpackage DataModel.FileStorage |
|
8
|
|
|
*/ |
|
9
|
|
|
|
|
10
|
|
|
use Symfony\Component\HttpFoundation\File\UploadedFile; |
|
11
|
|
|
|
|
12
|
|
|
/** |
|
13
|
|
|
* Get the size of the specified directory. |
|
14
|
|
|
* |
|
15
|
|
|
* @param string $dir The full path of the directory |
|
16
|
|
|
* @param int $total_size Add to current dir size |
|
17
|
|
|
* |
|
18
|
|
|
* @return int The size of the directory in bytes |
|
19
|
|
|
*/ |
|
20
|
|
|
function get_dir_size($dir, $total_size = 0) { |
|
21
|
|
|
$handle = @opendir($dir); |
|
22
|
|
|
while ($file = @readdir($handle)) { |
|
|
|
|
|
|
23
|
|
|
if (in_array($file, ['.', '..'])) { |
|
24
|
|
|
continue; |
|
25
|
|
|
} |
|
26
|
|
|
if (is_dir($dir . $file)) { |
|
27
|
|
|
$total_size = get_dir_size($dir . $file . "/", $total_size); |
|
28
|
|
|
} else { |
|
29
|
|
|
$total_size += filesize($dir . $file); |
|
30
|
|
|
} |
|
31
|
|
|
} |
|
32
|
|
|
@closedir($handle); |
|
|
|
|
|
|
33
|
|
|
|
|
34
|
|
|
return($total_size); |
|
35
|
|
|
} |
|
36
|
|
|
|
|
37
|
|
|
/** |
|
38
|
|
|
* Crops and resizes an image |
|
39
|
|
|
* |
|
40
|
|
|
* @param string $source Path to source image |
|
41
|
|
|
* @param string $destination Path to destination |
|
42
|
|
|
* If not set, will modify the source image |
|
43
|
|
|
* @param array $params An array of cropping/resizing parameters |
|
44
|
|
|
* - INT 'w' represents the width of the new image |
|
45
|
|
|
* With upscaling disabled, this is the maximum width |
|
46
|
|
|
* of the new image (in case the source image is |
|
47
|
|
|
* smaller than the expected width) |
|
48
|
|
|
* - INT 'h' represents the height of the new image |
|
49
|
|
|
* With upscaling disabled, this is the maximum height |
|
50
|
|
|
* - INT 'x1', 'y1', 'x2', 'y2' represent optional cropping |
|
51
|
|
|
* coordinates. The source image will first be cropped |
|
52
|
|
|
* to these coordinates, and then resized to match |
|
53
|
|
|
* width/height parameters |
|
54
|
|
|
* - BOOL 'square' - square images will fill the |
|
55
|
|
|
* bounding box (width x height). In Imagine's terms, |
|
56
|
|
|
* this equates to OUTBOUND mode |
|
57
|
|
|
* - BOOL 'upscale' - if enabled, smaller images |
|
58
|
|
|
* will be upscaled to fit the bounding box. |
|
59
|
|
|
* @return bool |
|
60
|
|
|
* @since 2.3 |
|
61
|
|
|
*/ |
|
62
|
|
|
function elgg_save_resized_image($source, $destination = null, array $params = []) { |
|
63
|
|
|
return _elgg_services()->imageService->resize($source, $destination, $params); |
|
64
|
|
|
} |
|
65
|
|
|
|
|
66
|
|
|
/** |
|
67
|
|
|
* Delete a directory and all its contents |
|
68
|
|
|
* |
|
69
|
|
|
* @param string $directory Directory to delete |
|
70
|
|
|
* |
|
71
|
|
|
* @return bool |
|
72
|
|
|
*/ |
|
73
|
|
|
function delete_directory($directory) { |
|
74
|
|
|
|
|
75
|
214 |
|
if (!file_exists($directory)) { |
|
76
|
213 |
|
return true; |
|
77
|
|
|
} |
|
78
|
|
|
|
|
79
|
2 |
|
if (!is_dir($directory)) { |
|
80
|
|
|
return false; |
|
81
|
|
|
} |
|
82
|
|
|
|
|
83
|
|
|
// sanity check: must be a directory |
|
84
|
2 |
|
if (!$handle = opendir($directory)) { |
|
85
|
|
|
return false; |
|
86
|
|
|
} |
|
87
|
|
|
|
|
88
|
|
|
// loop through all files |
|
89
|
2 |
|
while (($file = readdir($handle)) !== false) { |
|
90
|
2 |
|
if (in_array($file, ['.', '..'])) { |
|
91
|
2 |
|
continue; |
|
92
|
|
|
} |
|
93
|
|
|
|
|
94
|
2 |
|
$path = "$directory/$file"; |
|
95
|
2 |
|
if (is_dir($path)) { |
|
96
|
|
|
// recurse down through directory |
|
97
|
2 |
|
if (!delete_directory($path)) { |
|
98
|
2 |
|
return false; |
|
99
|
|
|
} |
|
100
|
|
|
} else { |
|
101
|
|
|
// delete file |
|
102
|
1 |
|
unlink($path); |
|
103
|
|
|
} |
|
104
|
|
|
} |
|
105
|
|
|
|
|
106
|
|
|
// remove empty directory |
|
107
|
2 |
|
closedir($handle); |
|
108
|
2 |
|
return rmdir($directory); |
|
109
|
|
|
} |
|
110
|
|
|
|
|
111
|
|
|
/** |
|
112
|
|
|
* Returns the category of a file from its MIME type |
|
113
|
|
|
* |
|
114
|
|
|
* @param string $mime_type The MIME type |
|
115
|
|
|
* |
|
116
|
|
|
* @return string 'document', 'audio', 'video', or 'general' if the MIME type was unrecognized |
|
117
|
|
|
* @since 1.10 |
|
118
|
|
|
*/ |
|
119
|
|
|
function elgg_get_file_simple_type($mime_type) { |
|
120
|
47 |
|
$params = ['mime_type' => $mime_type]; |
|
121
|
47 |
|
return elgg_trigger_plugin_hook('simple_type', 'file', $params, 'general'); |
|
122
|
|
|
} |
|
123
|
|
|
|
|
124
|
|
|
/** |
|
125
|
|
|
* Register file-related handlers on "init, system" event |
|
126
|
|
|
* |
|
127
|
|
|
* @return void |
|
128
|
|
|
* @access private |
|
129
|
|
|
*/ |
|
130
|
|
|
function _elgg_filestore_init() { |
|
131
|
|
|
|
|
132
|
|
|
// Fix MIME type detection for Microsoft zipped formats |
|
133
|
93 |
|
elgg_register_plugin_hook_handler('mime_type', 'file', '_elgg_filestore_detect_mimetype'); |
|
134
|
|
|
|
|
135
|
|
|
// Parse category of file from MIME type |
|
136
|
93 |
|
elgg_register_plugin_hook_handler('simple_type', 'file', '_elgg_filestore_parse_simpletype'); |
|
137
|
|
|
|
|
138
|
|
|
// Unit testing |
|
139
|
93 |
|
elgg_register_plugin_hook_handler('unit_test', 'system', '_elgg_filestore_test'); |
|
140
|
|
|
|
|
141
|
|
|
// Handler for serving embedded icons |
|
142
|
93 |
|
elgg_register_page_handler('serve-icon', '_elgg_filestore_serve_icon_handler'); |
|
|
|
|
|
|
143
|
|
|
|
|
144
|
|
|
// Touch entity icons if entity access id has changed |
|
145
|
93 |
|
elgg_register_event_handler('update:after', 'object', '_elgg_filestore_touch_icons'); |
|
146
|
93 |
|
elgg_register_event_handler('update:after', 'group', '_elgg_filestore_touch_icons'); |
|
147
|
|
|
|
|
148
|
|
|
// Move entity icons if entity owner has changed |
|
149
|
93 |
|
elgg_register_event_handler('update:after', 'object', '_elgg_filestore_move_icons'); |
|
150
|
93 |
|
elgg_register_event_handler('update:after', 'group', '_elgg_filestore_move_icons'); |
|
151
|
93 |
|
} |
|
152
|
|
|
|
|
153
|
|
|
/** |
|
154
|
|
|
* Fix MIME type detection for Microsoft zipped formats |
|
155
|
|
|
* |
|
156
|
|
|
* @param string $hook "mime_type" |
|
157
|
|
|
* @param string $type "file" |
|
158
|
|
|
* @param string $mime_type Detected MIME type |
|
159
|
|
|
* @param array $params Hook parameters |
|
160
|
|
|
* |
|
161
|
|
|
* @return string The MIME type |
|
162
|
|
|
* @access private |
|
163
|
|
|
*/ |
|
164
|
|
|
function _elgg_filestore_detect_mimetype($hook, $type, $mime_type, $params) { |
|
|
|
|
|
|
165
|
|
|
|
|
166
|
37 |
|
$original_filename = elgg_extract('original_filename', $params); |
|
167
|
37 |
|
$ext = pathinfo($original_filename, PATHINFO_EXTENSION); |
|
168
|
|
|
|
|
169
|
37 |
|
return (new \Elgg\Filesystem\MimeTypeDetector())->fixDetectionErrors($mime_type, $ext); |
|
170
|
|
|
} |
|
171
|
|
|
|
|
172
|
|
|
/** |
|
173
|
|
|
* Parse a file category of file from a MIME type |
|
174
|
|
|
* |
|
175
|
|
|
* @param string $hook "simple_type" |
|
176
|
|
|
* @param string $type "file" |
|
177
|
|
|
* @param string $simple_type The category of file |
|
178
|
|
|
* @param array $params Hook parameters |
|
179
|
|
|
* |
|
180
|
|
|
* @return string 'document', 'audio', 'video', or 'general' if the MIME type is unrecognized |
|
181
|
|
|
* @access private |
|
182
|
|
|
*/ |
|
183
|
|
|
function _elgg_filestore_parse_simpletype($hook, $type, $simple_type, $params) { |
|
|
|
|
|
|
184
|
|
|
|
|
185
|
47 |
|
$mime_type = elgg_extract('mime_type', $params); |
|
186
|
|
|
|
|
187
|
47 |
|
switch ($mime_type) { |
|
188
|
1 |
|
case "application/msword": |
|
189
|
1 |
|
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document": |
|
190
|
1 |
|
case "application/pdf": |
|
191
|
|
|
return "document"; |
|
192
|
|
|
|
|
193
|
1 |
|
case "application/ogg": |
|
194
|
|
|
return "audio"; |
|
195
|
|
|
} |
|
196
|
|
|
|
|
197
|
47 |
|
if (preg_match('~^(audio|image|video)/~', $mime_type, $m)) { |
|
198
|
46 |
|
return $m[1]; |
|
199
|
|
|
} |
|
200
|
1 |
|
if (0 === strpos($mime_type, 'text/') || false !== strpos($mime_type, 'opendocument')) { |
|
201
|
|
|
return "document"; |
|
202
|
|
|
} |
|
203
|
|
|
|
|
204
|
|
|
// unrecognized MIME |
|
205
|
1 |
|
return $simple_type; |
|
206
|
|
|
} |
|
207
|
|
|
|
|
208
|
|
|
/** |
|
209
|
|
|
* Unit tests for files |
|
210
|
|
|
* |
|
211
|
|
|
* @param string $hook 'unit_test' |
|
212
|
|
|
* @param string $type 'system' |
|
213
|
|
|
* @param mixed $value Array of tests |
|
214
|
|
|
* |
|
215
|
|
|
* @return array |
|
216
|
|
|
* @access private |
|
217
|
|
|
* @codeCoverageIgnore |
|
218
|
|
|
*/ |
|
219
|
|
|
function _elgg_filestore_test($hook, $type, $value) { |
|
|
|
|
|
|
220
|
|
|
$value[] = ElggCoreFilestoreTest::class; |
|
221
|
|
|
return $value; |
|
222
|
|
|
} |
|
223
|
|
|
|
|
224
|
|
|
/** |
|
225
|
|
|
* Returns file's download URL |
|
226
|
|
|
* |
|
227
|
|
|
* @note This does not work for files with custom filestores. |
|
228
|
|
|
* |
|
229
|
|
|
* @param \ElggFile $file File object or entity (must have the default filestore) |
|
230
|
|
|
* @param bool $use_cookie Limit URL validity to current session only |
|
231
|
|
|
* @param string $expires URL expiration, as a string suitable for strtotime() |
|
232
|
|
|
* @return string |
|
233
|
|
|
*/ |
|
234
|
|
|
function elgg_get_download_url(\ElggFile $file, $use_cookie = true, $expires = '+2 hours') { |
|
235
|
|
|
return $file->getDownloadURL($use_cookie, $expires); |
|
236
|
|
|
} |
|
237
|
|
|
|
|
238
|
|
|
/** |
|
239
|
|
|
* Returns file's URL for inline display |
|
240
|
|
|
* Suitable for displaying cacheable resources, such as user avatars |
|
241
|
|
|
* |
|
242
|
|
|
* @note This does not work for files with custom filestores. |
|
243
|
|
|
* |
|
244
|
|
|
* @param \ElggFile $file File object or entity (must have the default filestore) |
|
245
|
|
|
* @param bool $use_cookie Limit URL validity to current session only |
|
246
|
|
|
* @param string $expires URL expiration, as a string suitable for strtotime() |
|
247
|
|
|
* @return string |
|
248
|
|
|
*/ |
|
249
|
|
|
function elgg_get_inline_url(\ElggFile $file, $use_cookie = false, $expires = '') { |
|
250
|
1 |
|
return $file->getInlineURL($use_cookie, $expires); |
|
251
|
|
|
} |
|
252
|
|
|
|
|
253
|
|
|
/** |
|
254
|
|
|
* Returns a URL suitable for embedding entity's icon in a text editor. |
|
255
|
|
|
* We can not use elgg_get_inline_url() for these purposes due to a URL structure |
|
256
|
|
|
* bound to user session and file modification time. |
|
257
|
|
|
* This function returns a generic (permanent) URL that will then be resolved to |
|
258
|
|
|
* an inline URL whenever requested. |
|
259
|
|
|
* |
|
260
|
|
|
* @param \ElggEntity $entity Entity |
|
261
|
|
|
* @param string $size Size |
|
262
|
|
|
* @return string |
|
263
|
|
|
* @since 2.2 |
|
264
|
|
|
*/ |
|
265
|
|
|
function elgg_get_embed_url(\ElggEntity $entity, $size) { |
|
266
|
|
|
return elgg_normalize_url("serve-icon/$entity->guid/$size"); |
|
267
|
|
|
} |
|
268
|
|
|
|
|
269
|
|
|
/** |
|
270
|
|
|
* Handler for /serve-icon resources |
|
271
|
|
|
* /serve-icon/<entity_guid>/<size> |
|
272
|
|
|
* |
|
273
|
|
|
* @return void |
|
274
|
|
|
* @access private |
|
275
|
|
|
* @since 2.2 |
|
276
|
|
|
*/ |
|
277
|
|
|
function _elgg_filestore_serve_icon_handler() { |
|
278
|
|
|
$response = _elgg_services()->iconService->handleServeIconRequest(); |
|
279
|
|
|
$response->send(); |
|
280
|
|
|
exit; |
|
|
|
|
|
|
281
|
|
|
} |
|
282
|
|
|
|
|
283
|
|
|
/** |
|
284
|
|
|
* Reset icon URLs if access_id has changed |
|
285
|
|
|
* |
|
286
|
|
|
* @param string $event "update:after" |
|
287
|
|
|
* @param string $type "object"|"group" |
|
288
|
|
|
* @param ElggObject $entity Entity |
|
289
|
|
|
* @return void |
|
290
|
|
|
* @access private |
|
291
|
|
|
*/ |
|
292
|
|
|
function _elgg_filestore_touch_icons($event, $type, $entity) { |
|
|
|
|
|
|
293
|
14 |
|
$original_attributes = $entity->getOriginalAttributes(); |
|
294
|
14 |
|
if (!array_key_exists('access_id', $original_attributes)) { |
|
295
|
9 |
|
return; |
|
296
|
|
|
} |
|
297
|
5 |
|
if ($entity instanceof \ElggFile) { |
|
298
|
|
|
// we touch the file to invalidate any previously generated download URLs |
|
299
|
|
|
$entity->setModifiedTime(); |
|
300
|
|
|
} |
|
301
|
5 |
|
$sizes = array_keys(elgg_get_icon_sizes($entity->getType(), $entity->getSubtype())); |
|
302
|
5 |
|
foreach ($sizes as $size) { |
|
303
|
5 |
|
$icon = $entity->getIcon($size); |
|
304
|
5 |
|
if ($icon->exists()) { |
|
305
|
5 |
|
$icon->setModifiedTime(); |
|
306
|
|
|
} |
|
307
|
|
|
} |
|
308
|
5 |
|
} |
|
309
|
|
|
|
|
310
|
|
|
/** |
|
311
|
|
|
* Listen to entity ownership changes and update icon ownership by moving |
|
312
|
|
|
* icons to their new owner's directory on filestore. |
|
313
|
|
|
* |
|
314
|
|
|
* This will only transfer icons that have a custom location on filestore |
|
315
|
|
|
* and are owned by the entity's owner (instead of the entity itself). |
|
316
|
|
|
* Even though core icon service does not store icons in the entity's owner |
|
317
|
|
|
* directory, there are plugins that do (e.g. file plugin) - this handler |
|
318
|
|
|
* helps such plugins avoid ownership mismatch. |
|
319
|
|
|
* |
|
320
|
|
|
* @param string $event "update:after" |
|
321
|
|
|
* @param string $type "object"|"group" |
|
322
|
|
|
* @param ElggObject $entity Entity |
|
323
|
|
|
* @return void |
|
324
|
|
|
* @access private |
|
325
|
|
|
*/ |
|
326
|
|
|
function _elgg_filestore_move_icons($event, $type, $entity) { |
|
|
|
|
|
|
327
|
|
|
|
|
328
|
14 |
|
$original_attributes = $entity->getOriginalAttributes(); |
|
329
|
14 |
|
if (empty($original_attributes['owner_guid'])) { |
|
330
|
13 |
|
return; |
|
331
|
|
|
} |
|
332
|
|
|
|
|
333
|
1 |
|
$previous_owner_guid = $original_attributes['owner_guid']; |
|
334
|
1 |
|
$new_owner_guid = $entity->owner_guid; |
|
335
|
|
|
|
|
336
|
1 |
|
$sizes = elgg_get_icon_sizes($entity->getType(), $entity->getSubtype()); |
|
337
|
|
|
|
|
338
|
1 |
|
foreach ($sizes as $size => $opts) { |
|
339
|
1 |
|
$new_icon = $entity->getIcon($size); |
|
340
|
1 |
|
if ($new_icon->owner_guid == $entity->guid) { |
|
341
|
|
|
// we do not need to update icons that are owned by the entity itself |
|
342
|
1 |
|
continue; |
|
343
|
|
|
} |
|
344
|
|
|
|
|
345
|
|
|
if ($new_icon->owner_guid != $new_owner_guid) { |
|
346
|
|
|
// a plugin implements some custom logic |
|
347
|
|
|
continue; |
|
348
|
|
|
} |
|
349
|
|
|
|
|
350
|
|
|
$old_icon = new \ElggIcon(); |
|
351
|
|
|
$old_icon->owner_guid = $previous_owner_guid; |
|
352
|
|
|
$old_icon->setFilename($new_icon->getFilename()); |
|
353
|
|
|
if (!$old_icon->exists()) { |
|
354
|
|
|
// there is no icon to move |
|
355
|
|
|
continue; |
|
356
|
|
|
} |
|
357
|
|
|
|
|
358
|
|
|
if ($new_icon->exists()) { |
|
359
|
|
|
// there is already a new icon |
|
360
|
|
|
// just removing the old one |
|
361
|
|
|
$old_icon->delete(); |
|
362
|
|
|
elgg_log("Entity $entity->guid has been transferred to a new owner but an icon was " |
|
363
|
|
|
. "left behind under {$old_icon->getFilenameOnFilestore()}. " |
|
364
|
|
|
. "Old icon has been deleted", 'NOTICE'); |
|
365
|
|
|
continue; |
|
366
|
|
|
} |
|
367
|
|
|
|
|
368
|
|
|
$old_icon->transfer($new_icon->owner_guid, $new_icon->getFilename()); |
|
369
|
|
|
elgg_log("Entity $entity->guid has been transferred to a new owner. " |
|
370
|
|
|
. "Icon was moved from {$old_icon->getFilenameOnFilestore()} to {$new_icon->getFilenameOnFilestore()}.", 'NOTICE'); |
|
371
|
|
|
} |
|
372
|
1 |
|
} |
|
373
|
|
|
|
|
374
|
|
|
/** |
|
375
|
|
|
* Returns an array of uploaded file objects regardless of upload status/errors |
|
376
|
|
|
* |
|
377
|
|
|
* @param string $input_name Form input name |
|
378
|
|
|
* @return UploadedFile[]|false |
|
379
|
|
|
*/ |
|
380
|
|
|
function elgg_get_uploaded_files($input_name) { |
|
381
|
|
|
return _elgg_services()->uploads->getFiles($input_name); |
|
382
|
|
|
} |
|
383
|
|
|
|
|
384
|
|
|
/** |
|
385
|
|
|
* Returns a single valid uploaded file object |
|
386
|
|
|
* |
|
387
|
|
|
* @param string $input_name Form input name |
|
388
|
|
|
* @param bool $check_for_validity If there is an uploaded file, is it required to be valid |
|
389
|
|
|
* |
|
390
|
|
|
* @return UploadedFile|false |
|
391
|
|
|
*/ |
|
392
|
|
|
function elgg_get_uploaded_file($input_name, $check_for_validity = true) { |
|
393
|
|
|
return _elgg_services()->uploads->getFile($input_name, $check_for_validity); |
|
394
|
|
|
} |
|
395
|
|
|
|
|
396
|
|
|
/** |
|
397
|
|
|
* Returns a ElggTempFile which can handle writing/reading of data to a temporary file location |
|
398
|
|
|
* |
|
399
|
|
|
* @return ElggTempFile |
|
400
|
|
|
* @since 3.0 |
|
401
|
|
|
*/ |
|
402
|
|
|
function elgg_get_temp_file() { |
|
403
|
1 |
|
return new ElggTempFile(); |
|
404
|
|
|
} |
|
405
|
|
|
|
|
406
|
|
|
/** |
|
407
|
|
|
* @see \Elgg\Application::loadCore Do not do work here. Just register for events. |
|
408
|
|
|
*/ |
|
409
|
|
|
return function(\Elgg\EventsService $events, \Elgg\HooksRegistrationService $hooks) { |
|
|
|
|
|
|
410
|
18 |
|
$events->registerHandler('init', 'system', '_elgg_filestore_init', 100); |
|
411
|
|
|
}; |
|
412
|
|
|
|