Passed
Pull Request — development (#3580)
by Emanuele
06:53
created

getAttachmentFromTopic()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 27
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
eloc 10
dl 0
loc 27
rs 9.9332
c 0
b 0
f 0
cc 2
nc 2
nop 2
ccs 0
cts 3
cp 0
crap 6
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\Cache\Cache;
21
use ElkArte\Errors\AttachmentErrorContext;
22
use ElkArte\Graphics\Image;
23
use ElkArte\Http\FsockFetchWebdata;
24
use ElkArte\TemporaryAttachment;
25
use ElkArte\Themes\ThemeLoader;
26
use ElkArte\TokenHash;
27
use ElkArte\User;
28
use ElkArte\AttachmentsDirectory;
29
use ElkArte\TemporaryAttachmentsList;
30
use ElkArte\FileFunctions;
31
32
/**
33
 * Handles the actual saving of attachments to a directory.
34
 *
35
 * What it does:
36
 *
37
 * - Loops through $_FILES['attachment'] array and saves each file to the current attachments' folder.
38
 * - Validates the save location actually exists.
39
 *
40
 * @param int|null $id_msg = null or id of the message with attachments, if any.
41
 *                  If null, this is an upload in progress for a new post.
42
 * @return bool
43
 * @package Attachments
44
 */
45
function processAttachments($id_msg = null)
46
{
47
	global $context, $modSettings, $txt, $topic, $board;
48 2
49
	$attach_errors = AttachmentErrorContext::context();
50 2
51 2
	$file_functions = FileFunctions::instance();
52
	$tmp_attachments = new TemporaryAttachmentsList();
53
	$attachmentDirectory = new AttachmentsDirectory($modSettings, database());
54 2
55
	// Make sure we're uploading to the right place.
56
	$attachmentDirectory->automanageCheckDirectory(isset($_REQUEST['action']) && $_REQUEST['action'] === 'admin');
57 2
	$attach_current_dir = $attachmentDirectory->getCurrent();
58
	if (!$file_functions->isDir($attach_current_dir))
59 2
	{
60
		$tmp_attachments->setSystemError('attach_folder_warning');
61 2
		\ElkArte\Errors\Errors::instance()->log_error(sprintf($txt['attach_folder_admin_warning'], $attach_current_dir), 'critical');
62
	}
63 2
64 2
	if ($tmp_attachments->hasSystemError() === false && !isset($context['attachments']['quantity']))
65
	{
66
		$context['attachments']['quantity'] = 0;
67
		$context['attachments']['total_size'] = 0;
68
69
		// If this isn't a new post, check the current attachments.
70
		if (!empty($id_msg))
71
		{
72
			list ($context['attachments']['quantity'], $context['attachments']['total_size']) = attachmentsSizeForMessage($id_msg);
73 2
		}
74
	}
75
76
	// There are files in session (temporary attachments list), likely already processed
77
	$ignore_temp = false;
78
	if ($tmp_attachments->getPostParam('files') !== null && $tmp_attachments->hasAttachments())
79
	{
80
		// Let's try to keep them. But...
81
		$ignore_temp = true;
82
83
		// If new files are being added. We can't ignore those
84
		if (!empty($_FILES['attachment']['tmp_name']))
85
		{
86
			// If the array is not empty
87
			if (count(array_filter($_FILES['attachment']['tmp_name'])) !== 0)
88 2
			{
89 2
				$ignore_temp = false;
90
			}
91
		}
92
93
		// Need to make space for the new files. So, bye bye.
94
		if (!$ignore_temp)
95
		{
96
			$tmp_attachments->removeAll(User::$info->id);
97
			$tmp_attachments->unset();
98
99
			$attach_errors->activate()->addError('temp_attachments_flushed');
100
		}
101
	}
102
103
	if (!isset($_FILES['attachment']['name']))
104
	{
105
		$_FILES['attachment']['tmp_name'] = [];
106
	}
107
108
	// Remember where we are at. If it's anywhere at all.
109
	if (!$ignore_temp)
110
	{
111
		$tmp_attachments->setPostParam([
112
			'msg' => $id_msg ?? 0,
113
			'last_msg' => (int) ($_REQUEST['last_msg'] ?? 0),
114 2
			'topic' => (int) ($topic ?? 0),
115
			'board' => (int) ($board ?? 0),
116 2
		]);
117
	}
118
119
	// If we have an initial error, lets just display it.
120 2
	if ($tmp_attachments->hasSystemError())
121
	{
122 2
		// This is a generic error
123 2
		$attach_errors->activate();
124 2
		$attach_errors->addError('attach_no_upload');
125 2
126 2
		// And delete the files 'cos they ain't going nowhere.
127
		foreach ($_FILES['attachment']['tmp_name'] as $n => $dummy)
128
		{
129
			if (is_writable($_FILES['attachment']['tmp_name'][$n]))
130
			{
131 2
				unlink($_FILES['attachment']['tmp_name'][$n]);
132
			}
133
		}
134 2
135 2
		$_FILES['attachment']['tmp_name'] = array();
136
	}
137
138 2
	// Loop through $_FILES['attachment'] array and move each file to the current attachments' folder.
139
	foreach ($_FILES['attachment']['tmp_name'] as $n => $dummy)
140
	{
141
		if ($_FILES['attachment']['name'][$n] == '')
142
		{
143
			continue;
144
		}
145
146 2
		// First, let's check for PHP upload errors.
147
		$errors = attachmentUploadChecks($n);
148
149
		$tokenizer = new TokenHash();
150 2
		$temp_file = new TemporaryAttachment([
151
			'name' => basename($_FILES['attachment']['name'][$n]),
152
			'tmp_name' => $_FILES['attachment']['tmp_name'][$n],
153
			'attachid' => $tmp_attachments->getTplName(User::$info->id, $tokenizer->generate_hash(16)),
154
			'public_attachid' => $tmp_attachments->getTplName(User::$info->id, $tokenizer->generate_hash(16)),
155
			'user_id' => User::$info->id,
156
			'size' => $_FILES['attachment']['size'][$n],
157
			'type' => $_FILES['attachment']['type'][$n],
158
			'id_folder' => $attachmentDirectory->currentDirectoryId(),
159
			'mime' => getMimeType($_FILES['attachment']['tmp_name'][$n]),
160
		]);
161
162
		// If we are error free, Try to move and rename the file before doing more checks on it.
163
		if (empty($errors))
164
		{
165
			$temp_file->moveUploaded($attach_current_dir);
166
		}
167
		// Upload error(s) were detected, flag the error, remove the file
168
		else
169
		{
170
			$temp_file->setErrors($errors);
171
			$temp_file->remove(false);
172
		}
173
174
		// The file made it to the server, so do more checks injection, size, extension
175
		$temp_file->doChecks($attachmentDirectory);
176
177
		// Sort out the errors for display and delete any associated files.
178
		if ($temp_file->hasErrors())
179
		{
180
			$attach_errors->addAttach($temp_file['attachid'], $temp_file->getName());
0 ignored issues
show
Bug introduced by
It seems like $temp_file['attachid'] can also be of type array; however, parameter $id of ElkArte\Errors\AttachmentErrorContext::addAttach() 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

180
			$attach_errors->addAttach(/** @scrutinizer ignore-type */ $temp_file['attachid'], $temp_file->getName());
Loading history...
181
			$log_these = ['attachments_no_create', 'attachments_no_write', 'attach_timeout',
182
						  'ran_out_of_space', 'cant_access_upload_path', 'attach_0_byte_file', 'bad_attachment'];
183
184
			foreach ($temp_file->getErrors() as $error)
185
			{
186
				$error = array_filter($error);
187
				$attach_errors->addError(isset($error[1]) ? $error : $error[0]);
188
				if (in_array($error[0], $log_these))
189
				{
190
					\ElkArte\Errors\Errors::instance()->log_error($temp_file->getName() . ': ' . $txt[$error[0]], 'critical');
191
192
					// For critical errors, we don't want the file or session data to persist
193
					$temp_file->remove(false);
194
				}
195
			}
196
		}
197
198
		// Want to correct for phone rotated photos, hell yeah ya do!
199
		if (!empty($modSettings['attachment_autorotate']))
200
		{
201
			$temp_file->autoRotate();
202
		}
203
204
		$tmp_attachments->addAttachment($temp_file);
205
	}
206
207
	// Mod authors, finally a hook to hang an alternate attachment upload system upon
208
	// Upload to the current attachment folder with the file name $attachID or 'post_tmp_' . User::$info->id . '_' . md5(mt_rand())
209
	// Populate TemporaryAttachmentsList[$attachID] with the following:
210
	//   name => The file name
211
	//   tmp_name => Path to the temp file (AttachmentsDirectory->getCurrent() . '/' . $attachID).
212
	//   size => File size (required).
213
	//   type => MIME type (optional if not available on upload).
214
	//   id_folder => AttachmentsDirectory->currentDirectoryId
215
	//   errors => An array of errors (use the index of the $txt variable for that error).
216
	// Template changes can be done using "integrate_upload_template".
217
	call_integration_hook('integrate_attachment_upload');
218
219
	return $ignore_temp;
220
}
221
222
/**
223
 * Checks if an uploaded file produced any appropriate error code
224
 *
225
 * What it does:
226
 *
227
 * - Checks for error codes in the error segment of the file array that is
228
 * created by PHP during the file upload.
229
 *
230 2
 * @param int $attachID
231
 *
232 2
 * @return array
233
 * @package Attachments
234
 */
235
function attachmentUploadChecks($attachID)
236
{
237
	global $modSettings, $txt;
238
239
	$errors = array();
240
241
	// Did PHP create any errors during the upload processing of this file?
242
	if (!empty($_FILES['attachment']['error'][$attachID]))
243
	{
244
		switch ($_FILES['attachment']['error'][$attachID])
245
		{
246
			case 1:
247
			case 2:
248
				// 1 The file exceeds the max_filesize directive in php.ini
249
				// 2 The uploaded file exceeds the MAX_FILE_SIZE directive in the HTML form.
250
				$errors[] = array('file_too_big', array($modSettings['attachmentSizeLimit']));
251
				break;
252
			case 3:
253
			case 4:
254
			case 8:
255
				// 3 partially uploaded
256
				// 4 no file uploaded
257
				// 8 upload blocked by extension
258
				\ElkArte\Errors\Errors::instance()->log_error($_FILES['attachment']['name'][$attachID] . ': ' . $txt['php_upload_error_' . $_FILES['attachment']['error'][$attachID]]);
259
				$errors[] = 'attach_php_error';
260
				break;
261
			case 6:
262
			case 7:
263
				// 6 Missing or a full a temp directory on the server
264
				// 7 Failed to write file
265
				\ElkArte\Errors\Errors::instance()->log_error($_FILES['attachment']['name'][$attachID] . ': ' . $txt['php_upload_error_' . $_FILES['attachment']['error'][$attachID]], 'critical');
266
				$errors[] = 'attach_php_error';
267
				break;
268
			default:
269
				\ElkArte\Errors\Errors::instance()->log_error($_FILES['attachment']['name'][$attachID] . ': ' . $txt['php_upload_error_' . $_FILES['attachment']['error'][$attachID]]);
270
				$errors[] = 'attach_php_error';
271
		}
272
	}
273
274
	return $errors;
275
}
276
277
/**
278
 * Create an attachment, with the given array of parameters.
279
 *
280
 * What it does:
281
 *
282
 * - Adds any additional or missing parameters to $attachmentOptions.
283
 * - Renames the temporary file.
284
 * - Creates a thumbnail if the file is an image and the option enabled.
285
 *
286
 * @param mixed[] $attachmentOptions associative array of options
287
 *
288
 * @return bool
289
 * @package Attachments
290
 */
291
function createAttachment(&$attachmentOptions)
292
{
293
	global $modSettings;
294
295
	$db = database();
296
	$attachmentsDir = new AttachmentsDirectory($modSettings, $db);
297
298
	$image = new Image($attachmentOptions['tmp_name']);
299
300
	// If this is an image we need to set a few additional parameters.
301
	$is_image = $image->isImageLoaded();
302
	$size = $is_image ? $image->getImageDimensions() : array(0, 0, 0);
303
	list ($attachmentOptions['width'], $attachmentOptions['height']) = $size;
304
	$attachmentOptions['width'] = max(0, $attachmentOptions['width']);
305
	$attachmentOptions['height'] = max(0, $attachmentOptions['height']);
306
307
	// If it's an image get the mime type right.
308
	if ($is_image)
309
	{
310
		$attachmentOptions['mime_type'] = getValidMimeImageType($size[2]);
311
312
		// Want to correct for phonetographer photos?
313
		if (!empty($modSettings['attachment_autorotate']))
314
		{
315
			$image->autoRotate();
316
		}
317
	}
318
319
	// Get the hash if no hash has been given yet.
320
	if (empty($attachmentOptions['file_hash']))
321
	{
322
		$attachmentOptions['file_hash'] = getAttachmentFilename($attachmentOptions['name'], 0, null, true);
323
	}
324
325
	// Assuming no-one set the extension let's take a look at it.
326
	if (empty($attachmentOptions['fileext']))
327
	{
328
		$attachmentOptions['fileext'] = strtolower(strrpos($attachmentOptions['name'], '.') !== false ? substr($attachmentOptions['name'], strrpos($attachmentOptions['name'], '.') + 1) : '');
329
		if (strlen($attachmentOptions['fileext']) > 8 || '.' . $attachmentOptions['fileext'] == $attachmentOptions['name'])
330
		{
331
			$attachmentOptions['fileext'] = '';
332
		}
333
	}
334
335
	$db->insert('',
336
		'{db_prefix}attachments',
337
		array(
338
			'id_folder' => 'int', 'id_msg' => 'int', 'filename' => 'string-255', 'file_hash' => 'string-40', 'fileext' => 'string-8',
339
			'size' => 'int', 'width' => 'int', 'height' => 'int',
340
			'mime_type' => 'string-20', 'approved' => 'int',
341
		),
342
		array(
343
			(int) $attachmentOptions['id_folder'], (int) $attachmentOptions['post'], $attachmentOptions['name'], $attachmentOptions['file_hash'], $attachmentOptions['fileext'],
344
			(int) $attachmentOptions['size'], (empty($attachmentOptions['width']) ? 0 : (int) $attachmentOptions['width']), (empty($attachmentOptions['height']) ? '0' : (int) $attachmentOptions['height']),
345
			(!empty($attachmentOptions['mime_type']) ? $attachmentOptions['mime_type'] : ''), (int) $attachmentOptions['approved'],
346
		),
347
		array('id_attach')
348
	);
349
	$attachmentOptions['id'] = $db->insert_id('{db_prefix}attachments');
350
351
	// @todo Add an error here maybe?
352
	if (empty($attachmentOptions['id']))
353
	{
354
		return false;
355
	}
356
357
	// Now that we have the attach id, let's rename this and finish up.
358
	$attachmentOptions['destination'] = getAttachmentFilename(basename($attachmentOptions['name']), $attachmentOptions['id'], $attachmentOptions['id_folder'], false, $attachmentOptions['file_hash']);
359
	rename($attachmentOptions['tmp_name'], $attachmentOptions['destination']);
360
361
	// If it's not approved then add to the approval queue.
362
	if (!$attachmentOptions['approved'])
363
	{
364
		$db->insert('',
365
			'{db_prefix}approval_queue',
366
			array(
367
				'id_attach' => 'int', 'id_msg' => 'int',
368
			),
369
			array(
370
				$attachmentOptions['id'], (int) $attachmentOptions['post'],
371
			),
372
			array()
373
		);
374
	}
375
376
	if (empty($modSettings['attachmentThumbnails']) || !$is_image || (empty($attachmentOptions['width']) && empty($attachmentOptions['height'])))
377
	{
378
		return true;
379
	}
380
381
	// Like thumbnails, do we?
382
	if (!empty($modSettings['attachmentThumbWidth']) && !empty($modSettings['attachmentThumbHeight'])
383
		&& ($attachmentOptions['width'] > $modSettings['attachmentThumbWidth'] || $attachmentOptions['height'] > $modSettings['attachmentThumbHeight']))
384
	{
385
		$thumb_filename = $attachmentOptions['name'] . '_thumb';
386
		$thumb_path = $attachmentOptions['destination'] . '_thumb';
387
		$thumb_image = $image->createThumbnail($modSettings['attachmentThumbWidth'], $modSettings['attachmentThumbHeight'], $thumb_path);
388
		if ($thumb_image !== false)
389
		{
390
			// Figure out how big we actually made it.
391
			$size = $thumb_image->getImageDimensions();
392
			list ($thumb_width, $thumb_height) = $size;
393
394
			$thumb_mime = getValidMimeImageType($size[2]);
395
			$thumb_size = $thumb_image->getFilesize();
396
			$thumb_file_hash = getAttachmentFilename($thumb_filename, 0, null, true);
397
398
			// We should check the file size and count here since thumbs are added to the existing totals.
399
			$attachmentsDir->checkDirSize($thumb_size);
400
			$current_dir_id = $attachmentsDir->currentDirectoryId();
401
402
			// If a new folder has been already created. Gotta move this thumb there then.
403
			if ($attachmentsDir->isCurrentDirectoryId($attachmentOptions['id_folder']) === false)
404
			{
405
				$current_dir = $attachmentsDir->getCurrent();
406
				$current_dir_id = $attachmentsDir->currentDirectoryId();
407
				rename($thumb_path, $current_dir . '/' . $thumb_filename);
408
				$thumb_path = $current_dir . '/' . $thumb_filename;
409
			}
410
411
			// To the database we go!
412
			$db->insert('',
413
				'{db_prefix}attachments',
414
				array(
415
					'id_folder' => 'int', 'id_msg' => 'int', 'attachment_type' => 'int', 'filename' => 'string-255', 'file_hash' => 'string-40', 'fileext' => 'string-8',
416
					'size' => 'int', 'width' => 'int', 'height' => 'int', 'mime_type' => 'string-20', 'approved' => 'int',
417
				),
418
				array(
419
					$current_dir_id, (int) $attachmentOptions['post'], 3, $thumb_filename, $thumb_file_hash, $attachmentOptions['fileext'],
420
					$thumb_size, $thumb_width, $thumb_height, $thumb_mime, (int) $attachmentOptions['approved'],
421
				),
422
				array('id_attach')
423
			);
424
			$attachmentOptions['thumb'] = $db->insert_id('{db_prefix}attachments');
425
426
			if (!empty($attachmentOptions['thumb']))
427
			{
428
				$db->query('', '
429
					UPDATE {db_prefix}attachments
430
					SET id_thumb = {int:id_thumb}
431
					WHERE id_attach = {int:id_attach}',
432
					array(
433
						'id_thumb' => $attachmentOptions['thumb'],
434
						'id_attach' => $attachmentOptions['id'],
435
					)
436
				);
437
438
				rename($thumb_path, getAttachmentFilename($thumb_filename, $attachmentOptions['thumb'], $current_dir_id, false, $thumb_file_hash));
439
			}
440
		}
441
	}
442
443
	return true;
444
}
445
446
/**
447
 * Returns if the given attachment ID is an image file or not
448
 *
449
 * What it does:
450
 *
451
 * - Given an attachment id, checks that it exists as an attachment
452
 * - Verifies the message its associated is on a board the user can see
453
 * - Sets 'is_image' if the attachment is an image file
454
 * - Returns basic attachment values
455
 *
456
 * @param int $id_attach
457
 *
458
 * @return array|bool
459
 * @package Attachments
460
 */
461
function isAttachmentImage($id_attach)
462
{
463
	$db = database();
464
465
	// Make sure this attachment is on this board.
466
	$attachmentData = array();
467
	$db->fetchQuery('
468
		SELECT
469
			a.filename, a.fileext, a.id_attach, a.attachment_type, a.mime_type, a.approved, 
470
			a.downloads, a.size, a.width, a.height, m.id_topic, m.id_board
471
		FROM {db_prefix}attachments as a
472
			INNER JOIN {db_prefix}messages AS m ON (m.id_msg = a.id_msg)
473
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board AND {query_see_board})
474
		WHERE id_attach = {int:attach}
475
			AND attachment_type = {int:type}
476
			AND a.approved = {int:approved}
477
		LIMIT 1',
478
		array(
479
			'attach' => $id_attach,
480
			'approved' => 1,
481
			'type' => 0,
482
		)
483
	)->fetch_callback(
484
		function ($row) use (&$attachmentData) {
485
			$attachmentData = $row;
486
			$attachmentData['is_image'] = substr($attachmentData['mime_type'], 0, 5) === 'image';
487
			$attachmentData['size'] = byte_format($attachmentData['size']);
488
		}
489
	);
490
491
	return !empty($attachmentData) ? $attachmentData : false;
492
}
493
494
/**
495
 * Saves a file and stores it locally for avatar use by id_member.
496
 *
497
 * What it does:
498
 *
499
 * - supports GIF, JPG, PNG, BMP and WBMP formats.
500
 * - uses createThumbnail() to resize to max_width by max_height, and saves the result to a file.
501
 * - updates the database info for the member's avatar.
502
 * - returns whether the download and resize was successful.
503
 *
504
 * @param string $temporary_path the full path to the temporary file
505
 * @param int $memID member ID
506
 * @param int $max_width
507
 * @param int $max_height
508
 * @return bool whether the download and resize was successful.
509
 * @package Attachments
510
 */
511
function saveAvatar($temporary_path, $memID, $max_width, $max_height)
512
{
513
	global $modSettings;
514
515
	$db = database();
516
517
	// Just making sure there is a non-zero member.
518
	if (empty($memID))
519
	{
520
		return false;
521
	}
522
523
	$tokenizer = new TokenHash();
524
	$ext = !empty($modSettings['avatar_download_png']) ? 'png' : 'jpeg';
525
	$destName = 'avatar_' . $memID . '_' . $tokenizer->generate_hash(16) . '.' . $ext;
526
527
	// Clear out any old attachment
528
	require_once(SUBSDIR . '/ManageAttachments.subs.php');
529
	removeAttachments(array('id_member' => $memID));
530
531
	$db->insert('',
532
		'{db_prefix}attachments',
533
		array(
534
			'id_member' => 'int', 'attachment_type' => 'int', 'filename' => 'string-255',
535
			'file_hash' => 'string-255', 'fileext' => 'string-8', 'size' => 'int', 'id_folder' => 'int',
536
		),
537
		array(
538
			$memID, 1, $destName, '', $ext, 1, 1,
539
		),
540
		array('id_attach')
541
	);
542
	$attachID = $db->insert_id('{db_prefix}attachments');
543
544
	// The destination filename depends on the custom dir for avatars
545
	$destName = $modSettings['custom_avatar_dir'] . '/' . $destName;
546
	$format = !empty($modSettings['avatar_download_png']) ? IMAGETYPE_PNG : IMAGETYPE_JPEG;
547
548
	// Resize and rotate it.
549
	$image = new Image($temporary_path);
550
	if (!$image->isImageLoaded())
551
	{
552
		return false;
553
	}
554
555
	if (!empty($modSettings['attachment_autorotate']))
556
	{
557
		$image->autoRotate();
558
	}
559
560
	$thumb_image = $image->createThumbnail($max_width, $max_height, $destName, $format);
561
	if ($thumb_image !== false)
562
	{
563
		list ($width, $height) = $thumb_image->getImageDimensions();
564
		$mime_type = $thumb_image->getMimeType();
565
566
		// Write filesize in the database.
567
		$db->query('', '
568
			UPDATE {db_prefix}attachments
569
			SET 
570
				size = {int:filesize}, width = {int:width}, height = {int:height}, 
571
				mime_type = {string:mime_type}
572
			WHERE id_attach = {int:current_attachment}',
573
			array(
574
				'filesize' => $thumb_image->getFilesize(),
575
				'width' => (int) $width,
576
				'height' => (int) $height,
577
				'current_attachment' => $attachID,
578
				'mime_type' => $mime_type,
579
			)
580
		);
581
582
		// Retain this globally in case the script wants it.
583
		$modSettings['new_avatar_data'] = array(
584
			'id' => $attachID,
585
			'filename' => $destName,
586
			'type' => 1,
587
		);
588
589
		return true;
590
	}
591
592
	// Having a problem with image manipulation
593
	$db->query('', '
594
		DELETE FROM {db_prefix}attachments
595
		WHERE id_attach = {int:current_attachment}',
596
		array(
597
			'current_attachment' => $attachID,
598
		)
599
	);
600
601
	return false;
602
}
603
604
/**
605
 * Get the size of a specified image with better error handling.
606
 *
607
 * What it does:
608
 *
609
 * - Uses getimagesizefromstring() to determine the dimensions of an image file.
610
 * - Attempts to connect to the server first, so it won't time out.
611
 * - Attempts to read a short byte range of the file, just enough to validate
612
 * the mime type.
613
 *
614
 * @param string $url
615
 * @return mixed[]|bool the image size as array(width, height), or false on failure
616
 * @package Attachments
617
 */
