vendor/contao/core-bundle/src/Resources/contao/library/Contao/Template.php line 151

Open in your IDE?
  1. <?php
  2. /*
  3. * This file is part of Contao.
  4. *
  5. * (c) Leo Feyer
  6. *
  7. * @license LGPL-3.0-or-later
  8. */
  9. namespace Contao;
  10. use Contao\CoreBundle\Routing\ResponseContext\JsonLd\JsonLdManager;
  11. use Contao\Image\ImageInterface;
  12. use Contao\Image\PictureConfiguration;
  13. use MatthiasMullie\Minify\CSS;
  14. use MatthiasMullie\Minify\JS;
  15. use Spatie\SchemaOrg\Graph;
  16. use Symfony\Component\HttpFoundation\Response;
  17. use Symfony\Component\VarDumper\VarDumper;
  18. /**
  19. * Parses and outputs template files
  20. *
  21. * The class supports loading template files, adding variables to them and then
  22. * printing them to the screen. It functions as abstract parent class for the
  23. * two core classes "BackendTemplate" and "FrontendTemplate".
  24. *
  25. * Usage:
  26. *
  27. * $template = new BackendTemplate();
  28. * $template->name = 'Leo Feyer';
  29. * $template->output();
  30. *
  31. * @property string $style
  32. * @property array|string $cssID
  33. * @property string $class
  34. * @property string $inColumn
  35. * @property string $headline
  36. * @property array $hl
  37. * @property string $content
  38. * @property string $action
  39. * @property string $enforceTwoFactor
  40. * @property string $targetPath
  41. * @property string $message
  42. * @property string $href
  43. * @property string $twoFactor
  44. * @property string $explain
  45. * @property string $active
  46. * @property string $enableButton
  47. * @property string $disableButton
  48. * @property boolean $enable
  49. * @property boolean $isEnabled
  50. * @property string $secret
  51. * @property string $textCode
  52. * @property string $qrCode
  53. * @property string $scan
  54. * @property string $verify
  55. * @property string $verifyHelp
  56. * @property boolean $showBackupCodes
  57. * @property array $backupCodes
  58. * @property boolean $trustedDevicesEnabled
  59. * @property array $trustedDevices
  60. * @property string $currentDevice
  61. */
  62. abstract class Template extends Controller
  63. {
  64. use TemplateInheritance;
  65. /**
  66. * Output buffer
  67. * @var string
  68. */
  69. protected $strBuffer;
  70. /**
  71. * Content type
  72. * @var string
  73. */
  74. protected $strContentType;
  75. /**
  76. * Template data
  77. * @var array
  78. */
  79. protected $arrData = array();
  80. /**
  81. * Valid JavaScipt types
  82. * @var array
  83. * @see http://www.w3.org/TR/html5/scripting-1.html#scriptingLanguages
  84. */
  85. protected static $validJavaScriptTypes = array
  86. (
  87. 'application/ecmascript',
  88. 'application/javascript',
  89. 'application/x-ecmascript',
  90. 'application/x-javascript',
  91. 'text/ecmascript',
  92. 'text/javascript',
  93. 'text/javascript1.0',
  94. 'text/javascript1.1',
  95. 'text/javascript1.2',
  96. 'text/javascript1.3',
  97. 'text/javascript1.4',
  98. 'text/javascript1.5',
  99. 'text/jscript',
  100. 'text/livescript',
  101. 'text/x-ecmascript',
  102. 'text/x-javascript',
  103. );
  104. /**
  105. * Create a new template object
  106. *
  107. * @param string $strTemplate The template name
  108. * @param string $strContentType The content type (defaults to "text/html")
  109. */
  110. public function __construct($strTemplate='', $strContentType='text/html')
  111. {
  112. parent::__construct();
  113. $this->strTemplate = $strTemplate;
  114. $this->strContentType = $strContentType;
  115. }
  116. /**
  117. * Set an object property
  118. *
  119. * @param string $strKey The property name
  120. * @param mixed $varValue The property value
  121. */
  122. public function __set($strKey, $varValue)
  123. {
  124. $this->arrData[$strKey] = $varValue;
  125. }
  126. /**
  127. * Return an object property
  128. *
  129. * @param string $strKey The property name
  130. *
  131. * @return mixed The property value
  132. */
  133. public function __get($strKey)
  134. {
  135. if (isset($this->arrData[$strKey]))
  136. {
  137. if (\is_object($this->arrData[$strKey]) && \is_callable($this->arrData[$strKey]))
  138. {
  139. return $this->arrData[$strKey]();
  140. }
  141. return $this->arrData[$strKey];
  142. }
  143. return parent::__get($strKey);
  144. }
  145. /**
  146. * Execute a callable and return the result
  147. *
  148. * @param string $strKey The name of the key
  149. * @param array $arrParams The parameters array
  150. *
  151. * @return mixed The callable return value
  152. *
  153. * @throws \InvalidArgumentException If the callable does not exist
  154. */
  155. public function __call($strKey, $arrParams)
  156. {
  157. if (!isset($this->arrData[$strKey]) || !\is_callable($this->arrData[$strKey]))
  158. {
  159. throw new \InvalidArgumentException("$strKey is not set or not a callable");
  160. }
  161. return ($this->arrData[$strKey])(...$arrParams);
  162. }
  163. /**
  164. * Check whether a property is set
  165. *
  166. * @param string $strKey The property name
  167. *
  168. * @return boolean True if the property is set
  169. */
  170. public function __isset($strKey)
  171. {
  172. return isset($this->arrData[$strKey]);
  173. }
  174. /**
  175. * Adds a function to a template which is evaluated only once. This is helpful for
  176. * lazy-evaluating data where we can use functions without arguments. Let's say
  177. * you wanted to lazy-evaluate a variable like this:
  178. *
  179. * $template->hasText = function () use ($article) {
  180. * return ContentModel::countPublishedByPidAndTable($article->id, 'tl_news') > 0;
  181. * };
  182. *
  183. * This would cause a query everytime $template->hasText is accessed in the
  184. * template. You can improve this by turning it into this:
  185. *
  186. * $template->hasText = Template::once(function () use ($article) {
  187. * return ContentModel::countPublishedByPidAndTable($article->id, 'tl_news') > 0;
  188. * });
  189. */
  190. public static function once(callable $callback)
  191. {
  192. $result = null;
  193. return static function () use (&$callback, &$result)
  194. {
  195. if ($callback !== null)
  196. {
  197. $result = $callback();
  198. $callback = null;
  199. }
  200. return $result;
  201. };
  202. }
  203. /**
  204. * Set the template data from an array
  205. *
  206. * @param array $arrData The data array
  207. */
  208. public function setData($arrData)
  209. {
  210. $this->arrData = $arrData;
  211. }
  212. /**
  213. * Return the template data as array
  214. *
  215. * @return array The data array
  216. */
  217. public function getData()
  218. {
  219. return $this->arrData;
  220. }
  221. /**
  222. * Set the template name
  223. *
  224. * @param string $strTemplate The template name
  225. */
  226. public function setName($strTemplate)
  227. {
  228. $this->strTemplate = $strTemplate;
  229. }
  230. /**
  231. * Return the template name
  232. *
  233. * @return string The template name
  234. */
  235. public function getName()
  236. {
  237. return $this->strTemplate;
  238. }
  239. /**
  240. * Set the output format
  241. *
  242. * @param string $strFormat The output format
  243. */
  244. public function setFormat($strFormat)
  245. {
  246. $this->strFormat = $strFormat;
  247. }
  248. /**
  249. * Return the output format
  250. *
  251. * @return string The output format
  252. */
  253. public function getFormat()
  254. {
  255. return $this->strFormat;
  256. }
  257. /**
  258. * Print all template variables to the screen using print_r
  259. *
  260. * @deprecated Deprecated since Contao 4.3, to be removed in Contao 5.
  261. * Use Template::dumpTemplateVars() instead.
  262. */
  263. public function showTemplateVars()
  264. {
  265. trigger_deprecation('contao/core-bundle', '4.0', 'Using "Contao\Template::showTemplateVars()" has been deprecated and will no longer work in Contao 5.0. Use "Contao\Template::dumpTemplateVars()" instead.');
  266. $this->dumpTemplateVars();
  267. }
  268. /**
  269. * Print all template variables to the screen using the Symfony VarDumper component
  270. */
  271. public function dumpTemplateVars()
  272. {
  273. VarDumper::dump($this->arrData);
  274. }
  275. /**
  276. * Parse the template file and return it as string
  277. *
  278. * @return string The template markup
  279. */
  280. public function parse()
  281. {
  282. if (!$this->strTemplate)
  283. {
  284. return '';
  285. }
  286. // HOOK: add custom parse filters
  287. if (isset($GLOBALS['TL_HOOKS']['parseTemplate']) && \is_array($GLOBALS['TL_HOOKS']['parseTemplate']))
  288. {
  289. foreach ($GLOBALS['TL_HOOKS']['parseTemplate'] as $callback)
  290. {
  291. $this->import($callback[0]);
  292. $this->{$callback[0]}->{$callback[1]}($this);
  293. }
  294. }
  295. return $this->inherit();
  296. }
  297. /**
  298. * Parse the template file and print it to the screen
  299. *
  300. * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
  301. * Use Template::getResponse() instead.
  302. */
  303. public function output()
  304. {
  305. trigger_deprecation('contao/core-bundle', '4.0', 'Using "Contao\Template::output()" has been deprecated and will no longer work in Contao 5.0. Use "Contao\Template::getResponse()" instead.');
  306. $this->compile();
  307. header('Content-Type: ' . $this->strContentType . '; charset=' . System::getContainer()->getParameter('kernel.charset'));
  308. echo $this->strBuffer;
  309. }
  310. /**
  311. * Return a response object
  312. *
  313. * @return Response The response object
  314. */
  315. public function getResponse()
  316. {
  317. $this->compile();
  318. $response = new Response($this->strBuffer);
  319. $response->headers->set('Content-Type', $this->strContentType);
  320. $response->setCharset(System::getContainer()->getParameter('kernel.charset'));
  321. return $response;
  322. }
  323. /**
  324. * Return a route relative to the base URL
  325. *
  326. * @param string $strName The route name
  327. * @param array $arrParams The route parameters
  328. *
  329. * @return string The route
  330. */
  331. public function route($strName, $arrParams=array())
  332. {
  333. $strUrl = System::getContainer()->get('router')->generate($strName, $arrParams);
  334. $strUrl = substr($strUrl, \strlen(Environment::get('path')) + 1);
  335. return StringUtil::ampersand($strUrl);
  336. }
  337. /**
  338. * Return the preview route
  339. *
  340. * @param string $strName The route name
  341. * @param array $arrParams The route parameters
  342. *
  343. * @return string The route
  344. */
  345. public function previewRoute($strName, $arrParams=array())
  346. {
  347. $container = System::getContainer();
  348. if (!$previewScript = $container->getParameter('contao.preview_script'))
  349. {
  350. return $this->route($strName, $arrParams);
  351. }
  352. $router = $container->get('router');
  353. $context = $router->getContext();
  354. $context->setBaseUrl($previewScript);
  355. $strUrl = $router->generate($strName, $arrParams);
  356. $strUrl = substr($strUrl, \strlen(Environment::get('path')) + 1);
  357. $context->setBaseUrl('');
  358. return StringUtil::ampersand($strUrl);
  359. }
  360. /**
  361. * Returns a translated message
  362. *
  363. * @param string $strId
  364. * @param array $arrParams
  365. * @param string $strDomain
  366. *
  367. * @return string
  368. */
  369. public function trans($strId, array $arrParams=array(), $strDomain='contao_default')
  370. {
  371. return System::getContainer()->get('translator')->trans($strId, $arrParams, $strDomain);
  372. }
  373. /**
  374. * Helper method to allow quick access in the Contao templates for safe raw (unencoded) output.
  375. * It replaces (or optionally removes) Contao insert tags and removes all HTML.
  376. *
  377. * Be careful when using this. It must NOT be used within regular HTML when $value
  378. * is uncontrolled user input. It's useful to ensure raw values within e.g. <code> examples
  379. * or JSON-LD arrays.
  380. */
  381. public function rawPlainText(string $value, bool $removeInsertTags = false): string
  382. {
  383. return System::getContainer()->get('contao.string.html_decoder')->inputEncodedToPlainText($value, $removeInsertTags);
  384. }
  385. /**
  386. * Helper method to allow quick access in the Contao templates for safe raw (unencoded) output.
  387. *
  388. * Compared to $this->rawPlainText() it adds new lines before and after block level HTML elements
  389. * and only then removes the rest of the HTML tags.
  390. *
  391. * Be careful when using this. It must NOT be used within regular HTML when $value
  392. * is uncontrolled user input. It's useful to ensure raw values within e.g. <code> examples
  393. * or JSON-LD arrays.
  394. */
  395. public function rawHtmlToPlainText(string $value, bool $removeInsertTags = false): string
  396. {
  397. return System::getContainer()->get('contao.string.html_decoder')->htmlToPlainText($value, $removeInsertTags);
  398. }
  399. /**
  400. * Adds schema.org JSON-LD data to the current response context
  401. */
  402. public function addSchemaOrg(array $jsonLd): void
  403. {
  404. $responseContext = System::getContainer()->get('contao.routing.response_context_accessor')->getResponseContext();
  405. if (!$responseContext || !$responseContext->has(JsonLdManager::class))
  406. {
  407. return;
  408. }
  409. /** @var JsonLdManager $jsonLdManager */
  410. $jsonLdManager = $responseContext->get(JsonLdManager::class);
  411. $type = $jsonLdManager->createSchemaOrgTypeFromArray($jsonLd);
  412. $jsonLdManager
  413. ->getGraphForSchema(JsonLdManager::SCHEMA_ORG)
  414. ->set($type, $jsonLd['identifier'] ?? Graph::IDENTIFIER_DEFAULT)
  415. ;
  416. }
  417. /**
  418. * Render a figure
  419. *
  420. * The provided configuration array is used to configure a FigureBuilder.
  421. * If not explicitly set, the default template "image.html5" will be used
  422. * to render the results. To use the core's default Twig template, pass
  423. * "@ContaoCore/Image/Studio/figure.html.twig" as $template argument.
  424. *
  425. * @param int|string|FilesModel|ImageInterface $from Can be a FilesModel, an ImageInterface, a tl_files UUID/ID/path or a file system path
  426. * @param int|string|array|PictureConfiguration|null $size A picture size configuration or reference
  427. * @param array<string, mixed> $configuration Configuration for the FigureBuilder
  428. * @param string $template A Contao or Twig template
  429. *
  430. * @return string|null Returns null if the resource is invalid
  431. */
  432. public function figure($from, $size, $configuration = array(), $template = 'image')
  433. {
  434. return System::getContainer()->get('contao.image.studio.figure_renderer')->render($from, $size, $configuration, $template);
  435. }
  436. /**
  437. * Returns an asset path
  438. *
  439. * @param string $path
  440. * @param string|null $packageName
  441. *
  442. * @return string
  443. */
  444. public function asset($path, $packageName = null)
  445. {
  446. $url = System::getContainer()->get('assets.packages')->getUrl($path, $packageName);
  447. $basePath = '/';
  448. $request = System::getContainer()->get('request_stack')->getMainRequest();
  449. if ($request !== null)
  450. {
  451. $basePath = $request->getBasePath() . '/';
  452. }
  453. if (0 === strncmp($url, $basePath, \strlen($basePath)))
  454. {
  455. return substr($url, \strlen($basePath));
  456. }
  457. // Contao paths are relative to the <base> tag, so remove leading slashes
  458. return $url;
  459. }
  460. /**
  461. * Returns an asset version
  462. *
  463. * @param string $path
  464. * @param string|null $packageName
  465. *
  466. * @return string
  467. */
  468. public function assetVersion($path, $packageName = null)
  469. {
  470. return System::getContainer()->get('assets.packages')->getVersion($path, $packageName);
  471. }
  472. /**
  473. * Returns a container parameter
  474. *
  475. * @param string $strKey
  476. *
  477. * @return mixed
  478. */
  479. public function param($strKey)
  480. {
  481. return System::getContainer()->getParameter($strKey);
  482. }
  483. /**
  484. * Compile the template
  485. *
  486. * @internal Do not call this method in your code. It will be made private in Contao 5.0.
  487. */
  488. protected function compile()
  489. {
  490. if (!$this->strBuffer)
  491. {
  492. $this->strBuffer = $this->parse();
  493. }
  494. }
  495. /**
  496. * Return the debug bar string
  497. *
  498. * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
  499. */
  500. protected function getDebugBar()
  501. {
  502. trigger_deprecation('contao/core-bundle', '4.0', 'Using "Contao\Template::getDebugBar()" has been deprecated and will no longer work in Contao 5.0.');
  503. }
  504. /**
  505. * Minify the HTML markup preserving pre, script, style and textarea tags
  506. *
  507. * @param string $strHtml The HTML markup
  508. *
  509. * @return string The minified HTML markup
  510. */
  511. public function minifyHtml($strHtml)
  512. {
  513. if (System::getContainer()->getParameter('kernel.debug'))
  514. {
  515. return $strHtml;
  516. }
  517. // Split the markup based on the tags that shall be preserved
  518. $arrChunks = preg_split('@(</?pre[^>]*>)|(</?script[^>]*>)|(</?style[^>]*>)|( ?</?textarea[^>]*>)@i', $strHtml, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
  519. $strHtml = '';
  520. $blnPreserveNext = false;
  521. $blnOptimizeNext = false;
  522. $strType = null;
  523. // Check for valid JavaScript types (see #7927)
  524. $isJavaScript = static function ($strChunk)
  525. {
  526. $typeMatch = array();
  527. if (preg_match('/\stype\s*=\s*(?:(?J)(["\'])\s*(?<type>.*?)\s*\1|(?<type>[^\s>]+))/i', $strChunk, $typeMatch) && !\in_array(strtolower($typeMatch['type']), static::$validJavaScriptTypes))
  528. {
  529. return false;
  530. }
  531. if (preg_match('/\slanguage\s*=\s*(?:(?J)(["\'])\s*(?<type>.*?)\s*\1|(?<type>[^\s>]+))/i', $strChunk, $typeMatch) && !\in_array('text/' . strtolower($typeMatch['type']), static::$validJavaScriptTypes))
  532. {
  533. return false;
  534. }
  535. return true;
  536. };
  537. // Recombine the markup
  538. foreach ($arrChunks as $strChunk)
  539. {
  540. if (strncasecmp($strChunk, '<pre', 4) === 0 || strncasecmp(ltrim($strChunk), '<textarea', 9) === 0)
  541. {
  542. $blnPreserveNext = true;
  543. }
  544. elseif (strncasecmp($strChunk, '<script', 7) === 0)
  545. {
  546. if ($isJavaScript($strChunk))
  547. {
  548. $blnOptimizeNext = true;
  549. $strType = 'js';
  550. }
  551. else
  552. {
  553. $blnPreserveNext = true;
  554. }
  555. }
  556. elseif (strncasecmp($strChunk, '<style', 6) === 0)
  557. {
  558. $blnOptimizeNext = true;
  559. $strType = 'css';
  560. }
  561. elseif ($blnPreserveNext)
  562. {
  563. $blnPreserveNext = false;
  564. }
  565. elseif ($blnOptimizeNext)
  566. {
  567. $blnOptimizeNext = false;
  568. // Minify inline scripts
  569. if ($strType == 'js')
  570. {
  571. $objMinify = new JS();
  572. $objMinify->add($strChunk);
  573. $strChunk = $objMinify->minify();
  574. }
  575. elseif ($strType == 'css')
  576. {
  577. $objMinify = new CSS();
  578. $objMinify->add($strChunk);
  579. $strChunk = $objMinify->minify();
  580. }
  581. }
  582. else
  583. {
  584. // Remove line indentations and trailing spaces
  585. $strChunk = str_replace("\r", '', $strChunk);
  586. $strChunk = preg_replace(array('/^[\t ]+/m', '/[\t ]+$/m', '/\n\n+/'), array('', '', "\n"), $strChunk);
  587. }
  588. $strHtml .= $strChunk;
  589. }
  590. return trim($strHtml);
  591. }
  592. /**
  593. * Generate the markup for a style sheet tag
  594. *
  595. * @param string $href The script path
  596. * @param string $media The media type string
  597. * @param mixed $mtime The file mtime
  598. *
  599. * @return string The markup string
  600. */
  601. public static function generateStyleTag($href, $media=null, $mtime=false)
  602. {
  603. // Add the filemtime if not given and not an external file
  604. if ($mtime === null && !preg_match('@^https?://@', $href))
  605. {
  606. $container = System::getContainer();
  607. $projectDir = $container->getParameter('kernel.project_dir');
  608. if (file_exists($projectDir . '/' . $href))
  609. {
  610. $mtime = filemtime($projectDir . '/' . $href);
  611. }
  612. else
  613. {
  614. $webDir = StringUtil::stripRootDir($container->getParameter('contao.web_dir'));
  615. // Handle public bundle resources in the contao.web_dir folder
  616. if (file_exists($projectDir . '/' . $webDir . '/' . $href))
  617. {
  618. $mtime = filemtime($projectDir . '/' . $webDir . '/' . $href);
  619. }
  620. }
  621. }
  622. if ($mtime)
  623. {
  624. $href .= '?v=' . substr(md5($mtime), 0, 8);
  625. }
  626. return '<link rel="stylesheet" href="' . $href . '"' . (($media && $media != 'all') ? ' media="' . $media . '"' : '') . '>';
  627. }
  628. /**
  629. * Generate the markup for inline CSS code
  630. *
  631. * @param string $script The CSS code
  632. *
  633. * @return string The markup string
  634. */
  635. public static function generateInlineStyle($script)
  636. {
  637. return '<style>' . $script . '</style>';
  638. }
  639. /**
  640. * Generate the markup for a JavaScript tag
  641. *
  642. * @param string $src The script path
  643. * @param boolean $async True to add the async attribute
  644. * @param mixed $mtime The file mtime
  645. * @param string|null $hash An optional integrity hash
  646. * @param string|null $crossorigin An optional crossorigin attribute
  647. * @param string|null $referrerpolicy An optional referrerpolicy attribute
  648. *
  649. * @return string The markup string
  650. */
  651. public static function generateScriptTag($src, $async=false, $mtime=false, $hash=null, $crossorigin=null, $referrerpolicy=null)
  652. {
  653. // Add the filemtime if not given and not an external file
  654. if ($mtime === null && !preg_match('@^https?://@', $src))
  655. {
  656. $container = System::getContainer();
  657. $projectDir = $container->getParameter('kernel.project_dir');
  658. if (file_exists($projectDir . '/' . $src))
  659. {
  660. $mtime = filemtime($projectDir . '/' . $src);
  661. }
  662. else
  663. {
  664. $webDir = StringUtil::stripRootDir($container->getParameter('contao.web_dir'));
  665. // Handle public bundle resources in the contao.web_dir folder
  666. if (file_exists($projectDir . '/' . $webDir . '/' . $src))
  667. {
  668. $mtime = filemtime($projectDir . '/' . $webDir . '/' . $src);
  669. }
  670. }
  671. }
  672. if ($mtime)
  673. {
  674. $src .= '?v=' . substr(md5($mtime), 0, 8);
  675. }
  676. return '<script src="' . $src . '"' . ($async ? ' async' : '') . ($hash ? ' integrity="' . $hash . '"' : '') . ($crossorigin ? ' crossorigin="' . $crossorigin . '"' : '') . ($referrerpolicy ? ' referrerpolicy="' . $referrerpolicy . '"' : '') . '></script>';
  677. }
  678. /**
  679. * Generate the markup for an inline JavaScript
  680. *
  681. * @param string $script The JavaScript code
  682. *
  683. * @return string The markup string
  684. */
  685. public static function generateInlineScript($script)
  686. {
  687. return '<script>' . $script . '</script>';
  688. }
  689. /**
  690. * Generate the markup for an RSS feed tag
  691. *
  692. * @param string $href The script path
  693. * @param string $format The feed format
  694. * @param string $title The feed title
  695. *
  696. * @return string The markup string
  697. */
  698. public static function generateFeedTag($href, $format, $title)
  699. {
  700. return '<link type="application/' . $format . '+xml" rel="alternate" href="' . $href . '" title="' . StringUtil::specialchars($title) . '">';
  701. }
  702. /**
  703. * Flush the output buffers
  704. *
  705. * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
  706. */
  707. public function flushAllData()
  708. {
  709. trigger_deprecation('contao/core-bundle', '4.0', 'Using "Contao\Template::flushAllData()" has been deprecated and will no longer work in Contao 5.0.');
  710. if (\function_exists('fastcgi_finish_request'))
  711. {
  712. fastcgi_finish_request();
  713. }
  714. elseif (\PHP_SAPI !== 'cli')
  715. {
  716. $status = ob_get_status(true);
  717. $level = \count($status);
  718. while ($level-- > 0 && (!empty($status[$level]['del']) || (isset($status[$level]['flags']) && ($status[$level]['flags'] & PHP_OUTPUT_HANDLER_REMOVABLE) && ($status[$level]['flags'] & PHP_OUTPUT_HANDLER_FLUSHABLE))))
  719. {
  720. ob_end_flush();
  721. }
  722. flush();
  723. }
  724. }
  725. }
  726. class_alias(Template::class, 'Template');