Passed
Push — master ( c90017...d9e5dd )
by Spuds
01:05 queued 24s
created

canRemoveAttachment()   D

Complexity

Conditions 15
Paths 272

Size

Total Lines 41
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 240

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 15
eloc 20
nc 272
nop 2
dl 0
loc 41
ccs 0
cts 0
cp 0
crap 240
rs 4.1833
c 1
b 0
f 0

How to fix   Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * This file handles the uploading and creation of attachments
5
 * as well as the auto management of the attachment directories.
6
 * Note to enhance documentation later:
7
 * attachment_type = 3 is a thumbnail, etc.
8
 *
9
 * @name      ElkArte Forum
10
 * @copyright ElkArte Forum contributors
11
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause
12
 *
13
 * This file contains code covered by:
14
 * copyright:	2011 Simple Machines (http://www.simplemachines.org)
15
 * license:  	BSD, See included LICENSE.TXT for terms and conditions.
16
 *
17
 * @version 1.1.8
18
 *
19
 */
20
21
/**
22
 * Approve an attachment, or maybe even more - no permission check!
23
 *
24
 * @package Attachments
25
 * @param int[] $attachments
26
 */
27
function approveAttachments($attachments)
28
{
29
	$db = database();
30
31
	if (empty($attachments))
32
		return 0;
33
34
	// For safety, check for thumbnails...
35
	$request = $db->query('', '
36
		SELECT
37
			a.id_attach, a.id_member, COALESCE(thumb.id_attach, 0) AS id_thumb
38
		FROM {db_prefix}attachments AS a
39
			LEFT JOIN {db_prefix}attachments AS thumb ON (thumb.id_attach = a.id_thumb)
40
		WHERE a.id_attach IN ({array_int:attachments})
41
			AND a.attachment_type = {int:attachment_type}',
42
		array(
43
			'attachments' => $attachments,
44
			'attachment_type' => 0,
45
		)
46
	);
47
	$attachments = array();
48
	while ($row = $db->fetch_assoc($request))
49
	{
50
		// Update the thumbnail too...
51
		if (!empty($row['id_thumb']))
52
			$attachments[] = $row['id_thumb'];
53
54
		$attachments[] = $row['id_attach'];
55
	}
56
	$db->free_result($request);
57
58
	if (empty($attachments))
59
		return 0;
60
61
	// Approving an attachment is not hard - it's easy.
62
	$db->query('', '
63
		UPDATE {db_prefix}attachments
64
		SET approved = {int:is_approved}
65
		WHERE id_attach IN ({array_int:attachments})',
66
		array(
67
			'attachments' => $attachments,
68
			'is_approved' => 1,
69
		)
70
	);
71
72
	// In order to log the attachments, we really need their message and filename
73
	$db->fetchQueryCallback('
74
		SELECT m.id_msg, a.filename
75
		FROM {db_prefix}attachments AS a
76
			INNER JOIN {db_prefix}messages AS m ON (a.id_msg = m.id_msg)
77
		WHERE a.id_attach IN ({array_int:attachments})
78
			AND a.attachment_type = {int:attachment_type}',
79
		array(
80
			'attachments' => $attachments,
81
			'attachment_type' => 0,
82
		),
83
		function ($row)
0 ignored issues
show
Bug introduced by
function(...) { /* ... */ } of type callable is incompatible with the type object|string expected by parameter $callback of Database::fetchQueryCallback(). ( Ignorable by Annotation )

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

83
		/** @scrutinizer ignore-type */ function ($row)
Loading history...
84
		{
85
			logAction(
86
				'approve_attach',
87
				array(
88
					'message' => $row['id_msg'],
89
					'filename' => preg_replace('~&amp;#(\\d{1,7}|x[0-9a-fA-F]{1,6});~', '&#\\1;', Util::htmlspecialchars($row['filename'])),
90
				)
91
			);
92
		}
93
	);
94
95
	// Remove from the approval queue.
96
	$db->query('', '
97
		DELETE FROM {db_prefix}approval_queue
98
		WHERE id_attach IN ({array_int:attachments})',
99
		array(
100
			'attachments' => $attachments,
101
		)
102
	);
103
104
	call_integration_hook('integrate_approve_attachments', array($attachments));
105
}
106
107
/**
108
 * Removes attachments or avatars based on a given query condition.
109
 *
110
 * - Called by remove avatar/attachment functions.
111
 * - It removes attachments based that match the $condition.
112
 * - It allows query_types 'messages' and 'members', whichever is need by the
113
 * $condition parameter.
114
 * - It does no permissions check.
115
 *
116
 * @package Attachments
117
 * @param mixed[] $condition
118
 * @param string $query_type
119
 * @param bool $return_affected_messages = false
120
 * @param bool $autoThumbRemoval = true
121
 * @return int[]|boolean returns affected messages if $return_affected_messages is set to true
122
 */
123
function removeAttachments($condition, $query_type = '', $return_affected_messages = false, $autoThumbRemoval = true)
124
{
125 6
	global $modSettings;
126
127 6
	$db = database();
128
129
	// @todo This might need more work!
130 6
	$new_condition = array();
131
	$query_parameter = array(
132 6
		'thumb_attachment_type' => 3,
133 6
	);
134 6
	$do_logging = array();
135
136 6
	if (is_array($condition))
0 ignored issues
show
introduced by
The condition is_array($condition) is always true.
Loading history...
137 6
	{
138 6
		foreach ($condition as $real_type => $restriction)
139
		{
140
			// Doing a NOT?
141 6
			$is_not = substr($real_type, 0, 4) == 'not_';
142 6
			$type = $is_not ? substr($real_type, 4) : $real_type;
143
144
			// @todo the !empty($restriction) is a trick to override the checks on $_POST['attach_del'] in Post.controller
145
			// In theory it should not be necessary
146 6
			if (in_array($type, array('id_member', 'id_attach', 'id_msg')) && !empty($restriction))
147 6
				$new_condition[] = 'a.' . $type . ($is_not ? ' NOT' : '') . ' IN (' . (is_array($restriction) ? '{array_int:' . $real_type . '}' : '{int:' . $real_type . '}') . ')';
148 6
			elseif ($type == 'attachment_type')
149 6
				$new_condition[] = 'a.attachment_type = {int:' . $real_type . '}';
150 6
			elseif ($type == 'poster_time')
151
				$new_condition[] = 'm.poster_time < {int:' . $real_type . '}';
152 6
			elseif ($type == 'last_login')
153
				$new_condition[] = 'mem.last_login < {int:' . $real_type . '}';
154 6
			elseif ($type == 'size')
155
				$new_condition[] = 'a.size > {int:' . $real_type . '}';
156 6
			elseif ($type == 'id_topic')
157 6
				$new_condition[] = 'm.id_topic IN (' . (is_array($restriction) ? '{array_int:' . $real_type . '}' : '{int:' . $real_type . '}') . ')';
158
159
			// Add the parameter!
160 6
			$query_parameter[$real_type] = $restriction;
161
162 6
			if ($type == 'do_logging')
163 6
				$do_logging = $condition['id_attach'];
164 6
		}
165
166 6
		if (empty($new_condition))
167 6
		{
168
			return false;
169
		}
170
171 6
		$condition = implode(' AND ', $new_condition);
172 6
	}
173
174
	// Delete it only if it exists...
175 6
	$msgs = array();
176 6
	$attach = array();
177 6
	$parents = array();
178
179 6
	require_once(SUBSDIR . '/Attachments.subs.php');
180
181
	// Get all the attachment names and id_msg's.
182 6
	$request = $db->query('', '
183
		SELECT
184 6
			a.id_folder, a.filename, a.file_hash, a.attachment_type, a.id_attach, a.id_member' . ($query_type == 'messages' ? ', m.id_msg' : ', a.id_msg') . ',
185
			thumb.id_folder AS thumb_folder, COALESCE(thumb.id_attach, 0) AS id_thumb, thumb.filename AS thumb_filename, thumb.file_hash AS thumb_file_hash, thumb_parent.id_attach AS id_parent
186 6
		FROM {db_prefix}attachments AS a' .($query_type == 'members' ? '
187 6
			INNER JOIN {db_prefix}members AS mem ON (mem.id_member = a.id_member)' : ($query_type == 'messages' ? '
188 6
			INNER JOIN {db_prefix}messages AS m ON (m.id_msg = a.id_msg)' : '')) . '
189
			LEFT JOIN {db_prefix}attachments AS thumb ON (thumb.id_attach = a.id_thumb)
190
			LEFT JOIN {db_prefix}attachments AS thumb_parent ON (thumb.attachment_type = {int:thumb_attachment_type} AND thumb_parent.id_thumb = a.id_attach)
191 6
		WHERE ' . $condition,
192
		$query_parameter
193 6
	);
194 6
	while ($row = $db->fetch_assoc($request))
195
	{
196
		// Figure out the "encrypted" filename and unlink it ;).
197
		if ($row['attachment_type'] == 1)
198
		{
199
			// if attachment_type = 1, it's... an avatar in a custom avatar directory.
200
			// wasn't it obvious? :P
201
			// @todo look again at this.
202
			@unlink($modSettings['custom_avatar_dir'] . '/' . $row['filename']);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

202
			/** @scrutinizer ignore-unhandled */ @unlink($modSettings['custom_avatar_dir'] . '/' . $row['filename']);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
203
		}
204
		else
205
		{
206
			$filename = getAttachmentFilename($row['filename'], $row['id_attach'], $row['id_folder'], false, $row['file_hash']);
207
			@unlink($filename);
208
209
			// If this was a thumb, the parent attachment should know about it.
210
			if (!empty($row['id_parent']))
211
				$parents[] = $row['id_parent'];
212
213
			// If this attachments has a thumb, remove it as well.
214
			if (!empty($row['id_thumb']) && $autoThumbRemoval)
215
			{
216
				$thumb_filename = getAttachmentFilename($row['thumb_filename'], $row['id_thumb'], $row['thumb_folder'], false, $row['thumb_file_hash']);
217
				@unlink($thumb_filename);
218
				$attach[] = $row['id_thumb'];
219
			}
220
		}
221
222
		// Make a list.
223
		if ($return_affected_messages && empty($row['attachment_type']))
224
			$msgs[] = $row['id_msg'];
225
		$attach[] = $row['id_attach'];
226
	}
227 6
	$db->free_result($request);
228
229
	// Removed attachments don't have to be updated anymore.
230 6
	$parents = array_diff($parents, $attach);
231 6
	if (!empty($parents))
232 6
		$db->query('', '
233
			UPDATE {db_prefix}attachments
234
			SET id_thumb = {int:no_thumb}
235
			WHERE id_attach IN ({array_int:parent_attachments})',
236
			array(
237
				'parent_attachments' => $parents,
238
				'no_thumb' => 0,
239
			)
240
		);
241
242 6
	if (!empty($do_logging))
243 6
	{
244
		// In order to log the attachments, we really need their message and filename
245
		$db->fetchQueryCallback('
246
			SELECT m.id_msg, a.filename
247
			FROM {db_prefix}attachments AS a
248
				INNER JOIN {db_prefix}messages AS m ON (a.id_msg = m.id_msg)
249
			WHERE a.id_attach IN ({array_int:attachments})
250
				AND a.attachment_type = {int:attachment_type}',
251
			array(
252
				'attachments' => $do_logging,
253
				'attachment_type' => 0,
254
			),
255
			function ($row)
0 ignored issues
show
Bug introduced by
function(...) { /* ... */ } of type callable is incompatible with the type object|string expected by parameter $callback of Database::fetchQueryCallback(). ( Ignorable by Annotation )

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

255
			/** @scrutinizer ignore-type */ function ($row)
Loading history...
256
			{
257
				logAction(
258
					'remove_attach',
259
					array(
260
						'message' => $row['id_msg'],
261
						'filename' => preg_replace('~&amp;#(\\d{1,7}|x[0-9a-fA-F]{1,6});~', '&#\\1;', Util::htmlspecialchars($row['filename'])),
262
					)
263
				);
264
			}
265
		);
266
	}
267
268 6
	if (!empty($attach))
269 6
		$db->query('', '
270
			DELETE FROM {db_prefix}attachments
271
			WHERE id_attach IN ({array_int:attachment_list})',
272
			array(
273
				'attachment_list' => $attach,
274
			)
275
		);
276
277 6
	call_integration_hook('integrate_remove_attachments', array($attach));
278
279
	if ($return_affected_messages)
280 6
		return array_unique($msgs);
281
	else
282 6
		return true;
283
}
284
285
/**
286
 * Return an array of attachments directories.
287
 *
288
 * @package Attachments
289
 * @see getAttachmentPath()
290
 */
291
function attachmentPaths()
292
{
293
	global $modSettings;
294
295
	if (empty($modSettings['attachmentUploadDir']))
296
		return array(BOARDDIR . '/attachments');
297
	elseif (!empty($modSettings['currentAttachmentUploadDir']))
298
	{
299
		// we have more directories
300
		if (!is_array($modSettings['attachmentUploadDir']))
301
			$modSettings['attachmentUploadDir'] = Util::unserialize($modSettings['attachmentUploadDir']);
302
303
		return $modSettings['attachmentUploadDir'];
304
	}
305
	else
306
		return array($modSettings['attachmentUploadDir']);
307
}
308
309
/**
310
 * How many attachments we have overall.
311
 *
312
 * @package Attachments
313
 * @return int
314
 */
315
function getAttachmentCount()
316
{
317
	$db = database();
318
319
	// Get the number of attachments....
320
	$request = $db->query('', '
321
		SELECT COUNT(*)
322
		FROM {db_prefix}attachments
323
		WHERE attachment_type = {int:attachment_type}
324
			AND id_member = {int:guest_id_member}',
325
		array(
326
			'attachment_type' => 0,
327
			'guest_id_member' => 0,
328
		)
329
	);
330
	list ($num_attachments) = $db->fetch_row($request);
331
	$db->free_result($request);
332
333
	return $num_attachments;
334
}
335
336
/**
337
 * How many attachments we have in a certain folder.
338
 *
339
 * @package Attachments
340
 * @param string $folder
341
 */
342
function getFolderAttachmentCount($folder)
343
{
344
	$db = database();
345
346
	// Get the number of attachments....
347
	$request = $db->query('', '
348
		SELECT COUNT(*)
349
		FROM {db_prefix}attachments
350
		WHERE id_folder = {int:folder_id}
351
			AND attachment_type != {int:attachment_type}',
352
		array(
353
			'folder_id' => $folder,
354
			'attachment_type' => 1,
355
		)
356
	);
357
	list ($num_attachments) = $db->fetch_row($request);
358
	$db->free_result($request);
359
360
	return $num_attachments;
361
}
362
363
/**
364
 * How many avatars do we have. Need to know. :P
365
 *
366
 * @package Attachments
367
 * @return int
368
 */
369
function getAvatarCount()
370
{
371
	$db = database();
372
373
	// Get the avatar amount....
374
	$request = $db->query('', '
375
		SELECT COUNT(*)
376
		FROM {db_prefix}attachments
377
		WHERE id_member != {int:guest_id_member}',
378
		array(
379
			'guest_id_member' => 0,
380
		)
381
	);
382
	list ($num_avatars) = $db->fetch_row($request);
383
	$db->free_result($request);
384
385
	return $num_avatars;
386
}
387
388
/**
389
 * Get the attachments directories, as an array.
390
 *
391
 * @package Attachments
392
 * @return mixed[] the attachments directory/directories
393
 */
394
function getAttachmentDirs()
395
{
396
	global $modSettings;
397
398
	if (!empty($modSettings['currentAttachmentUploadDir']))
399
		$attach_dirs = Util::unserialize($modSettings['attachmentUploadDir']);
400
	elseif (!empty($modSettings['attachmentUploadDir']))
401
		$attach_dirs = array($modSettings['attachmentUploadDir']);
402
	else
403
		$attach_dirs = array(BOARDDIR . '/attachments');
404
405
	return $attach_dirs;
406
}
407
408
/**
409
 * Simple function to remove the strictly needed of orphan attachments.
410
 *
411
 * - This is used from attachments maintenance.
412
 * - It assumes the files have no message, no member information.
413
 * - It only removes the attachments and thumbnails from the database.
414
 *
415
 * @package Attachments
416
 * @param int[] $attach_ids
417
 */
418
function removeOrphanAttachments($attach_ids)
419
{
420
	$db = database();
421
422
	$db->query('', '
423
		DELETE FROM {db_prefix}attachments
424
		WHERE id_attach IN ({array_int:to_remove})',
425
		array(
426
			'to_remove' => $attach_ids,
427
		)
428
	);
429
430
	$db->query('', '
431
		UPDATE {db_prefix}attachments
432
			SET id_thumb = {int:no_thumb}
433
			WHERE id_thumb IN ({array_int:to_remove})',
434
			array(
435
				'to_remove' => $attach_ids,
436
				'no_thumb' => 0,
437
			)
438
		);
439
}
440
441
/**
442
 * Set or retrieve the size of an attachment.
443
 *
444
 * @package Attachments
445
 * @param int $attach_id
446
 * @param int|null $filesize = null
447
 */
448
function attachment_filesize($attach_id, $filesize = null)
449
{
450
	$db = database();
451
452
	if ($filesize === null)
453
	{
454
		$result = $db->query('', '
455
			SELECT size
456
			FROM {db_prefix}attachments
457
			WHERE id_attach = {int:id_attach}',
458
			array(
459
				'id_attach' => $attach_id,
460
			)
461
		);
462
		if (!empty($result))
463
		{
464
			list ($filesize) = $db->fetch_row($result);
465
			$db->free_result($result);
466
			return $filesize;
467
		}
468
		return false;
469
	}
470
	else
471
	{
472
		$db->query('', '
473
			UPDATE {db_prefix}attachments
474
			SET size = {int:filesize}
475
			WHERE id_attach = {int:id_attach}',
476
			array(
477
				'filesize' => $filesize,
478
				'id_attach' => $attach_id,
479
			)
480
		);
481
	}
482
}
483
484
/**
485
 * Set or retrieve the ID of the folder where an attachment is stored on disk.
486
 *
487
 * @package Attachments
488
 * @param int $attach_id
489
 * @param int|null $folder_id = null
490
 */
491
function attachment_folder($attach_id, $folder_id = null)
492
{
493
	$db = database();
494
495
	if ($folder_id === null)
496
	{
497
		$result = $db->query('', '
498
			SELECT id_folder
499
			FROM {db_prefix}attachments
500
			WHERE id_attach = {int:id_attach}',
501
			array(
502
				'id_attach' => $attach_id,
503
			)
504
		);
505
		if (!empty($result))
506
		{
507
			list ($folder_id) = $db->fetch_row($result);
508
			$db->free_result($result);
509
			return $folder_id;
510
		}
511
		return false;
512
	}
513
	else
514
	{
515
		$db->query('', '
516
			UPDATE {db_prefix}attachments
517
			SET id_folder = {int:new_folder}
518
			WHERE id_attach = {int:id_attach}',
519
			array(
520
				'new_folder' => $folder_id,
521
				'id_attach' => $attach_id,
522
			)
523
		);
524
	}
525
}
526
527
/**
528
 * Get the last attachment ID without a thumbnail.
529
 *
530
 * @package Attachments
531
 */
532
function maxNoThumb()
533
{
534
	$db = database();
535
536
	$result = $db->query('', '
537
		SELECT MAX(id_attach)
538
		FROM {db_prefix}attachments
539
		WHERE id_thumb != {int:no_thumb}',
540
		array(
541
			'no_thumb' => 0,
542
		)
543
	);
544
	list ($thumbnails) = $db->fetch_row($result);
545
	$db->free_result($result);
546
547
	return $thumbnails;
548
}
549
550
/**
551
 * Finds orphan thumbnails in the database
552
 *
553
 * - Checks in groups of 500
554
 * - Called by attachment maintenance
555
 * - If $fix_errors is set to true it will attempt to remove the thumbnail from disk
556
 *
557
 * @package Attachments
558
 * @param int $start
559
 * @param boolean $fix_errors
560
 * @param string[] $to_fix
561
 */
562
function findOrphanThumbnails($start, $fix_errors, $to_fix)
563
{
564
	$db = database();
565
566
	require_once(SUBSDIR . '/Attachments.subs.php');
567
568
	$result = $db->query('', '
569
		SELECT thumb.id_attach, thumb.id_folder, thumb.filename, thumb.file_hash
570
		FROM {db_prefix}attachments AS thumb
571
			LEFT JOIN {db_prefix}attachments AS tparent ON (tparent.id_thumb = thumb.id_attach)
572
		WHERE thumb.id_attach BETWEEN {int:substep} AND {int:substep} + 499
573
			AND thumb.attachment_type = {int:thumbnail}
574
			AND tparent.id_attach IS NULL',
575
		array(
576
			'thumbnail' => 3,
577
			'substep' => $start,
578
		)
579
	);
580
	$to_remove = array();
581
	if ($db->num_rows($result) != 0)
582
	{
583
		$to_fix[] = 'missing_thumbnail_parent';
584
		while ($row = $db->fetch_assoc($result))
585
		{
586
			// Only do anything once... just in case
587
			if (!isset($to_remove[$row['id_attach']]))
588
			{
589
				$to_remove[$row['id_attach']] = $row['id_attach'];
590
591
				// If we are repairing remove the file from disk now.
592
				if ($fix_errors && in_array('missing_thumbnail_parent', $to_fix))
593
				{
594
					$filename = getAttachmentFilename($row['filename'], $row['id_attach'], $row['id_folder'], false, $row['file_hash']);
595
					@unlink($filename);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

595
					/** @scrutinizer ignore-unhandled */ @unlink($filename);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
596
				}
597
			}
598
		}
599
	}
600
	$db->free_result($result);
601
602
	// Do we need to delete what we have?
603
	if ($fix_errors && !empty($to_remove) && in_array('missing_thumbnail_parent', $to_fix))
604
	{
605
		$db->query('', '
606
			DELETE FROM {db_prefix}attachments
607
			WHERE id_attach IN ({array_int:to_remove})
608
				AND attachment_type = {int:attachment_type}',
609
			array(
610
				'to_remove' => $to_remove,
611
				'attachment_type' => 3,
612
			)
613
		);
614
	}
615
616
	return $to_remove;
617
}
618
619
/**
620
 * Finds parents who thing they do have thumbnails, but don't
621
 *
622
 * - Checks in groups of 500
623
 * - Called by attachment maintenance
624
 * - If $fix_errors is set to true it will attempt to remove the thumbnail from disk
625
 *
626
 * @package Attachments
627
 * @param int $start
628
 * @param boolean $fix_errors
629
 * @param string[] $to_fix
630
 */
631
function findParentsOrphanThumbnails($start, $fix_errors, $to_fix)
632
{
633
	$db = database();
634
635
	$to_update = $db->fetchQueryCallback('
636
		SELECT a.id_attach
637
		FROM {db_prefix}attachments AS a
638
			LEFT JOIN {db_prefix}attachments AS thumb ON (thumb.id_attach = a.id_thumb)
639
		WHERE a.id_attach BETWEEN {int:substep} AND {int:substep} + 499
640
			AND a.id_thumb != {int:no_thumb}
641
			AND thumb.id_attach IS NULL',
642
		array(
643
			'no_thumb' => 0,
644
			'substep' => $start,
645
		),
646
		function ($row)
0 ignored issues
show
Bug introduced by
function(...) { /* ... */ } of type callable is incompatible with the type object|string expected by parameter $callback of Database::fetchQueryCallback(). ( Ignorable by Annotation )

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

646
		/** @scrutinizer ignore-type */ function ($row)
Loading history...
647
		{
648
			return $row['id_attach'];
649
		}
650
	);
651
652
	// Do we need to delete what we have?
653
	if ($fix_errors && !empty($to_update) && in_array('parent_missing_thumbnail', $to_fix))
654
	{
655
		$db->query('', '
656
			UPDATE {db_prefix}attachments
657
			SET id_thumb = {int:no_thumb}
658
			WHERE id_attach IN ({array_int:to_update})',
659
			array(
660
				'to_update' => $to_update,
661
				'no_thumb' => 0,
662
			)
663
		);
664
	}
665
666
	return $to_update;
667
}
668
669
/**
670
 * Goes thought all the attachments and checks that they exist
671
 *
672
 * - Goes in increments of 250
673
 * - if $fix_errors is true will remove empty files, update wrong filesizes in the DB and
674
 * - remove DB entries if the file can not be found.
675
 *
676
 * @package Attachments
677
 * @param int $start
678
 * @param boolean $fix_errors
679
 * @param string[] $to_fix
680
 */
681
function repairAttachmentData($start, $fix_errors, $to_fix)
682
{
683
	global $modSettings;
684
685
	$db = database();
686
687
	require_once(SUBSDIR . '/Attachments.subs.php');
688
689
	$repair_errors = array(
690
		'wrong_folder' => 0,
691
		'missing_extension' => 0,
692
		'file_missing_on_disk' => 0,
693
		'file_size_of_zero' => 0,
694
		'file_wrong_size' => 0
695
	);
696
697
	$result = $db->query('', '
698
		SELECT id_attach, id_folder, filename, file_hash, size, attachment_type
699
		FROM {db_prefix}attachments
700
		WHERE id_attach BETWEEN {int:substep} AND {int:substep} + 249',
701
		array(
702
			'substep' => $start,
703
		)
704
	);
705
	$to_remove = array();
706
	while ($row = $db->fetch_assoc($result))
707
	{
708
		// Get the filename.
709
		if ($row['attachment_type'] == 1)
710
			$filename = $modSettings['custom_avatar_dir'] . '/' . $row['filename'];
711
		else
712
			$filename = getAttachmentFilename($row['filename'], $row['id_attach'], $row['id_folder'], false, $row['file_hash']);
713
714
		// File doesn't exist?
715
		if (!file_exists($filename))
716
		{
717
			// If we're lucky it might just be in a different folder.
718
			if (!empty($modSettings['currentAttachmentUploadDir']))
719
			{
720
				// Get the attachment name without the folder.
721
				$attachment_name = !empty($row['file_hash']) ? $row['id_attach'] . '_' . $row['file_hash'] . '.elk' : getLegacyAttachmentFilename($row['filename'], $row['id_attach'], null, true);
722
723
				if (!is_array($modSettings['attachmentUploadDir']))
724
					$modSettings['attachmentUploadDir'] = Util::unserialize($modSettings['attachmentUploadDir']);
725
726
				// Loop through the other folders looking for this file
727
				foreach ($modSettings['attachmentUploadDir'] as $id => $dir)
728
				{
729
					if (file_exists($dir . '/' . $attachment_name))
730
					{
731
						$repair_errors['wrong_folder']++;
732
733
						// Are we going to fix this now?
734
						if ($fix_errors && in_array('wrong_folder', $to_fix))
735
							attachment_folder($row['id_attach'], $id);
736
737
						// Found it, on to the next attachment
738
						continue 2;
739
					}
740
				}
741
742
				if (!empty($row['file_hash']))
743
				{
744
					// It may be without the elk extension (something wrong during upgrade/conversion)
745
					$attachment_name = $row['id_attach'] . '_' . $row['file_hash'];
746
747
					if (!is_array($modSettings['attachmentUploadDir']))
748
						$modSettings['attachmentUploadDir'] = Util::unserialize($modSettings['attachmentUploadDir']);
749
750
					// Loop through the other folders looking for this file
751
					foreach ($modSettings['attachmentUploadDir'] as $id => $dir)
752
					{
753
						if (file_exists($dir . '/' . $attachment_name))
754
						{
755
							$repair_errors['missing_extension']++;
756
757
							// Are we going to fix this now?
758
							if ($fix_errors && in_array('missing_extension', $to_fix))
759
							{
760
								rename($dir . '/' . $attachment_name, $dir . '/' . $attachment_name . '.elk');
761
								attachment_folder($row['id_attach'], $id);
762
							}
763
764
							// Found it, on to the next attachment
765
							continue 2;
766
						}
767
					}
768
				}
769
			}
770
771
			// Could not find it anywhere
772
			$to_remove[] = $row['id_attach'];
773
			$repair_errors['file_missing_on_disk']++;
774
		}
775
		// An empty file on disk?
776
		elseif (filesize($filename) == 0)
777
		{
778
			$repair_errors['file_size_of_zero']++;
779
780
			// Fixing?
781
			if ($fix_errors && in_array('file_size_of_zero', $to_fix))
782
			{
783
				$to_remove[] = $row['id_attach'];
784
				@unlink($filename);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

784
				/** @scrutinizer ignore-unhandled */ @unlink($filename);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
785
			}
786
		}
787
		// Size listed and actual size are not the same?
788
		elseif (filesize($filename) != $row['size'])
789
		{
790
			$repair_errors['file_wrong_size']++;
791
792
			// Fix it here?
793
			if ($fix_errors && in_array('file_wrong_size', $to_fix))
794
				attachment_filesize($row['id_attach'], filesize($filename));
795
		}
796
	}
797
	$db->free_result($result);
798
799
	// Do we need to delete what we have?
800
	if ($fix_errors && !empty($to_remove) && in_array('file_missing_on_disk', $to_fix))
801
		removeOrphanAttachments($to_remove);
802
803
	return $repair_errors;
804
}
805
806
/**
807
 * Finds avatar files that are not assigned to any members
808
 *
809
 * - If $fix_errors is set, it will
810
 *
811
 * @package Attachments
812
 * @param int $start
813
 * @param boolean $fix_errors
814
 * @param string[] $to_fix
815
 */
816
function findOrphanAvatars($start, $fix_errors, $to_fix)
817
{
818
	global $modSettings;
819
820
	$db = database();
821
822
	require_once(SUBSDIR . '/Attachments.subs.php');
823
824
	$to_remove = $db->fetchQueryCallback('
825
		SELECT a.id_attach, a.id_folder, a.filename, a.file_hash, a.attachment_type
826
		FROM {db_prefix}attachments AS a
827
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = a.id_member)
828
		WHERE a.id_attach BETWEEN {int:substep} AND {int:substep} + 499
829
			AND a.id_member != {int:no_member}
830
			AND a.id_msg = {int:no_msg}
831
			AND mem.id_member IS NULL',
832
		array(
833
			'no_member' => 0,
834
			'no_msg' => 0,
835
			'substep' => $start,
836
		),
837
		function ($row) use ($fix_errors, $to_fix, $modSettings)
0 ignored issues
show
Bug introduced by
function(...) { /* ... */ } of type callable is incompatible with the type object|string expected by parameter $callback of Database::fetchQueryCallback(). ( Ignorable by Annotation )

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

837
		/** @scrutinizer ignore-type */ function ($row) use ($fix_errors, $to_fix, $modSettings)
Loading history...
838
		{
839
			// If we are repairing remove the file from disk now.
840
			if ($fix_errors && in_array('avatar_no_member', $to_fix))
841
			{
842
				if ($row['attachment_type'] == 1)
843
					$filename = $modSettings['custom_avatar_dir'] . '/' . $row['filename'];
844
				else
845
					$filename = getAttachmentFilename($row['filename'], $row['id_attach'], $row['id_folder'], false, $row['file_hash']);
846
				@unlink($filename);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

846
				/** @scrutinizer ignore-unhandled */ @unlink($filename);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
847
			}
848
849
			return $row['id_attach'];
850
		}
851
	);
852
853
	// Do we need to delete what we have?
854
	if ($fix_errors && !empty($to_remove) && in_array('avatar_no_member', $to_fix))
855
	{
856
		$db->query('', '
857
			DELETE FROM {db_prefix}attachments
858
			WHERE id_attach IN ({array_int:to_remove})
859
				AND id_member != {int:no_member}
860
				AND id_msg = {int:no_msg}',
861
			array(
862
				'to_remove' => $to_remove,
863
				'no_member' => 0,
864
				'no_msg' => 0,
865
			)
866
		);
867
	}
868
869
	return $to_remove;
870
}
871
872
/**
873
 * Finds attachments that are not used in any message
874
 *
875
 * @package Attachments
876
 * @param int $start
877
 * @param boolean $fix_errors
878
 * @param string[] $to_fix
879
 */
