createAttachment()   F
last analyzed

Complexity

Conditions 27
Paths 1044

Size

Total Lines 164
Code Lines 80

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 309.1971

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 27
eloc 80
nc 1044
nop 1
dl 0
loc 164
ccs 16
cts 59
cp 0.2712
crap 309.1971
rs 0
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file handles the uploading and creation of attachments
5
 * as well as the auto management of the attachment directories.
6
 * Note to enhance documentation later:
7
 * attachment_type = 3 is a thumbnail, etc.
8
 *
9
 * @package   ElkArte Forum
10
 * @copyright ElkArte Forum contributors
11
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
12
 *
13
 * This file contains code covered by:
14
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
15
 *
16
 * @version 2.0 dev
17
 *
18
 */
19
20
use ElkArte\Attachments\AttachmentsDirectory;
21
use ElkArte\Attachments\TemporaryAttachmentProcess;
22
use ElkArte\Cache\Cache;
23
use ElkArte\Errors\Errors;
0 ignored issues
show
Bug introduced by
The type ElkArte\Errors\Errors was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
24
use ElkArte\Graphics\Image;
25
use ElkArte\Helper\FileFunctions;
26
use ElkArte\Helper\TokenHash;
27
use ElkArte\Http\FsockFetchWebdata;
28
use ElkArte\Themes\ThemeLoader;
29
30
/**
31
 * Handles the actual saving of attachments to a directory.
32
 *
33
 * @deprecated since 2.0, use the TemporaryAttachmentProcess class
34
 *
35
 * @return bool
36
 */
37
function processAttachments($id_msg = 0)
38
{
39
	$processAttachments = new TemporaryAttachmentProcess();
40
	return $processAttachments->processAttachments($id_msg);
41
}
42
43
/**
44
 * Checks if an uploaded file produced any appropriate error code
45
 *
46
 * What it does:
47
 *
48 2
 * - Checks for error codes in the error segment of the file array that is
49
 * created by PHP during the file upload.
50 2
 *
51 2
 * @param int $attachID
52
 *
53
 * @return array
54 2
 */
55
function doPHPUploadChecks($attachID)
56
{
57 2
	global $modSettings, $txt;
58
59 2
	$errors = [];
60
61 2
	// Did PHP create any errors during the upload processing of this file?
62
	if (!empty($_FILES['attachment']['error'][$attachID]))
63 2
	{
64 2
		switch ($_FILES['attachment']['error'][$attachID])
65
		{
66
			case UPLOAD_ERR_INI_SIZE:
67
			case UPLOAD_ERR_FORM_SIZE:
68
				$errors[] = ['file_too_big', [$modSettings['attachmentSizeLimit']]];
69
				break;
70
			case UPLOAD_ERR_PARTIAL:
71
			case UPLOAD_ERR_NO_FILE:
72
			case UPLOAD_ERR_EXTENSION:
73 2
				Errors::instance()->log_error($_FILES['attachment']['name'][$attachID] . ': ' . $txt['php_upload_error_' . $_FILES['attachment']['error'][$attachID]]);
74
				$errors[] = 'attach_php_error';
75
				break;
76
			case UPLOAD_ERR_NO_TMP_DIR:
77
			case UPLOAD_ERR_CANT_WRITE:
78
				Errors::instance()->log_error($_FILES['attachment']['name'][$attachID] . ': ' . $txt['php_upload_error_' . $_FILES['attachment']['error'][$attachID]], 'critical');
79
				$errors[] = 'attach_php_error';
80
				break;
81
			default:
82
				Errors::instance()->log_error($_FILES['attachment']['name'][$attachID] . ': ' . $txt['php_upload_error_' . $_FILES['attachment']['error'][$attachID]]);
83
				$errors[] = 'attach_php_error';
84
		}
85
	}
86
87
	return $errors;
88 2
}
89 2
90
/**
91
 * Create an attachment, with the given array of parameters.
92
 *
93
 * What it does:
94
 *
95
 * - Adds any additional or missing parameters to $attachmentOptions.
96
 * - Renames the temporary file.
97
 * - Creates a thumbnail if the file is an image and the option enabled.
98
 *
99
 * @param array $attachmentOptions associative array of options
100
 *
101
 * @return bool
102
 */
