/sally/core/lib/sly/Service/Asset.php
PHP | 405 lines | 255 code | 87 blank | 63 comment | 28 complexity | 63885c29e624239fe3d93d9d49a8bc08 MD5 | raw file
- <?php
- /*
- * Copyright (c) 2013, webvariants GbR, http://www.webvariants.de
- *
- * This file is released under the terms of the MIT license. You can find the
- * complete text in the attached LICENSE file or online at:
- *
- * http://www.opensource.org/licenses/mit-license.php
- */
- /**
- * @author Dave
- */
- class sly_Service_Asset {
- const CACHE_DIR = 'public/sally/static-cache'; ///< string
- const TEMP_DIR = 'internal/sally/temp'; ///< string
- const EVENT_PROCESS_ASSET = 'SLY_CACHE_PROCESS_ASSET'; ///< string
- const EVENT_REVALIDATE_ASSETS = 'SLY_CACHE_REVALIDATE_ASSETS'; ///< string
- const EVENT_GET_PROTECTED_ASSETS = 'SLY_CACHE_GET_PROTECTED_ASSETS'; ///< string
- const EVENT_IS_PROTECTED_ASSET = 'SLY_CACHE_IS_PROTECTED_ASSET'; ///< string
- const ACCESS_PUBLIC = 'public'; ///< string
- const ACCESS_PROTECTED = 'protected'; ///< string
- protected $dispatcher;
- protected $config;
- private $forceGen = true; ///< boolean
- private $accessCache = array(); ///< array
- /**
- * Constructor
- *
- * @param sly_Configuration $config
- * @param sly_Event_IDispatcher $dispatcher
- */
- public function __construct(sly_Configuration $config, sly_Event_IDispatcher $dispatcher) {
- $this->initCache();
- $this->dispatcher = $dispatcher;
- $this->config = $config;
- $dispatcher->register(self::EVENT_PROCESS_ASSET, array($this, 'processLessCSS'));
- }
- /**
- * @param boolean $force
- */
- public function setForceGeneration($force = true) {
- $this->forceGen = (boolean) $force;
- }
- public function validateCache() {
- // [assets/css/main.css, data/mediapool/foo.jpg, ...] (echte, vorhandene Dateien)
- $protected = $this->dispatcher->filter(self::EVENT_GET_PROTECTED_ASSETS, array());
- $protected = array_unique(array_filter($protected));
- $removeAll = !sly_Core::config()->get('asset_cache/file_cache', true);
- foreach (array(self::ACCESS_PUBLIC, self::ACCESS_PROTECTED) as $access) {
- foreach (array('plain', 'gzip', 'deflate') as $encoding) {
- $dir = new sly_Util_Directory(realpath($this->getCacheDir($access, $encoding)));
- $list = $dir->listRecursive();
- // just in case ... should never happen because of $this->initCache()
- if ($list === false) continue;
- foreach ($list as $file) {
- // "/../data/dyn/public/sally/static-cache/gzip/assets/css/main.css"
- $cacheFile = $this->getCacheFile($file, $access, $encoding);
- $realfile = SLY_BASE.'/'.$file;
- if (!file_exists($cacheFile)) {
- continue;
- }
- // if original file is missing, ask listeners
- if (!file_exists($realfile)) {
- $translated = $this->dispatcher->filter(self::EVENT_REVALIDATE_ASSETS, array($file));
- $realfile = SLY_BASE.'/'.reset($translated); // "there can only be one!" ... or at least we hope so
- }
- $relative = str_replace(SLY_BASE, '', $realfile);
- $relative = str_replace('\\', '/', $relative);
- $relative = trim($relative, '/');
- // delete cache file if original is missing or outdated or file cache is disabled
- if ($removeAll || !file_exists($realfile) || filemtime($cacheFile) < filemtime($realfile)) {
- unlink($cacheFile);
- }
- // delete file if it's protected but where in public or vice versa
- elseif (in_array($relative, $protected) !== ($access === self::ACCESS_PROTECTED)) {
- unlink($cacheFile);
- }
- }
- }
- }
- }
- protected function normalizePath($path) {
- $path = sly_Util_Directory::normalize($path);
- $path = str_replace('..', '', $path);
- $path = str_replace(DIRECTORY_SEPARATOR, '/', sly_Util_Directory::normalize($path));
- $path = str_replace('./', '/', $path);
- $path = str_replace(DIRECTORY_SEPARATOR, '/', sly_Util_Directory::normalize($path));
- if (empty($path)) {
- return '';
- }
- if ($path[0] === '/') {
- $path = substr($path, 1);
- }
- return $path;
- }
- public function process($file, $encoding) {
- // check if the file can be streamed
- $blocked = $this->config->get('BLOCKED_EXTENSIONS');
- $ok = true;
- foreach ($blocked as $ext) {
- if (sly_Util_String::endsWith($file, $ext)) {
- $ok = false;
- break;
- }
- }
- if ($ok) {
- $normalized = $this->normalizePath($file);
- $ok = strpos($normalized, '/') === false; // allow files in root directory (favicon)
- if (!$ok) {
- $allowed = $this->config->get('ASSETS_DIRECTORIES');
- foreach ($allowed as $path) {
- if (sly_Util_String::startsWith($file, $path)) {
- $ok = true;
- break;
- }
- }
- }
- }
- if (!$ok) {
- throw new sly_Authorisation_Exception('Forbidden');
- }
- // do the work
- $isProtected = $this->isProtected($file);
- $access = $isProtected ? self::ACCESS_PROTECTED : self::ACCESS_PUBLIC;
- // "/../data/dyn/public/sally/static-cache/[access]/gzip/assets/css/main.css"
- $cacheFile = $this->getCacheFile($file, $access, $encoding);
- // if the file already exists, stop here (can only happen if someone
- // manually requests index.php?slycontroller...).
- if (file_exists($cacheFile) && !$this->forceGen) {
- return new sly_Response('', 400);
- }
- // let listeners process the file
- $tmpFile = $this->dispatcher->filter(self::EVENT_PROCESS_ASSET, $file);
- // now we can check if a listener has generated a valid file
- if (!is_file($tmpFile)) return null;
- if (sly_Core::config()->get('asset_cache/file_cache', true)) {
- // create the encoded file
- $this->generateCacheFile($tmpFile, $cacheFile, $encoding);
- if (!file_exists($cacheFile)) {
- return null;
- }
- }
- // return the plain, unencoded file to the asset controller
- return $tmpFile;
- }
- public function isProtected($file) {
- if (!isset($this->accessCache[$file])) {
- $this->accessCache[$file] = $this->dispatcher->filter(self::EVENT_IS_PROTECTED_ASSET, false, compact('file'));
- }
- return $this->accessCache[$file];
- }
- public function removeCacheFiles($file) {
- foreach (array(self::ACCESS_PUBLIC, self::ACCESS_PROTECTED) as $access) {
- foreach (array('plain', 'gzip', 'deflate') as $encoding) {
- // "/../data/dyn/public/sally/static-cache/gzip/assets/css/main.css"
- $cacheFile = $this->getCacheFile($file, $access, $encoding);
- if (!file_exists($cacheFile)) {
- continue;
- }
- unlink($cacheFile);
- }
- }
- }
- /**
- * @param string $access
- * @param string $encoding
- * @return string
- */
- protected function getCacheDir($access, $encoding) {
- return sly_Util_Directory::join(SLY_DYNFOLDER, self::CACHE_DIR, $access, $encoding);
- }
- /**
- * @param string $file
- * @param string $access
- * @param string $encoding
- * @return string
- */
- protected function getCacheFile($file, $access, $encoding) {
- return sly_Util_Directory::join($this->getCacheDir($access, $encoding), $file);
- }
- /**
- * @param string $sourceFile
- * @param string $cacheFile
- */
- protected function generateCacheFile($sourceFile, $cacheFile, $encoding) {
- clearstatcache();
- sly_Util_Directory::create(dirname($cacheFile), $this->getDirPerm());
- $level = error_reporting(0);
- switch ($encoding) {
- case 'gzip':
- $out = gzopen($cacheFile, 'wb');
- $in = fopen($sourceFile, 'rb');
- while (!feof($in)) {
- $buf = fread($in, 4096);
- gzwrite($out, $buf);
- }
- fclose($in);
- gzclose($out);
- break;
- case 'deflate':
- $out = fopen($cacheFile, 'wb');
- $in = fopen($sourceFile, 'rb');
- stream_filter_append($out, 'zlib.deflate', STREAM_FILTER_WRITE, 6);
- while (!feof($in)) {
- fwrite($out, fread($in, 4096));
- }
- fclose($in);
- fclose($out);
- break;
- case 'plain':
- default:
- copy($sourceFile, $cacheFile);
- }
- if (file_exists($cacheFile)) {
- chmod($cacheFile, $this->getFilePerm());
- }
- error_reporting($level);
- }
- /**
- * @param array $params
- * @return string
- */
- public function processLessCSS(array $params) {
- $file = $params['subject'];
- if (sly_Util_String::endsWith($file, '.less') && file_exists(SLY_BASE.'/'.$file)) {
- $css = sly_Util_Lessphp::process($file);
- $dir = SLY_DYNFOLDER.'/'.self::TEMP_DIR;
- $tmpFile = $dir.'/'.md5($file).'.less';
- sly_Util_Directory::create($dir, $this->getDirPerm());
- file_put_contents($tmpFile, $css);
- chmod($tmpFile, $this->getFilePerm());
- return $tmpFile;
- }
- return $file;
- }
- private function initCache() {
- $dirPerm = $this->getDirPerm();
- $filePerm = $this->getFilePerm();
- $dir = SLY_DYNFOLDER.'/'.self::CACHE_DIR;
- sly_Util_Directory::create($dir, $dirPerm);
- $install = SLY_COREFOLDER.'/install/static-cache/';
- $htaccess = $dir.'/.htaccess';
- if (!file_exists($htaccess)) {
- copy($install.'.htaccess', $htaccess);
- chmod($htaccess, $filePerm);
- }
- $protect_php = $dir.'/protect.php';
- if (!file_exists($protect_php)) {
- $jumper = self::getJumper($dir);
- $contents = file_get_contents($install.'protect.php');
- $contents = str_replace('___JUMPER___', $jumper, $contents);
- file_put_contents($protect_php, $contents);
- chmod($protect_php, $filePerm);
- }
- foreach (array(self::ACCESS_PUBLIC, self::ACCESS_PROTECTED) as $access) {
- sly_Util_Directory::create($dir.'/'.$access.'/gzip', $dirPerm);
- sly_Util_Directory::create($dir.'/'.$access.'/deflate', $dirPerm);
- sly_Util_Directory::create($dir.'/'.$access.'/plain', $dirPerm);
- $file = $dir.'/'.$access.'/gzip/.htaccess';
- if (!file_exists($file)) {
- copy($install.'gzip.htaccess', $file);
- chmod($file, $filePerm);
- }
- $file = $dir.'/'.$access.'/deflate/.htaccess';
- if (!file_exists($file)) {
- copy($install.'deflate.htaccess', $file);
- chmod($file, $filePerm);
- }
- }
- sly_Util_Directory::createHttpProtected($dir.'/'.self::ACCESS_PROTECTED);
- }
- /**
- * @param string $dir
- * @return string
- */
- private static function getJumper($dir) {
- static $jumper = null;
- if ($jumper === null) {
- $pathDiff = trim(substr($dir, strlen(SLY_BASE)), DIRECTORY_SEPARATOR);
- $jumper = str_repeat('../', substr_count($pathDiff, DIRECTORY_SEPARATOR)+1);
- }
- return $jumper;
- }
- public static function clearCache() {
- $me = new self(sly_Core::config(), sly_Core::dispatcher());
- $dir = $me->getCacheDir('', '');
- // Remember htaccess files, so that we do not kill and re-create them.
- // This is important for servers which have custom rules (like RewriteBase settings).
- $htaccess['root'] = file_get_contents($dir.'/.htaccess');
- foreach (array(self::ACCESS_PUBLIC, self::ACCESS_PROTECTED) as $access) {
- $htaccess[$access.'_gzip'] = file_get_contents($dir.'/'.$access.'/gzip/.htaccess');
- $htaccess[$access.'_deflate'] = file_get_contents($dir.'/'.$access.'/deflate/.htaccess');
- }
- // remove the cache directory
- $obj = new sly_Util_Directory($dir);
- $obj->delete(true);
- // clear the temp dir
- $tmpDir = sly_Util_Directory::join(SLY_DYNFOLDER, self::TEMP_DIR);
- $obj = new sly_Util_Directory($tmpDir);
- $obj->deleteFiles(true);
- // re-init the cache dir
- $me->initCache();
- // restore the original .htaccess files again
- $htaccess['root'] = file_put_contents($dir.'/.htaccess', $htaccess['root']);
- foreach (array(self::ACCESS_PUBLIC, self::ACCESS_PROTECTED) as $access) {
- file_put_contents($dir.'/'.$access.'/gzip/.htaccess', $htaccess[$access.'_gzip']);
- file_put_contents($dir.'/'.$access.'/deflate/.htaccess', $htaccess[$access.'_deflate']);
- }
- }
- private function getFilePerm() {
- return sly_Core::getFilePerm();
- }
- private function getDirPerm() {
- return sly_Core::getDirPerm();
- }
- }