PageRenderTime 43ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/framework/classes/frontcache.php

https://github.com/jay3/core
PHP | 480 lines | 363 code | 54 blank | 63 comment | 65 complexity | e6edc5ec2a48661817de8b48bfc9308b MD5 | raw file
Possible License(s): LGPL-2.1
  1. <?php
  2. /**
  3. * NOVIUS OS - Web OS for digital communication
  4. *
  5. * @copyright 2011 Novius
  6. * @license GNU Affero General Public License v3 or (at your option) any later version
  7. * http://www.gnu.org/licenses/agpl-3.0.html
  8. * @link http://www.novius-os.org
  9. */
  10. namespace Nos;
  11. class CacheNotFoundException extends \Exception
  12. {
  13. }
  14. class CacheExpiredException extends \Exception
  15. {
  16. }
  17. class FrontCache
  18. {
  19. protected static $_php_begin = null;
  20. /**
  21. * Loads any default caching settings when available
  22. */
  23. public static function _init()
  24. {
  25. \Config::load('cache', true);
  26. }
  27. protected static function _phpBegin()
  28. {
  29. if (static::$_php_begin !== null) {
  30. return static::$_php_begin;
  31. }
  32. \Config::load('crypt', true);
  33. static::$_php_begin = md5(\Config::get('crypt.hmac').'begin');
  34. return static::$_php_begin;
  35. }
  36. /**
  37. * Write a HMVC call in cache for after cache execution.
  38. *
  39. * @param string $uri Route for the request.
  40. * @param array $args The method parameters.
  41. */
  42. public static function callHmvcUncached($uri, $args = array())
  43. {
  44. echo static::_phpBegin();
  45. // Serialize allow to persist objects in the cache file
  46. // API is Nos\Nos::hmvc('location', array('args' => $args))
  47. echo 'echo \Nos\Nos::hmvc('.var_export($uri, true).', unserialize('.var_export(serialize(array('args' => $args)), true).'));';
  48. echo '?>';
  49. }
  50. /**
  51. * Write a View forge in cache for after cache execution.
  52. *
  53. * @param string $file The view filename
  54. * @param array $data Array of values
  55. * @param boolean $auto_filter Set to true or false to set auto encoding
  56. */
  57. public static function viewForgeUncached($file = null, $data = null, $auto_filter = null)
  58. {
  59. echo static::_phpBegin();
  60. // Serialize allow to persist objects in the cache file
  61. echo 'echo View::forge('.var_export($file, true).', unserialize('.var_export(serialize($data), true).'), '.var_export($auto_filter, true).');';
  62. echo '?>';
  63. }
  64. public static function forge($path)
  65. {
  66. return new static($path);
  67. }
  68. public static function get($path, $params = array())
  69. {
  70. if (empty($params['callback_func']) || !is_callable($params['callback_func'])) {
  71. \Fuel::$profiling && \Profiler::console($params);
  72. \Fuel::$profiling && \Console::logError(new \Exception(), "Invalid callback_func.");
  73. return;
  74. }
  75. $params = \Arr::merge(
  76. array(
  77. 'callback_args' => array(),
  78. 'duration' => \Config::get('novius-os.cache_duration_function', 10),
  79. 'controller' => null,
  80. ),
  81. $params
  82. );
  83. $cache = new static($path);
  84. try {
  85. return $cache->executeOrStart($params['controller']);
  86. } catch (CacheNotFoundException $e) {
  87. call_user_func_array($params['callback_func'], $params['callback_args']);
  88. return $cache->saveAndExecute($params['duration'], $params['controller']);
  89. }
  90. }
  91. protected $_init_path = null;
  92. protected $_path = null;
  93. protected $_path_suffix = null;
  94. protected $_suffix_handlers = array();
  95. protected $_level = null;
  96. protected $_content = '';
  97. protected $_lock_fp = null;
  98. public function __construct($path = false)
  99. {
  100. $path = \File::validOSPath($path);
  101. if ($path == false) {
  102. $this->_path = false;
  103. } else {
  104. $path = \Config::get('cache_dir').$path;
  105. $this->_init_path = $path.'.php';
  106. $this->_path_suffix = $path.'.cache.suffixes/';
  107. $this->reset();
  108. }
  109. }
  110. public function reset()
  111. {
  112. $this->_path = $this->_init_path;
  113. $this->_suffix_handlers = array();
  114. }
  115. public function addSuffixHandler(array $handler)
  116. {
  117. if (isset($handler['type'])) {
  118. $this->_suffix_handlers[] = $handler;
  119. } else {
  120. $this->_suffix_handlers = $this->_suffix_handlers + $handler;
  121. }
  122. $this->_suffix_handlers = array_unique($this->_suffix_handlers);
  123. $this->_suffix_handlers();
  124. return $this->_path !== $this->_init_path ? $this : null;
  125. }
  126. protected function _suffix_handlers()
  127. {
  128. $suffixes = array();
  129. foreach ($this->_suffix_handlers as $handler) {
  130. $type = isset($handler['type']) ? $handler['type'] : null;
  131. switch ($type) {
  132. case 'GET':
  133. if (!empty($handler['keys'])) {
  134. $keys = (array) $handler['keys'];
  135. foreach ($keys as $key) {
  136. if (isset($_GET[$key])) {
  137. $suffixes[] = 'GET['.urlencode($key).']='.(is_array($_GET[$key]) ? http_build_query($_GET[$key]) : urlencode($_GET[$key]));
  138. }
  139. }
  140. }
  141. break;
  142. case 'callable':
  143. if (!empty($handler['callable']) && is_callable($handler['callable'], false, $callable_name)) {
  144. if (empty($callable_name)) {
  145. \Log::warning('Suffix handlers can\'t be a closure');
  146. break;
  147. }
  148. $args = is_array($handler['args']) ? $handler['args'] : array();
  149. $suffix = call_user_func_array($handler['callable'], $args);
  150. if (!empty($suffix)) {
  151. $suffixes[] = $suffix;
  152. }
  153. }
  154. break;
  155. }
  156. }
  157. if (!empty($suffixes)) {
  158. $this->_path = $this->_path_suffix.implode('&', $suffixes).'.php';
  159. $basename = basename($this->_path);
  160. if (\Str::length($basename) > 100) {
  161. $this->_path = dirname($this->_path).DS.\Str::sub($basename, 0, 100).md5($basename).'.php';
  162. }
  163. }
  164. }
  165. public function execute($controller = null)
  166. {
  167. // Get an exclusive lock
  168. //$this->_lock_fp = fopen($this->_path, 'c');
  169. //flock($this->_lock_fp, LOCK_EX);
  170. if (!empty($this->_path) && is_file($this->_path)) {
  171. try {
  172. ob_start();
  173. include $this->_path;
  174. $this->content = ob_get_clean();
  175. //flock($this->_lock_fp, LOCK_UN);
  176. return $this->content;
  177. } catch (CacheExpiredException $e) {
  178. ob_end_clean();
  179. @unlink($this->_path);
  180. }
  181. }
  182. throw new CacheNotFoundException();
  183. }
  184. public function start()
  185. {
  186. ob_start();
  187. ob_implicit_flush(false);
  188. $this->_level = ob_get_level();
  189. }
  190. public static function checkExpires($expires)
  191. {
  192. if ($expires > 0 && $expires <= time()) {
  193. throw new CacheExpiredException();
  194. }
  195. }
  196. public function save($duration = -1, $controller = null)
  197. {
  198. $prepend = '';
  199. $this->_content = '';
  200. if ($duration == -1) {
  201. //flock($this->_lock_fp, LOCK_UN);
  202. $expires = 0;
  203. $this->_path = \Config::get('novius-os.temp_dir').DS.uniqid('page/').'.php';
  204. } else {
  205. $expires = time() + $duration;
  206. $prepend .= '<?php
  207. '.__CLASS__.'::checkExpires('.$expires.');'."\n";
  208. if (!empty($controller)) {
  209. if ($this->_path === $this->_init_path && !empty($this->_suffix_handlers)) {
  210. $prepend .= $this->cacheSuffixHandlers();
  211. }
  212. $prepend .= '
  213. if (!empty($controller)) {
  214. $controller->rebuildCache(unserialize('.var_export(serialize($controller->getCache()), true).'));
  215. }'."\n";
  216. }
  217. $prepend .= '?>';
  218. \Fuel::$profiling && \Profiler::console('FrontCache:'.\Fuel::clean_path($this->_path).' saved for '.$duration.' s.');
  219. }
  220. while (ob_get_level() >= $this->_level) {
  221. $this->_content .= ob_get_clean();
  222. }
  223. // Prevent PHP injection using a <script language=php> tag
  224. $this->_content = preg_replace('`<script\s+language=(.?)php\1\s*>(.*?)</script>`i', '&lt;script language=$1php$1>$2&lt;/script>', $this->_content);
  225. $this->_content = preg_replace('`<\?(?!xml)`i', '&lt;?', $this->_content);
  226. $this->_content = str_replace('<?xml', "<?= '<?' ?>xml", $this->_content);
  227. if (null !== static::$_php_begin) {
  228. $this->_content = strtr(
  229. $this->_content,
  230. array(
  231. // Replace legitimate PHP tags
  232. static::$_php_begin => "<?php\n",
  233. )
  234. );
  235. }
  236. $this->_content = $prepend.$this->_content;
  237. if (!static::store($this->_path, $this->_content, $duration == -1)) {
  238. trigger_error('Cache could not be written! (path = '.$this->_path.')', E_USER_WARNING);
  239. }
  240. //flock($this->_lock_fp, LOCK_UN);
  241. if ($duration != -1 && $this->_path !== $this->_init_path && !empty($this->_suffix_handlers) && !is_file($this->_init_path)) {
  242. $content = "<?php\n";
  243. if (!empty($controller)) {
  244. $content .= $this->cacheSuffixHandlers();
  245. }
  246. $content .= '
  247. throw new \Nos\CacheNotFoundException();
  248. '."\n";
  249. $content .= '?>';
  250. if (!static::store($this->_init_path, $content, $duration == -1)) {
  251. trigger_error('Cache could not be written! (path = '.$this->_init_path.')', E_USER_WARNING);
  252. }
  253. }
  254. }
  255. public function saveAndExecute($duration = -1, $controller = null)
  256. {
  257. $this->save($duration, $controller);
  258. return $this->execute($controller);
  259. }
  260. public function executeOrStart($controller = null)
  261. {
  262. try {
  263. return $this->execute($controller);
  264. } catch (CacheNotFoundException $e) {
  265. \Fuel::$profiling && \Profiler::console('Publicache:'.\Fuel::clean_path($this->_path).' has expired.');
  266. $this->start();
  267. throw $e;
  268. }
  269. }
  270. protected function cacheSuffixHandlers()
  271. {
  272. return '
  273. if (!empty($controller)) {
  274. $cache = $controller->addCacheSuffixHandler(unserialize('.var_export(serialize($this->_suffix_handlers), true).'));
  275. if (!empty($cache)) {
  276. echo $cache->execute($controller);
  277. return;
  278. }
  279. }'."\n";
  280. }
  281. protected static function store($path, $content, $temporary = false)
  282. {
  283. $dir = dirname($path);
  284. // check if specified subdir exists
  285. if (!@is_dir($dir)) {
  286. // create non existing dir
  287. if (!@mkdir($dir, 0755, true)) {
  288. return false;
  289. }
  290. }
  291. file_put_contents($path, $content);
  292. if ($temporary) {
  293. register_shutdown_function('unlink', $path);
  294. }
  295. return true;
  296. }
  297. public function delete()
  298. {
  299. // Delete plain file, like 'my/page.html.php'
  300. if (is_file($this->_path)) {
  301. @unlink($this->_path);
  302. }
  303. // Delete sub-files
  304. // Remove trailing .php to get 'my/page.html'
  305. $path = substr($this->_path, 0, -4);
  306. // Remove extension to get 'my/page'
  307. $extension = pathinfo($path, PATHINFO_EXTENSION);
  308. if (!empty($extension)) {
  309. $path = substr($path, 0, - strlen($extension) - 1);
  310. }
  311. // Delete the directory 'my/page/*'
  312. if (is_dir($path)) {
  313. try {
  314. \File::delete_dir($path, true, true);
  315. } catch (\Exception $e) {
  316. \Log::exception($e, 'Error while deleting the directory "'.$path.'" cache. ');
  317. }
  318. }
  319. // Delete suffixes directory 'my/page.cache.suffixes/*'
  320. if (is_dir($this->_path_suffix)) {
  321. try {
  322. \File::delete_dir($this->_path_suffix, true, true);
  323. } catch (\Exception $e) {
  324. \Log::exception($e, 'Error while deleting the suffix directory "'.$this->_path_suffix.'" cache. ');
  325. }
  326. }
  327. }
  328. /**
  329. * @param $urls: list of absolute urls to be deleted
  330. * @param null $base; base url (by default, take \Uri::base(false)
  331. */
  332. public static function deleteUrls($urls, $base = null)
  333. {
  334. if (!is_array($urls)) {
  335. $urls = array($urls);
  336. }
  337. if ($base === null) {
  338. $base = \Uri::base(false);
  339. }
  340. foreach ($urls as $url) {
  341. $cache_path = \Nos\FrontCache::getPathFromUrl($base, parse_url($url, PHP_URL_PATH));
  342. \Nos\FrontCache::forge($cache_path)->delete();
  343. }
  344. }
  345. /**
  346. * @param $enhancers: list of enhancer in which to delete urls
  347. * @param $relative_urls: list of relative urls to be deleted
  348. * @param null $context: context where to find
  349. */
  350. public static function deleteEnhancersUrls($enhancers, $relative_urls, $context = null)
  351. {
  352. if (!is_array($enhancers)) {
  353. $enhancers = array($enhancers);
  354. }
  355. if (!is_array($relative_urls)) {
  356. $relative_urls = array($relative_urls);
  357. }
  358. $enhancedUrls = array();
  359. foreach ($enhancers as $enhancer) {
  360. foreach ($relative_urls as $url) {
  361. $enhancedUrls = array_merge($enhancedUrls, static::getAllEnhancedUrls($enhancer, $url, $context));
  362. }
  363. }
  364. static::deleteUrls($enhancedUrls);
  365. }
  366. public static function deleteDir($path)
  367. {
  368. try {
  369. \File::delete_dir(\Config::get('cache_dir').$path, true, true);
  370. } catch (\Exception $e) {
  371. }
  372. }
  373. /**
  374. * Inspired from the Tool_Enhancer::_url method.
  375. * @todo: _url needs to be refactored and then this function can be moved to Tool_Enhancer...
  376. *
  377. * @param $enhancer_name: name of the enhancer inside which want to search the url
  378. * @param $relative_enhanced_url: relative url after the page name
  379. * @param null $context: context (null = all contexts)
  380. * @return array: list of urls
  381. */
  382. protected static function getAllEnhancedUrls($enhancer_name, $relative_enhanced_url, $context = null)
  383. {
  384. // Check if any page contains this enhancer
  385. $page_enhanced = Config_Data::get('page_enhanced.'.$enhancer_name, array());
  386. if (empty($page_enhanced)) {
  387. return array();
  388. }
  389. $urls = array();
  390. // This files contains all the urlPath for the pages containing an URL enhancer
  391. $url_enhanced = Config_Data::get('url_enhanced', array());
  392. foreach ($page_enhanced as $page_id => $page_params) {
  393. if (is_array($page_params['published'])) {
  394. $now = \Date::forge()->format('mysql');
  395. $published = (empty($page_params['published']['start']) ||
  396. $page_params['published']['start'] < $now) &&
  397. (empty($page_params['published']['end']) ||
  398. $now < $page_params['published']['end']);
  399. } else {
  400. $published = $page_params['published'] == true;
  401. }
  402. if ((!$context || $page_params['context'] == $context) && $published) {
  403. $url_params = \Arr::get($url_enhanced, $page_id, false);
  404. if ($url_params) {
  405. if (empty($relative_enhanced_url) && !empty($url_params['url'])) {
  406. $url_params['url'] = substr($url_params['url'], 0, -1).'.html';
  407. }
  408. $urls[$page_id] =
  409. Tools_Url::context($url_params['context']).
  410. $url_params['url'].$relative_enhanced_url;
  411. }
  412. }
  413. }
  414. return $urls;
  415. }
  416. public function get_path()
  417. {
  418. return $this->_path;
  419. }
  420. public static function getPathFromUrl($base, $url)
  421. {
  422. $url = (empty($url) ? 'index/' : $url);
  423. return 'pages'.DS.str_replace(array('http://', 'https://'), array('', ''), rtrim($base, '/')).DS.trim($url, '/');
  424. }
  425. }