103
function createAttachment(&$attachmentOptions)
104
{
105
	global $modSettings;
106
107
	$db = database();
108
	$attachmentsDir = new AttachmentsDirectory($modSettings, $db);
109
110
	$image = new Image($attachmentOptions['tmp_name']);
111
112
	// If this is an image we need to set a few additional parameters.
113
	$is_image = $image->isImageLoaded();
114 2
	$size = $is_image ? $image->getImageDimensions() : [0, 0, 0];
115
	list ($attachmentOptions['width'], $attachmentOptions['height']) = $size;
116 2
	$attachmentOptions['width'] = max(0, $attachmentOptions['width']);
117
	$attachmentOptions['height'] = max(0, $attachmentOptions['height']);
118
119
	// If it's an image, get the mime type right.
120 2
	if ($is_image)
121
	{
122 2
		$attachmentOptions['mime_type'] = getValidMimeImageType($size[2]);
123 2
124 2
		// Want to correct for phonetographer photos?
125 2
		if (!empty($modSettings['attachment_autorotate']))
126 2
		{
127
			$image->autoRotate();
128
		}
129
	}
130
131 2
	// Get the hash if no hash has been given yet.
132
	if (empty($attachmentOptions['file_hash']))
133
	{
134 2
		$attachmentOptions['file_hash'] = getAttachmentFilename($attachmentOptions['name'], 0, null, true);
135 2
	}
136
137
	// Assuming no-one set the extension let's take a look at it.
138 2
	if (empty($attachmentOptions['fileext']))
139
	{
140
		$attachmentOptions['fileext'] = strtolower(strrpos($attachmentOptions['name'], '.') !== false ? substr($attachmentOptions['name'], strrpos($attachmentOptions['name'], '.') + 1) : '');
141
		if (strlen($attachmentOptions['fileext']) > 8 || '.' . $attachmentOptions['fileext'] === $attachmentOptions['name'])
142
		{
143
			$attachmentOptions['fileext'] = '';
144
		}
145
	}
146 2
147
	$db->insert('',
148
		'{db_prefix}attachments',
149
		[
150 2
			'id_folder' => 'int', 'id_msg' => 'int', 'filename' => 'string-255', 'file_hash' => 'string-40', 'fileext' => 'string-8',
151
			'size' => 'int', 'width' => 'int', 'height' => 'int',
152
			'mime_type' => 'string-20', 'approved' => 'int',
153
		],
154
		[
155
			(int) $attachmentOptions['id_folder'],
156
			(int) $attachmentOptions['post'],
157
			$attachmentOptions['name'],
158
			$attachmentOptions['file_hash'],
159
			$attachmentOptions['fileext'],
160
			(int) $attachmentOptions['size'],
161
			(empty($attachmentOptions['width']) ? 0 : (int) $attachmentOptions['width']),
162
			(empty($attachmentOptions['height']) ? 0 : (int) $attachmentOptions['height']),
163
			(!empty($attachmentOptions['mime_type']) ? $attachmentOptions['mime_type'] : ''),
164
			(int) $attachmentOptions['approved'],
165
		],
166
		['id_attach']
167
	);
168
	$attachmentOptions['id'] = $db->insert_id('{db_prefix}attachments');
169
170
	// @todo Add an error here maybe?
171
	if (empty($attachmentOptions['id']))
172
	{
173
		return false;
174
	}
175
176
	// Now that we have the attach id, let's rename this and finish up.
177
	$attachmentOptions['destination'] = getAttachmentFilename(basename($attachmentOptions['name']), $attachmentOptions['id'], $attachmentOptions['id_folder'], false, $attachmentOptions['file_hash']);
178
	if (rename($attachmentOptions['tmp_name'], $attachmentOptions['destination']) && $is_image)
179
	{
180
		// Let the manipulator the (loaded) file new location
181
		$image->setFileName($attachmentOptions['destination']);
182
	}
183
184
	// If it's not approved then add to the approval queue.
185
	if (!$attachmentOptions['approved'])
186
	{
187
		$db->insert('',
188
			'{db_prefix}approval_queue',
189
			[
190
				'id_attach' => 'int', 'id_msg' => 'int',
191
			],
192
			[
193
				$attachmentOptions['id'], (int) $attachmentOptions['post'],
194
			],
195
			[]
196
		);
197
	}
198
199
	if (empty($modSettings['attachmentThumbnails']) || !$is_image || (empty($attachmentOptions['width']) && empty($attachmentOptions['height'])))
200
	{
201
		return true;
202
	}
203
204
	// Like thumbnails, do we?
205
	if (!empty($modSettings['attachmentThumbWidth']) && !empty($modSettings['attachmentThumbHeight'])
206
		&& ($attachmentOptions['width'] > $modSettings['attachmentThumbWidth'] || $attachmentOptions['height'] > $modSettings['attachmentThumbHeight']))
207
	{
208
		$thumb_filename = $attachmentOptions['name'] . '_thumb';
209
		$thumb_path = $attachmentOptions['destination'] . '_thumb';
210
		$thumb_image = $image->createThumbnail($modSettings['attachmentThumbWidth'], $modSettings['attachmentThumbHeight'], $thumb_path);
211
		if ($thumb_image !== false)
212
		{
213
			// Figure out how big we actually made it.
214
			$size = $thumb_image->getImageDimensions();
215
			list ($thumb_width, $thumb_height) = $size;
216
217
			$thumb_mime = getValidMimeImageType($size[2]);
218
			$thumb_size = $thumb_image->getFilesize();
219
			$thumb_file_hash = getAttachmentFilename($thumb_filename, 0, null, true);
220
221
			// We should check the file size and count here since thumbs are added to the existing totals.
222
			$attachmentsDir->checkDirSize($thumb_size);
223
			$current_dir_id = $attachmentsDir->currentDirectoryId();
224
225
			// If a new folder has been already created. Gotta move this thumb there then.
226
			if ($attachmentsDir->isCurrentDirectoryId($attachmentOptions['id_folder']) === false)
227
			{
228
				$current_dir = $attachmentsDir->getCurrent();
229
				$current_dir_id = $attachmentsDir->currentDirectoryId();
230 2
				rename($thumb_path, $current_dir . '/' . $thumb_filename);
231
				$thumb_path = $current_dir . '/' . $thumb_filename;
232 2
			}
233
234
			// To the database we go!
235
			$db->insert('',
236
				'{db_prefix}attachments',
237
				[
238
					'id_folder' => 'int', 'id_msg' => 'int', 'attachment_type' => 'int', 'filename' => 'string-255', 'file_hash' => 'string-40', 'fileext' => 'string-8',
239
					'size' => 'int', 'width' => 'int', 'height' => 'int', 'mime_type' => 'string-20', 'approved' => 'int',
240
				],
241
				[
242
					$current_dir_id, (int) $attachmentOptions['post'], 3, $thumb_filename, $thumb_file_hash, $attachmentOptions['fileext'],
243
					$thumb_size, $thumb_width, $thumb_height, $thumb_mime, (int) $attachmentOptions['approved'],
244
				],
245
				['id_attach']
246
			);
247
			$attachmentOptions['thumb'] = $db->insert_id('{db_prefix}attachments');
248
249
			if (!empty($attachmentOptions['thumb']))
250
			{
251
				$db->query('', '
252
					UPDATE {db_prefix}attachments
253
					SET id_thumb = {int:id_thumb}
254
					WHERE id_attach = {int:id_attach}',
255
					[
256
						'id_thumb' => $attachmentOptions['thumb'],
257
						'id_attach' => $attachmentOptions['id'],
258
					]
259
				);
260
261
				rename($thumb_path, getAttachmentFilename($thumb_filename, $attachmentOptions['thumb'], $current_dir_id, false, $thumb_file_hash));
262
			}
263
		}
264
	}
265
266
	return true;
267
}
268
269
/**
270
 * Get the specified attachment.
271
 *
272
 * What it does:
273
 *
274
 * - This includes a check of the topic
275
 * - it only returns the attachment if it's indeed attached to a message in the topic given as parameter, and
276
 * query_see_board...
277
 * - Must return the same array keys as getAvatar() and getAttachmentThumbFromTopic()
278
 *
279
 * @param int $id_attach
280
 * @param int $id_topic
281
 *
282
 * @return array
283
 */
