Issues (1696)

sources/ElkArte/Controller/Attachment.php (13 issues)

1
<?php
2
3
/**
4
 * Attachment display.
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
 * This file contains code covered by:
11
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
12
 *
13
 * @version 2.0 dev
14
 *
15
 */
16
17
namespace ElkArte\Controller;
18
19
use ElkArte\AbstractController;
20
use ElkArte\Action;
21
use ElkArte\Attachments\AttachmentsDirectory;
22
use ElkArte\Attachments\TemporaryAttachmentChunk;
23
use ElkArte\Attachments\TemporaryAttachmentProcess;
24
use ElkArte\Attachments\TemporaryAttachmentsList;
25
use ElkArte\Errors\AttachmentErrorContext;
26
use ElkArte\Exceptions\Exception;
27
use ElkArte\Graphics\Image;
28
use ElkArte\Graphics\TextImage;
29
use ElkArte\Helper\FileFunctions;
30
use ElkArte\Http\Headers;
31
use ElkArte\Languages\Txt;
32
use ElkArte\Themes\ThemeLoader;
33
use ElkArte\User;
34
35
/**
36
 * Everything to do with attachment handling / processing
37
 *
38
 * What it does:
39
 *
40
 * - Handles the downloading of an attachment or avatar
41
 * - Handles the uploading of attachments via Ajax
42
 * - Increments the download count where applicable
43
 *
44 2
 */
