Issues (1065)

Sources/Subs-Attachments.php (6 issues)

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
 *
7
 * Simple Machines Forum (SMF)
8
 *
9
 * @package SMF
10
 * @author Simple Machines https://www.simplemachines.org
11
 * @copyright 2025 Simple Machines and individual contributors
12
 * @license https://www.simplemachines.org/about/smf/license.php BSD
13
 *
14
 * @version 2.1.5
15
 */
16
17
if (!defined('SMF'))
18
	die('No direct access...');
19
20
/**
21
 * Check if the current directory is still valid or not.
22
 * If not creates the new directory
23
 *
24
 * @return void|bool False if any error occurred
25
 */
26
function automanage_attachments_check_directory()
27
{
28
	global $smcFunc, $boarddir, $modSettings, $context;
29
30
	// Not pretty, but since we don't want folders created for every post. It'll do unless a better solution can be found.
31
	if (isset($_REQUEST['action']) && $_REQUEST['action'] == 'admin')
32
		$doit = true;
33
	elseif (empty($modSettings['automanage_attachments']))
34
		return;
35
	elseif (!isset($_FILES))
36
		return;
37
	elseif (isset($_FILES['attachment']))
38
		foreach ($_FILES['attachment']['tmp_name'] as $dummy)
39
			if (!empty($dummy))
40
			{
41
				$doit = true;
42
				break;
43
			}
44
45
	if (!isset($doit))
46
		return;
47
48
	$year = date('Y');
49
	$month = date('m');
50
51
	$rand = md5(mt_rand());
52
	$rand1 = $rand[1];
53
	$rand = $rand[0];
54
55
	if (!empty($modSettings['attachment_basedirectories']) && !empty($modSettings['use_subdirectories_for_attachments']))
56
	{
57
		if (!is_array($modSettings['attachment_basedirectories']))
58
			$modSettings['attachment_basedirectories'] = $smcFunc['json_decode']($modSettings['attachment_basedirectories'], true);
59
		$base_dir = array_search($modSettings['basedirectory_for_attachments'], $modSettings['attachment_basedirectories']);
60
	}
61
	else
62
		$base_dir = 0;
63
64
	if ($modSettings['automanage_attachments'] == 1)
65
	{
66
		if (!isset($modSettings['last_attachments_directory']))
67
			$modSettings['last_attachments_directory'] = array();
68
		if (!is_array($modSettings['last_attachments_directory']))
69
			$modSettings['last_attachments_directory'] = $smcFunc['json_decode']($modSettings['last_attachments_directory'], true);
70
		if (!isset($modSettings['last_attachments_directory'][$base_dir]))
71
			$modSettings['last_attachments_directory'][$base_dir] = 0;
72
	}
73
74
	$basedirectory = (!empty($modSettings['use_subdirectories_for_attachments']) ? ($modSettings['basedirectory_for_attachments']) : $boarddir);
75
	//Just to be sure: I don't want directory separators at the end
76
	$sep = (DIRECTORY_SEPARATOR === '\\') ? '\/' : DIRECTORY_SEPARATOR;
77
	$basedirectory = rtrim($basedirectory, $sep);
78
79
	switch ($modSettings['automanage_attachments'])
80
	{
81
		case 1:
82
			$updir = $basedirectory . DIRECTORY_SEPARATOR . 'attachments_' . (isset($modSettings['last_attachments_directory'][$base_dir]) ? $modSettings['last_attachments_directory'][$base_dir] : 0);
83
			break;
84
		case 2:
85
			$updir = $basedirectory . DIRECTORY_SEPARATOR . $year;
86
			break;
87
		case 3:
88
			$updir = $basedirectory . DIRECTORY_SEPARATOR . $year . DIRECTORY_SEPARATOR . $month;
89
			break;
90
		case 4:
91
			$updir = $basedirectory . DIRECTORY_SEPARATOR . (empty($modSettings['use_subdirectories_for_attachments']) ? 'attachments-' : 'random_') . $rand;
92
			break;
93
		case 5:
94
			$updir = $basedirectory . DIRECTORY_SEPARATOR . (empty($modSettings['use_subdirectories_for_attachments']) ? 'attachments-' : 'random_') . $rand . DIRECTORY_SEPARATOR . $rand1;
95
			break;
96
		default :
97
			$updir = '';
98
	}
99
100
	if (!is_array($modSettings['attachmentUploadDir']))
101
		$modSettings['attachmentUploadDir'] = $smcFunc['json_decode']($modSettings['attachmentUploadDir'], true);
102
	if (!in_array($updir, $modSettings['attachmentUploadDir']) && !empty($updir))
103
		$outputCreation = automanage_attachments_create_directory($updir);
104
	elseif (in_array($updir, $modSettings['attachmentUploadDir']))
105
		$outputCreation = true;
106
107
	if ($outputCreation)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $outputCreation does not seem to be defined for all execution paths leading up to this point.
Loading history...
108
	{
109
		$modSettings['currentAttachmentUploadDir'] = array_search($updir, $modSettings['attachmentUploadDir']);
110
		$context['attach_dir'] = $modSettings['attachmentUploadDir'][$modSettings['currentAttachmentUploadDir']];
111
112
		updateSettings(array(
113
			'currentAttachmentUploadDir' => $modSettings['currentAttachmentUploadDir'],
114
		));
115
	}
116
117
	return $outputCreation;
118
}
119
120
/**
121
 * Creates a directory
122
 *
123
 * @param string $updir The directory to be created
124
 *
125
 * @return bool False on errors
126
 */
127
function automanage_attachments_create_directory($updir)
128
{
129
	global $smcFunc, $modSettings, $context, $boarddir;
130
131
	$tree = get_directory_tree_elements($updir);
132
	$count = count($tree);
133
134
	$directory = attachments_init_dir($tree, $count);
135
	if ($directory === false)
136
	{
137
		// Maybe it's just the folder name
138
		$tree = get_directory_tree_elements($boarddir . DIRECTORY_SEPARATOR . $updir);
139
		$count = count($tree);
140
141
		$directory = attachments_init_dir($tree, $count);
142
		if ($directory === false)
143
			return false;
144
	}
145
146
	$directory .= DIRECTORY_SEPARATOR . array_shift($tree);
147
148
	while ($count != -1)
149
	{
150
		if (is_path_allowed($directory) && !@is_dir($directory))
151
		{
152
			if (!@mkdir($directory, 0755))
153
			{
154
				$context['dir_creation_error'] = 'attachments_no_create';
155
				return false;
156
			}
157
		}
158
159
		$directory .= DIRECTORY_SEPARATOR . array_shift($tree);
160
		$count--;
161
	}
162
163
	// Check if the dir is writable.
164
	if (!smf_chmod($directory))
165
	{
166
		$context['dir_creation_error'] = 'attachments_no_write';
167
		return false;
168
	}
169
170
	// Everything seems fine...let's create the .htaccess
171
	if (!file_exists($directory . DIRECTORY_SEPARATOR . '.htaccess'))
172
		secureDirectory($updir, true);
173
174
	$sep = (DIRECTORY_SEPARATOR === '\\') ? '\/' : DIRECTORY_SEPARATOR;
175
	$updir = rtrim($updir, $sep);
176
177
	// Only update if it's a new directory
178
	if (!in_array($updir, $modSettings['attachmentUploadDir']))
179
	{
180
		$modSettings['currentAttachmentUploadDir'] = max(array_keys($modSettings['attachmentUploadDir'])) + 1;
181
		$modSettings['attachmentUploadDir'][$modSettings['currentAttachmentUploadDir']] = $updir;
182
183
		updateSettings(array(
184
			'attachmentUploadDir' => $smcFunc['json_encode']($modSettings['attachmentUploadDir']),
185
			'currentAttachmentUploadDir' => $modSettings['currentAttachmentUploadDir'],
186
		), true);
187
		$modSettings['attachmentUploadDir'] = $smcFunc['json_decode']($modSettings['attachmentUploadDir'], true);
188
	}
189
190
	$context['attach_dir'] = $modSettings['attachmentUploadDir'][$modSettings['currentAttachmentUploadDir']];
191
	return true;
192
}
193
194
/**
195
 * Check if open_basedir restrictions are in effect.
196
 * If so check if the path is allowed.
197
 *
198
 * @param string $path The path to check
199
 *
200
 * @return bool True if the path is allowed, false otherwise.
201
 */
202
function is_path_allowed($path)
203
{
204
	$open_basedir = ini_get('open_basedir');
205
206
	if (empty($open_basedir))
207
		return true;
208
209
	$restricted_paths = explode(PATH_SEPARATOR, $open_basedir);
210
211
	foreach ($restricted_paths as $restricted_path)
212
	{
213
		if (mb_strpos($path, $restricted_path) === 0)
214
			return true;
215
	}
216
217
	return false;
218
}
219
220
/**
221
 * Called when a directory space limit is reached.
222
 * Creates a new directory and increments the directory suffix number.
223
 *
224
 * @return void|bool False on errors, true if successful, nothing if auto-management of attachments is disabled
225
 */