618
function url_image_size($url)
619
{
620
	// Can we pull this from the cache... please please?
621
	$temp = array();
622
	if (Cache::instance()->getVar($temp, 'url_image_size-' . md5($url), 240))
623
	{
624
		return $temp;
625
	}
626
627
	$url_path = parse_url($url, PHP_URL_PATH);
628
	$extension = pathinfo($url_path, PATHINFO_EXTENSION);
629
630
	switch ($extension)
631
	{
632
		case 'jpg':
633
		case 'jpeg':
634
			// Size location block is variable, so we fetch a meaningful chunk
635
			$range = 32768;
636
			break;
637
		case 'png':
638
			// Size will be in the first 24 bytes
639
			$range = 1024;
640
			break;
641
		case 'gif':
642
			// Size will be in the first 10 bytes
643
			$range = 1024;
644
			break;
645
		case 'bmp':
646
			// Size will be in the first 32 bytes
647
			$range = 1024;
648
			break;
649
		default:
650
			$range = 16384;
651
	}
652
653
	$image = new FsockFetchWebdata(array('max_length' => $range));
654
	$image->get_url_data($url);
655
656
	// The server may not understand Range: so lets try to fetch the entire thing
657
	// assuming we were not simply turned away
658
	if ($image->result('code') != 200 && $image->result('code') != 403)
659
	{
660
		$image = new FsockFetchWebdata(array());
661
		$image->get_url_data($url);
662
	}
663
664
	// Here is the data, getimagesizefromstring does not care if its a complete image, it only
665
	// searches for size headers in a given data set.
666
	$data = $image->result('body');
667
	$size = 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

667
	$size = getimagesizefromstring(/** @scrutinizer ignore-type */ $data);
Loading history...
668
669
	// Well, ok, umm, fail!
670
	if ($data === false || $size === false)
671
	{
672
		return array(-1, -1, -1);
673
	}
674
675
	Cache::instance()->put('url_image_size-' . md5($url), $size, 240);
676
677
	return $size;
678
}
679
680
/**
681
 * Get all attachments associated with a set of posts.
682
 *
683
 * What it does:
684
 *  - This does not check permissions.
685
 *
686
 * @param int[] $messages array of messages ids
687
 * @param bool $includeUnapproved = false
688
 * @param string|null $filter name of a callback function
689
 * @param mixed[] $all_posters
690
 *
691
 * @return array
692
 * @package Attachments
693
 */
