Login   Register  
PHP Classes
elePHPant
Icontem

File: engine/handler.image.php

Recommend this page to a friend!
Stumble It! Stumble It! Bookmark in del.icio.us Bookmark in del.icio.us
  Classes of Kristo Vaher  >  Wave Framework  >  engine/handler.image.php  >  Download  
File: engine/handler.image.php
Role: Application script
Content type: text/plain
Description: Image Handler
Class: Wave Framework
MVC framework for building Web sites and APIs
Author: By
Last change: Updated jQuery 1.10.0 and 2.0.1. Added better API versioning support that also takes into account overrides folders. Refactoring of Imager class to reduce duplicate code between Image Handler and Imager. Also fixed the bug introduced in 3.6.2 that generated incorrect image sprite sizes in dynamic image loading. Code review and some minor changes.
Date: 2013-05-25 10:10
Size: 16,043 bytes
 

Contents

Class file image Download
<?php

/**
 * Wave Framework <http://www.waveframework.com>
 * 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'].$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();
	}

?>