284
function getAttachmentFromTopic($id_attach, $id_topic)
285
{
286
	$db = database();
287
288
	// Make sure this attachment is on this board.
289
	$attachmentData = [];
290
	$request = $db->fetchQuery('
291
		SELECT 
292
			a.id_folder, a.filename, a.file_hash, a.fileext, a.id_attach, a.attachment_type, 
293
			a.mime_type, a.approved, m.id_member
294
		FROM {db_prefix}attachments AS a
295
			INNER JOIN {db_prefix}messages AS m ON (m.id_msg = a.id_msg AND m.id_topic = {int:current_topic})
296
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board AND {query_see_board})
297
		WHERE a.id_attach = {int:attach}
298
		LIMIT 1',
299
		[
300
			'attach' => $id_attach,
301
			'current_topic' => $id_topic,
302
		]
303
	);
304
	if ($request->num_rows() !== 0)
305
	{
306
		$attachmentData = $request->fetch_assoc();
307
		$attachmentData['id_folder'] = (int) $attachmentData['id_folder'];
308
		$attachmentData['id_attach'] = (int) $attachmentData['id_attach'];
309
		$attachmentData['id_member'] = (int) $attachmentData['id_member'];
310
		$attachmentData['attachment_type'] = (int) $attachmentData['attachment_type'];
311
	}
312
	$request->free_result();
313
314
	return $attachmentData;
315
}
316
317
/**
318
 * Get the thumbnail of specified attachment.
319
 *
320
 * What it does:
321
 *
322
 * - This includes a check of the topic
323
 * - it only returns the attachment if it's indeed attached to a message in the topic given as parameter, and
324
 * query_see_board...
325
 * - Must return the same array keys as getAvatar() & getAttachmentFromTopic
326
 *
327
 * @param int $id_attach
328
 * @param int $id_topic
329
 *
330
 * @return array
331
 */
332
function getAttachmentThumbFromTopic($id_attach, $id_topic)
333
{
334
	$db = database();
335
336
	// Make sure this attachment is on this board.
337
	$request = $db->fetchQuery('
338
		SELECT 
339
			th.id_folder, th.filename, th.file_hash, th.fileext, th.id_attach, 
340
			th.attachment_type, th.mime_type,
341
			a.id_folder AS attach_id_folder, a.filename AS attach_filename,
342
			a.file_hash AS attach_file_hash, a.fileext AS attach_fileext,
343
			a.id_attach AS attach_id_attach, a.attachment_type AS attach_attachment_type,
344
			a.mime_type AS attach_mime_type,
345
		 	a.approved, m.id_member
346
		FROM {db_prefix}attachments AS a
347
			INNER JOIN {db_prefix}messages AS m ON (m.id_msg = a.id_msg AND m.id_topic = {int:current_topic})
348
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board AND {query_see_board})
349
			LEFT JOIN {db_prefix}attachments AS th ON (th.id_attach = a.id_thumb)
350
		WHERE a.id_attach = {int:attach}',
351
		[
352
			'attach' => $id_attach,
353
			'current_topic' => $id_topic,
354
		]
355
	);
356
	$attachmentData = [
357
		'id_folder' => '', 'filename' => '', 'file_hash' => '', 'fileext' => '', 'id_attach' => '',
358
		'attachment_type' => '', 'mime_type' => '', 'approved' => '', 'id_member' => ''];
359
	if ($request->num_rows() !== 0)
360
	{
361
		$row = $request->fetch_assoc();
362
363
		// If there is a hash then the thumbnail exists
364
		if (!empty($row['file_hash']))
365
		{
366
			$attachmentData = [
367
				'id_folder' => $row['id_folder'],
368
				'filename' => $row['filename'],
369
				'file_hash' => $row['file_hash'],
370
				'fileext' => $row['fileext'],
371
				'id_attach' => $row['id_attach'],
372
				'attachment_type' => $row['attachment_type'],
373
				'mime_type' => $row['mime_type'],
374
				'approved' => $row['approved'],
375
				'id_member' => $row['id_member'],
376
			];
377
		}
378
		// otherwise $modSettings['attachmentThumbnails'] may be (or was) off, so original file
379
		elseif (getValidMimeImageType($row['attach_mime_type']) !== '')
380
		{
381
			$attachmentData = [
382
				'id_folder' => $row['attach_id_folder'],
383
				'filename' => $row['attach_filename'],
384
				'file_hash' => $row['attach_file_hash'],
385
				'fileext' => $row['attach_fileext'],
386
				'id_attach' => $row['attach_id_attach'],
387
				'attachment_type' => $row['attach_attachment_type'],
388
				'mime_type' => $row['attach_mime_type'],
389
				'approved' => $row['approved'],
390
				'id_member' => $row['id_member'],
391
			];
392
		}
393
	}
394
395
	return $attachmentData;