694
function getAttachments($messages, $includeUnapproved = false, $filter = null, $all_posters = array())
695
{
696
	global $modSettings;
697
698
	$db = database();
699
700
	$attachments = array();
701
	$temp = array();
702
	$db->fetchQuery('
703
		SELECT
704
			a.id_attach, a.id_folder, a.id_msg, a.filename, a.file_hash, COALESCE(a.size, 0) AS filesize, a.downloads, a.approved,
705
			a.width, a.height' . (empty($modSettings['attachmentShowImages']) || empty($modSettings['attachmentThumbnails']) ? '' : ',
706
			COALESCE(thumb.id_attach, 0) AS id_thumb, thumb.width AS thumb_width, thumb.height AS thumb_height') . '
707
			FROM {db_prefix}attachments AS a' . (empty($modSettings['attachmentShowImages']) || empty($modSettings['attachmentThumbnails']) ? '' : '
708
			LEFT JOIN {db_prefix}attachments AS thumb ON (thumb.id_attach = a.id_thumb)') . '
709
		WHERE a.id_msg IN ({array_int:message_list})
710
			AND a.attachment_type = {int:attachment_type}',
711
		array(
712
			'message_list' => $messages,
713
			'attachment_type' => 0,
714
		)
715
	)->fetch_callback(
716
		function ($row) use ($includeUnapproved, $filter, $all_posters, &$attachments, &$temp) {
717
			if (!$row['approved'] && !$includeUnapproved
718
				&& (empty($filter) || !call_user_func($filter, $row, $all_posters)))
719
			{
720
				return;
721
			}
722
723
			$temp[$row['id_attach']] = $row;
724
725
			if (!isset($attachments[$row['id_msg']]))
726
			{
727
				$attachments[$row['id_msg']] = array();
728
			}
729
		}
730
	);
731
732
	// This is better than sorting it with the query...
733
	ksort($temp);
734
735
	foreach ($temp as $row)
736
	{
737
		$attachments[$row['id_msg']][] = $row;
738
	}
739
740
	return $attachments;
741
}
742
743
/**
744
 * Function to retrieve server-stored avatar files
745
 *
746
 * @param string $directory
747
 * @return array
748
 * @package Attachments
749
 */
