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

engine/lib/filestore.php (10 issues)

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);
0 ignored issues
show
Security Best Practice introduced by Brett Profitt
It seems like you do not handle an error condition for closedir(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

32
	/** @scrutinizer ignore-unhandled */ @closedir($handle);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
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) {
2 ignored issues
show
The parameter $type 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

164
function _elgg_filestore_detect_mimetype($hook, /** @scrutinizer ignore-unused */ $type, $mime_type, $params) {

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...
The parameter $hook 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

164
function _elgg_filestore_detect_mimetype(/** @scrutinizer ignore-unused */ $hook, $type, $mime_type, $params) {

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...
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) {
2 ignored issues
show
The parameter $hook 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

183
function _elgg_filestore_parse_simpletype(/** @scrutinizer ignore-unused */ $hook, $type, $simple_type, $params) {

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...
The parameter $type 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

183
function _elgg_filestore_parse_simpletype($hook, /** @scrutinizer ignore-unused */ $type, $simple_type, $params) {

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...
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;
1 ignored issue
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
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) {
1 ignored issue
show
The parameter $event 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

292
function _elgg_filestore_touch_icons(/** @scrutinizer ignore-unused */ $event, $type, $entity) {

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...
The parameter $type 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

292
function _elgg_filestore_touch_icons($event, /** @scrutinizer ignore-unused */ $type, $entity) {

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...
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) {
1 ignored issue
show
The parameter $event 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

326
function _elgg_filestore_move_icons(/** @scrutinizer ignore-unused */ $event, $type, $entity) {

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...
The parameter $type 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

326
function _elgg_filestore_move_icons($event, /** @scrutinizer ignore-unused */ $type, $entity) {

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...
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