V Error4×xPresenteru nefunguje handle<signal> komponenty

m.brecher
Generous Backer | 736
+
0
-

Mám tento problém – v Error4×xPresenteru nefunguje handle<signal> komponenty.

V layout.latte mám vloženo odhlašovací tlačítko, pro odhlášení signálem komponenty, a uživatel zůstane na stejné stránce (Presenter:action se nezmění).

layout.latte:

{if $user->isLoggedIn()}
	{control logoutButton}
{else}
	<a n:href="User:default" class="login-button">Přihlásit</a>
{/if}

Odhlašovací tlačítko vyrábím jako komponentu tovární metodou v BasePresenteru:

    public function createComponentLogoutButton()
    {
        return new App\Components\LogoutButton;
    }

Třída LogoutButton – obsahuje handler pro obsluhu signálu logoutButton:logout! k volání handleru ale nedojde

class LogoutButton extends Component
{
    public function  render()
    {
        $this->presenter->template->render(__DIR__.'/templates/logoutButton.latte');
    }

    public function  handleLogout(): void
    {
        $this->presenter->user->logout();
        $this->presenter->redirect('this');
    }
}

Šablona tlačítka logoutButton.latte:

<a n:href="logoutButton:logout!" class="login-button">Odhlásit!</a>

Chci, aby se při BadRequestException vykreslil celý layout webu včetně menu. Výjimka BadRequestException skončí nakonec u Error4×xPresenteru. V layoutu je vloženo menu, které se generuje z dat v databázi a odhlašovací tlačítko. Chybová šablona 404.latte se vykreslí do standardního layoutu layout.latte a aby byly k dispozici data, dědí ErrorXxxPresenter z BasePresenteru, kde využije metodu injektXxx() a beforeRender(), kde se závislosti předají do layoutu. Děděním předejdu duplikování kódu, i když s sebou možná nese drobné riziko ohledně bezpečnosti (?)

final class Error4xxPresenter extends BasePresenter
{
   .......
}

Všechno to funguje, až na situaci, když se vyvolá při testování BadRequestException např. záměrně chybějící šablonou akce. Vzniknou hned tři problémy:

  1. Tlačítko neodkazuje na původní presenter, ale na Error4×xPresenter.
  2. Po kliknutí na odhlašovací tlačítko se v presenteru vyvolá signál logoutButton:logout, nedojde ale k zavolání LogoutButton::handleLogout()
  3. $this->presenter->redirect(‚this‘) v handleru signálu tlačítku by přesměroval na aktuální presenter a akci, což je Error4×x, k tomu sice díky chybám ani nedošlo, ale i toto není dobré

Pokud není BadRequestException vš funguje ok, když se vyvolá BadRequestException, tlačítko se vykreslí, signál je presenterem správně zachycen, ale nezavolá se LogoutButton::handleLogout().

Odhaduji, že framework nette nedovolí zpracovat signál, když se presenter forwarduje na další presenter – asi z bezpečnostních důvodů (?)

Zkusil jsem ještě toto, ale není to v Nette dovoleno:

<a n:href=":Presenter:akce:logoutButton:logout!" class="login-button">Odhlásit!</a>

Zřejmě není úplně dobrý nápad při BadRequestException se pokoušet odhlásit. Tak asi při vyvolání Error4×xPresenteru v layout.latte odstraním vykreslení tlačítka a bude to.

K uvedené problematice mám dva dotazy:

  1. Není chybou z pohledu best practice a z pohledu bezpečnosti podědit Error4×xPresenter extends BasePresenter?
  2. Je zablokování signálu v Error4×xPresenteru správné chování frameworku, nebo tam mám nějakou chybu a mělo by to fungovat?

Díky za případné nápady.

Editoval m.brecher (24. 9. 2021 3:25)

m.brecher
Generous Backer | 736
+
0
-

