Throwing exceptions from Twig templates fires onDisplayErrorPage.[code] event allowing better error pages

This commit is contained in:
Matias Griese
2021-09-03 18:39:41 +03:00
parent 47875a4525
commit ec37fd065f
8 changed files with 232 additions and 101 deletions

View File

@@ -4,6 +4,7 @@
1. [](#new)
* Added `|yaml` filter to convert input to YAML
* Added `route` and `request` to `onPageNotFound` event
* Throwing exceptions from Twig templates fires `onDisplayErrorPage.[code]` event allowing better error pages
3. [](#bugfix)
* Fixed escaping in PageIndex::getLevelListing()
* Fixed validation of `number` type [#3433](https://github.com/getgrav/grav/issues/3433)

143
composer.lock generated
View File

@@ -311,26 +311,26 @@
},
{
"name": "doctrine/collections",
"version": "1.6.7",
"version": "1.6.8",
"source": {
"type": "git",
"url": "https://github.com/doctrine/collections.git",
"reference": "55f8b799269a1a472457bd1a41b4f379d4cfba4a"
"reference": "1958a744696c6bb3bb0d28db2611dc11610e78af"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/collections/zipball/55f8b799269a1a472457bd1a41b4f379d4cfba4a",
"reference": "55f8b799269a1a472457bd1a41b4f379d4cfba4a",
"url": "https://api.github.com/repos/doctrine/collections/zipball/1958a744696c6bb3bb0d28db2611dc11610e78af",
"reference": "1958a744696c6bb3bb0d28db2611dc11610e78af",
"shasum": ""
},
"require": {
"php": "^7.1.3 || ^8.0"
},
"require-dev": {
"doctrine/coding-standard": "^6.0",
"phpstan/phpstan-shim": "^0.9.2",
"phpunit/phpunit": "^7.0",
"vimeo/psalm": "^3.8.1"
"doctrine/coding-standard": "^9.0",
"phpstan/phpstan": "^0.12",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.1.5",
"vimeo/psalm": "^4.2.1"
},
"type": "library",
"autoload": {
@@ -374,9 +374,9 @@
],
"support": {
"issues": "https://github.com/doctrine/collections/issues",
"source": "https://github.com/doctrine/collections/tree/1.6.7"
"source": "https://github.com/doctrine/collections/tree/1.6.8"
},
"time": "2020-07-27T17:53:49+00:00"
"time": "2021-08-10T18:51:53+00:00"
},
{
"name": "donatj/phpuseragentparser",
@@ -642,16 +642,16 @@
},
{
"name": "filp/whoops",
"version": "2.14.0",
"version": "2.14.1",
"source": {
"type": "git",
"url": "https://github.com/filp/whoops.git",
"reference": "fdf92f03e150ed84d5967a833ae93abffac0315b"
"reference": "15ead64e9828f0fc90932114429c4f7923570cb1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/filp/whoops/zipball/fdf92f03e150ed84d5967a833ae93abffac0315b",
"reference": "fdf92f03e150ed84d5967a833ae93abffac0315b",
"url": "https://api.github.com/repos/filp/whoops/zipball/15ead64e9828f0fc90932114429c4f7923570cb1",
"reference": "15ead64e9828f0fc90932114429c4f7923570cb1",
"shasum": ""
},
"require": {
@@ -701,7 +701,7 @@
],
"support": {
"issues": "https://github.com/filp/whoops/issues",
"source": "https://github.com/filp/whoops/tree/2.14.0"
"source": "https://github.com/filp/whoops/tree/2.14.1"
},
"funding": [
{
@@ -709,7 +709,7 @@
"type": "github"
}
],
"time": "2021-07-13T12:00:00+00:00"
"time": "2021-08-29T12:00:00+00:00"
},
{
"name": "getgrav/cache",
@@ -2258,16 +2258,16 @@
},
{
"name": "symfony/console",
"version": "v4.4.29",
"version": "v4.4.30",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "8baf0bbcfddfde7d7225ae8e04705cfd1081cd7b"
"reference": "a3f7189a0665ee33b50e9e228c46f50f5acbed22"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/8baf0bbcfddfde7d7225ae8e04705cfd1081cd7b",
"reference": "8baf0bbcfddfde7d7225ae8e04705cfd1081cd7b",
"url": "https://api.github.com/repos/symfony/console/zipball/a3f7189a0665ee33b50e9e228c46f50f5acbed22",
"reference": "a3f7189a0665ee33b50e9e228c46f50f5acbed22",
"shasum": ""
},
"require": {
@@ -2328,7 +2328,7 @@
"description": "Eases the creation of beautiful and testable command line interfaces",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/console/tree/v4.4.29"
"source": "https://github.com/symfony/console/tree/v4.4.30"
},
"funding": [
{
@@ -2344,7 +2344,7 @@
"type": "tidelift"
}
],
"time": "2021-07-27T19:04:53+00:00"
"time": "2021-08-25T19:27:26+00:00"
},
{
"name": "symfony/contracts",
@@ -2442,16 +2442,16 @@
},
{
"name": "symfony/event-dispatcher",
"version": "v4.4.27",
"version": "v4.4.30",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
"reference": "958a128b184fcf0ba45ec90c0e88554c9327c2e9"
"reference": "2fe81680070043c4c80e7cedceb797e34f377bac"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/958a128b184fcf0ba45ec90c0e88554c9327c2e9",
"reference": "958a128b184fcf0ba45ec90c0e88554c9327c2e9",
"url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/2fe81680070043c4c80e7cedceb797e34f377bac",
"reference": "2fe81680070043c4c80e7cedceb797e34f377bac",
"shasum": ""
},
"require": {
@@ -2506,7 +2506,7 @@
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/event-dispatcher/tree/v4.4.27"
"source": "https://github.com/symfony/event-dispatcher/tree/v4.4.30"
},
"funding": [
{
@@ -2522,7 +2522,7 @@
"type": "tidelift"
}
],
"time": "2021-07-23T15:41:52+00:00"
"time": "2021-08-04T20:31:23+00:00"
},
{
"name": "symfony/http-client",
@@ -3009,16 +3009,16 @@
},
{
"name": "symfony/process",
"version": "v4.4.27",
"version": "v4.4.30",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
"reference": "0b7dc5599ac4aa6d7b936c8f7d10abae64f6cf7f"
"reference": "13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/process/zipball/0b7dc5599ac4aa6d7b936c8f7d10abae64f6cf7f",
"reference": "0b7dc5599ac4aa6d7b936c8f7d10abae64f6cf7f",
"url": "https://api.github.com/repos/symfony/process/zipball/13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d",
"reference": "13d3161ef63a8ec21eeccaaf9a4d7f784a87a97d",
"shasum": ""
},
"require": {
@@ -3051,7 +3051,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/process/tree/v4.4.27"
"source": "https://github.com/symfony/process/tree/v4.4.30"
},
"funding": [
{
@@ -3067,20 +3067,20 @@
"type": "tidelift"
}
],
"time": "2021-07-23T15:41:52+00:00"
"time": "2021-08-04T20:31:23+00:00"
},
{
"name": "symfony/var-dumper",
"version": "v4.4.27",
"version": "v4.4.30",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "391d6d0e7a06ab54eb7c38fab29b8d174471b3ba"
"reference": "7f65c44c2ce80d3a0fcdb6385ee0ad535e45660c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/391d6d0e7a06ab54eb7c38fab29b8d174471b3ba",
"reference": "391d6d0e7a06ab54eb7c38fab29b8d174471b3ba",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/7f65c44c2ce80d3a0fcdb6385ee0ad535e45660c",
"reference": "7f65c44c2ce80d3a0fcdb6385ee0ad535e45660c",
"shasum": ""
},
"require": {
@@ -3140,7 +3140,7 @@
"dump"
],
"support": {
"source": "https://github.com/symfony/var-dumper/tree/v4.4.27"
"source": "https://github.com/symfony/var-dumper/tree/v4.4.30"
},
"funding": [
{
@@ -3156,7 +3156,7 @@
"type": "tidelift"
}
],
"time": "2021-07-23T15:41:52+00:00"
"time": "2021-08-04T20:31:23+00:00"
},
{
"name": "symfony/yaml",
@@ -3580,20 +3580,20 @@
},
{
"name": "codeception/lib-innerbrowser",
"version": "1.5.0",
"version": "1.5.1",
"source": {
"type": "git",
"url": "https://github.com/Codeception/lib-innerbrowser.git",
"reference": "4b0d89b37fe454e060a610a85280a87ab4f534f1"
"reference": "31b4b56ad53c3464fcb2c0a14d55a51a201bd3c2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Codeception/lib-innerbrowser/zipball/4b0d89b37fe454e060a610a85280a87ab4f534f1",
"reference": "4b0d89b37fe454e060a610a85280a87ab4f534f1",
"url": "https://api.github.com/repos/Codeception/lib-innerbrowser/zipball/31b4b56ad53c3464fcb2c0a14d55a51a201bd3c2",
"reference": "31b4b56ad53c3464fcb2c0a14d55a51a201bd3c2",
"shasum": ""
},
"require": {
"codeception/codeception": "*@dev",
"codeception/codeception": "4.*@dev",
"ext-dom": "*",
"ext-json": "*",
"ext-mbstring": "*",
@@ -3634,9 +3634,9 @@
],
"support": {
"issues": "https://github.com/Codeception/lib-innerbrowser/issues",
"source": "https://github.com/Codeception/lib-innerbrowser/tree/1.5.0"
"source": "https://github.com/Codeception/lib-innerbrowser/tree/1.5.1"
},
"time": "2021-04-23T06:18:29+00:00"
"time": "2021-08-30T15:21:42+00:00"
},
{
"name": "codeception/module-asserts",
@@ -4569,16 +4569,16 @@
},
{
"name": "phpstan/phpstan",
"version": "0.12.94",
"version": "0.12.98",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpstan.git",
"reference": "3d0ba4c198a24e3c3fc489f3ec6ac9612c4be5d6"
"reference": "3bb7cc246c057405dd5e290c3ecc62ab51d57e00"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/3d0ba4c198a24e3c3fc489f3ec6ac9612c4be5d6",
"reference": "3d0ba4c198a24e3c3fc489f3ec6ac9612c4be5d6",
"url": "https://api.github.com/repos/phpstan/phpstan/zipball/3bb7cc246c057405dd5e290c3ecc62ab51d57e00",
"reference": "3bb7cc246c057405dd5e290c3ecc62ab51d57e00",
"shasum": ""
},
"require": {
@@ -4609,7 +4609,7 @@
"description": "PHPStan - PHP Static Analysis Tool",
"support": {
"issues": "https://github.com/phpstan/phpstan/issues",
"source": "https://github.com/phpstan/phpstan/tree/0.12.94"
"source": "https://github.com/phpstan/phpstan/tree/0.12.98"
},
"funding": [
{
@@ -4629,7 +4629,7 @@
"type": "tidelift"
}
],
"time": "2021-07-30T09:05:27+00:00"
"time": "2021-09-02T12:33:01+00:00"
},
{
"name": "phpstan/phpstan-deprecation-rules",
@@ -5002,16 +5002,16 @@
},
{
"name": "phpunit/phpunit",
"version": "9.5.8",
"version": "9.5.9",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "191768ccd5c85513b4068bdbe99bb6390c7d54fb"
"reference": "ea8c2dfb1065eb35a79b3681eee6e6fb0a6f273b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/191768ccd5c85513b4068bdbe99bb6390c7d54fb",
"reference": "191768ccd5c85513b4068bdbe99bb6390c7d54fb",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/ea8c2dfb1065eb35a79b3681eee6e6fb0a6f273b",
"reference": "ea8c2dfb1065eb35a79b3681eee6e6fb0a6f273b",
"shasum": ""
},
"require": {
@@ -5089,7 +5089,7 @@
],
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.8"
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.5.9"
},
"funding": [
{
@@ -5101,7 +5101,7 @@
"type": "github"
}
],
"time": "2021-07-31T15:17:34+00:00"
"time": "2021-08-31T06:47:40+00:00"
},
{
"name": "psr/http-client",
@@ -6008,6 +6008,7 @@
"type": "github"
}
],
"abandoned": true,
"time": "2020-09-28T06:45:17+00:00"
},
{
@@ -6326,16 +6327,16 @@
},
{
"name": "symfony/dom-crawler",
"version": "v5.3.4",
"version": "v5.3.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/dom-crawler.git",
"reference": "2dd8890bd01be59a5221999c05ccf0fcafcb354f"
"reference": "c7eef3a60ccfdd8eafe07f81652e769ac9c7146c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/dom-crawler/zipball/2dd8890bd01be59a5221999c05ccf0fcafcb354f",
"reference": "2dd8890bd01be59a5221999c05ccf0fcafcb354f",
"url": "https://api.github.com/repos/symfony/dom-crawler/zipball/c7eef3a60ccfdd8eafe07f81652e769ac9c7146c",
"reference": "c7eef3a60ccfdd8eafe07f81652e769ac9c7146c",
"shasum": ""
},
"require": {
@@ -6381,7 +6382,7 @@
"description": "Eases DOM navigation for HTML and XML documents",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/dom-crawler/tree/v5.3.4"
"source": "https://github.com/symfony/dom-crawler/tree/v5.3.7"
},
"funding": [
{
@@ -6397,20 +6398,20 @@
"type": "tidelift"
}
],
"time": "2021-07-23T15:55:36+00:00"
"time": "2021-08-29T19:32:13+00:00"
},
{
"name": "symfony/finder",
"version": "v5.3.4",
"version": "v5.3.7",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "17f50e06018baec41551a71a15731287dbaab186"
"reference": "a10000ada1e600d109a6c7632e9ac42e8bf2fb93"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/17f50e06018baec41551a71a15731287dbaab186",
"reference": "17f50e06018baec41551a71a15731287dbaab186",
"url": "https://api.github.com/repos/symfony/finder/zipball/a10000ada1e600d109a6c7632e9ac42e8bf2fb93",
"reference": "a10000ada1e600d109a6c7632e9ac42e8bf2fb93",
"shasum": ""
},
"require": {
@@ -6443,7 +6444,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/v5.3.4"
"source": "https://github.com/symfony/finder/tree/v5.3.7"
},
"funding": [
{
@@ -6459,7 +6460,7 @@
"type": "tidelift"
}
],
"time": "2021-07-23T15:54:19+00:00"
"time": "2021-08-04T21:20:46+00:00"
},
{
"name": "theseer/tokenizer",

View File

@@ -9,6 +9,7 @@
namespace Grav\Common;
use Composer\Autoload\ClassLoader;
use Grav\Common\Config\Config;
use Grav\Common\Config\Setup;
use Grav\Common\Helpers\Exif;
@@ -152,6 +153,13 @@ class Grav extends Container
{
if (null === self::$instance) {
self::$instance = static::load($values);
/** @var ClassLoader|null $loader */
$loader = self::$instance['loader'] ?? null;
if ($loader) {
// Load fix for Deferred Twig Extension
$loader->addPsr4('Phive\\Twig\\Extensions\\Deferred\\', LIB_DIR . 'Phive/Twig/Extensions/Deferred/', true);
}
} elseif ($values) {
$instance = self::$instance;
foreach ($values as $key => $value) {

View File

@@ -10,6 +10,7 @@
namespace Grav\Common\Processors;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Framework\RequestHandler\Exception\RequestException;
use Grav\Plugin\Form\Forms;
use RocketTheme\Toolbox\Event\Event;
use Psr\Http\Message\ResponseInterface;
@@ -48,10 +49,14 @@ class PagesProcessor extends ProcessorBase
$page = $this->container['page'];
if (!$page->routable()) {
$exception = new RequestException($request, 'Page Not Found', 404);
$route = $this->container['route'];
// If no page found, fire event
$event = new Event([
'page' => $page,
'code' => $exception->getCode(),
'message' => $exception->getMessage(),
'exception' => $exception,
'route' => $route,
'request' => $request
]);

View File

@@ -0,0 +1,19 @@
<?php
/**
* @package Grav\Common\Twig\Exception
*
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
* @license MIT License; see LICENSE file for details.
*/
namespace Grav\Common\Twig\Exception;
/**
* TwigException gets thrown when you use {% throw code message %} in twig.
*
* This allows Grav to catch 401, 403 and 404 exceptions and display proper error page.
*/
class TwigException extends \RuntimeException
{
}

View File

@@ -43,7 +43,7 @@ class TwigNodeThrow extends Node
$compiler->addDebugInfo($this);
$compiler
->write('throw new \RuntimeException(')
->write('throw new \Grav\Common\Twig\Exception\TwigException(')
->subcompile($this->getNode('message'))
->write(', ')
->write($this->getAttribute('code') ?: 500)

View File

@@ -16,6 +16,7 @@ use Grav\Common\Language\Language;
use Grav\Common\Language\LanguageCodes;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Pages;
use Grav\Common\Twig\Exception\TwigException;
use Grav\Common\Twig\Extension\FilesystemExtension;
use Grav\Common\Twig\Extension\GravExtension;
use Grav\Common\Utils;
@@ -26,6 +27,7 @@ use RuntimeException;
use Twig\Cache\FilesystemCache;
use Twig\Environment;
use Twig\Error\LoaderError;
use Twig\Error\RuntimeError;
use Twig\Extension\CoreExtension;
use Twig\Extension\DebugExtension;
use Twig\Extension\StringLoaderExtension;
@@ -404,38 +406,63 @@ class Twig
*/
public function processSite($format = null, array $vars = [])
{
// set the page now its been processed
$this->grav->fireEvent('onTwigSiteVariables');
/** @var Pages $pages */
$pages = $this->grav['pages'];
/** @var PageInterface $page */
$page = $this->grav['page'];
$content = $page->content();
$twig_vars = $this->twig_vars;
$twig_vars['theme'] = $this->grav['config']->get('theme');
$twig_vars['pages'] = $pages->root();
$twig_vars['page'] = $page;
$twig_vars['header'] = $page->header();
$twig_vars['media'] = $page->media();
$twig_vars['content'] = $content;
// determine if params are set, if so disable twig cache
$params = $this->grav['uri']->params(null, true);
if (!empty($params)) {
$this->twig->setCache(false);
}
// Get Twig template layout
$template = $this->getPageTwigTemplate($page, $format);
$page->templateFormat($format);
try {
$grav = $this->grav;
// set the page now its been processed
$grav->fireEvent('onTwigSiteVariables');
/** @var Pages $pages */
$pages = $grav['pages'];
/** @var PageInterface $page */
$page = $grav['page'];
$twig_vars = $this->twig_vars;
$twig_vars['theme'] = $grav['config']->get('theme');
$twig_vars['pages'] = $pages->root();
$twig_vars['page'] = $page;
$twig_vars['header'] = $page->header();
$twig_vars['media'] = $page->media();
$twig_vars['content'] = $page->content();
// determine if params are set, if so disable twig cache
$params = $grav['uri']->params(null, true);
if (!empty($params)) {
$this->twig->setCache(false);
}
// Get Twig template layout
$template = $this->getPageTwigTemplate($page, $format);
$page->templateFormat($format);
$output = $this->twig->render($template, $vars + $twig_vars);
} catch (LoaderError $e) {
$error_msg = $e->getMessage();
throw new RuntimeException($error_msg, 400, $e);
throw new RuntimeException($e->getMessage(), 400, $e);
} catch (RuntimeError $e) {
$prev = $e->getPrevious();
if ($prev instanceof TwigException) {
$code = $prev->getCode() ?: 500;
// Fire onPageNotFound event.
$event = new Event([
'page' => $page,
'code' => $code,
'message' => $prev->getMessage(),
'exception' => $prev,
'route' => $grav['route'],
'request' => $grav['request']
]);
$event = $grav->fireEvent("onDisplayErrorPage.{$code}", $event);
$newPage = $event['page'];
if ($newPage && $newPage !== $page) {
unset($grav['page']);
$grav['page'] = $newPage;
return $this->processSite($newPage->templateFormat(), $vars);
}
}
throw $e;
}
return $output;

View File

@@ -0,0 +1,70 @@
<?php
// Fix too many ob_get_clean() calls when exception is thrown inside the template.
namespace Phive\Twig\Extensions\Deferred;
class DeferredExtension extends \Twig_Extension
{
/**
* @var array
*/
private $blocks = array();
/**
* {@inheritdoc}
*/
public function getTokenParsers()
{
return array(new DeferredTokenParser());
}
/**
* {@inheritdoc}
*/
public function getNodeVisitors()
{
return array(new DeferredNodeVisitor());
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'deferred';
}
public function defer(\Twig_Template $template, $blockName)
{
ob_start();
$templateName = $template->getTemplateName();
$this->blocks[$templateName][] = [ob_get_level(), $blockName];
}
public function resolve(\Twig_Template $template, array $context, array $blocks)
{
$templateName = $template->getTemplateName();
if (empty($this->blocks[$templateName])) {
return;
}
while ($block = array_pop($this->blocks[$templateName])) {
[$level, $blockName] = $block;
if (ob_get_level() !== $level) {
continue;
}
$buffer = ob_get_clean();
$blocks[$blockName] = array($template, 'block_'.$blockName.'_deferred');
$template->displayBlock($blockName, $context, $blocks);
echo $buffer;
}
if ($parent = $template->getParent($context)) {
$this->resolve($parent, $context, $blocks);
}
}
}