AttachmentsDisplay::getAttachmentsArray()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Handles the job of attachment directory management.
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * @version 2.0 dev
11
 *
12
 */
13
14
namespace ElkArte\Attachments;
15
16
use ElkArte\User;
17
18
/**
19
 * Class AttachmentsDisplay
20
 */
21
class AttachmentsDisplay
22
{
23
	/** @var array The good old attachments array */
24
	public array $attachments = [];
25
26
	/** @var array The message array */
27
	protected array $messages = [];
28
29
	/** @var bool If unapproved posts/attachments should be shown */
30
	protected bool $includeUnapproved = false;
31
32
	/**
33
	 * @param int[] $messages
34
	 * @param int[] $posters
35
	 * @param bool $includeUnapproved
36
	 */
37
	public function __construct(array $messages, array $posters, bool $includeUnapproved)
38
	{
39
		$this->messages = $messages;
40
		$this->includeUnapproved = $includeUnapproved;
41
42
		// Fetch attachments.
43
		if (allowedTo('view_attachments'))
44
		{
45
			// Reminder: this should not be necessary, it removes the current user from the list of posters if not present among the actual list of posters
46
			if (isset($posters[-1]))
47
			{
48
				unset($posters[-1]);
49
			}
50
51
			// The filter returns false when:
52
			//  - the attachment is unapproved, and
53
			//  - the viewer is not the poster of the message where the attachment is
54
			$this->getAttachments(
55
				$this->messages,
56
				$this->includeUnapproved,
57
				static fn($attachment_info, $all_posters) => !(!$attachment_info['approved'] && (!isset($all_posters[$attachment_info['id_msg']]) || $all_posters[$attachment_info['id_msg']] !== User::$info->id)),
0 ignored issues
show
Bug Best Practice introduced by
The property id does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
58
				$posters
59
			);
60
		}
61
	}
62
63
	/**
64
	 * Get all attachments associated with a set of posts.
65
	 *
66
	 * What it does:
67
	 *  - This does not check permissions.
68
	 *
69
	 * @param int[] $messages array of messages ids
70
	 * @param bool $includeUnapproved = false
71
	 * @param callable|null $filter name of a callback function
72
	 * @param array $all_posters
73
	 *
74
	 */
75
	protected function getAttachments(array $messages, bool $includeUnapproved = false, callable $filter = null, array $all_posters = []): void
76
	{
77
		global $modSettings;
78
79
		$db = database();
80
81
		// Nothing to do
82
		if ($messages === [])
83
		{
84
			$this->attachments = [];
85
			return;
86
		}
87
88
		$attachments = [];
89
		$db->fetchQuery('
90
			SELECT
91
				a.id_attach, a.id_folder, a.id_msg, a.filename, a.file_hash, COALESCE(a.size, 0) AS filesize, a.downloads, a.approved,
92
				a.width, a.height' . (empty($modSettings['attachmentShowImages']) || empty($modSettings['attachmentThumbnails']) ? '' : ',
93
				COALESCE(thumb.id_attach, 0) AS id_thumb, thumb.width AS thumb_width, thumb.height AS thumb_height') . '
94
				FROM {db_prefix}attachments AS a' . (empty($modSettings['attachmentShowImages']) || empty($modSettings['attachmentThumbnails']) ? '' : '
95
				LEFT JOIN {db_prefix}attachments AS thumb ON (thumb.id_attach = a.id_thumb)') . '
96
			WHERE a.id_msg IN ({array_int:message_list})
97
				AND a.attachment_type = {int:attachment_type}
98
			ORDER BY a.id_msg ASC, a.id_attach ASC',
99
			[
100
				'message_list' => $messages,
101
				'attachment_type' => 0,
102
			]
103
		)->fetch_callback(
104
			static function ($row) use ($includeUnapproved, $filter, $all_posters, &$attachments) {
105
				if (!$row['approved'] && !$includeUnapproved
106
					&& (empty($filter) || !$filter($row, $all_posters)))
107
				{
108
					return;
109
				}
110
111
				if (!isset($attachments[$row['id_msg']]))
112
				{
113
					$attachments[$row['id_msg']] = [];
114
				}
115
116
				$attachments[$row['id_msg']][] = $row;
117
			}
118
		);
119
120
		$this->attachments = $attachments;
121
	}
122
123
	/**
124
	 * This loads an attachment's contextual data including, most importantly, its size if it is an image.
125
	 *
126
	 * What it does:
127
	 *
128
	 * - Pre-condition: $attachments array to have been filled with the proper attachment data, as Display() does.
129
	 * - It requires the view_attachments permission to calculate image size.
130
	 * - It attempts to keep the "aspect ratio" of the posted image in line, even if it has to be resized by
131
	 * the max_image_width and max_image_height settings.
132
	 *
133
	 * @param int $id_msg message number to load attachments for
134
	 * @return array of attachments
135
	 * @todo change this pre-condition, too fragile and error-prone.
136
	 *
137
	 */
138
	public function loadAttachmentContext(int $id_msg): array
139
	{
140
		global $context, $modSettings, $scripturl, $topic;
141
142
		// Set up the attachment info - based on code by Meriadoc.
143
		$attachmentData = [];
144
		$ilaData = [];
145
		$have_unapproved = false;
146
147
		if (isset($this->attachments[$id_msg]) && !empty($modSettings['attachmentEnable']))
148
		{
149
			foreach ($this->attachments[$id_msg] as $i => $attachment)
150
			{
151
				$attachmentData[$i] = [
152
					'id' => $attachment['id_attach'],
153
					'name' => preg_replace('~&amp;#(\\d{1,7}|x[0-9a-fA-F]{1,6});~', '&#\\1;', htmlspecialchars($attachment['filename'], ENT_COMPAT, 'UTF-8')),
154
					'downloads' => $attachment['downloads'],
155
					'size' => byte_format($attachment['filesize']),
156
					'byte_size' => $attachment['filesize'],
157
					'href' => $scripturl . '?action=dlattach;topic=' . $topic . '.0;attach=' . $attachment['id_attach'],
158
					'link' => '<a href="' . $scripturl . '?action=dlattach;topic=' . $topic . '.0;attach=' . $attachment['id_attach'] . '">' . htmlspecialchars($attachment['filename'], ENT_COMPAT, 'UTF-8') . '</a>',
159
					'is_image' => !empty($attachment['width']) && !empty($attachment['height']) && !empty($modSettings['attachmentShowImages']),
160
					'is_approved' => $attachment['approved'],
161
					'file_hash' => $attachment['file_hash'],
162
				];
163
164
				// If something is unapproved we'll note it so we can sort them.
165
				if (!$attachment['approved'])
166
				{
167
					$have_unapproved = true;
168
				}
169
170
				if ($attachmentData[$i]['is_image'])
171
				{
172
					$this->prepareAttachmentImage($attachmentData[$i], $attachment, $id_msg);
173
				}
174
175
				// If this is an ILA
176
				if ($attachment['approved']
177
					&& !empty($context['ila_dont_show_attach_below'])
178
					&& in_array($attachment['id_attach'], $context['ila_dont_show_attach_below']))
179
				{
180
					$ilaData[$i] = $attachmentData[$i];
181
					unset($attachmentData[$i]);
182
				}
183
			}
184
		}
185
186
		// Do we need to instigate a sort?
187
		if ($have_unapproved)
188
		{
189
			// Unapproved attachments go first.
190
			usort($attachmentData, static fn($a, $b) => $b['is_approved'] <=> $a['is_approved']);
191
		}
192
193
		return [$attachmentData, $ilaData];
194
	}
195
196
	/**
197
	 * Function that prepares image attachments
198
	 *
199
	 * What it does:
200
	 * - Generates thumbnail if non exists, and they are enabled
201
	 *
202
	 * @param $attachmentData
203
	 * @param $attachment
204
	 * @param $id_msg
205
	 * @return void
206
	 */
207
	public function prepareAttachmentImage(&$attachmentData, $attachment, $id_msg): void
208
	{
209
		global $modSettings, $topic;
210
211
		$attachmentData['real_width'] = $attachment['width'];
212
		$attachmentData['width'] = $attachment['width'];
213
		$attachmentData['real_height'] = $attachment['height'];
214
		$attachmentData['height'] = $attachment['height'];
215
216
		// Let's see, do we want thumbs?
217
		if (!empty($modSettings['attachmentThumbnails'])
218
			&& !empty($modSettings['attachmentThumbWidth'])
219
			&& !empty($modSettings['attachmentThumbHeight'])
220
			&& ($attachment['width'] > $modSettings['attachmentThumbWidth'] || $attachment['height'] > $modSettings['attachmentThumbHeight']) && strlen($attachment['filename']) < 249)
221
		{
222
			// A proper thumb doesn't exist yet? Create one! Or, it needs update.
223
			if (empty($attachment['id_thumb'])
224
				|| $attachment['thumb_width'] > $modSettings['attachmentThumbWidth']
225
				|| $attachment['thumb_height'] > $modSettings['attachmentThumbHeight'])
226
			{
227
				$filename = getAttachmentFilename($attachment['filename'], $attachment['id_attach'], $attachment['id_folder'], false, $attachment['file_hash']);
228
				$attachment = array_merge($attachment, updateAttachmentThumbnail($filename, $attachment['id_attach'], $id_msg, $attachment['id_thumb'], $attachment['filename']));
229
			}
230
231
			// Only adjust dimensions on successful thumbnail creation.
232
			if (!empty($attachment['thumb_width']) && !empty($attachment['thumb_height']))
233
			{
234
				$attachmentData['width'] = $attachment['thumb_width'];
235
				$attachmentData['height'] = $attachment['thumb_height'];
236
			}
237
		}
238
239
		// If we have a thumbnail, make note of it!
240
		if (!empty($attachment['id_thumb']))
241
		{
242
			$attachmentData['thumbnail'] = [
243
				'id' => $attachment['id_thumb'],
244
				'href' => getUrl('action', ['action' => 'dlattach', 'topic' => $topic . '.0', 'attach' => $attachment['id_thumb'], 'image']),
245
			];
246
		}
247
248
		$attachmentData['thumbnail']['has_thumb'] = !empty($attachment['id_thumb']);
249
250
		// If thumbnails are disabled, check the maximum size of the image
251
		if (!$attachmentData['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'])))