Nakonec jsem dospěl k názoru, že ErrorPresenter by neměl z BasePresenteru dědit z bezpečnostních důvodů.

Error4×xPresenter jsem odvodil z Nette\Application\UI\Presenter, závislost pro vykreslení menu v layout.latte jsem předal metodou inject<component>() a problematickou komponentu logoutButton jsem zrušil úplně:

final class Error4xxPresenter extends Nette\Application\UI\Presenter  // zrušeno dědění z BasePresenteru
{
    private App\Model\SectionModel $sectionModel;

    public function injectSectionModel(App\Model\SectionModel $sectionModel): void  // získání závislosti pro layout.latte
    {
        $this->sectionModel = $sectionModel;    // pro @layout.latte
    }
	public function renderDefault( ... ): void
    {
        $this->template->sections = $this->sectionModel->getSectionList();  // předání závislosti do layout.latte
	    .......
	}
}

Neexistenci komponenty logoutButton v layout.latte při volání ErrrorPresenteru jsem ošetřil takto:

{if isset($presenter['logoutButton'])}{control logoutButton}{/if}
corben
Člen | 4
+
0
-

Řeším ten samý problém a nevím si s tím rady. Já na férovku (jako znouzecnost řešení) dal natvrdo link: <a href=„/?do=navigation-openModal“ class=„ajax“>…, kdy volám handler by správně měl být <a href=„{link openModal!}“…, když volám URL kde není 404 tak to funguje.

Prakticky by stačilo, kdyby se dal podvrhnout v ErrorPresenteru komponentám jiný presenter.

Marek Bartoš
Nette Blogger | 1165
+
+1
-

Problém je, že Nette nedovoluje mít na error presenter routu a tak v něm ani nefungují „this“ odkazy. Trik je v tom v interním error presenteru jen forwardovat na jiný, routovatelný presenter.

Inspirovat se můžeš tady https://github.com/…8f/src/Error
Důležité jsou UI\ErrorForwardPresenter, UI\ErrorPresenterUtil a Public\ErrorPresenter

ErrorForwardPresenter je ten hlavní, registrovaný do Nette. Stará se o logování chyb a přesměrování na jiný error presenter.
Public\ErrorPresenter pak jen (skrze použitou traitu) vykresluje chybou stránku podle kódu exception, případně simuluje chybu při přímém přístupu uživatelem

corben
Člen | 4
+
0
-

Díky moc, vyzkouším :)

m.brecher
Generous Backer | 736
+
0
-

Ahoj @MarekBartoš a @corben

nemám teď tolik času, ale zkusil jsem ke svému Error4×xPresenteru přidat odpovídající routu:

    public static function createRouter(): RouteList
    {
        $router = new RouteList;

        $router->addRoute('page/not-found', 'Error4xx:default');

        .......

        return $router;
    }

a Latte mě na neexistující stránce v layoutu který Error4×xPresenter používá vykresluje odkaz se signálem na komponentu:

<a n:href="out!" class="box-link">Odhlásit</a>

Vykreslené html:

<a class="box-link" href="/dev/project/page-not-found?do=loginButton-out">Odhlásit</a>

Tlačítko sice nefunguje, ale vzhledem k tomu, že se link na signál vykresluje bezpochyby to půjde nějak dodělat. Díky moc @MarekBartoš za navedení správným směrem.

Ošetřovat přímý vstup na url pro 404 není nutné, protože se tam vykreslí Page Not Found stránka.

Ale asi bude užitečné sem dát moje řešení – je totiž zcela jednoduché. Jiným uživatelům Nette by se mohlo hodit. Dám zítra, musím kód upravit.

Editoval m.brecher (5. 10. 2022 1:56)

m.brecher
Generous Backer | 736
+
0
-

Další testování ErrorPresenteru a Error4×xPresenteru přineslo důležité poznatky:

Jak jdou jednotlivé requesty při výjimce za sebou jsem prozkoumal v Tracy panelu DIC ve službě application.application, parametr requests.

