createAttachment()   F
last analyzed

Complexity

Conditions 27
Paths 1044

Size

Total Lines 157
Code Lines 81

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 286.9261

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 27
eloc 81
c 1
b 0
f 0
nc 1044
nop 1
dl 0
loc 157
rs 0
ccs 16
cts 55
cp 0.2909
crap 286.9261

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

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

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

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