• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nette / application / 27919019709

21 Jun 2026 10:08PM UTC coverage: 84.111% (+0.05%) from 84.059%
27919019709

push

github

dg
phpstan fix

2038 of 2423 relevant lines covered (84.11%)

0.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.17
/src/Application/UI/Presenter.php
1
<?php declare(strict_types=1);
1✔
2

3
/**
4
 * This file is part of the Nette Framework (https://nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
namespace Nette\Application\UI;
9

10
use Nette;
11
use Nette\Application;
12
use Nette\Application\Helpers;
13
use Nette\Application\LinkGenerator;
14
use Nette\Application\Responses;
15
use Nette\Http;
16
use Nette\Utils\Arrays;
17
use function array_slice, count, dirname, func_get_args, func_num_args, implode, in_array, is_array, is_dir, is_file, is_string, ltrim, preg_match, preg_replace, str_starts_with, strcasecmp, strlen, strncmp, strpos, strrpos, strtr, substr, substr_count, trigger_error, ucfirst;
18
use const DIRECTORY_SEPARATOR;
19

20

21
/**
22
 * Presenter component represents a webpage instance. It converts Request to Response.
23
 *
24
 * @property-read Nette\Application\Request $request
25
 * @property-read string $action
26
 * @property      string $view
27
 * @property      string|bool $layout
28
 * @property-read \stdClass $payload
29
 * @property-read Nette\Http\Session $session
30
 * @property-read Nette\Security\User $user
31
 */