226
function automanage_attachments_by_space()
227
{
228
	global $smcFunc, $modSettings, $boarddir;
229
230
	if (!isset($modSettings['automanage_attachments']) || (!empty($modSettings['automanage_attachments']) && $modSettings['automanage_attachments'] != 1))
231
		return;
232
233
	$basedirectory = !empty($modSettings['use_subdirectories_for_attachments']) ? $modSettings['basedirectory_for_attachments'] : $boarddir;
234
	// Just to be sure: I don't want directory separators at the end
235
	$sep = (DIRECTORY_SEPARATOR === '\\') ? '\/' : DIRECTORY_SEPARATOR;
236
	$basedirectory = rtrim($basedirectory, $sep);
237
238
	// Get the current base directory
239
	if (!empty($modSettings['use_subdirectories_for_attachments']) && !empty($modSettings['attachment_basedirectories']))
240
	{
241
		$base_dir = array_search($modSettings['basedirectory_for_attachments'], $modSettings['attachment_basedirectories']);
242
		$base_dir = !empty($modSettings['automanage_attachments']) ? $base_dir : 0;
243
	}
244
	else
245
		$base_dir = 0;
246
247
	// Get the last attachment directory for that base directory
248
	if (empty($modSettings['last_attachments_directory'][$base_dir]))
249
		$modSettings['last_attachments_directory'][$base_dir] = 0;
250
	// And increment it.
251
	$modSettings['last_attachments_directory'][$base_dir]++;
252
253
	$updir = $basedirectory . DIRECTORY_SEPARATOR . 'attachments_' . $modSettings['last_attachments_directory'][$base_dir];
254
	if (automanage_attachments_create_directory($updir))
255
	{
256
		$modSettings['currentAttachmentUploadDir'] = array_search($updir, $modSettings['attachmentUploadDir']);
257
		updateSettings(array(
258
			'last_attachments_directory' => $smcFunc['json_encode']($modSettings['last_attachments_directory']),
259
			'currentAttachmentUploadDir' => $modSettings['currentAttachmentUploadDir'],
260
		));
261
		$modSettings['last_attachments_directory'] = $smcFunc['json_decode']($modSettings['last_attachments_directory'], true);
262
263
		return true;
264
	}
265
	else
266
		return false;
267
}
268
269
/**
270
 * Split a path into a list of all directories and subdirectories
271
 *
272
 * @param string $directory A path
273
 *
274
 * @return array|bool An array of all the directories and subdirectories or false on failure
275
 */
276
function get_directory_tree_elements($directory)
277
{
278
	/*
279
		In Windows server both \ and / can be used as directory separators in paths
280
		In Linux (and presumably *nix) servers \ can be part of the name
281
		So for this reasons:
282
			* in Windows we need to explode for both \ and /
283
			* while in linux should be safe to explode only for / (aka DIRECTORY_SEPARATOR)
284
	*/
285
	if (DIRECTORY_SEPARATOR === '\\')
286
		$tree = preg_split('#[\\\/]#', $directory);
287
	else
288
	{
289
		if (substr($directory, 0, 1) != DIRECTORY_SEPARATOR)
290
			return false;
291
292
		$tree = explode(DIRECTORY_SEPARATOR, trim($directory, DIRECTORY_SEPARATOR));
293
	}
294
	return $tree;
295
}
296
297
/**
298
 * Return the first part of a path (i.e. c:\ or / + the first directory), used by automanage_attachments_create_directory
299
 *
300
 * @param array $tree An array
301
 * @param int $count The number of elements in $tree
302
 *
303
 * @return string|bool The first part of the path or false on error
304
 */
305
function attachments_init_dir(&$tree, &$count)
306
{
307
	$directory = '';
308
	// If on Windows servers the first part of the path is the drive (e.g. "C:")
309
	if (DIRECTORY_SEPARATOR === '\\')
310
	{
311
		//Better be sure that the first part of the path is actually a drive letter...
312
		//...even if, I should check this in the admin page...isn't it?
313
		//...NHAAA Let's leave space for users' complains! :P
314
		if (preg_match('/^[a-z]:$/i', $tree[0]))
315
			$directory = array_shift($tree);
316
		else
317
			return false;
318
319
		$count--;
320
	}
321
	return $directory;
322
}
323
324
/**
325
 * Moves an attachment to the proper directory and set the relevant data into $_SESSION['temp_attachments']
326
 */