880
function findOrphanAttachments($start, $fix_errors, $to_fix)
881
{
882
	$db = database();
883
884
	require_once(SUBSDIR . '/Attachments.subs.php');
885
886
	$to_remove = $db->fetchQueryCallback('
887
		SELECT a.id_attach, a.id_folder, a.filename, a.file_hash
888
		FROM {db_prefix}attachments AS a
889
			LEFT JOIN {db_prefix}messages AS m ON (m.id_msg = a.id_msg)
890
		WHERE a.id_attach BETWEEN {int:substep} AND {int:substep} + 499
891
			AND a.id_member = {int:no_member}
892
			AND a.id_msg != {int:no_msg}
893
			AND m.id_msg IS NULL',
894
		array(
895
			'no_member' => 0,
896
			'no_msg' => 0,
897
			'substep' => $start,
898
		),
899
		function ($row) use ($fix_errors, $to_fix)
0 ignored issues
show
Bug introduced by
function(...) { /* ... */ } of type callable is incompatible with the type object|string expected by parameter $callback of Database::fetchQueryCallback(). ( Ignorable by Annotation )

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

899
		/** @scrutinizer ignore-type */ function ($row) use ($fix_errors, $to_fix)
Loading history...
900
		{
901
			// If we are repairing remove the file from disk now.
902
			if ($fix_errors && in_array('attachment_no_msg', $to_fix))
903
			{
904
				$filename = getAttachmentFilename($row['filename'], $row['id_attach'], $row['id_folder'], false, $row['file_hash']);
905
				@unlink($filename);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

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

905
				/** @scrutinizer ignore-unhandled */ @unlink($filename);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
906
			}
907
908
			return $row['id_attach'];
909
		}
910
	);
911
912
	// Do we need to delete what we have?
913
	if ($fix_errors && !empty($to_remove) && in_array('attachment_no_msg', $to_fix))
914
	{
915
		$db->query('', '
916
			DELETE FROM {db_prefix}attachments
917
			WHERE id_attach IN ({array_int:to_remove})
918
				AND id_member = {int:no_member}
919
				AND id_msg != {int:no_msg}',
920
			array(
921
				'to_remove' => $to_remove,
922
				'no_member' => 0,
923
				'no_msg' => 0,
924
			)
925
		);
926
	}
927
928
	return $to_remove;
929
}
930
931
/**
932
 * Get the max attachment ID which is a thumbnail.
933
 *
934
 * @package Attachments
935
 */
