1<?php 2declare(strict_types=1); 3/** 4 * PHPTAL templating engine 5 * 6 * @category HTML 7 * @package PHPTAL 8 * @author Laurent Bedubourg <lbedubourg@motion-twin.com> 9 * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk> 10 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License 11 * @link http://phptal.org/ 12 */ 13 14namespace PhpTal; 15 16use PhpTal\Php\TalesInternal; 17use RuntimeException; 18use stdClass; 19use Throwable; 20 21/** 22 * PHPTAL template entry point. 23 * 24 * @category HTML 25 * @package PHPTAL 26 * @author Laurent Bedubourg <lbedubourg@motion-twin.com> 27 * @author Kornel Lesiński <kornel@aardvarkmedia.co.uk> 28 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License 29 * @link http://phptal.org/ 30 */ 31class PHPTAL implements PhpTalInterface 32{ 33 34 public const PHPTAL_VERSION = '3_0_2'; 35 36 /** 37 * constants for output mode 38 * @see setOutputMode() 39 */ 40 public const XHTML = 11; 41 public const XML = 22; 42 public const HTML5 = 55; 43 44 /** 45 * @see getPreFilters() 46 * 47 * @var FilterInterface[] 48 */ 49 protected $prefilters = []; 50 51 /** 52 * The postfilter which will get called on every run 53 * 54 * @var FilterInterface 55 */ 56 protected $postfilter; 57 58 /** 59 * list of template source repositories given to file source resolver 60 * 61 * @var string[] 62 */ 63 protected $repositories = []; 64 65 /** 66 * template path (path that has been set, not necessarily loaded) 67 * 68 * @var string|null 69 */ 70 protected $path; 71 72 /** 73 * template source resolvers (classes that search for templates by name) 74 * 75 * @var SourceResolverInterface[] 76 */ 77 protected $resolvers = []; 78 79 /** 80 * template source (only set when not working with file) 81 * 82 * @var StringSource|null 83 */ 84 protected $source; 85 86 /** 87 * destination of PHP intermediate file 88 * 89 * @var string 90 */ 91 protected $codeFile; 92 93 /** 94 * php function generated for the template 95 * 96 * @var string 97 */ 98 protected $functionName; 99 100 /** 101 * set to true when template is ready for execution 102 * 103 * @var bool 104 */ 105 protected $prepared = false; 106 107 /** 108 * associative array of phptal:id => \PhpTal\TriggerInterface 109 * 110 * @var TriggerInterface[] 111 */ 112 protected $triggers = []; 113 114 /** 115 * i18n translator 116 * 117 * @var TranslationServiceInterface|null 118 */ 119 protected $translator; 120 121 /** 122 * global execution context 123 * 124 * @var stdClass 125 */ 126 protected $globalContext; 127 128 /** 129 * current execution context 130 * 131 * @var Context 132 */ 133 protected $context; 134 135 /** 136 * list of on-error caught exceptions 137 * 138 * @var \Exception[] 139 */ 140 protected $errors = []; 141 142 /** 143 * encoding used throughout 144 * 145 * @var string 146 */ 147 protected $encoding = 'UTF-8'; 148 149 /** 150 * type of syntax used in generated templates 151 * 152 * @var int 153 */ 154 protected $outputMode = self::XHTML; 155 156 // configuration properties 157 158 /** 159 * don't use code cache 160 * 161 * @var bool 162 */ 163 protected $forceReparse = false; 164 165 /** 166 * directory where code cache is 167 * 168 * @var string 169 */ 170 private $phpCodeDestination; 171 172 /** 173 * @var string 174 */ 175 private $phpCodeExtension = 'php'; 176 177 /** 178 * number of days 179 * 180 * @var float 181 */ 182 private $cacheLifetime = 30.; 183 184 /** 185 * 1/x 186 * 187 * @var int 188 */ 189 private $cachePurgeFrequency = 30; 190 191 /** 192 * speeds up calls to external templates 193 * 194 * @var PhpTalInterface[] 195 */ 196 private $externalMacroTemplatesCache = []; 197 198 /** 199 * @var int 200 */ 201 private $subpathRecursionLevel = 0; 202 203 /** 204 * @param string $path Template file path. 205 */ 206 public function __construct(?string $path = null) 207 { 208 $this->path = $path; 209 $this->globalContext = new stdClass(); 210 $this->context = new Context(); 211 $this->context->setGlobal($this->globalContext); 212 213 $this->setPhpCodeDestination(sys_get_temp_dir()); 214 } 215 216 /** 217 * Clone template state and context. 218 * 219 * @return void 220 */ 221 public function __clone() 222 { 223 $this->context = $this->context->pushContext(); 224 } 225 226 /** 227 * Set template from file path. 228 * 229 * @param string $path filesystem path, 230 * or any path that will be accepted by source resolver 231 * 232 * @return $this 233 */ 234 public function setTemplate(?string $path): PhpTalInterface 235 { 236 $this->prepared = false; 237 $this->functionName = null; 238 $this->codeFile = null; 239 $this->path = $path; 240 $this->source = null; 241 $this->context->_docType = null; 242 $this->context->_xmlDeclaration = null; 243 return $this; 244 } 245 246 /** 247 * Set template from source. 248 * 249 * Should be used only with temporary template sources. 250 * Use setTemplate() or addSourceResolver() whenever possible. 251 * 252 * @param string $src The phptal template source. 253 * @param string $path Fake and 'unique' template path. 254 * 255 * @return $this 256 */ 257 public function setSource(string $src, ?string $path = null): PhpTalInterface 258 { 259 $this->prepared = false; 260 $this->functionName = null; 261 $this->codeFile = null; 262 $this->source = new StringSource($src, $path); 263 $this->path = $this->source->getRealPath(); 264 $this->context->_docType = null; 265 $this->context->_xmlDeclaration = null; 266 return $this; 267 } 268 269 /** 270 * Specify where to look for templates. 271 * 272 * @param mixed $rep string or Array of repositories 273 * 274 * @return $this 275 */ 276 public function setTemplateRepository($rep): PhpTalInterface 277 { 278 if (is_array($rep)) { 279 $this->repositories = $rep; 280 } else { 281 $this->repositories[] = $rep; 282 } 283 return $this; 284 } 285 286 /** 287 * Get template repositories. 288 * 289 * @return array 290 */ 291 public function getTemplateRepositories(): array 292 { 293 return $this->repositories; 294 } 295 296 /** 297 * Clears the template repositories. 298 * 299 * @return $this 300 */ 301 public function clearTemplateRepositories(): PhpTalInterface 302 { 303 $this->repositories = []; 304 return $this; 305 } 306 307 /** 308 * Specify how to look for templates. 309 * 310 * @param SourceResolverInterface $resolver instance of resolver 311 * 312 * @return $this 313 */ 314 public function addSourceResolver(SourceResolverInterface $resolver): PhpTalInterface 315 { 316 $this->resolvers[] = $resolver; 317 return $this; 318 } 319 320 /** 321 * Ignore XML/XHTML comments on parsing. 322 * Comments starting with <!--! are always stripped. 323 * 324 * @param bool $bool if true all comments are stripped during parse 325 * 326 * @return $this 327 */ 328 public function stripComments(bool $bool): PhpTalInterface 329 { 330 $this->resetPrepared(); 331 332 if ($bool) { 333 $this->prefilters['_phptal_strip_comments_'] = new PreFilter\StripComments(); 334 } else { 335 unset($this->prefilters['_phptal_strip_comments_']); 336 } 337 return $this; 338 } 339 340 /** 341 * Set output mode 342 * XHTML output mode will force elements like <link/>, <meta/> and <img/>, etc. 343 * to be empty and threats attributes like selected, checked to be 344 * boolean attributes. 345 * 346 * XML output mode outputs XML without such modifications 347 * and is neccessary to generate RSS feeds properly. 348 * 349 * @param int $mode (\PhpTal\PHPTAL::XML, \PhpTal\PHPTAL::XHTML or \PhpTal\PHPTAL::HTML5). 350 * 351 * @return $this 352 * @throws Exception\ConfigurationException 353 */ 354 public function setOutputMode(int $mode): PhpTalInterface 355 { 356 $this->resetPrepared(); 357 358 if (!in_array($mode, [static::XHTML, static::XML, static::HTML5], true)) { 359 throw new Exception\ConfigurationException('Unsupported output mode ' . $mode); 360 } 361 $this->outputMode = $mode; 362 return $this; 363 } 364 365 /** 366 * Get output mode 367 * @see setOutputMode() 368 * 369 * @return int output mode constant 370 */ 371 public function getOutputMode(): int 372 { 373 return $this->outputMode; 374 } 375 376 /** 377 * Set input and ouput encoding. Encoding is case-insensitive. 378 * 379 * @param string $enc example: 'UTF-8' 380 * 381 * @return $this 382 */ 383 public function setEncoding(string $enc): PhpTalInterface 384 { 385 $enc = strtoupper($enc); 386 if ($enc !== $this->encoding) { 387 $this->encoding = $enc; 388 if ($this->translator) { 389 $this->translator->setEncoding($enc); 390 } 391 392 $this->resetPrepared(); 393 } 394 return $this; 395 } 396 397 /** 398 * Get input and ouput encoding. 399 * 400 * @return string 401 */ 402 public function getEncoding(): string 403 { 404 return $this->encoding; 405 } 406 407 /** 408 * Set the storage location for intermediate PHP files. 409 * The path cannot contain characters that would be interpreted by glob() (e.g. *[]?) 410 * 411 * @param string $path Intermediate file path. 412 * 413 * @return void 414 */ 415 public function setPhpCodeDestination(string $path): void 416 { 417 $this->phpCodeDestination = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; 418 $this->resetPrepared(); 419 } 420 421 /** 422 * Get the storage location for intermediate PHP files. 423 * 424 * @return string 425 */ 426 public function getPhpCodeDestination(): string 427 { 428 return $this->phpCodeDestination; 429 } 430 431 /** 432 * Set the file extension for intermediate PHP files. 433 * 434 * @param string $extension The file extension. 435 * 436 * @return $this 437 */ 438 public function setPhpCodeExtension(string $extension): PhpTalInterface 439 { 440 $this->phpCodeExtension = $extension; 441 $this->resetPrepared(); 442 return $this; 443 } 444 445 /** 446 * Get the file extension for intermediate PHP files. 447 */ 448 public function getPhpCodeExtension(): string 449 { 450 return $this->phpCodeExtension; 451 } 452 453 /** 454 * Flags whether to ignore intermediate php files and to 455 * reparse templates every time (if set to true). 456 * 457 * DON'T USE IN PRODUCTION - this makes PHPTAL many times slower. 458 * 459 * @param bool $bool Forced reparse state. 460 * 461 * @return $this 462 */ 463 public function setForceReparse(bool $bool): PhpTalInterface 464 { 465 $this->forceReparse = $bool; 466 return $this; 467 } 468 469 /** 470 * Get the value of the force reparse state. 471 * 472 * @return bool 473 */ 474 public function getForceReparse(): bool 475 { 476 return $this->forceReparse; 477 } 478 479 /** 480 * Set I18N translator. 481 * 482 * This sets encoding used by the translator, so be sure to use encoding-dependent 483 * features of the translator (e.g. addDomain) _after_ calling setTranslator. 484 * 485 * @param TranslationServiceInterface $t instance 486 * 487 * @return $this 488 */ 489 public function setTranslator(TranslationServiceInterface $t): PhpTalInterface 490 { 491 $this->translator = $t; 492 $t->setEncoding($this->getEncoding()); 493 return $this; 494 } 495 496 /** 497 * Add new prefilter to filter chain. 498 * Prefilters are called only once template is compiled. 499 * 500 * PreFilters must inherit PreFilter class. 501 * (in future this method will allow string with filter name instead of object) 502 * 503 * @param PreFilter $filter PreFilter object or name of prefilter to add 504 * 505 * @return $this 506 */ 507 final public function addPreFilter(PreFilter $filter): PhpTalInterface 508 { 509 $this->resetPrepared(); 510 $this->prefilters[] = $filter; 511 return $this; 512 } 513 514 /** 515 * Sets the level of recursion for template cache directories 516 * 517 * @param int $recursion_level 518 * 519 * @return self 520 */ 521 public function setSubpathRecursionLevel(int $recursion_level): PhpTalInterface 522 { 523 $this->subpathRecursionLevel = $recursion_level; 524 return $this; 525 } 526 527 /** 528 * Array with all prefilter objects *or strings* that are names of prefilter classes. 529 * (the latter is not implemented in 1.2.1) 530 * 531 * Array keys may be non-numeric! 532 * 533 * @return FilterInterface[] 534 */ 535 protected function getPreFilters(): array 536 { 537 return $this->prefilters; 538 } 539 540 /** 541 * Returns string that is unique for every different configuration of prefilters. 542 * Result of prefilters may be cached until this string changes. 543 * 544 * You can override this function. 545 * 546 * @return string 547 */ 548 private function getPreFiltersCacheId(): string 549 { 550 $cacheid = ''; 551 foreach ($this->getPreFilters() as $key => $prefilter) { 552 if ($prefilter instanceof PreFilter) { 553 $cacheid .= $key . $prefilter->getCacheId(); 554 } else { 555 $cacheid .= $key . get_class($prefilter); 556 } 557 } 558 return $cacheid; 559 } 560 561 /** 562 * Instantiate prefilters 563 * 564 * @return FilterInterface[] 565 */ 566 private function getPreFilterInstances(): array 567 { 568 $prefilters = $this->getPreFilters(); 569 570 foreach ($prefilters as $prefilter) { 571 if ($prefilter instanceof PreFilter) { 572 $prefilter->setPHPTAL($this); 573 } 574 } 575 return $prefilters; 576 } 577 578 /** 579 * Set template post filter. 580 * It will be called every time after template generates output. 581 * 582 * See PHPTAL_PostFilter class. 583 * 584 * @param FilterInterface $filter filter instance 585 * 586 * @return $this 587 */ 588 public function setPostFilter(FilterInterface $filter): PhpTalInterface 589 { 590 $this->postfilter = $filter; 591 return $this; 592 } 593 594 /** 595 * Register a trigger for specified phptal:id. 596 * 597 * @param string $id phptal:id to look for 598 * @param TriggerInterface $trigger 599 * 600 * @return $this 601 */ 602 public function addTrigger(string $id, TriggerInterface $trigger): PhpTalInterface 603 { 604 $this->triggers[$id] = $trigger; 605 return $this; 606 } 607 608 /** 609 * Returns trigger for specified phptal:id. 610 * 611 * @param string $id phptal:id 612 * 613 * @return TriggerInterface|null 614 */ 615 public function getTrigger(string $id): ?TriggerInterface 616 { 617 return $this->triggers[$id] ?? null; 618 } 619 620 /** 621 * Set a context variable. 622 * Use it by setting properties on PHPTAL object. 623 * 624 * @param string $varname 625 * @param mixed $value 626 * 627 * @return void 628 * @throws Exception\InvalidVariableNameException 629 */ 630 public function __set($varname, $value) 631 { 632 $this->context->set($varname, $value); 633 } 634 635 /** 636 * Set a context variable. 637 * 638 * @see \PhpTal\PHPTAL::__set() 639 * @param string $varname name of the variable 640 * @param mixed $value value of the variable 641 * 642 * @return $this 643 * @throws Exception\InvalidVariableNameException 644 */ 645 public function set(string $varname, $value): PhpTalInterface 646 { 647 $this->context->set($varname, $value); 648 return $this; 649 } 650 651 /** 652 * Execute the template code and return generated markup. 653 * 654 * @return string 655 * @throws Exception\TemplateException 656 * @throws Throwable 657 */ 658 public function execute(): string 659 { 660 $res = ''; 661 662 try { 663 if (!$this->prepared) { 664 // includes generated template PHP code 665 $this->prepare(); 666 } 667 $this->context->echoDeclarations(false); 668 669 $templateFunction = $this->getFunctionName(); 670 671 try { 672 ob_start(); 673 $templateFunction($this, $this->context); 674 $res = ob_get_clean(); 675 } catch (Throwable $e) { 676 ob_end_clean(); 677 throw $e; 678 } 679 680 // unshift doctype 681 if ($this->context->_docType) { 682 $res = $this->context->_docType . $res; 683 } 684 685 // unshift xml declaration 686 if ($this->context->_xmlDeclaration) { 687 $res = $this->context->_xmlDeclaration . "\n" . $res; 688 } 689 690 if ($this->postfilter !== null) { 691 return $this->postfilter->filter($res); 692 } 693 } catch (Throwable $e) { 694 ExceptionHandler::handleException($e, $this->getEncoding()); 695 } 696 697 return $res; 698 } 699 700 /** 701 * Execute and echo template without buffering of the output. 702 * This function does not allow postfilters nor DOCTYPE/XML declaration. 703 * 704 * @return void 705 * @throws Exception\TemplateException 706 * @throws Throwable 707 */ 708 public function echoExecute(): void 709 { 710 try { 711 if (!$this->prepared) { 712 // includes generated template PHP code 713 $this->prepare(); 714 } 715 716 if ($this->postfilter !== null) { 717 throw new Exception\ConfigurationException('echoExecute() does not support postfilters'); 718 } 719 720 $this->context->echoDeclarations(true); 721 722 $templateFunction = $this->getFunctionName(); 723 $templateFunction($this, $this->context); 724 } catch (Throwable $e) { 725 ExceptionHandler::handleException($e, $this->getEncoding()); 726 } 727 } 728 729 /** 730 * This is PHPTAL's internal function that handles 731 * execution of macros from templates. 732 * 733 * $this is caller's context (the file where execution had originally started) 734 * 735 * @param string $path 736 * @param PhpTalInterface $local_tpl is PHPTAL instance of the file in which macro is defined 737 * (it will be different from $this if it's external macro call) 738 * 739 * @throws Exception\IOException 740 * @throws Exception\MacroMissingException 741 * @throws Exception\TemplateException 742 * @throws Throwable 743 */ 744 final public function executeMacroOfTemplate(string $path, PhpTalInterface $local_tpl): void 745 { 746 // extract macro source file from macro name, if macro path does not 747 // contain filename, then the macro is assumed to be local 748 749 if (preg_match('/^(.*?)\/([a-z0-9_-]*)$/i', $path, $m)) { 750 [, $file, $macroName] = $m; 751 752 if (isset($this->externalMacroTemplatesCache[$file])) { 753 $tpl = $this->externalMacroTemplatesCache[$file]; 754 } else { 755 $tpl = clone $this; 756 array_unshift($tpl->repositories, dirname($this->source->getRealPath())); 757 $tpl->setTemplate($file); 758 $tpl->prepare(); 759 760 // keep it small (typically only 1 or 2 external files are used) 761 if (count($this->externalMacroTemplatesCache) > 10) { 762 $this->externalMacroTemplatesCache = []; 763 } 764 $this->externalMacroTemplatesCache[$file] = $tpl; 765 } 766 767 $fun = $tpl->getFunctionName() . '_' . str_replace('-', '_', $macroName); 768 if (!function_exists($fun)) { 769 throw new Exception\MacroMissingException( 770 "Macro '$macroName' is not defined in $file", 771 $this->getSource()->getRealPath() 772 ); 773 } 774 775 $fun($tpl, $this); 776 } else { 777 // call local macro 778 $fun = $local_tpl->getFunctionName() . '_' . str_replace('-', '_', $path); 779 if (!function_exists($fun)) { 780 throw new Exception\MacroMissingException( 781 "Macro '$path' is not defined", 782 $local_tpl->getSource()->getRealPath() 783 ); 784 } 785 $fun($local_tpl, $this); 786 } 787 } 788 789 /** 790 * ensure that getCodePath will return up-to-date path 791 * 792 * @return void 793 * @throws Exception\ConfigurationException 794 * @throws Exception\IOException 795 */ 796 private function setCodeFile(): void 797 { 798 $this->findTemplate(); 799 $this->codeFile = $this->getPhpCodeDestination() . $this->getSubPath() . '/' . $this->getFunctionName() 800 . '.' . $this->getPhpCodeExtension(); 801 } 802 803 /** 804 * Generate a subpath structure depending on the config 805 * 806 * @return string 807 */ 808 private function getSubPath(): string 809 { 810 $real_path = md5($this->getFunctionName()); 811 $path = ''; 812 for ($i = 0; $i < $this->subpathRecursionLevel; $i++) { 813 $path .= '/' . $real_path[$i]; 814 } 815 if (!file_exists($this->getPhpCodeDestination() . $path) && 816 !mkdir($concurrentDirectory = $this->getPhpCodeDestination() . $path, 0777, true) && 817 !is_dir($concurrentDirectory)) { 818 throw new RuntimeException(sprintf('Directory "%s" was not created', $concurrentDirectory)); 819 } 820 return $path; 821 } 822 823 /** 824 * @return void 825 */ 826 protected function resetPrepared(): void 827 { 828 $this->prepared = false; 829 $this->functionName = null; 830 $this->codeFile = null; 831 } 832 833 /** 834 * Prepare template without executing it. 835 * 836 * @return self 837 * @throws Exception\ConfigurationException 838 * @throws Exception\IOException 839 * @throws Exception\TemplateException 840 * @throws Throwable 841 */ 842 public function prepare(): PhpTalInterface 843 { 844 // clear just in case settings changed and cache is out of date 845 $this->externalMacroTemplatesCache = []; 846 847 // find the template source file and update function name 848 $this->setCodeFile(); 849 850 if (!function_exists($this->getFunctionName())) { 851 // parse template if php generated code does not exists or template 852 // source file modified since last generation or force reparse is set 853 if ($this->getForceReparse() || !file_exists($this->getCodePath())) { 854 // i'm not sure where that belongs, but not in normal path of execution 855 // because some sites have _a lot_ of files in temp 856 if ($this->getCachePurgeFrequency() && mt_rand() % $this->getCachePurgeFrequency() === 0) { 857 $this->cleanUpGarbage(); 858 } 859 860 $result = $this->parse(); 861 862 if (!file_put_contents($this->getCodePath(), $result)) { 863 throw new Exception\IOException('Unable to open '.$this->getCodePath().' for writing'); 864 } 865 866 // the awesome thing about eval() is that parse errors don't stop PHP. 867 // when PHP dies during eval, fatal error is printed and 868 // can be captured with output buffering 869 ob_start(); 870 try { 871 eval("?>\n".$result); 872 } catch (Throwable $e) { 873 ob_end_clean(); 874 throw $e; 875 } 876 877 if (!function_exists($this->getFunctionName())) { 878 $msg = str_replace('eval()\'d code', $this->getCodePath(), ob_get_clean()); 879 880 // greedy .* ensures last match 881 $line = preg_match('/.*on line (\d+)$/m', $msg, $m) ? $m[1] : 0; 882 throw new Exception\TemplateException(trim($msg), $this->getCodePath(), $line); 883 } 884 ob_end_clean(); 885 } else { 886 // eval trick is used only on first run, 887 // just in case it causes any problems with opcode accelerators 888 require $this->getCodePath(); 889 } 890 } 891 892 $this->prepared = true; 893 return $this; 894 } 895 896 /** 897 * get how long compiled templates and phptal:cache files are kept, in days 898 * 899 * @return float 900 */ 901 private function getCacheLifetime(): float 902 { 903 return $this->cacheLifetime; 904 } 905 906 /** 907 * set how long compiled templates and phptal:cache files are kept 908 * 909 * @param float $days number of days 910 * 911 * @return $this 912 */ 913 public function setCacheLifetime(float $days): PhpTalInterface 914 { 915 $this->cacheLifetime = max(0.5, $days); 916 return $this; 917 } 918 919 /** 920 * PHPTAL will scan cache and remove old files on every nth compile 921 * Set to 0 to disable cleanups 922 * 923 * @param int $n 924 * 925 * @return $this 926 */ 927 public function setCachePurgeFrequency(int $n): PhpTalInterface 928 { 929 $this->cachePurgeFrequency = $n; 930 return $this; 931 } 932 933 /** 934 * how likely cache cleaning can happen 935 * @see self::setCachePurgeFrequency() 936 * 937 * @return int 938 */ 939 private function getCachePurgeFrequency(): int 940 { 941 return $this->cachePurgeFrequency; 942 } 943 944 945 /** 946 * Removes all compiled templates from cache that 947 * are older than getCacheLifetime() days 948 * 949 * @return void 950 */ 951 public function cleanUpGarbage(): void 952 { 953 $cacheFilesExpire = (int) (time() - $this->getCacheLifetime() * 3600 * 24); 954 955 // relies on templates sorting order being related to their modification dates 956 $upperLimit = $this->getPhpCodeDestination() . $this->getFunctionNamePrefix($cacheFilesExpire) . '_'; 957 $lowerLimit = $this->getPhpCodeDestination() . $this->getFunctionNamePrefix(); 958 959 // last * gets phptal:cache 960 $cacheFiles = glob(sprintf( 961 '%s%stpl_????????_*.%s*', 962 $this->getPhpCodeDestination(), 963 str_repeat('*/', $this->subpathRecursionLevel), 964 $this->getPhpCodeExtension() 965 ), GLOB_NOSORT); 966 967 if ($cacheFiles) { 968 foreach ($cacheFiles as $index => $file) { 969 // comparison here skips filenames that are certainly too new 970 if (strcmp($file, $upperLimit) <= 0 || strpos($file, $lowerLimit) === 0) { 971 $time = filemtime($file); 972 if ($time && $time < $cacheFilesExpire) { 973 @unlink($file); 974 } 975 } 976 } 977 } 978 } 979 980 /** 981 * Removes content cached with phptal:cache for currently set template 982 * Must be called after setSource/setTemplate. 983 * 984 * @return void 985 * @throws Exception\ConfigurationException 986 * @throws Exception\IOException 987 */ 988 public function cleanUpCache(): void 989 { 990 $filename = $this->getCodePath(); 991 $cacheFiles = glob($filename . '?*', GLOB_NOSORT); 992 if ($cacheFiles) { 993 foreach ($cacheFiles as $file) { 994 if (strpos($file, $filename) !== 0) { 995 continue; 996 } // safety net 997 @unlink($file); 998 } 999 } 1000 $this->prepared = false; 1001 } 1002 1003 /** 1004 * Returns the path of the intermediate PHP code file. 1005 * 1006 * The returned file may be used to cleanup (unlink) temporary files 1007 * generated by temporary templates or more simply for debug. 1008 * 1009 * @return string 1010 * @throws Exception\ConfigurationException 1011 * @throws Exception\IOException 1012 */ 1013 public function getCodePath(): string 1014 { 1015 if (!$this->codeFile) { 1016 $this->setCodeFile(); 1017 } 1018 return $this->codeFile; 1019 } 1020 1021 /** 1022 * Returns the generated template function name. 1023 * 1024 * @return string 1025 */ 1026 public function getFunctionName(): string 1027 { 1028 // function name is used as base for caching, so it must be unique for 1029 // every combination of settings that changes code in compiled template 1030 1031 if (!$this->functionName) { 1032 // just to make tempalte name recognizable 1033 $basename = preg_replace('/\.[a-z]{3,5}$/', '', basename($this->source->getRealPath())); 1034 $basename = substr(trim(preg_replace('/[^a-zA-Z0-9]+/', '_', $basename), '_'), 0, 20); 1035 1036 $hash = md5( 1037 static::PHPTAL_VERSION . PHP_VERSION 1038 . $this->source->getRealPath() 1039 . $this->getEncoding() 1040 . $this->getPreFiltersCacheId() 1041 . $this->getOutputMode(), 1042 true 1043 ); 1044 1045 // uses base64 rather than hex to make filename shorter. 1046 // there is loss of some bits due to name constraints and case-insensivity, 1047 // but that's still over 110 bits in addition to basename and timestamp. 1048 $hash = strtr(rtrim(base64_encode($hash), '='), '+/=', '_A_'); 1049 1050 $this->functionName = $this->getFunctionNamePrefix($this->source->getLastModifiedTime()) . 1051 $basename . '__' . $hash; 1052 } 1053 return $this->functionName; 1054 } 1055 1056 /** 1057 * Returns prefix used for function name. 1058 * Function name is also base name for the template. 1059 * 1060 * @param int|null $timestamp unix timestamp with template modification date 1061 * 1062 * @return string 1063 */ 1064 private function getFunctionNamePrefix(?int $timestamp = null): string 1065 { 1066 // tpl_ prefix and last modified time must not be changed, 1067 // because cache cleanup relies on that 1068 return 'tpl_' . sprintf('%08x', $timestamp ?? 0) . '_'; 1069 } 1070 1071 /** 1072 * Returns template translator. 1073 * 1074 * @return TranslationServiceInterface|null 1075 */ 1076 public function getTranslator(): ?TranslationServiceInterface 1077 { 1078 return $this->translator; 1079 } 1080 1081 /** 1082 * Returns array of exceptions caught by tal:on-error attribute. 1083 * 1084 * @return \Exception[] 1085 */ 1086 public function getErrors(): array 1087 { 1088 return $this->errors; 1089 } 1090 1091 /** 1092 * Public for phptal templates, private for user. 1093 * 1094 * @param \Exception $error 1095 * 1096 * @return void 1097 */ 1098 public function addError(\Exception $error): void 1099 { 1100 $this->errors[] = $error; 1101 } 1102 1103 /** 1104 * Returns current context object. 1105 * Use only in Triggers. 1106 * 1107 * @return Context 1108 */ 1109 public function getContext(): Context 1110 { 1111 return $this->context; 1112 } 1113 1114 /** 1115 * only for use in generated template code 1116 * 1117 * @return stdClass 1118 */ 1119 public function getGlobalContext(): stdClass 1120 { 1121 return $this->globalContext; 1122 } 1123 1124 /** 1125 * only for use in generated template code 1126 * 1127 * @return Context 1128 */ 1129 final public function pushContext(): Context 1130 { 1131 $this->context = $this->context->pushContext(); 1132 return $this->context; 1133 } 1134 1135 /** 1136 * only for use in generated template code 1137 * 1138 * @return Context 1139 */ 1140 final public function popContext(): Context 1141 { 1142 $this->context = $this->context->popContext(); 1143 return $this->context; 1144 } 1145 1146 /** 1147 * Parse currently set template, prefilter and generate PHP code. 1148 * 1149 * @return string (compiled PHP code) 1150 * @throws Exception\ConfigurationException 1151 * @throws Exception\ParserException 1152 * @throws Exception\TemplateException 1153 * @throws Exception\PhpTalException 1154 */ 1155 protected function parse(): string 1156 { 1157 $data = $this->source->getData(); 1158 1159 $prefilters = $this->getPreFilterInstances(); 1160 foreach ($prefilters as $prefilter) { 1161 $data = $prefilter->filter($data); 1162 } 1163 1164 $realpath = $this->source->getRealPath(); 1165 $parser = new Dom\SaxXmlParser($this->encoding); 1166 1167 $builder = new Dom\PHPTALDocumentBuilder(); 1168 $tree = $parser->parseString($builder, $data, $realpath)->getResult(); 1169 1170 foreach ($prefilters as $prefilter) { 1171 if ($prefilter instanceof PreFilter) { 1172 $prefilter->filterDOM($tree); 1173 } 1174 } 1175 1176 $state = new Php\State($this); 1177 1178 $codewriter = new Php\CodeWriter($state); 1179 $codewriter->doTemplateFile($this->getFunctionName(), $tree); 1180 1181 return $codewriter->getResult(); 1182 } 1183 1184 /** 1185 * Search template source location. 1186 * 1187 * @return void 1188 * @throws Exception\ConfigurationException 1189 * @throws Exception\IOException 1190 */ 1191 protected function findTemplate(): void 1192 { 1193 if ($this->path === null) { 1194 throw new Exception\ConfigurationException('No template file specified'); 1195 } 1196 1197 if ($this->source !== null) { 1198 return; 1199 } 1200 1201 if ($this->resolvers === [] && !$this->repositories) { 1202 $this->source = new FileSource($this->path); 1203 } else { 1204 foreach ($this->resolvers as $resolver) { 1205 $source = $resolver->resolve($this->path); 1206 if ($source !== null) { 1207 $this->source = $source; 1208 return; 1209 } 1210 } 1211 1212 $resolver = new FileSourceResolver($this->repositories); 1213 $this->source = $resolver->resolve($this->path); 1214 } 1215 1216 if (!$this->source) { 1217 throw new Exception\IOException('Unable to locate template file '.$this->path); 1218 } 1219 } 1220 1221 /** 1222 * @return SourceInterface 1223 */ 1224 public function getSource(): SourceInterface 1225 { 1226 return $this->source; 1227 } 1228 1229 /** 1230 * @return PHPTAL 1231 */ 1232 public function allowPhpModifier(): PhpTalInterface 1233 { 1234 TalesInternal::setPhpModifierAllowed(true); 1235 return $this; 1236 } 1237 1238 /** 1239 * @return PHPTAL 1240 */ 1241 public function disallowPhpModifier(): PhpTalInterface 1242 { 1243 TalesInternal::setPhpModifierAllowed(false); 1244 return $this; 1245 } 1246} 1247