750
function getServerStoredAvatars($directory)
751
{
752
	global $context, $txt, $modSettings;
753
754
	$result = [];
755
	$file_functions = FileFunctions::instance();
756
757
	// You can always have no avatar
758
	$result[] = array(
759
		'filename' => 'blank.png',
760
		'checked' => in_array($context['member']['avatar']['server_pic'], array('', 'blank.png')),
761
		'name' => $txt['no_pic'],
762
		'is_dir' => false
763
	);
764
765
	// Not valid is easy
766
	$avatarDir = $modSettings['avatar_directory'] . (!empty($directory) ? '/' : '') . $directory;
767
	if (!$file_functions->isDir($avatarDir))
768
	{
769
		return $result;
770
	}
771
772
	// Find all avatars under, and in, the avatar directory
773
	$serverAvatars = new RecursiveIteratorIterator(
774
		new RecursiveDirectoryIterator(
775
			$avatarDir,
776
			RecursiveDirectoryIterator::SKIP_DOTS
777
		),
778
		\RecursiveIteratorIterator::SELF_FIRST,
779
		\RecursiveIteratorIterator::CATCH_GET_CHILD
780
	);
781
	$key = 0;
782
	foreach ($serverAvatars as $entry)
783
	{
784
		if ($entry->isDir())
785
		{
786
			// Add a new directory
787
			$result[] = array(
788
				'filename' => htmlspecialchars(basename($entry), ENT_COMPAT, 'UTF-8'),
789
				'checked' => strpos($context['member']['avatar']['server_pic'], basename($entry) . '/') !== false,
790
				'name' => '[' . htmlspecialchars(str_replace('_', ' ', basename($entry)), ENT_COMPAT, 'UTF-8') . ']',
791
				'is_dir' => true,
792
				'files' => []
793
			);
794
			$key++;
795
796
			continue;
797
		}
798
799
		// Add the files under the current directory we are iterating on
800
		if (!in_array($entry->getFilename(), array('blank.png', 'index.php', '.htaccess')))
801
		{
802
			$extension = $entry->getExtension();
803
			$filename = $entry->getBasename('.' . $extension);
804
805
			// Make sure it is an image.
806
			if (empty(getValidMimeImageType($extension)))
807
			{
808
				continue;
809
			}
810
811
			$result[$key]['files'][] = [
812
				'filename' => htmlspecialchars($entry->getFilename(), ENT_COMPAT, 'UTF-8'),
813
				'checked' => $entry->getFilename() == $context['member']['avatar']['server_pic'],
814
				'name' => htmlspecialchars(str_replace('_', ' ', $filename), ENT_COMPAT, 'UTF-8'),
815
				'is_dir' => false
816
			];
817
818
			if (dirname($entry->getPath(), 1) === $modSettings['avatar_directory'])
819
			{
820
				$context['avatar_list'][] = str_replace($modSettings['avatar_directory'] . '/', '', $entry->getPathname());
821
			}
822
		}
823
	}
824
825
	return $result;
826
}
827
828
/**
829
 * Update an attachment's thumbnail
830
 *
831
 * @param string $filename the actual name of the file
832
 * @param int $id_attach the numeric attach id
833 2
 * @param int $id_msg the numeric message the attachment is associated with
834 2
 * @param int $old_id_thumb = 0 id of thumbnail to remove, such as from our post form
835
 * @param string $real_filename the fully qualified hash name of where the file is
836
 * @return array The updated information
837
 * @package Attachments
838
 */
839 2
function updateAttachmentThumbnail($filename, $id_attach, $id_msg, $old_id_thumb = 0, $real_filename = '')
840 2
{
841
	global $modSettings;
842 1
843
	$attachment = array('id_attach' => $id_attach);
844 2
845 2
	// Load our image functions, it will determine which graphics library to use
846
	$image = new Image($filename);
847 2
848 2
	// Image is not autorotated because it was at the time of upload (hopefully)
849 2
	$thumb_filename = (!empty($real_filename) ? $real_filename : $filename) . '_thumb';
850
	$thumb_image = $image->createThumbnail($modSettings['attachmentThumbWidth'], $modSettings['attachmentThumbHeight']);
851 2
852 2
	if ($thumb_image instanceof Image)
853 2
	{
854
		// So what folder are we putting this image in?
855 2
		$attachmentsDir = new AttachmentsDirectory($modSettings, database());
856 2
		$id_folder_thumb = $attachmentsDir->currentDirectoryId();
857
858
		// Calculate the size of the created thumbnail.
859
		$size = $thumb_image->getImageDimensions();
860
		list ($attachment['thumb_width'], $attachment['thumb_height']) = $size;
861
		$thumb_size = $thumb_image->getFilesize();
862
863
		// Figure out the mime type and other details
864
		$thumb_mime = getValidMimeImageType($size[2]);
865 2
		$thumb_ext = substr($thumb_mime, strpos($thumb_mime, '/') + 1);
866 2
		$thumb_hash = getAttachmentFilename($thumb_filename, 0, null, true);
867
868
		// Add this beauty to the database.
869
		$db = database();
870 2
		$db->insert('',
871
			'{db_prefix}attachments',
872
			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'),
873
			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),
874
			array('id_attach')
875
		);
876
877
		$attachment['id_thumb'] = $db->insert_id('{db_prefix}attachments');
878 2
		if (!empty($attachment['id_thumb']))
879 2
		{
880
			$db->query('', '
881
				UPDATE {db_prefix}attachments
882 2
				SET id_thumb = {int:id_thumb}
883
				WHERE id_attach = {int:id_attach}',
884
				array(
885
					'id_thumb' => $attachment['id_thumb'],
886
					'id_attach' => $attachment['id_attach'],
887 2
				)
888
			);
889 2
890
			$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

890
			$thumb_realname = getAttachmentFilename($thumb_filename, /** @scrutinizer ignore-type */ $attachment['id_thumb'], $id_folder_thumb, false, $thumb_hash);
Loading history...
891
			if (file_exists($filename . '_thumb'))
892
			{
893
				rename($filename . '_thumb', $thumb_realname);
894
			}
895
896
			// Do we need to remove an old thumbnail?
897
			if (!empty($old_id_thumb))
898
			{
899
				require_once(SUBSDIR . '/ManageAttachments.subs.php');
900
				removeAttachments(array('id_attach' => $old_id_thumb), '', false, false);
901
			}
902
		}
903
	}
