246 lines
7.7 KiB
PHP
246 lines
7.7 KiB
PHP
<?php
|
|
|
|
// phpcs:disable PSR1.Files.SideEffects.FoundWithSymbols
|
|
|
|
use dokuwiki\plugin\luxtools\Path;
|
|
|
|
if (!defined('DOKU_INC')) define('DOKU_INC', __DIR__ . '/../../../');
|
|
if (!defined('NOSESSION')) define('NOSESSION', true); // we do not use a session or authentication here (better caching)
|
|
if (!defined('DOKU_DISABLE_GZIP_OUTPUT')) define('DOKU_DISABLE_GZIP_OUTPUT', 1); // we gzip ourself here
|
|
require_once(DOKU_INC . 'inc/init.php');
|
|
|
|
global $INPUT;
|
|
|
|
$syntax = plugin_load('syntax', 'luxtools');
|
|
if (!$syntax) die('plugin disabled?');
|
|
|
|
$pathUtil = new Path($syntax->getConf('paths'));
|
|
$path = $INPUT->str('root') . $INPUT->str('file');
|
|
|
|
/**
|
|
* Send a file to the client with basic caching headers.
|
|
*
|
|
* @param string $path
|
|
* @param string $mime
|
|
* @param bool $download
|
|
* @param string|null $downloadName
|
|
* @param string|null $etag
|
|
* @param int|null $mtime
|
|
* @param int|null $maxAge
|
|
* @return void
|
|
*/
|
|
function luxtools_sendfile($path, $mime, $download = false, $downloadName = null, $etag = null, $mtime = null, $maxAge = null)
|
|
{
|
|
header('Content-Type: ' . $mime);
|
|
header('X-Content-Type-Options: nosniff');
|
|
|
|
if ($download) {
|
|
$downloadName = $downloadName ?: basename($path);
|
|
header('Content-Disposition: attachment; filename="' . $downloadName . '"');
|
|
}
|
|
|
|
if ($etag !== null) {
|
|
header('ETag: "' . $etag . '"');
|
|
}
|
|
if ($mtime !== null) {
|
|
header('Last-Modified: ' . gmdate('D, d M Y H:i:s', $mtime) . ' GMT');
|
|
}
|
|
|
|
if ($maxAge !== null) {
|
|
header('Cache-Control: public, max-age=' . (int)$maxAge . ', immutable');
|
|
}
|
|
|
|
// Conditional request handling
|
|
if ($etag !== null && isset($_SERVER['HTTP_IF_NONE_MATCH'])) {
|
|
if (trim($_SERVER['HTTP_IF_NONE_MATCH']) === '"' . $etag . '"') {
|
|
http_status(304);
|
|
exit;
|
|
}
|
|
}
|
|
if ($mtime !== null && isset($_SERVER['HTTP_IF_MODIFIED_SINCE'])) {
|
|
$ims = strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE']);
|
|
if ($ims !== false && $ims >= $mtime) {
|
|
http_status(304);
|
|
exit;
|
|
}
|
|
}
|
|
|
|
http_sendfile($path);
|
|
readfile($path);
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Create a thumbnail file using GD.
|
|
*
|
|
* @param string $src
|
|
* @param string $dst
|
|
* @param int $maxW
|
|
* @param int $maxH
|
|
* @param string $dstFormat 'jpg' or 'png'
|
|
* @param int $quality JPEG quality 0-100
|
|
* @return bool
|
|
*/
|
|
function luxtools_create_thumb_gd($src, $dst, $maxW, $maxH, $dstFormat, $quality)
|
|
{
|
|
if (!function_exists('imagecreatetruecolor')) return false;
|
|
if (!is_readable($src)) return false;
|
|
|
|
$info = @getimagesize($src);
|
|
if (!is_array($info) || empty($info[0]) || empty($info[1]) || empty($info['mime'])) return false;
|
|
|
|
$srcW = (int)$info[0];
|
|
$srcH = (int)$info[1];
|
|
$srcMime = (string)$info['mime'];
|
|
if ($srcW <= 0 || $srcH <= 0) return false;
|
|
|
|
$maxW = max(1, (int)$maxW);
|
|
$maxH = max(1, (int)$maxH);
|
|
|
|
$scale = min($maxW / $srcW, $maxH / $srcH, 1);
|
|
$dstW = max(1, (int)floor($srcW * $scale));
|
|
$dstH = max(1, (int)floor($srcH * $scale));
|
|
|
|
switch ($srcMime) {
|
|
case 'image/jpeg':
|
|
if (!function_exists('imagecreatefromjpeg')) return false;
|
|
$srcImg = @imagecreatefromjpeg($src);
|
|
break;
|
|
case 'image/png':
|
|
if (!function_exists('imagecreatefrompng')) return false;
|
|
$srcImg = @imagecreatefrompng($src);
|
|
break;
|
|
case 'image/gif':
|
|
if (!function_exists('imagecreatefromgif')) return false;
|
|
$srcImg = @imagecreatefromgif($src);
|
|
break;
|
|
case 'image/webp':
|
|
if (!function_exists('imagecreatefromwebp')) return false;
|
|
$srcImg = @imagecreatefromwebp($src);
|
|
break;
|
|
default:
|
|
return false;
|
|
}
|
|
|
|
if (!$srcImg) return false;
|
|
|
|
$dstImg = imagecreatetruecolor($dstW, $dstH);
|
|
if (!$dstImg) {
|
|
imagedestroy($srcImg);
|
|
return false;
|
|
}
|
|
|
|
// Preserve transparency for formats that support it
|
|
if ($dstFormat === 'png') {
|
|
imagealphablending($dstImg, false);
|
|
imagesavealpha($dstImg, true);
|
|
$transparent = imagecolorallocatealpha($dstImg, 0, 0, 0, 127);
|
|
imagefilledrectangle($dstImg, 0, 0, $dstW, $dstH, $transparent);
|
|
}
|
|
|
|
$ok = imagecopyresampled($dstImg, $srcImg, 0, 0, 0, 0, $dstW, $dstH, $srcW, $srcH);
|
|
imagedestroy($srcImg);
|
|
|
|
if (!$ok) {
|
|
imagedestroy($dstImg);
|
|
return false;
|
|
}
|
|
|
|
// Write to a temporary file then rename atomically
|
|
$tmp = $dst . '.tmp.' . getmypid();
|
|
@io_mkdir_p(dirname($dst));
|
|
|
|
$written = false;
|
|
if ($dstFormat === 'png') {
|
|
if (!function_exists('imagepng')) {
|
|
$written = false;
|
|
} else {
|
|
// compression: 0 (none) .. 9 (max). Use a reasonable default.
|
|
$written = @imagepng($dstImg, $tmp, 6);
|
|
}
|
|
} else {
|
|
if (!function_exists('imagejpeg')) {
|
|
$written = false;
|
|
} else {
|
|
$q = max(0, min(100, (int)$quality));
|
|
$written = @imagejpeg($dstImg, $tmp, $q);
|
|
}
|
|
}
|
|
imagedestroy($dstImg);
|
|
|
|
if (!$written || !is_file($tmp)) {
|
|
@unlink($tmp);
|
|
return false;
|
|
}
|
|
|
|
// Best-effort atomic move
|
|
if (!@rename($tmp, $dst)) {
|
|
// fallback copy
|
|
$ok = @copy($tmp, $dst);
|
|
@unlink($tmp);
|
|
if (!$ok) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
$pathInfo = $pathUtil->getPathInfo($path, false);
|
|
if ($pathUtil::isWikiControlled($pathInfo['path'])) {
|
|
throw new Exception('Access to wiki files is not allowed');
|
|
}
|
|
|
|
if (!is_readable($pathInfo['path'])) {
|
|
header('Content-Type: text/plain');
|
|
http_status(404);
|
|
echo 'Path not readable: ' . $pathInfo['path'];
|
|
exit;
|
|
}
|
|
[$ext, $mime, $download] = mimetype($pathInfo['path'], false);
|
|
|
|
// Optional thumbnail mode: ?thumb=1&w=150&h=150
|
|
$thumb = (int)$INPUT->int('thumb');
|
|
$w = (int)$INPUT->int('w');
|
|
$h = (int)$INPUT->int('h');
|
|
$q = (int)$INPUT->int('q');
|
|
if ($q <= 0) $q = 80;
|
|
|
|
$isImage = is_string($mime) && str_starts_with($mime, 'image/');
|
|
$wantThumb = $thumb === 1 && $isImage && ($w > 0 || $h > 0);
|
|
|
|
if ($wantThumb) {
|
|
if ($w <= 0) $w = $h;
|
|
if ($h <= 0) $h = $w;
|
|
|
|
global $conf;
|
|
$srcMtime = @filemtime($pathInfo['path']) ?: time();
|
|
|
|
// Decide output format (prefer PNG when transparency is likely)
|
|
$dstFormat = ($mime === 'image/png' || $mime === 'image/gif') ? 'png' : 'jpg';
|
|
$dstMime = ($dstFormat === 'png') ? 'image/png' : 'image/jpeg';
|
|
$hash = sha1($pathInfo['path'] . '|' . $srcMtime . '|w=' . $w . '|h=' . $h . '|q=' . $q . '|f=' . $dstFormat);
|
|
$sub = substr($hash, 0, 2);
|
|
$cacheDir = rtrim($conf['cachedir'], '/');
|
|
$thumbPath = $cacheDir . '/luxtools/thumbs/' . $sub . '/' . $hash . '.' . $dstFormat;
|
|
|
|
if (!is_file($thumbPath)) {
|
|
$ok = luxtools_create_thumb_gd($pathInfo['path'], $thumbPath, $w, $h, $dstFormat, $q);
|
|
if (!$ok || !is_file($thumbPath)) {
|
|
// Fallback: serve original if we cannot thumbnail
|
|
luxtools_sendfile($pathInfo['path'], $mime, $download, basename($pathInfo['path']), null, $srcMtime, 3600);
|
|
}
|
|
}
|
|
|
|
// Cached thumbs are immutable because filename includes mtime
|
|
luxtools_sendfile($thumbPath, $dstMime, false, null, $hash, @filemtime($thumbPath) ?: $srcMtime, 31536000);
|
|
}
|
|
|
|
// Default: serve original file
|
|
$basename = basename($pathInfo['path']);
|
|
luxtools_sendfile($pathInfo['path'], $mime, $download, $basename, null, @filemtime($pathInfo['path']) ?: null, 3600);
|
|
} catch (Exception $e) {
|
|
header('Content-Type: text/plain');
|
|
http_status(403);
|
|
echo $e->getMessage();
|
|
exit;
|
|
}
|