396
}
397
398
/**
399
 * Returns if the given attachment ID is an image file or not
400
 *
401
 * What it does:
402
 *
403
 * - Given an attachment id, checks that it exists as an attachment
404
 * - Verifies the message its associated is on a board the user can see
405
 * - Sets 'is_image' if the attachment is an image file
406
 * - Returns basic attachment values
407
 *
408
 * @param int $id_attach
409
 *
410
 * @return array|bool
411
 */
412
function isAttachmentImage($id_attach)
413
{
414
	$db = database();
415
416
	// Make sure this attachment is on this board.
417
	$attachmentData = [];
418
	$db->fetchQuery('
419
		SELECT
420
			a.filename, a.fileext, a.id_attach, a.attachment_type, a.mime_type, a.approved, 
421
			a.downloads, a.size, a.width, a.height, m.id_topic, m.id_board
422
		FROM {db_prefix}attachments as a
423
			INNER JOIN {db_prefix}messages AS m ON (m.id_msg = a.id_msg)
424
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board AND {query_see_board})
425
		WHERE id_attach = {int:attach}
426
			AND attachment_type = {int:type}
427
		LIMIT 1',
428
		[
429
			'attach' => $id_attach,
430
			'type' => 0,
431
		]
432
	)->fetch_callback(
433
		function ($row) use (&$attachmentData) {
434
			$attachmentData = $row;
435
			$attachmentData['is_image'] = str_starts_with($attachmentData['mime_type'], 'image');
436
			$attachmentData['size'] = byte_format($attachmentData['size']);
437
			$attachmentData['is_approved'] = $row['approved'] === '1';
438
		}
439
	);
440
441
	return !empty($attachmentData) ? $attachmentData : false;
442
}
443
444
/**
445
 * Increase download counter for id_attach.
446
 *
447
 * What it does:
448
 *
449
 * - Does not check if it's a thumbnail.
450
 *
451
 * @param int $id_attach
452
 */
453
function increaseDownloadCounter($id_attach)
454
{
455
	$db = database();
456
457
	$db->fetchQuery('
458
		UPDATE {db_prefix}attachments
459
		SET downloads = downloads + 1
460
		WHERE id_attach = {int:id_attach}',
461
		[
462
			'id_attach' => $id_attach,
463
		]
464
	);
465
}
466
467
/**
468
 * Saves a file and stores it locally for avatar use by id_member.
469
 *
470
 * What it does:
471
 *
472
 * - supports input of GIF, JPG, PNG, BMP, WEBP and WBMP formats
473
 * - outputs png, jpg or webp based on best choice
474
 * - uses createThumbnail() to resize to max_width by max_height, and saves the result to a file.
475
 * - updates the database info for the member's avatar.
476
 * - returns whether the download and resize was successful.
477
 *
478
 * @param string $temporary_path the full path to the temporary file
479
 * @param int $memID member ID
480
 * @param int $max_width
481
 * @param int $max_height
482
 * @return bool whether the download and resize was successful.
483
 */
484
function saveAvatar($temporary_path, $memID, $max_width, $max_height)
485
{
486
	global $modSettings;
487
488
	$db = database();
489
490
	// Just making sure there is a non-zero member.
491
	if (empty($memID))
492
	{
493
		return false;
494
	}
495
496
	// Get this party started
497
	$valid_avatar_extensions = [
498
		IMAGETYPE_PNG => 'png',
499
		IMAGETYPE_JPEG => 'jpeg',
500
		IMAGETYPE_WEBP => 'webp'
501
	];
502
503
	$image = new Image($temporary_path);
504
	if (!$image->isImageLoaded())
505
	{
506
		return false;
507
	}
508
509
	$format = $image->getDefaultFormat();
510
	$ext = $valid_avatar_extensions[$format];
511
	$tokenizer = new TokenHash();
512
	$fileName = 'avatar_' . $memID . '_' . $tokenizer->generate_hash(16) . '.' . $ext;
513
514
	// Clear out any old attachment
515
	require_once(SUBSDIR . '/ManageAttachments.subs.php');
516
	removeAttachments(['id_member' => $memID]);
517
518
	$db->insert('',
519
		'{db_prefix}attachments',
520
		[
521
			'id_member' => 'int', 'attachment_type' => 'int', 'filename' => 'string-255',
522
			'file_hash' => 'string-255', 'fileext' => 'string-8', 'size' => 'int', 'id_folder' => 'int',
523
		],
524
		[
525
			$memID, 1, $fileName, '', $ext, 1, 1,
526
		],
527
		['id_attach']
528
	);
529
	$attachID = $db->insert_id('{db_prefix}attachments');
530
531
	// The destination filename depends on the custom dir for avatars
532
	$destName = $modSettings['custom_avatar_dir'] . '/' . $fileName;
533
534
	// Resize and rotate it.
535
	if (!empty($modSettings['attachment_autorotate']))
536
	{
537
		$image->autoRotate();
538
	}
539
540
	$thumb_image = $image->createThumbnail($max_width, $max_height, $destName, $format);
541
	if ($thumb_image !== false)
542
	{
543
		list ($width, $height) = $thumb_image->getImageDimensions();
544
		$mime_type = $thumb_image->getMimeType();
545
546
		// Write filesize in the database.
547
		$db->query('', '
548
			UPDATE {db_prefix}attachments
549
			SET 
550
				size = {int:filesize}, width = {int:width}, height = {int:height}, 
551
				mime_type = {string:mime_type}
552
			WHERE id_attach = {int:current_attachment}',
553
			[
554
				'filesize' => $thumb_image->getFilesize(),
555
				'width' => (int) $width,
556
				'height' => (int) $height,
557
				'current_attachment' => $attachID,
558
				'mime_type' => $mime_type,
559
			]
560
		);
561
562
		// Retain this globally in case the script wants it.
563
		$modSettings['new_avatar_data'] = [
564
			'id' => $attachID,
565
			'filename' => $destName,
566
			'type' => 1,
567
		];
568
569
		return true;
570
	}
571
572
	// Having a problem with image manipulation, rotation, resize, etc
573
	$db->query('', '
574
		DELETE FROM {db_prefix}attachments
575
		WHERE id_attach = {int:current_attachment}',
576
		[
577
			'current_attachment' => $attachID,
578
		]
579
	);
580
581
	return false;
582
}
583
584
/**
585
 * Get the size of a specified image with better error handling.
586
 *
587
 * What it does:
588
 *
589
 * - Uses getimagesizefromstring() to determine the dimensions of an image file.
590
 * - Attempts to connect to the server first, so it won't time out.
591
 * - Attempts to read a short byte range of the file, just enough to validate
592
 * the mime type.
593
 *
594
 * @param string $url
595
 * @return array|bool the image size as array(width, height), or false on failure
596
 */
