added passthrough and updated documentation a bit

This commit is contained in:
Chris
2025-07-12 11:30:36 +02:00
parent 653cb2f7ac
commit 6b4293fc3b
14 changed files with 138 additions and 111 deletions

View File

@@ -57,6 +57,8 @@ Then open http://localhost:8080 in your browser
#### Dropped configuration options
- TITLE
- UPLOAD_FORM_LOCATION
- ALLOW_BLOATING
#### File name in URL
In Picthsare v3 we have changed the requirement for the image to be the last part of the URL. While with older PictShare Versions you could put the file name at any place of the URL, we now require the last part of the URL to be the file name.

View File

@@ -5,7 +5,6 @@ services:
context: .
dockerfile: docker/Dockerfile
environment:
- SERVER_NAME=:80
- URL=http://localhost:8080/
- MAX_UPLOAD_SIZE=1000
# Security settings
@@ -43,8 +42,8 @@ services:
- LOG_VIEWS=true
# caching
- REDIS_CACHING=true
- REDIS_SERVER=/run/redis/redis.sock
- REDIS_PORT=0
- REDIS_SERVER=localhost
- REDIS_PORT=6379
ports:
- 8080:80
volumes:
@@ -52,4 +51,5 @@ services:
- ./logs:/app/public/logs
- ./src:/app/public/src
- ./web:/app/public/web
- ./tmp:/app/public/tmp
- ./tmp/redis:/var/lib/redis

View File

@@ -40,9 +40,8 @@ services:
- LOG_VIEWS=false
# caching
- REDIS_CACHING=true
- REDIS_SERVER=/run/redis/redis.sock
- REDIS_PORT=0
- SERVER_NAME=:80 # required by caddy
- REDIS_SERVER=localhost
- REDIS_PORT=6379
ports:
- 8080:80
volumes:

View File