32
abstract class Presenter extends Control implements Application\IPresenter
33
{
34
        /** bad link handling {@link Presenter::$invalidLinkMode} */
35
        public const
36
                InvalidLinkSilent = 0b0000,
37
                InvalidLinkWarning = 0b0001,
38
                InvalidLinkException = 0b0010,
39
                InvalidLinkTextual = 0b0100;
40

41
        /** @internal special parameter key */
42
        public const
43
                PresenterKey = 'presenter',
44
                SignalKey = 'do',
45
                ActionKey = 'action',
46
                FlashKey = '_fid',
47
                DefaultAction = 'default';
48

49
        /** @deprecated use Presenter::InvalidLinkSilent */
50
        public const INVALID_LINK_SILENT = self::InvalidLinkSilent;
51

52
        /** @deprecated use Presenter::InvalidLinkWarning */
53
        public const INVALID_LINK_WARNING = self::InvalidLinkWarning;
54

55
        /** @deprecated use Presenter::InvalidLinkException */
56
        public const INVALID_LINK_EXCEPTION = self::InvalidLinkException;
57

58
        /** @deprecated use Presenter::InvalidLinkTextual */
59
        public const INVALID_LINK_TEXTUAL = self::InvalidLinkTextual;
60

61
        /** @deprecated use Presenter::PresenterKey */
62
        public const PRESENTER_KEY = self::PresenterKey;
63

64
        /** @deprecated use Presenter::SignalKey */
65
        public const SIGNAL_KEY = self::SignalKey;
66

67
        /** @deprecated use Presenter::ActionKey */
68
        public const ACTION_KEY = self::ActionKey;
69

70
        /** @deprecated use Presenter::FlashKey */
71
        public const FLASH_KEY = self::FlashKey;
72

73
        /** @deprecated use Presenter::DefaultAction */
74
        public const DEFAULT_ACTION = self::DefaultAction;
75

76
        public int $invalidLinkMode = self::InvalidLinkSilent;
77

78
        /** @var array<callable(static): void>  Occurs before starup() */
79
        public array $onStartup = [];
80

81
        /** @var array<callable(static): void>  Occurs before render*() and after beforeRender() */
82
        public array $onRender = [];
83

84
        /** @var array<callable(static, Application\Response): void>  Occurs before shutdown() */
85
        public array $onShutdown = [];
86

87
        /** automatically call canonicalize() */
88
        public bool $autoCanonicalize = true;
89

90
        /** use absolute Urls or paths? */
91
        public bool $absoluteUrls = false;
92

93
        /**
94
         * @var list<string>
95
         * @deprecated  use #[Requires(methods: ...)] to specify allowed methods
96
         */
97
        public array $allowedMethods = ['GET', 'POST', 'HEAD', 'PUT', 'DELETE', 'PATCH'];
98
        private ?Nette\Application\Request $request = null;
99
        private ?Nette\Application\Response $response = null;
100

101
        /** @var array<string, array<string, mixed>> */
102
        private array $globalParams = [];
103

104
        /** @var array<string, mixed> */
105
        private array $globalState;
106

107
        /** @var ?array<string, string|false> */
108
        private ?array $globalStateSinces;
109
        private string $action = '';
110
        private string $view = '';
111
        private bool $forwarded = false;
112
        private string|bool $layout = '';
113
        private \stdClass $payload;
114
        private string $signalReceiver;
115
        private ?string $signal = null;
116
        private bool $ajaxMode;
117
        private bool $startupCheck = false;
118
        private readonly Nette\Http\IRequest $httpRequest;
119
        private readonly Nette\Http\IResponse $httpResponse;
120
        private readonly ?Nette\Http\Session $session;
121
        private readonly ?Nette\Security\User $user;
122
        private readonly ?TemplateFactory $templateFactory;
123
        private readonly LinkGenerator $linkGenerator;
124

125

126
        public function __construct()
127
        {
128
        }
1✔
129

130

131
        final public function getRequest(): ?Application\Request
132
        {
133
                return $this->request;
1✔
134
        }
135

136

137
        /**
138
         * @return ($throw is true ? static : static)
139
         */
140
        final public function getPresenter(bool $throw = true): static
1✔
141
        {
142
                return $this;
1✔
143
        }
144

145

146
        /** @deprecated */
147
        final public function getPresenterIfExists(): static
148
        {
149
                return $this;
×
150
        }
151

152

153
        /** @deprecated */
154
        final public function hasPresenter(): bool
155
        {
156
                return true;
×
157
        }
158

159

160
        /**
161
         * Returns a name that uniquely identifies component.
162
         */
163
        public function getUniqueId(): string
164
        {
165
                return '';
1✔
166
        }
167

168

169
        /**
170
         * Checks whether the current presenter belongs to the given module.
171
         */
172
        public function isModuleCurrent(string $module): bool
1✔
173
        {
174
                $current = Helpers::splitName((string) $this->getName())[0];
1✔
175
                return str_starts_with($current . ':', ltrim($module . ':', ':'));
1✔
176
        }
177

178

179
        /**
180
         * Checks whether the current request was forwarded from another presenter or action.
181
         */
182
        public function isForwarded(): bool
183
        {
184
                return $this->forwarded || $this->request?->isMethod($this->request::FORWARD);
1✔
185
        }
186

187

188
        /********************* interface IPresenter ****************d*g**/
189

190

191
        public function run(Application\Request $request): Application\Response
1✔
192
        {
193
                $this->request = $request;
1✔
194
                $this->setParent($this->getParent(), $request->getPresenterName());
1✔
195

196
                if (!$this->httpResponse->isSent()) {
1✔
197
                        $this->httpResponse->addHeader('Vary', 'X-Requested-With');
1✔
198
                }
199

200
                $this->initGlobalParameters();
1✔
201

202
                try {
203
                        // CHECK REQUIREMENTS
204
                        (new AccessPolicy(static::getReflection()))->checkAccess($this);
1✔
205
                        $this->checkRequirements(static::getReflection());
1✔
206
                        $this->checkHttpMethod();
1✔
207

208
                        // STARTUP
209
                        Arrays::invoke($this->onStartup, $this);
1✔
210
                        $this->startup();
1✔
211
                        if (!$this->startupCheck) {
1✔
212
                                $class = static::getReflection()->getMethod('startup')->getDeclaringClass()->getName();
×
213
                                throw new Nette\InvalidStateException("Method $class::startup() or its parents doesn't call parent::startup().");
×
214
                        }
215

216
                        // calls $this->action<Action>()
217
                        try {
218
                                actionMethod:
219
                                $this->tryCall(static::formatActionMethod($this->action), $this->params);
1✔
220
                        } catch (Application\SwitchException $e) {
1✔
221
                                $this->changeAction($e->getMessage());
1✔
222
                                $this->autoCanonicalize = false;
1✔
223
                                goto actionMethod;
1✔
224
                        }
225

226
                        // autoload components
227
                        foreach ($this->globalParams as $id => $foo) {
1✔
228
                                $this->getComponent($id, throw: false);
1✔
229
                        }
230

231
                        if ($this->autoCanonicalize) {
1✔
232
                                $this->canonicalize();
1✔
233
                        }
234

235
                        if ($this->httpRequest->isMethod('head')) {
1✔
236
                                $this->terminate();
×
237
                        }
238

239
                        // SIGNAL HANDLING
240
                        // calls $this->handle<Signal>()
241
                        $this->processSignal();
1✔
242

243
                        // RENDERING VIEW
244
                        $this->beforeRender();
1✔
245
                        Arrays::invoke($this->onRender, $this);
1✔
246
                        // calls $this->render<View>()
247
                        try {
248
                                renderMethod:
249
                                $this->tryCall(static::formatRenderMethod($this->view), $this->params);
1✔
250
                        } catch (Application\SwitchException $e) {
1✔
251
                                $this->setView($e->getMessage());
×
252
                                goto renderMethod;
×
253
                        }
254
                        $this->afterRender();
1✔
255

256
                        // finish template rendering
257
                        $this->sendTemplate();
1✔
258

259
                } catch (Application\SwitchException $e) {
1✔
260
                        throw new \LogicException('Switch is only allowed inside action*() or render*() method.', 0, $e);
×
261
                } catch (Application\AbortException) {
1✔
262
                }
263

264
                // save component tree persistent state
265
                $this->saveGlobalState();
1✔
266

267
                if ($this->isAjax()) {
1✔
268
                        $this->getPayload()->state = $this->getGlobalState();
1✔
269
                        try {
270
                                if ($this->response instanceof Responses\TextResponse && $this->isControlInvalid()) {
1✔
271
                                        $this->snippetMode = true;
×
272
                                        $this->response->send($this->httpRequest, $this->httpResponse);
×
273
                                        $this->sendPayload();
1✔
274
                                }
275
                        } catch (Application\AbortException) {
×
276
                        }
277
                }
278

279
                if ($this->hasFlashSession()) {
1✔
280
                        $this->getFlashSession()->setExpiration('30 seconds');
1✔
281
                }
282

283
                if (!$this->response) {
1✔
284
                        $this->response = new Responses\VoidResponse;
1✔
285
                }
286

287
                Arrays::invoke($this->onShutdown, $this, $this->response);
1✔
288
                $this->shutdown($this->response);
1✔
289

290
                return $this->response;
1✔
291
        }
292

293

294
        /**
295
         * Called before action method. Override to run initialization common to all actions.
296
         * @return void
297
         */
298
        protected function startup()
299
        {
300
                $this->startupCheck = true;
1✔
301
        }
1✔
302

303

304
        /**
305
         * Called before the view is rendered. Override to set up common template variables.
306
         * @return void
307
         */
308
        protected function beforeRender()
309
        {
310
        }
1✔
311

312

313
        /**
314
         * Called after the view is rendered. Override for post-render processing.
315
         */
316
        protected function afterRender(): void
317
        {
318
        }
1✔
319

320

321
        /**
322
         * Called after the response is ready. Override for cleanup after request handling.
323
         */
324
        protected function shutdown(Application\Response $response): void
1✔
325
        {
326
        }
1✔
327

328

329
        /**
330
         * This method will be called when CSRF is detected.
331
         */
332
        public function detectedCsrf(): void
333
        {
334
                try {
335
                        $this->redirect('this');
1✔
336
                } catch (InvalidLinkException $e) {
1✔
337
                        $this->error($e->getMessage());
×
338
                }
339
        }
340

341

342
        /** @deprecated  use #[Requires(methods: ...)] to specify allowed methods */
343
        protected function checkHttpMethod(): void
344
        {
345
                if ($this->allowedMethods &&
1✔
346
                        !in_array($method = $this->httpRequest->getMethod(), $this->allowedMethods, strict: true)
1✔
347
                ) {
348
                        $this->httpResponse->setHeader('Allow', implode(',', $this->allowedMethods));
×
349
                        $this->error("Method $method is not allowed", Nette\Http\IResponse::S405_MethodNotAllowed);
×
350
                }
351
        }
1✔
352

353

354
        /********************* signal handling ****************d*g**/
355

356

357
        /**
358
         * @throws BadSignalException
359
         */
360
        public function processSignal(): void
361
        {
362
                if (!isset($this->signal)) {
1✔
363
                        return;
1✔
364
                }
365

366
                $component = $this->signalReceiver === ''
1✔
367
                        ? $this
1✔
368
                        : $this->getComponent($this->signalReceiver, throw: false);
1✔
369
                if ($component === null) {
1✔
370
                        throw new BadSignalException("The signal receiver component '$this->signalReceiver' is not found.");
1✔
371

372
                } elseif (!$component instanceof SignalReceiver) {
1✔
373
                        throw new BadSignalException("The signal receiver component '$this->signalReceiver' is not SignalReceiver implementor.");
×
374
                }
375

376
                $component->signalReceived($this->signal);
1✔
377
                $this->signal = null;
1✔
378
        }
1✔
379

380

381
        /**
382
         * Returns pair signal receiver and name.
383
         * @return ?array{string, string}
384
         */
385
        final public function getSignal(): ?array
386
        {
387
                return $this->signal === null ? null : [$this->signalReceiver, $this->signal];
1✔
388
        }
389

390

391
        /**
392
         * Checks if the signal receiver is the given one.
393
         */
394
        final public function isSignalReceiver(
395
                Nette\ComponentModel\Component|string $component,
396
                string|bool|null $signal = null,
397
        ): bool
398
        {
399
                if ($component instanceof Nette\ComponentModel\Component) {
×
400
                        $component = $component === $this
×
401
                                ? ''
×
402
                                : $component->lookupPath(self::class);
×
403
                }
404

405
                if ($this->signal === null) {
×
406
                        return false;
×
407

408
                } elseif ($signal === true) {
×
409
                        return $component === ''
×
410
                                || strncmp($this->signalReceiver . '-', $component . '-', strlen($component) + 1) === 0;
×
411

412
                } elseif ($signal === null) {
×
413
                        return $this->signalReceiver === $component;
×
414
                }
415

416
                return $this->signalReceiver === $component && strcasecmp((string) $signal, $this->signal) === 0;
×
417
        }
418

419

420
        /********************* rendering ****************d*g**/
421

422

423
        /**
424
         * Returns current action name.
425
         */
426
        final public function getAction(bool $fullyQualified = false): string
1✔
427
        {
428
                return $fullyQualified
1✔
429
                        ? ':' . $this->getName() . ':' . $this->action
×
430
                        : $this->action;
1✔
431
        }
432

433

434
        /**
435
         * Changes current action.
436
         */
437
        public function changeAction(string $action): void
1✔
438
        {
439
                $this->forwarded = true;
1✔
440
                $this->action = $this->view = $action;
1✔
441
        }
1✔
442

443

444
        /**
445
         * Switch from current action or render method to another.
446
         */
447
        public function switch(string $action): never
1✔
448
        {
449
                throw new Application\SwitchException($action);
1✔
450
        }
×
451

452

453
        /**
454
         * Returns current view.
455
         */
456
        final public function getView(): string
457
        {
458
                return $this->view;
1✔
459
        }
460

461

462
        /**
463
         * Changes current view. Any name is allowed.
464
         */
465
        public function setView(string $view): static
1✔
466
        {
467
                $this->forwarded = true;
1✔
468
                $this->view = $view;
1✔
469
                return $this;
1✔
470
        }
471

472

473
        /**
474
         * Returns current layout name.
475
         */
476
        final public function getLayout(): string|bool
477
        {
478
                return $this->layout;
×
479
        }
480

481

482
        /**
483
         * Changes or disables layout.
484
         */
485
        public function setLayout(string|bool $layout): static
1✔
486
        {
487
                $this->layout = $layout === false ? false : (string) $layout;
1✔
488
                return $this;
1✔
489
        }
490

491

492
        /**
493
         * @throws Nette\Application\AbortException
494
         * @return never
495
         */
496
        public function sendTemplate(?Template $template = null): void
1✔
497
        {
498
                $template ??= $this->getTemplate();
1✔
499
                foreach ($this->getReflection()->getTemplateVariables($this) as $name) {
1✔
500
                        $template->$name ??= $this->$name;
1✔
501
                }
502
                if ($template->getFile() === null) {
1✔
503
                        $template->setFile($this->findTemplateFile());
×
504
                }
505
                $this->sendResponse(new Responses\TextResponse($template));
1✔
506
        }
507

508

509
        /**
510
         * Finds template file name.
511
         */
512
        public function findTemplateFile(): string
513
        {
514
                $files = $this->formatTemplateFiles();
×
515
                foreach ($files as $file) {
×
516
                        if (is_file($file)) {
×
517
                                return $file;
×
518
                        }
519
                }
520

521
                $file = strtr($files[0], '/', DIRECTORY_SEPARATOR);
×
522
                $this->error("Page not found. Missing template '$file'.");
×
523
        }
524

525

526
        /**
527
         * Finds layout template file name.
528
         * @internal
529
         */
530
        public function findLayoutTemplateFile(): ?string
531
        {
532
                if ($this->layout === false) {
×
533
                        return null;
×
534
                }
535

536
                $files = $this->formatLayoutTemplateFiles();
×
537
                foreach ($files as $file) {
×
538
                        if (is_file($file)) {
×
539
                                return $file;
×
540
                        }
541
                }
542

543
                if ($this->layout) {
×
544
                        $file = strtr($files[0], '/', DIRECTORY_SEPARATOR);
×
545
                        throw new Nette\FileNotFoundException("Layout not found. Missing template '$file'.");
×
546
                }
547

548
                return null;
×
549
        }
550

551

552
        /**
553
         * Formats layout template file names.
554
         * @return non-empty-list<string>
555
         */
556
        public function formatLayoutTemplateFiles(): array
557
        {
558
                if (is_string($this->layout) && preg_match('#/|\\\#', $this->layout)) {
1✔
559
                        return [$this->layout];
1✔
560
                }
561

562
                $layout = $this->layout ?: 'layout';
1✔
563
                $dir = dirname((string) static::getReflection()->getFileName());
1✔
564
                $levels = substr_count((string) $this->getName(), ':');
1✔
565
                if (!is_dir("$dir/templates")) {
1✔
566
                        $dir = dirname($origDir = $dir);
1✔
567
                        if (!is_dir("$dir/templates")) {
1✔
568
                                $list = ["$origDir/@$layout.latte"];
1✔
569
                                do {
570
                                        $list[] = "$dir/@$layout.latte";
1✔
571
                                } while ($levels-- && ($dir = dirname($dir)));
1✔
572
                                return $list;
1✔
573
                        }
574
                }
575

576
                [, $presenter] = Helpers::splitName((string) $this->getName());
1✔
577
                $list = [
1✔
578
                        "$dir/templates/$presenter/@$layout.latte",
1✔
579
                        "$dir/templates/$presenter.@$layout.latte",
1✔
580
                ];
581
                do {
582
                        $list[] = "$dir/templates/@$layout.latte";
1✔
583
                } while ($levels-- && ($dir = dirname($dir)));
1✔
584

585
                return $list;
1✔
586
        }
587

588

589
        /**
590
         * Formats view template file names.
591
         * @return non-empty-list<string>
592
         */
593
        public function formatTemplateFiles(): array
594
        {
595
                $dir = dirname((string) static::getReflection()->getFileName());
1✔
596
                if (!is_dir("$dir/templates")) {
1✔
597
                        $dir = dirname($origDir = $dir);
1✔
598
                        if (!is_dir("$dir/templates")) {
1✔
599
                                return [
600
                                        "$origDir/$this->view.latte",
1✔
601
                                ];
602
                        }
603
                }
604

605
                [, $presenter] = Helpers::splitName((string) $this->getName());
1✔
606
                return [
607
                        "$dir/templates/$presenter/$this->view.latte",
1✔
608
                        "$dir/templates/$presenter.$this->view.latte",
1✔
609
                ];
610
        }
611

612

613
        /**
614
         * Formats action method name.
615
         */
616
        public static function formatActionMethod(string $action): string
1✔
617
        {
618
                return 'action' . ucfirst($action);
1✔
619
        }
620

621

622
        /**
623
         * Formats render view method name.
624
         */
625
        public static function formatRenderMethod(string $view): string
1✔
626
        {
627
                return 'render' . ucfirst($view);
1✔
628
        }
629

630

631
        /**
632
         * @template T of Template
633
         * @param ?class-string<T>  $class
634
         * @return ($class is null ? Template : T)
635
         */
636
        protected function createTemplate(?string $class = null): Template
1✔
637
        {
638
                $class ??= $this->formatTemplateClass();
1✔
639
                return $this->getTemplateFactory()->createTemplate($this, $class);
1✔
640
        }
641

642

643
        /** @return ?class-string<Template> */
644
        public function formatTemplateClass(): ?string
645
        {
646
                $base = preg_replace('#Presenter$#', '', static::class);
1✔
647
                return $this->checkTemplateClass($base . ucfirst($this->action) . 'Template')
1✔
648
                        ?? $this->checkTemplateClass($base . 'Template');
1✔
649
        }
650

651

652
        /********************* partial AJAX rendering ****************d*g**/
653

654

655
        final public function getPayload(): \stdClass
656
        {
657
                return $this->payload ??= new \stdClass;
1✔
658
        }
659

660

661
        /**
662
         * Is AJAX request?
663
         */
664
        public function isAjax(): bool
665
        {
666
                if (!isset($this->ajaxMode)) {
1✔
667
                        $this->ajaxMode = $this->httpRequest->isAjax();
1✔
668
                }
669

670
                return $this->ajaxMode;
1✔
671
        }
672

673

674
        /**
675
         * Sends AJAX payload to the output.
676
         * @throws Nette\Application\AbortException
677
         * @return never
678
         */
679
        public function sendPayload(): void
680
        {
681
                $this->sendResponse(new Responses\JsonResponse($this->getPayload()));
×
682
        }
683

684

685
        /**
686
         * Sends JSON data to the output.
687
         * @throws Nette\Application\AbortException
688
         * @return never
689
         */
690
        public function sendJson(mixed $data): void
691
        {
692
                $this->sendResponse(new Responses\JsonResponse($data));
×
693
        }
694

695

696
        /********************* navigation & flow ****************d*g**/
697

698

699
        /**
700
         * Sends response and terminates presenter.
701
         * @throws Nette\Application\AbortException
702
         * @return never
703
         */
704
        public function sendResponse(Application\Response $response): void
1✔
705
        {
706
                $this->response = $response;
1✔
707
                $this->terminate();
1✔
708
        }
709

710

711
        /**
712
         * Correctly terminates presenter.
713
         * @throws Nette\Application\AbortException
714
         * @return never
715
         */
716
        public function terminate(): void
717
        {
718
                throw new Application\AbortException;
1✔
719
        }
720

721

722
        /**
723
         * Forward to another presenter or action.
724
         * @param  array|mixed  $args
725
         * @throws Nette\Application\AbortException
726
         * @return never
727
         */
728
        public function forward(string|Nette\Application\Request $destination, $args = []): void
1✔
729
        {
730
                if ($destination instanceof Application\Request) {
1✔
731
                        $this->sendResponse(new Responses\ForwardResponse($destination));
×
732
                }
733

734
                $args = func_num_args() < 3 && is_array($args)
1✔
735
                        ? $args
1✔
736
                        : array_slice(func_get_args(), 1);
×
737
                $request = $this->linkGenerator->createRequest($this, $destination, $args, 'forward');
1✔
738
                $this->sendResponse(new Responses\ForwardResponse($request));
1✔
739
        }
740

741

742
        /**
743
         * Redirect to another URL and ends presenter execution.
744
         * @throws Nette\Application\AbortException
745
         * @return never
746
         */
747
        public function redirectUrl(string $url, ?int $httpCode = null): void
1✔
748
        {
749
                if ($this->isAjax()) {
1✔
750
                        $this->getPayload()->redirect = $url;
×
751
                        $this->sendPayload();
×
752

753
                } elseif (!$httpCode) {
1✔
754
                        $httpCode = $this->httpRequest->isMethod('post')
1✔
755
                                ? Http\IResponse::S303_PostGet
1✔
756
                                : Http\IResponse::S302_Found;
1✔
757
                }
758

759
                $this->sendResponse(new Responses\RedirectResponse($url, $httpCode));
1✔
760
        }
761

762

763
        /**
764
         * Returns the last created Request.
765
         * @internal
766
         */
767
        final public function getLastCreatedRequest(): ?Application\Request
768
        {
769
                return $this->linkGenerator->lastRequest;
1✔
770
        }
771

772

773
        /**
774
         * Returns the last created Request flag.
775
         * @internal
776
         */
777
        final public function getLastCreatedRequestFlag(string $flag): bool
1✔
778
        {
779
                return (bool) $this->linkGenerator->lastRequest?->hasFlag($flag);
1✔
780
        }
781

782

783
        /**
784
         * Conditional redirect to canonicalized URI.
785
         * @param  mixed  ...$args
786
         * @throws Nette\Application\AbortException
787
         */
788
        public function canonicalize(?string $destination = null, ...$args): void
1✔
789
        {
790
                $request = $this->request;
1✔
791
                if ($this->isAjax() || (!$request->isMethod('get') && !$request->isMethod('head'))) {
1✔
792
                        return;
1✔
793
                }
794

795
                $args = count($args) === 1 && is_array($args[0] ?? null)
1✔
796
                        ? $args[0]
×
797
                        : $args;
1✔
798
                try {
799
                        $url = $this->linkGenerator->link(
1✔
800
                                $destination ?: $this->action,
1✔
801
                                $args + $this->getGlobalState() + $request->getParameters(),
1✔
802
                                $this,
803
                                'redirectX',
1✔
804
                        );
805
                } catch (InvalidLinkException) {
×
806
                }
807

808
                if (!isset($url) || $this->httpRequest->getUrl()->isEqual($url)) {
1✔
809
                        return;
×
810
                }
811

812
                $code = $request->hasFlag($request::VARYING)
1✔
813
                        ? Http\IResponse::S302_Found
1✔
814
                        : Http\IResponse::S301_MovedPermanently;
1✔
815
                $this->sendResponse(new Responses\RedirectResponse($url, $code));
1✔
816
        }
817

818

819
        /**
820
         * Attempts to cache the sent entity by its last modification date.
821
         * @param  ?string  $etag  strong entity tag validator
822
         * @param  ?string  $expire  like '20 minutes'
823
         * @throws Nette\Application\AbortException
824
         */
825
        public function lastModified(
826
                string|int|\DateTimeInterface|null $lastModified,
827
                ?string $etag = null,
828
                ?string $expire = null,
829
        ): void
830
        {
831
                if ($expire !== null) {
×
832
                        $this->httpResponse->setExpiration($expire);
×
833
                }
834

835
                $helper = new Http\Context($this->httpRequest, $this->httpResponse);
×
836
                if (!$helper->isModified($lastModified, $etag)) {
×
837
                        $this->terminate();
×
838
                }
839
        }
840

841

842
        /**
843
         * @deprecated @internal
844
         * @param  array<string, mixed>  $args
845
         */
846
        protected function createRequest(Component $component, string $destination, array $args, string $mode): ?string
847
        {
848
                return $this->linkGenerator->link($destination, $args, $component, $mode);
×
849
        }
850

851

852
        /**
853
         * @deprecated @internal
854
         * @return array{absolute: bool, path: string, signal: bool, args: ?array<string, mixed>, fragment: string}
855
         */
856
        public static function parseDestination(string $destination): array
857
        {
858
                return LinkGenerator::parseDestination($destination);
×
859
        }
860

861

862
        /** @deprecated @internal */
863
        protected function requestToUrl(Application\Request $request, ?bool $relative = null): string
864
        {
865
                return $this->linkGenerator->requestToUrl($request, $relative ?? !$this->absoluteUrls);
×
866
        }
867

868

869
        /**
870
         * Invalid link handler. Descendant can override this method to change default behaviour.
871
         * @throws InvalidLinkException
872
         */
873
        protected function handleInvalidLink(InvalidLinkException $e): string
1✔
874
        {
875
                if ($this->invalidLinkMode & self::InvalidLinkException) {
1✔
876
                        throw $e;
1✔
877
                } elseif ($this->invalidLinkMode & self::InvalidLinkWarning) {
1✔
878
                        trigger_error('Invalid link: ' . $e->getMessage(), E_USER_WARNING);
1✔
879
                }
880

881
                return $this->invalidLinkMode & self::InvalidLinkTextual
1✔
882
                        ? '#error: ' . $e->getMessage()
1✔
883
                        : '#';
1✔
884
        }
885

886

887
        /********************* request serialization ****************d*g**/
888

889

890
        /**
891
         * Stores current request to session.
892
         */
893
        public function storeRequest(string $expiration = '+ 10 minutes'): string
1✔
894
        {
895
                $session = $this->getSession('Nette.Application/requests');
1✔
896
                do {
897
                        $key = Nette\Utils\Random::generate(5);
1✔
898
                } while ($session->get($key));
1✔
899

900
                $session->set($key, [$this->user?->getId(), $this->request]);
1✔
901
                $session->setExpiration($expiration, $key);
1✔
902
                return $key;
1✔
903
        }
904

905

906
        /**
907
         * Restores request from session.
908
         */
909
        public function restoreRequest(string $key): void
1✔
910
        {
911
                $session = $this->getSession('Nette.Application/requests');
1✔
912
                $data = $session->get($key);
1✔
913
                if (!$data || ($data[0] !== null && $data[0] !== $this->getUser()->getId())) {
1✔
914
                        return;
1✔
915
                }
916

917
                $request = clone $data[1];
1✔
918
                assert($request instanceof Application\Request);
919
                $session->remove($key);
1✔
920
                $params = $request->getParameters();
1✔
921
                $params[self::FlashKey] = $this->getFlashKey();
1✔
922
                $request->setParameters($params);
1✔
923
                if ($request->isMethod('POST')) {
1✔
924
                        $request->setFlag(Application\Request::RESTORED, true);
1✔
925
                        $this->sendResponse(new Responses\ForwardResponse($request));
1✔
926
                } else {
927
                        $this->redirectUrl($this->linkGenerator->requestToUrl($request));
×
928
                }
929
        }
930

931

932
        /********************* interface StatePersistent ****************d*g**/
933

934

935
        /**
936
         * Descendant can override this method to return the names of custom persistent components.
937
         * @return list<string>
938
         */
939
        public static function getPersistentComponents(): array
940
        {
941
                return [];
1✔
942
        }
943

944

945
        /**
946
         * Saves state information for all subcomponents to $this->globalState.
947
         * @param  ?class-string  $forClass
948
         * @return array<string, mixed>
949
         */
950
        public function getGlobalState(?string $forClass = null): array
1✔
951
        {
952
                $sinces = &$this->globalStateSinces;
1✔
953

954
                if (!isset($this->globalState)) {
1✔
955
                        $state = [];
1✔
956
                        foreach ($this->globalParams as $id => $params) {
1✔
957
                                $prefix = $id . self::NameSeparator;
1✔
958
                                foreach ($params as $key => $val) {
1✔
959
                                        $state[$prefix . $key] = $val;
1✔
960
                                }
961
                        }
962

963
                        $this->saveStatePartial($state, new ComponentReflection($forClass ?? $this));
1✔
964

965
                        if ($sinces === null) {
1✔
966
                                $sinces = [];
1✔
967
                                foreach ($this->getReflection()->getPersistentParams() as $name => $meta) {
1✔
968
                                        $sinces[$name] = $meta['since'];
1✔
969
                                }
970
                        }
971

972
                        $persistents = $this->getReflection()->getPersistentComponents();
1✔
973

974
                        $since = false;
1✔
975
                        foreach ($this->getComponentTree() as $component) {
1✔
976
                                if ($component->getParent() === $this) {
1✔
977
                                        // counts on child-first search
978
                                        $since = $persistents[(string) $component->getName()]['since'] ?? false; // false = nonpersistent
1✔
979
                                }
980

981
                                if (!$component instanceof StatePersistent || !$component instanceof Component) {
1✔
982
                                        continue;
×
983
                                }
984

985
                                $prefix = $component->getUniqueId() . self::NameSeparator;
1✔
986
                                $params = [];
1✔
987
                                $component->saveState($params);
1✔
988
                                foreach ($params as $key => $val) {
1✔
989
                                        $state[$prefix . $key] = $val;
1✔
990
                                        $sinces[$prefix . $key] = $since;
1✔
991
                                }
992
                        }
993
                } else {
994
                        $state = $this->globalState;
1✔
995
                }
996

997
                if ($forClass !== null) {
1✔
998
                        $tree = Helpers::getClassesAndTraits($forClass);
1✔
999
                        $since = null;
1✔
1000
                        foreach ($state as $key => $foo) {
1✔
1001
                                if (!isset($sinces[$key])) {
1✔
1002
                                        $x = strpos($key, self::NameSeparator);
1✔
1003
                                        $x = $x === false ? $key : substr($key, 0, $x);
1✔
1004
                                        $sinces[$key] = $sinces[$x] ?? false;
1✔
1005
                                }
1006

1007
                                if ($since !== $sinces[$key]) {
1✔
1008
                                        $since = $sinces[$key];
1✔
1009
                                        $ok = $since && isset($tree[$since]);
1✔
1010
                                }
1011

1012
                                if (!$ok) {
1✔
1013
                                        unset($state[$key]);
1✔
1014
                                }
1015
                        }
1016
                }
1017

1018
                return $state;
1✔
1019
        }
1020

1021

1022
        /**
1023
         * Permanently saves state information for all subcomponents to $this->globalState.
1024
         */
1025
        protected function saveGlobalState(): void
1026
        {
1027
                $this->globalParams = [];
1✔
1028
                $this->globalState = $this->getGlobalState();
1✔
1029
        }
1✔
1030

1031

1032
        /**
1033
         * Initializes $this->globalParams, $this->signal & $this->signalReceiver, $this->action, $this->view. Called by run().
1034
         * @throws Nette\Application\BadRequestException if action name is not valid
1035
         */
1036
        private function initGlobalParameters(): void
1037
        {
1038
                // init $this->globalParams
1039
                $this->globalParams = [];
1✔
1040
                $selfParams = [];
1✔
1041
                $request = $this->getRequest();
1✔
1042

1043
                $params = $request->getParameters();
1✔
1044
                if (($tmp = $request->getPost('_' . self::SignalKey)) !== null) {
1✔
1045
                        $params[self::SignalKey] = $tmp;
1✔
1046
                } elseif ($this->isAjax()) {
1✔
1047
                        $params += $request->getPost();
1✔
1048
                        if (($tmp = $request->getPost(self::SignalKey)) !== null) {
1✔
1049
                                $params[self::SignalKey] = $tmp;
1✔
1050
                        }
1051
                }
1052

1053
                foreach ($params as $key => $value) {
1✔
1054
                        if (!preg_match('#^((?:[a-z0-9_]+-)*)((?!\d+$)[a-z0-9_]+)$#Di', (string) $key, $matches)) {
1✔
1055
                                continue;
×
1056
                        } elseif (!$matches[1]) {
1✔
1057
                                $selfParams[$key] = $value;
1✔
1058
                        } else {
1059
                                $this->globalParams[substr($matches[1], 0, -1)][$matches[2]] = $value;
1✔
1060
                        }
1061
                }
1062

1063
                // init & validate $this->action & $this->view
1064
                $action = $selfParams[self::ActionKey] ?? self::DefaultAction;
1✔
1065
                if (!is_string($action) || !Nette\Utils\Strings::match($action, '#^[a-zA-Z0-9][a-zA-Z0-9_\x7f-\xff]*$#D')) {
1✔
1066
                        $this->error('Action name is not valid.');
1✔
1067
                }
1068

1069
                $this->changeAction($action);
1✔
1070
                $this->forwarded = false;
1✔
1071

1072
                // init $this->signalReceiver and key 'signal' in appropriate params array
1073
                $this->signalReceiver = $this->getUniqueId();
1✔
1074
                if (isset($selfParams[self::SignalKey])) {
1✔
1075
                        $param = $selfParams[self::SignalKey];
1✔
1076
                        if (!is_string($param)) {
1✔
1077
                                $this->error('Signal name is not string.');
1✔
1078
                        }
1079

1080
                        $pos = strrpos($param, '-');
1✔
1081
                        if ($pos) {
1✔
1082
                                $this->signalReceiver = substr($param, 0, $pos);
1✔
1083
                                $this->signal = substr($param, $pos + 1);
1✔
1084
                        } else {
1085
                                $this->signalReceiver = $this->getUniqueId();
1✔
1086
                                $this->signal = $param;
1✔
1087
                        }
1088

1089
                        if ($this->signal === '') {
1✔
1090
                                $this->signal = null;
×
1091
                        }
1092
                }
1093

1094
                $this->loadState($selfParams);
1✔
1095
        }
1✔
1096

1097

1098
        /**
1099
         * Pops parameters for specified component.
1100
         * @return array<string, mixed>
1101
         * @internal
1102
         */
1103
        final public function popGlobalParameters(string $id): array
1✔
1104
        {
1105
                $res = $this->globalParams[$id] ?? [];
1✔
1106
                unset($this->globalParams[$id]);
1✔
1107
                return $res;
1✔
1108
        }
1109

1110

1111
        /********************* flash session ****************d*g**/
1112

1113

1114
        private function getFlashKey(): ?string
1115
        {
1116
                $flashKey = $this->getParameter(self::FlashKey);
1✔
1117
                return is_string($flashKey) && $flashKey !== ''
1✔
1118
                        ? $flashKey
1✔
1119
                        : null;
1✔
1120
        }
1121

1122

1123
        /**
1124
         * Checks if a flash session namespace exists.
1125
         */
1126
        public function hasFlashSession(): bool
1127
        {
1128
                $flashKey = $this->getFlashKey();
1✔
1129
                return $flashKey !== null
1✔
1130
                        && $this->getSession()->hasSection('Nette.Application.Flash/' . $flashKey);
1✔
1131
        }
1132

1133

1134
        /**
1135
         * Returns session namespace provided to pass temporary data between redirects.
1136
         */
1137
        public function getFlashSession(): Http\SessionSection
1138
        {
1139
                $flashKey = $this->getFlashKey();
1✔
1140
                if ($flashKey === null) {
1✔
1141
                        $this->params[self::FlashKey] = $flashKey = Nette\Utils\Random::generate(4);
1✔
1142
                }
1143

1144
                return $this->getSession('Nette.Application.Flash/' . $flashKey);
1✔
1145
        }
1146

1147

1148
        /********************* services ****************d*g**/
1149

1150

1151
        final public function injectPrimary(
1✔
1152
                Http\IRequest $httpRequest,
1153
                Http\IResponse $httpResponse,
1154
                ?Application\IPresenterFactory $presenterFactory = null,
1155
                ?Nette\Routing\Router $router = null,
1156
                ?Http\Session $session = null,
1157
                ?Nette\Security\User $user = null,
1158
                ?TemplateFactory $templateFactory = null,
1159
        ): void
1160
        {
1161
                $this->httpRequest = $httpRequest;
1✔
1162
                $this->httpResponse = $httpResponse;
1✔
1163
                $this->session = $session;
1✔
1164
                $this->user = $user;
1✔
1165
                $this->templateFactory = $templateFactory;
1✔
1166
                if ($router && $presenterFactory) {
1✔
1167
                        $url = $httpRequest->getUrl();
1✔
1168
                        $this->linkGenerator = new LinkGenerator(
1✔
1169
                                $router,
1✔
1170
                                new Http\UrlScript($url->getHostUrl() . $url->getScriptPath()),
1✔
1171
                                $presenterFactory,
1172
                        );
1173
                }
1174
        }
1✔
1175

1176

1177
        final public function getHttpRequest(): Http\IRequest
1178
        {
1179
                return $this->httpRequest;
1✔
1180
        }
1181

1182

1183
        final public function getHttpResponse(): Http\IResponse
1184
        {
1185
                return $this->httpResponse;
1✔
1186
        }
1187

1188

1189
        /**
1190
         * @return ($namespace is null ? Http\Session : Http\SessionSection)
1191
         */
1192
        final public function getSession(?string $namespace = null): Http\Session|Http\SessionSection
1✔
1193
        {
1194
                if (empty($this->session)) {
1✔
1195
                        throw new Nette\InvalidStateException('Service Session has not been set.');
×
1196
                }
1197

1198
                return $namespace === null
1✔
1199
                        ? $this->session
1✔
1200
                        : $this->session->getSection($namespace);
1✔
1201
        }
1202

1203

1204
        final public function getUser(): Nette\Security\User
1205
        {
1206
                return $this->user ?? throw new Nette\InvalidStateException('Service User has not been set.');
1✔
1207
        }
1208

1209

1210
        final public function getTemplateFactory(): TemplateFactory
1211
        {
1212
                return $this->templateFactory ?? throw new Nette\InvalidStateException('Service TemplateFactory has not been set.');
1✔
1213
        }
1214

1215

1216
        final protected function getLinkGenerator(): LinkGenerator
1217
        {
1218
                return $this->linkGenerator ?? throw new Nette\InvalidStateException('Unable to create link to other presenter, service PresenterFactory or Router has not been set.');
1✔
1219
        }
1220
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc