PageRenderTime 46ms CodeModel.GetById 15ms RepoModel.GetById 1ms app.codeStats 0ms

/modules/cms/classes/CodeParser.php

https://gitlab.com/gideonmarked/yovelife
PHP | 242 lines | 132 code | 42 blank | 68 comment | 17 complexity | 9af76ecca92f13d1039de69042ef325a MD5 | raw file
  1. <?php namespace Cms\Classes;
  2. use File;
  3. use Lang;
  4. use Cache;
  5. use Config;
  6. use SystemException;
  7. /**
  8. * Parses the PHP code section of CMS objects.
  9. *
  10. * @package october\cms
  11. * @author Alexey Bobkov, Samuel Georges
  12. */
  13. class CodeParser
  14. {
  15. /**
  16. * @var \Cms\Classes\CmsCompoundObject A reference to the CMS object being parsed.
  17. */
  18. protected $object;
  19. /**
  20. * @var string Contains a path to the CMS object's file being parsed.
  21. */
  22. protected $filePath;
  23. /**
  24. * @var mixed The internal cache, keeps parsed object information during a request.
  25. */
  26. static protected $cache = [];
  27. /**
  28. * @var string Key for the parsed PHP file information cache.
  29. */
  30. protected $dataCacheKey = 'cms-php-file-data';
  31. /**
  32. * Creates the class instance
  33. * @param \Cms\Classes\CmsCompoundObject A reference to a CMS object to parse.
  34. */
  35. public function __construct(CmsCompoundObject $object)
  36. {
  37. $this->object = $object;
  38. $this->filePath = $object->getFilePath();
  39. }
  40. /**
  41. * Parses the CMS object's PHP code section and returns an array with the following keys:
  42. * - className
  43. * - filePath (path to the parsed PHP file)
  44. * - offset (PHP section offset in the template file)
  45. * - source ('parser', 'request-cache', or 'cache')
  46. * @return array
  47. */
  48. public function parse()
  49. {
  50. /*
  51. * If the object has already been parsed in this request return the cached data.
  52. */
  53. if (array_key_exists($this->filePath, self::$cache)) {
  54. self::$cache[$this->filePath]['source'] = 'request-cache';
  55. return self::$cache[$this->filePath];
  56. }
  57. /*
  58. * Try to load the parsed data from the file cache
  59. */
  60. $path = $this->getFilePath();
  61. $result = [
  62. 'filePath' => $path,
  63. 'offset' => 0
  64. ];
  65. if (File::isFile($path)) {
  66. $cachedInfo = $this->getCachedFileInfo();
  67. if ($cachedInfo !== null && $cachedInfo['mtime'] == $this->object->mtime) {
  68. $result['className'] = $cachedInfo['className'];
  69. $result['source'] = 'cache';
  70. return self::$cache[$this->filePath] = $result;
  71. }
  72. }
  73. /*
  74. * If the file was not found, or the cache is stale, prepare the new file and cache information about it
  75. */
  76. $uniqueName = uniqid().'_'.abs(crc32(md5(mt_rand())));
  77. $className = 'Cms'.$uniqueName.'Class';
  78. $body = $this->object->code;
  79. $body = preg_replace('/^\s*function/m', 'public function', $body);
  80. $codeNamespaces = [];
  81. $pattern = '/(use\s+[a-z0-9_\\\\]+(\s+as\s+[a-z0-9_]+)?;\n?)/mi';
  82. preg_match_all($pattern, $body, $namespaces);
  83. $body = preg_replace($pattern, '', $body);
  84. $parentClass = $this->object->getCodeClassParent();
  85. if ($parentClass !== null) {
  86. $parentClass = ' extends '.$parentClass;
  87. }
  88. $fileContents = '<?php '.PHP_EOL;
  89. foreach ($namespaces[0] as $namespace) {
  90. $fileContents .= $namespace;
  91. }
  92. $fileContents .= 'class '.$className.$parentClass.PHP_EOL;
  93. $fileContents .= '{'.PHP_EOL;
  94. $fileContents .= $body.PHP_EOL;
  95. $fileContents .= '}'.PHP_EOL;
  96. $this->validate($fileContents);
  97. $dir = dirname($path);
  98. if (!File::isDirectory($dir) && !@File::makeDirectory($dir, 0777, true)) {
  99. throw new SystemException(Lang::get('system::lang.directory.create_fail', ['name'=>$dir]));
  100. }
  101. if (!@File::put($path, $fileContents)) {
  102. throw new SystemException(Lang::get('system::lang.file.create_fail', ['name'=>$dir]));
  103. }
  104. $cached = $this->getCachedInfo();
  105. if (!$cached) {
  106. $cached = [];
  107. }
  108. $result['className'] = $className;
  109. $result['source'] = 'parser';
  110. $cacheItem = $result;
  111. $cacheItem['mtime'] = $this->object->mtime;
  112. $cached[$this->filePath] = $cacheItem;
  113. Cache::put($this->dataCacheKey, base64_encode(serialize($cached)), 1440);
  114. return self::$cache[$this->filePath] = $result;
  115. }
  116. /**
  117. * Runs the object's PHP file and returns the corresponding object.
  118. * @param \Cms\Classes\Page $page Specifies the CMS page.
  119. * @param \Cms\Classes\Layout $layout Specifies the CMS layout.
  120. * @param \Cms\Classes\Controller $controller Specifies the CMS controller.
  121. * @return mixed
  122. */
  123. public function source($page, $layout, $controller)
  124. {
  125. $data = $this->parse();
  126. $className = $data['className'];
  127. if (!class_exists($className)) {
  128. require_once $data['filePath'];
  129. }
  130. if (!class_exists($className) && ($data = $this->handleCorruptCache())) {
  131. $className = $data['className'];
  132. }
  133. return new $className($page, $layout, $controller);
  134. }
  135. /**
  136. * In some rare cases the cache file will not contain the class
  137. * name we expect. When this happens, destroy the corrupt file,
  138. * flush the request cache, and repeat the cycle.
  139. * @return void
  140. */
  141. protected function handleCorruptCache()
  142. {
  143. $path = $this->getFilePath();
  144. if (File::isFile($path)) {
  145. File::delete($path);
  146. }
  147. unset(self::$cache[$this->filePath]);
  148. return $this->parse();
  149. }
  150. /**
  151. * Evaluates PHP content in order to detect syntax errors.
  152. * The method handles PHP errors and throws exceptions.
  153. */
  154. protected function validate($php)
  155. {
  156. eval('?>'.$php);
  157. }
  158. /**
  159. * Returns path to the cached parsed file
  160. * @return string
  161. */
  162. protected function getFilePath()
  163. {
  164. $hash = abs(crc32($this->filePath));
  165. $result = storage_path().'/cms/cache/';
  166. $result .= substr($hash, 0, 2).'/';
  167. $result .= substr($hash, 2, 2).'/';
  168. $result .= basename($this->filePath).'.php';
  169. return $result;
  170. }
  171. /**
  172. * Returns information about all cached files.
  173. * @return mixed Returns an array representing the cached data or NULL.
  174. */
  175. protected function getCachedInfo()
  176. {
  177. $cached = Cache::get($this->dataCacheKey, false);
  178. if (
  179. $cached !== false &&
  180. ($cached = @unserialize(@base64_decode($cached))) !== false
  181. ) {
  182. return $cached;
  183. }
  184. return null;
  185. }
  186. /**
  187. * Returns information about a cached file
  188. * @return integer
  189. */
  190. protected function getCachedFileInfo()
  191. {
  192. $cached = $this->getCachedInfo();
  193. if ($cached !== null) {
  194. if (array_key_exists($this->filePath, $cached)) {
  195. return $cached[$this->filePath];
  196. }
  197. }
  198. return null;
  199. }
  200. }