936
function getMaxThumbnail()
937
{
938
	$db = database();
939
940
	$result = $db->query('', '
941
		SELECT MAX(id_attach)
942
		FROM {db_prefix}attachments
943
		WHERE attachment_type = {int:thumbnail}',
944
		array(
945
			'thumbnail' => 3,
946
		)
947
	);
948
	list ($thumbnail) = $db->fetch_row($result);
949
	$db->free_result($result);
950
951
	return $thumbnail;
952
}
953
954
/**
955
 * Get the max attachment ID.
956
 *
957
 * @package Attachments
958
 */
959
function maxAttachment()
960
{
961
	$db = database();
962
963
	$result = $db->query('', '
964
		SELECT MAX(id_attach)
965
		FROM {db_prefix}attachments',
966
		array(
967
		)
968
	);
969
	list ($attachment) = $db->fetch_row($result);
970
	$db->free_result($result);
971
972
	return $attachment;
973
}
974
975
/**
976
 * Check multiple attachments IDs against the database.
977
 *
978
 * @package Attachments
979
 * @param int[] $attachments
980
 * @param string $approve_query
981
 */
982
function validateAttachments($attachments, $approve_query)
983
{
984
	$db = database();
985
986
	// double check the attachments array, pick only what is returned from the database
987
	return $db->fetchQueryCallback('
988
		SELECT a.id_attach, m.id_board, m.id_msg, m.id_topic
989
		FROM {db_prefix}attachments AS a
990
			INNER JOIN {db_prefix}messages AS m ON (m.id_msg = a.id_msg)
991
			LEFT JOIN {db_prefix}boards AS b ON (m.id_board = b.id_board)
992
		WHERE a.id_attach IN ({array_int:attachments})
993
			AND a.approved = {int:not_approved}
994
			AND a.attachment_type = {int:attachment_type}
995
			AND {query_see_board}
996
			' . $approve_query,
997
		array(
998
			'attachments' => $attachments,
999
			'not_approved' => 0,
1000
			'attachment_type' => 0,
1001
		),
1002
		function ($row)
0 ignored issues
show
Bug introduced by
function(...) { /* ... */ } of type callable is incompatible with the type object|string expected by parameter $callback of Database::fetchQueryCallback(). ( Ignorable by Annotation )

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

1002
		/** @scrutinizer ignore-type */ function ($row)
Loading history...
1003
		{
1004
			return $row['id_attach'];
1005
		}
1006
	);
1007
}
1008
1009
/**
1010
 * Finds an attachments parent topic/message and returns the values in an array
1011
 *
1012
 * @package Attachments
1013
 * @param int $attachment
1014
 */