ErrorPresenter obdrží Nette\Application\Request s method = ‚FORWARD‘

Error4×xPresenter také obdrží Nette\Application\Request s method = ‚FORWARD‘

Mám to v projektu tak, že Error4×xPresenter dědí z BasePresenter, tím podědí i všechny globální komponenty a služby pro ně:

final class Error4xxPresenter extends BasePresenter
{
	public function startup(): void
	{
		parent::startup();
		if (!$this->getRequest()->isMethod(Request::FORWARD)) {
			$this->error();
		}
	}

    public function beforeRender()
    {
        parent::beforeRender();
        $this->setLayout('error-layout');      // @error-layout dědí z @base-layout ;)
        $this->template->robotIndex = false;   // při 404 nadbytečné, ale raději to tam dávám
    }

    public function renderDefault(?BadRequestException $exception = null): void
    {
        $this->template->httpCode = $exception?->getCode()? 404; // toto je VELMI VELMI důležité, GET/POST => 404 !!!
        $this->template->setFile($this->appDir . '/templates/Error/400.latte');  // používám jednu šablonu pro všechno
    }
}

Když přidám routu jak jsem ukázal kód o příspěvek výše, tak generování linků na signály funguje, ale co nefunguje je zpracování v presenteru handle<Signal>. Blokování provádí Nette Framework a sice kvůli property method=‚FORWARD‘ Nette\Application\Request-u který je v Error4×xPresenter-u. Když půjdeme na routu Error4×xPresenter-u přímo z url, tak to zase skončí chybou kvůli tomuhle:

	public function startup(): void
	{
		parent::startup();
		if (!$this->getRequest()->isMethod(Request::FORWARD)) {
			$this->error();	          // kód z nette/web-project nepustí požadavek GET do error presenteru - proč?
		}
	}

Vyhození výjimky $presenter->error() jsem zakomentoval a ověřil, že přístupem z url, kdy je method=‚GET‘ zpracování signálů funguje.

Nojo, jenže Nette Framework automaticky podstrčí do forwardnutého presenteru request s metodou ‚FORWARD‘. To je naprosto v pořádku, takhle to být asi musí, ale problém je ten, že se tím na jedné straně zablokují signály, ale v dokumentaci Nette je forward na jiný presenter prezentován jako legální způsob jak přehodit zpracování requestu na jiný presenter víceméně na úrovni přesměrování $presenter->redirect(). Bezpochyby by bylo dobré do dokumentace vysvětlit, jaký je zamýšlený účel forwardu na jiný presenter a zda by se měl/neměl běžně používat + upozornit na to, že při forwardu jsou signály blokované (kvůli bezpečnosti?). Já si netroufám tohle do dokumentace zodpovědně psát, protože jde o zpracování výjimek a otázku bezpečnosti. Zde je nezbytná součinnost autora @DavidGrudl , ten ví proč při ‚FORWARD‘ blokuje Framework signály.

Takže, necháme forward z ErrorPresenteru na Error4×xPresenter být jak je a zaměříme se na to, jak zprovoznit signály. To je naštěstí velmi, velmi jednoduché. Protože díky routě Error4×xPresenter-u se generují GET linky na Error4×xPresenter, tak signály jdou přímo metodou GET (formuláře POST) na Error4×xPresenter. Ten jak jsem si udělal dědí z BasePresenteru a tam obsluha signálů je. Takže nechme požadavky GET/POST žít:

	public function startup(): void
	{
		parent::startup();
		if (!$this->getRequest()->isMethod(Request::FORWARD)) {
//			$this->error();   // necháme projít i požadavky GET/POST
		}
	}

a to je všechno. Máme velmi jednoduchý a nativní způsob jak zajistit, aby na 404 stránce byl normální layout včetně zpracování signálů komponent i formulářů. Netestoval jsem jestli i ajaxové signály fungují, ale proč by neměly?