904
905
	return $attachment;
906
}
907
908
/**
909
 * Compute and return the total size of attachments to a single message.
910
 *
911
 * @param int $id_msg
912
 * @param bool $include_count = true if true, it also returns the attachments count
913
 * @package Attachments
914
 * @return mixed
915
 */
916
function attachmentsSizeForMessage($id_msg, $include_count = true)
917
{
918
	$db = database();
919
920
	if ($include_count)
921
	{
922
		$request = $db->fetchQuery('
923
			SELECT 
924
				COUNT(*), SUM(size)
925
			FROM {db_prefix}attachments
926
			WHERE id_msg = {int:id_msg}
927
				AND attachment_type = {int:attachment_type}',
928
			array(
929
				'id_msg' => $id_msg,
930
				'attachment_type' => 0,
931
			)
932
		);
933
	}
934
	else
935
	{
936
		$request = $db->fetchQuery('
937
			SELECT 
938
				COUNT(*)
939
			FROM {db_prefix}attachments
940
			WHERE id_msg = {int:id_msg}
941
				AND attachment_type = {int:attachment_type}',
942
			array(
943
				'id_msg' => $id_msg,
944
				'attachment_type' => 0,
945
			)
946
		);
947
	}
948
949
	return $request->fetch_row();
950
}
951
952
/**
953 2
 * This loads an attachment's contextual data including, most importantly, its size if it is an image.
954
 *
955 2
 * What it does:
956
 *
957 2
 * - Pre-condition: $attachments array to have been filled with the proper attachment data, as Display() does.
958 2
 * - It requires the view_attachments permission to calculate image size.
959 2
 * - It attempts to keep the "aspect ratio" of the posted image in line, even if it has to be resized by
960
 * the max_image_width and max_image_height settings.
961
 *
962 2
 * @param int $id_msg message number to load attachments for
963 2
 * @return array of attachments
964 2
 * @todo change this pre-condition, too fragile and error-prone.
965 2
 *
966
 * @package Attachments
967
 */
968
function loadAttachmentContext($id_msg)
969 2
{
970 2
	global $attachments, $modSettings, $scripturl, $topic;
971
972 2
	// Set up the attachment info - based on code by Meriadoc.
973
	$attachmentData = array();
974
	$have_unapproved = false;
975
	if (isset($attachments[$id_msg]) && !empty($modSettings['attachmentEnable']))
976
	{
977
		foreach ($attachments[$id_msg] as $i => $attachment)
978
		{
979
			$attachmentData[$i] = array(
980
				'id' => $attachment['id_attach'],
981
				'name' => preg_replace('~&amp;#(\\d{1,7}|x[0-9a-fA-F]{1,6});~', '&#\\1;', htmlspecialchars($attachment['filename'], ENT_COMPAT, 'UTF-8')),
982
				'downloads' => $attachment['downloads'],
983
				'size' => byte_format($attachment['filesize']),
984
				'byte_size' => $attachment['filesize'],
985
				'href' => $scripturl . '?action=dlattach;topic=' . $topic . '.0;attach=' . $attachment['id_attach'],
986 2
				'link' => '<a href="' . $scripturl . '?action=dlattach;topic=' . $topic . '.0;attach=' . $attachment['id_attach'] . '">' . htmlspecialchars($attachment['filename'], ENT_COMPAT, 'UTF-8') . '</a>',
987
				'is_image' => !empty($attachment['width']) && !empty($attachment['height']) && !empty($modSettings['attachmentShowImages']),
988
				'is_approved' => $attachment['approved'],
989
				'file_hash' => $attachment['file_hash'],
990 2
			);
991
992 2
			// If something is unapproved we'll note it so we can sort them.
993
			if (!$attachment['approved'])
994
			{
995
				$have_unapproved = true;
996
			}
997 2
998
			if (!$attachmentData[$i]['is_image'])
999
			{
1000
				continue;
1001
			}
1002
1003
			$attachmentData[$i]['real_width'] = $attachment['width'];
1004
			$attachmentData[$i]['width'] = $attachment['width'];
1005
			$attachmentData[$i]['real_height'] = $attachment['height'];
1006
			$attachmentData[$i]['height'] = $attachment['height'];
1007
1008
			// Let's see, do we want thumbs?
1009
			if (!empty($modSettings['attachmentThumbnails']) && !empty($modSettings['attachmentThumbWidth']) && !empty($modSettings['attachmentThumbHeight']) && ($attachment['width'] > $modSettings['attachmentThumbWidth'] || $attachment['height'] > $modSettings['attachmentThumbHeight']) && strlen($attachment['filename']) < 249)
1010
			{
1011
				// A proper thumb doesn't exist yet? Create one! Or, it needs update.
1012
				if (empty($attachment['id_thumb']) || $attachment['thumb_width'] > $modSettings['attachmentThumbWidth'] || $attachment['thumb_height'] > $modSettings['attachmentThumbHeight'] || ($attachment['thumb_width'] < $modSettings['attachmentThumbWidth'] && $attachment['thumb_height'] < $modSettings['attachmentThumbHeight']))
1013
				{
1014
					$filename = getAttachmentFilename($attachment['filename'], $attachment['id_attach'], $attachment['id_folder'], false, $attachment['file_hash']);
1015
					$attachment = array_merge($attachment, updateAttachmentThumbnail($filename, $attachment['id_attach'], $id_msg, $attachment['id_thumb'], $attachment['filename']));
1016
				}
1017
1018
				// Only adjust dimensions on successful thumbnail creation.
1019
				if (!empty($attachment['thumb_width']) && !empty($attachment['thumb_height']))
1020
				{
1021
					$attachmentData[$i]['width'] = $attachment['thumb_width'];
1022
					$attachmentData[$i]['height'] = $attachment['thumb_height'];
1023
				}
1024
			}
1025
1026
			if (!empty($attachment['id_thumb']))
1027
			{
1028
				$attachmentData[$i]['thumbnail'] = array(
1029
					'id' => $attachment['id_thumb'],
1030
					'href' => getUrl('action', ['action' => 'dlattach', 'topic' => $topic . '.0', 'attach' => $attachment['id_thumb'], 'image']),
1031
				);
1032
			}
1033
			$attachmentData[$i]['thumbnail']['has_thumb'] = !empty($attachment['id_thumb']);
1034
1035
			// If thumbnails are disabled, check the maximum size of the image.
1036
			if (!$attachmentData[$i]['thumbnail']['has_thumb'] && ((!empty($modSettings['max_image_width']) && $attachment['width'] > $modSettings['max_image_width']) || (!empty($modSettings['max_image_height']) && $attachment['height'] > $modSettings['max_image_height'])))
1037
			{
1038
				if (!empty($modSettings['max_image_width']) && (empty($modSettings['max_image_height']) || $attachment['height'] * $modSettings['max_image_width'] / $attachment['width'] <= $modSettings['max_image_height']))
1039
				{
1040
					$attachmentData[$i]['width'] = $modSettings['max_image_width'];
1041
					$attachmentData[$i]['height'] = floor($attachment['height'] * $modSettings['max_image_width'] / $attachment['width']);
1042
				}
1043
				elseif (!empty($modSettings['max_image_width']))
1044
				{
1045
					$attachmentData[$i]['width'] = floor($attachment['width'] * $modSettings['max_image_height'] / $attachment['height']);
1046
					$attachmentData[$i]['height'] = $modSettings['max_image_height'];
1047
				}
1048
			}
1049
			elseif ($attachmentData[$i]['thumbnail']['has_thumb'])
1050
			{
1051
				// Data attributes for use in expandThumb
1052
				$attachmentData[$i]['thumbnail']['lightbox'] = 'data-lightboxmessage="' . $id_msg . '" data-lightboximage="' . $attachment['id_attach'] . '"';
1053
1054
				/*
1055
				// If the image is too large to show inline, make it a popup.
1056
				// @todo this needs to be removed or depreciated
1057
				if (((!empty($modSettings['max_image_width']) && $attachmentData[$i]['real_width'] > $modSettings['max_image_width']) || (!empty($modSettings['max_image_height']) && $attachmentData[$i]['real_height'] > $modSettings['max_image_height'])))
1058
				{
1059
					$attachmentData[$i]['thumbnail']['javascript'] = 'return reqWin(\'' . $attachmentData[$i]['href'] . ';image\', ' . ($attachment['width'] + 20) . ', ' . ($attachment['height'] + 20) . ', true);';
1060
				}
1061
				else
1062
				{
1063
					$attachmentData[$i]['thumbnail']['javascript'] = 'return expandThumb(' . $attachment['id_attach'] . ');';
1064
				}
1065
				*/
1066
			}
1067
1068
			if (!$attachmentData[$i]['thumbnail']['has_thumb'])
1069
			{
1070
				$attachmentData[$i]['downloads']++;
1071
			}
1072
		}
1073
	}
1074
1075
	// Do we need to instigate a sort?
1076
	if ($have_unapproved)
1077
	{
1078
		// Unapproved attachments go first.
1079
		usort($attachmentData, function($a, $b) {
1080
			if ($a['is_approved'] === $b['is_approved'])
1081
			{
1082
				return 0;
1083
			}
1084
1085
			return $a['is_approved'] > $b['is_approved'] ? -1 : 1;
1086
		});
1087
	}
1088
1089
	return $attachmentData;
1090
}
1091
1092
/**
1093
 * Older attachments may still use this function.
1094
 *
1095
 * @param string $filename
1096
 * @param int $attachment_id
1097
 * @param string|null $dir
1098
 * @param bool $new
1099
 *
1100
 * @return null|string|string[]
1101
 * @package Attachments
1102
 */