1015
function attachmentBelongsTo($attachment)
1016
{
1017
	$db = database();
1018
1019
	$request = $db->query('', '
1020
		SELECT a.id_attach, m.id_board, m.id_msg, m.id_topic
1021
		FROM {db_prefix}attachments AS a
1022
			INNER JOIN {db_prefix}messages AS m ON (m.id_msg = a.id_msg)
1023
			LEFT JOIN {db_prefix}boards AS b ON (m.id_board = b.id_board)
1024
		WHERE a.id_attach = ({int:attachment})
1025
			AND a.attachment_type = {int:attachment_type}
1026
			AND {query_see_board}
1027
		LIMIT 1',
1028
		array(
1029
			'attachment' => $attachment,
1030
			'attachment_type' => 0,
1031
		)
1032
	);
1033
	$attachment = $db->fetch_assoc($request);
1034
	$db->free_result($request);
1035
1036
	return $attachment;
1037
}
1038
1039
/**
1040
 * Checks an attachments id
1041
 *
1042
 * @package Attachments
1043
 * @param int $id_attach
1044
 * @return boolean
1045
 */
1046
function validateAttachID($id_attach)
1047
{
1048
	$db = database();
1049
1050
	$request = $db->query('', '
1051
		SELECT id_attach
1052
		FROM {db_prefix}attachments
1053
		WHERE id_attach = {int:attachment_id}
1054
		LIMIT 1',
1055
		array(
1056
			'attachment_id' => $id_attach,
1057
		)
1058
	);
1059
	$count = $db->num_rows($request);
1060
	$db->free_result($request);
1061
1062
	return ($count == 0) ? false : true;
1063
}
1064
1065
/**
1066
 * Callback function for action_unapproved_attachments
1067
 *
1068
 * - retrieve all the attachments waiting for approval the approver can approve
1069
 *
1070
 * @package Attachments
1071
 * @param int $start The item to start with (for pagination purposes)
1072
 * @param int $items_per_page  The number of items to show per page
1073
 * @param string $sort A string indicating how to sort the results
1074
 * @param string $approve_query additional restrictions based on the boards the approver can see
1075
 * @return mixed[] an array of unapproved attachments
1076
 */