Blokovat signály pro ErrorPresenter je jasné, ale pokud člověk ví co dělá a dá si pozor, tak mě nenapadá důvod proč blokovat na vstupu do Error4×xPresenteru GET i POST a proč by nemohl mít Error4×xPresenter svoji routu. Tak jako tak se na té GET/POST routě uživateli vykreslí 404 – to je právě naprosto klíčové, člověk musí naprosto přesně vědět, že musí Error4×xPresenter dobře zabezpečit.

Současně jsem v kódu nette/web-project skeletonu objevil, že autor skeletonu pro nette/web-project asi zamýšlel, že by ErrorPresenter měl z requestu vysosat modul a separátor a forwardnout pro každý modul na samostatný Error4×xPresenter. To je skvělá myšlenka, protože pak je naprosto jednoduché, aby každý modul vykresloval svoje 404 do svého layoutu a 404stránky se mohly modul od modulu řešit individuálně.

Jenže je tam CHYBA (pokud se samozřejmě nemýlím já, což je dost často), protože se sosá jméno modulu ze špatného requestu – je potřeba jít o jeden request v historii requestů zpátky. Takhle jak to tam je udělané vyjde vždycky aktuální presenter Error a modul žádný (při standardní konfiguraci v common.neon). Sice to funguje, ale ne tak, jak bylo zamýšleno.

Správně by to mělo vypadat nějak takhle:

final class ErrorPresenter implements Nette\Application\IPresenter
{
	use Nette\SmartObject;

	public function __construct(
        private ILogger $logger,
    )
	{}

	public function run(Application\Request $request): Application\Response
	{
		$exception = $request->getParameter('exception');
        if ($exception instanceof BadRequestException) {
            $originalRequest = $request->getParameter('request');  // jdeme o jeden request v historii zpátky nette/web-project to nemá!!
			[$module, , $sep] = Helpers::splitName($originalRequest->getPresenterName());  // zde získáme modul, kde došlo k výjimce
            return new Responses\ForwardResponse($request->setPresenterName($module . $sep . 'Error4xx'));
		}
	.........
	}
}

Budu rád, když se k problematice ErrorPresenteru vyjádří další zkušení znalci Nette.

m.brecher
Generous Backer | 736
+
0
-

Ještě mě napadlo, že v řešení ErrorPresenteru jak jsem ho nastínil výše – tj. že necháme přímé http požadavky GET/POST na routu Error4×xPresenteru projít a ošetříme to tak, že zajistíme, aby těmto požadavkům se vrátila korektní 404 stránka je asi jedno skryté nebezpečí. Vlastně to nesouvisí s propuštěním GET/POST požadavků, ale s následným děděním Error4×xPresenteru ze standardního BasePresenteru a vykreslování standardního layoutu webu včetně všech komponent, kde jsou různé ajaxové subkomponenty apod… Totiž, že by se uvnitř takto komplikované 404 stránky opět vyhodila 404. Obávám se, aby nenastala interní smyčka např.: 404 stránka ⇒ standardní layout webu ⇒ ajaxová komponenta ⇒ chyba 404 v komponentě ⇒ ErrorPresenter(FORWARD) ⇒ Error4×xPresenter(FORWARD) ⇒ 404 stránka ⇒ standardní layout webu ⇒ ajaxová komponenta .....

Přemýšlím, že tím, že Nette blokuje zpracování signálů pro presentery s metodou FORWARD se možná i předchází eventuálním potížím se zpracováním signálů a snižuje se riziko výše popsané interní smyčky.

Takže můj momentální závěr je – zvážit, zda je nutné v layoutu 404 stránky používat jakékoliv složité prvky, jestli není lepší je odstranit a vykreslit standardní layout webu bez jakýchkoliv komponent, potom není potřeba žádná routa na Error4×xPresenter a můžeme nechat standardní http požadavky GET/POST na Error4×xPresenter zablokované. Bezpečnost je hodně o jednoduchosti.