597
function url_image_size($url)
598
{
599
	// Can we pull this from the cache... please please?
600
	$temp = [];
601
	if (Cache::instance()->getVar($temp, 'url_image_size-' . md5($url), 3600))
602
	{
603
		return $temp;
604
	}
605
606
	$url_path = parse_url($url, PHP_URL_PATH);
607
	$extension = pathinfo($url_path, PATHINFO_EXTENSION);
608
609
	// Set a RANGE to read
610
	switch ($extension)
611
	{
612
		case 'jpg':
613
		case 'jpeg':
614
			// Size location block is variable, so we fetch a meaningful chunk
615
			$range = 32768;
616
			break;
617
		case 'png':
618
			// Size will be in the first 24 bytes
619
			$range = 1024;
620
			break;
621
		case 'gif':
622
			// Size will be in the first 10 bytes
623
			$range = 1024;
624
			break;
625
		case 'bmp':
626
			// Size will be in the first 32 bytes
627
			$range = 1024;
628
			break;
629
		default:
630
			// Read the entire file then, webp for example might have the exif at the end
631
			$range = 0;
632
	}
633
634
	$image = new FsockFetchWebdata(['max_length' => $range]);
635
	$image->get_url_data($url);
636
637
	// The server may not understand Range: so lets try to fetch the entire thing
638
	// assuming we were not simply turned away and did not already try
639
	if ($range !== 0 && $image->result('code') != 200 && $image->result('code') != 403)
640
	{
641
		$image = new FsockFetchWebdata([]);
642
		$image->get_url_data($url);
643
	}
644
645
	// Here is the data, getimagesizefromstring does not care if its a complete image, it only
646
	// searches for size headers in a given data set.
647
	$data = $image->result('body');
648
	unset($image);
649
	$size = empty($data) ? elk_getimagesize($url) : getimagesizefromstring($data);
0 ignored issues
show
Bug introduced by
It seems like $data can also be of type string[]; however, parameter $string of getimagesizefromstring() does only seem to accept string, 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 ignore-type  annotation

649
	$size = empty($data) ? elk_getimagesize($url) : getimagesizefromstring(/** @scrutinizer ignore-type */ $data);
Loading history...
650
651
	// Well, ok, umm, fail!
652
	if ($size === false || $data === false)
0 ignored issues
show
introduced by
The condition $data === false is always false.
Loading history...
653
	{
654
		$size = [-1, -1, -1];
655
	}
656
657
	// Save this for 1hour, its not like the image size is going to change, and if we
658
	// failed, no sense trying again and again!
659
	Cache::instance()->put('url_image_size-' . md5($url), $size, 3600);
660
661
	return $size;
662
}
663
664
/**
665
 * Function to retrieve server-stored avatar files
666
 *
667
 * @param string $directory
668
 * @return array
669
 */
670
function getServerStoredAvatars($directory)
671
{
672
	global $context, $txt, $modSettings;
673
674
	$result = [];
675
	$file_functions = FileFunctions::instance();
676
677
	// You can always have no avatar
678
	$result[] = [
679
		'filename' => 'blank.png',
680
		'checked' => in_array($context['member']['avatar']['server_pic'], ['', 'blank.png']),
681
		'name' => $txt['no_pic'],
682
		'is_dir' => false
683
	];
684
685
	// Not valid is easy
686
	$avatarDir = $modSettings['avatar_directory'] . (!empty($directory) ? '/' : '') . $directory;
687
	if (!$file_functions->isDir($avatarDir))
688
	{
689
		return $result;
690
	}
691
692
	// Find all avatars under, and in, the avatar directory
693
	$serverAvatars = new RecursiveIteratorIterator(
694
		new RecursiveDirectoryIterator(
695
			$avatarDir,
696
			FilesystemIterator::SKIP_DOTS
697
		),
698
		\RecursiveIteratorIterator::SELF_FIRST,
699
		\RecursiveIteratorIterator::CATCH_GET_CHILD
700
	);
701
	$key = 0;
702
	foreach ($serverAvatars as $entry)
703
	{
704
		if ($entry->isDir())
705
		{
706
			// Add a new directory
707
			$result[] = [
708
				'filename' => htmlspecialchars(basename($entry), ENT_COMPAT, 'UTF-8'),
709
				'checked' => str_contains($context['member']['avatar']['server_pic'], basename($entry) . '/'),
710
				'name' => '[' . htmlspecialchars(str_replace('_', ' ', basename($entry)), ENT_COMPAT, 'UTF-8') . ']',
711
				'is_dir' => true,
712
				'files' => []
713
			];
714
			$key++;
715
716
			continue;
717
		}
718
719
		// Add the files under the current directory we are iterating on
720
		if (!in_array($entry->getFilename(), ['blank.png', 'index.php', '.htaccess']))
721
		{
722
			$extension = $entry->getExtension();
723
			$filename = $entry->getBasename('.' . $extension);
724
725
			// Make sure it is an image.
726
			if (empty(getValidMimeImageType($extension)))
727
			{
728
				continue;
729
			}
730
731
			$result[$key]['files'][] = [
732
				'filename' => htmlspecialchars($entry->getFilename(), ENT_COMPAT, 'UTF-8'),
733
				'checked' => $entry->getFilename() == $context['member']['avatar']['server_pic'],
734
				'name' => htmlspecialchars(str_replace('_', ' ', $filename), ENT_COMPAT, 'UTF-8'),
735
				'is_dir' => false
736
			];
737
738
			if (dirname($entry->getPath(), 1) === $modSettings['avatar_directory'])
739
			{
740
				$context['avatar_list'][] = str_replace($modSettings['avatar_directory'] . '/', '', $entry->getPathname());
741
			}
742
		}
743
	}
744
745
	return $result;
746
}
747
748
/**
749
 * Update an attachment's thumbnail
750
 *
751
 * @param string $filename the actual name of the file
752
 * @param int $id_attach the numeric attach id
753
 * @param int $id_msg the numeric message the attachment is associated with
754
 * @param int $old_id_thumb = 0 id of thumbnail to remove, such as from our post form
755
 * @param string $real_filename the fully qualified hash name of where the file is
756
 * @return array The updated information
757
 */