327
function processAttachments()
328
{
329
	global $context, $modSettings, $smcFunc, $txt, $user_info;
330
331
	// Make sure we're uploading to the right place.
332
	if (!empty($modSettings['automanage_attachments']))
333
		automanage_attachments_check_directory();
334
335
	if (!is_array($modSettings['attachmentUploadDir']))
336
		$modSettings['attachmentUploadDir'] = $smcFunc['json_decode']($modSettings['attachmentUploadDir'], true);
337
338
	$context['attach_dir'] = $modSettings['attachmentUploadDir'][$modSettings['currentAttachmentUploadDir']];
339
340
	// Is the attachments folder actualy there?
341
	if (!empty($context['dir_creation_error']))
342
		$initial_error = $context['dir_creation_error'];
343
	elseif (!is_dir($context['attach_dir']))
344
	{
345
		$initial_error = 'attach_folder_warning';
346
		log_error(sprintf($txt['attach_folder_admin_warning'], $context['attach_dir']), 'critical');
347
	}
348
349
	if (!isset($initial_error) && !isset($context['attachments']))
350
	{
351
		// If this isn't a new post, check the current attachments.
352
		if (isset($_REQUEST['msg']))
353
		{
354
			$request = $smcFunc['db_query']('', '
355
				SELECT COUNT(*), SUM(size)
356
				FROM {db_prefix}attachments
357
				WHERE id_msg = {int:id_msg}
358
					AND attachment_type = {int:attachment_type}',
359
				array(
360
					'id_msg' => (int) $_REQUEST['msg'],
361
					'attachment_type' => 0,
362
				)
363
			);
364
			list ($context['attachments']['quantity'], $context['attachments']['total_size']) = $smcFunc['db_fetch_row']($request);
365
			$smcFunc['db_free_result']($request);
366
		}
367
		else
368
			$context['attachments'] = array(
369
				'quantity' => 0,
370
				'total_size' => 0,
371
			);
372
	}
373
374
	// Hmm. There are still files in session.
375
	$ignore_temp = false;
376
	if (!empty($_SESSION['temp_attachments']['post']['files']) && count($_SESSION['temp_attachments']) > 1)
377
	{
378
		// Let's try to keep them. But...
379
		$ignore_temp = true;
380
		// If new files are being added. We can't ignore those
381
		foreach ($_FILES['attachment']['tmp_name'] as $dummy)
382
			if (!empty($dummy))
383
			{
384
				$ignore_temp = false;
385
				break;
386
			}
387
388
		// Need to make space for the new files. So, bye bye.
389
		if (!$ignore_temp)
390
		{
391
			foreach ($_SESSION['temp_attachments'] as $attachID => $attachment)
392
				if (strpos($attachID, 'post_tmp_' . $user_info['id']) !== false)
393
					unlink($attachment['tmp_name']);
394
395
			$context['we_are_history'] = $txt['error_temp_attachments_flushed'];
396
			$_SESSION['temp_attachments'] = array();
397
		}
398
	}
399
400
	if (!isset($_FILES['attachment']['name']))
401
		$_FILES['attachment']['tmp_name'] = array();
402
403
	if (!isset($_SESSION['temp_attachments']))
404
		$_SESSION['temp_attachments'] = array();
405
406
	// Remember where we are at. If it's anywhere at all.
407
	if (!$ignore_temp)
408
		$_SESSION['temp_attachments']['post'] = array(
409
			'msg' => !empty($_REQUEST['msg']) ? $_REQUEST['msg'] : 0,
410
			'last_msg' => !empty($_REQUEST['last_msg']) ? $_REQUEST['last_msg'] : 0,
411
			'topic' => !empty($topic) ? $topic : 0,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $topic seems to never exist and therefore empty should always be true.
Loading history...
412
			'board' => !empty($board) ? $board : 0,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $board seems to never exist and therefore empty should always be true.
Loading history...
413
		);
414
415
	// If we have an initial error, lets just display it.
416
	if (!empty($initial_error))
417
	{
418
		$_SESSION['temp_attachments']['initial_error'] = $initial_error;
419
420
		// And delete the files 'cos they ain't going nowhere.
421
		foreach ($_FILES['attachment']['tmp_name'] as $n => $dummy)
422
			if (file_exists($_FILES['attachment']['tmp_name'][$n]))
423
				unlink($_FILES['attachment']['tmp_name'][$n]);
424
425
		$_FILES['attachment']['tmp_name'] = array();
426
	}
427
428
	// Loop through $_FILES['attachment'] array and move each file to the current attachments folder.
429
	foreach ($_FILES['attachment']['tmp_name'] as $n => $dummy)
430
	{
431
		if ($_FILES['attachment']['name'][$n] == '')
432
			continue;
433
434
		// First, let's first check for PHP upload errors.
435
		$errors = array();
436
		if (!empty($_FILES['attachment']['error'][$n]))
437
		{
438
			if ($_FILES['attachment']['error'][$n] == 2)
439
				$errors[] = array('file_too_big', array($modSettings['attachmentSizeLimit']));
440
			elseif ($_FILES['attachment']['error'][$n] == 6)
441
				log_error($_FILES['attachment']['name'][$n] . ': ' . $txt['php_upload_error_6'], 'critical');
442
			else
443
				log_error($_FILES['attachment']['name'][$n] . ': ' . $txt['php_upload_error_' . $_FILES['attachment']['error'][$n]]);
444
			if (empty($errors))
445
				$errors[] = 'attach_php_error';
446
		}
447
448
		// Try to move and rename the file before doing any more checks on it.
449
		$attachID = 'post_tmp_' . $user_info['id'] . '_' . md5(mt_rand());
450
		$destName = $context['attach_dir'] . '/' . $attachID;
451
		if (empty($errors))
452
		{
453
			// The reported MIME type of the attachment might not be reliable.
454
			$detected_mime_type = get_mime_type($_FILES['attachment']['tmp_name'][$n], true);
455
			if ($detected_mime_type !== false)
456
				$_FILES['attachment']['type'][$n] = $detected_mime_type;
457
458
			$_SESSION['temp_attachments'][$attachID] = array(
459
				'name' => $smcFunc['htmlspecialchars'](basename($_FILES['attachment']['name'][$n])),
460
				'tmp_name' => $destName,
461
				'size' => $_FILES['attachment']['size'][$n],
462
				'type' => $_FILES['attachment']['type'][$n],
463
				'id_folder' => $modSettings['currentAttachmentUploadDir'],
464
				'errors' => array(),
465
			);
466
467
			// Move the file to the attachments folder with a temp name for now.
468
			if (@move_uploaded_file($_FILES['attachment']['tmp_name'][$n], $destName))
469
				smf_chmod($destName, 0644);
470
			else
471
			{
472
				$_SESSION['temp_attachments'][$attachID]['errors'][] = 'attach_timeout';
473
				if (file_exists($_FILES['attachment']['tmp_name'][$n]))
474
					unlink($_FILES['attachment']['tmp_name'][$n]);
475
			}
476
		}
477
		else
478
		{
479
			$_SESSION['temp_attachments'][$attachID] = array(
480
				'name' => $smcFunc['htmlspecialchars'](basename($_FILES['attachment']['name'][$n])),
481
				'tmp_name' => $destName,
482
				'errors' => $errors,
483
			);
484
485
			if (file_exists($_FILES['attachment']['tmp_name'][$n]))
486
				unlink($_FILES['attachment']['tmp_name'][$n]);
487
		}
488
		// If there's no errors to this point. We still do need to apply some additional checks before we are finished.
489
		if (empty($_SESSION['temp_attachments'][$attachID]['errors']))
490
			attachmentChecks($attachID);
491
	}
492
	// Mod authors, finally a hook to hang an alternate attachment upload system upon
493
	// Upload to the current attachment folder with the file name $attachID or 'post_tmp_' . $user_info['id'] . '_' . md5(mt_rand())
494
	// Populate $_SESSION['temp_attachments'][$attachID] with the following:
495
	//   name => The file name
496
	//   tmp_name => Path to the temp file ($context['attach_dir'] . '/' . $attachID).
497
	//   size => File size (required).
498
	//   type => MIME type (optional if not available on upload).
499
	//   id_folder => $modSettings['currentAttachmentUploadDir']
500
	//   errors => An array of errors (use the index of the $txt variable for that error).
501
	// Template changes can be done using "integrate_upload_template".
502
	call_integration_hook('integrate_attachment_upload', array());
503
}
504
505
/**
506
 * Performs various checks on an uploaded file.
507
 * - Requires that $_SESSION['temp_attachments'][$attachID] be properly populated.
508
 *
509
 * @param int $attachID The ID of the attachment
510
 * @return bool Whether the attachment is OK
511
 */
