Passed
Push — master ( c0a3a7...3b84a4 )
by Jeroen
58:51
created

engine/lib/filestore.php (1 issue)

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) {
1 ignored issue
show
The parameter $hooks is not used and could be removed. ( Ignorable by Annotation )

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

409
return function(\Elgg\EventsService $events, /** @scrutinizer ignore-unused */ \Elgg\HooksRegistrationService $hooks) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
410 18
	$events->registerHandler('init', 'system', '_elgg_filestore_init', 100);
411
};
412