758
function updateAttachmentThumbnail($filename, $id_attach, $id_msg, $old_id_thumb = 0, $real_filename = '')
759
{
760
	global $modSettings;
761
762
	$attachment = ['id_attach' => $id_attach];
763
764
	// Load our image functions, it will determine which graphics library to use
765
	$image = new Image($filename);
766
767
	// Image is not autorotated because it was at the time of upload (hopefully)
768
	$thumb_filename = (!empty($real_filename) ? $real_filename : $filename) . '_thumb';
769
	$thumb_image = $image->createThumbnail($modSettings['attachmentThumbWidth'], $modSettings['attachmentThumbHeight']);
770
771
	if ($thumb_image instanceof Image)
772
	{
773
		// So what folder are we putting this image in?
774
		$attachmentsDir = new AttachmentsDirectory($modSettings, database());
775
		$id_folder_thumb = $attachmentsDir->currentDirectoryId();
776
777
		// Calculate the size of the created thumbnail.
778
		$size = $thumb_image->getImageDimensions();
779
		list ($attachment['thumb_width'], $attachment['thumb_height']) = $size;
780
		$thumb_size = $thumb_image->getFilesize();
781
782
		// Figure out the mime type and other details
783
		$thumb_mime = getValidMimeImageType($size[2]);
784
		$thumb_ext = substr($thumb_mime, strpos($thumb_mime, '/') + 1);
785
		$thumb_hash = getAttachmentFilename($thumb_filename, 0, null, true);
786
787
		// Add this beauty to the database.
788
		$db = database();
789
		$db->insert('',
790
			'{db_prefix}attachments',
791
			['id_folder' => 'int', 'id_msg' => 'int', 'attachment_type' => 'int', 'filename' => 'string-255', 'file_hash' => 'string-40', 'size' => 'int', 'width' => 'int', 'height' => 'int', 'fileext' => 'string-8', 'mime_type' => 'string-255'],
792
			[$id_folder_thumb, $id_msg, 3, $thumb_filename, $thumb_hash, (int) $thumb_size, (int) $attachment['thumb_width'], (int) $attachment['thumb_height'], $thumb_ext, $thumb_mime],
793
			['id_attach']
794
		);
795
796
		$attachment['id_thumb'] = $db->insert_id('{db_prefix}attachments');
797
		if (!empty($attachment['id_thumb']))
798
		{
799
			$db->query('', '
800
				UPDATE {db_prefix}attachments
801
				SET id_thumb = {int:id_thumb}
802
				WHERE id_attach = {int:id_attach}',
803
				[
804
					'id_thumb' => $attachment['id_thumb'],
805
					'id_attach' => $attachment['id_attach'],
806
				]
807
			);
808
809
			$thumb_realname = getAttachmentFilename($thumb_filename, $attachment['id_thumb'], $id_folder_thumb, false, $thumb_hash);
0 ignored issues
show
Bug introduced by
It seems like $attachment['id_thumb'] can also be of type boolean; however, parameter $attachment_id of getAttachmentFilename() does only seem to accept integer|null, 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 ignore-type  annotation

809
			$thumb_realname = getAttachmentFilename($thumb_filename, /** @scrutinizer ignore-type */ $attachment['id_thumb'], $id_folder_thumb, false, $thumb_hash);
Loading history...
810
			if (file_exists($filename . '_thumb'))
811
			{
812
				rename($filename . '_thumb', $thumb_realname);
813
			}
814
815
			// Do we need to remove an old thumbnail?
816
			if (!empty($old_id_thumb))
817
			{
818
				require_once(SUBSDIR . '/ManageAttachments.subs.php');
819
				removeAttachments(['id_attach' => $old_id_thumb], '', false, false);
820
			}
821
		}
822
	}
823
824
	return $attachment;
825
}
826
827
/**
828
 * Compute and return the total size of attachments to a single message.
829
 *
830
 * @param int $id_msg
831
 * @param bool $include_count = true if true, it also returns the attachments count
832
 * @return mixed
833 2
 */