1077
function list_getUnapprovedAttachments($start, $items_per_page, $sort, $approve_query)
1078
{
1079
	global $scripturl;
1080
1081
	$db = database();
1082
1083
	$bbc_parser = \BBC\ParserWrapper::instance();
1084
1085
	// Get all unapproved attachments.
1086
	return $db->fetchQueryCallback('
1087
		SELECT a.id_attach, a.filename, a.size, m.id_msg, m.id_topic, m.id_board, m.subject, m.body, m.id_member,
1088
			COALESCE(mem.real_name, m.poster_name) AS poster_name, m.poster_time,
1089
			t.id_member_started, t.id_first_msg, b.name AS board_name, c.id_cat, c.name AS cat_name
1090
		FROM {db_prefix}attachments AS a
1091
			INNER JOIN {db_prefix}messages AS m ON (m.id_msg = a.id_msg)
1092
			INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic)
1093
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board)
1094
			LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)
1095
			LEFT JOIN {db_prefix}categories AS c ON (c.id_cat = b.id_cat)
1096
		WHERE a.approved = {int:not_approved}
1097
			AND a.attachment_type = {int:attachment_type}
1098
			AND {query_see_board}
1099
			{raw:approve_query}
1100
		ORDER BY {raw:sort}
1101
		LIMIT {int:start}, {int:items_per_page}',
1102
		array(
1103
			'not_approved' => 0,
1104
			'attachment_type' => 0,
1105
			'start' => $start,
1106
			'sort' => $sort,
1107
			'items_per_page' => $items_per_page,
1108
			'approve_query' => $approve_query,
1109
		),
1110
		function ($row) use ($scripturl, $bbc_parser)
0 ignored issues
show
Bug introduced by
function(...) { /* ... */ } of type callable is incompatible with the type object|string expected by parameter $callback of Database::fetchQueryCallback(). ( Ignorable by Annotation )

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

1110
		/** @scrutinizer ignore-type */ function ($row) use ($scripturl, $bbc_parser)
