xgit simple git

nexus

nexus

clone git clone https://kb.hax.al/nexus

api/upload.php

1 <?php
2 require_once __DIR__ . '/../includes/bootstrap.php';
3 
4 // ── Auth & method ────────────────────────────────────────────────
5 if (!$USER)
6     json_out(['error' => 'Not logged in'], 401);
7 if ($_SERVER['REQUEST_METHOD'] !== 'POST')
8     json_out(['error' => 'Method not allowed'], 405);
9 if (!csrf_ok())
10     json_out(['error' => 'Invalid CSRF token'], 403);
11 
12 // ── File present? ────────────────────────────────────────────────
13 // $_FILES is empty when PHP silently drops the upload because it exceeds
14 // php.ini upload_max_filesize or post_max_size.
15 if (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 // ── PHP upload error codes ────────────────────────────────────────
24 if ($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 // ── Detect real MIME type from file content (not browser header) ──
38 // finfo is reliable; fall back to getimagesize if finfo not available.
39 $realMime = null;
40 if (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 // Validate the real MIME type
49 $allowed = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
50 if ($realMime && !in_array($realMime, $allowed)) {
51     json_out(['error' => 'Only JPEG, PNG, GIF and WebP images are allowed (detected: ' . $realMime . ')'], 400);
52 }
53 
54 // ── Size limit ────────────────────────────────────────────────────
55 $maxMb    = max(1, min(50, (int) cfg('max_upload_mb', '5')));
56 $maxBytes = $maxMb * 1024 * 1024;
57 if ($f['size'] > $maxBytes) {
58     json_out(['error' => 'Image too large — maximum is ' . $maxMb . ' MB'], 400);
59 }
60 
61 // ── Validate it is a real image (catches non-images finfo might miss) ──
62 $info = @getimagesize($f['tmp_name']);
63 if (!$info) {
64     json_out(['error' => 'File does not appear to be a valid image'], 400);
65 }
66 
67 // ── Safe extension from detected MIME ────────────────────────────
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 // ── Ensure upload directory exists and is writable ────────────────
79 $dir = UPLOADS . '/';
80 if (!is_dir($dir)) {
81     if (!mkdir($dir, 0755, true)) {
82         json_out(['error' => 'Upload directory could not be created'], 500);
83     }
84 }
85 if (!is_writable($dir)) {
86     json_out(['error' => 'Upload directory is not writable'], 500);
87 }
88 
89 // ── Move file ─────────────────────────────────────────────────────
90 if (!move_uploaded_file($f['tmp_name'], $dir . $name)) {
91     json_out(['error' => 'Could not save uploaded file'], 500);
92 }
93 
94 // ── Add security .htaccess to uploads dir (prevent PHP execution) ─
95 $ht = $dir . '.htaccess';
96 if (!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 
105 json_out(['ok' => true, 'url' => BASE . '/public/uploads/' . $name]);