PageRenderTime 54ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/sally/core/lib/sly/Service/Asset.php

https://bitbucket.org/mediastuttgart/sallycms-0.7
PHP | 405 lines | 255 code | 87 blank | 63 comment | 28 complexity | 63885c29e624239fe3d93d9d49a8bc08 MD5 | raw file
  1. <?php
  2. /*
  3. * Copyright (c) 2013, webvariants GbR, http://www.webvariants.de
  4. *
  5. * This file is released under the terms of the MIT license. You can find the
  6. * complete text in the attached LICENSE file or online at:
  7. *
  8. * http://www.opensource.org/licenses/mit-license.php
  9. */
  10. /**
  11. * @author Dave
  12. */
  13. class sly_Service_Asset {
  14. const CACHE_DIR = 'public/sally/static-cache'; ///< string
  15. const TEMP_DIR = 'internal/sally/temp'; ///< string
  16. const EVENT_PROCESS_ASSET = 'SLY_CACHE_PROCESS_ASSET'; ///< string
  17. const EVENT_REVALIDATE_ASSETS = 'SLY_CACHE_REVALIDATE_ASSETS'; ///< string
  18. const EVENT_GET_PROTECTED_ASSETS = 'SLY_CACHE_GET_PROTECTED_ASSETS'; ///< string
  19. const EVENT_IS_PROTECTED_ASSET = 'SLY_CACHE_IS_PROTECTED_ASSET'; ///< string
  20. const ACCESS_PUBLIC = 'public'; ///< string
  21. const ACCESS_PROTECTED = 'protected'; ///< string
  22. protected $dispatcher;
  23. protected $config;
  24. private $forceGen = true; ///< boolean
  25. private $accessCache = array(); ///< array
  26. /**
  27. * Constructor
  28. *
  29. * @param sly_Configuration $config
  30. * @param sly_Event_IDispatcher $dispatcher
  31. */
  32. public function __construct(sly_Configuration $config, sly_Event_IDispatcher $dispatcher) {
  33. $this->initCache();
  34. $this->dispatcher = $dispatcher;
  35. $this->config = $config;
  36. $dispatcher->register(self::EVENT_PROCESS_ASSET, array($this, 'processLessCSS'));
  37. }
  38. /**
  39. * @param boolean $force
  40. */
  41. public function setForceGeneration($force = true) {
  42. $this->forceGen = (boolean) $force;
  43. }
  44. public function validateCache() {
  45. // [assets/css/main.css, data/mediapool/foo.jpg, ...] (echte, vorhandene Dateien)
  46. $protected = $this->dispatcher->filter(self::EVENT_GET_PROTECTED_ASSETS, array());
  47. $protected = array_unique(array_filter($protected));
  48. $removeAll = !sly_Core::config()->get('asset_cache/file_cache', true);
  49. foreach (array(self::ACCESS_PUBLIC, self::ACCESS_PROTECTED) as $access) {
  50. foreach (array('plain', 'gzip', 'deflate') as $encoding) {
  51. $dir = new sly_Util_Directory(realpath($this->getCacheDir($access, $encoding)));
  52. $list = $dir->listRecursive();
  53. // just in case ... should never happen because of $this->initCache()
  54. if ($list === false) continue;
  55. foreach ($list as $file) {
  56. // "/../data/dyn/public/sally/static-cache/gzip/assets/css/main.css"
  57. $cacheFile = $this->getCacheFile($file, $access, $encoding);
  58. $realfile = SLY_BASE.'/'.$file;
  59. if (!file_exists($cacheFile)) {
  60. continue;
  61. }
  62. // if original file is missing, ask listeners
  63. if (!file_exists($realfile)) {
  64. $translated = $this->dispatcher->filter(self::EVENT_REVALIDATE_ASSETS, array($file));
  65. $realfile = SLY_BASE.'/'.reset($translated); // "there can only be one!" ... or at least we hope so
  66. }
  67. $relative = str_replace(SLY_BASE, '', $realfile);
  68. $relative = str_replace('\\', '/', $relative);
  69. $relative = trim($relative, '/');
  70. // delete cache file if original is missing or outdated or file cache is disabled
  71. if ($removeAll || !file_exists($realfile) || filemtime($cacheFile) < filemtime($realfile)) {
  72. unlink($cacheFile);
  73. }
  74. // delete file if it's protected but where in public or vice versa
  75. elseif (in_array($relative, $protected) !== ($access === self::ACCESS_PROTECTED)) {
  76. unlink($cacheFile);
  77. }
  78. }
  79. }
  80. }
  81. }
  82. protected function normalizePath($path) {
  83. $path = sly_Util_Directory::normalize($path);
  84. $path = str_replace('..', '', $path);
  85. $path = str_replace(DIRECTORY_SEPARATOR, '/', sly_Util_Directory::normalize($path));
  86. $path = str_replace('./', '/', $path);
  87. $path = str_replace(DIRECTORY_SEPARATOR, '/', sly_Util_Directory::normalize($path));
  88. if (empty($path)) {
  89. return '';
  90. }
  91. if ($path[0] === '/') {
  92. $path = substr($path, 1);
  93. }
  94. return $path;
  95. }
  96. public function process($file, $encoding) {
  97. // check if the file can be streamed
  98. $blocked = $this->config->get('BLOCKED_EXTENSIONS');
  99. $ok = true;
  100. foreach ($blocked as $ext) {
  101. if (sly_Util_String::endsWith($file, $ext)) {
  102. $ok = false;
  103. break;
  104. }
  105. }
  106. if ($ok) {
  107. $normalized = $this->normalizePath($file);
  108. $ok = strpos($normalized, '/') === false; // allow files in root directory (favicon)
  109. if (!$ok) {
  110. $allowed = $this->config->get('ASSETS_DIRECTORIES');
  111. foreach ($allowed as $path) {
  112. if (sly_Util_String::startsWith($file, $path)) {
  113. $ok = true;
  114. break;
  115. }
  116. }
  117. }
  118. }
  119. if (!$ok) {
  120. throw new sly_Authorisation_Exception('Forbidden');
  121. }
  122. // do the work
  123. $isProtected = $this->isProtected($file);
  124. $access = $isProtected ? self::ACCESS_PROTECTED : self::ACCESS_PUBLIC;
  125. // "/../data/dyn/public/sally/static-cache/[access]/gzip/assets/css/main.css"
  126. $cacheFile = $this->getCacheFile($file, $access, $encoding);
  127. // if the file already exists, stop here (can only happen if someone
  128. // manually requests index.php?slycontroller...).
  129. if (file_exists($cacheFile) && !$this->forceGen) {
  130. return new sly_Response('', 400);
  131. }
  132. // let listeners process the file
  133. $tmpFile = $this->dispatcher->filter(self::EVENT_PROCESS_ASSET, $file);
  134. // now we can check if a listener has generated a valid file
  135. if (!is_file($tmpFile)) return null;
  136. if (sly_Core::config()->get('asset_cache/file_cache', true)) {
  137. // create the encoded file
  138. $this->generateCacheFile($tmpFile, $cacheFile, $encoding);
  139. if (!file_exists($cacheFile)) {
  140. return null;
  141. }
  142. }
  143. // return the plain, unencoded file to the asset controller
  144. return $tmpFile;
  145. }
  146. public function isProtected($file) {
  147. if (!isset($this->accessCache[$file])) {
  148. $this->accessCache[$file] = $this->dispatcher->filter(self::EVENT_IS_PROTECTED_ASSET, false, compact('file'));
  149. }
  150. return $this->accessCache[$file];
  151. }
  152. public function removeCacheFiles($file) {
  153. foreach (array(self::ACCESS_PUBLIC, self::ACCESS_PROTECTED) as $access) {
  154. foreach (array('plain', 'gzip', 'deflate') as $encoding) {
  155. // "/../data/dyn/public/sally/static-cache/gzip/assets/css/main.css"
  156. $cacheFile = $this->getCacheFile($file, $access, $encoding);
  157. if (!file_exists($cacheFile)) {
  158. continue;
  159. }
  160. unlink($cacheFile);
  161. }
  162. }
  163. }
  164. /**
  165. * @param string $access
  166. * @param string $encoding
  167. * @return string
  168. */
  169. protected function getCacheDir($access, $encoding) {
  170. return sly_Util_Directory::join(SLY_DYNFOLDER, self::CACHE_DIR, $access, $encoding);
  171. }
  172. /**
  173. * @param string $file
  174. * @param string $access
  175. * @param string $encoding
  176. * @return string
  177. */
  178. protected function getCacheFile($file, $access, $encoding) {
  179. return sly_Util_Directory::join($this->getCacheDir($access, $encoding), $file);
  180. }
  181. /**
  182. * @param string $sourceFile
  183. * @param string $cacheFile
  184. */
  185. protected function generateCacheFile($sourceFile, $cacheFile, $encoding) {
  186. clearstatcache();
  187. sly_Util_Directory::create(dirname($cacheFile), $this->getDirPerm());
  188. $level = error_reporting(0);
  189. switch ($encoding) {
  190. case 'gzip':
  191. $out = gzopen($cacheFile, 'wb');
  192. $in = fopen($sourceFile, 'rb');
  193. while (!feof($in)) {
  194. $buf = fread($in, 4096);
  195. gzwrite($out, $buf);
  196. }
  197. fclose($in);
  198. gzclose($out);
  199. break;
  200. case 'deflate':
  201. $out = fopen($cacheFile, 'wb');
  202. $in = fopen($sourceFile, 'rb');
  203. stream_filter_append($out, 'zlib.deflate', STREAM_FILTER_WRITE, 6);
  204. while (!feof($in)) {
  205. fwrite($out, fread($in, 4096));
  206. }
  207. fclose($in);
  208. fclose($out);
  209. break;
  210. case 'plain':
  211. default:
  212. copy($sourceFile, $cacheFile);
  213. }
  214. if (file_exists($cacheFile)) {
  215. chmod($cacheFile, $this->getFilePerm());
  216. }
  217. error_reporting($level);
  218. }
  219. /**
  220. * @param array $params
  221. * @return string
  222. */
  223. public function processLessCSS(array $params) {
  224. $file = $params['subject'];
  225. if (sly_Util_String::endsWith($file, '.less') && file_exists(SLY_BASE.'/'.$file)) {
  226. $css = sly_Util_Lessphp::process($file);
  227. $dir = SLY_DYNFOLDER.'/'.self::TEMP_DIR;
  228. $tmpFile = $dir.'/'.md5($file).'.less';
  229. sly_Util_Directory::create($dir, $this->getDirPerm());
  230. file_put_contents($tmpFile, $css);
  231. chmod($tmpFile, $this->getFilePerm());
  232. return $tmpFile;
  233. }
  234. return $file;
  235. }
  236. private function initCache() {
  237. $dirPerm = $this->getDirPerm();
  238. $filePerm = $this->getFilePerm();
  239. $dir = SLY_DYNFOLDER.'/'.self::CACHE_DIR;
  240. sly_Util_Directory::create($dir, $dirPerm);
  241. $install = SLY_COREFOLDER.'/install/static-cache/';
  242. $htaccess = $dir.'/.htaccess';
  243. if (!file_exists($htaccess)) {
  244. copy($install.'.htaccess', $htaccess);
  245. chmod($htaccess, $filePerm);
  246. }
  247. $protect_php = $dir.'/protect.php';
  248. if (!file_exists($protect_php)) {
  249. $jumper = self::getJumper($dir);
  250. $contents = file_get_contents($install.'protect.php');
  251. $contents = str_replace('___JUMPER___', $jumper, $contents);
  252. file_put_contents($protect_php, $contents);
  253. chmod($protect_php, $filePerm);
  254. }
  255. foreach (array(self::ACCESS_PUBLIC, self::ACCESS_PROTECTED) as $access) {
  256. sly_Util_Directory::create($dir.'/'.$access.'/gzip', $dirPerm);
  257. sly_Util_Directory::create($dir.'/'.$access.'/deflate', $dirPerm);
  258. sly_Util_Directory::create($dir.'/'.$access.'/plain', $dirPerm);
  259. $file = $dir.'/'.$access.'/gzip/.htaccess';
  260. if (!file_exists($file)) {
  261. copy($install.'gzip.htaccess', $file);
  262. chmod($file, $filePerm);
  263. }
  264. $file = $dir.'/'.$access.'/deflate/.htaccess';
  265. if (!file_exists($file)) {
  266. copy($install.'deflate.htaccess', $file);
  267. chmod($file, $filePerm);
  268. }
  269. }
  270. sly_Util_Directory::createHttpProtected($dir.'/'.self::ACCESS_PROTECTED);
  271. }
  272. /**
  273. * @param string $dir
  274. * @return string
  275. */
  276. private static function getJumper($dir) {
  277. static $jumper = null;
  278. if ($jumper === null) {
  279. $pathDiff = trim(substr($dir, strlen(SLY_BASE)), DIRECTORY_SEPARATOR);
  280. $jumper = str_repeat('../', substr_count($pathDiff, DIRECTORY_SEPARATOR)+1);
  281. }
  282. return $jumper;
  283. }
  284. public static function clearCache() {
  285. $me = new self(sly_Core::config(), sly_Core::dispatcher());
  286. $dir = $me->getCacheDir('', '');
  287. // Remember htaccess files, so that we do not kill and re-create them.
  288. // This is important for servers which have custom rules (like RewriteBase settings).
  289. $htaccess['root'] = file_get_contents($dir.'/.htaccess');
  290. foreach (array(self::ACCESS_PUBLIC, self::ACCESS_PROTECTED) as $access) {
  291. $htaccess[$access.'_gzip'] = file_get_contents($dir.'/'.$access.'/gzip/.htaccess');
  292. $htaccess[$access.'_deflate'] = file_get_contents($dir.'/'.$access.'/deflate/.htaccess');
  293. }
  294. // remove the cache directory
  295. $obj = new sly_Util_Directory($dir);
  296. $obj->delete(true);
  297. // clear the temp dir
  298. $tmpDir = sly_Util_Directory::join(SLY_DYNFOLDER, self::TEMP_DIR);
  299. $obj = new sly_Util_Directory($tmpDir);
  300. $obj->deleteFiles(true);
  301. // re-init the cache dir
  302. $me->initCache();
  303. // restore the original .htaccess files again
  304. $htaccess['root'] = file_put_contents($dir.'/.htaccess', $htaccess['root']);
  305. foreach (array(self::ACCESS_PUBLIC, self::ACCESS_PROTECTED) as $access) {
  306. file_put_contents($dir.'/'.$access.'/gzip/.htaccess', $htaccess[$access.'_gzip']);
  307. file_put_contents($dir.'/'.$access.'/deflate/.htaccess', $htaccess[$access.'_deflate']);
  308. }
  309. }
  310. private function getFilePerm() {
  311. return sly_Core::getFilePerm();
  312. }
  313. private function getDirPerm() {
  314. return sly_Core::getDirPerm();
  315. }
  316. }