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

/lib/TemplateEngine.php

https://github.com/UCF/Kurogo-Mobile-Web
PHP | 388 lines | 251 code | 68 blank | 69 comment | 47 complexity | 7c88c6312d77b2f2e021c4e3c0844cda MD5 | raw file
Possible License(s): LGPL-3.0
  1. <?php
  2. /**
  3. * @package Core
  4. */
  5. /**
  6. */
  7. require_once realpath(LIB_DIR.'/smarty/Smarty.class.php');
  8. /**
  9. * @package Core
  10. */
  11. class TemplateEngine extends Smarty {
  12. static $accessKey = 0;
  13. public $extendsTrackerCurrentInclude = '';
  14. public $extendsTrackerSeenFiles = array();
  15. //
  16. // Extends file tracking
  17. //
  18. // The following functions track {extends} files used for device templates.
  19. // The purpose is to allow us to have extends relationships between files
  20. // with the same name in different directories, but to be able to know
  21. // which directories have already been used so we don't loop.
  22. //
  23. // Fortunately for us, Smarty handles each extends chain as a unit
  24. // keeping the template resource_name the name of the include file
  25. // throughout the entire process.
  26. //
  27. // So our basic technique is to track all the files used for a given resource_name
  28. // and to toss the array either when the resource_name file changes or when we
  29. // leave the template via the postfilter below.
  30. private function extendsTrackerReset($templateToMatch=null) {
  31. if (!$templateToMatch || $this->extendsTrackerUsingTemplate($templateToMatch)) {
  32. if (Kurogo::getOptionalSiteVar('TEMPLATE_DEBUG')) {
  33. error_log('**** RESETTING TRACKER'.($this->extendsTrackerCurrentInclude ? " (old include {$this->extendsTrackerCurrentInclude})" : ''));
  34. }
  35. $this->extendsTrackerCurrentInclude = '';
  36. $this->extendsTrackerSeenFiles = array();
  37. }
  38. }
  39. private function extendsTrackerCheckTemplate($template) {
  40. if ($template->resource_type == 'file' && !$this->extendsTrackerUsingTemplate($template)) {
  41. if (Kurogo::getOptionalSiteVar('TEMPLATE_DEBUG')) {
  42. error_log("**** RESETTING TRACKER (new include {$template->resource_name})");
  43. }
  44. $this->extendsTrackerCurrentInclude = $template->resource_name;
  45. $this->extendsTrackerSeenFiles = array();
  46. $this->extendsTrackerAddFile($template->resource_name);
  47. }
  48. }
  49. private function extendsTrackerUsingTemplate($template) {
  50. return
  51. ($template->resource_type == 'file') &&
  52. ($template->resource_name == $this->extendsTrackerCurrentInclude);
  53. }
  54. private function extendsTrackerSeenFile($file) {
  55. return isset($this->extendsTrackerSeenFiles[$file]);
  56. }
  57. private function extendsTrackerAddFile($file) {
  58. if (Kurogo::getOptionalSiteVar('TEMPLATE_DEBUG')) {
  59. error_log("**** ADDING TO TRACKER -- {$file}");
  60. }
  61. $this->extendsTrackerSeenFiles[$file] = true;
  62. }
  63. //
  64. // Finding include files
  65. //
  66. static private function getIncludeFile($name) {
  67. $subDir = dirname($name);
  68. $page = basename($name, '.tpl');
  69. $pagetype = Kurogo::deviceClassifier()->getPagetype();
  70. $platform = Kurogo::deviceClassifier()->getPlatform();
  71. if (strlen($subDir)) { $subDir .= '/'; }
  72. $checkDirs = array(
  73. 'THEME_DIR' => THEME_DIR,
  74. 'SITE_APP_DIR' => SITE_APP_DIR,
  75. 'APP_DIR' => APP_DIR,
  76. );
  77. $checkFiles = array(
  78. "$subDir$page-$pagetype-$platform.tpl", // platform-specific
  79. "$subDir$page-$pagetype.tpl", // pagetype-specific
  80. "$subDir$page.tpl" // default
  81. );
  82. foreach ($checkFiles as $file) {
  83. foreach ($checkDirs as $type => $dir) {
  84. $test = realpath_exists("$dir/$file");
  85. if ($test) {
  86. if (Kurogo::getOptionalSiteVar('TEMPLATE_DEBUG')) {
  87. error_log(__FUNCTION__."($pagetype-$platform) choosing '$type/$file' for '$name'");
  88. }
  89. return addslashes($test);
  90. }
  91. }
  92. }
  93. return $name;
  94. }
  95. //
  96. // Finding extends files
  97. //
  98. static private function getExtendsFile($name, $template) {
  99. $pagetype = Kurogo::deviceClassifier()->getPagetype();
  100. $platform = Kurogo::deviceClassifier()->getPlatform();
  101. $checkDirs = array(
  102. 'THEME_DIR' => THEME_DIR,
  103. 'SITE_APP_DIR' => SITE_APP_DIR,
  104. 'APP_DIR' => APP_DIR,
  105. );
  106. foreach ($checkDirs as $type => $dir) {
  107. $test = realpath_exists("$dir/$name");
  108. if ($test && !$template->smarty->extendsTrackerSeenFile($test)) {
  109. if (Kurogo::getOptionalSiteVar('TEMPLATE_DEBUG')) {
  110. error_log(__FUNCTION__."($pagetype-$platform) choosing '$type/$name' for '$name'");
  111. }
  112. $template->smarty->extendsTrackerAddFile($test);
  113. return addslashes($test);
  114. }
  115. }
  116. return false;
  117. }
  118. //
  119. // Prefilter to map include and extend directives to real files
  120. //
  121. private static function replaceVariables($string, $variables) {
  122. $search = array();
  123. $replace = array();
  124. // TODO: fix this so it doesn't match on single { or }
  125. if (preg_match_all(';{?\$([A-za-z]\w*)}?;', $string, $matches, PREG_PATTERN_ORDER)) {
  126. foreach ($matches[1] as $i => $variable) {
  127. if (isset($variables[$variable]) && is_string($variables[$variable])) {
  128. $search[] = $matches[0][$i];
  129. $replace[] = $variables[$variable];
  130. }
  131. }
  132. }
  133. return $search ? str_replace($search, $replace, $string) : $string;
  134. }
  135. public static function smartyPrefilterHandleIncludeAndExtends($source, $template) {
  136. $template->smarty->extendsTrackerCheckTemplate($template);
  137. $variables = $template->smarty->getTemplateVars();
  138. // findIncludes
  139. $search = array();
  140. $replace = array();
  141. if (preg_match_all(';file\s*=\s*"findInclude:([^"]+)";', $source, $matches, PREG_PATTERN_ORDER)) {
  142. foreach ($matches[1] as $i => $name) {
  143. $path = self::getIncludeFile(self::replaceVariables($name, $variables));
  144. if ($path) {
  145. $search[] = $matches[0][$i];
  146. $replace[] = 'file="file:'.$path.'"';
  147. if (Kurogo::getOptionalSiteVar('TEMPLATE_DEBUG')) {
  148. error_log(__FUNCTION__." replacing include $name with $path");
  149. }
  150. } else {
  151. trigger_error(__FUNCTION__." FAILED to find INCLUDE for $name", E_USER_ERROR);
  152. }
  153. }
  154. }
  155. if (preg_match_all(';file\s*=\s*"findExtends:([^"]+)";', $source, $matches, PREG_PATTERN_ORDER)) {
  156. foreach ($matches[1] as $i => $name) {
  157. $path = self::getExtendsFile(self::replaceVariables($name, $variables), $template);
  158. if ($path) {
  159. $search[] = $matches[0][$i];
  160. $replace[] = 'file="file:'.$path.'"';
  161. if (Kurogo::getOptionalSiteVar('TEMPLATE_DEBUG')) {
  162. error_log(__FUNCTION__." replacing extends $name with $path");
  163. }
  164. } else {
  165. trigger_error(__FUNCTION__." FAILED to find EXTENDS for $name", E_USER_ERROR);
  166. }
  167. }
  168. }
  169. return $search ? str_replace($search, $replace, $source) : $source;
  170. }
  171. //
  172. // Postfilter to detect when we are leaving an extends dependency chain
  173. //
  174. public static function smartyPostfilterHandleIncludeAndExtends($source, $template) {
  175. $template->smarty->extendsTrackerReset($template);
  176. return $source;
  177. }
  178. private static function stripWhitespaceReplace($search, $replace, &$subject) {
  179. $len = strlen($search);
  180. $pos = 0;
  181. for ($i = 0, $count = count($replace); $i < $count; $i++) {
  182. if (($pos = strpos($subject, $search, $pos)) !== false) {
  183. $subject = substr_replace($subject, $replace[$i], $pos, $len);
  184. } else {
  185. break;
  186. }
  187. }
  188. }
  189. public static function smartyOutputfilterAddURLPrefixAndStripWhitespace($source, $smarty) {
  190. // rewrite urls for the device classifier in case our root is not /
  191. // also handles debugging mode for paths without hostnames
  192. $source = preg_replace(
  193. ';(url\("?\'?|href\s*=\s*"|src\s*=\s*"|action\s*=\s*")('.URL_PREFIX.'|'.URL_DEVICE_DEBUG_PREFIX.'|/);', '\1'.URL_PREFIX, $source);
  194. if (Kurogo::getSiteVar('DEVICE_DEBUG')) {
  195. // if we are in debugging mode we need to also rewrite full paths with hostnames
  196. $source = preg_replace(
  197. ';(url\("?\'?|href\s*=\s*"|src\s*=\s*")('.FULL_URL_PREFIX.'|'.FULL_URL_BASE.');', '\1'.FULL_URL_PREFIX, $source);
  198. }
  199. // Most of the following code comes from the stripwhitespace filter:
  200. // Pull out the style blocks
  201. preg_match_all("!<style[^>]*?>.*?</style>!is", $source, $match);
  202. $styleBlocks = $match[0];
  203. $source = preg_replace("!<style[^>]*?>.*?</style>!is", '@@@SMARTY:TRIM:STYLE@@@', $source);
  204. // Pull out the script blocks
  205. preg_match_all("!<script[^>]*?>.*?</script>!is", $source, $match);
  206. $scriptBlocks = $match[0];
  207. $source = preg_replace("!<script[^>]*?>.*?</script>!is", '@@@SMARTY:TRIM:SCRIPT@@@', $source);
  208. // Pull out the pre blocks
  209. preg_match_all("!<pre[^>]*?>.*?</pre>!is", $source, $match);
  210. $preBlocks = $match[0];
  211. $source = preg_replace("!<pre[^>]*?>.*?</pre>!is", '@@@SMARTY:TRIM:PRE@@@', $source);
  212. // Pull out the textarea blocks
  213. preg_match_all("!<textarea[^>]*?>.*?</textarea>!is", $source, $match);
  214. $textareaBlocks = $match[0];
  215. $source = preg_replace("!<textarea[^>]*?>.*?</textarea>!is", '@@@SMARTY:TRIM:TEXTAREA@@@', $source);
  216. // remove all leading spaces, tabs and carriage returns NOT
  217. // preceeded by a php close tag.
  218. $source = trim(preg_replace('/((?<!\?>)\n)[\s]+/m', '\1', $source));
  219. // remove all newlines before and after tags.
  220. $source = preg_replace('/\n*(<[^>]+>)\n*/m', '\1', $source);
  221. // strip spaces around non-breaking spaces
  222. $source = preg_replace('/\s*&nbsp;\s*/m', '&nbsp;', $source);
  223. // replace runs of spaces with a single space.
  224. $source = preg_replace('/\s+/m', ' ', $source);
  225. // restore textarea, pre, script and style blocks
  226. self::stripWhitespaceReplace("@@@SMARTY:TRIM:TEXTAREA@@@", $textareaBlocks, $source);
  227. self::stripWhitespaceReplace("@@@SMARTY:TRIM:PRE@@@", $preBlocks, $source);
  228. self::stripWhitespaceReplace("@@@SMARTY:TRIM:SCRIPT@@@", $scriptBlocks, $source);
  229. self::stripWhitespaceReplace("@@@SMARTY:TRIM:STYLE@@@", $styleBlocks, $source);
  230. return $source;
  231. }
  232. //
  233. // Access key block and template plugins
  234. //
  235. public static function smartyBlockAccessKeyLink($params, $content, &$smarty, &$repeat) {
  236. if (empty($params['href'])) {
  237. trigger_error("assign: missing 'href' parameter");
  238. }
  239. $html = '';
  240. if (!$repeat) {
  241. $html = '<a href="'.$params['href'].'"';
  242. if (isset($params['class'])) {
  243. $html .= " class=\"{$params['class']}\"";
  244. }
  245. if (isset($params['id'])) {
  246. $html .= " id=\"{$params['id']}\"";
  247. }
  248. if (self::$accessKey < 10) {
  249. $html .= ' accesskey="'.self::$accessKey.'">'.self::$accessKey.': ';
  250. self::$accessKey++;
  251. } else {
  252. $html .= '>';
  253. }
  254. $html .= $content.'</a>';
  255. }
  256. return $html;
  257. }
  258. public static function smartyTemplateAccessKeyReset($params, &$smarty) {
  259. if (!isset($params['index'])) {
  260. trigger_error("assign: missing 'index' parameter");
  261. return;
  262. }
  263. if (self::$accessKey == 0 || (isset($params['force']) && $params['force'])) {
  264. self::$accessKey = $params['index'];
  265. }
  266. }
  267. public function nosecure($string) {
  268. return str_replace('https://','http://',$string);
  269. }
  270. //
  271. // Constructor
  272. //
  273. function __construct() {
  274. parent::__construct();
  275. // Fix this in a later release -- currently generates lots of warnings
  276. $this->error_reporting = E_ALL & ~E_NOTICE;
  277. // Device info
  278. $pagetype = Kurogo::deviceClassifier()->getPagetype();
  279. $platform = Kurogo::deviceClassifier()->getPlatform();
  280. $supportsCerts = Kurogo::deviceClassifier()->getSupportsCerts();
  281. // Smarty configuration
  282. $this->setCompileDir (CACHE_DIR.'/smarty/templates');
  283. $this->setCacheDir (CACHE_DIR.'/smarty/html');
  284. $this->setCompileId ("$pagetype-$platform");
  285. $this->registerFilter('pre', array('TemplateEngine',
  286. 'smartyPrefilterHandleIncludeAndExtends'));
  287. $this->registerFilter('post', array('TemplateEngine',
  288. 'smartyPostfilterHandleIncludeAndExtends'));
  289. // Postfilter to add url prefix to absolute urls and
  290. // strip unnecessary whitespace (ignores <pre>, <script>, etc)
  291. $this->registerFilter('output', array('TemplateEngine',
  292. 'smartyOutputfilterAddURLPrefixAndStripWhitespace'));
  293. $this->registerPlugin('block', 'html_access_key_link',
  294. 'TemplateEngine::smartyBlockAccessKeyLink');
  295. $this->registerPlugin('function', 'html_access_key_reset',
  296. 'TemplateEngine::smartyTemplateAccessKeyReset');
  297. $this->registerPlugin('modifier', 'nosecure',
  298. array($this,'nosecure'));
  299. // variables common to all modules
  300. $this->assign('pagetype', $pagetype);
  301. $this->assign('platform', $platform);
  302. $this->assign('supportsCerts', $supportsCerts ? 1 : 0);
  303. $this->assign('showDeviceDetection', Kurogo::getSiteVar('DEVICE_DETECTION_DEBUG'));
  304. $this->assign('moduleDebug', Kurogo::getSiteVar('MODULE_DEBUG'));
  305. }
  306. //
  307. // Display template for device and theme
  308. //
  309. function displayForDevice($page, $cacheID = null, $compileID = null) {
  310. $this->extendsTrackerReset();
  311. $this->display(self::getIncludeFile($page), $cacheID, $compileID);
  312. }
  313. //
  314. // Fetch template contents for device and theme
  315. //
  316. function fetchForDevice($page, $cacheID = null, $compileID = null) {
  317. $this->extendsTrackerReset();
  318. return $this->fetch(self::getIncludeFile($page), $cacheID, $compileID);
  319. }
  320. }