1103
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

1103
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...
1104
{
1105
	global $modSettings;
1106
1107
	$clean_name = $filename;
1108
1109
	// Sorry, no spaces, dots, or anything else but letters allowed.
1110
	$clean_name = preg_replace(array('/\s/', '/[^\w_\.\-]/'), array('_', ''), $clean_name);
1111
1112
	$enc_name = $attachment_id . '_' . strtr($clean_name, '.', '_') . md5($clean_name);
1113
	$clean_name = preg_replace('~\.[\.]+~', '.', $clean_name);
1114
1115
	if (empty($attachment_id) || ($new && empty($modSettings['attachmentEncryptFilenames'])))
1116
	{
1117
		return $clean_name;
1118
	}
1119
	elseif ($new)
1120
	{
1121
		return $enc_name;
1122
	}
1123
1124
	$attachmentsDir = new AttachmentsDirectory($modSettings, database());
1125
	$path = $attachmentsDir->getCurrent();
1126
1127
	$filename = file_exists($path . '/' . $enc_name) ? $path . '/' . $enc_name : $path . '/' . $clean_name;
1128
1129
	return $filename;
1130
}
1131
1132
/**
1133
 * Binds a set of attachments to a message.
1134
 *
1135
 * @param int $id_msg
1136
 * @param int[] $attachment_ids
1137
 * @package Attachments
1138
 */
1139
function bindMessageAttachments($id_msg, $attachment_ids)
1140
{
1141
	$db = database();
1142
1143
	$db->query('', '
1144
		UPDATE {db_prefix}attachments
1145
		SET id_msg = {int:id_msg}
1146
		WHERE id_attach IN ({array_int:attachment_list})',
1147
		array(
1148
			'attachment_list' => $attachment_ids,
1149
			'id_msg' => $id_msg,
1150
		)
1151
	);
1152
}
1153
1154
/**
1155
 * Get an attachment's encrypted filename. If $new is true, won't check for file existence.
1156
 *
1157
 * - If new is set returns a hash for the db
1158
 * - If no file hash is supplied, determines one and returns it
1159
 * - Returns the path to the file
1160
 *
1161
 * @param string $filename The name of the file
1162
 * @param int|null $attachment_id The ID of the attachment
1163
 * @param string|null $dir Which directory it should be in (null to use current)
1164
 * @param bool $new If this is a new attachment, if so just returns a hash
1165
 * @param string $file_hash The file hash
1166
 *
1167
 * @return string
1168
 * @todo this currently returns the hash if new, and the full filename otherwise.
1169
 * Something messy like that.
1170
 * @todo and of course everything relies on this behavior and work around it. :P.
1171
 * Converters included.
1172
 */
1173
function getAttachmentFilename($filename, $attachment_id, $dir = null, $new = false, $file_hash = '')
1174
{
1175
	global $modSettings;
1176
1177
	// Just make up a nice hash...
1178
	if ($new)
1179
	{
1180
		$tokenizer = new TokenHash();
1181
1182
		return $tokenizer->generate_hash(32);
1183
	}
1184
1185
	// In case of files from the old system, do a legacy call.
1186
	if (empty($file_hash))
1187
	{
1188
		return getLegacyAttachmentFilename($filename, $attachment_id, $dir, $new);
1189
	}
1190
1191
	// If we were passed the directory id, use it
1192
	$modSettings['currentAttachmentUploadDir'] = $dir;
1193
	$attachmentsDir = new AttachmentsDirectory($modSettings, database());
1194
	$path = $attachmentsDir->getCurrent();
1195
1196
	return $path . '/' . $attachment_id . '_' . $file_hash . '.elk';
1197
}
1198
1199
/**
1200
 * Returns the board and the topic the attachment belongs to.
1201
 *
1202
 * @param int $id_attach
1203
 * @return int[]|bool on fail else an array of id_board, id_topic
1204
 * @package Attachments
1205
 */