252
		{
253
			if (!empty($modSettings['max_image_width']) && (empty($modSettings['max_image_height']) || $attachment['height'] * $modSettings['max_image_width'] / $attachment['width'] <= $modSettings['max_image_height']))
254
			{
255
				$attachmentData['width'] = $modSettings['max_image_width'];
256
				$attachmentData['height'] = floor($attachment['height'] * $modSettings['max_image_width'] / $attachment['width']);
257
			}
258
			elseif (!empty($modSettings['max_image_width']))
259
			{
260
				$attachmentData['width'] = floor($attachment['width'] * $modSettings['max_image_height'] / $attachment['height']);
261
				$attachmentData['height'] = $modSettings['max_image_height'];
262
			}
263
		}
264
		elseif ($attachmentData['thumbnail']['has_thumb'])
265
		{
266
			// Data attributes for use in expandThumbLB
267
			$attachmentData['thumbnail']['lightbox'] = 'data-lightboxmessage="' . $id_msg . '" data-lightboximage="' . $attachment['id_attach'] . '"';
268
		}
269
270
		if (!$attachmentData['thumbnail']['has_thumb'])
271
		{
272
			$attachmentData['downloads']++;
273
		}
274
	}
275
276
	/**
277
	 * Returns the array of attachments produced from getAttachments())
278
	 *
279
	 * @return array An array of attachments indexed by message ID
280
	 */
281
	public function getAttachmentsArray(): array
282
	{
283
		return $this->attachments;
284
	}
285
286
	/**
287
	 * Returns only the attachment data array for a given message
288
	 *
289
	 * @param int $id_msg message ID to load attachments for
290
	 * @return array attachment data
291
	 */
292
	public function getAttachmentData(int $id_msg): array
293
	{
294
		[$attachmentData,] = $this->loadAttachmentContext($id_msg);
295
296
		return $attachmentData;
297
	}
298
299
	/**
300
	 * Returns only the ILA (inline attachment) data array for a given message
301
	 *
302
	 * @param int $id_msg message ID to load attachments for
303
	 * @return array ILA data
304
	 */
305
	public function getIlaData(int $id_msg): array
306
	{
307
		[, $ilaData] = $this->loadAttachmentContext($id_msg);
308
309
		return $ilaData;
310
	}
311
}
312