From ab4ba3487a9382c261758fd933e9f091bd98415e Mon Sep 17 00:00:00 2001 From: Christian Haschek Date: Fri, 22 May 2026 15:32:19 +0200 Subject: [PATCH 1/2] fix: improve file type detection and Redis connection handling --- .gitignore | 3 +- src/api/info.php | 22 +++++++- .../image/image.controller.php | 2 +- src/inc/core.php | 56 +++++++++++++------ web/index.php | 16 ++++-- 5 files changed, 73 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index da7e0fad..c35614fd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ notice.txt .vscode .phpunit.result.cache docs/ -.claude \ No newline at end of file +.claude +.antigravity* \ No newline at end of file diff --git a/src/api/info.php b/src/api/info.php index fbf2206f..7b0df461 100644 --- a/src/api/info.php +++ b/src/api/info.php @@ -41,10 +41,28 @@ function getInfoAboutHash($hash) return array('status'=>'err','reason'=>'File not found'); $size = filesize($file); $size_hr = renderSize($size); - $content_type = exec("file -bi " . escapeshellarg($file)); + + $content_type = false; + try { + if (class_exists('finfo')) { + $finfo = new finfo(FILEINFO_MIME); + $content_type = $finfo->file($file); + } + } catch (\Throwable $t) { + // ignore + } + + if (!$content_type && function_exists('mime_content_type')) { + $content_type = @mime_content_type($file); + } + + if (!$content_type && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { + $content_type = @exec("file -bi " . escapeshellarg($file)); + } + + $type = $content_type; if($content_type && strpos($content_type,'/')!==false && strpos($content_type,';')!==false) { - $type = $content_type; $c = explode(';',$type); $type = $c[0]; } diff --git a/src/content-controllers/image/image.controller.php b/src/content-controllers/image/image.controller.php index 49b27dc7..d2651171 100644 --- a/src/content-controllers/image/image.controller.php +++ b/src/content-controllers/image/image.controller.php @@ -314,7 +314,7 @@ function gifToMP4($gifpath,$target) function saveObjOfImage($im,$path,$type) { - $tmppath = '/tmp/'.getNewHash($type,12); + $tmppath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . getNewHash($type,12); switch($type) { case 'jpeg': diff --git a/src/inc/core.php b/src/inc/core.php index a48bc016..1f221b78 100644 --- a/src/inc/core.php +++ b/src/inc/core.php @@ -350,10 +350,15 @@ function storageControllerUpload($hash) function stringInFile($string,$file) { - $handle = fopen($file, 'r'); + if (!file_exists($file)) return false; + $handle = @fopen($file, 'r'); + if (!$handle) return false; while (($line = fgets($handle)) !== false) { $line=trim($line); - if($line==$string) return true; + if($line==$string) { + fclose($handle); + return true; + } } fclose($handle); return false; @@ -463,18 +468,24 @@ function renderSize($byte) function getTypeOfFile($url) { - // on linux use the "file" command or it will handle everything as octet-stream - if(strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') - { - $content_type = exec("file -bi " . escapeshellarg($url)); - if($content_type && strpos($content_type,'/')!==false && strpos($content_type,';')!==false) - $type = $content_type; + $type = false; + try { + if (class_exists('finfo')) { + $finfo = new finfo(FILEINFO_MIME); + $type = $finfo->file($url); + } + } catch (\Throwable $t) { + // ignore } - else - { - //for windows we'll use mime_content_type. Make sure you have enabled the "exif" extension in php.ini - $type = mime_content_type($url); + + if (!$type && function_exists('mime_content_type')) { + $type = @mime_content_type($url); } + + if (!$type && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { + $type = @exec("file -bi " . escapeshellarg($url)); + } + if(!$type) return false; if(startsWith($type,'text')) return 'text'; $arr = explode(';', trim($type)); @@ -1366,13 +1377,22 @@ function rebuildMeta(){ function getFileMimeType($file) { try { - $finfo = new finfo(FILEINFO_MIME_TYPE); - return $finfo->file($file); - } catch (Exception $e) { - //fallback to shell command if finfo is not available - $mimeType = shell_exec('file --mime-type -b ' . escapeshellarg($file)); - return trim($mimeType); + if (class_exists('finfo')) { + $finfo = new finfo(FILEINFO_MIME_TYPE); + return $finfo->file($file); + } + } catch (\Throwable $e) { + // ignore } + + if (function_exists('mime_content_type')) { + $mime = @mime_content_type($file); + if ($mime) return $mime; + } + + //fallback to shell command if finfo and mime_content_type are not available + $mimeType = shell_exec('file --mime-type -b ' . escapeshellarg($file)); + return trim($mimeType); } function getRelativeToDataPath(string $path): string diff --git a/web/index.php b/web/index.php index 88280cfa..720c2a02 100644 --- a/web/index.php +++ b/web/index.php @@ -22,13 +22,21 @@ // redis if(!defined('REDIS_CACHING') || REDIS_CACHING == true) { - $GLOBALS['redis'] = new Redis(); - $GLOBALS['redis']->connect((!defined('REDIS_SERVER'))?'localhost':REDIS_SERVER, (!defined('REDIS_PORT'))?6379:REDIS_PORT); + try { + $GLOBALS['redis'] = new Redis(); + $redis_host = (!defined('REDIS_SERVER')) ? 'localhost' : REDIS_SERVER; + $redis_port = (!defined('REDIS_PORT')) ? 6379 : REDIS_PORT; + // connect with 1.0 second timeout to prevent locking up PHP workers if Redis is down + @$GLOBALS['redis']->connect($redis_host, $redis_port, 1.0); + } catch (\Throwable $e) { + $GLOBALS['redis'] = null; + error_log("Failed to connect to Redis: " . $e->getMessage()); + } } -//parse the URL to an array and filter it -$url = array_filter(explode('/',ltrim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH),'/'))); +//parse the URL to an array and filter it, keeping '0' values +$url = array_filter(explode('/',ltrim(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH),'/')), 'strlen'); if($url[0] == 'api') { From 169fb25ef2bcd0edafb6cc6b44a9038f68ffefaf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 22 May 2026 13:43:51 +0000 Subject: [PATCH 2/2] merge master into v3 and resolve conflicts Agent-Logs-Url: https://github.com/HaschekSolutions/pictshare/sessions/b4a1d30e-2644-45aa-b4aa-72acee1af475 Co-authored-by: geek-at <2073090+geek-at@users.noreply.github.com> --- .github/workflows/build-docker.yml | 3 +- .github/workflows/test.yml | 6 +- .gitignore | 5 +- docker/Dockerfile | 3 + docker/rootfs/start.sh | 3 + logs/.gitignore | 2 - .../text/text.controller.php | 15 ++++- src/inc/core.php | 62 +++++++++++++------ src/templates/admin.logs-table.html.php | 61 ++++++++++++++++-- src/templates/admin.stats-table.html.php | 12 +++- src/templates/admin.stats.html.php | 6 +- src/templates/index.html.php | 2 +- src/templates/text.html.php | 11 +++- src/templates/video.html.php | 7 ++- tests/Unit/StatsCacheTest.php | 4 +- tmp/.gitignore | 2 - web/index.php | 2 +- 17 files changed, 160 insertions(+), 46 deletions(-) delete mode 100644 logs/.gitignore delete mode 100755 tmp/.gitignore diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 73836d62..5bd562e4 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -56,4 +56,5 @@ jobs: platforms: linux/amd64,linux/arm64 push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file + labels: ${{ steps.meta.outputs.labels }} + build-args: PICTSHARE_VERSION=${{ steps.meta.outputs.version }} \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e87853e9..10b35e36 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,15 +22,17 @@ jobs: - name: Install dev dependencies (PHPUnit) run: | docker run --rm \ + --entrypoint bash \ -v "${{ github.workspace }}:/app/public" \ pictshare-test \ - bash -c "cd /app/public/src/lib && composer install --no-interaction" + -c "cd /app/public/src/lib && composer install --no-interaction" - name: Run PHPUnit run: | docker run --rm \ + --entrypoint php \ -v "${{ github.workspace }}:/app/public" \ pictshare-test \ - php /app/public/src/lib/vendor/bin/phpunit \ + /app/public/src/lib/vendor/bin/phpunit \ --configuration /app/public/phpunit.xml \ --colors=never diff --git a/.gitignore b/.gitignore index c35614fd..905ddab5 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,7 @@ notice.txt .phpunit.result.cache docs/ .claude -.antigravity* \ No newline at end of file +.antigravity* +logs +data +tmp diff --git a/docker/Dockerfile b/docker/Dockerfile index deb686d2..90c94ff7 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -30,6 +30,9 @@ WORKDIR /app/public/ VOLUME /app/public/data VOLUME /app/public/logs +ARG PICTSHARE_VERSION=git +ENV PICTSHARE_VERSION=${PICTSHARE_VERSION} + EXPOSE 80 ENTRYPOINT ["/etc/start.sh"] \ No newline at end of file diff --git a/docker/rootfs/start.sh b/docker/rootfs/start.sh index 0ae7c04f..262455ff 100644 --- a/docker/rootfs/start.sh +++ b/docker/rootfs/start.sh @@ -19,6 +19,8 @@ _maxUploadSize() { _filePermissions() { echo "[i] Setting file permissions" + mkdir -p /app/public/tmp + chmod -R 755 /app/public/tmp } _buildConfig() { @@ -58,6 +60,7 @@ _buildConfig() { echo "define('REDIS_SERVER', '${REDIS_SERVER:-/run/redis/redis.sock}');" echo "define('REDIS_PORT', ${REDIS_PORT:-6379});" echo "define('ADMIN_PASSWORD', '${ADMIN_PASSWORD:-}');" + echo "define('PICTSHARE_VERSION', '${PICTSHARE_VERSION:-git}');" } # starting redis diff --git a/logs/.gitignore b/logs/.gitignore deleted file mode 100644 index c96a04f0..00000000 --- a/logs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -* -!.gitignore \ No newline at end of file diff --git a/src/content-controllers/text/text.controller.php b/src/content-controllers/text/text.controller.php index d291d3ec..d57cb340 100644 --- a/src/content-controllers/text/text.controller.php +++ b/src/content-controllers/text/text.controller.php @@ -36,8 +36,19 @@ public function handleHash($hash,$url,$path=false) exit; } } - else - return renderTemplate('text.html.php',array('hash'=>$hash,'content'=>htmlentities(file_get_contents($path)))); + else { + $fileSize = filesize($path); + $memLimit = ini_get('memory_limit'); + $memBytes = (int)$memLimit; + if (str_ends_with($memLimit, 'G')) $memBytes = (int)$memLimit * 1073741824; + elseif (str_ends_with($memLimit, 'M')) $memBytes = (int)$memLimit * 1048576; + elseif (str_ends_with($memLimit, 'K')) $memBytes = (int)$memLimit * 1024; + + if ($memBytes > 0 && $fileSize > $memBytes / 4) + return renderTemplate('text.html.php', ['hash' => $hash, 'content' => null, 'filesize' => $fileSize]); + + return renderTemplate('text.html.php', ['hash' => $hash, 'content' => htmlentities(file_get_contents($path)), 'filesize' => $fileSize]); + } } public function handleUpload($tmpfile,$hash=false,$passthrough=false) diff --git a/src/inc/core.php b/src/inc/core.php index 1f221b78..9d0a6185 100644 --- a/src/inc/core.php +++ b/src/inc/core.php @@ -92,14 +92,23 @@ function architect($u) session_start(); switch($u[1]){ case 'rebuild-meta': - if(!$_SESSION['admin']) + if(!$_SESSION['admin']) { header('Location: /admin'); + return; + } return renderTemplate('index.html.php',['main'=>'
'.rebuildMeta().'
']); case 'stats': if(!$_SESSION['admin']) { header('Location: /admin'); return; } + if(isset($u[2]) && $u[2] === 'delete' && isset($u[3])) { + $hash = $u[3]; + deleteHash($hash); + if(isset($GLOBALS['redis']) && $GLOBALS['redis']) + $GLOBALS['redis']->hDel('stats:index', $hash); + return; // empty response — HTMX removes the row + } if(isset($u[2]) && $u[2] === 'data') { // HTMX fragment — return bare tbody rows, no layout wrapper $page = max(1, (int)($_GET['page'] ?? 1)); @@ -116,19 +125,31 @@ function architect($u) : 0; return renderTemplate('index.html.php',['main'=>renderTemplate('admin.stats.html.php',['built_at'=>$builtAt])]); case 'logs': - if(!$_SESSION['admin']) + if(!$_SESSION['admin']) { header('Location: /admin'); - switch($u[2]) - { - case 'app': - return renderTemplate('index.html.php',['main'=>renderTemplate('admin.logs-table.html.php',['type'=>'app','logs'=>getLogs('app',$u[3])])]); - case 'error': - return renderTemplate('index.html.php',['main'=>renderTemplate('admin.logs-table.html.php',['type'=>'error','logs'=>getLogs('error',$u[3])])]); - case 'views': - return renderTemplate('index.html.php',['main'=>renderTemplate('admin.logs-table.html.php',['type'=>'views','logs'=>getLogs('views',$u[3])])]); - default: - return renderTemplate('index.html.php',['main'=>renderTemplate('admin.logs.html.php')]); + return; } + if(in_array($u[2], ['app','error','views'])) { + $type = $u[2]; + $filter = $u[3] ?? false; + $perPage = 200; + $page = max(1, (int)($_GET['page'] ?? 1)); + $allLogs = array_reverse(getLogs($type, $filter)); + $total = count($allLogs); + $total_pages = max(1, (int)ceil($total / $perPage)); + $page = min($page, $total_pages); + $logs = array_slice($allLogs, ($page - 1) * $perPage, $perPage); + return renderTemplate('index.html.php',['main'=>renderTemplate('admin.logs-table.html.php',[ + 'type' => $type, + 'filter' => $filter, + 'logs' => $logs, + 'page' => $page, + 'total_pages' => $total_pages, + 'total' => $total, + 'perPage' => $perPage, + ])]); + } + return renderTemplate('index.html.php',['main'=>renderTemplate('admin.logs.html.php')]); case 'reports': if(!$_SESSION['admin']) { header('Location: /admin'); @@ -1168,7 +1189,7 @@ function isCacheStale(): bool if (!isset($GLOBALS['redis']) || !$GLOBALS['redis']) return true; $builtAt = $GLOBALS['redis']->get('stats:built_at'); if (!$builtAt) return true; - return (time() - (int)$builtAt) > 300; + return (time() - (int)$builtAt) > 1800; } function rebuildStatsCache(): void @@ -1422,12 +1443,17 @@ function getRelativeToDataPath(string $path): string } function serveFile($path){ - $relativePath = getRelativeToDataPath($path); - //since x-accel-redirect does not support paths outside its root, we need to check if the path is relative or absolute - if(startsWith($relativePath,'..')) + try { + $relativePath = getRelativeToDataPath($path); + //since x-accel-redirect does not support paths outside its root, we need to check if the path is relative or absolute + if(startsWith($relativePath,'..')) + readfile($path); + else + header('X-Accel-Redirect: '. $relativePath); + } catch (InvalidArgumentException) { + // realpath() failed (file may not exist or path is unresolvable); serve directly readfile($path); - else - header('X-Accel-Redirect: '. $relativePath); + } } function getReports(){ diff --git a/src/templates/admin.logs-table.html.php b/src/templates/admin.logs-table.html.php index 3d4e5e44..9c978bd7 100644 --- a/src/templates/admin.logs-table.html.php +++ b/src/templates/admin.logs-table.html.php @@ -8,8 +8,59 @@

-
-    
-
\ No newline at end of file + + +

+ Showing of entries (latest first) +

+ + 1): ?> + + + +
+ + 1): ?> + + diff --git a/src/templates/admin.stats-table.html.php b/src/templates/admin.stats-table.html.php index a41a6f2c..2699a744 100644 --- a/src/templates/admin.stats-table.html.php +++ b/src/templates/admin.stats-table.html.php @@ -1,5 +1,5 @@ -No uploads found. +No uploads found. @@ -8,16 +8,24 @@ + =1073741824?round($s/1073741824,1).' GB':($s>=1048576?round($s/1048576,1).' MB':($s>=1024?round($s/1024,1).' KB':$s.' B')); ?> View logs + + Delete + diff --git a/src/templates/admin.stats.html.php b/src/templates/admin.stats.html.php index db43c808..7d0d5acc 100644 --- a/src/templates/admin.stats.html.php +++ b/src/templates/admin.stats.html.php @@ -8,7 +8,8 @@