@@ -29,11 +29,9 @@ _buildConfig() {
echo "define('CONTENTCONTROLLERS', '${CONTENTCONTROLLERS:-}');"
echo "define('MASTER_DELETE_CODE', '${MASTER_DELETE_CODE:-}');"
echo "define('MASTER_DELETE_IP', '${MASTER_DELETE_IP:-}');"
echo "define('UPLOAD_FORM_LOCATION', '${UPLOAD_FORM_LOCATION:-}');"
echo "define('UPLOAD_CODE', '${UPLOAD_CODE:-}');"
echo "define('LOG_UPLOADER', ${LOG_UPLOADER:-false});"
echo "define('MAX_RESIZED_IMAGES',${MAX_RESIZED_IMAGES:--1});"
echo "define('ALLOW_BLOATING', ${ALLOW_BLOATING:-false});"
echo "define('SHOW_ERRORS', ${SHOW_ERRORS:-false});"
echo "define('JPEG_COMPRESSION', ${JPEG_COMPRESSION:-90});"
echo "define('PNG_COMPRESSION', ${PNG_COMPRESSION:-6});"

View File

@@ -12,7 +12,7 @@ class IdenticonController implements ContentController
//returns all extensions registered by this type of content
public function getRegisteredExtensions(){return array('identicon');}
public function handleHash($hash,$url)
public function handleHash($hash,$url,$path=false)
{
unset($url[array_search('identicon',$url)]);
$url = array_values($url);
@@ -29,7 +29,7 @@ class IdenticonController implements ContentController
echo $icon;
}
public function handleUpload($tmpfile,$hash=false)
public function handleUpload($tmpfile,$hash=false,$passthrough=false)
{
return array('status'=>'err','hash'=>$hash,'reason'=>'Cannot upload to Identicons');
}

View File

@@ -23,7 +23,7 @@ class ImageController implements ContentController
public function getRegisteredExtensions(){return array('png','bmp','gif','jpg','jpeg','x-png','webp');}
public function handleUpload($tmpfile,$hash=false)
public function handleUpload($tmpfile,$hash=false,$passthrough=false)
{
$type = exif_imagetype($tmpfile); //http://www.php.net/manual/en/function.exif-imagetype.php
switch($type)
@@ -91,14 +91,16 @@ class ImageController implements ContentController
return array('status'=>'err','hash'=>$hash,'reason'=>'Custom hash already exists');
}
storeFile($tmpfile,$hash,true);
if($passthrough===false)
storeFile($tmpfile,$hash,true);
return array('status'=>'ok','hash'=>$hash,'url'=>getURL().$hash);
}
public function handleHash($hash,$url)
public function handleHash($hash,$url,$path=false)
{
$path = getDataDir().DS.$hash.DS.$hash;
if($path===false)
$path = getDataDir().DS.$hash.DS.$hash;
$type = getExtensionOfFilename($hash);
//get all our sub files where all the good functions lie
@@ -151,7 +153,7 @@ class ImageController implements ContentController
//so if we take all parameters in key=>value form and hash it
//we get one nice little hash for every eventuality
$modhash = md5(http_build_query($modifiers,'',','));
$newpath = getDataDir().DS.$hash.DS.$modhash.'_'.$hash;
$newpath = dirname($path).DS.$modhash.'_'.$hash;
$im = $this->getObjOfImage($path);
$f = new Filter();
@@ -183,7 +185,7 @@ class ImageController implements ContentController
break;
case 'mp4':
$mp4path = getDataDir().DS.$hash.DS.$hash.'mp4';
$mp4path = dirname($path).DS.$hash.'mp4';
if(!file_exists($mp4path))
$this->gifToMP4($path,$mp4path);
$path = $mp4path;
@@ -199,7 +201,7 @@ class ImageController implements ContentController
}
header ("Content-type: image/jpeg");
header('X-Accel-Redirect: '.str_replace(getDataDir().DS,'',$preview));
serveFile($preview);
exit;
}
else if(in_array('download',$url))
@@ -212,7 +214,7 @@ class ImageController implements ContentController
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($path));
header('X-Accel-Redirect: '.str_replace(getDataDir().DS,'',$path));
serveFile($path);
exit;
}
}
@@ -244,7 +246,7 @@ class ImageController implements ContentController
header ("Last-Modified: ".gmdate('D, d M Y H:i:s ', filemtime($path)) . 'GMT');
header ("ETag: $hash");
header('Cache-control: public, max-age=31536000');
header('X-Accel-Redirect: '.str_replace(getDataDir().DS,'',$path));
serveFile($path);
break;
case 'png':
@@ -252,7 +254,7 @@ class ImageController implements ContentController
header ("Last-Modified: ".gmdate('D, d M Y H:i:s ', filemtime($path)) . 'GMT');
header ("ETag: $hash");
header('Cache-control: public, max-age=31536000');
header('X-Accel-Redirect: '.str_replace(getDataDir().DS,'',$path));
serveFile($path);
break;
@@ -261,7 +263,7 @@ class ImageController implements ContentController
header ("Last-Modified: ".gmdate('D, d M Y H:i:s ', filemtime($path)) . 'GMT');
header ("ETag: $hash");
header('Cache-control: public, max-age=31536000');
header('X-Accel-Redirect: '.str_replace(getDataDir().DS,'',$path));
serveFile($path);
break;
case 'webp':
@@ -269,7 +271,7 @@ class ImageController implements ContentController
header ("Last-Modified: ".gmdate('D, d M Y H:i:s ', filemtime($path)) . 'GMT');
header ("ETag: $hash");
header('Cache-control: public, max-age=31536000');
header('X-Accel-Redirect: '.str_replace(getDataDir().DS,'',$path));
serveFile($path);
break;
}
}

View File

@@ -7,7 +7,7 @@ class PlaceholderController implements ContentController
//returns all extensions registered by this type of content
public function getRegisteredExtensions(){return array('placeholder');}
public function handleHash($hash,$url)
public function handleHash($hash,$url,$path=false)
{
$path = getDataDir().DS.$hash.DS.$hash;
@@ -43,7 +43,7 @@ class PlaceholderController implements ContentController
imagejpeg($img,null,(defined('JPEG_COMPRESSION')?JPEG_COMPRESSION:90));
}
public function handleUpload($tmpfile,$hash=false)
public function handleUpload($tmpfile,$hash=false,$passthrough=false)
{
return array('status'=>'err','hash'=>$hash,'reason'=>'Cannot upload to placeholder image');
}

View File