834 2
function attachmentsSizeForMessage($id_msg, $include_count = true)
835
{
836
	$db = database();
837
838
	if ($include_count)
839 2
	{
840 2
		$request = $db->fetchQuery('
841
			SELECT 
842 1
				COUNT(*), SUM(size)
843
			FROM {db_prefix}attachments
844 2
			WHERE id_msg = {int:id_msg}
845 2
				AND attachment_type = {int:attachment_type}',
846
			[
847 2
				'id_msg' => $id_msg,
848 2
				'attachment_type' => 0,
849 2
			]
850
		);
851 2
	}
852 2
	else
853 2
	{
854
		$request = $db->fetchQuery('
855 2
			SELECT 
856 2
				COUNT(*)
857
			FROM {db_prefix}attachments
858
			WHERE id_msg = {int:id_msg}
859
				AND attachment_type = {int:attachment_type}',
860
			[
861
				'id_msg' => $id_msg,
862
				'attachment_type' => 0,
863
			]
864
		);
865 2
	}
866 2
867
	return $request->fetch_row();
868
}
869
870 2
/**
871
 * Older attachments may still use this function.
872
 *
873
 * @param string $filename
874
 * @param int $attachment_id
875
 * @param string|null $dir
876
 * @param bool $new
877
 *
878 2
 * @return null|string|string[]
879 2
 */
880
function getLegacyAttachmentFilename($filename, $attachment_id, $dir = null, $new = false)
0 ignored issues
show
Unused Code introduced by
The parameter $dir 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

880
function getLegacyAttachmentFilename($filename, $attachment_id, /** @scrutinizer ignore-unused */ $dir = null, $new = false)

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...
881
{
882 2
	global $modSettings;
883
884
	$clean_name = $filename;
885
886
	// Sorry, no spaces, dots, or anything else but letters allowed.
887 2
	$clean_name = preg_replace(['/\s/', '/[^\w_\.\-]/'], ['_', ''], $clean_name);
888
889 2
	$enc_name = $attachment_id . '_' . str_replace('.', '_', $clean_name) . md5($clean_name);
890
	$clean_name = preg_replace('~\.[\.]+~', '.', $clean_name);
891
892
	if (empty($attachment_id) || ($new && empty($modSettings['attachmentEncryptFilenames'])))
893
	{
894
		return $clean_name;
895
	}
896
897
	if ($new)
898
	{
899
		return $enc_name;
900
	}
901
902
	$attachmentsDir = new AttachmentsDirectory($modSettings, database());
903
	$path = $attachmentsDir->getCurrent();
904
905
	return file_exists($path . '/' . $enc_name) ? $path . '/' . $enc_name : $path . '/' . $clean_name;
906
}
907
908
/**
909
 * Binds a set of attachments to a message.
910
 *
911
 * @param int $id_msg
912
 * @param int[] $attachment_ids
913
 */
914
function bindMessageAttachments($id_msg, $attachment_ids)
915
{
916
	$db = database();
917
918
	$db->query('', '
919
		UPDATE {db_prefix}attachments
920
		SET id_msg = {int:id_msg}
921
		WHERE id_attach IN ({array_int:attachment_list})',
922
		[
923
			'attachment_list' => $attachment_ids,
924
			'id_msg' => $id_msg,
925
		]
926
	);
927
}
928
929
/**
930
 * Get an attachment's encrypted filename. If $new is true, won't check for file existence.
931
 *
932
 * - If new is set returns a hash for the db
933
 * - If no file hash is supplied, determines one and returns it
934
 * - Returns the path to the file
935
 *
936
 * @param string $filename The name of the file
937
 * @param int|null $attachment_id The ID of the attachment
938
 * @param string|null $dir Which directory it should be in (null to use current)
939
 * @param bool $new If this is a new attachment, if so just returns a hash
940
 * @param string $file_hash The file hash
941
 *
942
 * @return string
943
 * @todo this currently returns the hash if new, and the full filename otherwise.
944
 * Something messy like that.
945
 * @todo and of course everything relies on this behavior and work around it. :P.
946
 * Converters included.
947
 */
948
function getAttachmentFilename($filename, $attachment_id, $dir = null, $new = false, $file_hash = '')
949
{
950
	global $modSettings;
951
952
	// Just make up a nice hash...
953 2
	if ($new)
954
	{
955 2
		$tokenizer = new TokenHash();
956
957 2
		return $tokenizer->generate_hash(32);
958 2
	}
959 2
960
	// In case of files from the old system, do a legacy call.
961
	if (empty($file_hash))
962 2
	{
963 2
		return getLegacyAttachmentFilename($filename, $attachment_id, $dir, $new);
964 2
	}
965 2
966
	// If we were passed the directory id, use it
967
	$modSettings['currentAttachmentUploadDir'] = $dir;
968
	$attachmentsDir = new AttachmentsDirectory($modSettings, database());
969 2
	$path = $attachmentsDir->getCurrent();
970 2
971
	return $path . '/' . $attachment_id . '_' . $file_hash . '.elk';
972 2
}
973
974
/**
975
 * Returns the board and the topic the attachment belongs to.
976
 *
977
 * @param int $id_attach
978
 * @return int[]|bool on fail else an array of id_board, id_topic
979
 */
980
function getAttachmentPosition($id_attach)
981
{
982
	$db = database();
983
984
	// Make sure this attachment is on this board.
985
	$request = $db->fetchQuery('
986 2
		SELECT 
987
			m.id_board, m.id_topic
988
		FROM {db_prefix}attachments AS a
989
			LEFT JOIN {db_prefix}messages AS m ON (m.id_msg = a.id_msg)
990 2
			LEFT JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board)
991
		WHERE a.id_attach = {int:attach}
992 2
			AND {query_see_board}
993
		LIMIT 1',
994
		[
995
			'attach' => $id_attach,
996
		]
997 2
	);
998
999
	$attachmentData = $request->fetch_all();
1000
1001
	if (empty($attachmentData))
1002
	{
1003
		return false;
1004
	}
1005
1006
	return $attachmentData[0];
1007
}
1008
1009
/**
1010
 * Simple wrapper for getimagesize
1011
 *
1012
 * @param string $file
1013
 * @param string|bool $error return array or false on error
1014
 *
1015
 * @return array|bool
1016
 */
