Passed
Pull Request — development (#3829)
by Spuds
07:44
created

url_image_size()   B

Complexity

Conditions 8
Paths 9

Size

Total Lines 48
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

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

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

791
			$thumb_realname = getAttachmentFilename($thumb_filename, /** @scrutinizer ignore-type */ $attachment['id_thumb'], $id_folder_thumb, false, $thumb_hash);
Loading history...
792
			if (file_exists($filename . '_thumb'))
793
			{
794
				rename($filename . '_thumb', $thumb_realname);
795
			}
796
797
			// Do we need to remove an old thumbnail?
798
			if (!empty($old_id_thumb))
799
			{
800
				require_once(SUBSDIR . '/ManageAttachments.subs.php');
801
				removeAttachments(['id_attach' => $old_id_thumb], '', false, false);
802
			}
803
		}
804
	}
805
806
	return $attachment;
807
}
808
809
/**
810
 * Compute and return the total size of attachments to a single message.
811
 *
812
 * @param int $id_msg
813
 * @param bool $include_count = true if true, it also returns the attachments count
814
 * @return int[]|int
815
 */
816
function attachmentsSizeForMessage($id_msg, $include_count = true)
817
{
818
	$db = database();
819
820
	if ($include_count)
821
	{
822
		$request = $db->fetchQuery('
823
			SELECT 
824
				COUNT(*), SUM(size)
825
			FROM {db_prefix}attachments
826
			WHERE id_msg = {int:id_msg}
827
				AND attachment_type = {int:attachment_type}',
828
			[
829
				'id_msg' => $id_msg,
830
				'attachment_type' => 0,
831
			]
832
		);
833 2
	}
834 2
	else
835
	{
836
		$request = $db->fetchQuery('
837
			SELECT 
838
				COUNT(*)
839 2
			FROM {db_prefix}attachments
840 2
			WHERE id_msg = {int:id_msg}
841
				AND attachment_type = {int:attachment_type}',
842 1
			[
843
				'id_msg' => $id_msg,
844 2
				'attachment_type' => 0,
845 2
			]
846
		);
847 2
	}
848 2
849 2
	$row = $request->fetch_row();
850
851 2
	return array_map(static function ($v) {
852 2
		return $v === null ? 0 : (int) $v;
853 2
	}, $row);
854
}
855 2
856 2
/**
857
 * Older attachments may still use this function.
858
 *
859
 * @param string $filename
860
 * @param int $attachment_id
861
 * @param string|null $dir
862
 * @param bool $new
863
 *
864
 * @return null|string|string[]
865 2
 */
866 2
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

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