512
function attachmentChecks($attachID)
513
{
514
	global $modSettings, $context, $sourcedir, $smcFunc;
515
516
	// No data or missing data .... Not necessarily needed, but in case a mod author missed something.
517
	if (empty($_SESSION['temp_attachments'][$attachID]))
518
		$error = '$_SESSION[\'temp_attachments\'][$attachID]';
519
520
	elseif (empty($attachID))
521
		$error = '$attachID';
522
523
	elseif (empty($context['attachments']))
524
		$error = '$context[\'attachments\']';
525
526
	elseif (empty($context['attach_dir']))
527
		$error = '$context[\'attach_dir\']';
528
529
	// Let's get their attention.
530
	if (!empty($error))
531
		fatal_lang_error('attach_check_nag', 'debug', array($error));
532
533
	// Just in case this slipped by the first checks, we stop it here and now
534
	if ($_SESSION['temp_attachments'][$attachID]['size'] == 0)
535
	{
536
		$_SESSION['temp_attachments'][$attachID]['errors'][] = 'attach_0_byte_file';
537
		return false;
538
	}
539
540
	// First, the dreaded security check. Sorry folks, but this shouldn't be avoided.
541
	$size = @getimagesize($_SESSION['temp_attachments'][$attachID]['tmp_name']);
542
	if (is_array($size) && isset($size[2], $context['valid_image_types'][$size[2]]))
543
	{
544
		require_once($sourcedir . '/Subs-Graphics.php');
545
		if (!checkImageContents($_SESSION['temp_attachments'][$attachID]['tmp_name'], !empty($modSettings['attachment_image_paranoid'])))
546
		{
547
			// It's bad. Last chance, maybe we can re-encode it?
548
			if (empty($modSettings['attachment_image_reencode']) || (!reencodeImage($_SESSION['temp_attachments'][$attachID]['tmp_name'], $size[2])))
549
			{
550
				// Nothing to do: not allowed or not successful re-encoding it.
551
				$_SESSION['temp_attachments'][$attachID]['errors'][] = 'bad_attachment';
552
				return false;
553
			}
554
			// Success! However, successes usually come for a price:
555
			// we might get a new format for our image...
556
			$old_format = $size[2];
557
			$size = @getimagesize($_SESSION['temp_attachments'][$attachID]['tmp_name']);
558
			if (!(empty($size)) && ($size[2] != $old_format))
559
				$_SESSION['temp_attachments'][$attachID]['type'] = 'image/' . $context['valid_image_types'][$size[2]];
560
		}
561
	}
562
	// SVGs have their own set of security checks.
563
	elseif ($_SESSION['temp_attachments'][$attachID]['type'] === 'image/svg+xml')
564
	{
565
		require_once($sourcedir . '/Subs-Graphics.php');
566
		if (!checkSvgContents($_SESSION['temp_attachments'][$attachID]['tmp_name']))
567
		{
568
			$_SESSION['temp_attachments'][$attachID]['errors'][] = 'bad_attachment';
569
			return false;
570
		}
571
	}
572
573
	// Is there room for this sucker?
574
	if (!empty($modSettings['attachmentDirSizeLimit']) || !empty($modSettings['attachmentDirFileLimit']))
575
	{
576
		// Check the folder size and count. If it hasn't been done already.
577
		if (empty($context['dir_size']) || empty($context['dir_files']))
578
		{
579
			$request = $smcFunc['db_query']('', '
580
				SELECT COUNT(*), SUM(size)
581
				FROM {db_prefix}attachments
582
				WHERE id_folder = {int:folder_id}
583
					AND attachment_type != {int:type}',
584
				array(
585
					'folder_id' => $modSettings['currentAttachmentUploadDir'],
586
					'type' => 1,
587
				)
588
			);
589
			list ($context['dir_files'], $context['dir_size']) = $smcFunc['db_fetch_row']($request);
590
			$smcFunc['db_free_result']($request);
591
		}
592
		$context['dir_size'] += $_SESSION['temp_attachments'][$attachID]['size'];
593
		$context['dir_files']++;
594
595
		// Are we about to run out of room? Let's notify the admin then.
596
		if (empty($modSettings['attachment_full_notified']) && !empty($modSettings['attachmentDirSizeLimit']) && $modSettings['attachmentDirSizeLimit'] > 4000 && $context['dir_size'] > ($modSettings['attachmentDirSizeLimit'] - 2000) * 1024
0 ignored issues
show
Consider adding parentheses for clarity. Current Interpretation: (empty($modSettings['att...entDirFileLimit'] > 500, Probably Intended Meaning: empty($modSettings['atta...ntDirFileLimit'] > 500)
Loading history...
597
			|| (!empty($modSettings['attachmentDirFileLimit']) && $modSettings['attachmentDirFileLimit'] * .95 < $context['dir_files'] && $modSettings['attachmentDirFileLimit'] > 500))
598
		{
599
			require_once($sourcedir . '/Subs-Admin.php');
600
			emailAdmins('admin_attachments_full');
601
			updateSettings(array('attachment_full_notified' => 1));
602
		}
603
604
		// // No room left.... What to do now???
605
		if (!empty($modSettings['attachmentDirFileLimit']) && $context['dir_files'] > $modSettings['attachmentDirFileLimit']
606
			|| (!empty($modSettings['attachmentDirSizeLimit']) && $context['dir_size'] > $modSettings['attachmentDirSizeLimit'] * 1024))
607
		{
608
			if (!empty($modSettings['automanage_attachments']) && $modSettings['automanage_attachments'] == 1)
609
			{
610
				// Move it to the new folder if we can.
611
				if (automanage_attachments_by_space())
612
				{
613
					rename($_SESSION['temp_attachments'][$attachID]['tmp_name'], $context['attach_dir'] . '/' . $attachID);
614
					$_SESSION['temp_attachments'][$attachID]['tmp_name'] = $context['attach_dir'] . '/' . $attachID;
615
					$_SESSION['temp_attachments'][$attachID]['id_folder'] = $modSettings['currentAttachmentUploadDir'];
616
					$context['dir_size'] = 0;
617
					$context['dir_files'] = 0;
618
				}
619
				// Or, let the user know that it ain't gonna happen.
620
				else
621
				{
622
					if (isset($context['dir_creation_error']))
623
						$_SESSION['temp_attachments'][$attachID]['errors'][] = $context['dir_creation_error'];
624
					else
625
						$_SESSION['temp_attachments'][$attachID]['errors'][] = 'ran_out_of_space';
626
				}
627
			}
628
			else
629
				$_SESSION['temp_attachments'][$attachID]['errors'][] = 'ran_out_of_space';
630
		}
631
	}
632
633
	// Is the file too big?
634
	$context['attachments']['total_size'] += $_SESSION['temp_attachments'][$attachID]['size'];
635
	if (!empty($modSettings['attachmentSizeLimit']) && $_SESSION['temp_attachments'][$attachID]['size'] > $modSettings['attachmentSizeLimit'] * 1024)
636
		$_SESSION['temp_attachments'][$attachID]['errors'][] = array('file_too_big', array(comma_format($modSettings['attachmentSizeLimit'], 0)));
637
638
	// Check the total upload size for this post...
639
	if (!empty($modSettings['attachmentPostLimit']) && $context['attachments']['total_size'] > $modSettings['attachmentPostLimit'] * 1024)
640
		$_SESSION['temp_attachments'][$attachID]['errors'][] = array('attach_max_total_file_size', array(comma_format($modSettings['attachmentPostLimit'], 0), comma_format($modSettings['attachmentPostLimit'] - (($context['attachments']['total_size'] - $_SESSION['temp_attachments'][$attachID]['size']) / 1024), 0)));
641
642
	// Have we reached the maximum number of files we are allowed?
643
	$context['attachments']['quantity']++;
644
645
	// Set a max limit if none exists
646
	if (empty($modSettings['attachmentNumPerPostLimit']) && $context['attachments']['quantity'] >= 50)
647
		$modSettings['attachmentNumPerPostLimit'] = 50;
648
649
	if (!empty($modSettings['attachmentNumPerPostLimit']) && $context['attachments']['quantity'] > $modSettings['attachmentNumPerPostLimit'])
650
		$_SESSION['temp_attachments'][$attachID]['errors'][] = array('attachments_limit_per_post', array($modSettings['attachmentNumPerPostLimit']));
651
652
	// File extension check
653
	if (!empty($modSettings['attachmentCheckExtensions']))
654
	{
655
		$allowed = explode(',', strtolower($modSettings['attachmentExtensions']));
656
		foreach ($allowed as $k => $dummy)
657
			$allowed[$k] = trim($dummy);
658
659
		if (!in_array(strtolower(substr(strrchr($_SESSION['temp_attachments'][$attachID]['name'], '.'), 1)), $allowed))
660
		{
661
			$allowed_extensions = strtr(strtolower($modSettings['attachmentExtensions']), array(',' => ', '));
662
			$_SESSION['temp_attachments'][$attachID]['errors'][] = array('cant_upload_type', array($allowed_extensions));
663
		}
664
	}
665
666
	// Undo the math if there's an error
667
	if (!empty($_SESSION['temp_attachments'][$attachID]['errors']))
668
	{
669
		if (isset($context['dir_size']))
670
			$context['dir_size'] -= $_SESSION['temp_attachments'][$attachID]['size'];
671
		if (isset($context['dir_files']))
672
			$context['dir_files']--;
673
		$context['attachments']['total_size'] -= $_SESSION['temp_attachments'][$attachID]['size'];
674
		$context['attachments']['quantity']--;
675
		return false;
676
	}
677
678
	return true;
679
}
680
681
/**
682
 * Create an attachment, with the given array of parameters.
683
 * - Adds any additional or missing parameters to $attachmentOptions.
684
 * - Renames the temporary file.
685
 * - Creates a thumbnail if the file is an image and the option enabled.
686
 *
687
 * @param array $attachmentOptions An array of attachment options
688
 * @return bool Whether the attachment was created successfully
689
 */
690
function createAttachment(&$attachmentOptions)
691
{
692
	global $modSettings, $sourcedir, $smcFunc, $context, $txt;
693
694
	require_once($sourcedir . '/Subs-Graphics.php');
695
696
	// If this is an image we need to set a few additional parameters.
697
	$size = @getimagesize($attachmentOptions['tmp_name']);
698
	list ($attachmentOptions['width'], $attachmentOptions['height']) = $size;
699
700
	if (!empty($attachmentOptions['mime_type']) && $attachmentOptions['mime_type'] === 'image/svg+xml')
701
	{
702
		foreach (getSvgSize($attachmentOptions['tmp_name']) as $key => $value)
703
			$attachmentOptions[$key] = $value === INF ? 0 : $value;
704
	}
705
706
	if (function_exists('exif_read_data') && ($exif_data = @exif_read_data($attachmentOptions['tmp_name'])) !== false && !empty($exif_data['Orientation']))
707
		if (in_array($exif_data['Orientation'], [5, 6, 7, 8]))
708
		{
709
			$new_width = $attachmentOptions['height'];
710
			$new_height = $attachmentOptions['width'];
711
			$attachmentOptions['width'] = $new_width;
712
			$attachmentOptions['height'] = $new_height;
713
		}
714
715
	// If it's an image get the mime type right.
716
	if (empty($attachmentOptions['mime_type']) && $attachmentOptions['width'])
717
	{
718
		// Got a proper mime type?
719
		if (!empty($size['mime']))
720
			$attachmentOptions['mime_type'] = $size['mime'];
721
722
		// Otherwise a valid one?
723
		elseif (isset($context['valid_image_types'][$size[2]]))
724
			$attachmentOptions['mime_type'] = 'image/' . $context['valid_image_types'][$size[2]];
725
	}
726
727
	// It is possible we might have a MIME type that isn't actually an image but still have a size.
728
	// For example, Shockwave files will be able to return size but be 'application/shockwave' or similar.
729
	if (!empty($attachmentOptions['mime_type']) && strpos($attachmentOptions['mime_type'], 'image/') !== 0)
730
	{
731
		$attachmentOptions['width'] = 0;
732
		$attachmentOptions['height'] = 0;
733
	}
734
735
	// Get the hash if no hash has been given yet.
736
	if (empty($attachmentOptions['file_hash']))
737
		$attachmentOptions['file_hash'] = getAttachmentFilename($attachmentOptions['name'], false, null, true);
0 ignored issues
show
false of type false is incompatible with the type integer expected by parameter $attachment_id of getAttachmentFilename(). ( Ignorable by Annotation )

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

737
		$attachmentOptions['file_hash'] = getAttachmentFilename($attachmentOptions['name'], /** @scrutinizer ignore-type */ false, null, true);
Loading history...
738
739
	// Assuming no-one set the extension let's take a look at it.
740
	if (empty($attachmentOptions['fileext']))
741
	{
742
		$attachmentOptions['fileext'] = strtolower(strrpos($attachmentOptions['name'], '.') !== false ? substr($attachmentOptions['name'], strrpos($attachmentOptions['name'], '.') + 1) : '');
743
		if (strlen($attachmentOptions['fileext']) > 8 || '.' . $attachmentOptions['fileext'] == $attachmentOptions['name'])
744
			$attachmentOptions['fileext'] = '';
745
	}
746
747
	// This defines which options to use for which columns in the insert query.
748
	// Mods using the hook can add columns and even change the properties of existing columns,
749
	// but if they delete one of these columns, it will be reset to the default defined here.
750
	$attachmentStandardInserts = $attachmentInserts = array(
751
		// Format: 'column' => array('type', 'option')
752
		'id_folder' => array('int', 'id_folder'),
753
		'id_msg' => array('int', 'post'),
754
		'filename' => array('string-255', 'name'),
755
		'file_hash' => array('string-40', 'file_hash'),
756
		'fileext' => array('string-8', 'fileext'),
757
		'size' => array('int', 'size'),
758
		'width' => array('int', 'width'),
759
		'height' => array('int', 'height'),
760
		'mime_type' => array('string-20', 'mime_type'),
761
		'approved' => array('int', 'approved'),
762
	);
763
764
	// Last chance to change stuff!
765
	call_integration_hook('integrate_createAttachment', array(&$attachmentOptions, &$attachmentInserts));
766
767
	// Make sure the folder is valid...
768
	$tmp = is_array($modSettings['attachmentUploadDir']) ? $modSettings['attachmentUploadDir'] : $smcFunc['json_decode']($modSettings['attachmentUploadDir'], true);
769
	$folders = array_keys($tmp);
770
	if (empty($attachmentOptions['id_folder']) || !in_array($attachmentOptions['id_folder'], $folders))
771
		$attachmentOptions['id_folder'] = $modSettings['currentAttachmentUploadDir'];
772
773
	// Make sure all required columns are present, in case a mod screwed up.
774
	foreach ($attachmentStandardInserts as $column => $insert_info)
775
		if (!isset($attachmentInserts[$column]))
776
			$attachmentInserts[$column] = $insert_info;
777
778
	// Set up the columns and values to insert, in the correct order.
779
	$attachmentColumns = array();
780
	$attachmentValues = array();
781
	foreach ($attachmentInserts as $column => $insert_info)
782
	{
783
		$attachmentColumns[$column] = $insert_info[0];
784
785
		if (!empty($insert_info[0]) && $insert_info[0] == 'int')
786
			$attachmentValues[] = (int) $attachmentOptions[$insert_info[1]];
787
		else
788
			$attachmentValues[] = $attachmentOptions[$insert_info[1]];
789
	}
790
791
	// Create the attachment in the database.
792
	$attachmentOptions['id'] = $smcFunc['db_insert']('',
793
		'{db_prefix}attachments',
794
		$attachmentColumns,
795
		$attachmentValues,
796
		array('id_attach'),
797
		1
798
	);
799
800
	// Attachment couldn't be created.
801
	if (empty($attachmentOptions['id']))
802
	{
803
		loadLanguage('Errors');
804
		log_error($txt['attachment_not_created'], 'general');
805
		return false;
806
	}
807
808
	// Now that we have the attach id, let's rename this sucker and finish up.
809
	$attachmentOptions['destination'] = getAttachmentFilename(basename($attachmentOptions['name']), $attachmentOptions['id'], $attachmentOptions['id_folder'], false, $attachmentOptions['file_hash']);
810
	rename($attachmentOptions['tmp_name'], $attachmentOptions['destination']);
811
812
	// If it's not approved then add to the approval queue.
813
	if (!$attachmentOptions['approved'])
814
	{
815
		$smcFunc['db_insert']('',
816
			'{db_prefix}approval_queue',
817
			array(
818
				'id_attach' => 'int', 'id_msg' => 'int',
819
			),
820
			array(
821
				$attachmentOptions['id'], (int) $attachmentOptions['post'],
822
			),
823
			array()
824
		);
825
826
		// Queue background notification task.
827
		$smcFunc['db_insert'](
828
			'insert',
829
			'{db_prefix}background_tasks',
830
			array(
831
				'task_file' => 'string',
832
				'task_class' => 'string',
833
				'task_data' => 'string',
834
				'claimed_time' => 'int'
835
			),
836
			array(
837
					'$sourcedir/tasks/CreateAttachment-Notify.php',
838
					'CreateAttachment_Notify_Background',
839
					$smcFunc['json_encode'](
840
						array(
841
							'id' => $attachmentOptions['id'],
842
						)
843
					),
844
				0
845
			),
846
			array(
847
				'id_task'
848
			)
849
		);
850
	}
851
852
	if (empty($modSettings['attachmentThumbnails']) || (empty($attachmentOptions['width']) && empty($attachmentOptions['height'])))
853
		return true;
854
855
	// Like thumbnails, do we?
856
	if (!empty($modSettings['attachmentThumbWidth']) && !empty($modSettings['attachmentThumbHeight']) && ($attachmentOptions['width'] > $modSettings['attachmentThumbWidth'] || $attachmentOptions['height'] > $modSettings['attachmentThumbHeight']))
857
	{
858
		if (createThumbnail($attachmentOptions['destination'], $modSettings['attachmentThumbWidth'], $modSettings['attachmentThumbHeight']))
859
		{
860
			// Figure out how big we actually made it.
861
			$size = @getimagesize($attachmentOptions['destination'] . '_thumb');
862
			list ($thumb_width, $thumb_height) = $size;
863
864
			if (!empty($size['mime']))
865
				$thumb_mime = $size['mime'];
866
			elseif (isset($context['valid_image_types'][$size[2]]))
867
				$thumb_mime = 'image/' . $context['valid_image_types'][$size[2]];
868
			// Lord only knows how this happened...
869
			else
870
				$thumb_mime = '';
871
872
			$thumb_filename = $attachmentOptions['name'] . '_thumb';
873
			$thumb_size = filesize($attachmentOptions['destination'] . '_thumb');
874
			$thumb_file_hash = getAttachmentFilename($thumb_filename, false, null, true);
875
			$thumb_path = $attachmentOptions['destination'] . '_thumb';
876
877
			// We should check the file size and count here since thumbs are added to the existing totals.
878
			if (!empty($modSettings['automanage_attachments']) && $modSettings['automanage_attachments'] == 1 && !empty($modSettings['attachmentDirSizeLimit']) || !empty($modSettings['attachmentDirFileLimit']))
0 ignored issues
show
Consider adding parentheses for clarity. Current Interpretation: (! empty($modSettings['a...tachmentDirFileLimit']), Probably Intended Meaning: ! empty($modSettings['au...achmentDirFileLimit']))
Loading history...
879
			{
880
				$context['dir_size'] = isset($context['dir_size']) ? $context['dir_size'] += $thumb_size : $context['dir_size'] = 0;
881
				$context['dir_files'] = isset($context['dir_files']) ? $context['dir_files']++ : $context['dir_files'] = 0;
882
883
				// If the folder is full, try to create a new one and move the thumb to it.
884
				if ($context['dir_size'] > $modSettings['attachmentDirSizeLimit'] * 1024 || $context['dir_files'] + 2 > $modSettings['attachmentDirFileLimit'])
885
				{
886
					if (automanage_attachments_by_space())
887
					{
888
						rename($thumb_path, $context['attach_dir'] . '/' . $thumb_filename);
889
						$thumb_path = $context['attach_dir'] . '/' . $thumb_filename;
890
						$context['dir_size'] = 0;
891
						$context['dir_files'] = 0;
892
					}
893
				}
894
			}
895
			// If a new folder has been already created. Gotta move this thumb there then.
896
			if ($modSettings['currentAttachmentUploadDir'] != $attachmentOptions['id_folder'])
897
			{
898
				rename($thumb_path, $context['attach_dir'] . '/' . $thumb_filename);
899
				$thumb_path = $context['attach_dir'] . '/' . $thumb_filename;
900
			}
901
902
			// To the database we go!
903
			$attachmentOptions['thumb'] = $smcFunc['db_insert']('',
904
				'{db_prefix}attachments',
905
				array(
906
					'id_folder' => 'int', 'id_msg' => 'int', 'attachment_type' => 'int', 'filename' => 'string-255', 'file_hash' => 'string-40', 'fileext' => 'string-8',
907
					'size' => 'int', 'width' => 'int', 'height' => 'int', 'mime_type' => 'string-20', 'approved' => 'int',
908
				),
909
				array(
910
					$modSettings['currentAttachmentUploadDir'], (int) $attachmentOptions['post'], 3, $thumb_filename, $thumb_file_hash, $attachmentOptions['fileext'],
911
					$thumb_size, $thumb_width, $thumb_height, $thumb_mime, (int) $attachmentOptions['approved'],
912
				),
913
				array('id_attach'),
914
				1
915
			);
916
917
			if (!empty($attachmentOptions['thumb']))
918
			{
919
				$smcFunc['db_query']('', '
920
					UPDATE {db_prefix}attachments
921
					SET id_thumb = {int:id_thumb}
922
					WHERE id_attach = {int:id_attach}',
923
					array(
924
						'id_thumb' => $attachmentOptions['thumb'],
925
						'id_attach' => $attachmentOptions['id'],
926
					)
927
				);
928
929
				rename($thumb_path, getAttachmentFilename($thumb_filename, $attachmentOptions['thumb'], $modSettings['currentAttachmentUploadDir'], false, $thumb_file_hash));
930
			}
931
		}
932
	}
933
934
	return true;
935
}
936
937
/**
938
 * Assigns the given attachments to the given message ID.
939
 *
940
 * @param $attachIDs array of attachment IDs to assign.
941
 * @param $msgID integer the message ID.
942
 *
943
 * @return boolean false on error or missing params.
944
 */
945
function assignAttachments($attachIDs = array(), $msgID = 0)
946
{
947
	global $smcFunc;
948
949
	// Oh, come on!
950
	if (empty($attachIDs) || empty($msgID))
951
		return false;
952
953
	// "I see what is right and approve, but I do what is wrong."
954
	call_integration_hook('integrate_assign_attachments', array(&$attachIDs, &$msgID));
955
956
	// One last check
957
	if (empty($attachIDs))
958
		return false;
959
960
	// Perform.
961
	$smcFunc['db_query']('', '
962
		UPDATE {db_prefix}attachments
963
		SET id_msg = {int:id_msg}
964
		WHERE id_attach IN ({array_int:attach_ids})',
965
		array(
966
			'id_msg' => $msgID,
967
			'attach_ids' => $attachIDs,
968
		)
969
	);
970
971
	return true;
972
}
973
974
/**
975
 * Gets an attach ID and tries to load all its info.
976
 *
977
 * @param int $attachID the attachment ID to load info from.
978
 *
979
 * @return mixed If succesful, it will return an array of loaded data. String, most likely a $txt key if there was some error.
980
 */
981
function parseAttachBBC($attachID = 0)
982
{
983
	global $board, $modSettings, $context, $scripturl, $smcFunc, $user_info;
984
	static $view_attachment_boards;
985
986
	if (!isset($view_attachment_boards))
987
		$view_attachment_boards = boardsAllowedTo('view_attachments');
988
989
	// Meh...
990
	if (empty($attachID))
991
		return 'attachments_no_data_loaded';
992
993
	// Make it easy.
994
	$msgID = !empty($_REQUEST['msg']) ? (int) $_REQUEST['msg'] : 0;
995
996
	// Perhaps someone else wants to do the honors? Yes, this also includes dealing with previews ;)
997
	$externalParse = call_integration_hook('integrate_pre_parseAttachBBC', array($attachID, $msgID));
998
999
	// "I am innocent of the blood of this just person: see ye to it."
1000
	if (!empty($externalParse) && (is_string($externalParse) || is_array($externalParse)))
1001
		return $externalParse;
1002
1003
	// Are attachments enabled?
1004
	if (empty($modSettings['attachmentEnable']))
1005
		return 'attachments_not_enable';
1006
1007
	$check_board_perms = !isset($_SESSION['attachments_can_preview'][$attachID]) && $view_attachment_boards !== array(0);
1008
1009
	// There is always the chance someone else has already done our dirty work...
1010
	// If so, all pertinent checks were already done. Hopefully...
1011
	if (!empty($context['current_attachments']) && !empty($context['current_attachments'][$attachID]))
1012
		return $context['current_attachments'][$attachID];
1013
1014
	// Can the user view attachments on this board?
1015
	if ($check_board_perms && !empty($board) && !in_array($board, $view_attachment_boards))
1016
		return 'attachments_not_allowed_to_see';
1017
1018
	// Get the message info associated with this particular attach ID.
1019
	$attachInfo = getAttachMsgInfo($attachID);
1020
1021
	// There is always the chance this attachment no longer exists or isn't associated to a message anymore...
1022
	if (empty($attachInfo))
1023
		return 'attachments_no_data_loaded';
1024
1025
	if (empty($attachInfo['msg']) && empty($context['preview_message']))
1026
		return 'attachments_no_msg_associated';
1027
1028
	// Can the user view attachments on the board that holds the attachment's original post?
1029
	// (This matters when one post quotes another on a different board.)
1030
	if ($check_board_perms && !in_array($attachInfo['board'], $view_attachment_boards))
1031
		return 'attachments_not_allowed_to_see';
1032
1033
	if (empty($context['loaded_attachments'][$attachInfo['msg']]))
1034
		prepareAttachsByMsg(array($attachInfo['msg']));
1035
1036
	if (isset($context['loaded_attachments'][$attachInfo['msg']][$attachID]))
1037
		$attachContext = $context['loaded_attachments'][$attachInfo['msg']][$attachID];
1038
1039
	// In case the user manually typed the thumbnail's ID into the BBC
1040
	elseif (!empty($context['loaded_attachments'][$attachInfo['msg']]))
1041
	{
1042
		foreach ($context['loaded_attachments'][$attachInfo['msg']] as $foundAttachID => $foundAttach)
1043
		{
1044
			if (array_key_exists('id_thumb', $foundAttach) && $foundAttach['id_thumb'] == $attachID)
1045
			{
1046
				$attachContext = $context['loaded_attachments'][$attachInfo['msg']][$foundAttachID];
1047
				$attachID = $foundAttachID;
1048
				break;
1049
			}
1050
		}
1051
	}
1052
1053
	// Load this particular attach's context.
1054
	if (!empty($attachContext))
1055
	{
1056
		// Skip unapproved attachment, unless they belong to the user or the user can approve them.
1057
		if (!$context['loaded_attachments'][$attachInfo['msg']][$attachID]['approved'] &&
1058
			$modSettings['postmod_active'] && !allowedTo('approve_posts') &&
1059
			$context['loaded_attachments'][$attachInfo['msg']][$attachID]['id_member'] != $user_info['id'])
1060
		{
1061
			unset($context['loaded_attachments'][$attachInfo['msg']][$attachID]);
1062
			return 'attachments_unapproved';
1063
		}
1064
		$attachLoaded = loadAttachmentContext($attachContext['id_msg'], $context['loaded_attachments']);
1065
	}
1066
	else
1067
		return 'attachments_no_data_loaded';
1068
1069
	if (empty($attachLoaded))
1070
		return 'attachments_no_data_loaded';
1071
1072
	else
1073
		$attachContext = $attachLoaded[$attachID];
1074
1075
	// It's theoretically possible that prepareAttachsByMsg() changed the board id, so check again.
1076
	if ($check_board_perms && !in_array($attachContext['board'], $view_attachment_boards))
1077
		return 'attachments_not_allowed_to_see';
1078
1079
	// You may or may not want to show this under the post.
1080
	if (!empty($modSettings['dont_show_attach_under_post']) && !isset($context['show_attach_under_post'][$attachID]))
1081
		$context['show_attach_under_post'][$attachID] = $attachID;
1082
1083
	// Last minute changes?
1084
	call_integration_hook('integrate_post_parseAttachBBC', array(&$attachContext));
1085
1086
	// Don't do any logic with the loaded data, leave it to whoever called this function.
1087
	return $attachContext;
1088
}
1089
1090
/**
1091
 * Gets raw info directly from the attachments table.
1092
 *
1093
 * @param array $attachIDs An array of attachments IDs.
1094
 *
1095
 * @return array
1096
 */
1097
function getRawAttachInfo($attachIDs)
1098
{
1099
	global $smcFunc, $modSettings;
1100
1101
	if (empty($attachIDs))
1102
		return array();
1103
1104
	$return = array();
1105
1106
	$request = $smcFunc['db_query']('', '
1107
		SELECT a.id_attach, a.id_msg, a.id_member, a.size, a.mime_type, a.id_folder, a.filename' . (empty($modSettings['attachmentShowImages']) || empty($modSettings['attachmentThumbnails']) ? '' : ',
1108
			COALESCE(thumb.id_attach, 0) AS id_thumb, thumb.width AS thumb_width, thumb.height AS thumb_height') . '
1109
		FROM {db_prefix}attachments AS a' . (empty($modSettings['attachmentShowImages']) || empty($modSettings['attachmentThumbnails']) ? '' : '
1110
			LEFT JOIN {db_prefix}attachments AS thumb ON (thumb.id_attach = a.id_thumb)') . '
1111
		WHERE a.id_attach IN ({array_int:attach_ids})
1112
		LIMIT 1',
1113
		array(
1114
			'attach_ids' => (array) $attachIDs,
1115
		)
1116
	);
1117
1118
	if ($smcFunc['db_num_rows']($request) != 1)
1119
		return array();
1120
1121
	while ($row = $smcFunc['db_fetch_assoc']($request))
1122
		$return[$row['id_attach']] = array(
1123
			'name' => $smcFunc['htmlspecialchars']($row['filename']),
1124
			'size' => $row['size'],
1125
			'attachID' => $row['id_attach'],
1126
			'unchecked' => false,
1127
			'approved' => 1,
1128
			'mime_type' => $row['mime_type'],
1129
			'thumb' => $row['id_thumb'],
1130
		);
1131
	$smcFunc['db_free_result']($request);
1132
1133
	return $return;
1134
}
1135
1136
/**
1137
 * Gets all needed message data associated with an attach ID
1138
 *
1139
 * @param int $attachID the attachment ID to load info from.
1140
 *
1141
 * @return array
1142
 */
1143
function getAttachMsgInfo($attachID)
1144
{
1145
	global $smcFunc, $context;
1146
1147
	if (empty($attachID))
1148
		return array();
1149
1150
	if (!isset($context['loaded_attachments']))
1151
		$context['loaded_attachments'] = array();
1152
1153
	foreach ($context['loaded_attachments'] as $msgRows)
1154
	{
1155
		if (empty($msgRows[$attachID]))
1156
			continue;
1157
1158
		$row = array(
1159
			'msg' => $msgRows[$attachID]['id_msg'],
1160
			'topic' => $msgRows[$attachID]['topic'],
1161
			'board' => $msgRows[$attachID]['board'],
1162
		);
1163
1164
		return $row;
1165
	}
1166
1167
	$request = $smcFunc['db_query']('', '
1168
		SELECT a.id_msg AS msg, m.id_topic AS topic, m.id_board AS board
1169
		FROM {db_prefix}attachments AS a
1170
			LEFT JOIN {db_prefix}messages AS m ON (m.id_msg = a.id_msg)
1171
		WHERE id_attach = {int:id_attach}
1172
		LIMIT 1',
1173
		array(
1174
			'id_attach' => (int) $attachID,
1175
		)
1176
	);
1177
1178
	if ($smcFunc['db_num_rows']($request) != 1)
1179
		return array();
1180
1181
	$row = $smcFunc['db_fetch_assoc']($request);
1182
	$smcFunc['db_free_result']($request);
1183
1184
	return $row;
1185
}
1186
1187
/**
1188
 * This loads an attachment's contextual data including, most importantly, its size if it is an image.
1189
 * It requires the view_attachments permission to calculate image size.
1190
 * It attempts to keep the "aspect ratio" of the posted image in line, even if it has to be resized by
1191
 * the max_image_width and max_image_height settings.
1192
 *
1193
 * @param int $id_msg ID of the post to load attachments for
1194
 * @param array $attachments  An array of already loaded attachments. This function no longer depends on having $topic declared, thus, you need to load the actual topic ID for each attachment.
1195
 * @return array An array of attachment info
1196
 */
1197
function loadAttachmentContext($id_msg, $attachments)
1198
{
1199
	global $modSettings, $txt, $scripturl, $sourcedir, $smcFunc, $context;
1200
1201
	if (empty($attachments) || empty($attachments[$id_msg]))
1202
		return array();
1203
1204
	// Set up the attachment info - based on code by Meriadoc.
1205
	$attachmentData = array();
1206
	$have_unapproved = false;
1207
	if (isset($attachments[$id_msg]) && !empty($modSettings['attachmentEnable']))
1208
	{
1209
		foreach ($attachments[$id_msg] as $i => $attachment)
1210
		{
1211
			$attachmentData[$i] = array(
1212
				'id' => $attachment['id_attach'],
1213
				'name' => preg_replace('~&amp;#(\\d{1,7}|x[0-9a-fA-F]{1,6});~', '&#\\1;', $smcFunc['htmlspecialchars'](un_htmlspecialchars($attachment['filename']))),
1214
				'downloads' => $attachment['downloads'],
1215
				'size' => ($attachment['filesize'] < 1024000) ? round($attachment['filesize'] / 1024, 2) . ' ' . $txt['kilobyte'] : round($attachment['filesize'] / 1024 / 1024, 2) . ' ' . $txt['megabyte'],
1216
				'byte_size' => $attachment['filesize'],
1217
				'href' => $scripturl . '?action=dlattach;attach=' . $attachment['id_attach'],
1218
				'link' => '<a href="' . $scripturl . '?action=dlattach;attach=' . $attachment['id_attach'] . '" class="bbc_link">' . $smcFunc['htmlspecialchars'](un_htmlspecialchars($attachment['filename'])) . '</a>',
1219
				'is_image' => !empty($attachment['width']) && !empty($attachment['height']),
1220
				'is_approved' => $attachment['approved'],
1221
				'topic' => $attachment['topic'],
1222
				'board' => $attachment['board'],
1223
				'mime_type' => $attachment['mime_type'],
1224
			);
1225
1226
			// If something is unapproved we'll note it so we can sort them.
1227
			if (!$attachment['approved'])
1228
				$have_unapproved = true;
1229
1230
			if (!$attachmentData[$i]['is_image'])
1231
				continue;
1232
1233
			$attachmentData[$i]['real_width'] = $attachment['width'];
1234
			$attachmentData[$i]['width'] = $attachment['width'];
1235
			$attachmentData[$i]['real_height'] = $attachment['height'];
1236
			$attachmentData[$i]['height'] = $attachment['height'];
1237
1238
			// Let's see, do we want thumbs?
1239
			if (!empty($modSettings['attachmentShowImages']) && !empty($modSettings['attachmentThumbnails']) && !empty($modSettings['attachmentThumbWidth']) && !empty($modSettings['attachmentThumbHeight']) && ($attachment['width'] > $modSettings['attachmentThumbWidth'] || $attachment['height'] > $modSettings['attachmentThumbHeight']) && strlen($attachment['filename']) < 249)
1240
			{
1241
				// A proper thumb doesn't exist yet? Create one!
1242
				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']))
1243
				{
1244
					$filename = getAttachmentFilename($attachment['filename'], $attachment['id_attach'], $attachment['id_folder']);
1245
1246
					require_once($sourcedir . '/Subs-Graphics.php');
1247
					if (createThumbnail($filename, $modSettings['attachmentThumbWidth'], $modSettings['attachmentThumbHeight']))
1248
					{
1249
						// So what folder are we putting this image in?
1250
						if (!empty($modSettings['currentAttachmentUploadDir']))
1251
						{
1252
							if (!is_array($modSettings['attachmentUploadDir']))
1253
								$modSettings['attachmentUploadDir'] = $smcFunc['json_decode']($modSettings['attachmentUploadDir'], true);
1254
							$id_folder_thumb = $modSettings['currentAttachmentUploadDir'];
1255
						}
1256
						else
1257
						{
1258
							$id_folder_thumb = 1;
1259
						}
1260
1261
						// Calculate the size of the created thumbnail.
1262
						$size = @getimagesize($filename . '_thumb');
1263
						list ($attachment['thumb_width'], $attachment['thumb_height']) = $size;
1264
						$thumb_size = filesize($filename . '_thumb');
1265
1266
						// What about the extension?
1267
						$thumb_ext = isset($context['valid_image_types'][$size[2]]) ? $context['valid_image_types'][$size[2]] : '';
1268
1269
						// Figure out the mime type.
1270
						if (!empty($size['mime']))
1271
							$thumb_mime = $size['mime'];
1272
						else
1273
							$thumb_mime = 'image/' . $thumb_ext;
1274
1275
						$thumb_filename = $attachment['filename'] . '_thumb';
1276
						$thumb_hash = getAttachmentFilename($thumb_filename, false, null, true);
1277
						$old_id_thumb = $attachment['id_thumb'];
1278
1279
						// Add this beauty to the database.
1280
						$attachment['id_thumb'] = $smcFunc['db_insert']('',
1281
							'{db_prefix}attachments',
1282
							array('id_folder' => 'int', 'id_msg' => 'int', 'attachment_type' => 'int', 'filename' => 'string', 'file_hash' => 'string', 'size' => 'int', 'width' => 'int', 'height' => 'int', 'fileext' => 'string', 'mime_type' => 'string'),
1283
							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),
1284
							array('id_attach'),
1285
							1
1286
						);
1287
1288
						if (!empty($attachment['id_thumb']))
1289
						{
1290
							$smcFunc['db_query']('', '
1291
								UPDATE {db_prefix}attachments
1292
								SET id_thumb = {int:id_thumb}
1293
								WHERE id_attach = {int:id_attach}',
1294
								array(
1295
									'id_thumb' => $attachment['id_thumb'],
1296
									'id_attach' => $attachment['id_attach'],
1297
								)
1298
							);
1299
1300
							$thumb_realname = getAttachmentFilename($thumb_filename, $attachment['id_thumb'], $id_folder_thumb, false, $thumb_hash);
1301
							rename($filename . '_thumb', $thumb_realname);
1302
1303
							// Do we need to remove an old thumbnail?
1304
							if (!empty($old_id_thumb))
1305
							{
1306
								require_once($sourcedir . '/ManageAttachments.php');
1307
								removeAttachments(array('id_attach' => $old_id_thumb), '', false, false);
1308
							}
1309
						}
1310
					}
1311
				}
1312
1313
				// Only adjust dimensions on successful thumbnail creation.
1314
				if (!empty($attachment['thumb_width']) && !empty($attachment['thumb_height']))
1315
				{
1316
					$attachmentData[$i]['width'] = $attachment['thumb_width'];
1317
					$attachmentData[$i]['height'] = $attachment['thumb_height'];
1318
				}
1319
			}
1320
1321
			if (!empty($attachment['id_thumb']))
1322
				$attachmentData[$i]['thumbnail'] = array(
1323
					'id' => $attachment['id_thumb'],
1324
					'href' => $scripturl . '?action=dlattach;attach=' . $attachment['id_thumb'] . ';image;thumb',
1325
				);
1326
			$attachmentData[$i]['thumbnail']['has_thumb'] = !empty($attachment['id_thumb']);
1327
1328
			// If thumbnails are disabled, check the maximum size of the image.
1329
			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'])))
1330
			{
1331
				if (!empty($modSettings['max_image_width']) && (empty($modSettings['max_image_height']) || $attachment['height'] * $modSettings['max_image_width'] / $attachment['width'] <= $modSettings['max_image_height']))
1332
				{
1333
					$attachmentData[$i]['width'] = $modSettings['max_image_width'];
1334
					$attachmentData[$i]['height'] = floor($attachment['height'] * $modSettings['max_image_width'] / $attachment['width']);
1335
				}
1336
				elseif (!empty($modSettings['max_image_width']))
1337
				{
1338
					$attachmentData[$i]['width'] = floor($attachment['width'] * $modSettings['max_image_height'] / $attachment['height']);
1339
					$attachmentData[$i]['height'] = $modSettings['max_image_height'];
1340
				}
1341
			}
1342
			elseif ($attachmentData[$i]['thumbnail']['has_thumb'])
1343
			{
1344
				// If the image is too large to show inline, make it a popup.
1345
				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'])))
1346
					$attachmentData[$i]['thumbnail']['javascript'] = 'return reqWin(\'' . $attachmentData[$i]['href'] . ';image\', ' . ($attachment['width'] + 20) . ', ' . ($attachment['height'] + 20) . ', true);';
1347
				else
1348
					$attachmentData[$i]['thumbnail']['javascript'] = 'return expandThumb(' . $attachment['id_attach'] . ');';
1349
			}
1350
1351
			if (!$attachmentData[$i]['thumbnail']['has_thumb'])
1352
				$attachmentData[$i]['downloads']++;
1353
1354
			// Describe undefined dimensions as "unknown".
1355
			// This can happen if an uploaded SVG is missing some key data.
1356
			foreach (array('real_width', 'real_height') as $key)
1357
			{
1358
				if (!isset($attachmentData[$i][$key]) || $attachmentData[$i][$key] === INF)
1359
				{
1360
					loadLanguage('Admin');
1361
					$attachmentData[$i][$key] = ' (' . $txt['unknown'] . ') ';
1362
				}
1363
			}
1364
		}
1365
	}
1366
1367
	// Do we need to instigate a sort?
1368
	if ($have_unapproved)
1369
		uasort(
1370
			$attachmentData,
1371
			function($a, $b)
1372
			{
1373
				if ($a['is_approved'] == $b['is_approved'])
1374
					return 0;
1375
1376
				return $a['is_approved'] > $b['is_approved'] ? -1 : 1;
1377
			}
1378
		);
1379
1380
	return $attachmentData;
1381
}
1382
1383
/**
1384
 * prepare the Attachment api for all messages
1385
 *
1386
 * @param int array $msgIDs the message ID to load info from.
1387
 *
1388
 * @return void
1389
 */
1390
function prepareAttachsByMsg($msgIDs)
1391
{
1392
	global $context, $modSettings, $smcFunc, $sourcedir;
1393
1394
	if (empty($context['loaded_attachments']))
1395
		$context['loaded_attachments'] = array();
1396
	// Remove all $msgIDs that we already processed
1397
	else
1398
		$msgIDs = array_diff($msgIDs, array_keys($context['loaded_attachments']), array(0));
1399
1400
	// Ensure that $msgIDs doesn't contain zero or non-integers.
1401
	$msgIDs = array_filter(array_map('intval', $msgIDs));
1402
1403
	if (!empty($msgIDs) || !empty($_SESSION['attachments_can_preview']))
1404
	{
1405
		// Where clause - there may or may not be msg ids, & may or may not be attachs to preview,
1406
		// depending on post vs edit, inserted or not, preview or not, post error or not, etc.
1407
		// Either way, they may be needed in a display of a list of posts or in the dropzone 'mock' list of thumbnails.
1408
		$msg_or_att = '';
1409
		if (!empty($msgIDs))
1410
			$msg_or_att .= 'a.id_msg IN ({array_int:message_id}) ';
1411
		if (!empty($msgIDs) && !empty($_SESSION['attachments_can_preview']))
1412
			$msg_or_att .= 'OR ';
1413
		if (!empty($_SESSION['attachments_can_preview']))
1414
			$msg_or_att .= 'a.id_attach IN ({array_int:preview_attachments})';
1415
1416
		// This tries to get all attachments for the page being displayed, to build a cache of attach info.
1417
		// This is also being used by POST, so it may need to grab attachs known only in the session.
1418
		$request = $smcFunc['db_query']('', '
1419
			SELECT
1420
				a.id_attach, a.id_folder, a.id_msg, a.filename, a.file_hash, COALESCE(a.size, 0) AS filesize, a.downloads, a.approved, m.id_topic AS topic, m.id_board AS board, m.id_member, a.mime_type,
1421
				a.width, a.height' . (empty($modSettings['attachmentShowImages']) || empty($modSettings['attachmentThumbnails']) ? '' : ',
1422
				COALESCE(thumb.id_attach, 0) AS id_thumb, thumb.width AS thumb_width, thumb.height AS thumb_height') . '
1423
			FROM {db_prefix}attachments AS a' . (empty($modSettings['attachmentShowImages']) || empty($modSettings['attachmentThumbnails']) ? '' : '
1424
				LEFT JOIN {db_prefix}attachments AS thumb ON (thumb.id_attach = a.id_thumb)') . '
1425
				LEFT JOIN {db_prefix}messages AS m ON (m.id_msg = a.id_msg)
1426
			WHERE a.attachment_type = {int:attachment_type}
1427
				AND (' . $msg_or_att . ')',
1428
			array(
1429
				'message_id' => $msgIDs,
1430
				'attachment_type' => 0,
1431
				'preview_attachments' => !empty($_SESSION['attachments_can_preview']) ? array_keys(array_filter($_SESSION['attachments_can_preview'])) : array(0),
1432
			)
1433
		);
1434
		$rows = $smcFunc['db_fetch_all']($request);
1435
		$smcFunc['db_free_result']($request);
1436
1437
		foreach ($rows as $row)
1438
		{
1439
			// SVGs are special.
1440
			if ($row['mime_type'] === 'image/svg+xml')
1441
			{
1442
				if (empty($row['width']) || empty($row['height']))
1443
				{
1444
					require_once($sourcedir . '/Subs-Graphics.php');
1445
1446
					$row = array_merge($row, getSvgSize(getAttachmentFilename($row['filename'], $row['id_attach'], $row['id_folder'])));
1447
				}
1448
1449
				// SVG is its own thumbnail.
1450
				if (isset($row['id_thumb']))
1451
				{
1452
					$row['id_thumb'] = $row['id_attach'];
1453
1454
					// For SVGs, we don't need to calculate thumbnail size precisely.
1455
					$row['thumb_width'] = min($row['width'], !empty($modSettings['attachmentThumbWidth']) ? $modSettings['attachmentThumbWidth'] : 1000);
1456
					$row['thumb_height'] = min($row['height'], !empty($modSettings['attachmentThumbHeight']) ? $modSettings['attachmentThumbHeight'] : 1000);
1457
1458
					// Must set the thumbnail's CSS dimensions manually.
1459
					addInlineCss('img#thumb_' . $row['id_thumb'] . ':not(.original_size) {width: ' . $row['thumb_width'] . 'px; height: ' . $row['thumb_height'] . 'px;}');
1460
				}
1461
			}
1462
1463
			if (empty($context['loaded_attachments'][$row['id_msg']]))
1464
				$context['loaded_attachments'][$row['id_msg']] = array();
1465
1466
			$context['loaded_attachments'][$row['id_msg']][$row['id_attach']] = $row;
1467
1468
			// This is better than sorting it with the query...
1469
			ksort($context['loaded_attachments'][$row['id_msg']]);
1470
		}
1471
	}
1472
}
1473
1474
?>