Template.php 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149
  1. <?php
  2. // +----------------------------------------------------------------------
  3. // | ThinkPHP [ WE CAN DO IT JUST THINK ]
  4. // +----------------------------------------------------------------------
  5. // | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
  6. // +----------------------------------------------------------------------
  7. // | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
  8. // +----------------------------------------------------------------------
  9. // | Author: liu21st <liu21st@gmail.com>
  10. // +----------------------------------------------------------------------
  11. namespace think;
  12. use think\exception\TemplateNotFoundException;
  13. use think\template\TagLib;
  14. /**
  15. * ThinkPHP分离出来的模板引擎
  16. * 支持XML标签和普通标签的模板解析
  17. * 编译型模板引擎 支持动态缓存
  18. */
  19. class Template
  20. {
  21. // 模板变量
  22. protected $data = [];
  23. // 引擎配置
  24. protected $config = [
  25. 'view_path' => '', // 模板路径
  26. 'view_base' => '',
  27. 'view_suffix' => 'html', // 默认模板文件后缀
  28. 'view_depr' => DS,
  29. 'cache_suffix' => 'php', // 默认模板缓存后缀
  30. 'tpl_deny_func_list' => 'echo,exit', // 模板引擎禁用函数
  31. 'tpl_deny_php' => false, // 默认模板引擎是否禁用PHP原生代码
  32. 'tpl_begin' => '{', // 模板引擎普通标签开始标记
  33. 'tpl_end' => '}', // 模板引擎普通标签结束标记
  34. 'strip_space' => false, // 是否去除模板文件里面的html空格与换行
  35. 'tpl_cache' => true, // 是否开启模板编译缓存,设为false则每次都会重新编译
  36. 'compile_type' => 'file', // 模板编译类型
  37. 'cache_prefix' => '', // 模板缓存前缀标识,可以动态改变
  38. 'cache_time' => 0, // 模板缓存有效期 0 为永久,(以数字为值,单位:秒)
  39. 'layout_on' => false, // 布局模板开关
  40. 'layout_name' => 'layout', // 布局模板入口文件
  41. 'layout_item' => '{__CONTENT__}', // 布局模板的内容替换标识
  42. 'taglib_begin' => '{', // 标签库标签开始标记
  43. 'taglib_end' => '}', // 标签库标签结束标记
  44. 'taglib_load' => true, // 是否使用内置标签库之外的其它标签库,默认自动检测
  45. 'taglib_build_in' => 'cx', // 内置标签库名称(标签使用不必指定标签库名称),以逗号分隔 注意解析顺序
  46. 'taglib_pre_load' => '', // 需要额外加载的标签库(须指定标签库名称),多个以逗号分隔
  47. 'display_cache' => false, // 模板渲染缓存
  48. 'cache_id' => '', // 模板缓存ID
  49. 'tpl_replace_string' => [],
  50. 'tpl_var_identify' => 'array', // .语法变量识别,array|object|'', 为空时自动识别
  51. ];
  52. private $literal = [];
  53. private $includeFile = []; // 记录所有模板包含的文件路径及更新时间
  54. protected $storage;
  55. /**
  56. * 构造函数
  57. * @access public
  58. * @param array $config
  59. */
  60. public function __construct(array $config = [])
  61. {
  62. $this->config['cache_path'] = TEMP_PATH;
  63. $this->config = array_merge($this->config, $config);
  64. $this->config['taglib_begin'] = $this->stripPreg($this->config['taglib_begin']);
  65. $this->config['taglib_end'] = $this->stripPreg($this->config['taglib_end']);
  66. $this->config['tpl_begin'] = $this->stripPreg($this->config['tpl_begin']);
  67. $this->config['tpl_end'] = $this->stripPreg($this->config['tpl_end']);
  68. // 初始化模板编译存储器
  69. $type = $this->config['compile_type'] ? $this->config['compile_type'] : 'File';
  70. $class = false !== strpos($type, '\\') ? $type : '\\think\\template\\driver\\' . ucwords($type);
  71. $this->storage = new $class();
  72. }
  73. /**
  74. * 字符串替换 避免正则混淆
  75. * @access private
  76. * @param string $str
  77. * @return string
  78. */
  79. private function stripPreg($str)
  80. {
  81. return str_replace(
  82. ['{', '}', '(', ')', '|', '[', ']', '-', '+', '*', '.', '^', '?'],
  83. ['\{', '\}', '\(', '\)', '\|', '\[', '\]', '\-', '\+', '\*', '\.', '\^', '\?'],
  84. $str);
  85. }
  86. /**
  87. * 模板变量赋值
  88. * @access public
  89. * @param mixed $name
  90. * @param mixed $value
  91. * @return void
  92. */
  93. public function assign($name, $value = '')
  94. {
  95. if (is_array($name)) {
  96. $this->data = array_merge($this->data, $name);
  97. } else {
  98. $this->data[$name] = $value;
  99. }
  100. }
  101. /**
  102. * 模板引擎参数赋值
  103. * @access public
  104. * @param mixed $name
  105. * @param mixed $value
  106. */
  107. public function __set($name, $value)
  108. {
  109. $this->config[$name] = $value;
  110. }
  111. /**
  112. * 模板引擎配置项
  113. * @access public
  114. * @param array|string $config
  115. * @return string|void|array
  116. */
  117. public function config($config)
  118. {
  119. if (is_array($config)) {
  120. $this->config = array_merge($this->config, $config);
  121. } elseif (isset($this->config[$config])) {
  122. return $this->config[$config];
  123. } else {
  124. return;
  125. }
  126. }
  127. /**
  128. * 模板变量获取
  129. * @access public
  130. * @param string $name 变量名
  131. * @return mixed
  132. */
  133. public function get($name = '')
  134. {
  135. if ('' == $name) {
  136. return $this->data;
  137. } else {
  138. $data = $this->data;
  139. foreach (explode('.', $name) as $key => $val) {
  140. if (isset($data[$val])) {
  141. $data = $data[$val];
  142. } else {
  143. $data = null;
  144. break;
  145. }
  146. }
  147. return $data;
  148. }
  149. }
  150. /**
  151. * 渲染模板文件
  152. * @access public
  153. * @param string $template 模板文件
  154. * @param array $vars 模板变量
  155. * @param array $config 模板参数
  156. * @return void
  157. */
  158. public function fetch($template, $vars = [], $config = [])
  159. {
  160. if ($vars) {
  161. $this->data = $vars;
  162. }
  163. if ($config) {
  164. $this->config($config);
  165. }
  166. if (!empty($this->config['cache_id']) && $this->config['display_cache']) {
  167. // 读取渲染缓存
  168. $cacheContent = Cache::get($this->config['cache_id']);
  169. if (false !== $cacheContent) {
  170. echo $cacheContent;
  171. return;
  172. }
  173. }
  174. $template = $this->parseTemplateFile($template);
  175. if ($template) {
  176. $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($this->config['layout_name'] . $template) . '.' . ltrim($this->config['cache_suffix'], '.');
  177. if (!$this->checkCache($cacheFile)) {
  178. // 缓存无效 重新模板编译
  179. $content = file_get_contents($template);
  180. $this->compiler($content, $cacheFile);
  181. }
  182. // 页面缓存
  183. ob_start();
  184. ob_implicit_flush(0);
  185. // 读取编译存储
  186. $this->storage->read($cacheFile, $this->data);
  187. // 获取并清空缓存
  188. $content = ob_get_clean();
  189. if (!empty($this->config['cache_id']) && $this->config['display_cache']) {
  190. // 缓存页面输出
  191. Cache::set($this->config['cache_id'], $content, $this->config['cache_time']);
  192. }
  193. echo $content;
  194. }
  195. }
  196. /**
  197. * 渲染模板内容
  198. * @access public
  199. * @param string $content 模板内容
  200. * @param array $vars 模板变量
  201. * @param array $config 模板参数
  202. * @return void
  203. */
  204. public function display($content, $vars = [], $config = [])
  205. {
  206. if ($vars) {
  207. $this->data = $vars;
  208. }
  209. if ($config) {
  210. $this->config($config);
  211. }
  212. $cacheFile = $this->config['cache_path'] . $this->config['cache_prefix'] . md5($content) . '.' . ltrim($this->config['cache_suffix'], '.');
  213. if (!$this->checkCache($cacheFile)) {
  214. // 缓存无效 模板编译
  215. $this->compiler($content, $cacheFile);
  216. }
  217. // 读取编译存储
  218. $this->storage->read($cacheFile, $this->data);
  219. }
  220. /**
  221. * 设置布局
  222. * @access public
  223. * @param mixed $name 布局模板名称 false 则关闭布局
  224. * @param string $replace 布局模板内容替换标识
  225. * @return Template
  226. */
  227. public function layout($name, $replace = '')
  228. {
  229. if (false === $name) {
  230. // 关闭布局
  231. $this->config['layout_on'] = false;
  232. } else {
  233. // 开启布局
  234. $this->config['layout_on'] = true;
  235. // 名称必须为字符串
  236. if (is_string($name)) {
  237. $this->config['layout_name'] = $name;
  238. }
  239. if (!empty($replace)) {
  240. $this->config['layout_item'] = $replace;
  241. }
  242. }
  243. return $this;
  244. }
  245. /**
  246. * 检查编译缓存是否有效
  247. * 如果无效则需要重新编译
  248. * @access private
  249. * @param string $cacheFile 缓存文件名
  250. * @return boolean
  251. */
  252. private function checkCache($cacheFile)
  253. {
  254. // 未开启缓存功能
  255. if (!$this->config['tpl_cache']) {
  256. return false;
  257. }
  258. // 缓存文件不存在
  259. if (!is_file($cacheFile)) {
  260. return false;
  261. }
  262. // 读取缓存文件失败
  263. if (!$handle = @fopen($cacheFile, "r")) {
  264. return false;
  265. }
  266. // 读取第一行
  267. preg_match('/\/\*(.+?)\*\//', fgets($handle), $matches);
  268. if (!isset($matches[1])) {
  269. return false;
  270. }
  271. $includeFile = unserialize($matches[1]);
  272. if (!is_array($includeFile)) {
  273. return false;
  274. }
  275. // 检查模板文件是否有更新
  276. foreach ($includeFile as $path => $time) {
  277. if (is_file($path) && filemtime($path) > $time) {
  278. // 模板文件如果有更新则缓存需要更新
  279. return false;
  280. }
  281. }
  282. // 检查编译存储是否有效
  283. return $this->storage->check($cacheFile, $this->config['cache_time']);
  284. }
  285. /**
  286. * 检查编译缓存是否存在
  287. * @access public
  288. * @param string $cacheId 缓存的id
  289. * @return boolean
  290. */
  291. public function isCache($cacheId)
  292. {
  293. if ($cacheId && $this->config['display_cache']) {
  294. // 缓存页面输出
  295. return Cache::has($cacheId);
  296. }
  297. return false;
  298. }
  299. /**
  300. * 编译模板文件内容
  301. * @access private
  302. * @param string $content 模板内容
  303. * @param string $cacheFile 缓存文件名
  304. * @return void
  305. */
  306. private function compiler(&$content, $cacheFile)
  307. {
  308. // 判断是否启用布局
  309. if ($this->config['layout_on']) {
  310. if (false !== strpos($content, '{__NOLAYOUT__}')) {
  311. // 可以单独定义不使用布局
  312. $content = str_replace('{__NOLAYOUT__}', '', $content);
  313. } else {
  314. // 读取布局模板
  315. $layoutFile = $this->parseTemplateFile($this->config['layout_name']);
  316. if ($layoutFile) {
  317. // 替换布局的主体内容
  318. $content = str_replace($this->config['layout_item'], $content, file_get_contents($layoutFile));
  319. }
  320. }
  321. } else {
  322. $content = str_replace('{__NOLAYOUT__}', '', $content);
  323. }
  324. // 模板解析
  325. $this->parse($content);
  326. if ($this->config['strip_space']) {
  327. /* 去除html空格与换行 */
  328. $find = ['~>\s+<~', '~>(\s+\n|\r)~'];
  329. $replace = ['><', '>'];
  330. $content = preg_replace($find, $replace, $content);
  331. }
  332. // 优化生成的php代码
  333. $content = preg_replace('/\?>\s*<\?php\s(?!echo\b)/s', '', $content);
  334. // 模板过滤输出
  335. $replace = $this->config['tpl_replace_string'];
  336. $content = str_replace(array_keys($replace), array_values($replace), $content);
  337. // 添加安全代码及模板引用记录
  338. $content = '<?php if (!defined(\'THINK_PATH\')) exit(); /*' . serialize($this->includeFile) . '*/ ?>' . "\n" . $content;
  339. // 编译存储
  340. $this->storage->write($cacheFile, $content);
  341. $this->includeFile = [];
  342. return;
  343. }
  344. /**
  345. * 模板解析入口
  346. * 支持普通标签和TagLib解析 支持自定义标签库
  347. * @access public
  348. * @param string $content 要解析的模板内容
  349. * @return void
  350. */
  351. public function parse(&$content)
  352. {
  353. // 内容为空不解析
  354. if (empty($content)) {
  355. return;
  356. }
  357. // 替换literal标签内容
  358. $this->parseLiteral($content);
  359. // 解析继承
  360. $this->parseExtend($content);
  361. // 解析布局
  362. $this->parseLayout($content);
  363. // 检查include语法
  364. $this->parseInclude($content);
  365. // 替换包含文件中literal标签内容
  366. $this->parseLiteral($content);
  367. // 检查PHP语法
  368. $this->parsePhp($content);
  369. // 获取需要引入的标签库列表
  370. // 标签库只需要定义一次,允许引入多个一次
  371. // 一般放在文件的最前面
  372. // 格式:<taglib name="html,mytag..." />
  373. // 当TAGLIB_LOAD配置为true时才会进行检测
  374. if ($this->config['taglib_load']) {
  375. $tagLibs = $this->getIncludeTagLib($content);
  376. if (!empty($tagLibs)) {
  377. // 对导入的TagLib进行解析
  378. foreach ($tagLibs as $tagLibName) {
  379. $this->parseTagLib($tagLibName, $content);
  380. }
  381. }
  382. }
  383. // 预先加载的标签库 无需在每个模板中使用taglib标签加载 但必须使用标签库XML前缀
  384. if ($this->config['taglib_pre_load']) {
  385. $tagLibs = explode(',', $this->config['taglib_pre_load']);
  386. foreach ($tagLibs as $tag) {
  387. $this->parseTagLib($tag, $content);
  388. }
  389. }
  390. // 内置标签库 无需使用taglib标签导入就可以使用 并且不需使用标签库XML前缀
  391. $tagLibs = explode(',', $this->config['taglib_build_in']);
  392. foreach ($tagLibs as $tag) {
  393. $this->parseTagLib($tag, $content, true);
  394. }
  395. // 解析普通模板标签 {$tagName}
  396. $this->parseTag($content);
  397. // 还原被替换的Literal标签
  398. $this->parseLiteral($content, true);
  399. return;
  400. }
  401. /**
  402. * 检查PHP语法
  403. * @access private
  404. * @param string $content 要解析的模板内容
  405. * @return void
  406. * @throws \think\Exception
  407. */
  408. private function parsePhp(&$content)
  409. {
  410. // 短标签的情况要将<?标签用echo方式输出 否则无法正常输出xml标识
  411. $content = preg_replace('/(<\?(?!php|=|$))/i', '<?php echo \'\\1\'; ?>' . "\n", $content);
  412. // PHP语法检查
  413. if ($this->config['tpl_deny_php'] && false !== strpos($content, '<?php')) {
  414. throw new Exception('not allow php tag', 11600);
  415. }
  416. return;
  417. }
  418. /**
  419. * 解析模板中的布局标签
  420. * @access private
  421. * @param string $content 要解析的模板内容
  422. * @return void
  423. */
  424. private function parseLayout(&$content)
  425. {
  426. // 读取模板中的布局标签
  427. if (preg_match($this->getRegex('layout'), $content, $matches)) {
  428. // 替换Layout标签
  429. $content = str_replace($matches[0], '', $content);
  430. // 解析Layout标签
  431. $array = $this->parseAttr($matches[0]);
  432. if (!$this->config['layout_on'] || $this->config['layout_name'] != $array['name']) {
  433. // 读取布局模板
  434. $layoutFile = $this->parseTemplateFile($array['name']);
  435. if ($layoutFile) {
  436. $replace = isset($array['replace']) ? $array['replace'] : $this->config['layout_item'];
  437. // 替换布局的主体内容
  438. $content = str_replace($replace, $content, file_get_contents($layoutFile));
  439. }
  440. }
  441. } else {
  442. $content = str_replace('{__NOLAYOUT__}', '', $content);
  443. }
  444. return;
  445. }
  446. /**
  447. * 解析模板中的include标签
  448. * @access private
  449. * @param string $content 要解析的模板内容
  450. * @return void
  451. */
  452. private function parseInclude(&$content)
  453. {
  454. $regex = $this->getRegex('include');
  455. $func = function ($template) use (&$func, &$regex, &$content) {
  456. if (preg_match_all($regex, $template, $matches, PREG_SET_ORDER)) {
  457. foreach ($matches as $match) {
  458. $array = $this->parseAttr($match[0]);
  459. $file = $array['file'];
  460. unset($array['file']);
  461. // 分析模板文件名并读取内容
  462. $parseStr = $this->parseTemplateName($file);
  463. foreach ($array as $k => $v) {
  464. // 以$开头字符串转换成模板变量
  465. if (0 === strpos($v, '$')) {
  466. $v = $this->get(substr($v, 1));
  467. }
  468. $parseStr = str_replace('[' . $k . ']', $v, $parseStr);
  469. }
  470. $content = str_replace($match[0], $parseStr, $content);
  471. // 再次对包含文件进行模板分析
  472. $func($parseStr);
  473. }
  474. unset($matches);
  475. }
  476. };
  477. // 替换模板中的include标签
  478. $func($content);
  479. return;
  480. }
  481. /**
  482. * 解析模板中的extend标签
  483. * @access private
  484. * @param string $content 要解析的模板内容
  485. * @return void
  486. */
  487. private function parseExtend(&$content)
  488. {
  489. $regex = $this->getRegex('extend');
  490. $array = $blocks = $baseBlocks = [];
  491. $extend = '';
  492. $func = function ($template) use (&$func, &$regex, &$array, &$extend, &$blocks, &$baseBlocks) {
  493. if (preg_match($regex, $template, $matches)) {
  494. if (!isset($array[$matches['name']])) {
  495. $array[$matches['name']] = 1;
  496. // 读取继承模板
  497. $extend = $this->parseTemplateName($matches['name']);
  498. // 递归检查继承
  499. $func($extend);
  500. // 取得block标签内容
  501. $blocks = array_merge($blocks, $this->parseBlock($template));
  502. return;
  503. }
  504. } else {
  505. // 取得顶层模板block标签内容
  506. $baseBlocks = $this->parseBlock($template, true);
  507. if (empty($extend)) {
  508. // 无extend标签但有block标签的情况
  509. $extend = $template;
  510. }
  511. }
  512. };
  513. $func($content);
  514. if (!empty($extend)) {
  515. if ($baseBlocks) {
  516. $children = [];
  517. foreach ($baseBlocks as $name => $val) {
  518. $replace = $val['content'];
  519. if (!empty($children[$name])) {
  520. // 如果包含有子block标签
  521. foreach ($children[$name] as $key) {
  522. $replace = str_replace($baseBlocks[$key]['begin'] . $baseBlocks[$key]['content'] . $baseBlocks[$key]['end'], $blocks[$key]['content'], $replace);
  523. }
  524. }
  525. if (isset($blocks[$name])) {
  526. // 带有{__block__}表示与所继承模板的相应标签合并,而不是覆盖
  527. $replace = str_replace(['{__BLOCK__}', '{__block__}'], $replace, $blocks[$name]['content']);
  528. if (!empty($val['parent'])) {
  529. // 如果不是最顶层的block标签
  530. $parent = $val['parent'];
  531. if (isset($blocks[$parent])) {
  532. $blocks[$parent]['content'] = str_replace($blocks[$name]['begin'] . $blocks[$name]['content'] . $blocks[$name]['end'], $replace, $blocks[$parent]['content']);
  533. }
  534. $blocks[$name]['content'] = $replace;
  535. $children[$parent][] = $name;
  536. continue;
  537. }
  538. } elseif (!empty($val['parent'])) {
  539. // 如果子标签没有被继承则用原值
  540. $children[$val['parent']][] = $name;
  541. $blocks[$name] = $val;
  542. }
  543. if (!$val['parent']) {
  544. // 替换模板中的顶级block标签
  545. $extend = str_replace($val['begin'] . $val['content'] . $val['end'], $replace, $extend);
  546. }
  547. }
  548. }
  549. $content = $extend;
  550. unset($blocks, $baseBlocks);
  551. }
  552. return;
  553. }
  554. /**
  555. * 替换页面中的literal标签
  556. * @access private
  557. * @param string $content 模板内容
  558. * @param boolean $restore 是否为还原
  559. * @return void
  560. */
  561. private function parseLiteral(&$content, $restore = false)
  562. {
  563. $regex = $this->getRegex($restore ? 'restoreliteral' : 'literal');
  564. if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {
  565. if (!$restore) {
  566. $count = count($this->literal);
  567. // 替换literal标签
  568. foreach ($matches as $match) {
  569. $this->literal[] = substr($match[0], strlen($match[1]), -strlen($match[2]));
  570. $content = str_replace($match[0], "<!--###literal{$count}###-->", $content);
  571. $count++;
  572. }
  573. } else {
  574. // 还原literal标签
  575. foreach ($matches as $match) {
  576. $content = str_replace($match[0], $this->literal[$match[1]], $content);
  577. }
  578. // 清空literal记录
  579. $this->literal = [];
  580. }
  581. unset($matches);
  582. }
  583. return;
  584. }
  585. /**
  586. * 获取模板中的block标签
  587. * @access private
  588. * @param string $content 模板内容
  589. * @param boolean $sort 是否排序
  590. * @return array
  591. */
  592. private function parseBlock(&$content, $sort = false)
  593. {
  594. $regex = $this->getRegex('block');
  595. $result = [];
  596. if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE)) {
  597. $right = $keys = [];
  598. foreach ($matches as $match) {
  599. if (empty($match['name'][0])) {
  600. if (count($right) > 0) {
  601. $tag = array_pop($right);
  602. $start = $tag['offset'] + strlen($tag['tag']);
  603. $length = $match[0][1] - $start;
  604. $result[$tag['name']] = [
  605. 'begin' => $tag['tag'],
  606. 'content' => substr($content, $start, $length),
  607. 'end' => $match[0][0],
  608. 'parent' => count($right) ? end($right)['name'] : '',
  609. ];
  610. $keys[$tag['name']] = $match[0][1];
  611. }
  612. } else {
  613. // 标签头压入栈
  614. $right[] = [
  615. 'name' => $match[2][0],
  616. 'offset' => $match[0][1],
  617. 'tag' => $match[0][0],
  618. ];
  619. }
  620. }
  621. unset($right, $matches);
  622. if ($sort) {
  623. // 按block标签结束符在模板中的位置排序
  624. array_multisort($keys, $result);
  625. }
  626. }
  627. return $result;
  628. }
  629. /**
  630. * 搜索模板页面中包含的TagLib库
  631. * 并返回列表
  632. * @access private
  633. * @param string $content 模板内容
  634. * @return array|null
  635. */
  636. private function getIncludeTagLib(&$content)
  637. {
  638. // 搜索是否有TagLib标签
  639. if (preg_match($this->getRegex('taglib'), $content, $matches)) {
  640. // 替换TagLib标签
  641. $content = str_replace($matches[0], '', $content);
  642. return explode(',', $matches['name']);
  643. }
  644. return;
  645. }
  646. /**
  647. * TagLib库解析
  648. * @access public
  649. * @param string $tagLib 要解析的标签库
  650. * @param string $content 要解析的模板内容
  651. * @param boolean $hide 是否隐藏标签库前缀
  652. * @return void
  653. */
  654. public function parseTagLib($tagLib, &$content, $hide = false)
  655. {
  656. if (false !== strpos($tagLib, '\\')) {
  657. // 支持指定标签库的命名空间
  658. $className = $tagLib;
  659. $tagLib = substr($tagLib, strrpos($tagLib, '\\') + 1);
  660. } else {
  661. $className = '\\think\\template\\taglib\\' . ucwords($tagLib);
  662. }
  663. /** @var Taglib $tLib */
  664. $tLib = new $className($this);
  665. $tLib->parseTag($content, $hide ? '' : $tagLib);
  666. return;
  667. }
  668. /**
  669. * 分析标签属性
  670. * @access public
  671. * @param string $str 属性字符串
  672. * @param string $name 不为空时返回指定的属性名
  673. * @return array
  674. */
  675. public function parseAttr($str, $name = null)
  676. {
  677. $regex = '/\s+(?>(?P<name>[\w-]+)\s*)=(?>\s*)([\"\'])(?P<value>(?:(?!\\2).)*)\\2/is';
  678. $array = [];
  679. if (preg_match_all($regex, $str, $matches, PREG_SET_ORDER)) {
  680. foreach ($matches as $match) {
  681. $array[$match['name']] = $match['value'];
  682. }
  683. unset($matches);
  684. }
  685. if (!empty($name) && isset($array[$name])) {
  686. return $array[$name];
  687. } else {
  688. return $array;
  689. }
  690. }
  691. /**
  692. * 模板标签解析
  693. * 格式: {TagName:args [|content] }
  694. * @access private
  695. * @param string $content 要解析的模板内容
  696. * @return void
  697. */
  698. private function parseTag(&$content)
  699. {
  700. $regex = $this->getRegex('tag');
  701. if (preg_match_all($regex, $content, $matches, PREG_SET_ORDER)) {
  702. foreach ($matches as $match) {
  703. $str = stripslashes($match[1]);
  704. $flag = substr($str, 0, 1);
  705. switch ($flag) {
  706. case '$':
  707. // 解析模板变量 格式 {$varName}
  708. // 是否带有?号
  709. if (false !== $pos = strpos($str, '?')) {
  710. $array = preg_split('/([!=]={1,2}|(?<!-)[><]={0,1})/', substr($str, 0, $pos), 2, PREG_SPLIT_DELIM_CAPTURE);
  711. $name = $array[0];
  712. $this->parseVar($name);
  713. $this->parseVarFunction($name);
  714. $str = trim(substr($str, $pos + 1));
  715. $this->parseVar($str);
  716. $first = substr($str, 0, 1);
  717. if (strpos($name, ')')) {
  718. // $name为对象或是自动识别,或者含有函数
  719. if (isset($array[1])) {
  720. $this->parseVar($array[2]);
  721. $name .= $array[1] . $array[2];
  722. }
  723. switch ($first) {
  724. case '?':
  725. $str = '<?php echo (' . $name . ') ? ' . $name . ' : ' . substr($str, 1) . '; ?>';
  726. break;
  727. case '=':
  728. $str = '<?php if(' . $name . ') echo ' . substr($str, 1) . '; ?>';
  729. break;
  730. default:
  731. $str = '<?php echo ' . $name . '?' . $str . '; ?>';
  732. }
  733. } else {
  734. if (isset($array[1])) {
  735. $this->parseVar($array[2]);
  736. $express = $name . $array[1] . $array[2];
  737. } else {
  738. $express = false;
  739. }
  740. // $name为数组
  741. switch ($first) {
  742. case '?':
  743. // {$varname??'xxx'} $varname有定义则输出$varname,否则输出xxx
  744. $str = '<?php echo ' . ($express ?: 'isset(' . $name . ')') . '?' . $name . ':' . substr($str, 1) . '; ?>';
  745. break;
  746. case '=':
  747. // {$varname?='xxx'} $varname为真时才输出xxx
  748. $str = '<?php if(' . ($express ?: '!empty(' . $name . ')') . ') echo ' . substr($str, 1) . '; ?>';
  749. break;
  750. case ':':
  751. // {$varname?:'xxx'} $varname为真时输出$varname,否则输出xxx
  752. $str = '<?php echo ' . ($express ?: '!empty(' . $name . ')') . '?' . $name . $str . '; ?>';
  753. break;
  754. default:
  755. $str = '<?php echo ' . ($express ?: '!empty(' . $name . ')') . '?' . $str . '; ?>';
  756. }
  757. }
  758. } else {
  759. $this->parseVar($str);
  760. $this->parseVarFunction($str);
  761. $str = '<?php echo ' . $str . '; ?>';
  762. }
  763. break;
  764. case ':':
  765. // 输出某个函数的结果
  766. $str = substr($str, 1);
  767. $this->parseVar($str);
  768. $str = '<?php echo ' . $str . '; ?>';
  769. break;
  770. case '~':
  771. // 执行某个函数
  772. $str = substr($str, 1);
  773. $this->parseVar($str);
  774. $str = '<?php ' . $str . '; ?>';
  775. break;
  776. case '-':
  777. case '+':
  778. // 输出计算
  779. $this->parseVar($str);
  780. $str = '<?php echo ' . $str . '; ?>';
  781. break;
  782. case '/':
  783. // 注释标签
  784. $flag2 = substr($str, 1, 1);
  785. if ('/' == $flag2 || ('*' == $flag2 && substr(rtrim($str), -2) == '*/')) {
  786. $str = '';
  787. }
  788. break;
  789. default:
  790. // 未识别的标签直接返回
  791. $str = $this->config['tpl_begin'] . $str . $this->config['tpl_end'];
  792. break;
  793. }
  794. $content = str_replace($match[0], $str, $content);
  795. }
  796. unset($matches);
  797. }
  798. return;
  799. }
  800. /**
  801. * 模板变量解析,支持使用函数
  802. * 格式: {$varname|function1|function2=arg1,arg2}
  803. * @access public
  804. * @param string $varStr 变量数据
  805. * @return void
  806. */
  807. public function parseVar(&$varStr)
  808. {
  809. $varStr = trim($varStr);
  810. if (preg_match_all('/\$[a-zA-Z_](?>\w*)(?:[:\.][0-9a-zA-Z_](?>\w*))+/', $varStr, $matches, PREG_OFFSET_CAPTURE)) {
  811. static $_varParseList = [];
  812. while ($matches[0]) {
  813. $match = array_pop($matches[0]);
  814. //如果已经解析过该变量字串,则直接返回变量值
  815. if (isset($_varParseList[$match[0]])) {
  816. $parseStr = $_varParseList[$match[0]];
  817. } else {
  818. if (strpos($match[0], '.')) {
  819. $vars = explode('.', $match[0]);
  820. $first = array_shift($vars);
  821. if ('$Think' == $first) {
  822. // 所有以Think.打头的以特殊变量对待 无需模板赋值就可以输出
  823. $parseStr = $this->parseThinkVar($vars);
  824. } elseif ('$Request' == $first) {
  825. // 获取Request请求对象参数
  826. $method = array_shift($vars);
  827. if (!empty($vars)) {
  828. $params = implode('.', $vars);
  829. if ('true' != $params) {
  830. $params = '\'' . $params . '\'';
  831. }
  832. } else {
  833. $params = '';
  834. }
  835. $parseStr = '\think\Request::instance()->' . $method . '(' . $params . ')';
  836. } else {
  837. switch ($this->config['tpl_var_identify']) {
  838. case 'array': // 识别为数组
  839. $parseStr = $first . '[\'' . implode('\'][\'', $vars) . '\']';
  840. break;
  841. case 'obj': // 识别为对象
  842. $parseStr = $first . '->' . implode('->', $vars);
  843. break;
  844. default: // 自动判断数组或对象
  845. $parseStr = '(is_array(' . $first . ')?' . $first . '[\'' . implode('\'][\'', $vars) . '\']:' . $first . '->' . implode('->', $vars) . ')';
  846. }
  847. }
  848. } else {
  849. $parseStr = str_replace(':', '->', $match[0]);
  850. }
  851. $_varParseList[$match[0]] = $parseStr;
  852. }
  853. $varStr = substr_replace($varStr, $parseStr, $match[1], strlen($match[0]));
  854. }
  855. unset($matches);
  856. }
  857. return;
  858. }
  859. /**
  860. * 对模板中使用了函数的变量进行解析
  861. * 格式 {$varname|function1|function2=arg1,arg2}
  862. * @access public
  863. * @param string $varStr 变量字符串
  864. * @return void
  865. */
  866. public function parseVarFunction(&$varStr)
  867. {
  868. if (false == strpos($varStr, '|')) {
  869. return;
  870. }
  871. static $_varFunctionList = [];
  872. $_key = md5($varStr);
  873. //如果已经解析过该变量字串,则直接返回变量值
  874. if (isset($_varFunctionList[$_key])) {
  875. $varStr = $_varFunctionList[$_key];
  876. } else {
  877. $varArray = explode('|', $varStr);
  878. // 取得变量名称
  879. $name = array_shift($varArray);
  880. // 对变量使用函数
  881. $length = count($varArray);
  882. // 取得模板禁止使用函数列表
  883. $template_deny_funs = explode(',', $this->config['tpl_deny_func_list']);
  884. for ($i = 0; $i < $length; $i++) {
  885. $args = explode('=', $varArray[$i], 2);
  886. // 模板函数过滤
  887. $fun = trim($args[0]);
  888. switch ($fun) {
  889. case 'default': // 特殊模板函数
  890. if (false === strpos($name, '(')) {
  891. $name = '(isset(' . $name . ') && (' . $name . ' !== \'\')?' . $name . ':' . $args[1] . ')';
  892. } else {
  893. $name = '(' . $name . ' ?: ' . $args[1] . ')';
  894. }
  895. break;
  896. default: // 通用模板函数
  897. if (!in_array($fun, $template_deny_funs)) {
  898. if (isset($args[1])) {
  899. if (strstr($args[1], '###')) {
  900. $args[1] = str_replace('###', $name, $args[1]);
  901. $name = "$fun($args[1])";
  902. } else {
  903. $name = "$fun($name,$args[1])";
  904. }
  905. } else {
  906. if (!empty($args[0])) {
  907. $name = "$fun($name)";
  908. }
  909. }
  910. }
  911. }
  912. }
  913. $_varFunctionList[$_key] = $name;
  914. $varStr = $name;
  915. }
  916. return;
  917. }
  918. /**
  919. * 特殊模板变量解析
  920. * 格式 以 $Think. 打头的变量属于特殊模板变量
  921. * @access public
  922. * @param array $vars 变量数组
  923. * @return string
  924. */
  925. public function parseThinkVar($vars)
  926. {
  927. $type = strtoupper(trim(array_shift($vars)));
  928. $param = implode('.', $vars);
  929. if ($vars) {
  930. switch ($type) {
  931. case 'SERVER':
  932. $parseStr = '\\think\\Request::instance()->server(\'' . $param . '\')';
  933. break;
  934. case 'GET':
  935. $parseStr = '\\think\\Request::instance()->get(\'' . $param . '\')';
  936. break;
  937. case 'POST':
  938. $parseStr = '\\think\\Request::instance()->post(\'' . $param . '\')';
  939. break;
  940. case 'COOKIE':
  941. $parseStr = '\\think\\Cookie::get(\'' . $param . '\')';
  942. break;
  943. case 'SESSION':
  944. $parseStr = '\\think\\Session::get(\'' . $param . '\')';
  945. break;
  946. case 'ENV':
  947. $parseStr = '\\think\\Request::instance()->env(\'' . $param . '\')';
  948. break;
  949. case 'REQUEST':
  950. $parseStr = '\\think\\Request::instance()->request(\'' . $param . '\')';
  951. break;
  952. case 'CONST':
  953. $parseStr = strtoupper($param);
  954. break;
  955. case 'LANG':
  956. $parseStr = '\\think\\Lang::get(\'' . $param . '\')';
  957. break;
  958. case 'CONFIG':
  959. $parseStr = '\\think\\Config::get(\'' . $param . '\')';
  960. break;
  961. default:
  962. $parseStr = '\'\'';
  963. break;
  964. }
  965. } else {
  966. switch ($type) {
  967. case 'NOW':
  968. $parseStr = "date('Y-m-d g:i a',time())";
  969. break;
  970. case 'VERSION':
  971. $parseStr = 'THINK_VERSION';
  972. break;
  973. case 'LDELIM':
  974. $parseStr = '\'' . ltrim($this->config['tpl_begin'], '\\') . '\'';
  975. break;
  976. case 'RDELIM':
  977. $parseStr = '\'' . ltrim($this->config['tpl_end'], '\\') . '\'';
  978. break;
  979. default:
  980. if (defined($type)) {
  981. $parseStr = $type;
  982. } else {
  983. $parseStr = '';
  984. }
  985. }
  986. }
  987. return $parseStr;
  988. }
  989. /**
  990. * 分析加载的模板文件并读取内容 支持多个模板文件读取
  991. * @access private
  992. * @param string $templateName 模板文件名
  993. * @return string
  994. */
  995. private function parseTemplateName($templateName)
  996. {
  997. $array = explode(',', $templateName);
  998. $parseStr = '';
  999. foreach ($array as $templateName) {
  1000. if (empty($templateName)) {
  1001. continue;
  1002. }
  1003. if (0 === strpos($templateName, '$')) {
  1004. //支持加载变量文件名
  1005. $templateName = $this->get(substr($templateName, 1));
  1006. }
  1007. $template = $this->parseTemplateFile($templateName);
  1008. if ($template) {
  1009. // 获取模板文件内容
  1010. $parseStr .= file_get_contents($template);
  1011. }
  1012. }
  1013. return $parseStr;
  1014. }
  1015. /**
  1016. * 解析模板文件名
  1017. * @access private
  1018. * @param string $template 文件名
  1019. * @return string|false
  1020. */
  1021. private function parseTemplateFile($template)
  1022. {
  1023. if ('' == pathinfo($template, PATHINFO_EXTENSION)) {
  1024. if (strpos($template, '@')) {
  1025. list($module, $template) = explode('@', $template);
  1026. }
  1027. if (0 !== strpos($template, '/')) {
  1028. $template = str_replace(['/', ':'], $this->config['view_depr'], $template);
  1029. } else {
  1030. $template = str_replace(['/', ':'], $this->config['view_depr'], substr($template, 1));
  1031. }
  1032. if ($this->config['view_base']) {
  1033. $module = isset($module) ? $module : Request::instance()->module();
  1034. $path = $this->config['view_base'] . ($module ? $module . DS : '');
  1035. } else {
  1036. $path = isset($module) ? APP_PATH . $module . DS . basename($this->config['view_path']) . DS : $this->config['view_path'];
  1037. }
  1038. $template = realpath($path . $template . '.' . ltrim($this->config['view_suffix'], '.'));
  1039. }
  1040. if (is_file($template)) {
  1041. // 记录模板文件的更新时间
  1042. $this->includeFile[$template] = filemtime($template);
  1043. return $template;
  1044. } else {
  1045. throw new TemplateNotFoundException('template not exists:' . $template, $template);
  1046. }
  1047. }
  1048. /**
  1049. * 按标签生成正则
  1050. * @access private
  1051. * @param string $tagName 标签名
  1052. * @return string
  1053. */
  1054. private function getRegex($tagName)
  1055. {
  1056. $regex = '';
  1057. if ('tag' == $tagName) {
  1058. $begin = $this->config['tpl_begin'];
  1059. $end = $this->config['tpl_end'];
  1060. if (strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1) {
  1061. $regex = $begin . '((?:[\$]{1,2}[a-wA-w_]|[\:\~][\$a-wA-w_]|[+]{2}[\$][a-wA-w_]|[-]{2}[\$][a-wA-w_]|\/[\*\/])(?>[^' . $end . ']*))' . $end;
  1062. } else {
  1063. $regex = $begin . '((?:[\$]{1,2}[a-wA-w_]|[\:\~][\$a-wA-w_]|[+]{2}[\$][a-wA-w_]|[-]{2}[\$][a-wA-w_]|\/[\*\/])(?>(?:(?!' . $end . ').)*))' . $end;
  1064. }
  1065. } else {
  1066. $begin = $this->config['taglib_begin'];
  1067. $end = $this->config['taglib_end'];
  1068. $single = strlen(ltrim($begin, '\\')) == 1 && strlen(ltrim($end, '\\')) == 1 ? true : false;
  1069. switch ($tagName) {
  1070. case 'block':
  1071. if ($single) {
  1072. $regex = $begin . '(?:' . $tagName . '\b(?>(?:(?!name=).)*)\bname=([\'\"])(?P<name>[\$\w\-\/\.]+)\\1(?>[^' . $end . ']*)|\/' . $tagName . ')' . $end;
  1073. } else {
  1074. $regex = $begin . '(?:' . $tagName . '\b(?>(?:(?!name=).)*)\bname=([\'\"])(?P<name>[\$\w\-\/\.]+)\\1(?>(?:(?!' . $end . ').)*)|\/' . $tagName . ')' . $end;
  1075. }
  1076. break;
  1077. case 'literal':
  1078. if ($single) {
  1079. $regex = '(' . $begin . $tagName . '\b(?>[^' . $end . ']*)' . $end . ')';
  1080. $regex .= '(?:(?>[^' . $begin . ']*)(?>(?!' . $begin . '(?>' . $tagName . '\b[^' . $end . ']*|\/' . $tagName . ')' . $end . ')' . $begin . '[^' . $begin . ']*)*)';
  1081. $regex .= '(' . $begin . '\/' . $tagName . $end . ')';
  1082. } else {
  1083. $regex = '(' . $begin . $tagName . '\b(?>(?:(?!' . $end . ').)*)' . $end . ')';
  1084. $regex .= '(?:(?>(?:(?!' . $begin . ').)*)(?>(?!' . $begin . '(?>' . $tagName . '\b(?>(?:(?!' . $end . ').)*)|\/' . $tagName . ')' . $end . ')' . $begin . '(?>(?:(?!' . $begin . ').)*))*)';
  1085. $regex .= '(' . $begin . '\/' . $tagName . $end . ')';
  1086. }
  1087. break;
  1088. case 'restoreliteral':
  1089. $regex = '<!--###literal(\d+)###-->';
  1090. break;
  1091. case 'include':
  1092. $name = 'file';
  1093. case 'taglib':
  1094. case 'layout':
  1095. case 'extend':
  1096. if (empty($name)) {
  1097. $name = 'name';
  1098. }
  1099. if ($single) {
  1100. $regex = $begin . $tagName . '\b(?>(?:(?!' . $name . '=).)*)\b' . $name . '=([\'\"])(?P<name>[\$\w\-\/\.\:@,\\\\]+)\\1(?>[^' . $end . ']*)' . $end;
  1101. } else {
  1102. $regex = $begin . $tagName . '\b(?>(?:(?!' . $name . '=).)*)\b' . $name . '=([\'\"])(?P<name>[\$\w\-\/\.\:@,\\\\]+)\\1(?>(?:(?!' . $end . ').)*)' . $end;
  1103. }
  1104. break;
  1105. }
  1106. }
  1107. return '/' . $regex . '/is';
  1108. }
  1109. }