1206
function getAttachmentPosition($id_attach)
1207
{
1208
	$db = database();
1209
1210
	// Make sure this attachment is on this board.
1211
	$request = $db->fetchQuery('
1212
		SELECT 
1213
			m.id_board, m.id_topic
1214
		FROM {db_prefix}attachments AS a
1215
			LEFT JOIN {db_prefix}messages AS m ON (m.id_msg = a.id_msg)
1216
			LEFT JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board)
1217
		WHERE a.id_attach = {int:attach}
1218
			AND {query_see_board}
1219
		LIMIT 1',
1220
		array(
1221
			'attach' => $id_attach,
1222
		)
1223
	);
1224
1225 2
	$attachmentData = $request->fetch_all();
1226
1227
	if (empty($attachmentData))
1228 2
	{
1229 2
		return false;
1230 2
	}
1231
1232
	return $attachmentData[0];
1233
}
1234
1235
/**
1236
 * Simple wrapper for getimagesize
1237
 *
1238
 * @param string $file
1239
 * @param string|bool $error return array or false on error
1240
 *
1241
 * @return array|bool
1242
 */
1243
function elk_getimagesize($file, $error = 'array')
1244
{
1245
	$sizes = @getimagesize($file);
1246
1247
	// Can't get it, what shall we return
1248
	if (empty($sizes))
1249
	{
1250
		$sizes = $error === 'array' ? array(-1, -1, -1) : false;
1251
	}
1252
1253
	return $sizes;
1254
}
1255
1256
/**
1257
 * Checks if we have a known and support mime-type for which we have a thumbnail image
1258
 *
1259
 * @param string $file_ext
1260
 * @param bool $url
1261
 *
1262
 * @return bool|string
1263
 */
1264
function returnMimeThumb($file_ext, $url = false)
1265
{
1266
	global $settings;
1267
1268
	// These are not meant to be exhaustive, just some of the most common attached on a forum
1269
	$generics = array(
1270
		'arc' => array('tgz', 'zip', 'rar', '7z', 'gz'),
1271
		'doc' => array('doc', 'docx', 'wpd', 'odt'),
1272
		'sound' => array('wav', 'mp3', 'pcm', 'aiff', 'wma', 'm4a', 'flac'),
1273
		'video' => array('mp4', 'mgp', 'mpeg', 'mp4', 'wmv', 'mkv', 'flv', 'aiv', 'mov', 'swf'),
1274
		'txt' => array('rtf', 'txt', 'log'),
1275
		'presentation' => array('ppt', 'pps', 'odp'),
1276
		'spreadsheet' => array('xls', 'xlr', 'ods'),
1277
		'web' => array('html', 'htm')
1278
	);
1279
	foreach ($generics as $generic_extension => $generic_types)
1280
	{
1281
		if (in_array($file_ext, $generic_types))
1282
		{
1283
			$file_ext = $generic_extension;
1284
			break;
1285
		}
1286
	}
1287
1288
	static $distinct = array('arc', 'doc', 'sound', 'video', 'txt', 'presentation', 'spreadsheet', 'web',
1289
							 'c', 'cpp', 'css', 'csv', 'java', 'js', 'pdf', 'php', 'sql', 'xml');
1290
1291
	if (empty($settings))
1292
	{
1293
		ThemeLoader::loadEssentialThemeData();
1294
	}
1295
1296
	// Return the mine thumbnail if it exists or just the default
1297
	if (!in_array($file_ext, $distinct) || !file_exists($settings['theme_dir'] . '/images/mime_images/' . $file_ext . '.png'))
1298
	{
1299
		$file_ext = 'default';
1300
	}
1301
1302
	$location = $url ? $settings['theme_url'] : $settings['theme_dir'];
1303
1304
	return $location . '/images/mime_images/' . $file_ext . '.png';
1305
}
1306
1307
/**
1308
 * From either a mime type, an extension or an IMAGETYPE_* constant
1309
 * returns a valid image mime type
1310
 *
1311
 * @param string $mime
1312
 *
1313
 * @return string
1314
 */
1315
function getValidMimeImageType($mime)
1316
{
1317
	// These are the only valid image types.
1318
	$validImageTypes = array(
1319
		-1 => 'jpg',
1320
		// Starting from here are the IMAGETYPE_* constants
1321
		IMAGETYPE_GIF => 'gif',
1322
		IMAGETYPE_JPEG => 'jpeg',
1323
		IMAGETYPE_PNG => 'png',
1324
		IMAGETYPE_PSD => 'psd',
1325
		IMAGETYPE_BMP => 'bmp',
1326
		IMAGETYPE_TIFF_II => 'tiff',
1327
		IMAGETYPE_TIFF_MM => 'tiff',
1328
		IMAGETYPE_JPC => 'jpeg',
1329 2
		IMAGETYPE_IFF => 'iff',
1330
		IMAGETYPE_WBMP => 'bmp'
1331
	);
1332
1333
	$ext = (int) $mime > 0 && isset($validImageTypes[(int) $mime]) ? $validImageTypes[(int) $mime] : '';
1334
	if (empty($ext))
1335
	{
1336
		$ext = strtolower(trim(substr($mime, strpos($mime, '/')), '/'));
1337
	}
1338
1339
	return in_array($ext, $validImageTypes) ? 'image/' . $ext : '';
1340
}
1341
1342 2
/**
1343
 * This function returns the mimeType of a file using the best means available
1344
 *
1345
 * @param string $filename
1346
 * @return string
1347
 */
1348
function getMimeType($filename)
1349
{
1350
	$mimeType = '';
1351
1352
	// Check only existing readable files
1353
	if (!file_exists($filename) || !is_readable($filename))
1354
	{
1355
		return '';
1356
	}
1357
1358
	// Try finfo, this is the preferred way
1359
	if (function_exists('finfo_open'))
1360
	{
1361
		$finfo = finfo_open(FILEINFO_MIME);
1362
		$mimeType = finfo_file($finfo, $filename);
1363
		finfo_close($finfo);
1364
	}
1365
	// No finfo? What? lets try the old mime_content_type
1366
	elseif (function_exists('mime_content_type'))
1367
	{
1368
		$mimeType = mime_content_type($filename);
1369
	}
1370
	// Try using an exec call
1371
	elseif (function_exists('exec'))
1372
	{
1373
		$mimeType = @exec("/usr/bin/file -i -b $filename");
1374
	}
1375
1376
	// Still nothing? We should at least be able to get images correct
1377
	if (empty($mimeType))
1378
	{
1379
		$imageData = elk_getimagesize($filename, 'none');
1380
		if (!empty($imageData['mime']))
1381
		{
1382
			$mimeType = $imageData['mime'];
1383
		}
1384
	}
1385
1386
	// Account for long responses like text/plain; charset=us-ascii
1387
	if (!empty($mimeType) && strpos($mimeType, ';'))
1388
	{
1389
		list($mimeType,) = explode(';', $mimeType);
1390
	}
1391
1392
	return $mimeType;
1393
}
1394