Loading history...
1111
		{
1112
			return array(
1113
				'id' => $row['id_attach'],
1114
				'filename' => $row['filename'],
1115
				'size' => round($row['size'] / 1024, 2),
1116
				'time' => standardTime($row['poster_time']),
1117
				'html_time' => htmlTime($row['poster_time']),
1118
				'timestamp' => forum_time(true, $row['poster_time']),
1119
				'poster' => array(
1120
					'id' => $row['id_member'],
1121
					'name' => $row['poster_name'],
1122
					'link' => $row['id_member'] ? '<a href="' . $scripturl . '?action=profile;u=' . $row['id_member'] . '">' . $row['poster_name'] . '</a>' : $row['poster_name'],
1123
					'href' => $scripturl . '?action=profile;u=' . $row['id_member'],
1124
				),
1125
				'message' => array(
1126
					'id' => $row['id_msg'],
1127
					'subject' => $row['subject'],
1128
					'body' => $bbc_parser->parseMessage($row['body'], false),
1129
					'time' => standardTime($row['poster_time']),
1130
					'html_time' => htmlTime($row['poster_time']),
1131
					'timestamp' => forum_time(true, $row['poster_time']),
1132
					'href' => $scripturl . '?topic=' . $row['id_topic'] . '.msg' . $row['id_msg'] . '#msg' . $row['id_msg'],
1133
				),
1134
				'topic' => array(
1135
					'id' => $row['id_topic'],
1136
				),
1137
				'board' => array(
1138
					'id' => $row['id_board'],
1139
					'name' => $row['board_name'],
1140
				),
1141
				'category' => array(
1142
					'id' => $row['id_cat'],
1143
					'name' => $row['cat_name'],
1144
				),
1145
			);
1146
		}
1147
	);
1148
}
1149
1150
/**
1151
 * Callback function for action_unapproved_attachments
1152
 *
1153
 * - count all the attachments waiting for approval that this approver can approve
1154
 *
1155
 * @package Attachments
1156
 * @param string $approve_query additional restrictions based on the boards the approver can see
1157
 * @return int the number of unapproved attachments
1158
 */
1159
function list_getNumUnapprovedAttachments($approve_query)
1160
{
1161
	$db = database();
1162
1163
	// How many unapproved attachments in total?
1164
	$request = $db->query('', '
1165
		SELECT COUNT(*)
1166
		FROM {db_prefix}attachments AS a
1167
			INNER JOIN {db_prefix}messages AS m ON (m.id_msg = a.id_msg)
1168
			INNER JOIN {db_prefix}boards AS b ON (b.id_board = m.id_board)
1169
		WHERE a.approved = {int:not_approved}
1170
			AND a.attachment_type = {int:attachment_type}
1171
			AND {query_see_board}
1172
			' . $approve_query,
1173
		array(
1174
			'not_approved' => 0,
1175
			'attachment_type' => 0,
1176
		)
1177
	);
1178
	list ($total_unapproved_attachments) = $db->fetch_row($request);
1179
	$db->free_result($request);
1180
1181
	return $total_unapproved_attachments;
1182
}
1183
1184
/**
1185
 * Prepare the actual attachment directories to be displayed in the list.
1186
 *
1187
 * - Callback function for createList().
1188
 *
1189
 * @package Attachments
1190
 */
1191
function list_getAttachDirs()
1192
{
1193
	global $modSettings, $context, $txt, $scripturl;
1194
1195
	$db = database();
1196
1197
	$request = $db->query('', '
1198
		SELECT id_folder, COUNT(id_attach) AS num_attach, SUM(size) AS size_attach
1199
		FROM {db_prefix}attachments
1200
		WHERE attachment_type != {int:type}
1201
		GROUP BY id_folder',
1202
		array(
1203
			'type' => 1,
1204
		)
1205
	);
1206
	$expected_files = array();
1207
	$expected_size = array();
1208
	while ($row = $db->fetch_assoc($request))
1209
	{
1210
		$expected_files[$row['id_folder']] = $row['num_attach'];
1211
		$expected_size[$row['id_folder']] = $row['size_attach'];
1212
	}
1213
	$db->free_result($request);
1214
1215
	$attachdirs = array();
1216
	foreach ($modSettings['attachmentUploadDir'] as $id => $dir)
1217
	{
1218
		// If there aren't any attachments in this directory this won't exist.
1219
		if (!isset($expected_files[$id]))
1220
			$expected_files[$id] = 0;
1221
1222
		// Check if the directory is doing okay.
1223
		list ($status, $error, $files) = attachDirStatus($dir, $expected_files[$id]);
1224
1225
		// If it is one, let's show that it's a base directory.
1226
		$sub_dirs = 0;
1227
		$is_base_dir = false;
1228
		if (!empty($modSettings['attachment_basedirectories']))
1229
		{
1230
			$is_base_dir = in_array($dir, $modSettings['attachment_basedirectories']);
1231
1232
			// Count any sub-folders.
1233
			foreach ($modSettings['attachmentUploadDir'] as $sid => $sub)
1234
				if (strpos($sub, $dir . DIRECTORY_SEPARATOR) !== false)
1235
				{
1236
					$expected_files[$id]++;
1237
					$sub_dirs++;
1238
				}
1239
		}
1240
1241
		$attachdirs[] = array(
1242
			'id' => $id,
1243
			'current' => $id == $modSettings['currentAttachmentUploadDir'],
1244
			'disable_current' => isset($modSettings['automanage_attachments']) && $modSettings['automanage_attachments'] > 1,
1245
			'disable_base_dir' =>  $is_base_dir && $sub_dirs > 0 && !empty($files) && empty($error),
1246
			'path' => $dir,
1247
			'current_size' => !empty($expected_size[$id]) ? comma_format($expected_size[$id] / 1024, 0) : 0,
1248
			'num_files' => comma_format($expected_files[$id] - $sub_dirs, 0) . ($sub_dirs > 0 ? ' (' . $sub_dirs . ')' : ''),
1249
			'status' => ($is_base_dir ? $txt['attach_dir_basedir'] . '<br />' : '') . ($error ? '<div class="error">' : '') . str_replace('{repair_url}', $scripturl . '?action=admin;area=manageattachments;sa=repair;' . $context['session_var'] . '=' . $context['session_id'], $txt['attach_dir_' . $status]) . ($error ? '</div>' : ''),
1250
		);
1251
	}
1252
1253
	// Just stick a new directory on at the bottom.
1254
	if (isset($_REQUEST['new_path']))
1255
		$attachdirs[] = array(
1256
			'id' => max(array_merge(array_keys($expected_files), array_keys($modSettings['attachmentUploadDir']))) + 1,
1257
			'current' => false,
1258
			'path' => '',
1259
			'current_size' => '',
1260
			'num_files' => '',
1261
			'status' => '',
1262
		);
1263
1264
	return $attachdirs;
1265
}
1266
1267
/**
1268
 * Checks the status of an attachment directory and returns an array
1269
 * of the status key, if that status key signifies an error, and the file count.
1270
 *
1271
 * @package Attachments
1272
 * @param string $dir
1273
 * @param int $expected_files
1274
 */
1275
function attachDirStatus($dir, $expected_files)
1276
{
1277
	if (!is_dir($dir))
1278
		return array('does_not_exist', true, '');
1279
	elseif (!is_writable($dir))
1280
		return array('not_writable', true, '');
1281
1282
	// Count the files with a glob, easier and less time consuming
1283
	$glob = new GlobIterator($dir . '/*.elk', FilesystemIterator::SKIP_DOTS);
1284
	try
1285
	{
1286
		$num_files = $glob->count();
1287
	}
1288
	catch (\LogicException $e)
1289
	{
1290
		$num_files = count(iterator_to_array($glob));
1291
	}
1292
1293
	if ($num_files < $expected_files)
1294
		return array('files_missing', true, $num_files);
1295
	// Empty?
1296
	elseif ($expected_files == 0)
1297
		return array('unused', false, $num_files);
1298
	// All good!
1299
	else
1300
		return array('ok', false, $num_files);
1301
}
1302
1303
/**
1304
 * Prepare the base directories to be displayed in a list.
1305
 *
1306
 * - Callback function for createList().
1307
 *
1308
 * @package Attachments
1309
 */