Stats

0): ?> -

Last updated: minute(s) ago

+ +

Next refresh in: = 60 ? round($secsLeft/60).' minute(s)' : $secsLeft.' second(s)'?>

Redis unavailable — stats may load slowly.
@@ -56,6 +57,7 @@ class="form-control" Views log + - Loading… + Loading… diff --git a/src/templates/index.html.php b/src/templates/index.html.php index 8f8a5a04..23fb6a86 100644 --- a/src/templates/index.html.php +++ b/src/templates/index.html.php @@ -58,7 +58,7 @@ diff --git a/src/templates/text.html.php b/src/templates/text.html.php index 5c964009..da52f4e1 100644 --- a/src/templates/text.html.php +++ b/src/templates/text.html.php @@ -37,10 +37,17 @@

- Raw + Raw Download + +
+ This file is too large to display in the browser + (=1073741824?round($s/1073741824,1).' GB':($s>=1048576?round($s/1048576,1).' MB':round($s/1024,1).' KB'); ?>). + Use the Download button above. +
+
- +
diff --git a/src/templates/video.html.php b/src/templates/video.html.php index 0ee58a3f..8b57e559 100644 --- a/src/templates/video.html.php +++ b/src/templates/video.html.php @@ -37,9 +37,10 @@ +
-
- Raw Download + Raw Download