Marek Bartoš
Nette Blogger | 1165
+
0
-

Když se vyhodí exception v error presenteru, tak to způsobí fallback na interní error presenter v Nette.

m.brecher
Generous Backer | 736
+
0
-

Marek Bartoš napsal(a):

Když se vyhodí exception v error presenteru, tak to způsobí fallback na interní error presenter v Nette.

Ano souhlasím – a to by mohlo způsobit interní nekonečnou smyčku jak píšu nahoře, proto je potřeba:

  • zamezit vzniku 404 exception v error presenteru tím, že 404 stránka bude velmi jednoduchá
  • ošetřit v 404 stránce a jejím layoutu potenciální místa (komponenty), kde by další 404 exception mohla vzniknout např try/catch a při chybě takovou problematickou komponentu vůbec nevykreslovat

Editoval m.brecher (5. 10. 2022 10:53)

Marek Bartoš
Nette Blogger | 1165
+
0
-

Nezpůsobí. Skončí to na interním error presenteru, co je přímo v Nette. Ten tvůj to znova nezkusí.

Editoval Marek Bartoš (5. 10. 2022 11:39)

m.brecher
Generous Backer | 736
+
0
-

Marek Bartoš napsal(a):

Nezpůsobí. Skončí to na interním error presenteru, co je přímo v Nette. Ten tvůj to znova nezkusí.

Ano, máš pravdu, zkusil jsem co se stane, když vyvolám výjimku ve forwardnutém Error4×xPresenteru:

final class Error4xxPresenter extends FrontPresenter
{
	public function startup(): void
	{
		parent::startup();
        $this->error();	          // testovací vyvolání 404
		if (!$this->getRequest()->isMethod(Request::FORWARD)) {
			$this->error();
		}
	}

    .....

}

A v developer módu se vyhodí normální BadRequestException a zobrazí se chybová stránka Tracy i když ji máme v common.neon vypnutou:

application:
	errorPresenter: Error

	mapping:
		*: App\*\Presenters\*Presenter

	catchExceptions: true	# vypnutí tracy pro účely ladění 404 stránky

A když se přepne developer mód na production tak se vyhodí 500.

OK, dobře vymyšleno, takže Nette takhle pěkně myslí i na případné výjimky v samotných error presenterech.

Editoval m.brecher (5. 10. 2022 15:26)

m.brecher
Generous Backer | 736
+
0
-

Ještě připojuji poznámku k nette/web-project ErrorPresenteru – jak jsem výše psal, že je potřeba pro zjištění modulu ve kterém nastal upravit kód ErrorPresenteru a jít o jeden request v historii zpět, tak to není tak jednoduché. Záleží na tom, jak kde BadRequestException vznikne. Pokud ji vyhodí nějaký presenter aplikace, třeba na základě neexistence záznamu v databázi, tak moje oprava funguje. Ale BadRequestException může vyhodit i Router ještě předtím, než se presenter vytvoří a žádný request se nevytvoří. Potom máme v historii služby application.application jenom jeden request a sice ten s forwardovaným ErrorPresenterem. Potom můj kód nefunguje. Takže pokud by se to mělo takto použít, bylo by potřeba ještě ošetřit případ, kdy se request vůbec nevytvoří a zřejmě v tomto případě forwardovat na nějaký preferovaný modul.

Marek Bartoš
Nette Blogger | 1165
+
0
-

Takže pokud by se to mělo takto použít, bylo by potřeba ještě ošetřit případ, kdy se request vůbec nevytvoří a zřejmě v tomto případě forwardovat na nějaký preferovaný modul.

Pokud je request null, tak se přesměruje na error ve veřejné části webu
Viz https://github.com/…resenter.php#…

m.brecher
Generous Backer | 736
+
0
-

@MarekBartoš Díky za trpělivost se mnou.