1310
function list_getBaseDirs()
1311
{
1312
	global $modSettings, $txt;
1313
1314
	if (empty($modSettings['attachment_basedirectories']))
1315
		return;
1316
1317
	// Get a list of the base directories.
1318
	$basedirs = array();
1319
	foreach ($modSettings['attachment_basedirectories'] as $id => $dir)
1320
	{
1321
		// Loop through the attach directory array to count any sub-directories
1322
		$expected_dirs = 0;
1323
		foreach ($modSettings['attachmentUploadDir'] as $sid => $sub)
1324
			if (strpos($sub, $dir . DIRECTORY_SEPARATOR) !== false)
1325
				$expected_dirs++;
1326
1327
		if (!is_dir($dir))
1328
			$status = 'does_not_exist';
1329
		elseif (!is_writeable($dir))
1330
			$status = 'not_writable';
1331
		else
1332
			$status = 'ok';
1333
1334
		$basedirs[] = array(
1335
			'id' => $id,
1336
			'current' => $dir == $modSettings['basedirectory_for_attachments'],
1337
			'path' => $expected_dirs > 0 ? $dir : ('<input type="text" name="base_dir[' . $id . ']" value="' . $dir . '" size="40" />'),
1338
			'num_dirs' => $expected_dirs,
1339
			'status' => $status == 'ok' ? $txt['attach_dir_ok'] : ('<span class="error">' . $txt['attach_dir_' . $status] . '</span>'),
1340
		);
1341
	}
1342
1343
	if (isset($_REQUEST['new_base_path']))
1344
		$basedirs[] = array(
1345
			'id' => '',
1346
			'current' => false,
1347
			'path' => '<input type="text" name="new_base_dir" value="" size="40" />',
1348
			'num_dirs' => '',
1349
			'status' => '',
1350
		);
1351
1352
	return $basedirs;
1353
}
1354
1355
/**
1356
 * Return the number of files of the specified type recorded in the database.
1357
 *
1358
 * - (the specified type being attachments or avatars).
1359
 * - Callback function for createList()
1360
 *
1361
 * @package Attachments
1362
 * @param string $browse_type can be one of 'avatars' or not. (in which case they're attachments)
1363
 */
1364
function list_getNumFiles($browse_type)
1365
{
1366
	$db = database();
1367
1368
	// Depending on the type of file, different queries are used.
1369
	if ($browse_type === 'avatars')
1370
	{
1371
		$request = $db->query('', '
1372
			SELECT COUNT(*)
1373
			FROM {db_prefix}attachments
1374
			WHERE id_member != {int:guest_id_member}',
1375
			array(
1376
				'guest_id_member' => 0,
1377
			)
1378
		);
1379
	}
1380
	else
1381
	{
1382
		$request = $db->query('', '
1383
			SELECT COUNT(*) AS num_attach
1384
			FROM {db_prefix}attachments AS a
1385
				INNER JOIN {db_prefix}messages AS m ON (m.id_msg = a.id_msg)
1386
				INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic)
1387
				INNER JOIN {db_prefix}messages AS mf ON (mf.id_msg = t.id_first_msg)
1388
			WHERE a.attachment_type = {int:attachment_type}
1389
				AND a.id_member = {int:guest_id_member}',
1390
			array(
1391
				'attachment_type' => $browse_type === 'thumbs' ? '3' : '0',
1392
				'guest_id_member' => 0,
1393
			)
1394
		);
1395
	}
1396
1397
	list ($num_files) = $db->fetch_row($request);
1398
	$db->free_result($request);
1399
1400
	return $num_files;
1401
}
1402
1403
/**
1404
 * Returns the list of attachments files (avatars or not), recorded
1405
 * in the database, per the parameters received.
1406
 *
1407
 * - Callback function for createList()
1408
 *
1409
 * @package Attachments
1410
 * @param int $start The item to start with (for pagination purposes)
1411
 * @param int $items_per_page  The number of items to show per page
1412
 * @param string $sort A string indicating how to sort the results
1413
 * @param string $browse_type can be on eof 'avatars' or ... not. :P
1414
 */
1415
function list_getFiles($start, $items_per_page, $sort, $browse_type)
1416
{
1417
	global $txt;
1418
1419
	$db = database();
1420
1421
	// Choose a query depending on what we are viewing.
1422
	if ($browse_type === 'avatars')
1423
	{
1424
		return $db->fetchQuery('
1425
			SELECT
1426
				{string:blank_text} AS id_msg, COALESCE(mem.real_name, {string:not_applicable_text}) AS poster_name,
1427
				mem.last_login AS poster_time, 0 AS id_topic, a.id_member, a.id_attach, a.filename, a.file_hash, a.attachment_type,
1428
				a.size, a.width, a.height, a.downloads, {string:blank_text} AS subject, 0 AS id_board
1429
			FROM {db_prefix}attachments AS a
1430
				LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = a.id_member)
1431
			WHERE a.id_member != {int:guest_id}
1432
			ORDER BY {raw:sort}
1433
			LIMIT {int:start}, {int:per_page}',
1434
			array(
1435
				'guest_id' => 0,
1436
				'blank_text' => '',
1437
				'not_applicable_text' => $txt['not_applicable'],
1438
				'sort' => $sort,
1439
				'start' => $start,
1440
				'per_page' => $items_per_page,
1441
			)
1442
		);
1443
	}
1444
	else
1445
	{
1446
		return $db->fetchQuery('
1447
			SELECT
1448
				m.id_msg, COALESCE(mem.real_name, m.poster_name) AS poster_name, m.poster_time, m.id_topic, m.id_member,
1449
				a.id_attach, a.filename, a.file_hash, a.attachment_type, a.size, a.width, a.height, a.downloads, mf.subject, t.id_board
1450
			FROM {db_prefix}attachments AS a
1451
				INNER JOIN {db_prefix}messages AS m ON (m.id_msg = a.id_msg)
1452
				INNER JOIN {db_prefix}topics AS t ON (t.id_topic = m.id_topic)
1453
				INNER JOIN {db_prefix}messages AS mf ON (mf.id_msg = t.id_first_msg)
1454
				LEFT JOIN {db_prefix}members AS mem ON (mem.id_member = m.id_member)
1455
			WHERE a.attachment_type = {int:attachment_type}
1456
			ORDER BY {raw:sort}
1457
			LIMIT {int:start}, {int:per_page}',
1458
			array(
1459
				'attachment_type' => $browse_type == 'thumbs' ? '3' : '0',
1460
				'sort' => $sort,
1461
				'start' => $start,
1462
				'per_page' => $items_per_page,
1463
			)
1464
		);
1465
	}
1466
}
1467
1468
/**
1469
 * Return the overall attachments size
1470
 *
1471
 * @package Attachments
1472
 */
1473
function overallAttachmentsSize()
1474
{
1475
	$db = database();
1476
1477
	// Check the size of all the directories.
1478
	$request = $db->query('', '
1479
		SELECT SUM(size)
1480
		FROM {db_prefix}attachments
1481
		WHERE attachment_type != {int:type}',
1482
		array(
1483
			'type' => 1,
1484
		)
1485
	);
1486
	list ($attachmentDirSize) = $db->fetch_row($request);
1487
	$db->free_result($request);
1488
1489
	return byte_format($attachmentDirSize);
1490
}
1491
1492
/**
1493
 * Get files and size from the current attachments dir
1494
 *
1495
 * @package Attachments
1496
 */
1497
function currentAttachDirProperties()
1498
{
1499
	global $modSettings;
1500
1501
	return attachDirProperties($modSettings['currentAttachmentUploadDir']);
1502
}
1503
1504
/**
1505
 * Get files and size from the current attachments dir
1506
 *
1507
 * @package Attachments
1508
 * @param string $dir
1509
 */
1510
function attachDirProperties($dir)
1511
{
1512
	$db = database();
1513
1514
	$current_dir = array();
1515
1516
	$request = $db->query('', '
1517
		SELECT COUNT(*), SUM(size)
1518
		FROM {db_prefix}attachments
1519
		WHERE id_folder = {int:folder_id}
1520
			AND attachment_type != {int:type}',
1521
		array(
1522
			'folder_id' => $dir,
1523
			'type' => 1,
1524
		)
1525
	);
1526
	list ($current_dir['files'], $current_dir['size']) = $db->fetch_row($request);
1527
	$db->free_result($request);
1528
1529
	return $current_dir;
1530
}
1531
1532
/**
1533
 * Move avatars to their new directory.
1534
 *
1535
 * @package Attachments
1536
 */
