<?php
/**
* Wave Framework <http://github.com/kristovaher/Wave-Framework>
* Image Handler
*
* Image Handler is used by Index Gateway to return all image files to the user agent HTTP
* requests. Handler adds proper cache headers as well as supports on-demand image-editing,
* where it is possible to load an image file with specific resize algorithms and even image
* filtering. It also checks for files from overrides folder, which can be returned instead
* of the actual file.
*
* @package Index Gateway
* @author Kristo Vaher <kristo@waher.net>
* @copyright Copyright (c) 2012, Kristo Vaher
* @license GNU Lesser General Public License Version 3
* @tutorial /doc/pages/handler_image.htm
* @since 1.5.0
* @version 3.6.4
*/
// INITIALIZATION
// Stopping all requests that did not come from Index Gateway
if(!isset($resourceAddress)){
header('HTTP/1.1 403 Forbidden');
die();
}
// If access control header is set in configuration
if(isset($config['access-control'])){
header('Access-Control-Allow-Origin: '.$config['access-control']);
}
// Web root is the subfolder on public site
$webRoot=str_replace('index.php','',$_SERVER['SCRIPT_NAME']);
// Web root is the subfolder on public site
$systemRoot=str_replace('index.php','',$_SERVER['SCRIPT_FILENAME']);
// Dynamic resource loading can be turned off in configuration
if(!isset($config['dynamic-image-loading']) || $config['dynamic-image-loading']==true){
// If filename includes & symbol, then system assumes it should be dynamically generated
$parameters=array_unique(explode('&',$resourceFile));
} else {
$parameters=array();
$parameters[0]=$resourceFile;
}
// True filename is the last string in the string separated by & character
$resourceFile=array_pop($parameters);
// Current true file position
$resource=$resourceFolder.$resourceFile;
// Files from /resources/ folder can be overwritten if file with the same name is placed to /overrides/resources/
if(preg_match('/^'.str_replace('/','\/',$webRoot).'resources/',$resourceRequest)){
//Checking if file of the same name exists in overrides folder
$overrideFolder=str_replace($webRoot.'resources'.DIRECTORY_SEPARATOR,$webRoot.'overrides'.DIRECTORY_SEPARATOR.'resources'.DIRECTORY_SEPARATOR,$resourceFolder);
if(file_exists($overrideFolder.$resourceFile)){
// System will use an override as a resource, since it exists
$resource=$overrideFolder.$resourceFile;
}
}
// RESOURCE EXISTENCE CHECK
// If file does not exist then 404 is thrown
if(!file_exists($resource) && (!isset($config['404-image-placeholder']) || $config['404-image-placeholder']==true) && (file_exists(__ROOT__.'resources'.DIRECTORY_SEPARATOR.'placeholder.jpg') || file_exists(__ROOT__.'overrides'.DIRECTORY_SEPARATOR.'resources'.DIRECTORY_SEPARATOR.'placeholder.jpg'))){
// It's possible to overwrite the default image used for 404 placeholder
if(file_exists(__ROOT__.'overrides'.DIRECTORY_SEPARATOR.'resources'.DIRECTORY_SEPARATOR.'placeholder.jpg')){
$resource=__ROOT__.'overrides'.DIRECTORY_SEPARATOR.'resources'.DIRECTORY_SEPARATOR.'placeholder.jpg';
} else {
$resource=__ROOT__.'resources'.DIRECTORY_SEPARATOR.'placeholder.jpg';
}
// 404 header
header('HTTP/1.1 404 Not Found');
// Notifying Logger of 404 response code
if(isset($logger)){
$logger->setCustomLogData(array('response-code'=>404));
}
// This variable is used by cache to calculate cache filename, but since system is returning a placeholder instead, it is overwritten
// This allows system to keep all 404 placeholder image cache in the same cache file
$tmp=explode('/',str_replace($resourceFile,'placeholder.jpg',$resourceRequest));
$resourceRequest=array_pop($tmp);
} elseif(!file_exists($resource)){
// Adding log entry
if(isset($logger)){
$logger->setCustomLogData(array('response-code'=>404,'category'=>'image'));
$logger->writeLog();
}
// Returning 404 header
header('HTTP/1.1 404 Not Found');
die();
}
// CACHE AND BASE64 SETTINGS
// Default cache timeout of one month, unless timeout is set
if(!isset($config['resource-cache-timeout'])){
$config['resource-cache-timeout']=31536000; // A year
}
// Last-modified time of the original resource
$lastModified=filemtime($resource);
// This flag stores whether cache was used
$cacheUsed=false;
// No cache flag
$noCache=array_search('nocache',$parameters);
if($noCache!==false){
// Unsetting the key for nocache parameter
unset($parameters[$noCache]);
}
// Base64 flag
$base64=array_search('base64',$parameters);
if($base64!==false){
// Unsetting the key for base64 parameter
unset($parameters[$base64]);
}
// GENERATION BASED ON PARAMETERS
// If file seems to carry additional configuration options, then it is generated or loaded from cache
if(empty($parameters)){
// Pure image file request is considered 'cache used' due to it not needing any processing
$cacheUsed=true;
// IF NOT MODIFIED
// If the request timestamp is exactly the same, then we let the browser know of this
if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])==$lastModified){
// Adding log entry
if(isset($logger)){
$logger->setCustomLogData(array('response-code'=>304,'category'=>'image','cache-used'=>true));
$logger->writeLog();
}
// Cache headers (Last modified is never sent with 304 header, since it is often ignored)
header('Cache-Control: public,max-age='.$config['resource-cache-timeout']);
header('Expires: '.gmdate('D, d M Y H:i:s',($_SERVER['REQUEST_TIME']+$config['resource-cache-timeout'])).' GMT');
// Returning 304 header
header('HTTP/1.1 304 Not Modified');
die();
}
} else {
// Solving cache folders and directory
$cacheFilename=md5($lastModified.'&'.$config['version-system'].'&'.$config['version-api'].'&'.$resourceRequest).'.tmp';
$cacheDirectory=__ROOT__.'filesystem'.DIRECTORY_SEPARATOR.'cache'.DIRECTORY_SEPARATOR.'images'.DIRECTORY_SEPARATOR.substr($cacheFilename,0,2).DIRECTORY_SEPARATOR;
// IF NOT MODIFIED
// If cache file exists then cache modified is considered that time
if(!$noCache && file_exists($cacheDirectory.$cacheFilename)){
// Getting last modified time from cache file
$lastModified=filemtime($cacheDirectory.$cacheFilename);
// If the request timestamp is exactly the same, then we let the browser know of this
if(isset($_SERVER['HTTP_IF_MODIFIED_SINCE']) && strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])==$lastModified){
// Adding log entry
if(isset($logger)){
$logger->setCustomLogData(array('response-code'=>304,'category'=>'image','cache-used'=>true));
$logger->writeLog();
}
// Cache headers (Last modified is never sent with 304 header)
header('Cache-Control: public,max-age='.$config['resource-cache-timeout']);
header('Expires: '.gmdate('D, d M Y H:i:s',($_SERVER['REQUEST_TIME']+$config['resource-cache-timeout'])).' GMT');
// Returning 304 header
header('HTTP/1.1 304 Not Modified');
die();
}
} else {
// Otherwise it is server request time
$lastModified=$_SERVER['REQUEST_TIME'];
}
// GENERATING RESOURCE
// If resource cannot be found from cache, it is generated with Imager class
if($noCache || ($lastModified==$_SERVER['REQUEST_TIME'] || $lastModified<($_SERVER['REQUEST_TIME']-$config['resource-cache-timeout']))){
// LOADING THE IMAGE FOR DYNAMIC MANIPULATION
// Requiring WWW_Imager class that is used to do basic image manipulation
require(__ROOT__.'engine'.DIRECTORY_SEPARATOR.'class.www-imager.php');
// New Imager object, this is a wrapper around GD library
$picture=new WWW_Imager();
// Current image file is loaded into Imager
if(!$picture->input($resource)){
trigger_error('Cannot load image from '.$resource,E_USER_ERROR);
}
// FINDING SETTINGS FOR MANIPULATION FROM PARAMETERS
// This applies parameters that were found from file request to the Imager
$settings=$picture->parseParameters($parameters);
// If settings encountered a problem (such as incorrect parameters string)
if(!$settings){
// Adding log entry
if(isset($logger)){
$logger->setCustomLogData(array('response-code'=>404,'category'=>'image'));
$logger->writeLog();
}
// Returning 404 header
header('HTTP/1.1 404 Not Found');
die();
}
// IMAGE SETTING VALIDATION TO PROTECT THE SYSTEM FROM MALICIOUS REQUESTS
// System checks for legality of the entered values
// Whitelists allow to protect the server better from possible abuse and denial of service attacks
$allowed=true;
// If configuration file has not been set for dynamic max size, then it is defaulted to 1000x1000 maximum
if(!isset($config['dynamic-max-size'])){
// Default maximum image dimension height/width
$config['dynamic-max-size']=4096;
}
// Checking if image is within allowed parameters
if($settings['width']>$config['dynamic-max-size'] || $settings['height']>$config['dynamic-max-size'] || $settings['height']==0 || $settings['width']==0){
// If image dimensions are beyond allowed values
$allowed=false;
} elseif(isset($config['dynamic-size-whitelist']) && $config['dynamic-size-whitelist']!='' && !in_array($settings['width'].'x'.$settings['height'],explode(' ',$config['dynamic-size-whitelist']))){
// For size whitelist check
// If resolution has been changed and this resolution is not found in whitelist
$allowed=false;
} elseif(isset($config['dynamic-color-whitelist']) && $config['dynamic-color-whitelist']!='' && !in_array($settings['red'].','.$settings['green'].','.$settings['blue'],explode(' ',$config['dynamic-color-whitelist']))){
// For RGB whitelist check
// If RGB values are not defaults and this setting is not found in color whitelist
$allowed=false;
} elseif(isset($config['dynamic-quality-whitelist']) && $config['dynamic-quality-whitelist']!='' && $settings['quality'] && !in_array('@'.$settings['quality'],explode(' ',$config['dynamic-quality-whitelist']))){
// For quality whitelist check
// If quality values are not defaults and this setting is not found in quality whitelist
$allowed=false;
} elseif(isset($config['dynamic-position-whitelist']) && $config['dynamic-position-whitelist']!='' && !in_array($settings['top'].'-'.$settings['left'],explode(' ',$config['dynamic-position-whitelist']))){
// For position whitelist check
// If position values are not defaults and this setting is not found in position whitelist
$allowed=false;
} elseif(isset($config['dynamic-filter-whitelist']) && $config['dynamic-filter-whitelist']!='' && !empty($settings['filters'])){
// Making sure that dynamic image filters are allowed
if(isset($config['dynamic-image-filters']) && $config['dynamic-image-filters']==false){
// Filters are set, but dynamic image filters are not enabled
$allowed=false;
} else {
// For filter whitelist check
foreach($settings['filters'] as $filter){
// If filters are not in filter whitelist then processing is canceled
if($allowed && !in_array($filter['type'].'@'.$filter['alpha'].','.implode(',',$filter['settings']),explode(' ',$config['dynamic-filter-whitelist']))){
$allowed=false;
}
}
}
}
// IMAGE GENERATION BASED ON SETTINGS
// If whitelist checks did not fail and image dimensions are good
if($allowed){
// This applies settings to the image through Imager class
if(!$picture->applyParameters($settings)){
// Adding log entry
if(isset($logger)){
$logger->setCustomLogData(array('response-code'=>404,'category'=>'image'));
$logger->writeLog();
}
// Returning 404 header
header('HTTP/1.1 404 Not Found');
die();
}
// If cache folder does not exist, it is created
// This is done even if cache itself is not used due to dynamic image having to exist
if(!is_dir($cacheDirectory)){
if(!mkdir($cacheDirectory,0755)){
trigger_error('Cannot create cache folder',E_USER_ERROR);
}
}
// Resulting image is saved to cache
if(!$picture->output($cacheDirectory.$cacheFilename,$settings['quality'],$settings['format'])){
trigger_error('Cannot output image file',E_USER_ERROR);
}
} else {
// Adding log entry
if(isset($logger)){
$logger->setCustomLogData(array('response-code'=>404,'category'=>'image'));
$logger->writeLog();
}
// Returning 404 header
header('HTTP/1.1 404 Not Found');
die();
}
} else {
// To notify logger that cache was used
$cacheUsed=true;
}
// File URL is set to cache address
$resource=$cacheDirectory.$cacheFilename;
}
// HEADERS
// Serving up the correct content type header
if($base64){
// BASE64 text string
header('Content-Type: application/octet-stream');
header('Content-Transfer-Encoding: base64');
} else {
// Proper content-type is set based on file extension
if(isset($resourceExtension)){
switch($resourceExtension){
case 'jpg':
header('Content-Type: image/jpeg;');
break;
case 'jpeg':
header('Content-Type: image/jpeg;');
break;
case 'png':
header('Content-Type: image/png;');
break;
}
}
}
// If cache is used, then proper headers will be sent
if($noCache){
// User agent is told to cache these results for set duration
header('Cache-Control: no-cache,no-store');
header('Expires: '.gmdate('D, d M Y H:i:s',$_SERVER['REQUEST_TIME']).' GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s',$lastModified).' GMT');
} else {
// User agent is told to cache these results for set duration
header('Cache-Control: public,max-age='.$config['resource-cache-timeout']);
header('Expires: '.gmdate('D, d M Y H:i:s',($_SERVER['REQUEST_TIME']+$config['resource-cache-timeout'])).' GMT');
header('Last-Modified: '.gmdate('D, d M Y H:i:s',$lastModified).' GMT');
}
// Robots header
if(isset($config['image-robots'])){
// If image-specific robots setting is defined
header('Robots-Tag: '.$config['image-robots'],true);
} elseif(isset($config['robots'])){
// This sets general robots setting, if it is defined in configuration file
header('Robots-Tag: '.$config['robots'],true);
} else {
// If robots setting is not configured, system tells user agent not to cache the file
header('Robots-Tag: noindex,nocache,nofollow,noarchive,noimageindex,nosnippet',true);
}
// OUTPUT
// Returning image resource
if(!$base64){
// Getting current output length
$contentLength=filesize($resource);
// Content length is defined that can speed up website requests, letting user agent to determine file size
header('Content-Length: '.$contentLength);
// Returning the file to user agent
readfile($resource);
} else {
// Getting file contents and converting it to BASE64 string
$contents=base64_encode(file_get_contents($resource));
// Getting current output length
$contentLength=strlen($contents);
// Content length is defined that can speed up website requests, letting user agent to determine file size
header('Content-Length: '.$contentLength);
// Returning data to user agent
echo $contents;
}
// Cache resource is deleted if cache was requested to be off
if($noCache){
unlink($resource);
}
// WRITING TO LOG
// If Logger is defined then request is logged and can be used for performance review later
if(isset($logger)){
// Assigning custom log data to logger
$logger->setCustomLogData(array('cache-used'=>$cacheUsed,'content-length'=>$contentLength,'category'=>'image'));
// Writing log entry
$logger->writeLog();
}
?>
|