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

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. return static function () use (&$callback)
  193. {
  194. if (\is_callable($callback))
  195. {
  196. $callback = $callback();
  197. }
  198. return $callback;
  199. };
  200. }
  201. /**
  202. * Set the template data from an array
  203. *
  204. * @param array $arrData The data array
  205. */
  206. public function setData($arrData)
  207. {
  208. $this->arrData = $arrData;
  209. }
  210. /**
  211. * Return the template data as array
  212. *
  213. * @return array The data array
  214. */
  215. public function getData()
  216. {
  217. return $this->arrData;
  218. }
  219. /**
  220. * Set the template name
  221. *
  222. * @param string $strTemplate The template name
  223. */
  224. public function setName($strTemplate)
  225. {
  226. $this->strTemplate = $strTemplate;
  227. }
  228. /**
  229. * Return the template name
  230. *
  231. * @return string The template name
  232. */
  233. public function getName()
  234. {
  235. return $this->strTemplate;
  236. }
  237. /**
  238. * Set the output format
  239. *
  240. * @param string $strFormat The output format
  241. */
  242. public function setFormat($strFormat)
  243. {
  244. $this->strFormat = $strFormat;
  245. }
  246. /**
  247. * Return the output format
  248. *
  249. * @return string The output format
  250. */
  251. public function getFormat()
  252. {
  253. return $this->strFormat;
  254. }
  255. /**
  256. * Print all template variables to the screen using print_r
  257. *
  258. * @deprecated Deprecated since Contao 4.3, to be removed in Contao 5.
  259. * Use Template::dumpTemplateVars() instead.
  260. */
  261. public function showTemplateVars()
  262. {
  263. 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.');
  264. $this->dumpTemplateVars();
  265. }
  266. /**
  267. * Print all template variables to the screen using the Symfony VarDumper component
  268. */
  269. public function dumpTemplateVars()
  270. {
  271. VarDumper::dump($this->arrData);
  272. }
  273. /**
  274. * Parse the template file and return it as string
  275. *
  276. * @return string The template markup
  277. */
  278. public function parse()
  279. {
  280. if (!$this->strTemplate)
  281. {
  282. return '';
  283. }
  284. // HOOK: add custom parse filters
  285. if (isset($GLOBALS['TL_HOOKS']['parseTemplate']) && \is_array($GLOBALS['TL_HOOKS']['parseTemplate']))
  286. {
  287. foreach ($GLOBALS['TL_HOOKS']['parseTemplate'] as $callback)
  288. {
  289. $this->import($callback[0]);
  290. $this->{$callback[0]}->{$callback[1]}($this);
  291. }
  292. }
  293. return $this->inherit();
  294. }
  295. /**
  296. * Parse the template file and print it to the screen
  297. *
  298. * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
  299. * Use Template::getResponse() instead.
  300. */
  301. public function output()
  302. {
  303. 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.');
  304. $this->compile();
  305. header('Content-Type: ' . $this->strContentType . '; charset=' . System::getContainer()->getParameter('kernel.charset'));
  306. echo $this->strBuffer;
  307. }
  308. /**
  309. * Return a response object
  310. *
  311. * @return Response The response object
  312. */
  313. public function getResponse()
  314. {
  315. $this->compile();
  316. $response = new Response($this->strBuffer);
  317. $response->headers->set('Content-Type', $this->strContentType);
  318. $response->setCharset(System::getContainer()->getParameter('kernel.charset'));
  319. return $response;
  320. }
  321. /**
  322. * Return a route relative to the base URL
  323. *
  324. * @param string $strName The route name
  325. * @param array $arrParams The route parameters
  326. *
  327. * @return string The route
  328. */
  329. public function route($strName, $arrParams=array())
  330. {
  331. $strUrl = System::getContainer()->get('router')->generate($strName, $arrParams);
  332. $strUrl = substr($strUrl, \strlen(Environment::get('path')) + 1);
  333. return StringUtil::ampersand($strUrl);
  334. }
  335. /**
  336. * Return the preview route
  337. *
  338. * @param string $strName The route name
  339. * @param array $arrParams The route parameters
  340. *
  341. * @return string The route
  342. */
  343. public function previewRoute($strName, $arrParams=array())
  344. {
  345. $container = System::getContainer();
  346. if (!$previewScript = $container->getParameter('contao.preview_script'))
  347. {
  348. return $this->route($strName, $arrParams);
  349. }
  350. $router = $container->get('router');
  351. $context = $router->getContext();
  352. $context->setBaseUrl($previewScript);
  353. $strUrl = $router->generate($strName, $arrParams);
  354. $strUrl = substr($strUrl, \strlen(Environment::get('path')) + 1);
  355. $context->setBaseUrl('');
  356. return StringUtil::ampersand($strUrl);
  357. }
  358. /**
  359. * Returns a translated message
  360. *
  361. * @param string $strId
  362. * @param array $arrParams
  363. * @param string $strDomain
  364. *
  365. * @return string
  366. */
  367. public function trans($strId, array $arrParams=array(), $strDomain='contao_default')
  368. {
  369. return System::getContainer()->get('translator')->trans($strId, $arrParams, $strDomain);
  370. }
  371. /**
  372. * Helper method to allow quick access in the Contao templates for safe raw (unencoded) output.
  373. * It replaces (or optionally removes) Contao insert tags and removes all HTML.
  374. *
  375. * Be careful when using this. It must NOT be used within regular HTML when $value
  376. * is uncontrolled user input. It's useful to ensure raw values within e.g. <code> examples
  377. * or JSON-LD arrays.
  378. */
  379. public function rawPlainText(string $value, bool $removeInsertTags = false): string
  380. {
  381. return System::getContainer()->get('contao.string.html_decoder')->inputEncodedToPlainText($value, $removeInsertTags);
  382. }
  383. /**
  384. * Helper method to allow quick access in the Contao templates for safe raw (unencoded) output.
  385. *
  386. * Compared to $this->rawPlainText() it adds new lines before and after block level HTML elements
  387. * and only then removes the rest of the HTML tags.
  388. *
  389. * Be careful when using this. It must NOT be used within regular HTML when $value
  390. * is uncontrolled user input. It's useful to ensure raw values within e.g. <code> examples
  391. * or JSON-LD arrays.
  392. */
  393. public function rawHtmlToPlainText(string $value, bool $removeInsertTags = false): string
  394. {
  395. return System::getContainer()->get('contao.string.html_decoder')->htmlToPlainText($value, $removeInsertTags);
  396. }
  397. /**
  398. * Adds schema.org JSON-LD data to the current response context
  399. */
  400. public function addSchemaOrg(array $jsonLd): void
  401. {
  402. $responseContext = System::getContainer()->get('contao.routing.response_context_accessor')->getResponseContext();
  403. if (!$responseContext || !$responseContext->has(JsonLdManager::class))
  404. {
  405. return;
  406. }
  407. /** @var JsonLdManager $jsonLdManager */
  408. $jsonLdManager = $responseContext->get(JsonLdManager::class);
  409. $type = $jsonLdManager->createSchemaOrgTypeFromArray($jsonLd);
  410. $jsonLdManager
  411. ->getGraphForSchema(JsonLdManager::SCHEMA_ORG)
  412. ->set($type, $jsonLd['identifier'] ?? Graph::IDENTIFIER_DEFAULT)
  413. ;
  414. }
  415. /**
  416. * Render a figure
  417. *
  418. * The provided configuration array is used to configure a FigureBuilder.
  419. * If not explicitly set, the default template "image.html5" will be used
  420. * to render the results. To use the core's default Twig template, pass
  421. * "@ContaoCore/Image/Studio/figure.html.twig" as $template argument.
  422. *
  423. * @param int|string|FilesModel|ImageInterface $from Can be a FilesModel, an ImageInterface, a tl_files UUID/ID/path or a file system path
  424. * @param int|string|array|PictureConfiguration|null $size A picture size configuration or reference
  425. * @param array<string, mixed> $configuration Configuration for the FigureBuilder
  426. * @param string $template A Contao or Twig template
  427. *
  428. * @return string|null Returns null if the resource is invalid
  429. */
  430. public function figure($from, $size, $configuration = array(), $template = 'image')
  431. {
  432. return System::getContainer()->get('contao.image.studio.figure_renderer')->render($from, $size, $configuration, $template);
  433. }
  434. /**
  435. * Returns an asset path
  436. *
  437. * @param string $path
  438. * @param string|null $packageName
  439. *
  440. * @return string
  441. */
  442. public function asset($path, $packageName = null)
  443. {
  444. $url = System::getContainer()->get('assets.packages')->getUrl($path, $packageName);
  445. $basePath = '/';
  446. $request = System::getContainer()->get('request_stack')->getMainRequest();
  447. if ($request !== null)
  448. {
  449. $basePath = $request->getBasePath() . '/';
  450. }
  451. if (0 === strncmp($url, $basePath, \strlen($basePath)))
  452. {
  453. return substr($url, \strlen($basePath));
  454. }
  455. // Contao paths are relative to the <base> tag, so remove leading slashes
  456. return $url;
  457. }
  458. /**
  459. * Returns an asset version
  460. *
  461. * @param string $path
  462. * @param string|null $packageName
  463. *
  464. * @return string
  465. */
  466. public function assetVersion($path, $packageName = null)
  467. {
  468. return System::getContainer()->get('assets.packages')->getVersion($path, $packageName);
  469. }
  470. /**
  471. * Returns a container parameter
  472. *
  473. * @param string $strKey
  474. *
  475. * @return mixed
  476. */
  477. public function param($strKey)
  478. {
  479. return System::getContainer()->getParameter($strKey);
  480. }
  481. /**
  482. * Compile the template
  483. *
  484. * @internal Do not call this method in your code. It will be made private in Contao 5.0.
  485. */
  486. protected function compile()
  487. {
  488. if (!$this->strBuffer)
  489. {
  490. $this->strBuffer = $this->parse();
  491. }
  492. }
  493. /**
  494. * Return the debug bar string
  495. *
  496. * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
  497. */
  498. protected function getDebugBar()
  499. {
  500. trigger_deprecation('contao/core-bundle', '4.0', 'Using "Contao\Template::getDebugBar()" has been deprecated and will no longer work in Contao 5.0.');
  501. }
  502. /**
  503. * Minify the HTML markup preserving pre, script, style and textarea tags
  504. *
  505. * @param string $strHtml The HTML markup
  506. *
  507. * @return string The minified HTML markup
  508. */
  509. public function minifyHtml($strHtml)
  510. {
  511. if (System::getContainer()->getParameter('kernel.debug'))
  512. {
  513. return $strHtml;
  514. }
  515. // Split the markup based on the tags that shall be preserved
  516. $arrChunks = preg_split('@(</?pre[^>]*>)|(</?script[^>]*>)|(</?style[^>]*>)|( ?</?textarea[^>]*>)@i', $strHtml, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);
  517. $strHtml = '';
  518. $blnPreserveNext = false;
  519. $blnOptimizeNext = false;
  520. $strType = null;
  521. // Check for valid JavaScript types (see #7927)
  522. $isJavaScript = static function ($strChunk)
  523. {
  524. $typeMatch = array();
  525. if (preg_match('/\stype\s*=\s*(?:(?J)(["\'])\s*(?<type>.*?)\s*\1|(?<type>[^\s>]+))/i', $strChunk, $typeMatch) && !\in_array(strtolower($typeMatch['type']), static::$validJavaScriptTypes))
  526. {
  527. return false;
  528. }
  529. if (preg_match('/\slanguage\s*=\s*(?:(?J)(["\'])\s*(?<type>.*?)\s*\1|(?<type>[^\s>]+))/i', $strChunk, $typeMatch) && !\in_array('text/' . strtolower($typeMatch['type']), static::$validJavaScriptTypes))
  530. {
  531. return false;
  532. }
  533. return true;
  534. };
  535. // Recombine the markup
  536. foreach ($arrChunks as $strChunk)
  537. {
  538. if (strncasecmp($strChunk, '<pre', 4) === 0 || strncasecmp(ltrim($strChunk), '<textarea', 9) === 0)
  539. {
  540. $blnPreserveNext = true;
  541. }
  542. elseif (strncasecmp($strChunk, '<script', 7) === 0)
  543. {
  544. if ($isJavaScript($strChunk))
  545. {
  546. $blnOptimizeNext = true;
  547. $strType = 'js';
  548. }
  549. else
  550. {
  551. $blnPreserveNext = true;
  552. }
  553. }
  554. elseif (strncasecmp($strChunk, '<style', 6) === 0)
  555. {
  556. $blnOptimizeNext = true;
  557. $strType = 'css';
  558. }
  559. elseif ($blnPreserveNext)
  560. {
  561. $blnPreserveNext = false;
  562. }
  563. elseif ($blnOptimizeNext)
  564. {
  565. $blnOptimizeNext = false;
  566. // Minify inline scripts
  567. if ($strType == 'js')
  568. {
  569. $objMinify = new JS();
  570. $objMinify->add($strChunk);
  571. $strChunk = $objMinify->minify();
  572. }
  573. elseif ($strType == 'css')
  574. {
  575. $objMinify = new CSS();
  576. $objMinify->add($strChunk);
  577. $strChunk = $objMinify->minify();
  578. }
  579. }
  580. else
  581. {
  582. // Remove line indentations and trailing spaces
  583. $strChunk = str_replace("\r", '', $strChunk);
  584. $strChunk = preg_replace(array('/^[\t ]+/m', '/[\t ]+$/m', '/\n\n+/'), array('', '', "\n"), $strChunk);
  585. }
  586. $strHtml .= $strChunk;
  587. }
  588. return trim($strHtml);
  589. }
  590. /**
  591. * Generate the markup for a style sheet tag
  592. *
  593. * @param string $href The script path
  594. * @param string $media The media type string
  595. * @param mixed $mtime The file mtime
  596. *
  597. * @return string The markup string
  598. */
  599. public static function generateStyleTag($href, $media=null, $mtime=false)
  600. {
  601. // Add the filemtime if not given and not an external file
  602. if ($mtime === null && !preg_match('@^https?://@', $href))
  603. {
  604. $container = System::getContainer();
  605. $projectDir = $container->getParameter('kernel.project_dir');
  606. if (file_exists($projectDir . '/' . $href))
  607. {
  608. $mtime = filemtime($projectDir . '/' . $href);
  609. }
  610. else
  611. {
  612. $webDir = StringUtil::stripRootDir($container->getParameter('contao.web_dir'));
  613. // Handle public bundle resources in the contao.web_dir folder
  614. if (file_exists($projectDir . '/' . $webDir . '/' . $href))
  615. {
  616. $mtime = filemtime($projectDir . '/' . $webDir . '/' . $href);
  617. }
  618. }
  619. }
  620. if ($mtime)
  621. {
  622. $href .= '?v=' . substr(md5($mtime), 0, 8);
  623. }
  624. return '<link rel="stylesheet" href="' . $href . '"' . (($media && $media != 'all') ? ' media="' . $media . '"' : '') . '>';
  625. }
  626. /**
  627. * Generate the markup for inline CSS code
  628. *
  629. * @param string $script The CSS code
  630. *
  631. * @return string The markup string
  632. */
  633. public static function generateInlineStyle($script)
  634. {
  635. return '<style>' . $script . '</style>';
  636. }
  637. /**
  638. * Generate the markup for a JavaScript tag
  639. *
  640. * @param string $src The script path
  641. * @param boolean $async True to add the async attribute
  642. * @param mixed $mtime The file mtime
  643. * @param string|null $hash An optional integrity hash
  644. * @param string|null $crossorigin An optional crossorigin attribute
  645. * @param string|null $referrerpolicy An optional referrerpolicy attribute
  646. *
  647. * @return string The markup string
  648. */
  649. public static function generateScriptTag($src, $async=false, $mtime=false, $hash=null, $crossorigin=null, $referrerpolicy=null)
  650. {
  651. // Add the filemtime if not given and not an external file
  652. if ($mtime === null && !preg_match('@^https?://@', $src))
  653. {
  654. $container = System::getContainer();
  655. $projectDir = $container->getParameter('kernel.project_dir');
  656. if (file_exists($projectDir . '/' . $src))
  657. {
  658. $mtime = filemtime($projectDir . '/' . $src);
  659. }
  660. else
  661. {
  662. $webDir = StringUtil::stripRootDir($container->getParameter('contao.web_dir'));
  663. // Handle public bundle resources in the contao.web_dir folder
  664. if (file_exists($projectDir . '/' . $webDir . '/' . $src))
  665. {
  666. $mtime = filemtime($projectDir . '/' . $webDir . '/' . $src);
  667. }
  668. }
  669. }
  670. if ($mtime)
  671. {
  672. $src .= '?v=' . substr(md5($mtime), 0, 8);
  673. }
  674. return '<script src="' . $src . '"' . ($async ? ' async' : '') . ($hash ? ' integrity="' . $hash . '"' : '') . ($crossorigin ? ' crossorigin="' . $crossorigin . '"' : '') . ($referrerpolicy ? ' referrerpolicy="' . $referrerpolicy . '"' : '') . '></script>';
  675. }
  676. /**
  677. * Generate the markup for an inline JavaScript
  678. *
  679. * @param string $script The JavaScript code
  680. *
  681. * @return string The markup string
  682. */
  683. public static function generateInlineScript($script)
  684. {
  685. return '<script>' . $script . '</script>';
  686. }
  687. /**
  688. * Generate the markup for an RSS feed tag
  689. *
  690. * @param string $href The script path
  691. * @param string $format The feed format
  692. * @param string $title The feed title
  693. *
  694. * @return string The markup string
  695. */
  696. public static function generateFeedTag($href, $format, $title)
  697. {
  698. return '<link type="application/' . $format . '+xml" rel="alternate" href="' . $href . '" title="' . StringUtil::specialchars($title) . '">';
  699. }
  700. /**
  701. * Flush the output buffers
  702. *
  703. * @deprecated Deprecated since Contao 4.0, to be removed in Contao 5.0.
  704. */
  705. public function flushAllData()
  706. {
  707. trigger_deprecation('contao/core-bundle', '4.0', 'Using "Contao\Template::flushAllData()" has been deprecated and will no longer work in Contao 5.0.');
  708. if (\function_exists('fastcgi_finish_request'))
  709. {
  710. fastcgi_finish_request();
  711. }
  712. elseif (\PHP_SAPI !== 'cli')
  713. {
  714. $status = ob_get_status(true);
  715. $level = \count($status);
  716. 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))))
  717. {
  718. ob_end_flush();
  719. }
  720. flush();
  721. }
  722. }
  723. }
  724. class_alias(Template::class, 'Template');