@@ -13,7 +13,7 @@ class TextController implements ContentController
//returns all extensions registered by this type of content
public function getRegisteredExtensions(){return array('txt','text','csv');}
public function handleHash($hash,$url)
public function handleHash($hash,$url,$path=false)
{
$path = getDataDir().DS.$hash.DS.$hash;
@@ -32,7 +32,7 @@ class TextController implements ContentController
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($path));
header('X-Accel-Redirect: '.str_replace(getDataDir().DS,'',$path));
serveFile($path);
exit;
}
}
@@ -40,7 +40,7 @@ class TextController implements ContentController
return renderTemplate('text.html.php',array('hash'=>$hash,'content'=>htmlentities(file_get_contents($path))));
}
public function handleUpload($tmpfile,$hash=false)
public function handleUpload($tmpfile,$hash=false,$passthrough=false)
{
if($hash===false)
{
@@ -54,7 +54,8 @@ class TextController implements ContentController
return array('status'=>'err','hash'=>$hash,'reason'=>'Custom hash already exists');
}
storeFile($tmpfile,$hash,true);
if($passthrough===false)
storeFile($tmpfile,$hash,true);
return array('status'=>'ok','hash'=>$hash,'url'=>getURL().$hash);
}

View File

@@ -6,6 +6,6 @@ class UrlController implements ContentController
//returns all extensions registered by this type of content
public function getRegisteredExtensions(){return array('url');}
public function handleHash($hash,$url){}
public function handleUpload($tmpfile,$hash=false){}
public function handleHash($hash,$url,$path=false){}
public function handleUpload($tmpfile,$hash=false,$passthrough=false){}
}

View File

@@ -12,9 +12,10 @@ class VideoController implements ContentController
//returns all extensions registered by this type of content
public function getRegisteredExtensions(){return array('mp4');}
public function handleHash($hash,$url)
public function handleHash($hash,$url,$path=false)
{
$path = getDataDir().DS.$hash.DS.$hash;
if($path===false)
$path = getDataDir().DS.$hash.DS.$hash;
//@todo: - resize by changing $path
@@ -44,7 +45,7 @@ class VideoController implements ContentController
}
header ("Content-type: image/jpeg");
header('X-Accel-Redirect: '.str_replace(getDataDir().DS,'',$preview));
serveFile($preview);
}
else if(in_array('download',$url))
@@ -57,7 +58,7 @@ class VideoController implements ContentController
header('Cache-Control: must-revalidate');
header('Pragma: public');
header('Content-Length: ' . filesize($path));
header('X-Accel-Redirect: '.str_replace(getDataDir().DS,'',$path));
serveFile($path);
exit;
}
}
@@ -69,7 +70,7 @@ class VideoController implements ContentController
}
}
public function handleUpload($tmpfile,$hash=false)
public function handleUpload($tmpfile,$hash=false,$passthrough=false)
{
if($hash===false)
$hash = getNewHash('mp4',6);
@@ -80,10 +81,13 @@ class VideoController implements ContentController
return array('status'=>'err','hash'=>$hash,'reason'=>'Custom hash already exists');
}
$file = storeFile($tmpfile,$hash,true);
if($passthrough===false)
{
$file = storeFile($tmpfile,$hash,true);
if(!$this->rightEncodedMP4($file))
system("nohup php ".ROOT.DS.'tools'.DS.'re-encode_mp4.php force '.$hash." > /dev/null 2> /dev/null &");
if(!$this->rightEncodedMP4($file))
system("nohup php ".ROOT.DS.'tools'.DS.'re-encode_mp4.php force '.$hash." > /dev/null 2> /dev/null &");
}
return array('status'=>'ok','hash'=>$hash,'url'=>getURL().$hash);
}
@@ -96,7 +100,7 @@ class VideoController implements ContentController
header ("Last-Modified: ".gmdate('D, d M Y H:i:s ', filemtime($path)) . 'GMT');
header ("ETag: ".sha1_file($path));
header('Cache-control: public, max-age=31536000');
header('X-Accel-Redirect: '.str_replace(getDataDir().DS,'',$path));
serveFile($path);
exit();
}

View File

