<?php
/**
* CSScaffold
*
* CSScaffold is a CSS compiler and preprocessor that allows you to extend
* the CSS language easily. You can add your own properities, rules and at-rules
* and abstract the language as much as you want.
*
* Requires PHP 5.1.2
* Tested on PHP 5.3.0
*
* @package CSScaffold
* @author Anthony Short <anthonyshort@me.com>
* @copyright 2009 Anthony Short. All rights reserved.
* @license http://opensource.org/licenses/bsd-license.php New BSD License
* @link https://github.com/anthonyshort/csscaffold/master
*/
class Scaffold extends Scaffold_Utils
{
const VERSION = '2.0.0';
/**
* The configuration for Scaffold and all of it's modules.
* The config for Scaffold itself should just be inside the
* config array, and module configs should be inside an array
* with the key as the name of the module.
*
* @var array
*/
public static $config;
/**
* CSS object for each processing phase. As Scaffold loops
* through the files, it creates an new CSS object in this member
* variable. It is through this variable that modules can access
* the current CSS string being processed
*
* @var object
*/
public static $css;
/**
* The level of logged message to be thrown as an error. Setting this
* to 0 will mean only error-level messages are thrown. However, setting
* it to 1 will throw warnings as errors and halt the process.
*
* @var int
*/
private static $error_threshold;
/**
* The final, combined output of the CSS.
*
* @var Mixed
*/
public static $output = null;
/**
* Include paths
*
* These are used for finding files on the system. Rather than
* using PHP's built-in include paths, we just store the paths
* in this array and use the find_file function to locate it.
*
* @var array
*/
private static $include_paths = array();
/**
* Any files that are found with find_file are stored here so that
* any further requestes for the files are just given the path
* from this array, rather than searching for the file again.
*
* @var array
*/
private static $find_file_paths;
/**
* List of included modules. They are stored with the module name
* as the key, and the path to the module as the value. However,
* calling the modules method will return just the names of the modules.
*
* @var array
*/
public static $modules;
/**
* Flags allow Scaffold to create cache variants based on particular
* parameters. This could be the browser, the time etc.
*
* @var array
*/
public static $flags = array();
/**
* Options are used by modules to check if the user wants a paricular
* action to occur. They don't affect the cache, like flags do, so
* modules shouldn't modify the CSS string based on options. They
* can be used to modify the output or to perform some secondary
* action, like validating the CSS.
*
* @var array
*/
public static $options;
/**
* If Scaffold encounted an error. You can check this variable to
* see if there were any errors when in_production is set to true.
*
* @var boolean
*/
public static $has_error = false;
/**
* Stores the headers for sending to the browser.
*
* @var array
*/
private static $headers;
/**
* Parse the CSS. This takes an array of files, options and configs
* and parses the CSS, outputing the processed CSS string.
*
* @param array List of files
* @param array Configuration options
* @param string Options
* @param boolean Return the CSS rather than displaying it
* @return string The processed css file as a string
*/
public static function parse( $files, $config, $options = array(), $display = false )
{
# Benchmark will do the entire run from start to finish
Scaffold_Benchmark::start('system');
try
{
# Setup the cache and other variables/constants
Scaffold::setup($config);
self::$options = $options;
$css = false;
# Time it takes to get the flags
Scaffold_Benchmark::start('system.flags');
# Get the flags from each of the loaded modules.
$flags = (self::$flags === false) ? array() : self::flags();
# Time it takes to get the flags
Scaffold_Benchmark::stop('system.flags');
# The final, combined CSS file in the cache
$combined = md5(serialize(array($files,$flags))) . '.css';
/**
* Check if we should use the combined cache right now and skip unneeded processing
*/
if(SCAFFOLD_PRODUCTION === true AND Scaffold_Cache::exists($combined) AND Scaffold_Cache::is_fresh($combined))
{
Scaffold::$output = Scaffold_Cache::open($combined);
}
if(Scaffold::$output === null)
{
# We're processing the files
Scaffold_Benchmark::start('system.check_files');
foreach($files as $file)
{
# The time to process a single file
Scaffold_Benchmark::start('system.file.' . basename($file));
# Make sure this file is allowed
if(substr($file, 0, 4) == "http" OR substr($file, -4, 4) != ".css")
{
Scaffold::error('Scaffold cannot the requested file - ' . $file);
}
/**
* If there are flags, we'll include them in the filename
*/
if(!empty($flags))
{
# Webligo PHP5.1 compat
$cached_file = dirname($file) . DIRECTORY_SEPARATOR . substr(basename($file), 0, strrpos(basename($file), '.')) . '_' . implode('_', $flags) . '.css';
# $cached_file = dirname($file) . DIRECTORY_SEPARATOR . pathinfo($file, PATHINFO_FILENAME) . '_' . implode('_', $flags) . '.css';
}
else
{
$cached_file = $file;
}
$request = Scaffold::find_file($file, false, true);
/**
* While not in production, we want to to always recache, so we'll fake the time
*/
$modified = (SCAFFOLD_PRODUCTION) ? Scaffold_Cache::modified($cached_file) : 0;
/**
* If the CSS file has been changed, or the cached version doesn't exist
*/
if(!Scaffold_Cache::exists($cached_file) OR $modified < filemtime($request))
{
Scaffold_Cache::write( Scaffold::process($request), $cached_file );
Scaffold_Cache::remove($combined);
}
$css .= Scaffold_Cache::open($cached_file);
# The time it's taken to process this file
Scaffold_Benchmark::stop('system.file.' . basename($file));
}
Scaffold::$output = $css;
Scaffold::hook('output'); // Added by webligo developments
/**
* If any of the files have changed we need to recache the combined
*/
if(!Scaffold_Cache::exists($combined))
{
Scaffold_Cache::write(self::$output,$combined);
}
# The time it takes to process the files
Scaffold_Benchmark::stop('system.check_files');
/**
* Hook to modify what is sent to the browser
*/
if(SCAFFOLD_PRODUCTION === false) Scaffold::hook('display');
}
/**
* Set the HTTP headers for the request. Scaffold will set
* all the headers required to score an A grade on YSlow. This
* means your CSS will be sent as quickly as possible to the browser.
*/
$length = strlen(Scaffold::$output);
$modified = Scaffold_Cache::modified($combined);
$lifetime = (SCAFFOLD_PRODUCTION === true) ? $config['cache_lifetime'] : 0;
Scaffold::set_headers($modified,$lifetime,$length);
/**
* If the user wants us to render the CSS to the browser, we run this event.
* This will send the headers and output the processed CSS.
*/
if($display === true)
{
Scaffold::render(Scaffold::$output,$config['gzip_compression']);
}
# Benchmark will do the entire run from start to finish
Scaffold_Benchmark::stop('system');
}
/**
* If any errors were encountered
*/
catch( Exception $e )
{
/**
* The message returned by the error
*/
$message = $e->getMessage();
/**
* Load in the error view
*/
if(SCAFFOLD_PRODUCTION === false && $display === true)
{
Scaffold::send_headers();
require Scaffold::find_file('scaffold_error.php','views');
}
}
# Log the final execution time
#$benchmark = Scaffold_Benchmark::get('system');
#Scaffold_Log::log('Total Execution - ' . $benchmark['time']);
# Save the logs and exit
Scaffold_Event::run('system.shutdown');
return self::$output;
}
/**
* Sets the initial variables, checks if we need to process the css
* and then sends whichever file to the browser.
*
* @return void
*/
public static function setup($config)
{
/**
* Choose whether to show or hide errors
*/
if(SCAFFOLD_PRODUCTION === false)
{
ini_set('display_errors', true);
// START - Modified by Webligo Developments
// Also ignore the DEPRECATED warnings.
if (version_compare(phpversion(), '5.3.0', '>=') ) {
error_reporting(E_ALL & ~E_STRICT & ~E_DEPRECATED);
} else {
error_reporting(E_ALL & ~E_STRICT);
}
//error_reporting(E_ALL & ~E_STRICT);
// END - Modified by Webligo Developments
}
else
{
ini_set('display_errors', false);
error_reporting(0);
}
/**
* Define contstants for system paths for easier access.
*/
if(!defined('SCAFFOLD_SYSPATH') && !defined('SCAFFOLD_DOCROOT'))
{
define('SCAFFOLD_SYSPATH', self::fix_path($config['system']));
define('SCAFFOLD_DOCROOT', $config['document_root']);
define('SCAFFOLD_URLPATH', str_replace(SCAFFOLD_DOCROOT, '',SCAFFOLD_SYSPATH));
}
/**
* Add include paths for finding files
*/
Scaffold::add_include_path(SCAFFOLD_SYSPATH,SCAFFOLD_DOCROOT);
/**
* Tell the cache where to save files and for how long to keep them for
*/
Scaffold_Cache::setup( Scaffold::fix_path($config['cache']), $config['cache_lifetime'] );
/**
* The level at which logged messages will halt processing and be thrown as errors
*/
self::$error_threshold = $config['error_threshold'];
/**
* Disabling flags allows for quicker processing
*/
if($config['disable_flags'] === true)
{
self::$flags = false;
}
/**
* Tell the log where to save it's files. Set it to automatically save the log on exit
*/
if($config['enable_log'] === true)
{
// START - Modified by Webligo Developments
if( $config['log_path'] ) {
Scaffold_Log::log_directory($config['log_path']);
} else {
Scaffold_Log::log_directory(SCAFFOLD_SYSPATH.'logs');
}
// END - Modified by Webligo Developments
//Scaffold_Log::log_directory(SCAFFOLD_SYSPATH.'logs');
Scaffold_Event::add('system.shutdown', array('Scaffold_Log','save'));
}
/**
* Load each of the modules
*/
foreach(Scaffold::list_files(SCAFFOLD_SYSPATH.'modules') as $module)
{
$name = basename($module);
$module_config = SCAFFOLD_SYSPATH.'config/' . $name . '.php';
if(file_exists($module_config))
{
unset($config);
include $module_config;
self::$config[$name] = $config;
}
self::add_include_path($module);
if( $controller = Scaffold::find_file($name.'.php', false, true) )
{
require_once($controller);
self::$modules[$name] = new $name;
}
}
/**
* Module Initialization Hook
* This hook allows modules to load libraries and create events
* before any processing is done at all.
*/
self::hook('initialize');
/**
* Create the shutdown event
*/
Scaffold_Event::add('system.shutdown', array('Scaffold','shutdown'));
}
/**
* Parses the single CSS file
*
* @param $file The file to the parsed
* @return $css string
*/
public static function process($file)
{
/**
* This allows Scaffold to find files in the directory of the CSS file
*/
Scaffold::add_include_path($file);
/**
* We create a new CSS object for each file. This object
* allows modules to easily manipulate the CSS string.
* Note:Inline comments are stripped when the file is loaded.
*/
Scaffold::$css = new Scaffold_CSS($file);
/**
* Import Process Hook
* This hook is for doing any type of importing/including in the CSS
*/
self::hook('import_process');
/**
* Pre-process Hook
* There shouldn't be any heavy processing of the string here. Just pulling
* out @ rules, constants and other bits and pieces.
*/
self::hook('pre_process');
/**
* Process Hook
* The main process. None of the processes should conflict in any of the modules
*/
self::hook('process');
/**
* Post-process Hook
* After any non-standard CSS has been processed and removed. This is where
* the nested selectors are parsed. It's not perfectly standard CSS yet, but
* there shouldn't be an Scaffold syntax left at all.
*/
self::hook('post_process');
/**
* Formatting Hook
* Stylise the string, rewriting urls and other parts of the string. No heavy processing.
*/
self::hook('formatting_process');
/**
* Clean up the include paths
*/
self::remove_include_path($file);
// Webligo PHP5.1compat
if( Scaffold::$css instanceof Scaffold_CSS ) {
return Scaffold::$css->__toString();
} else {
return (string)Scaffold::$css;
}
}
/**
* Sets the HTTP headers for a particular file
*
* @param $param
* @return return type
*/
private static function set_headers($modified,$lifetime,$length)
{
self::$headers = array();
/**
* Set the expires headers
*/
$now = $expires = time();
// Set the expiration timestamp
$expires += $lifetime;
Scaffold::header('Last-Modified',gmdate('D, d M Y H:i:s T', $now));
Scaffold::header('Expires',gmdate('D, d M Y H:i:s T', $expires));
Scaffold::header('Cache-Control','max-age='.$lifetime);
/**
* Further caching headers
*/
Scaffold::header('ETag', md5(serialize(array($length,$modified))) );
Scaffold::header('Content-Type','text/css');
/**
* Content Length
* Sending Content-Length in CGI can result in unexpected behavior
*/
if(stripos(PHP_SAPI, 'cgi') === FALSE)
{
Scaffold::header('Content-Length',$length);
}
/**
* Set the expiration headers
*/
if (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE']))
{
if (($strpos = strpos($_SERVER['HTTP_IF_MODIFIED_SINCE'], ';')) !== FALSE)
{
// IE6 and perhaps other IE versions send length too, compensate here
$mod_time = substr($_SERVER['HTTP_IF_MODIFIED_SINCE'], 0, $strpos);
}
else
{
$mod_time = $_SERVER['HTTP_IF_MODIFIED_SINCE'];
}
$mod_time = strtotime($mod_time);
$mod_time_diff = $mod_time + $lifetime - time();
if ($mod_time_diff > 0)
{
// Re-send headers
Scaffold::header('Last-Modified', gmdate('D, d M Y H:i:s T', $mod_time) );
Scaffold::header('Expires', gmdate('D, d M Y H:i:s T', time() + $mod_time_diff) );
Scaffold::header('Cache-Control', 'max-age='.$mod_time_diff);
Scaffold::header('_status',304);
// Prevent any output
Scaffold::$output = '';
}
}
}
/**
* Allows modules to hook into the processing at any point
*
* @param $method The method to check for in each of the modules
* @return boolean
*/
private static function hook($method)
{
foreach(self::$modules as $module_name => $module)
{
if(method_exists($module,$method))
{
call_user_func(array($module_name,$method));
}
}
}
/**
* Renders the CSS
*
* @param $output What to display
* @return void
*/
public static function render($output,$level = false)
{
if ($level AND ini_get('output_handler') !== 'ob_gzhandler' AND (int) ini_get('zlib.output_compression') === 0)
{
if ($level < 1 OR $level > 9)
{
# Normalize the level to be an integer between 1 and 9. This
# step must be done to prevent gzencode from triggering an error
$level = max(1, min($level, 9));
}
if (stripos(@$_SERVER['HTTP_ACCEPT_ENCODING'], 'gzip') !== FALSE)
{
$compress = 'gzip';
}
elseif (stripos(@$_SERVER['HTTP_ACCEPT_ENCODING'], 'deflate') !== FALSE)
{
$compress = 'deflate';
}
}
if (isset($compress) AND $level > 0)
{
switch ($compress)
{
case 'gzip':
# Compress output using gzip
$output = gzencode($output, $level);
break;
case 'deflate':
# Compress output using zlib (HTTP deflate)
$output = gzdeflate($output, $level);
break;
}
# This header must be sent with compressed content to prevent browser caches from breaking
Scaffold::header('Vary','Accept-Encoding');
# Send the content encoding header
Scaffold::header('Content-Encoding',$compress);
# Set the compressed content length
Scaffold::header('Content-Length',strlen($output));
}
# Send the headers
Scaffold::send_headers();
echo $output;
exit;
}
/**
* Sends all of the stored headers to the browser
*
* @return void
*/
private static function send_headers()
{
if(!headers_sent())
{
self::$headers = array_unique(self::$headers);
foreach(self::$headers as $name => $value)
{
if($name != '_status')
{
header($name . ':' . $value);
}
else
{
if($value === 304)
{
header('Status: 304 Not Modified', TRUE, 304);
}
elseif($value === 500)
{
header('HTTP/1.1 500 Internal Server Error');
}
}
}
}
}
/**
* Prepares the final output and cleans up
*
* @return void
*/
public static function shutdown()
{
return self::$output = array(
'status' => self::$has_error,
'content' => self::$output,
'headers' => self::$headers,
'flags' => self::$flags,
'log' => Scaffold_Log::$log,
);
}
/**
* Displays an error and halts the parsing.
*
* @param $message
* @return void
*/
public static function error($message)
{
Scaffold_Log::log($message,0);
}
/**
* Uses the logging class to log a message
*
* @author your name
* @param $message
* @return void
*/
public static function log($message,$level)
{
if ($level <= self::$error_threshold)
{
self::error($message);
}
else
{
Scaffold_Log::log($message,$level);
}
}
/**
* Adds a new HTTP header for sending later.
*
* @author your name
* @param $name
* @param $value
* @return boolean
*/
private static function header($name,$value)
{
return self::$headers[$name] = $value;
}
/**
* Sets a cache flag
*
* @param $name The name of the flag to set
* @return void
*/
public static function flag_set($name)
{
return self::$flags[] = $name;
}
/**
* Checks if a flag is set
*
* @param $flag
* @return boolean
*/
public static function flag($flag)
{
return (in_array($flag,self::$flags)) ? true : false;
}
/**
* Gets the flags from each of the modules
*
* @param $param
* @return $array The array of flags
*/
public static function flags()
{
if(!empty(self::$flags))
return self::$flags;
self::hook('flag');
return (isset(self::$flags)) ? self::$flags : false;
}
/**
* Get all include paths.
*
* @return array
*/
public static function include_paths()
{
return self::$include_paths;
}
/**
* Adds a path to the include paths list
*
* @param $path The server path to add
* @return void
*/
public static function add_include_path($path)
{
if(func_num_args() > 1)
{
$args = func_get_args();
foreach($args as $inc)
self::add_include_path($inc);
}
if(is_file($path))
{
$path = dirname($path);
}
if(!in_array($path,self::$include_paths))
{
self::$include_paths[] = Scaffold_Utils::fix_path($path);
}
}
/**
* Removes an include path
*
* @param $path The server path to remove
* @return void
*/
public static function remove_include_path($path)
{
if(in_array($path, self::$include_paths))
{
unset(self::$include_paths[array_search($path, self::$include_paths)]);
}
}
/**
* Checks to see if an option is set
*
* @param $name
* @return boolean
*/
public static function option($name)
{
return isset(self::$options[$name]);
}
/**
* Loads a view file
*
* @param string The name of the view
* @param boolean Render the view immediately
* @param boolean Return the contents of the view
* @return void If the view is rendered
* @return string The contents of the view
*/
public static function view( $view, $render = false )
{
# Find the view file
$view = self::find_file($view . '.php', 'views', true);
# Display the view
if ($render === true)
{
include $view;
return;
}
# Return the view
else
{
ob_start();
echo file_get_contents($view);
return ob_get_clean();
}
}
/**
* Find a resource file in a given directory. Files will be located according
* to the order of the include paths.
*
* @throws error if file is required and not found
* @param string filename to look for
* @param string directory to search in
* @param boolean file required
* @return string if the file is found
* @return FALSE if the file is not found
*/
public static function find_file($filename, $directory = '', $required = FALSE)
{
# Search path
$search = $directory.DIRECTORY_SEPARATOR.$filename;
if( @file_exists($filename) )
{
return self::$find_file_paths[$filename] = $filename;
}
elseif( $directory && @file_exists($search))
{
return self::$find_file_paths[$search] = realpath($search);
}
if (isset(self::$find_file_paths[$search]))
return self::$find_file_paths[$search];
# Load include paths
$paths = self::include_paths();
# Nothing found, yet
$found = NULL;
if(in_array($directory, $paths))
{
if (is_file($directory.$filename))
{
# A matching file has been found
$found = $search;
}
}
else
{
foreach ($paths as $path)
{
if (is_file($path.$search))
{
# A matching file has been found
$found = realpath($path.$search);
# Stop searching
break;
}
elseif (is_file(realpath($path.$search)))
{
# A matching file has been found
$found = realpath($path.$search);
# Stop searching
break;
}
}
}
if ($found === NULL)
{
if ($required === TRUE)
{
# If the file is required, throw an exception
self::error("Cannot find the file: " . str_replace($_SERVER['DOCUMENT_ROOT'], DIRECTORY_SEPARATOR, $search));
}
else
{
# Nothing was found, return FALSE
$found = FALSE;
}
}
return self::$find_file_paths[$search] = $found;
}
/**
* Lists all files and directories in a resource path.
*
* @param string directory to search
* @param boolean list all files to the maximum depth?
* @param string full path to search (used for recursion, *never* set this manually)
* @return array filenames and directories
*/
public static function list_files($directory, $recursive = FALSE, $path = FALSE)
{
$files = array();
if ($path === FALSE)
{
if(is_dir($directory))
{
$files = array_merge($files, self::list_files($directory, $recursive, $directory));
}
else
{
foreach (array_reverse(self::include_paths()) as $path)
{
$files = array_merge($files, self::list_files($directory, $recursive, $path.$directory));
}
}
}
else
{
$path = rtrim($path, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR;
if (is_readable($path))
{
$items = (array) glob($path.'*');
if ( ! empty($items))
{
foreach ($items as $index => $item)
{
$name = pathinfo($item, PATHINFO_BASENAME);
if(substr($name, 0, 1) == '.' || substr($name, 0, 1) == '-')
{
continue;
}
$files[] = $item = str_replace('\\', DIRECTORY_SEPARATOR, $item);
// Handle recursion
if (is_dir($item) AND $recursive == TRUE)
{
// Filename should only be the basename
$item = pathinfo($item, PATHINFO_BASENAME);
// Append sub-directory search
$files = array_merge($files, self::list_files($directory, TRUE, $path.$item));
}
}
}
}
}
return $files;
}
}