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