e107_handlers/e_thumbnail_class.php
<?php
/**
* e107 website system
*
* Copyright (C) 2008-2020 e107 Inc (e107.org)
* Released under the terms and conditions of the
* GNU General Public License (http://www.gnu.org/licenses/gpl.txt)
*
*/
use Intervention\Image\ImageManagerStatic as Image;
/**
*
*/
class e_thumbnail
{
private $_debug = false;
private $_cache = true;
/**
* Page request
* @var array
*/
protected $_request = array();
/**
* @var string image source path (e107 path shortcode)
*/
protected $_src = null;
/**
* @var string source path modified/sanitized
*/
protected $_src_path = null;
/** Stores watermark prefs
*/
protected $_watermark = array('activate'=>null);
protected $_placeholder = false;
protected $_thumbQuality = 65;
protected $_upsize = true;
protected $_forceWebP = false;
/**
* Constructor - init paths
*
*
* @return void
*/
public function __construct()
{
}
/**
* Initialize the class with e107 core prefs.
* @param array $pref
* @return null
*/
public function init($pref)
{
$this->parseRequest();
if(!empty($this->_request['noinit']))
{
return null;
}
$this->_watermark = array(
'activate' => vartrue($pref['watermark_activate'], false),
'text' => vartrue($pref['watermark_text']),
'size' => vartrue($pref['watermark_size'], 20),
'pos' => vartrue($pref['watermark_pos'],"BR"),
'color' => vartrue($pref['watermark_color'],'fff'),
'font' => vartrue($pref['watermark_font']),
'margin' => vartrue($pref['watermark_margin'],30),
'shadowcolor' => vartrue($pref['watermark_shadowcolor'], '000000'),
'opacity' => vartrue($pref['watermark_opacity'], 20)
);
$this->_thumbQuality = vartrue($pref['thumbnail_quality'],65);
$this->_upsize = ((isset($this->_request['w']) && $this->_request['w'] > 110) || (isset($this->_request['aw']) && ($this->_request['aw'] > 110))); // don't resizeUp the icon images.
$this->_forceWebP = empty($this->_request['type']) && !empty($pref['thumb_to_webp']) && (strpos( $_SERVER['HTTP_ACCEPT'], 'image/webp' ) !== false) ? true : false;
// var_dump($this);
// exit;
return null;
}
/**
* @param array $array keys: activate, text, size, pos, color, font, margin, shadowcolor, opacity. @see above.
*/
public function setWatermark($array)
{
$this->_watermark = (array) $array;
}
/**
* Enabled/Disable debugging/testing.
* @param $val
*/
public function setDebug($val)
{
$this->_debug = (bool) $val;
}
/**
* Enable/disable image caching.
* @param bool $val
*/
public function setCache($val)
{
$this->_cache = (bool) $val;
}
/**
* @return $this
*/
private function parseRequest()
{
$e_QUERY = !empty($_SERVER['argv'][1]) ? $_SERVER['argv'][1] : e_QUERY;
if(isset($_GET['id'])) // very-basic url-tampering prevention and path cloaking
{
$e_QUERY = base64_decode($_GET['id']);
}
$e_QUERY= str_replace('e_AVATAR', 'e_AVATAR/', $e_QUERY); // FIXME Quick and dirty fix.
parse_str(str_replace('&', '&', $e_QUERY), $this->_request);
if(isset($this->_request['w']))
{
$this->_request['w'] = (int) $this->_request['w'];
}
if(isset($this->_request['h']))
{
$this->_request['h'] = (int) $this->_request['h'];
}
return $this;
}
/**
* Manually Set the Request parameters
* @param array $array keys: w, h, ah, aw, c(rop)
*/
public function setRequest($array)
{
$this->_request = (array) $array;
}
/**
* @return array|string|string[]
*/
private function getImageInfo()
{
$thumbnfo = pathinfo($this->_src_path);
if($this->_forceWebP === true || (!empty($this->_request['type']) && $this->_request['type'] == 'webp'))
{
$thumbnfo['extension'] = 'webp';
}
return $thumbnfo;
}
/**
* Validate and Sanitize the Request.
* @return bool true when request is okay.
*/
public function checkSrc()
{
if(!vartrue($this->_request['src'])) // display placeholder when src is missing.
{
$this->_placeholder = true;
return true;
}
$tp = e107::getParser();
// convert raw to SC path
$this->_request['src'] = str_replace($tp->getUrlConstants('raw'), $tp->getUrlConstants(), $this->_request['src']);
// convert absolute and full url to SC URL
$this->_src = $tp->createConstants($this->_request['src'], 'mix');
if(preg_match('#^(https?|ftps?|file)://#i', $this->_request['src']))
{
return false;
}
if(!is_writable(e_CACHE_IMAGE))
{
echo 'Cache folder not writeable! ';
return false;
}
// convert to relative server path
$path = $tp->replaceConstants(str_replace('..', '', $this->_src)); //should be safe enough
if(is_file($path) && is_readable($path))
{
$this->_src_path = $path;
return true;
}
if($this->_debug === true || defined("e_DEBUG_THUMBNAIL"))
{
echo "File Not Found: ".$path;
}
$this->_placeholder = true;
return true;
}
/**
* @return $this|false|string|void
*/
public function sendImage()
{
if($this->_placeholder == true)
{
$width = ($this->_request['aw']) ? $this->_request['aw'] : $this->_request['w'];
$height = ($this->_request['ah']) ? $this->_request['ah'] : $this->_request['h'];
$parm = array('size' => $width."x".$height);
$this->placeholder($parm);
return false;
}
if(!$this->_src_path) // would only happen during testing.
{
echo "no source";
return $this;
}
$thumbnfo = $this->getImageInfo();
$options = $this->getRequestOptions();
$fname = e107::getParser()->thumbCacheFile($this->_src_path, $options);
$cache_filename = e_CACHE_IMAGE . $fname;
$this->sendCachedImage($cache_filename, $thumbnfo);
// No Cached file found - proceed with image creation.
$img = Image::make($this->_src_path);
if(!empty($options['c'])) // Cropping: $quadrant T(op), B(ottom), C(enter), L(eft), R(right)
{
if(!empty($this->_request['ah']))
{
$this->_request['h'] = $this->_request['ah'];
}
if(!empty($this->_request['aw']))
{
$this->_request['w'] = $this->_request['aw'];
}
$key = $options['c'];
$fit = array(
'T' => 'top',
'C' => 'center',
'B' => 'bottom',
'L' => 'left',
'R' => 'right'
);
$position = varset($fit[$key],'top');
$img->fit(vartrue($this->_request['w'], null), vartrue($this->_request['h'], null), null, $position);
}
elseif(!empty($this->_request['w']) || !empty($this->_request['h']))
{
$img->resize(vartrue($this->_request['w'], null), vartrue($this->_request['h'], null), function ($constraint)
{
$constraint->aspectRatio();
if(!$this->_upsize)
{
$constraint->upsize();
}
});
}
elseif(!empty($this->_request['ah']) || !empty($this->_request['aw']))
{
$img->fit(vartrue($this->_request['aw'], null), vartrue($this->_request['ah'], null), null, 'top');
}
/*
todo watermark support. @see http://image.intervention.io/api/text
todo also @see: http://image.intervention.io/api/insert
*/
$img->save($cache_filename, $this->_thumbQuality, $thumbnfo['extension']);
$this->_request = array(); // reset the request.
if($this->_debug === true) // return the cache file path for testing.
{
return $cache_filename;
}
$imageData = $img->encode($thumbnfo['extension'], $this->_thumbQuality);
$thumbnfo['fsize'] = strlen($imageData);
$this->sendHeaders($thumbnfo);
echo $imageData;
exit;
}
/*
protected function sendImageOld() // for reference. - can be removed at a later date.
{
if($this->_placeholder == true)
{
$width = ($this->_request['aw']) ? $this->_request['aw'] : $this->_request['w'];
$height = ($this->_request['ah']) ? $this->_request['ah'] : $this->_request['h'];
$parm = array('size' => $width."x".$height);
$this->placeholder($parm);
return false;
}
if(!$this->_src_path)
{
echo "no source";
return $this;
}
$thumbnfo = pathinfo($this->_src_path);
$options = $this->getRequestOptions();
$fname = e107::getParser()->thumbCacheFile($this->_src_path, $options);
$cache_filename = e_CACHE_IMAGE . $fname;
$this->sendCachedImage($cache_filename,$thumbnfo);
require_once(e_HANDLER.'phpthumb/ThumbLib.inc.php');
if(!$thumb = PhpThumbFactory::create($this->_src_path))
{
if(getperms('0'))
{
echo "Couldn't load thumb factory";
}
return null;
}
$sizeUp = ((isset($this->_request['w']) && $this->_request['w'] > 110) || (isset($this->_request['aw']) && ($this->_request['aw'] > 110))); // don't resizeUp the icon images.
$thumb->setOptions(array(
'correctPermissions' => true,
'resizeUp' => $sizeUp,
'jpegQuality' => $this->_thumbQuality,
'interlace' => true // improves performance
));
// Image Cropping by Quadrant.
if(!empty($options['c'])) // $quadrant T(op), B(ottom), C(enter), L(eft), R(right)
{
if(!empty($this->_request['ah']))
{
$this->_request['h'] = $this->_request['ah'];
}
if(!empty($this->_request['aw']))
{
$this->_request['w'] = $this->_request['aw'];
}
$thumb->adaptiveResizeQuadrant((integer) vartrue($this->_request['w'], 0), (integer) vartrue($this->_request['h'], 0), $options['c']);
}
if(isset($this->_request['w']) || isset($this->_request['h']))
{
$thumb->resize((integer) vartrue($this->_request['w'], 0), (integer) vartrue($this->_request['h'], 0));
}
elseif(!empty($this->_request['ah']))
{
//Typically gives a better result with images of people than adaptiveResize().
$thumb->adaptiveResizeQuadrant((integer) vartrue($this->_request['aw'], 0), (integer) vartrue($this->_request['ah'], 0), 'T');
}
else
{
$thumb->adaptiveResize((integer) vartrue($this->_request['aw'], 0), (integer) vartrue($this->_request['ah'], 0));
}
if($this->_debug === true)
{
// echo "time: ".round((microtime(true) - $start),4);
var_dump($thumb);
return false;
}
// Watermark Option - See admin->MediaManager->prefs for details.
/ if(($this->_watermark['activate'] < $options['w']
|| $this->_watermark['activate'] < $options['aw']
|| $this->_watermark['activate'] < $options['h']
|| $this->_watermark['activate'] < $options['ah']
) && $this->_watermark['activate'] > 0 && $this->_watermark['font'] !='')
{
$tp = e107::getParser();
$this->_watermark['font'] = $tp->createConstants($this->_watermark['font'], 'mix');
$this->_watermark['font'] = realpath($tp->replaceConstants($this->_watermark['font'],'rel'));
// $thumb->WatermarkText($this->_watermark); // failing due to phpThumb::
}
// echo "hello";
// set cache
$thumb->save($cache_filename);
$this->_request = array(); // reset the request.
if($this->_debug === true) // return the cache file path for testing.
{
return $cache_filename;
}
// show thumb
$thumb->show();
return $this;
}
*/
/**
* When caching is enabled, send the cached image with headers and then exit the script.
* @param string $cache_filename
* @param array $thumbnfo
* @return null
*/
private function sendCachedImage($cache_filename, $thumbnfo)
{
if(!$this->_cache || !is_file($cache_filename) || !is_readable($cache_filename) || $this->_debug)
{
return null;
}
$thumbnfo['lmodified'] = filemtime($cache_filename);
$thumbnfo['md5s'] = md5_file($cache_filename);
$thumbnfo['fsize'] = filesize($cache_filename);
// Send required headers
if($this->_debug !== true)
{
$this->sendHeaders($thumbnfo);
}
// check browser cache
if (@$_SERVER['HTTP_IF_MODIFIED_SINCE'] && ($thumbnfo['lmodified'] <= strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])) && (isset($_SERVER['HTTP_IF_NONE_MATCH']) && trim($_SERVER['HTTP_IF_NONE_MATCH']) == $thumbnfo['md5s']))
{
header('HTTP/1.1 304 Not Modified');
//$bench->end()->logResult('thumb.php', $_GET['src'].' - 304 not modified');
exit;
}
eShims::readfile($cache_filename);
exit;
}
/**
* @return array
*/
private function getRequestOptions()
{
$ret = array();
$ret['w'] = isset($this->_request['w']) ? intval($this->_request['w']) : false;
$ret['h'] = isset($this->_request['h']) ? intval($this->_request['h']) : $ret['w'];
$ret['aw'] = isset($this->_request['aw']) ? intval($this->_request['aw']) : false;
$ret['ah'] = isset($this->_request['ah']) ? intval($this->_request['ah']) : $ret['aw'];
$ret['c'] = isset($this->_request['c']) ? strtoupper(substr(e107::getParser()->filter($this->_request['c'], 'str'),0,1)) : false;
// $ret['wm'] = isset($this->_request['wm']) ? intval($this->_request['wm']) : $ret['wm'];
if($ret['c'] == 'A') // auto
{
$ret['c'] = 'T'; // default is 'Top';
}
if(!empty($this->_request['type']))
{
$ret['type'] = $this->_request['type'];
}
elseif($this->_forceWebP)
{
$ret['type'] = 'webp';
}
return $ret;
}
/**
* @param $thumbnfo
* @return void
*/
private function sendHeaders($thumbnfo)
{
if(headers_sent($filename, $linenum))
{
echo 'Headers already sent in '.$filename.' on line '.$linenum;
exit;
}
if (function_exists('date_default_timezone_set'))
{
date_default_timezone_set('UTC');
}
header('Cache-Control: must-revalidate');
if(isset($thumbnfo['lmodified']))
{
header('Last-Modified: '.gmdate('D, d M Y H:i:s', $thumbnfo['lmodified']).' GMT');
}
if(isset($thumbnfo['fsize'])) // extra check, if empty image will fail.
{
header('Content-Length: '.$thumbnfo['fsize']);
}
header('Content-Disposition: filename='.$thumbnfo['basename']); // important for right-click save-as.
$ctype = $this->ctype($thumbnfo['extension']);
if(null !== $ctype)
{
header('Content-Type: '.$ctype);
}
// Expire header - 1 year
$time = time() + 365 * 86400;
header('Expires: '.gmdate("D, d M Y H:i:s", $time).' GMT');
if(isset($thumbnfo['md5s']))
{
header("Etag: ".$thumbnfo['md5s']);
}
}
/**
* @param $ftype
* @return mixed|string|null
*/
private function ctype($ftype)
{
static $known_types = array(
'gif' => 'image/gif',
'jpg' => 'image/jpeg',
'jpeg' => 'image/jpeg',
'png' => 'image/png',
'webp' => 'image/webp',
//'bmp' => 'image/bmp',
);
$ftype = strtolower($ftype);
if(isset($known_types[$ftype]))
{
return $known_types[$ftype];
}
return null;
}
// Display a placeholder image.
/**
* @param $parm
* @return void|null
*/
private function placeholder($parm)
{
if($this->_debug === true)
{
echo "Placeholder activated";
return null;
}
$getsize = $parm['size'] ?? '100x100';
header('location: https://placehold.co/'.$getsize);
header('Content-Length: 0');
exit();
}
}