array( 'files' => array(), // will be filled with addMedia() 'tplHtml' => '', 'fRewrite' => '', 'fCompress' => 'compressJs', ), 'css' => array( 'files' => array(), // will be filled with addMedia() 'tplHtml' => '', 'fRewrite' => 'rewriteCss', 'fCompress' => 'compressCss', ) ); /** * flag: show debug output? If it is true then you see the merged files, * replacements and statistic data in the html header section (as comment) * @var boolean */ var $bDebug = false; // debug enables additional infos /** * flag: merge files into a cachefile? If it is false then the source * files will be used. This is helpful in the development environment to * debug code with original files. * @var boolean */ var $bMerging = false; // use compressed files; false links to original files /** * flag: overwrite cachefile on each request? * You can use it in your development environment. It overrides the setting * of $this->iMinCacheTime and regenerates merged cachefiles on each request. * debug code with original files. * @var boolean */ var $bRewriteOnEachRequest = false; /** * minimum time to cache js and css cachefile on server - even one of the * source files have been changed. * @var integer */ var $iMinCacheTime = 3600; /** * name for basepath behind webroot * @var string */ var $sCacheBase = '/~cache/'; /** * absolute path of cache directory; it will be set in the method * setDefaults to [document root]/$this->sCacheBase/ * @var string */ var $sCacheDir = false; // ---------------------------------------------------------------------- // INIT // ---------------------------------------------------------------------- /** * init function; it sets defaults. * @return bool (dummy) */ public function __construct() { return $this->setDefaults(); } // ---------------------------------------------------------------------- // private functions // ---------------------------------------------------------------------- /** * generate html code to link the cachefile (or original files if cache is disabled) * @param string $sType type; one of css|js * @param array $aFile array with filedata (keys are attributes) * @return string html code */ private function _genHtml($sType, $aFile) { $s = $this->_aStatic[$sType]["tplHtml"]; // TODO: autodetect with pregmatch $aReplace = array("SRC", "DEFER", "HREF", "MEDIA", "TITLE"); foreach ($aReplace as $sFrom) { $sKey = strtolower($sFrom); $sTo = ""; if ($aFile[$sKey]) $sTo = " $sKey=\"" . $aFile[$sKey] . "\" "; $s = str_replace($sFrom, $sTo, $s); } $s = str_replace('defer="execution"', " ", $s); $s = str_replace(" ", " ", $s); $s = str_replace(" ", " ", $s); $s = str_replace(" >", ">", $s); return $s; } /** * show some statistics info about code compression an gzip * @param type $sOrig * @param type $sCrunched * @param type $sCommment * @return type */ private function _getStats($sOrig, $sCrunched, $sCommment) { if (!$this->bDebug) return ""; $iOrigSize = strlen($sOrig); $iNewSize = strlen($sCrunched); $igzSize1 = strlen(gzencode($sOrig)); $igzSize2 = strlen(gzencode($sCrunched)); return $sCommment . " statistics: ... orig size: $iOrigSize bytes ... compression function: " . $iNewSize . " byte (" . (100 - round($iNewSize / $iOrigSize * 100)) . "% less) ... gzip ...... orig " . $igzSize1 . " byte (" . (100 - round($igzSize1 / $iOrigSize * 100)) . "% less) ...... compressed: " . $igzSize2 . " byte (" . (100 - round($igzSize2 / $iNewSize * 100)) . "% less than crunched; " . (100 - round($igzSize2 / $iOrigSize * 100)) . "% less than original) "; } /** * write logging information (only if debug is enabled) * @param type $s * @return type */ private function _log($s) { if (!$this->bDebug) return true; if ($s == "===") $s = "================================================================================"; if ($s == "---") $s = "................................................................................"; echo "\n"; } // ---------------------------------------------------------------------- // add media files // ---------------------------------------------------------------------- /** * add a css file to this object * @param string $sFile path and filename of css file like in href attribute * @param string $sMedia media type * @param title $sTitle title (will be ignored so far) * @return bool (dummy) */ public function addCss($sFile, $sMedia = 'all', $sTitle = '') { return $this->addMedia("css", array( 'file' => $sFile, 'href' => $sFile, 'media' => $sMedia, 'title' => $sTitle, ) ); } /** * add a js file to the js array * @param string $sFile file to load * @param string $sDefer * @return type */ public function addJs($sFile, $defer = "execution") { return $this->addMedia("js", array( 'file' => $sFile, 'src' => $sFile, 'defer' => $defer, ) ); } /** * add media file to array; it is called by addJs() and addCss() * @param string $sType one of css|js * @param array $aFiledata; Keys: file: filename; other: attributes * @return type */ public function addMedia($sType, $aFiledata) { $sGroup = false; if ($sType == "css") $sGroup = $aFiledata["media"]; if ($sType == "js") $sGroup = $aFiledata["defer"]; if (!$sGroup) return false; return $this->_aStatic[$sType]["files"][$sGroup][] = $aFiledata; } /** * compress css code: remove spaces, tabs, line breaks * @param string $s css data * @return string rewritten css data */ public function compressCss($s) { // remove unneded spaces, tabs, new lines $s = str_replace(array("\r\n", "\r", "\n", "\t", ' ', ' ',), '', $s); /* remove unnecessary spaces */ foreach (explode(" ", "{ } ; , ; ( ) ") as $char) { $s = str_replace('\ *' . $char . '\ *', '{', $s); } // remove comments $s = preg_replace('#\/\*.*\*\/#U', '', $s); return $s; } /** * compress js code * @param string $s js code * @return string with new js code */ public function compressJs($s) { /* remove tabs, spaces, new lines, etc. */ $s = str_replace(array("\t", ' ', ' '), ' ', $s); /* remove unnecessary spaces */ foreach (explode(" ", "{ } ; , ; ( ) ") as $char) { $s = str_replace('\ *' . $char . '\ *', '{', $s); } return $s; } // ---------------------------------------------------------------------- // getter // ---------------------------------------------------------------------- /** * get current basepath of cache * @return boolean */ public function getCacheBase() { return $this->sCacheBase; } /** * get full path of current cache directory * @return boolean */ public function getCacheDir() { return $this->sCacheDir; } /** * get current debug flag * @return boolean */ public function getDebug() { return $this->bDebug; } /** * get current merge flag * @return boolean */ public function getMerging() { return $this->bMerging; } /** * get current caching time on server * @return integer */ public function getMinCacheTime() { return $this->iMinCacheTime; } /** * get current flag to rewrite the cache * @return boolean */ public function getRewriteCache() { return $this->bRewriteOnEachRequest; } // ---------------------------------------------------------------------- // // ---------------------------------------------------------------------- /** * rewrite urls in css data; this function finds relative * pathes and rewrites them to absolute pathes * @param string $s css content * @param string $sFile filename of included css (to detect the "current" path) * @return string the reswritten css code */ public function rewriteCss($s, $sFile) { $aFrom = array(); $aTo = array(); $treffer = array(); preg_match_all("/url\((.*)\)/", $s, $treffer, PREG_OFFSET_CAPTURE); foreach ($treffer[1] as $sUrl) { if (strpos($sUrl[0], "../") or true) { $sTo = $sUrl[0]; $sWrap = $sTo[0]; if ($sWrap == "'" or $sWrap == '"') { $sTo = str_replace($sWrap, "", $sTo); } else $sWrap = ""; // if there is no absolute path then try to rewrite if ($sTo[0] != "/") { $sTo = str_replace($sWrap, "", $sTo); // do replacements $sDir = dirname($sFile); $sTo = str_replace("../", "", $sTo, $count); for ($i = 0; $i < $count; $i++) { $sDir = str_replace(basename($sDir), "", $sDir); } $sTo = $sDir . "/" . $sTo; $sTo = str_replace("//", "/", $sTo); $sTo = str_replace("./", "", $sTo); // add wrapping characters $sTo = $sWrap . $sTo . $sWrap; $aFrom[] = $sUrl[0]; $aTo[] = $sTo; $this->_log("replacing " . $sUrl[0] . " to: " . $sTo . " "); } } } if (count($aFrom)) $s = str_replace($aFrom, $aTo, $s); return $s; } // ---------------------------------------------------------------------- // setter // ---------------------------------------------------------------------- /** * set another basepath (behind webroot) * @param string new basepath * @return boolean */ public function setCacheBase($str) { $this->sCacheDir = $_SERVER["DOCUMENT_ROOT"] . $str; return $this->sCacheBase = $str; } /** * set another cache directory. * @param string new basepath (below webroot) * @return boolean */ public function setCacheDir($str) { return $this->sCacheDir = $_SERVER["DOCUMENT_ROOT"] . $str; } /** * set debug flag (for development environment) * @param boolean set true to enable debug output * @return boolean */ public function setDebug($bool) { return $this->bDebug = (bool) $bool; } /** * set debug default settings * @return bool (always true) */ public function setDebugDefaults() { $this->iMinCacheTime = 0; $this->bDebug = true; $this->bMerging = true; // set to false that you can debug with original files $this->bRewriteOnEachRequest = true; return true; } /** * set all vars to initial values * @return bool (always true) */ public function setDefaults() { $this->sCacheBase = '/~cache/'; // added to url $this->iMinCacheTime = 3600; // force using the cachefile and don't update it $this->bDebug = false; // debug enables additional infos $this->bMerging = true; // use compressed files $this->sCacheDir = $_SERVER["DOCUMENT_ROOT"] . $this->sCacheBase; // local file $this->bRewriteOnEachRequest = false; return true; } /** * set merge flag (for development environment) * @param boolean set false to disable the generation of merged cachefiles * @return boolean */ public function setMerging($bool) { return $this->bMerging = (bool) $bool; } /** * set minimum cachetime on server for merged js and css files * @param integer value in seconds * @return boolean */ public function setMinCacheTime($int) { return $this->iMinCacheTime = (int) $int; } /** * set flag bRewriteOnEachRequest (for development environment) * @param boolean set true to ignore caching time * @return type */ public function setRewriteCache($bool) { return $this->bRewriteOnEachRequest = (bool) $bool; } // ---------------------------------------------------------------------- // // ---------------------------------------------------------------------- /** * Merge original files of a group to a single cachefile. It updates or just touches the cachefile. * Output is the html code to include the cached css/ js in the html header. * @param string $sType one of css|js * @return bool (dummy) */ public function writeMedia($sType) { $sHtml = ''; // generated html output to document $aFiles = $this->_aStatic[$sType]["files"]; $this->_log("==="); $this->_log("type: $sType"); $this->_log("THANKS FOR USING MY PHP CLASS STATICFILES!\n (this message and debug stuff below is shown with enabled debug only)\n www.axel-hahn.de"); // if caching is disabled then generate output with original files and return if (!$this->bMerging) { $this->_log("You disabled the merging feature - here you get the links to the original files."); foreach ($aFiles as $sGroup => $aFiledata) { foreach ($aFiledata as $aFile) { $sHtml.=$this->_genHtml($sType, $aFile); } } echo $sHtml; return true; } // step 1: get all files of a group $aFileList = array(); foreach ($aFiles as $sGroup => $aFiledata) { foreach ($aFiledata as $aFile) { $aFileList[$sGroup][] = $aFile['file']; } } // check / regenerate cache file foreach ($aFiles as $sGroup => $aFiledata) { $bCompress = true; $iNewest = 0; $sData = ''; // get cachefile and check age $sCacheFile = $sType . "_cache_" . md5($sGroup . implode($aFileList[$sGroup])) . "." . $sType; $sCacheFullFile = $this->sCacheDir . $sCacheFile; $this->_log("==="); $this->_log("$sType-group: \"$sGroup\""); $this->_log("cachefile = " . $sCacheFullFile); if (file_exists($sCacheFullFile)) { $bCompress = false; $aStatCache = stat($sCacheFullFile); $iCacheAge = date("U") - $aStatCache['mtime']; if ($iCacheAge > $this->iMinCacheTime) $bCompress = true; $this->_log("age of cachefile = $iCacheAge s (cachefile will be used $this->iMinCacheTime s; " . ($this->iMinCacheTime - $iCacheAge) . " s left)"); } else { $this->_log("cachefile does not exist yet"); } // if there is no cachefile or cachefile is out of date // then compare with newest filestamp if ($bCompress) { // merge all files into $sData // and get timestamp of newest file $iNewest foreach ($aFiledata as $aFile) { $this->_log("---"); $sFile = $aFile["file"]; $sFullFile = $_SERVER["DOCUMENT_ROOT"] . $sFile; if (!file_exists($sFullFile)) die("ERROR: writeMedia() failed. File not found: $sFullFile."); $aTmp = stat($sFullFile); if ($aTmp['mtime'] > $iNewest) $iNewest = $aTmp['mtime']; $s = file_get_contents($sFullFile); $this->_log("checking $sFile - \tunix ts: " . $aTmp['mtime'] . ";\t" . strlen($s) . " byte (group: " . $sGroup . ")"); if ($this->_aStatic[$sType]["fRewrite"]) { $this->_log("rewrite with \$this->" . $this->_aStatic[$sType]["fRewrite"]); $sRewritten = $this->{$this->_aStatic[$sType]["fRewrite"]}($s, $sFile); if (strlen($sRewritten) == 0) { $this->_log("OOOPS rewrite function for file $sFile - " . $this->_aStatic[$sType]["fRewrite"] . " returns ZERO(!!!) bytes"); } else $s = $sRewritten; } $sData.=$s; } $this->_log("unix ts of cachefile " . $aStatCache['mtime']); if (($aStatCache['mtime'] - $iMinCacheTime) < $iNewest || $this->bRewriteOnEachRequest) { $this->_log("Need to (re)generate cache. A static file is newer than cache or cache did not exist."); $sOrig = $sData; if ($this->_aStatic[$sType]["fCompress"]) { $this->_log("compress with \$this->" . $this->_aStatic[$sType]["fCompress"] . " "); $sData = $this->{$this->_aStatic[$sType]["fCompress"]}($sData); if (strlen($sData) == 0) { $this->_log("OOOPS compress function for type $sType - " . $this->_aStatic[$sType]["fCompress"] . " returns ZERO(!!!) bytes"); $this->_log("orig: " . strlen($sOrig) . " byte - files: \n" . print_r($aFileList[$sGroup], true) . " "); $sData = $sOrig; } } else { $this->_log("no compression for type " . $sType . " "); } $sStats = $this->_getStats($sOrig, $sData, " "); $this->_log($sStats); if ($this->bDebug) $sData = "/* AXEL HAHN - CACHED " . $sType . " MERGE FILE ... created " . date("Y-m-d H:i:s") . " " . $sStats . " (you see this only if debug is enabled) */ \n" . $sData; $this->_log("writing cachefile"); if (!file_exists(dirname($sCacheFullFile))) mkdir(dirname($sCacheFullFile)); file_put_contents($sCacheFullFile, $sData); } else { $this->_log("just touching cachefile to use it further $this->iMinCacheTime s"); touch($sCacheFullFile); } } // generate html output: link to the cacheile if ($sType == "css") $aCachefileoptions = array( 'file' => $this->sCacheBase . $sCacheFile, 'href' => $this->sCacheBase . $sCacheFile, 'media' => $sGroup, ); if ($sType == "js") { $aCachefileoptions = array( 'file' => $this->sCacheBase . $sCacheFile, 'src' => $this->sCacheBase . $sCacheFile, ); } if ($aCachefileoptions) $sHtml.=$this->_genHtml($sType, $aCachefileoptions) . "\n"; } echo $sHtml . "\n"; return true; } } ?>