Attachment::needTheme()   B
last analyzed

Complexity

Conditions 7
Paths 5

Size

Total Lines 21
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 10.1359

Importance

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

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

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

617
				/** @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...
618
619
				// Answer the question - no, it hasn't been modified ;).
620
				$headers
621
					->removeHeader('all')
622
					->headerSpecial($protocol . ' 304 Not Modified')
623
					->sendHeaders();
624
				exit;
0 ignored issues
show
Best Practice introduced by
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...
625
			}
626
		}
627
628
		// Check whether the ETag was sent back, and cache based on that...
629
		if (!empty($_SERVER['HTTP_IF_NONE_MATCH']) && strpos($_SERVER['HTTP_IF_NONE_MATCH'], $eTag) !== false)
630
		{
631
			@ob_end_clean();
632
633
			$headers
634
				->removeHeader('all')
635
				->headerSpecial($protocol . ' 304 Not Modified')
636
				->sendHeaders();
637
			exit;
0 ignored issues
show
Best Practice introduced by
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...
638
		}
639
640
		// Send the attachment headers.
641
		$headers
642
			->header('Expires', gmdate('D, d M Y H:i:s', time() + 525600 * 60) . ' GMT')
643
			->header('Last-Modified', gmdate('D, d M Y H:i:s', $check_filename ? filemtime($filename) : time() - 525600 * 60) . ' GMT')
644
			->header('Accept-Ranges', 'bytes')
645
			->header('Connection', 'close')
646
			->header('ETag', $eTag);
647
648
		// Different browsers like different standards...
649
		$headers->setAttachmentFileParams($mime_type, $real_filename, $disposition);
650
651
		// If this has an "image extension" - but isn't actually an image - then ensure it isn't cached cause of silly IE.
652
		if ($do_cache)
653
		{
654
			$headers
655
				->header('Cache-Control', 'max-age=' . (525600 * 60) . ', private');
656
		}
657
		else
658
		{
659
			$headers
660
				->header('Pragma', 'no-cache')
661
				->header('Cache-Control', 'no-cache');
662
		}
663
664
		// Try to buy some time...
665
		detectServer()->setTimeLimit(600);
666
	}
667
668
	/**
669
	 * Sends the requested file to the user.  If the file is compressible e.g.
670
	 * has a mine type of text/??? may compress the file prior to sending.
671
	 *
672
	 * @param string $filename
673
	 * @param string $mime_type
674
	 */
675
	public function send_file($filename, $mime_type)
676
	{
677
		$headers = Headers::instance();
678
		$body = file_get_contents($filename);
679
		$length = FileFunctions::instance()->fileSize($filename);
680
		$use_compression = $this->useCompression($mime_type);
681
682
		// If we can/should compress this file
683
		if ($use_compression && strlen($body) > 250)
684
		{
685
			$body = gzencode($body, 2);
686
			$length = strlen($body);
687
			$headers
688
				->header('Content-Encoding', 'gzip')
689
				->header('Vary', 'Accept-Encoding');
690
		}
691
692
		if (!empty($length))
693
		{
694
			$headers->header('Content-Length', $length);
695
		}
696
697
		// Forcibly end any output buffering going on.
698
		while (ob_get_level() > 0)
699
		{
700
			@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

700
			/** @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...
701
		}
702
703
		// Someone is getting a present
704
		$headers->send();
705
		echo $body;
706
	}
707
708
	/**
709
	 * "Simplified", cough, version of action_dlattach to send out thumbnails while creating
710
	 * or editing a message.
711
	 */
712
	public function action_tmpattach()
713
	{
714
		global $modSettings, $topic;
715
716
		// Make sure some attachment was requested!
717
		if (!isset($this->_req->query->attach))
718
		{
719
			$this->action_text_to_image('attachment_not_found');
720
		}
721
722
		// We will need some help
723
		require_once(SUBSDIR . '/Attachments.subs.php');
724
		$tmp_attachments = new TemporaryAttachmentsList();
725
		$attachmentsDir = new AttachmentsDirectory($modSettings, database());
726
727
		try
728
		{
729
			if (empty($topic) || (string) (int) $this->_req->query->attach !== (string) $this->_req->query->attach)
730
			{
731
				$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...
732
				$file_ext = pathinfo($attach_data['name'], PATHINFO_EXTENSION);
733
				$filename = $attach_data['tmp_name'];
734
				$id_attach = $attach_data['attachid'];
735
				$real_filename = $attach_data['name'];
736
				$mime_type = $attach_data['type'];
737
			}
738
			else
739
			{
740
				$id_attach = $this->_req->getQuery('attach', 'intval', -1);
741
742
				isAllowedTo('view_attachments');
743
				$attachment = getAttachmentFromTopic($id_attach, $topic);
744
				if (empty($attachment))
745
				{
746
					// Exit via action_text_to_image
747
					$this->action_text_to_image('attachment_not_found');
748
				}
749
750
				// Save some typing
751
				$id_folder = $attachment['id_folder'];
752
				$real_filename = $attachment['filename'];
753
				$file_hash = $attachment['file_hash'];
754
				$file_ext = $attachment['fileext'];
755
				$id_attach = $attachment['id_attach'];
756
				$attachment_type = (int) $attachment['attachment_type'];
757
				$mime_type = $attachment['mime_type'];
758
				$is_approved = $attachment['approved'];
759
				$id_member = (int) $attachment['id_member'];
760
761
				// If it isn't yet approved, do they have permission to view it?
762
				if (!$is_approved && ($id_member === 0 || $this->user->id !== $id_member)
763
					&& ($attachment_type === 0 || $attachment_type === 3))
764
				{
765
					isAllowedTo('approve_posts');
766
				}
767
768
				$filename = getAttachmentFilename($real_filename, $id_attach, $id_folder, false, $file_hash);
769
			}
770
		}
771
		catch (\Exception $exception)
772
		{
773
			throw new Exception($exception->getMessage(), false);
774
		}
775
776
		$resize = true;
777
778
		// Return mime type ala mimetype extension
779
		if (strpos(getMimeType($filename), 'image') !== 0)
780
		{
781
			$checkMime = returnMimeThumb($file_ext);
782
			$mime_type = 'image/png';
783
			$resize = false;
784
			$filename = $checkMime;
785
		}
786
787
		$eTag = '"' . substr($id_attach . $real_filename . filemtime($filename), 0, 64) . '"';
788
		$do_cache = !(!isset($this->_req->query->image) && getValidMimeImageType($file_ext) !== '');
789
790
		$this->prepare_headers($filename, $eTag, $mime_type, 'inline', $real_filename, $do_cache);
791
792
		// do not resize for ;image
793
		if ($resize && !isset($this->_req->query->ila, $this->_req->query->image))
794
		{
795
			// Create a thumbnail image
796
			$image = new Image($filename);
797
798
			$filename .= '_thumb';
799
			$max_width = $this->_req->isSet('thumb') && !empty($modSettings['attachmentThumbWidth']) ? $modSettings['attachmentThumbWidth'] : 300;
800
			$max_height = $this->_req->isSet('thumb') && !empty($modSettings['attachmentThumbHeight']) ? $modSettings['attachmentThumbHeight'] : 300;
801
802
			$image->createThumbnail($max_width, $max_height, $filename, null, false);
803
		}
804
805
		// With the headers complete, send the file data
806
		$this->send_file($filename, $mime_type);
807
808
		obExit(false);
809
	}
810
}
811