1 | <?php |
||
2 | |||
3 | /** |
||
4 | * This file contains handling attachments. |
||
5 | * |
||
6 | * Simple Machines Forum (SMF) |
||
7 | * |
||
8 | * @package SMF |
||
9 | * @author Simple Machines https://www.simplemachines.org |
||
10 | * @copyright 2022 Simple Machines and individual contributors |
||
11 | * @license https://www.simplemachines.org/about/smf/license.php BSD |
||
12 | * |
||
13 | * @version 2.1.2 |
||
14 | */ |
||
15 | |||
16 | if (!defined('SMF')) |
||
17 | die('No direct access...'); |
||
18 | |||
19 | /** |
||
20 | * Class Attachments |
||
21 | * |
||
22 | * This class handles adding/deleting attachments |
||
23 | */ |
||
24 | class Attachments |
||
25 | { |
||
26 | /** |
||
27 | * @var int $_msg The ID of the message this attachment is associated with |
||
28 | */ |
||
29 | protected $_msg = 0; |
||
30 | |||
31 | /** |
||
32 | * @var int|null $_board The ID of the board this attachment's post is in or null if it's not set |
||
33 | */ |
||
34 | protected $_board = null; |
||
35 | |||
36 | /** |
||
37 | * @var string|bool $_attachmentUploadDir An array of info about attachment upload directories or false |
||
38 | */ |
||
39 | protected $_attachmentUploadDir = false; |
||
40 | |||
41 | /** |
||
42 | * @var string $_attchDir The path to the current attachment directory |
||
43 | */ |
||
44 | protected $_attchDir = ''; |
||
45 | |||
46 | /** |
||
47 | * @var int $_currentAttachmentUploadDir ID of the current attachment directory |
||
48 | */ |
||
49 | protected $_currentAttachmentUploadDir; |
||
50 | |||
51 | /** |
||
52 | * @var bool $_canPostAttachment Whether or not an attachment can be posted |
||
53 | */ |
||
54 | protected $_canPostAttachment; |
||
55 | |||
56 | /** |
||
57 | * @var array $_generalErrors An array of information about any errors that occurred |
||
58 | */ |
||
59 | protected $_generalErrors = array(); |
||
60 | |||
61 | /** |
||
62 | * @var mixed $_initialError Not used? |
||
63 | */ |
||
64 | protected $_initialError; |
||
65 | |||
66 | /** |
||
67 | * @var array $_attachments Not used? |
||
68 | */ |
||
69 | protected $_attachments = array(); |
||
70 | |||
71 | /** |
||
72 | * @var array $_attachResults An array of information about the results of each file |
||
73 | */ |
||
74 | protected $_attachResults = array(); |
||
75 | |||
76 | /** |
||
77 | * @var array $_attachSuccess An array of information about successful attachments |
||
78 | */ |
||
79 | protected $_attachSuccess = array(); |
||
80 | |||
81 | /** |
||
82 | * @var array $_response An array of response information. @used-by \sendResponse() when adding attachments |
||
83 | */ |
||
84 | protected $_response = array( |
||
85 | 'error' => true, |
||
86 | 'data' => array(), |
||
87 | 'extra' => '', |
||
88 | ); |
||
89 | |||
90 | /** |
||
91 | * @var array $_subActions An array of all valid sub-actions |
||
92 | */ |
||
93 | protected $_subActions = array( |
||
94 | 'add', |
||
95 | 'delete', |
||
96 | ); |
||
97 | |||
98 | /** |
||
99 | * @var string|bool $_sa The current sub-action, or false if there isn't one |
||
100 | */ |
||
101 | protected $_sa = false; |
||
102 | |||
103 | /** |
||
104 | * Attachments constructor. |
||
105 | * |
||
106 | * Sets up some initial information - the message ID, board, current attachment upload dir, etc. |
||
107 | */ |
||
108 | public function __construct() |
||
109 | { |
||
110 | global $modSettings, $context; |
||
111 | |||
112 | $this->_msg = (int) !empty($_REQUEST['msg']) ? $_REQUEST['msg'] : 0; |
||
113 | $this->_board = (int) !empty($_REQUEST['board']) ? $_REQUEST['board'] : null; |
||
114 | |||
115 | $this->_currentAttachmentUploadDir = $modSettings['currentAttachmentUploadDir']; |
||
116 | |||
117 | $this->_attachmentUploadDir = $modSettings['attachmentUploadDir']; |
||
118 | |||
119 | $this->_attchDir = $context['attach_dir'] = $this->_attachmentUploadDir[$modSettings['currentAttachmentUploadDir']]; |
||
120 | |||
121 | $this->_canPostAttachment = $context['can_post_attachment'] = !empty($modSettings['attachmentEnable']) && $modSettings['attachmentEnable'] == 1 && (allowedTo('post_attachment', $this->_board) || ($modSettings['postmod_active'] && allowedTo('post_unapproved_attachments', $this->_board))); |
||
122 | } |
||
123 | |||
124 | /** |
||
125 | * Handles calling the appropriate function based on the sub-action |
||
126 | */ |
||
127 | public function call() |
||
128 | { |
||
129 | global $smcFunc, $sourcedir; |
||
130 | |||
131 | require_once($sourcedir . '/Subs-Attachments.php'); |
||
132 | |||
133 | // Need this. For reasons... |
||
134 | loadLanguage('Post'); |
||
135 | |||
136 | $this->_sa = !empty($_REQUEST['sa']) ? $smcFunc['htmlspecialchars']($smcFunc['htmltrim']($_REQUEST['sa'])) : false; |
||
137 | |||
138 | if ($this->_canPostAttachment && $this->_sa && in_array($this->_sa, $this->_subActions)) |
||
139 | $this->{$this->_sa}(); |
||
140 | |||
141 | // Just send a generic message. |
||
142 | else |
||
143 | $this->setResponse(array( |
||
144 | 'text' => $this->_sa == 'add' ? 'attach_error_title' : 'attached_file_deleted_error', |
||
145 | 'type' => 'error', |
||
146 | 'data' => false, |
||
147 | )); |
||
148 | |||
149 | // Back to the future, oh, to the browser! |
||
150 | $this->sendResponse(); |
||
151 | } |
||
152 | |||
153 | /** |
||
154 | * Handles deleting the attachment |
||
155 | */ |
||
156 | public function delete() |
||
157 | { |
||
158 | global $sourcedir; |
||
159 | |||
160 | // Need this, don't ask why just nod your head. |
||
161 | require_once($sourcedir . '/ManageAttachments.php'); |
||
162 | |||
163 | $attachID = !empty($_REQUEST['attach']) && is_numeric($_REQUEST['attach']) ? (int) $_REQUEST['attach'] : 0; |
||
164 | |||
165 | // Need something to work with. |
||
166 | if (!$attachID || (!empty($_SESSION['already_attached']) && !isset($_SESSION['already_attached'][$attachID]))) |
||
167 | return $this->setResponse(array( |
||
168 | 'text' => 'attached_file_deleted_error', |
||
169 | 'type' => 'error', |
||
170 | 'data' => false, |
||
171 | )); |
||
172 | |||
173 | // Lets pass some params and see what happens :P |
||
174 | $affectedMessage = removeAttachments(array('id_attach' => $attachID), '', true, true); |
||
175 | |||
176 | // Gotta also remove the attachment from the session var. |
||
177 | unset($_SESSION['already_attached'][$attachID]); |
||
178 | |||
179 | // $affectedMessage returns an empty array array(0) which php treats as non empty... awesome... |
||
180 | $this->setResponse(array( |
||
181 | 'text' => !empty($affectedMessage) ? 'attached_file_deleted' : 'attached_file_deleted_error', |
||
182 | 'type' => !empty($affectedMessage) ? 'info' : 'warning', |
||
183 | 'data' => $affectedMessage, |
||
184 | )); |
||
185 | } |
||
186 | |||
187 | /** |
||
188 | * Handles adding an attachment |
||
189 | */ |
||
190 | public function add() |
||
191 | { |
||
192 | // You gotta be able to post attachments. |
||
193 | if (!$this->_canPostAttachment) |
||
194 | return $this->setResponse(array( |
||
195 | 'text' => 'attached_file_cannot', |
||
196 | 'type' => 'error', |
||
197 | 'data' => false, |
||
198 | )); |
||
199 | |||
200 | // Process them at once! |
||
201 | $this->processAttachments(); |
||
202 | |||
203 | // The attachments was created and moved the the right folder, time to update the DB. |
||
204 | if (!empty($_SESSION['temp_attachments'])) |
||
205 | $this->createAttach(); |
||
206 | |||
207 | // Set the response. |
||
208 | $this->setResponse(); |
||
209 | } |
||
210 | |||
211 | /** |
||
212 | * Moves an attachment to the proper directory and set the relevant data into $_SESSION['temp_attachments'] |
||
213 | */ |
||
214 | protected function processAttachments() |
||
215 | { |
||
216 | global $context, $modSettings, $smcFunc, $user_info, $txt; |
||
217 | |||
218 | if (!isset($_FILES['attachment']['name'])) |
||
219 | $_FILES['attachment']['tmp_name'] = array(); |
||
220 | |||
221 | // If there are attachments, calculate the total size and how many. |
||
222 | $context['attachments']['total_size'] = 0; |
||
223 | $context['attachments']['quantity'] = 0; |
||
224 | |||
225 | // If this isn't a new post, check the current attachments. |
||
226 | if (isset($_REQUEST['msg'])) |
||
227 | { |
||
228 | $context['attachments']['quantity'] = count($context['current_attachments']); |
||
229 | foreach ($context['current_attachments'] as $attachment) |
||
230 | $context['attachments']['total_size'] += $attachment['size']; |
||
231 | } |
||
232 | |||
233 | // A bit of house keeping first. |
||
234 | if (!empty($_SESSION['temp_attachments']) && count($_SESSION['temp_attachments']) == 1) |
||
235 | unset($_SESSION['temp_attachments']); |
||
236 | |||
237 | // Our infamous SESSION var, we are gonna have soo much fun with it! |
||
238 | if (!isset($_SESSION['temp_attachments'])) |
||
239 | $_SESSION['temp_attachments'] = array(); |
||
240 | |||
241 | // Make sure we're uploading to the right place. |
||
242 | if (!empty($modSettings['automanage_attachments'])) |
||
243 | automanage_attachments_check_directory(); |
||
244 | |||
245 | // Is the attachments folder actually there? |
||
246 | if (!empty($context['dir_creation_error'])) |
||
247 | $this->_generalErrors[] = $context['dir_creation_error']; |
||
248 | |||
249 | // The current attach folder ha some issues... |
||
250 | elseif (!is_dir($this->_attchDir)) |
||
251 | { |
||
252 | $this->_generalErrors[] = 'attach_folder_warning'; |
||
253 | log_error(sprintf($txt['attach_folder_admin_warning'], $this->_attchDir), 'critical'); |
||
254 | } |
||
255 | |||
256 | // If this isn't a new post, check the current attachments. |
||
257 | if (empty($this->_generalErrors) && $this->_msg) |
||
258 | { |
||
259 | $context['attachments'] = array(); |
||
260 | $request = $smcFunc['db_query']('', ' |
||
261 | SELECT COUNT(*), SUM(size) |
||
262 | FROM {db_prefix}attachments |
||
263 | WHERE id_msg = {int:id_msg} |
||
264 | AND attachment_type = {int:attachment_type}', |
||
265 | array( |
||
266 | 'id_msg' => (int) $this->_msg, |
||
267 | 'attachment_type' => 0, |
||
268 | ) |
||
269 | ); |
||
270 | list ($context['attachments']['quantity'], $context['attachments']['total_size']) = $smcFunc['db_fetch_row']($request); |
||
271 | $smcFunc['db_free_result']($request); |
||
272 | } |
||
273 | |||
274 | else |
||
275 | $context['attachments'] = array( |
||
276 | 'quantity' => 0, |
||
277 | 'total_size' => 0, |
||
278 | ); |
||
279 | |||
280 | // Check for other general errors here. |
||
281 | |||
282 | // If we have an initial error, delete the files. |
||
283 | if (!empty($this->_generalErrors)) |
||
284 | { |
||
285 | // And delete the files 'cos they ain't going nowhere. |
||
286 | foreach ($_FILES['attachment']['tmp_name'] as $n => $dummy) |
||
287 | if (file_exists($_FILES['attachment']['tmp_name'][$n])) |
||
288 | unlink($_FILES['attachment']['tmp_name'][$n]); |
||
289 | |||
290 | $_FILES['attachment']['tmp_name'] = array(); |
||
291 | |||
292 | // No point in going further with this. |
||
293 | return; |
||
294 | } |
||
295 | |||
296 | // Loop through $_FILES['attachment'] array and move each file to the current attachments folder. |
||
297 | foreach ($_FILES['attachment']['tmp_name'] as $n => $dummy) |
||
298 | { |
||
299 | if ($_FILES['attachment']['name'][$n] == '') |
||
300 | continue; |
||
301 | |||
302 | // First, let's first check for PHP upload errors. |
||
303 | $errors = array(); |
||
304 | if (!empty($_FILES['attachment']['error'][$n])) |
||
305 | { |
||
306 | if ($_FILES['attachment']['error'][$n] == 2) |
||
307 | $errors[] = array('file_too_big', array($modSettings['attachmentSizeLimit'])); |
||
308 | |||
309 | else |
||
310 | log_error($_FILES['attachment']['name'][$n] . ': ' . $txt['php_upload_error_' . $_FILES['attachment']['error'][$n]]); |
||
311 | |||
312 | // Log this one, because... |
||
313 | if ($_FILES['attachment']['error'][$n] == 6) |
||
314 | log_error($_FILES['attachment']['name'][$n] . ': ' . $txt['php_upload_error_6'], 'critical'); |
||
315 | |||
316 | // Weird, no errors were cached, still fill out a generic one. |
||
317 | if (empty($errors)) |
||
318 | $errors[] = 'attach_php_error'; |
||
319 | } |
||
320 | |||
321 | // Try to move and rename the file before doing any more checks on it. |
||
322 | $attachID = 'post_tmp_' . $user_info['id'] . '_' . md5(mt_rand()); |
||
323 | $destName = $this->_attchDir . '/' . $attachID; |
||
324 | |||
325 | // No errors, YAY! |
||
326 | if (empty($errors)) |
||
327 | { |
||
328 | // The reported MIME type of the attachment might not be reliable. |
||
329 | $detected_mime_type = get_mime_type($_FILES['attachment']['tmp_name'][$n], true); |
||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||
330 | if ($detected_mime_type !== false) |
||
331 | $_FILES['attachment']['type'][$n] = $detected_mime_type; |
||
332 | |||
333 | $_SESSION['temp_attachments'][$attachID] = array( |
||
334 | 'name' => $smcFunc['htmlspecialchars'](basename($_FILES['attachment']['name'][$n])), |
||
335 | 'tmp_name' => $destName, |
||
336 | 'size' => $_FILES['attachment']['size'][$n], |
||
337 | 'type' => $_FILES['attachment']['type'][$n], |
||
338 | 'id_folder' => $modSettings['currentAttachmentUploadDir'], |
||
339 | 'errors' => array(), |
||
340 | ); |
||
341 | |||
342 | // Move the file to the attachments folder with a temp name for now. |
||
343 | if (@move_uploaded_file($_FILES['attachment']['tmp_name'][$n], $destName)) |
||
344 | smf_chmod($destName, 0644); |
||
345 | |||
346 | // This is madness!! |
||
347 | else |
||
348 | { |
||
349 | // File couldn't be moved. |
||
350 | $_SESSION['temp_attachments'][$attachID]['errors'][] = 'attach_timeout'; |
||
351 | if (file_exists($_FILES['attachment']['tmp_name'][$n])) |
||
352 | unlink($_FILES['attachment']['tmp_name'][$n]); |
||
353 | } |
||
354 | } |
||
355 | |||
356 | // Fill up a nice array with some data from the file and the errors encountered so far. |
||
357 | else |
||
358 | { |
||
359 | $_SESSION['temp_attachments'][$attachID] = array( |
||
360 | 'name' => $smcFunc['htmlspecialchars'](basename($_FILES['attachment']['name'][$n])), |
||
361 | 'tmp_name' => $destName, |
||
362 | 'errors' => $errors, |
||
363 | ); |
||
364 | |||
365 | if (file_exists($_FILES['attachment']['tmp_name'][$n])) |
||
366 | unlink($_FILES['attachment']['tmp_name'][$n]); |
||
367 | } |
||
368 | |||
369 | // If there's no errors to this point. We still do need to apply some additional checks before we are finished. |
||
370 | if (empty($_SESSION['temp_attachments'][$attachID]['errors'])) |
||
371 | attachmentChecks($attachID); |
||
372 | } |
||
373 | |||
374 | // Mod authors, finally a hook to hang an alternate attachment upload system upon |
||
375 | // Upload to the current attachment folder with the file name $attachID or 'post_tmp_' . $user_info['id'] . '_' . md5(mt_rand()) |
||
376 | // Populate $_SESSION['temp_attachments'][$attachID] with the following: |
||
377 | // name => The file name |
||
378 | // tmp_name => Path to the temp file ($this->_attchDir . '/' . $attachID). |
||
379 | // size => File size (required). |
||
380 | // type => MIME type (optional if not available on upload). |
||
381 | // id_folder => $modSettings['currentAttachmentUploadDir'] |
||
382 | // errors => An array of errors (use the index of the $txt variable for that error). |
||
383 | // Template changes can be done using "integrate_upload_template". |
||
384 | call_integration_hook('integrate_attachment_upload', array()); |
||
385 | } |
||
386 | |||
387 | /** |
||
388 | * Actually attaches the file |
||
389 | */ |
||
390 | protected function createAttach() |
||
391 | { |
||
392 | global $txt, $user_info, $modSettings; |
||
393 | |||
394 | // Create an empty session var to keep track of all the files we attached. |
||
395 | if (!isset($_SESSION['already_attached'])) |
||
396 | $_SESSION['already_attached'] = array(); |
||
397 | |||
398 | foreach ($_SESSION['temp_attachments'] as $attachID => $attachment) |
||
399 | { |
||
400 | $attachmentOptions = array( |
||
401 | 'post' => $this->_msg, |
||
402 | 'poster' => $user_info['id'], |
||
403 | 'name' => $attachment['name'], |
||
404 | 'tmp_name' => $attachment['tmp_name'], |
||
405 | 'size' => isset($attachment['size']) ? $attachment['size'] : 0, |
||
406 | 'mime_type' => isset($attachment['type']) ? $attachment['type'] : '', |
||
407 | 'id_folder' => isset($attachment['id_folder']) ? $attachment['id_folder'] : $modSettings['currentAttachmentUploadDir'], |
||
408 | 'approved' => !$modSettings['postmod_active'] || allowedTo('post_attachment'), |
||
409 | 'errors' => array(), |
||
410 | ); |
||
411 | |||
412 | if (empty($attachment['errors'])) |
||
413 | { |
||
414 | if (createAttachment($attachmentOptions)) |
||
415 | { |
||
416 | // Avoid JS getting confused. |
||
417 | $attachmentOptions['attachID'] = $attachmentOptions['id']; |
||
418 | unset($attachmentOptions['id']); |
||
419 | |||
420 | $_SESSION['already_attached'][$attachmentOptions['attachID']] = $attachmentOptions['attachID']; |
||
421 | |||
422 | if (!empty($attachmentOptions['thumb'])) |
||
423 | $_SESSION['already_attached'][$attachmentOptions['thumb']] = $attachmentOptions['thumb']; |
||
424 | |||
425 | if ($this->_msg) |
||
426 | assignAttachments($_SESSION['already_attached'], $this->_msg); |
||
427 | } |
||
428 | } |
||
429 | else |
||
430 | { |
||
431 | // Sort out the errors for display and delete any associated files. |
||
432 | $log_these = array('attachments_no_create', 'attachments_no_write', 'attach_timeout', 'ran_out_of_space', 'cant_access_upload_path', 'attach_0_byte_file'); |
||
433 | |||
434 | foreach ($attachment['errors'] as $error) |
||
435 | { |
||
436 | $attachmentOptions['errors'][] = sprintf($txt['attach_warning'], $attachment['name']); |
||
437 | |||
438 | if (!is_array($error)) |
||
439 | { |
||
440 | $attachmentOptions['errors'][] = $txt[$error]; |
||
441 | if (in_array($error, $log_these)) |
||
442 | log_error($attachment['name'] . ': ' . $txt[$error], 'critical'); |
||
443 | } |
||
444 | else |
||
445 | $attachmentOptions['errors'][] = vsprintf($txt[$error[0]], (array) $error[1]); |
||
446 | } |
||
447 | if (file_exists($attachment['tmp_name'])) |
||
448 | unlink($attachment['tmp_name']); |
||
449 | } |
||
450 | |||
451 | // You don't need to know. |
||
452 | unset($attachmentOptions['tmp_name']); |
||
453 | unset($attachmentOptions['destination']); |
||
454 | |||
455 | // Regardless of errors, pass the results. |
||
456 | $this->_attachResults[] = $attachmentOptions; |
||
457 | } |
||
458 | |||
459 | // Temp save this on the db. |
||
460 | if (!empty($_SESSION['already_attached'])) |
||
461 | $this->_attachSuccess = $_SESSION['already_attached']; |
||
462 | |||
463 | unset($_SESSION['temp_attachments']); |
||
464 | |||
465 | // Allow user to see previews for all of this post's attachments, even if the post hasn't been submitted yet. |
||
466 | if (!isset($_SESSION['attachments_can_preview'])) |
||
467 | $_SESSION['attachments_can_preview'] = array(); |
||
468 | if (!empty($_SESSION['already_attached'])) |
||
469 | $_SESSION['attachments_can_preview'] += array_fill_keys(array_keys($_SESSION['already_attached']), true); |
||
470 | } |
||
471 | |||
472 | /** |
||
473 | * Sets up the response information |
||
474 | * |
||
475 | * @param array $data Data for the response if we're not adding an attachment |
||
476 | */ |
||
477 | protected function setResponse($data = array()) |
||
478 | { |
||
479 | global $txt; |
||
480 | |||
481 | // Some default values in case something is missed or neglected :P |
||
482 | $this->_response = array( |
||
483 | 'text' => 'attach_php_error', |
||
484 | 'type' => 'error', |
||
485 | 'data' => false, |
||
486 | ); |
||
487 | |||
488 | // Adding needs some VIP treatment. |
||
489 | if ($this->_sa == 'add') |
||
490 | { |
||
491 | // Is there any generic errors? made some sense out of them! |
||
492 | if ($this->_generalErrors) |
||
493 | foreach ($this->_generalErrors as $k => $v) |
||
494 | $this->_generalErrors[$k] = (is_array($v) ? vsprintf($txt[$v[0]], (array) $v[1]) : $txt[$v]); |
||
495 | |||
496 | // Gotta urlencode the filename. |
||
497 | if ($this->_attachResults) |
||
498 | foreach ($this->_attachResults as $k => $v) |
||
499 | $this->_attachResults[$k]['name'] = urlencode($this->_attachResults[$k]['name']); |
||
500 | |||
501 | $this->_response = array( |
||
502 | 'files' => $this->_attachResults ? $this->_attachResults : false, |
||
503 | 'generalErrors' => $this->_generalErrors ? $this->_generalErrors : false, |
||
504 | ); |
||
505 | } |
||
506 | |||
507 | // Rest of us mere mortals gets no special treatment... |
||
508 | elseif (!empty($data)) |
||
509 | if (!empty($data['text']) && !empty($txt[$data['text']])) |
||
510 | $this->_response['text'] = $txt[$data['text']]; |
||
511 | } |
||
512 | |||
513 | /** |
||
514 | * Sends the response data |
||
515 | */ |
||
516 | protected function sendResponse() |
||
517 | { |
||
518 | global $smcFunc, $modSettings, $context; |
||
519 | |||
520 | ob_end_clean(); |
||
521 | |||
522 | if (!empty($modSettings['enableCompressedOutput'])) |
||
523 | @ob_start('ob_gzhandler'); |
||
524 | else |
||
525 | ob_start(); |
||
526 | |||
527 | // Set the header. |
||
528 | header('content-type: application/json; charset=' . $context['character_set'] . ''); |
||
529 | |||
530 | echo $smcFunc['json_encode']($this->_response ? $this->_response : array()); |
||
531 | |||
532 | // Done. |
||
533 | obExit(false); |
||
534 | die; |
||
535 | } |
||
536 | } |
||
537 | |||
538 | ?> |