1017
function elk_getimagesize($file, $error = 'array')
1018
{
1019
	$sizes = @getimagesize($file);
1020
1021
	// Can't get it, what shall we return
1022
	if (empty($sizes))
1023
	{
1024
		$sizes = $error === 'array' ? [-1, -1, -1] : false;
1025
	}
1026
1027
	return $sizes;
1028
}
1029
1030
/**
1031
 * Checks if we have a known and support mime-type for which we have a thumbnail image
1032
 *
1033
 * @param string $file_ext
1034
 * @param bool $url
1035
 *
1036
 * @return bool|string
1037
 */
1038
function returnMimeThumb($file_ext, $url = false)
1039
{
1040
	global $settings;
1041
1042
	// These are not meant to be exhaustive, just some of the most common attached on a forum
1043
	$generics = [
1044
		'arc' => ['tgz', 'zip', 'rar', '7z', 'gz'],
1045
		'doc' => ['doc', 'docx', 'wpd', 'odt'],
1046
		'sound' => ['wav', 'mp3', 'pcm', 'aiff', 'wma', 'm4a', 'flac'],
1047
		'video' => ['mp4', 'mgp', 'mpeg', 'mp4', 'wmv', 'mkv', 'flv', 'aiv', 'mov', 'swf'],
1048
		'txt' => ['rtf', 'txt', 'log'],
1049
		'presentation' => ['ppt', 'pps', 'odp'],
1050
		'spreadsheet' => ['xls', 'xlr', 'ods'],
1051
		'web' => ['html', 'htm']
1052
	];
1053
	foreach ($generics as $generic_extension => $generic_types)
1054
	{
1055
		if (in_array($file_ext, $generic_types))
1056
		{
1057
			$file_ext = $generic_extension;
1058
			break;
1059
		}
1060
	}
1061
1062
	static $distinct = ['arc', 'doc', 'sound', 'video', 'txt', 'presentation', 'spreadsheet', 'web',
1063
							 'c', 'cpp', 'css', 'csv', 'java', 'js', 'pdf', 'php', 'sql', 'xml'];
1064
1065
	if (empty($settings))
1066
	{
1067
		ThemeLoader::loadEssentialThemeData();
1068
	}
1069
1070
	// Return the mine thumbnail if it exists or just the default
1071
	if (!in_array($file_ext, $distinct) || !file_exists($settings['theme_dir'] . '/images/mime_images/' . $file_ext . '.png'))
1072
	{
1073
		$file_ext = 'default';
1074
	}
1075
1076
	$location = $url ? $settings['theme_url'] : $settings['theme_dir'];
1077
1078
	return $location . '/images/mime_images/' . $file_ext . '.png';
1079
}
1080
1081
/**
1082
 * From either a mime type, an extension or an IMAGETYPE_* constant
1083
 * returns a valid image mime type
1084
 *
1085
 * @param string $mime
1086
 *
1087
 * @return string
1088
 */
1089
function getValidMimeImageType($mime)
1090
{
1091
	// These are the only valid image types.
1092
	$validImageTypes = [
1093
		-1 => 'jpg',
1094
		// Starting from here are the IMAGETYPE_* constants
1095
		IMAGETYPE_GIF => 'gif',
1096
		IMAGETYPE_JPEG => 'jpeg',
1097
		IMAGETYPE_PNG => 'png',
1098
		IMAGETYPE_PSD => 'psd',
1099
		IMAGETYPE_BMP => 'bmp',
1100
		IMAGETYPE_TIFF_II => 'tiff',
1101
		IMAGETYPE_TIFF_MM => 'tiff',
1102
		IMAGETYPE_JPC => 'jpeg',
1103
		IMAGETYPE_IFF => 'iff',
1104
		IMAGETYPE_WBMP => 'bmp',
1105
		IMAGETYPE_WEBP => 'webp'
1106
	];
1107
1108
	$ext = (int) $mime > 0 && isset($validImageTypes[(int) $mime]) ? $validImageTypes[(int) $mime] : '';
1109
	if (empty($ext))
1110
	{
1111
		$ext = strtolower(trim(substr($mime, strpos($mime, '/')), '/'));
1112
	}
1113
1114
	return in_array($ext, $validImageTypes) ? 'image/' . $ext : '';
1115
}
1116
1117
/**
1118
 * This function returns the mimeType of a file using the best means available
1119
 *
1120
 * @param string $filename
1121
 * @return string
1122
 */
1123
function getMimeType($filename)
1124
{
1125
	$mimeType = '';
1126
1127
	// Check only existing readable files
1128
	if (!file_exists($filename) || !is_readable($filename))
1129
	{
1130
		return '';
1131
	}
1132
1133
	// Try finfo, this is the preferred way
1134
	if (function_exists('finfo_open'))
1135
	{
1136
		$finfo = finfo_open(FILEINFO_MIME);
1137
		$mimeType = finfo_file($finfo, $filename);
1138
		finfo_close($finfo);
1139
	}
1140
	// No finfo? What? lets try the old mime_content_type
1141
	elseif (function_exists('mime_content_type'))
1142
	{
1143
		$mimeType = mime_content_type($filename);
1144
	}
1145
	// Try using an exec call
1146
	elseif (function_exists('exec'))
1147
	{
1148
		$mimeType = @exec("/usr/bin/file -i -b $filename");
1149
	}
1150
1151
	// Still nothing? We should at least be able to get images correct
1152
	if (empty($mimeType))
1153
	{
1154
		$imageData = elk_getimagesize($filename, 'none');
1155
		if (!empty($imageData['mime']))
1156
		{
1157
			$mimeType = $imageData['mime'];
1158
		}
1159
	}
1160
1161
	// Account for long responses like text/plain; charset=us-ascii
1162
	if (!empty($mimeType) && strpos($mimeType, ';'))
1163
	{
1164
		list($mimeType,) = explode(';', $mimeType);
1165
	}
1166
1167
	return $mimeType;
1168
}
1169