xgit simple git

nexus

nexus

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

api/upload.php


1<?php
2require_once __DIR__ . '/../includes/bootstrap.php';
3 
4// ── Auth & method ────────────────────────────────────────────────
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// ── 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.
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// ── PHP upload error codes ────────────────────────────────────────
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// ── 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;
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// Validate the real MIME type
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// ── Size limit ────────────────────────────────────────────────────
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// ── Validate it is a real image (catches non-images finfo might miss) ──
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// ── 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 . '/';
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// ── Move file ─────────────────────────────────────────────────────
90if (!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';
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]);