@@ -17,6 +17,7 @@ class API
}
return match ($this->url[0]) {
'passthrough' => $this->passthrough(),
'upload' => $this->upload(),
'delete' => $this->delete(),
'info' => $this->info(),
@@ -25,6 +26,39 @@ class API
};
}
public function passthrough(){
$type = $this->url[1];
array_shift($this->url); //remove the first element which is 'passthrough'
array_shift($this->url); //remove the second element which is the type so now what's left is the modifiers
$tmpfile = $_FILES['file']['tmp_name'] ?? false;
if (!$tmpfile || !is_file($tmpfile))
{
header('HTTP/1.1 400 Bad Request');
return array('status' => 'err', 'reason' => 'No file uploaded');
}
$tmp_target = $tmpfile;
//$tmp_target = ROOT.DS.'tmp'.DS.'pt-'.md5(rand(1,9999)).basename($_FILES['file']['name']);
//move_uploaded_file($tmpfile, $tmp_target);
switch($type) {
case 'image':
$controller = new ImageController();
$upload = $controller->handleUpload($tmp_target, false, true); //only to validate the file
if ($upload['status'] == 'ok') {
$controller->handleHash($upload['hash'], $this->url, $tmp_target);
unlink($tmp_target); //remove the temporary file
exit();
}
else {
header('HTTP/1.1 400 Bad Request');
return $upload; //error
}
break;
default:
return array('status' => 'err', 'reason' => 'Unknown passthrough type');
}
}
public function debug()
{

View File

@@ -18,12 +18,9 @@ if(!defined('FFMPEG_BINARY'))
function architect($u)
{
//if there is no info in the URL, don't even bother checking with the controllers
//just show the site
if( ( (!defined('UPLOAD_FORM_LOCATION') || (defined('UPLOAD_FORM_LOCATION') && !UPLOAD_FORM_LOCATION)) && count($u)==0) || (defined('UPLOAD_FORM_LOCATION') && UPLOAD_FORM_LOCATION && '/'.implode('/',$u)==UPLOAD_FORM_LOCATION) )
// check if client address is allowed
if($u[0] == '' || $u[0] == '/')
{
// check if client address is allowed
$forbidden = false;
if(defined('ALLOWED_SUBNET') && ALLOWED_SUBNET != '' && !isIPInRange( getUserIP(), ALLOWED_SUBNET ))
{
@@ -99,6 +96,7 @@ function architect($u)
//check all parts of the URL for a valid hash
$hash = false;
$sc = getStorageControllers();
foreach($u as $el)
{
if(isExistingHash($el))
@@ -111,8 +109,6 @@ function architect($u)
// if we don't have a hash yet but the element looks like it could be a hash
if($hash === false && mightBeAHash($el))
{
if(!$sc)
$sc = getStorageControllers();
foreach($sc as $contr)
{
$c = new $contr();
@@ -363,69 +359,6 @@ function sizeStringToWidthHeight($size)
return array('width'=>$maxwidth,'height'=>$maxheight);
}
//
// from: https://stackoverflow.com/questions/25975943/php-serve-mp4-chrome-provisional-headers-are-shown-request-is-not-finished-ye
//
function serveFile($filename, $filename_output = false, $mime = 'application/octet-stream')
{
$buffer_size = 8192;
$expiry = 90; //days
if(!file_exists($filename))
{
throw new Exception('File not found: ' . $filename);
}
if(!is_readable($filename))
{
throw new Exception('File not readable: ' . $filename);
}
header_remove('Cache-Control');
header_remove('Pragma');
$byte_offset = 0;
$filesize_bytes = $filesize_original = filesize($filename);
header('Accept-Ranges: bytes', true);
header('Content-Type: ' . $mime, true);
header("Content-Disposition: inline;");
// Content-Range header for byte offsets
if (isset($_SERVER['HTTP_RANGE']) && preg_match('%bytes=(\d+)-(\d+)?%i', $_SERVER['HTTP_RANGE'], $match))
{
$byte_offset = (int) $match[1];//Offset signifies where we should begin to read the file
if (isset($match[2]))//Length is for how long we should read the file according to the browser, and can never go beyond the file size
{
$filesize_bytes = min((int) $match[2], $filesize_bytes - $byte_offset);
}
header("HTTP/1.1 206 Partial content");
header(sprintf('Content-Range: bytes %d-%d/%d', $byte_offset, $filesize_bytes - 1, $filesize_original)); ### Decrease by 1 on byte-length since this definition is zero-based index of bytes being sent
}
$byte_range = $filesize_bytes - $byte_offset;
header('Content-Length: ' . $byte_range);
header('Expires: ' . date('D, d M Y H:i:s', time() + 60 * 60 * 24 * $expiry) . ' GMT');
$buffer = '';
$bytes_remaining = $byte_range;
$handle = fopen($filename, 'r');
if(!$handle)
{
throw new Exception("Could not get handle for file: " . $filename);
}
if (fseek($handle, $byte_offset, SEEK_SET) == -1)
{
throw new Exception("Could not seek to byte offset %d", $byte_offset);
}
while ($bytes_remaining > 0)
{
$chunksize_requested = min($buffer_size, $bytes_remaining);
$buffer = fread($handle, $chunksize_requested);
$chunksize_real = strlen($buffer);
if ($chunksize_real == 0)
{
break;
}
$bytes_remaining -= $chunksize_real;
echo $buffer;
flush();
}
}
function sanatizeString($string)
{
return preg_replace("/[^a-zA-Z0-9._\-]+/", "", $string);
@@ -1276,4 +1209,39 @@ function getFileMimeType($file)
$mimeType = shell_exec('file --mime-type -b ' . escapeshellarg($file));
return trim($mimeType);
}
}
function getRelativeToDataPath(string $path): string
{
// Resolve real paths
$realBase = realpath(ROOT.DS.'data');
$realPath = realpath($path);
if ($realBase === false || $realPath === false) {
throw new InvalidArgumentException("Invalid path or base directory: $path, ".ROOT.DS.'data');
}
$baseParts = explode(DIRECTORY_SEPARATOR, trim($realBase, DIRECTORY_SEPARATOR));
$pathParts = explode(DIRECTORY_SEPARATOR, trim($realPath, DIRECTORY_SEPARATOR));
// Find common path length
$i = 0;
while (isset($baseParts[$i], $pathParts[$i]) && $baseParts[$i] === $pathParts[$i]) {
$i++;
}
// How many directories to go up from base
$upDirs = count($baseParts) - $i;
$relativeParts = array_merge(array_fill(0, $upDirs, '..'), array_slice($pathParts, $i));
return implode('/', $relativeParts);
}
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,'..'))
readfile($path);
else
header('X-Accel-Redirect: '. $relativePath);
}

View File

@@ -31,7 +31,7 @@ interface ContentController
* @param string $hash the hash (with extension eg '5saB2.pdf') of the file this controller will work with
* @param array $url contains all URL elements exploded with '/' so you can do your magic.
*/
public function handleHash($hash,$url);
public function handleHash($hash,$url,$path=false);
/** This method will be called if the upload script detects the content of a newly uploaded file as one of the
* extensions registered at "getRegisteredExtensions".
@@ -41,5 +41,5 @@ interface ContentController
* @param string $tmpfile is the location on disk of the temp file that was uploaded. It is your job to put it somewhere, your handleHash method will find it again
* @param array $hash (optional) if you want your upload to have a certain hash then add it here. This allows for user chosen hashes
*/
public function handleUpload($tmpfile,$hash=false);
public function handleUpload($tmpfile,$hash=false,$passthrough=false);
}

View File

@@ -156,7 +156,26 @@ if (file_exists(ROOT . DS . 'notice.txt'))
</div>
<div class="col-6">
<h2>On-the-fly modifications to images</h2>
<p>You can modify images on-the-fly (without saving them to the server) by specifying additional parameters in the API call to use Pictshare to "clean up" or "normalize" images uploaded to your app. The passthrough endpoint allows you to use the normal image modifiers in the URL. It won't save the image on the Server and will respond with the modified image directly.</p>
API call
<pre><code class="url">/passthrough/image/[modifiers]</code></pre>
<p>For example to force an uploaded image to 400x400 and have it grayscale you can do this:</p>
CURL example
<pre><code class="bash">curl -s -F "file=@myphoto.jpg" "<?= getURL() ?>api/passthrough/image/400x400/forcesize/gray" > result.jpg</code></pre>
If the status code is anything other than "200", it will return an error message in JSON format.
<pre><code class="json">
{
"status": "err",
"reason": "Not a valid image"
}</code></pre>
If the status code is "200", it will return the modified image directly.
</div>
</div>