1<?php
2require_once __DIR__ . '/../includes/bootstrap.php';
3
4
5if (!$USER)
6 json_out(['error' => 'Not logged in'], 401);
7if ($_SERVER['REQUEST_METHOD'] !== 'POST')
8 json_out(['error' => 'Method not allowed'], 405);
9if (!csrf_ok())
10 json_out(['error' => 'Invalid CSRF token'], 403);
11
12
13
14
15if (empty($_FILES['file']) || !isset($_FILES['file']['tmp_name'])) {
16 $phpLimit = ini_get('upload_max_filesize') ?: '?';
17 json_out(['error' => 'No file received. Check PHP upload_max_filesize (currently: ' . $phpLimit . ')'], 400);
18}
19
20$f = $_FILES['file'];
21$code = $f['error'] ?? UPLOAD_ERR_OK;
22
23
24if ($code !== UPLOAD_ERR_OK) {
25 $msgs = [
26 UPLOAD_ERR_INI_SIZE => 'File exceeds server upload limit (upload_max_filesize)',
27 UPLOAD_ERR_FORM_SIZE => 'File exceeds form size limit',
28 UPLOAD_ERR_PARTIAL => 'File was only partially uploaded',
29 UPLOAD_ERR_NO_FILE => 'No file was uploaded',
30 UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary folder',
31 UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
32 UPLOAD_ERR_EXTENSION => 'Upload blocked by a PHP extension',
33 ];
34 json_out(['error' => $msgs[$code] ?? 'Upload error code ' . $code], 400);
35}
36
37
38
39$realMime = null;
40if (function_exists('finfo_open')) {
41 $fi = finfo_open(FILEINFO_MIME_TYPE);
42 $realMime = finfo_file($fi, $f['tmp_name']);
43 finfo_close($fi);
44} elseif (function_exists('mime_content_type')) {
45 $realMime = mime_content_type($f['tmp_name']);
46}
47
48
49$allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
50if ($realMime && !in_array($realMime, $allowed)) {
51 json_out(['error' => 'Only JPEG, PNG, GIF and WebP images are allowed (detected: ' . $realMime . ')'], 400);
52}
53
54
55$maxMb = max(1, min(50, (int) cfg('max_upload_mb', '5')));
56$maxBytes = $maxMb * 1024 * 1024;
57if ($f['size'] > $maxBytes) {
58 json_out(['error' => 'Image too large — maximum is ' . $maxMb . ' MB'], 400);
59}
60
61
62$info = @getimagesize($f['tmp_name']);
63if (!$info) {
64 json_out(['error' => 'File does not appear to be a valid image'], 400);
65}
66
67
68$mimeToExt = [
69 'image/jpeg' => 'jpg',
70 'image/png' => 'png',
71 'image/gif' => 'gif',
72 'image/webp' => 'webp',
73];
74$detectedMime = $realMime ?: $info['mime'];
75$ext = $mimeToExt[$detectedMime] ?? ($mimeToExt[$info['mime']] ?? 'jpg');
76$name = 'img_' . uniqid('', true) . '.' . $ext;
77
78
79$dir = UPLOADS . '/';
80if (!is_dir($dir)) {
81 if (!mkdir($dir, 0755, true)) {
82 json_out(['error' => 'Upload directory could not be created'], 500);
83 }
84}
85if (!is_writable($dir)) {
86 json_out(['error' => 'Upload directory is not writable'], 500);
87}
88
89
90if (!move_uploaded_file($f['tmp_name'], $dir . $name)) {
91 json_out(['error' => 'Could not save uploaded file'], 500);
92}
93
94
95$ht = $dir . '.htaccess';
96if (!file_exists($ht)) {
97 file_put_contents($ht,
98 "# Deny PHP execution in uploads\n" .
99 "<FilesMatch \"\\.php\$\">\n" .
100 " Require all denied\n" .
101 "</FilesMatch>\n"
102 );
103}
104
105json_out(['ok' => true, 'url' => BASE . '/public/uploads/' . $name]);