45
class Attachment extends AbstractController
46 2
{
47
	/** @var int Maximum size of a file to compress */
48
	private const SMALL_COMPRESS_THRESHOLD = 1048576;
49 2
50
	/**
51
	 * {@inheritDoc}
52
	 */
53
	public function needTheme($action = '')
54
	{
55 2
		global $modSettings, $maintenance;
56
57 2
		// If guests are not allowed to browse and the user is a guest... kick him!
58
		if (empty($modSettings['allow_guestAccess']) && $this->user->is_guest)
0 ignored issues
show
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
59 2
		{
60
			return true;
61
		}
62
63
		// If not in maintenance or allowed to use the forum in maintenance
64
		if (empty($maintenance) || allowedTo('admin_forum'))
65
		{
66
			$sa = $this->_req->getQuery('sa', 'trim', '');
67
68
			// We will need to respond with Json
69
			return $sa === 'ulattach' || $sa === 'rmattach' || $sa === 'ulasync';
70
		}
71
72
		// ... politely kick them out
73
		return true;
74
	}
75
76
	/**
77
	 * {@inheritDoc}
78
	 */
79
	public function trackStats($action = '')
80
	{
81
		return false;
82
	}
83
84
	/**
85
	 * The default action is to download an attachment.
86
	 * This allows ?action=attachment to be forwarded to action_dlattach()
87
	 */
88
	public function action_index()
89
	{
90
		// add a subaction array to act accordingly
91
		$subActions = [
92
			'dlattach' => [$this, 'action_dlattach'],
93
			'tmpattach' => [$this, 'action_tmpattach'],
94
			'ulattach' => [$this, 'action_ulattach'],
95
			'ulasync' => [$this, 'action_ulasync'],
96
			'rmattach' => [$this, 'action_rmattach'],
97
		];
98
99
		// Setup the action handler
100
		$action = new Action('attachments');
101
		$subAction = $action->initialize($subActions, 'dlattach');
102
103
		// Call the action
104
		$action->dispatch($subAction);
105 2
	}
106
107 2
	/**
108
	 *  Method to upload attachments as fragments via ajax
109 2
	 *
110 2
	 * - Currently called by post attachment functionality
111 2
	 * - Passed the form data with session vars
112
	 * - Responds back with errors or file data
113
	 *
114 2
	 * @return bool Returns false if there was an error, otherwise true.
115 2
	 */
116 2
	public function action_ulasync(): bool
117 2
	{
118
		global $context;
119
120 2
		// Going to send back Json
121
		setJsonTemplate();
122
123
		// Final request, rebuild the file and do standard upload checks
124
		if ($this->_req->comparePost('async', 'complete', 'trim'))
125
		{
126
			$this->combineChunksAndProcess();
127
			return true;
128 2
		}
129
130 2
		Txt::load('Errors');
131
132 2
		// Process the chunk
133 2
		$chunk = new TemporaryAttachmentChunk();
134
		$resp_data = $chunk->action_async();
135 2
136
		// If we have a PHP upload error, set the error context
137 2
		if ($resp_data['result'] !== true)
138
		{
139 2
			$attach_errors = AttachmentErrorContext::context();
140 2
			$attach_errors->activate();
141
			if ($attach_errors->hasErrors())
142
			{
143
				$errors = $attach_errors->prepareErrors();
144 2
				foreach ($errors as $error)
145
				{
146 2
					$resp_data[] = $error;
147
				}
148
149 2
				$context['json_data'] = ['result' => false, 'data' => $resp_data];
150
				return false;
151 2
			}
152
		}
153
154 2
		// Set up the template details
155
		$context['json_data'] = $resp_data;
156
157
		return true;
158
	}
159
160
	/**
161
	 * Combines the temporary attachment chunks into a single file
162
	 *
163
	 * This method combines the temporary attachment chunks into a single file and performs the final
164
	 * processing request for the combined chunks. If the response data indicates that the result is
165
	 * successful, the method passes the file off to the action_ulattach method as if it were a single file.
166
	 * Otherwise, the response data is assigned to the $context['json_data'] variable.
167
	 *
168
	 * @return void
169
	 */
170
	private function combineChunksAndProcess(): void
171
	{
172
		global $context;
173 2
174
		// Final chuck processing request
175
		$chunk = new TemporaryAttachmentChunk();
176
177
		$resp_data = $chunk->action_combineChunks();
178
		if ($resp_data['result'] === true)
179 2
		{
180
			// Pass this off to action_ulattach just like it was a single file, set strict as false as we already have the
181 2
			// combined chunks in the attachment directory, and we don't need to verify it was a php upload any longer
182
			$this->action_ulattach(false);
183
		}
184
		else
185
		{
186
			$context['json_data'] = $resp_data;
187
		}
188
	}
189
190
	/**
191
	 *  Method to upload attachments via ajax
192
	 *
193
	 *  - Currently called by drag drop attachment functionality
194
	 *  - Passed the form data with session vars
195
	 *  - Responds back with errors or file data
196
	 *
197
	 * @param bool $strict True if attachment processing should use move_uploaded_file, rename otherwise. Default is true.
198
	 *
199
	 * @return bool|null False if the session is invalid or an error occurred, void otherwise.
200
	 */
201
	public function action_ulattach($strict = true): ?bool
202
	{
203
		global $context, $modSettings, $txt;
204
205
		$resp_data = [];
206
		Txt::load('Errors');
207
		$context['attachments']['can']['post'] = !empty($modSettings['attachmentEnable']) && (int) $modSettings['attachmentEnable'] === 1 && (allowedTo('post_attachment') || ($modSettings['postmod_active'] && allowedTo('post_unapproved_attachments')));
208
209
		// Set up the template details
210
		setJsonTemplate();
211
212
		// Make sure the session is still valid
213
		if (checkSession('post', '', false) !== '')
214
		{
215
			$context['json_data'] = ['result' => false, 'data' => $txt['session_timeout_file_upload']];
216
217
			return false;
218
		}
219
220
		// We should have files, otherwise why are we here?
221
		if (isset($_FILES['attachment']))
222
		{
223
			Txt::load('Post');
224
225
			$attach_errors = AttachmentErrorContext::context();
226
			$attach_errors->activate();
227
228
			if ($context['attachments']['can']['post'] && empty($this->_req->post->from_qr))
229
			{
230
				$processAttachments = new TemporaryAttachmentProcess();
231
				$processAttachments->strict = $strict;
232
				$processAttachments->processAttachments($this->_req->getPost('msg', 'intval', 0));
233
			}
234
235
			// Any mistakes?
236
			if ($attach_errors->hasErrors())
237
			{
238
				$errors = $attach_errors->prepareErrors();
239
240
				// Bad news for you, the attachments did not process, lets tell them why
241
				foreach ($errors as $error)
242
				{
243
					$resp_data[] = $error;
244
				}
245
246
				$context['json_data'] = ['result' => false, 'data' => $resp_data];
247
			}
248
			// No errors, lets get the details of what we have for our response back to the upload dialog
249
			else
250
			{
251
				$tmp_attachments = new TemporaryAttachmentsList();
252
				foreach ($tmp_attachments->toArray() as $val)
253
				{
254
					// We need to grab the name anyhow
255
					if (!empty($val['tmp_name']))
256
					{
257
						$resp_data = [
258
							'name' => $val['name'],
259
							'attachid' => $val['public_attachid'],
260
							'size' => $val['size'],
261
							'resized' => !empty($val['resized']),
262
						];
263
					}
264
				}
265
266
				$context['json_data'] = ['result' => true, 'data' => $resp_data];
267
			}
268
		}
269
		// Could not find the files you claimed to have sent
270
		else
271
		{
272
			$context['json_data'] = ['result' => false, 'data' => $txt['no_files_uploaded']];
273
		}
274
275
		return null;
276
	}
277
278
	/**
279
	 * Function to remove temporary attachments which were newly added via ajax calls
280
	 * or to remove previous saved ones from an existing post
281
	 *
282
	 * What it does:
283
	 *
284
	 * - Currently called by drag drop attachment functionality
285
	 * - Requires file name and file path
286
	 * - Responds back with success or error
287
	 */
288
	public function action_rmattach(): ?bool
289
	{
290
		global $context, $txt;
291
292
		// Prepare the template so we can respond with json
293
		setJsonTemplate();
294
295
		// Make sure the session is valid
296
		if (checkSession('post', '', false) !== '')
297
		{
298
			Txt::load('Errors');
299
			$context['json_data'] = ['result' => false, 'data' => $txt['session_timeout']];
300
301
			return false;
302
		}
303
304
		// We need a filename and path, or we are not going any further
305
		if (isset($this->_req->post->attachid))
306
		{
307
			$result = false;
308
			$tmp_attachments = new TemporaryAttachmentsList();
309
			if ($tmp_attachments->hasAttachments())
310
			{
311
				$attachId = $tmp_attachments->getIdFromPublic($this->_req->post->attachid);
312
313
				try
314
				{
315
					$tmp_attachments->removeById($attachId);
316
					$context['json_data'] = ['result' => true];
317
					$result = true;
318
				}
319
				catch (\Exception $e)
320
				{
321
					$result = $e->getMessage();
322
				}
323
			}
324
325
			// Not a temporary attachment, but a previously uploaded one?
326
			if ($result !== true)
327
			{
328
				require_once(SUBSDIR . '/ManageAttachments.subs.php');
329
				$attachId = $this->_req->getPost('attachid', 'intval');
330
				if (canRemoveAttachment($attachId, User::$info->id))
331
				{
332
					$result_tmp = removeAttachments(['id_attach' => $attachId], '', true);
333
					if (!empty($result_tmp))
334
					{
335
						$context['json_data'] = ['result' => true];
336
						$result = true;
337
					}
338
					else
339
					{
340
						$result = $result_tmp;
341
					}
342
				}
343
			}
344
345
			if ($result !== true)
346
			{
347
				Txt::load('Errors');
348
				$context['json_data'] = ['result' => false, 'data' => $txt[empty($result) ? 'attachment_not_found' : $result]];
349
			}
350
		}
351
		else
352
		{
353
			Txt::load('Errors');
354
			$context['json_data'] = ['result' => false, 'data' => $txt['attachment_not_found']];
355
		}
356
357
		return null;
358
	}
359
360
	/**
361
	 * Downloads an attachment or avatar, and increments the download count.
362
	 *
363
	 * What it does:
364
	 *
365
	 * - It requires the view_attachments permission. (not for avatars!)
366
	 * - It disables the session parser, and clears any previous output.
367
	 * - It is accessed via the query string ?action=dlattach.
368
	 * - Views to attachments and avatars do not increase hits and are not logged
369
	 *   in the "Who's Online" log.
370
	 *
371
	 * @throws Exception
372
	 */
373
	public function action_dlattach(): void
374
	{
375
		global $modSettings, $context, $topic, $board, $settings;
376
377
		// Some defaults that we need.
378
		$context['no_last_modified'] = true;
379
		$filename = null;
380
381
		// Make sure some attachment was requested!
382
		if (!isset($this->_req->query->attach))
383
		{
384
			if (!isset($this->_req->query->id))
385
			{
386
				// Give them the old can't find it image
387
				$this->action_text_to_image('attachment_not_found');
388
			}
389
390
			if ($this->_req->query->id === 'ila')
391
			{
392
				// Give them the old can't touch this
393
				$this->action_text_to_image(($this->user->is_guest ? 'not_applicable' : 'awaiting_approval'), 90, 90, true);
0 ignored issues
show
Bug Best Practice introduced by
The property is_guest does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
394
			}
395
		}
396
397
		// We need to do some work on attachments and avatars.
398
		require_once(SUBSDIR . '/Attachments.subs.php');
399
400
		// Temporary attachment, special case...
401
		if (isset($this->_req->query->attach) && strpos($this->_req->query->attach, 'post_tmp_' . $this->user->id . '_') !== false)
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...
402
		{
403
			// Return via tmpattach, back presumably to the post form
404
			$this->action_tmpattach();
405
		}
406
407
		$id_attach = $this->_req->getQuery('attach', 'intval', $this->_req->getQuery('id', 'intval', 0));
408
409
		// This is just a regular attachment... Avatars are no longer a dlattach option
410
		if (empty($topic) && !empty($id_attach))
411
		{
412
			$id_board = 0;
413
			$id_topic = 0;
414
			$attachPos = getAttachmentPosition($id_attach);
415
			if ($attachPos !== false)
416
			{
417
				[$id_board, $id_topic] = array_values($attachPos);
0 ignored issues
show
It seems like $attachPos can also be of type true; however, parameter $array of array_values() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

417
				[$id_board, $id_topic] = array_values(/** @scrutinizer ignore-type */ $attachPos);
Loading history...
418
			}
419
		}
420
		else
421
		{
422
			$id_board = $board;
423
			$id_topic = $topic;
424
		}
425
426
		isAllowedTo('view_attachments', $id_board);
427
428
		if ($this->_req->getQuery('thumb') === null)
429
		{
430
			$attachment = getAttachmentFromTopic($id_attach, $id_topic);
431
		}
432
		else
433
		{
434
			$this->_req->query->image = true;
435
			$attachment = getAttachmentThumbFromTopic($id_attach, $id_topic);
436
437
			// No file name, no thumbnail, no image.
438
			if (empty($attachment['filename']))
439
			{
440
				$full_attach = getAttachmentFromTopic($id_attach, $id_topic);
441
				$attachment['filename'] = empty($full_attach['filename']) ? '' : $full_attach['filename'];
442
				$attachment['id_attach'] = 0;
443
				$attachment['attachment_type'] = 0;
444
				$attachment['approved'] = $full_attach['approved'] ?? 0;
445
				$attachment['id_member'] = $full_attach['id_member'];
446
447
				// If it is a known extension, show a mimetype extension image
448
				$check = returnMimeThumb(empty($full_attach['fileext']) ? 'default' : $full_attach['fileext']);
449
				if ($check !== false)
0 ignored issues
show
The condition $check !== false is always true.
Loading history...
450
				{
451
					$attachment['fileext'] = 'png';
452
					$attachment['mime_type'] = 'image/png';
453
					$filename = $check;
454
				}
455
				else
456
				{
457
					$attachmentsDir = new AttachmentsDirectory($modSettings, database());
458
					$filename = $attachmentsDir->getCurrent() . '/' . $attachment['filename'];
459
				}
460
461
				if (strpos(getMimeType($filename), 'image') !== 0)
462
				{
463
					$attachment['fileext'] = 'png';
464
					$attachment['mime_type'] = 'image/png';
465
					$filename = $settings['theme_dir'] . '/images/mime_images/default.png';
466
				}
467
			}
468
		}
469
470
		if (empty($attachment))
471
		{
472
			// Exit via action_text_to_image
473
			$this->action_text_to_image('attachment_not_found');
474
		}
475
476
		$id_folder = $attachment['id_folder'] ?? '';
477
		$real_filename = $attachment['filename'] ?? '';
478
		$file_hash = $attachment['file_hash'] ?? '';
479
		$file_ext = $attachment['fileext'] ?? '';
480
		$id_attach = $attachment['id_attach'] ?? -1;
481
		$attachment_type = $attachment['attachment_type'] ?? -1;
482
		$mime_type = $attachment['mime_type'] ?? '';
483
		$is_approved = $attachment['approved'] ?? '';
484
		$id_member = $attachment['id_member'] ?? '';
485
486
		// If it isn't yet approved, do they have permission to view it?
487
		if (!$is_approved && ($id_member === 0 || $this->user->id !== $id_member) && ($attachment_type === 0 || $attachment_type === 3))
488
		{
489
			isAllowedTo('approve_posts', $id_board ?? $board);
490
		}
491
492
		// Update the download counter (unless it's a thumbnail).
493
		if (!empty($id_attach && $attachment_type != 3))
494
		{
495
			increaseDownloadCounter($id_attach);
496
		}
497
498
		if ($filename === null)
499
		{
500
			$filename = getAttachmentFilename($real_filename, $id_attach, $id_folder, false, $file_hash);
501
		}
502
503
		$eTag = '"' . substr($id_attach . $real_filename . @filemtime($filename), 0, 64) . '"';
0 ignored issues
show
Are you sure @filemtime($filename) of type false|integer can be used in concatenation? ( Ignorable by Annotation )

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

503
		$eTag = '"' . substr($id_attach . $real_filename . /** @scrutinizer ignore-type */ @filemtime($filename), 0, 64) . '"';
Loading history...
504
		$disposition = isset($this->_req->query->image) ? 'inline' : 'attachment';
505
		$do_cache = !(!isset($this->_req->query->image) && getValidMimeImageType($file_ext) !== '');
506
507
		// Make sure the mime type warrants an inline display.
508
		if (isset($this->_req->query->image) && !empty($mime_type) && strpos($mime_type, 'image/') !== 0)
509
		{
510
			unset($this->_req->query->image);
511
			$mime_type = '';
512
		}
513
		// Does this have a mime type?
514
		elseif (empty($mime_type) || (!isset($this->_req->query->image) && getValidMimeImageType($file_ext) !== ''))
515
		{
516
			$mime_type = '';
517
			if (isset($this->_req->query->image))
518
			{
519
				unset($this->_req->query->image);
520
			}
521
		}
522
523
		$this->prepare_headers($filename, $eTag, $mime_type, $disposition, $real_filename, $do_cache);
524
		$this->send_file($filename, $mime_type);
525
526
		obExit(false);
527
	}
528
529
	/**
530
	 * Generates a language image based on text for display, outputs that image and exits
531
	 *
532
	 * @param null|string $text if null will use default attachment not found string
533
	 * @param int $width If set, defines the width of the image, text font size will be scaled to fit
534
	 * @param int $height If set, defines the height of the image
535
	 * @param bool $split If true will break text strings so all words are separated by newlines
536
	 * @throws Exception
537
	 */
538
	public function action_text_to_image($text = null, $width = 200, $height = 75, $split = false): void
539
	{
540
		global $txt;
541
542
		new ThemeLoader();
543
		Txt::load('Errors');
544
		$text = $text === null ? $txt['attachment_not_found'] : $txt[$text] ?? $text;
545
		$text = $split ? str_replace(' ', "\n", $text) : $text;
546
547
		try
548
		{
549
			$img = new TextImage($text);
550
			$img = $img->generate($width, $height);
551
		}
552
		catch (\Exception)
553
		{
554
			throw new Exception('no_access', false);
555
		}
556
557
		$this->prepare_headers('no_image', 'no_image', 'image/png', 'inline', 'no_image.png', true, false);
558
		Headers::instance()->sendHeaders();
559
		echo $img;
560
561
		obExit(false);
562
	}
563
564
	/**
565
	 * If the mime type benefits from compression e.g. text/xyz and gzencode is
566
	 * available and the user agent accepts gzip, then return true, else false
567
	 *
568
	 * @param string $mime_type
569
	 * @return bool if we should compress the file
570
	 */
571
	public function useCompression($mime_type): bool
572
	{
573
		global $modSettings;
574
575
		// Not compressible, or not supported / requested by client
576
		if (!preg_match('~^(?:text/|application/(?:json|xml|rss\+xml)$)~i', $mime_type)
577
			|| (!isset($_SERVER['HTTP_ACCEPT_ENCODING']) || strpos($_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') === false))
578
		{
579
			return false;
580
		}
581
582
		// Support is available on the serve
583
		return !(!function_exists('gzencode') && !empty($modSettings['enableCompressedOutput']));
584
	}
585
586
	/**
587
	 * Takes care of sending out the most common headers.
588
	 *
589
	 * @param string $filename Full path+file name of the file in the filesystem
590
	 * @param string $eTag ETag cache validator
591
	 * @param string $mime_type The mime-type of the file
592
	 * @param string $disposition The value of the Content-Disposition header
593
	 * @param string $real_filename The original name of the file
594
	 * @param bool $do_cache Send a max-age header or not
595
	 * @param bool $check_filename When false, any check on $filename is skipped
596
	 */
597
	public function prepare_headers($filename, $eTag, $mime_type, $disposition, $real_filename, $do_cache, $check_filename = true): void
598
	{
599
		global $txt;
600
601
		$headers = Headers::instance();
602
		$protocol = detectServer()->getProtocol();
0 ignored issues
show
The assignment to $protocol is dead and can be removed.
Loading history...
603
604
		// No point in a nicer message, because this is supposed to be an attachment anyway...
605
		if ($check_filename && !FileFunctions::instance()->fileExists($filename))
606
		{
607
			Txt::load('Errors');
608
609
			$headers
610
				->removeHeader('all')
611
				->httpCode(404)
612
				->sendHeaders();
613
614
			// We need to die like this *before* we send any anti-caching headers as below.
615
			die('404 - ' . $txt['attachment_not_found']);
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
616
		}
617
618
		// If it hasn't been modified since the last time this attachment was retrieved, there's no need to display it again.
619
		if (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE']))
620
		{
621
			[$modified_since] = explode(';', $this->_req->server->HTTP_IF_MODIFIED_SINCE);
622
			if (!$check_filename || strtotime($modified_since) >= filemtime($filename))
623
			{
624
				$this->flush_buffers();
625
626
				// Answer the question - no, it hasn't been modified ;).
627
				$headers
628
					->removeHeader('all')
629
					->httpCode(304)
630
					->sendHeaders();
631
				exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
632
			}
633
		}
634
635
		// Check whether the ETag was sent back, and cache based on that...
636
		if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) && strpos($_SERVER['HTTP_IF_NONE_MATCH'], $eTag) !== false)
637
		{
638
			$this->flush_buffers();
639
640
			$headers
641
				->removeHeader('all')
642
				->httpCode(304)
643
				->sendHeaders();
644
			exit;
0 ignored issues
show
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
645
		}
646
647
		// Send the attachment headers.
648
		$headers
649
			->header('Expires', gmdate('D, d M Y H:i:s', time() + 525600 * 60) . ' GMT')
650
			->header('Last-Modified', gmdate('D, d M Y H:i:s', $check_filename ? filemtime($filename) : time() - 525600 * 60) . ' GMT')
651
			->header('Accept-Ranges', 'bytes')
652
			->header('Connection', 'close')
653
			->header('ETag', $eTag);
654
655
		// Different browsers like different standards...
656
		$headers->setAttachmentFileParams($mime_type, $real_filename, $disposition);
657
658
		// If this has an "image extension" - but isn't actually an image - then ensure it isn't cached cause of silly IE.
659
		if ($do_cache)
660
		{
661
			$headers
662
				->header('Cache-Control', 'max-age=' . (525600 * 60) . ', private');
663
		}
664
		else
665
		{
666
			$headers
667
				->header('Pragma', 'no-cache')
668
				->header('Cache-Control', 'no-cache');
669
		}
670
671
		// Try to buy some time...
672
		detectServer()->setTimeLimit(600);
673
	}
674
675
	/**
676
	 * Sends the requested file to the user.  If the file is compressible e.g.
677
	 * has a mine type of text/??? may compress the file prior to sending.
678
	 *
679
	 * @param string $filename
680
	 * @param string $mime_type
681
	 */
682
	public function send_file($filename, $mime_type): void
683
	{
684
		$headers = Headers::instance();
685
		$fileFuncs = FileFunctions::instance();
686
		$filesize = $fileFuncs->fileSize($filename);
687
		$use_compression = $this->useCompression($mime_type);
688
689
		// Flush any buffers that may be in place.
690
		$this->flush_buffers();
691
692
		// For small compressible files, compress in-memory and provide a compressed Content-Length
693
		if ($use_compression && $filesize > 24 && $filesize <= self::SMALL_COMPRESS_THRESHOLD)
694
		{
695
			$body = $fileFuncs->fileGetContents($filename);
696
			if ($body !== false)
697
			{
698
				$body = gzencode($body, 4);
699
				$length = strlen($body);
700
				$headers
701
					->header('Content-Encoding', 'gzip')
702
					->header('Vary', 'Accept-Encoding')
703
					->header('Content-Length', (string) $length);
704
				$headers->send();
705
				echo $body;
706
			}
707
708
			return;
709
		}
710
711
		// Uncompressed streaming (default and fallback)
712
		if (!empty($filesize))
713
		{
714
			$headers->header('Content-Length', (string) $filesize);
715
		}
716
717
		$headers->send();
718
		readfile($filename);
719
	}
720
721
	/**
722
	 * Flushes and clears all output buffers
723
	 *
724
	 * This method forcibly ends any ongoing output buffering to prevent issues such as double compression
725
	 * and oversized output buffers by iterating through and clearing all active buffer levels.
726
	 *
727
	 * @return void
728
	 */
729
	public function flush_buffers(): void
730
	{
731
		while (ob_get_level() > 0)
732
		{
733
			@ob_end_clean();
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for ob_end_clean(). 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

733
			/** @scrutinizer ignore-unhandled */ @ob_end_clean();

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...
734
		}
735
	}
736
737
	/**
738
	 * "Simplified", cough, version of action_dlattach to send out thumbnails while creating
739
	 * or editing a message.
740
	 */
741
	public function action_tmpattach(): void
742
	{
743
		global $modSettings, $topic;
744
745
		// Make sure some attachment was requested!
746
		if (!isset($this->_req->query->attach))
747
		{
748
			$this->action_text_to_image('attachment_not_found');
749
		}
750
751
		// We will need some help
752
		require_once(SUBSDIR . '/Attachments.subs.php');
753
		$tmp_attachments = new TemporaryAttachmentsList();
754
		$attachmentsDir = new AttachmentsDirectory($modSettings, database());
755
756
		try
757
		{
758
			if (empty($topic) || (string) (int) $this->_req->query->attach !== (string) $this->_req->query->attach)
759
			{
760
				$attach_data = $tmp_attachments->getTempAttachById($this->_req->query->attach, $attachmentsDir, 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...
$attachmentsDir of type ElkArte\Attachments\AttachmentsDirectory is incompatible with the type string expected by parameter $attachmentsDir of ElkArte\Attachments\Temp...st::getTempAttachById(). ( Ignorable by Annotation )

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

760
				$attach_data = $tmp_attachments->getTempAttachById($this->_req->query->attach, /** @scrutinizer ignore-type */ $attachmentsDir, User::$info->id);
Loading history...
761
				$file_ext = pathinfo($attach_data['name'], PATHINFO_EXTENSION);
762
				$filename = $attach_data['tmp_name'];
763
				$id_attach = $attach_data['attachid'];
764
				$real_filename = $attach_data['name'];
765
				$mime_type = $attach_data['type'];
766
			}
767
			else
768
			{
769
				$id_attach = $this->_req->getQuery('attach', 'intval', -1);
770
771
				isAllowedTo('view_attachments');
772
				$attachment = getAttachmentFromTopic($id_attach, $topic);
773
				if (empty($attachment))
774
				{
775
					// Exit via action_text_to_image
776
					$this->action_text_to_image('attachment_not_found');
777
				}
778
779
				// Save some typing
780
				$id_folder = $attachment['id_folder'];
781
				$real_filename = $attachment['filename'];
782
				$file_hash = $attachment['file_hash'];
783
				$file_ext = $attachment['fileext'];
784
				$id_attach = $attachment['id_attach'];
785
				$attachment_type = (int) $attachment['attachment_type'];
786
				$mime_type = $attachment['mime_type'];
787
				$is_approved = $attachment['approved'];
788
				$id_member = (int) $attachment['id_member'];
789
790
				// If it isn't yet approved, do they have permission to view it?
791
				if (!$is_approved && ($id_member === 0 || $this->user->id !== $id_member)
792
					&& ($attachment_type === 0 || $attachment_type === 3))
793
				{
794
					isAllowedTo('approve_posts');
795
				}
796
797
				$filename = getAttachmentFilename($real_filename, $id_attach, $id_folder, false, $file_hash);
798
			}
799
		}
800
		catch (\Exception $exception)
801
		{
802
			throw new Exception($exception->getMessage(), false);
803
		}
804
805
		$resize = true;
806
807
		// Return mime type ala mimetype extension
808
		if (strpos(getMimeType($filename), 'image') !== 0)
809
		{
810
			$checkMime = returnMimeThumb($file_ext);
811
			$mime_type = 'image/png';
812
			$resize = false;
813
			$filename = $checkMime;
814
		}
815
816
		$eTag = '"' . substr($id_attach . $real_filename . filemtime($filename), 0, 64) . '"';
817
		$do_cache = !(!isset($this->_req->query->image) && getValidMimeImageType($file_ext) !== '');
818
819
		$this->prepare_headers($filename, $eTag, $mime_type, 'inline', $real_filename, $do_cache);
820
821
		// do not resize for ;image
822
		if ($resize && !isset($this->_req->query->ila, $this->_req->query->image))
823
		{
824
			// Create a thumbnail image
825
			$image = new Image($filename);
826
827
			$filename .= '_thumb';
828
			$max_width = $this->_req->isSet('thumb') && !empty($modSettings['attachmentThumbWidth']) ? $modSettings['attachmentThumbWidth'] : 300;
829
			$max_height = $this->_req->isSet('thumb') && !empty($modSettings['attachmentThumbHeight']) ? $modSettings['attachmentThumbHeight'] : 300;
830
831
			$image->createThumbnail($max_width, $max_height, $filename, null, false);
832
		}
833
834
		// With the headers complete, send the file data
835
		$this->send_file($filename, $mime_type);
836
837
		obExit(false);
838
	}
839
}
840