handleAttachmentError()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 1
eloc 8
c 2
b 0
f 0
nc 1
nop 3
dl 0
loc 17
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Teampass - a collaborative passwords manager.
7
 * ---
8
 * This file is part of the TeamPass project.
9
 * 
10
 * TeamPass is free software: you can redistribute it and/or modify it
11
 * under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, version 3 of the License.
13
 * 
14
 * TeamPass is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU General Public License for more details.
18
 * 
19
 * You should have received a copy of the GNU General Public License
20
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
21
 * 
22
 * Certain components of this file may be under different licenses. For
23
 * details, see the `licenses` directory or individual file headers.
24
 * ---
25
 * @file      upload.attachments.php
26
 * @author    Nils Laumaillé ([email protected])
27
 * @copyright 2009-2025 Teampass.net
28
 * @license   GPL-3.0
29
 * @see       https://www.teampass.net
30
 */
31
32
use Symfony\Component\HttpFoundation\Request as RequestLocal;
33
use TeampassClasses\SessionManager\SessionManager;
34
use TeampassClasses\Language\Language;
35
use TeampassClasses\PerformChecks\PerformChecks;
36
use TeampassClasses\ConfigManager\ConfigManager;
37
38
39
// Load functions
40
require_once 'main.functions.php';
41
$session = SessionManager::getSession();
42
$request = RequestLocal::createFromGlobals();
43
// init
44
loadClasses('DB');
45
$lang = new Language();
46
47
// Load config if $SETTINGS not defined
48
if (empty($SETTINGS)) {
49
    $configManager = new ConfigManager();
50
    $SETTINGS = $configManager->getAllSettings();
51
}
52
53
// Do checks
54
// Instantiate the class with posted data
55
$checkUserAccess = new PerformChecks(
56
    dataSanitizer(
57
        [
58
            'type' => htmlspecialchars($request->request->get('type', ''), ENT_QUOTES, 'UTF-8'),
59
        ],
60
        [
61
            'type' => 'trim|escape',
62
        ],
63
    ),
64
    [
65
        'user_id' => returnIfSet($session->get('user-id'), null),
66
        'user_key' => returnIfSet($session->get('key'), null),
67
    ]
68
);
69
// Handle the case
70
echo $checkUserAccess->caseHandler();
71
if (
72
    $checkUserAccess->userAccessPage('items') === false ||
73
    $checkUserAccess->checkSession() === false
74
) {
75
    // Not allowed page
76
    $session->set('system-error_code', ERR_NOT_ALLOWED);
77
    include $SETTINGS['cpassman_dir'] . '/error.php';
78
    exit;
79
}
80
81
// Define Timezone
82
date_default_timezone_set($SETTINGS['timezone'] ?? 'UTC');
83
84
// Set header properties
85
header('Content-type: text/html; charset=utf-8');
86
header('Cache-Control: no-cache, no-store, must-revalidate');
87
error_reporting(E_ERROR);
88
set_time_limit(0);
89
90
// --------------------------------- //
91
92
//check for session
93
if (null !== $request->request->filter('PHPSESSID', null, FILTER_SANITIZE_FULL_SPECIAL_CHARS)) {
94
    session_id($request->request->filter('PHPSESSID', null, FILTER_SANITIZE_FULL_SPECIAL_CHARS));
95
} elseif (null !== $request->query->get('PHPSESSID')) {
96
    session_id(filter_var($request->query->get('PHPSESSID'), FILTER_SANITIZE_FULL_SPECIAL_CHARS));
97
} else {
98
    handleAttachmentError('No Session was found.', 100);
99
}
100
101
// Prepare POST variables
102
$post_user_token = $request->request->filter('user_upload_token', null, FILTER_SANITIZE_FULL_SPECIAL_CHARS);
103
$post_type_upload = $request->request->filter('type_upload', null, FILTER_SANITIZE_FULL_SPECIAL_CHARS);
104
$post_itemId = $request->request->filter('itemId', null, FILTER_SANITIZE_NUMBER_INT);
105
$post_files_number = $request->request->filter('files_number', null, FILTER_SANITIZE_NUMBER_INT);
106
$post_timezone = $request->request->filter('timezone', null, FILTER_SANITIZE_FULL_SPECIAL_CHARS);
107
$post_isNewItem = $request->request->filter('isNewItem', null, FILTER_SANITIZE_NUMBER_INT);
108
$post_randomId = $request->request->filter('randomId', null, FILTER_SANITIZE_NUMBER_INT);
109
$post_isPersonal = $request->request->filter('isPersonal', null, FILTER_SANITIZE_NUMBER_INT);
110
$post_fileSize= $request->request->filter('file_size', null, FILTER_SANITIZE_NUMBER_INT);
111
$chunk = $request->request->filter('chunk', 0, FILTER_SANITIZE_NUMBER_INT);
112
$chunks = $request->request->filter('chunks', 0, FILTER_SANITIZE_NUMBER_INT);
113
$fileName = $request->request->filter('name', '', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
114
115
// token check
116
if (null === $post_user_token) {
117
    handleAttachmentError('No user token found.', 110);
118
    exit();
119
} else {
120
    // Check post_max_size
121
    $POST_MAX_SIZE = ini_get('post_max_size');
122
    $unit = strtoupper(substr(trim($POST_MAX_SIZE), -1)); // Assurez-vous de bien gérer les espaces éventuels
123
    $units = ['G' => 1073741824, 'M' => 1048576, 'K' => 1024];
124
    $multiplier = $units[$unit] ?? 1; // Vérifie si l'unité est dans le tableau, sinon 1
125
    $maxSize = (int)$POST_MAX_SIZE * $multiplier;
126
    
127
    // CHeck if the POST is too big
128
    if (!empty($_SERVER['CONTENT_LENGTH']) && (int)$_SERVER['CONTENT_LENGTH'] > $maxSize && $maxSize > 0) {
129
        handleAttachmentError('POST exceeded maximum allowed size.', 111, 413);
130
    }
131
132
    // CHeck if file size is too big
133
    if ($post_fileSize > $maxSize && $maxSize > 0) {
134
        handleAttachmentError('File exceeds the maximum allowed size', 120, 413);
135
        die();
136
    }
137
    if (WIP === true) error_log('POST_MAX_SIZE: ' . $POST_MAX_SIZE." - CONTENT_LENGTH: ".$_SERVER['CONTENT_LENGTH']." - UNIT: ".$unit." - MAX: ".$maxSize." - MULTIPLIER: ".$multiplier." - FILE_SIZE: ".$post_fileSize);
138
    
139
    // delete expired tokens
140
    DB::delete(prefixTable('tokens'), 'end_timestamp < %i', time());
141
142
    if (
143
        null !== $session->get($post_user_token)
144
        && ($chunk < $chunks - 1)
145
        && $session->get($post_user_token) >= 0
146
    ) {
147
        // increase end_timestamp for token
148
        DB::update(
149
            prefixTable('tokens'),
150
            array(
151
                'end_timestamp' => time() + 10,
152
            ),
153
            'user_id = %i AND token = %s',
154
            $session->get('user-id'),
155
            $post_user_token
156
        );
157
    } else {
158
        // create a session if several files to upload
159
        if (
160
            null === $session->get($post_user_token)
161
            || empty($session->get($post_user_token)) === true
162
            || (int) $session->get($post_user_token) === 0
163
        ) {
164
            $session->set($post_user_token, $post_files_number);
165
        } elseif ((int) $session->get($post_user_token) > 0) {
166
            // increase end_timestamp for token
167
            DB::update(
168
                prefixTable('tokens'),
169
                array(
170
                    'end_timestamp' => time() + 30,
171
                ),
172
                'user_id = %i AND token = %s',
173
                $session->get('user-id'),
174
                $post_user_token
175
            );
176
            // decrease counter of files to upload
177
            $session->set($post_user_token, $session->get($post_user_token) - 1);
178
        } else {
179
            // no more files to upload, kill session
180
            $session->remove($post_user_token);
181
            handleAttachmentError('No user token found.', 110);
182
            die();
183
        }
184
185
        // check if token is expired
186
        $data = DB::queryFirstRow(
187
            'SELECT end_timestamp
188
            FROM ' . prefixTable('tokens') . '
189
            WHERE user_id = %i AND token = %s',
190
            $session->get('user-id'),
191
            $post_user_token
192
        );
193
        // clear user token
194
        if ((int) $session->get($post_user_token) === 0) {
195
            DB::delete(
196
                prefixTable('tokens'),
197
                'user_id = %i AND token = %s',
198
                $session->get('user-id'),
199
                $post_user_token
200
            );
201
            $session->remove($post_user_token);
202
        }
203
204
        if (time() > $data['end_timestamp']) {
205
            // too old
206
            $session->remove($post_user_token);
207
            handleAttachmentError('User token expired.', 110);
208
            die();
209
        }
210
    }
211
212
    // Load Settings
213
    if (empty($SETTINGS)) {
214
        $configManager = new ConfigManager();
215
        $SETTINGS = $configManager->getAllSettings();
216
    }
217
}
218
219
// HTTP headers for no cache etc
220
header('Expires: Mon, 26 Jul 1997 05:00:00 GMT');
221
header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT');
222
header('Cache-Control: no-store, no-cache, must-revalidate');
223
header('Cache-Control: post-check=0, pre-check=0', false);
224
225
$targetDir = $SETTINGS['path_to_upload_folder'];
226
227
$cleanupTargetDir = true; // Remove old files
228
$maxFileAge = 5 * 3600; // Temp file age in seconds
229
$MAX_FILENAME_LENGTH = 260;
230
$max_file_size_in_bytes = 2147483647; //2Go
231
232
if (null !== $post_timezone) {
233
    date_default_timezone_set($post_timezone);
234
}
235
236
// Validate the file size (Warning: the largest files supported by this code is 2GB)
237
$file_size = @filesize($_FILES['file']['tmp_name']);
238
if ($file_size === false || (int) $file_size > (int) $max_file_size_in_bytes) {
239
    handleAttachmentError('File exceeds the maximum allowed size', 120, 413);
240
}
241
if ($file_size <= 0) {
242
    handleAttachmentError('File size outside allowed lower bound', 112);
243
}
244
245
// Validate the upload
246
if (!isset($_FILES['file'])) {
247
    handleAttachmentError('No upload found in $_FILES for Filedata', 121);
248
} elseif (isset($_FILES['file']['error']) && $_FILES['file']['error'] != 0) {
249
    handleAttachmentError($_FILES['Filedata']['error'], 122);
250
} elseif (!isset($_FILES['file']['tmp_name']) || !@is_uploaded_file($_FILES['file']['tmp_name'])) {
251
    handleAttachmentError('Upload failed is_uploaded_file test.', 123);
252
} elseif (!isset($_FILES['file']['name'])) {
253
    handleAttachmentError('File has no name.', 113);
254
}
255
256
// Validate file name (for our purposes we'll just remove invalid characters)
257
$file_name = preg_replace('[^A-Za-z0-9]', '', strtolower(basename($_FILES['file']['name'])));
258
if (strlen($file_name) == 0 || strlen($file_name) > $MAX_FILENAME_LENGTH) {
259
    handleAttachmentError('Invalid file name: ' . $file_name . '.', 114);
260
}
261
262
// Validate file extension
263
$ext = strtolower(getFileExtension($_REQUEST['name']));
264
265
// Check if we should enforce extensions
266
if (($SETTINGS['upload_all_extensions_file'] ?? '0') !== '1') {
267
    if (
268
        in_array(
269
            $ext,
270
            explode(
271
                ',',
272
                $SETTINGS['upload_docext'] . ',' . $SETTINGS['upload_imagesext'] .
273
                    ',' . $SETTINGS['upload_pkgext'] . ',' . $SETTINGS['upload_otherext']
274
            )
275
        ) === false
276
    ) {
277
        handleAttachmentError('Invalid file extension.', 115, 415);
278
    }
279
}
280
281
// 5 minutes execution time
282
set_time_limit(5 * 60);
283
284
// Clean the fileName for security reasons
285
$fileInfo = pathinfo($fileName);
286
$fileName = base64_encode($fileInfo['filename']) . '.' . $fileInfo['extension'];
287
$fileFullSize = 0;
288
289
// Make sure the fileName is unique but only if chunking is disabled
290
if ($chunks < 2 && file_exists($targetDir . DIRECTORY_SEPARATOR . $fileName)) {
291
    $ext = strrpos($fileName, '.');
292
    $fileNameA = substr($fileName, 0, $ext);
293
    $fileNameB = substr($fileName, $ext);
294
295
    $count = 1;
296
    while (file_exists($targetDir . DIRECTORY_SEPARATOR . $fileNameA . '_' . $count . $fileNameB)) {
297
        ++$count;
298
    }
299
300
    $fileName = $fileNameA . '_' . $count . $fileNameB;
301
}
302
303
$filePath = $targetDir . DIRECTORY_SEPARATOR . $fileName;
304
305
// Create target dir
306
if (file_exists($targetDir) === false) {
307
    try {
308
        mkdir($targetDir, 0777, true);
309
    } catch (Exception $e) {
310
        print_r($e);
311
    }
312
}
313
314
// Remove old temp files
315
if ($cleanupTargetDir && is_dir($targetDir) && ($dir = opendir($targetDir))) {
316
    while (($file = readdir($dir)) !== false) {
317
        $tmpfilePath = $targetDir . DIRECTORY_SEPARATOR . $file;
318
319
        // Remove temp file if it is older than the max age and is not the current file
320
        if (
321
            preg_match('/\.part$/', $file)
322
            && (filemtime($tmpfilePath) < time() - $maxFileAge)
323
            && ($tmpfilePath != "{$filePath}.part")
324
        ) {
325
            fileDelete($tmpfilePath, $SETTINGS);
326
        }
327
    }
328
329
    closedir($dir);
330
} else {
331
    die('{"jsonrpc" : "2.0", "error" : {"code": 100, "message": "Failed to open temp directory."}, "id" : "id"}');
332
}
333
334
// Look for the content type header
335
$contentType = $_SERVER['CONTENT_TYPE']
336
    ?? $_SERVER['HTTP_CONTENT_TYPE']
337
    ?? '';
338
339
// Handle non multipart uploads older WebKit versions didn't support multipart in HTML5
340
if (strpos($contentType, 'multipart') !== false) {
341
    if (isset($_FILES['file']['tmp_name']) === true && is_uploaded_file($_FILES['file']['tmp_name']) === true) {
342
        // Open temp file
343
        $out = fopen("{$filePath}.part", $chunk == 0 ? 'wb' : 'ab');
344
345
        if ($out !== false) {
346
            // Read binary input stream and append it to temp file
347
            $in = fopen($_FILES['file']['tmp_name'], 'rb');
348
            $fileFullSize += (int) $_FILES['file']['size'];
349
350
            if ($in !== false) {
351
                while ($buff = fread($in, 4096)) {
352
                    fwrite($out, $buff);
353
                }
354
            } else {
355
                die('{"jsonrpc" : "2.0",
356
                    "error" : {"code": 101, "message": "Failed to open input stream."},
357
                    "id" : "id"}');
358
            }
359
            fclose($in);
360
            fclose($out);
361
362
            fileDelete($_FILES['file']['tmp_name'], $SETTINGS);
363
        } else {
364
            die('{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}');
365
        }
366
    } else {
367
        die('{"jsonrpc" : "2.0", "error" : {"code": 103, "message": "Failed to move uploaded file."}, "id" : "id"}');
368
    }
369
} else {
370
    // Open temp file
371
    $out = fopen("{$filePath}.part", $chunk == 0 ? 'wb' : 'ab');
372
373
    if ($out !== false) {
374
        // Read binary input stream and append it to temp file
375
        $in = fopen('php://input', 'rb');
376
377
        if ($in !== false) {
378
            while ($buff = fread($in, 4096)) {
379
                fwrite($out, $buff);
380
            }
381
        } else {
382
            die('{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}');
383
        }
384
        fclose($in);
385
        fclose($out);
386
    } else {
387
        die('{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}');
388
    }
389
}
390
391
// Check if file has been uploaded
392
if (!$chunks || $chunk == $chunks - 1) {
393
    // Strip the temp .part suffix off
394
    rename("{$filePath}.part", $filePath);
395
} else {
396
    // continue uploading other chunks
397
    die();
398
}
399
400
// Encrypt the file if requested
401
$newFile = encryptFile($fileName, $targetDir);
402
$newID = 0;
403
404
// Case ITEM ATTACHMENTS - Store to database
405
if (null !== $post_type_upload && $post_type_upload === 'item_attachments') {
406
    // Check case of new item
407
    if (
408
        isset($post_isNewItem) === true
409
        && (int) $post_isNewItem === 1
410
        && empty($post_randomId) === false
411
    ) {
412
        $post_itemId = $post_randomId;
413
    }
414
415
    DB::insert(
416
        prefixTable('files'),
417
        array(
418
            'id_item' => $post_itemId,
419
            'name' => 'b64:' . $fileName,   // add "b64:" prefix to indicate that the file name is base64 encoded
420
            'size' => $post_fileSize,
421
            'extension' => $fileInfo['extension'],
422
            'type' => $_FILES['file']['type'],
423
            'file' => $newFile['fileHash'],
424
            'status' => TP_ENCRYPTION_NAME,
425
        )
426
    );
427
    $newID = DB::insertId();
428
429
    // Store the key for users
430
    // Is this item a personal one?
431
    if ((int) $post_isPersonal === 0) {
432
        // It is not a personal item objectKey
433
        // This is a public object
434
        $users = DB::query(
435
            'SELECT id, public_key
436
            FROM ' . prefixTable('users') . '
437
            WHERE id NOT IN ("' . OTV_USER_ID . '","' . SSH_USER_ID . '","' . API_USER_ID . '")
438
            AND public_key != ""'
439
        );
440
        foreach ($users as $user) {
441
            // Insert in DB the new object key for this item by user
442
            DB::insert(
443
                prefixTable('sharekeys_files'),
444
                array(
445
                    'object_id' => $newID,
446
                    'user_id' => (int) $user['id'],
447
                    'share_key' => encryptUserObjectKey($newFile['objectKey'], $user['public_key']),
448
                )
449
            );
450
        }
451
    } else {
452
        DB::insert(
453
            prefixTable('sharekeys_files'),
454
            array(
455
                'object_id' => (int) $newID,
456
                'user_id' => (int) $session->get('user-id'),
457
                'share_key' => encryptUserObjectKey($newFile['objectKey'], $session->get('user-public_key')),
458
            )
459
        );
460
    }
461
462
    // Log upload into databse
463
    if ($post_isNewItem === false || (int) $post_isNewItem !== 1) {
464
        DB::insert(
465
            prefixTable('log_items'),
466
            array(
467
                'id_item' => $post_itemId,
468
                'date' => time(),
469
                'id_user' => $session->get('user-id'),
470
                'action' => 'at_modification',
471
                'raison' => 'at_add_file : ' . $fileName . ':' . $newID,
472
            )
473
        );
474
    }
475
}
476
477
// Return JSON-RPC response
478
die('{"jsonrpc" : "2.0", "result" : null, "id" : "' . $newID . '"}');
479
480
/**
481
 * Handle errors and kill script.
482
 *
483
 * @param string $message Message
484
 * 
485
 * @param integer $code    Code
486
 * 
487
 */
488
function handleAttachmentError($message, $code, $http_code = 400)
489
{
490
    // HTTP 40x code to avoid "success" in UI.
491
    http_response_code($http_code);
492
493
    // json error message
494
    echo json_encode([
495
        'jsonrpc' => '2.0',
496
        'error' => [
497
            'code' => $code,
498
            'message' => $message
499
        ],
500
        'id' => 'id'
501
    ]);
502
    
503
    // Force exit to avoid bypass filters.
504
    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...
505
}
506