1537
function moveAvatars()
1538
{
1539
	global $modSettings;
1540
1541
	$db = database();
1542
1543
	require_once(SUBSDIR . '/Attachments.subs.php');
1544
1545
	$request = $db->query('', '
1546
		SELECT id_attach, id_folder, id_member, filename, file_hash
1547
		FROM {db_prefix}attachments
1548
		WHERE attachment_type = {int:attachment_type}
1549
			AND id_member > {int:guest_id_member}',
1550
		array(
1551
			'attachment_type' => 0,
1552
			'guest_id_member' => 0,
1553
		)
1554
	);
1555
	$updatedAvatars = array();
1556
	while ($row = $db->fetch_assoc($request))
1557
	{
1558
		$filename = getAttachmentFilename($row['filename'], $row['id_attach'], $row['id_folder'], false, $row['file_hash']);
1559
1560
		if (rename($filename, $modSettings['custom_avatar_dir'] . '/' . $row['filename']))
1561
			$updatedAvatars[] = $row['id_attach'];
1562
	}
1563
	$db->free_result($request);
1564
1565
	if (!empty($updatedAvatars))
1566
		$db->query('', '
1567
			UPDATE {db_prefix}attachments
1568
			SET attachment_type = {int:attachment_type}
1569
			WHERE id_attach IN ({array_int:updated_avatars})',
1570
			array(
1571
				'updated_avatars' => $updatedAvatars,
1572
				'attachment_type' => 1,
1573
			)
1574
		);
1575
}
1576
1577
/**
1578
 * Select a group of attachments to move to a new destination
1579
 *
1580
 * Used by maintenance transfer attachments
1581
 * Returns number found and array of details
1582
 *
1583
 * @param string $from source location
1584
 * @param int $start
1585
 * @param int $limit
1586
 */
1587
function findAttachmentsToMove($from, $start, $limit)
1588
{
1589
	$db = database();
1590
1591
	// Find some attachments to move
1592
	$attachments = $db->fetchQuery('
1593
		SELECT id_attach, filename, id_folder, file_hash, size
1594
		FROM {db_prefix}attachments
1595
		WHERE id_folder = {int:folder}
1596
			AND attachment_type != {int:attachment_type}
1597
		LIMIT {int:start}, {int:limit}',
1598
		array(
1599
			'folder' => $from,
1600
			'attachment_type' => 1,
1601
			'start' => $start,
1602
			'limit' => $limit,
1603
		)
1604
	);
1605
	$number = count($attachments);
1606
1607
	return array($number, $attachments);
1608
}
1609
1610
/**
1611
 * Update the database to reflect the new directory of an array of attachments
1612
 *
1613
 * @param int[] $moved integer array of attachment ids
1614
 * @param string $new_dir new directory string
1615
 */
1616
function moveAttachments($moved, $new_dir)
1617
{
1618
	$db = database();
1619
1620
	// Update the database
1621
	$db->query('', '
1622
		UPDATE {db_prefix}attachments
1623
		SET id_folder = {int:new}
1624
		WHERE id_attach IN ({array_int:attachments})',
1625
		array(
1626
			'attachments' => $moved,
1627
			'new' => $new_dir,
1628
		)
1629
	);
1630
}
1631
1632
/**
1633
 * Extend the message body with a removal message.
1634
 *
1635
 * @package Attachments
1636
 * @param int[] $messages array of message id's to update
1637
 * @param string $notice notice to add
1638
 */
1639
function setRemovalNotice($messages, $notice)
1640
{
1641
	$db = database();
1642
1643
	$db->query('', '
1644
		UPDATE {db_prefix}messages
1645
		SET body = CONCAT(body, {string:notice})
1646
		WHERE id_msg IN ({array_int:messages})',
1647
		array(
1648
			'messages' => $messages,
1649
			'notice' => '<br /><br />' . $notice,
1650
		)
1651
	);
1652
}
1653
1654
/**
1655
 * Finds all the attachments of a single message.
1656
 *
1657
 * @package Attachments
1658
 * @param int $id_msg
1659
 * @param bool $unapproved if true returns also the unapproved attachments (default false)
1660
 * @todo $unapproved may be superfluous
1661
 * @return array
1662
 */
1663
function attachmentsOfMessage($id_msg, $unapproved = false)
1664
{
1665
	$db = database();
1666
1667
	return $db->fetchQueryCallback('
1668
		SELECT id_attach
1669
		FROM {db_prefix}attachments
1670
		WHERE id_msg = {int:id_msg}' . ($unapproved ? '' : '
1671
			AND approved = {int:is_approved}') . '
1672
			AND attachment_type = {int:attachment_type}',
1673
		array(
1674
			'id_msg' => $id_msg,
1675
			'is_approved' => 0,
1676
			'attachment_type' => 0,
1677
		),
1678
		function ($row)
0 ignored issues
show
Bug introduced by
function(...) { /* ... */ } of type callable is incompatible with the type object|string expected by parameter $callback of Database::fetchQueryCallback(). ( Ignorable by Annotation )

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

1678
		/** @scrutinizer ignore-type */ function ($row)
Loading history...
1679
		{
1680
			return $row['id_attach'];
1681
		}
1682
	);
1683
}
1684
1685
/**
1686
 * Counts attachments for the given folder.
1687
 *
1688
 * @package Attachments
1689
 * @param int $id_folder
1690
 */
1691
function countAttachmentsInFolders($id_folder)
1692
{
1693
	$db = database();
1694
1695
	// Let's not try to delete a path with files in it.
1696
	$request = $db->query('', '
1697
		SELECT COUNT(id_attach) AS num_attach
1698
		FROM {db_prefix}attachments
1699
		WHERE id_folder = {int:id_folder}',
1700
		array(
1701
			'id_folder' => $id_folder,
1702
		)
1703
	);
1704
	list ($num_attach) = $db->fetch_row($request);
1705
	$db->free_result($request);
1706
1707
	return $num_attach;
1708
}
1709
1710
/**
1711
 * Changes the folder id of all the attachments in a certain folder
1712
 *
1713
 * @package Attachments
1714
 * @param int $from - the folder the attachments are in
1715
 * @param int $to - the folder the attachments should be moved to
1716
 */
1717
function updateAttachmentIdFolder($from, $to)
1718
{
1719
	$db = database();
1720
1721
	$db->query('', '
1722
		UPDATE {db_prefix}attachments
1723
		SET id_folder = {int:folder_to}
1724
		WHERE id_folder = {int:folder_from}',
1725
		array(
1726
			'folder_from' => $from,
1727
			'folder_to' => $to,
1728
		)
1729
	);
1730
}
1731
1732
/**
1733
 * Validates the current user can remove a specified attachment
1734
 *
1735
 * - Has moderator / admin manage_attachments permission
1736
 * - Message is not locked, they have attach permissions and meets one of the following:
1737
 *    - Has modify any permission
1738
 *    - Is the owner of the message and within edit_disable_time
1739
 *    - Is allowed to edit messages in a thread they started
1740
 *
1741
 * @param int $id_attach
1742
 * @param int $id_member_requesting
1743
 *
1744
 * @return bool
1745
 */
1746
function canRemoveAttachment($id_attach, $id_member_requesting)
1747
{
1748
	global $modSettings;
1749
1750
	if (allowedTo('manage_attachments'))
1751
	{
1752
		return true;
1753
	}
1754
1755
	$db = database();
1756
	$request = $db->query('', '
1757
		SELECT 
1758
			m.id_board, m.id_member, m.approved, m.poster_time,
1759
			t.locked, t.id_member_started
1760
		FROM {db_prefix}attachments as a
1761
			LEFT JOIN {db_prefix}messages AS m ON m.id_msg = a.id_msg
1762
			LEFT JOIN {db_prefix}topics AS t ON t.id_topic = m.id_topic
1763
		WHERE a.id_attach = {int:id_attach}',
1764
		array(
1765
			'id_attach' => $id_attach,
1766
		)
1767
	);
1768
	if ($db->num_rows($request) != 0)
1769
	{
1770
		list($id_board, $id_member, $approved, $poster_time, $is_locked, $id_starter,) = $db->fetch_row($request);
1771
1772
		$is_owner = $id_member_requesting == $id_member;
1773
		$is_starter = $id_member_requesting == $id_starter;
1774
		$can_attach = allowedTo('post_attachment', $id_board) || ($modSettings['postmod_active'] && allowedTo('post_unapproved_attachments', $id_board));
1775
		$can_modify = (!$is_locked || allowedTo('moderate_board', $id_board))
1776
			&& (
1777
				allowedTo('modify_any', $id_board)
1778
				|| (allowedTo('modify_replies', $id_board) && $is_starter)
1779
				|| (allowedTo('modify_own', $id_board) && $is_owner && (empty($modSettings['edit_disable_time']) || !$approved || $poster_time + $modSettings['edit_disable_time'] * 60 > time()))
1780
			);
1781
1782
		$db->free_result($request);
1783
		return $can_attach && $can_modify;
1784
	}
1785
1786
	return false;
1787
}
1788