From 63e5bcd613f63a1d18527807c6014fcfb0a7818f Mon Sep 17 00:00:00 2001 From: Ambrose Casanova <279373485+ambrose5773@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:55:07 -0600 Subject: [PATCH 1/4] fix: keep view templates inside the views directory Reject absolute template paths and any resolved path outside the configured views directory. --- flight/template/View.php | 74 ++++++++++++++++++++++-- tests/ViewTest.php | 118 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 187 insertions(+), 5 deletions(-) diff --git a/flight/template/View.php b/flight/template/View.php index 17622fd..24a5724 100644 --- a/flight/template/View.php +++ b/flight/template/View.php @@ -161,9 +161,14 @@ class View /** * Gets the full path to a template file. * + * Absolute paths are rejected. The resolved file must stay inside the + * configured views directory. If that cannot be shown, this fails closed. + * * @param string $file Template file * * @return string Template file location + * + * @throws \Exception When the path is absolute or resolves outside the views directory. */ public function getTemplate(string $file): string { @@ -173,13 +178,74 @@ class View $file .= $ext; } - $is_windows = \strtoupper(\substr(PHP_OS, 0, 3)) === 'WIN'; + if ($this->isAbsolutePath($file)) { + throw new \Exception('Template path is not allowed.'); + } + + $viewsPath = \realpath($this->path); + if ($viewsPath === false || !$this->relativeStaysInside($file)) { + throw new \Exception('Template path is not allowed.'); + } + + $candidate = $this->path . \DIRECTORY_SEPARATOR . $file; + $resolved = \realpath($candidate); + if ($resolved === false) { + return $candidate; + } + + $root = \rtrim($viewsPath, \DIRECTORY_SEPARATOR) . \DIRECTORY_SEPARATOR; + if (\strpos($resolved, $root) !== 0) { + throw new \Exception('Template path is not allowed.'); + } + + return $resolved; + } + + /** + * True when $file is an absolute filesystem path. + */ + private function isAbsolutePath(string $file): bool + { + if ($file === '') { + return false; + } + + if ($file[0] === '/' || $file[0] === '\\') { + return true; + } + + return \strlen($file) > 1 && \ctype_alpha($file[0]) && $file[1] === ':'; + } + + /** + * True when relative segments in $file do not climb out of the views directory. + */ + private function relativeStaysInside(string $file): bool + { + $segments = \preg_split('#[\\/]+#', $file, -1, \PREG_SPLIT_NO_EMPTY); + if ($segments === false) { + return false; + } + + $depth = 0; + foreach ($segments as $segment) { + if ($segment === '.') { + continue; + } + + if ($segment === '..') { + if ($depth === 0) { + return false; + } + + $depth--; + continue; + } - if ((\substr($file, 0, 1) === '/') || ($is_windows && \substr($file, 1, 1) === ':')) { - return $file; + $depth++; } - return $this->path . DIRECTORY_SEPARATOR . $file; + return true; } /** diff --git a/tests/ViewTest.php b/tests/ViewTest.php index 56a6e0e..60162fb 100644 --- a/tests/ViewTest.php +++ b/tests/ViewTest.php @@ -111,12 +111,128 @@ class ViewTest extends TestCase $this->expectOutputString("Hello world, Bob!"); } + public function testRenderRelativePathThatStaysInsideViews(): void + { + $this->view->render('layouts/../hello', ['name' => 'Bob']); + + $this->expectOutputString('Hello, Bob!'); + } + public function testGetTemplateAbsolutePath(): void { $tmpfile = tmpfile(); $this->view->extension = ''; $file_path = stream_get_meta_data($tmpfile)['uri']; - $this->assertEquals($file_path, $this->view->getTemplate($file_path)); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Template path is not allowed.'); + $this->view->getTemplate($file_path); + } + + public function testRejectsDriveLetterTemplatePath(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Template path is not allowed.'); + $this->view->getTemplate('C:' . DIRECTORY_SEPARATOR . 'outside.php'); + } + + public function testRejectsTemplateThatLeavesViewsDirectory(): void + { + $outside = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'flight-view-outside-' . uniqid(); + mkdir($outside); + $note = $outside . DIRECTORY_SEPARATOR . 'note.php'; + file_put_contents($note, 'view->path); + $relative = $this->relativePathFrom($views, $note); + $relative = preg_replace('/\.php$/', '', $relative); + + try { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Template path is not allowed.'); + $this->view->render($relative); + } finally { + unlink($note); + rmdir($outside); + } + } + + public function testRejectsTemplateThatResolvesOutsideViewsDirectory(): void + { + $root = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'flight-view-root-' . uniqid(); + $views = $root . DIRECTORY_SEPARATOR . 'views'; + $outside = $root . DIRECTORY_SEPARATOR . 'outside'; + mkdir($root); + mkdir($views); + mkdir($outside); + $note = $outside . DIRECTORY_SEPARATOR . 'note.php'; + file_put_contents($note, 'removeDir($root); + $this->markTestSkipped('Symlink not available'); + } + + $view = new View($views); + + try { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Template path is not allowed.'); + $view->render('alias'); + } finally { + $this->removeDir($root); + } + } + + public function testRejectsMissingViewsDirectory(): void + { + $view = new View(sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'flight-missing-views-' . uniqid()); + + $this->expectException(Exception::class); + $this->expectExceptionMessage('Template path is not allowed.'); + $view->render('hello'); + } + + private function relativePathFrom(string $fromDir, string $toFile): string + { + $from = explode(DIRECTORY_SEPARATOR, rtrim($fromDir, DIRECTORY_SEPARATOR)); + $to = explode(DIRECTORY_SEPARATOR, $toFile); + $file = array_pop($to); + + while ($from !== [] && $to !== [] && $from[0] === $to[0]) { + array_shift($from); + array_shift($to); + } + + $up = array_fill(0, count($from), '..'); + return implode(DIRECTORY_SEPARATOR, array_merge($up, $to, [$file])); + } + + private function removeDir(string $dir): void + { + if (!is_dir($dir)) { + return; + } + + $items = scandir($dir); + if ($items === false) { + return; + } + + foreach ($items as $item) { + if ($item === '.' || $item === '..') { + continue; + } + $path = $dir . DIRECTORY_SEPARATOR . $item; + if (is_link($path) || is_file($path)) { + unlink($path); + continue; + } + $this->removeDir($path); + } + + rmdir($dir); } public function testE(): void From 8093a18fb33206de38055e664ff28be2f3409311 Mon Sep 17 00:00:00 2001 From: Ambrose Casanova <279373485+ambrose5773@users.noreply.github.com> Date: Sat, 3 Oct 2026 08:57:48 -0600 Subject: [PATCH 2/4] fix: reject Windows view paths that leave the views directory Normalize both separators before the containment check, and build the regression next to the views directory so it stays on the same drive. --- flight/template/View.php | 14 ++++++++------ tests/ViewTest.php | 28 +++++++++------------------- 2 files changed, 17 insertions(+), 25 deletions(-) diff --git a/flight/template/View.php b/flight/template/View.php index 24a5724..2e81ae3 100644 --- a/flight/template/View.php +++ b/flight/template/View.php @@ -222,17 +222,19 @@ class View */ private function relativeStaysInside(string $file): bool { - $segments = \preg_split('#[\\/]+#', $file, -1, \PREG_SPLIT_NO_EMPTY); - if ($segments === false) { - return false; - } - + $segments = \explode('/', \str_replace('\\', '/', $file)); $depth = 0; + foreach ($segments as $segment) { - if ($segment === '.') { + if ($segment === '' || $segment === '.') { continue; } + // A drive letter or colon is an absolute jump, not a view name. + if (\strpos($segment, ':') !== false) { + return false; + } + if ($segment === '..') { if ($depth === 0) { return false; diff --git a/tests/ViewTest.php b/tests/ViewTest.php index 60162fb..ac360d2 100644 --- a/tests/ViewTest.php +++ b/tests/ViewTest.php @@ -138,14 +138,11 @@ class ViewTest extends TestCase public function testRejectsTemplateThatLeavesViewsDirectory(): void { - $outside = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'flight-view-outside-' . uniqid(); + $outside = dirname($this->view->path) . DIRECTORY_SEPARATOR . 'flight-view-outside-' . uniqid(); mkdir($outside); $note = $outside . DIRECTORY_SEPARATOR . 'note.php'; file_put_contents($note, 'view->path); - $relative = $this->relativePathFrom($views, $note); - $relative = preg_replace('/\.php$/', '', $relative); + $relative = '..' . DIRECTORY_SEPARATOR . basename($outside) . DIRECTORY_SEPARATOR . 'note'; try { $this->expectException(Exception::class); @@ -157,6 +154,13 @@ class ViewTest extends TestCase } } + public function testRejectsEmbeddedDriveLetter(): void + { + $this->expectException(Exception::class); + $this->expectExceptionMessage('Template path is not allowed.'); + $this->view->getTemplate('layouts' . DIRECTORY_SEPARATOR . 'C:' . DIRECTORY_SEPARATOR . 'outside'); + } + public function testRejectsTemplateThatResolvesOutsideViewsDirectory(): void { $root = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'flight-view-root-' . uniqid(); @@ -194,20 +198,6 @@ class ViewTest extends TestCase $view->render('hello'); } - private function relativePathFrom(string $fromDir, string $toFile): string - { - $from = explode(DIRECTORY_SEPARATOR, rtrim($fromDir, DIRECTORY_SEPARATOR)); - $to = explode(DIRECTORY_SEPARATOR, $toFile); - $file = array_pop($to); - - while ($from !== [] && $to !== [] && $from[0] === $to[0]) { - array_shift($from); - array_shift($to); - } - - $up = array_fill(0, count($from), '..'); - return implode(DIRECTORY_SEPARATOR, array_merge($up, $to, [$file])); - } private function removeDir(string $dir): void { From eac9fef02142fae419a4c004c696fdd03014c189 Mon Sep 17 00:00:00 2001 From: Ambrose Casanova <279373485+ambrose5773@users.noreply.github.com> Date: Mon, 5 Oct 2026 06:57:20 -0600 Subject: [PATCH 3/4] feat: make view path containment opt-in with View::$restrictToPath Restore getTemplate() and its existing test exactly as on master. Default behavior is unchanged: absolute paths still render and exists() never throws. With restrictToPath on, render(), fetch() and exists() only accept files that resolve inside the views path. A blocked file has its own error message, separate from "not found". --- flight/template/View.php | 111 +++++++----------- tests/ViewTest.php | 243 ++++++++++++++++++++++----------------- 2 files changed, 175 insertions(+), 179 deletions(-) diff --git a/flight/template/View.php b/flight/template/View.php index 2e81ae3..a2fd8f8 100644 --- a/flight/template/View.php +++ b/flight/template/View.php @@ -22,6 +22,12 @@ class View public bool $preserveVars = true; + /** + * When true, render(), fetch() and exists() only accept template files + * that resolve inside $path. Off by default so existing behavior holds. + */ + public bool $restrictToPath = false; + /** * View variables. * @@ -116,6 +122,10 @@ class View throw new \Exception("Template file not found: {$normalized_path}."); } + if (!$this->isInsidePath($this->template)) { + throw new \Exception('Template file is outside the views path.'); + } + \extract($this->vars); if (\is_array($templateData) === true) { @@ -155,20 +165,17 @@ class View */ public function exists(string $file): bool { - return \file_exists($this->getTemplate($file)); + $template = $this->getTemplate($file); + + return \file_exists($template) && $this->isInsidePath($template); } /** * Gets the full path to a template file. * - * Absolute paths are rejected. The resolved file must stay inside the - * configured views directory. If that cannot be shown, this fails closed. - * * @param string $file Template file * * @return string Template file location - * - * @throws \Exception When the path is absolute or resolves outside the views directory. */ public function getTemplate(string $file): string { @@ -178,90 +185,50 @@ class View $file .= $ext; } - if ($this->isAbsolutePath($file)) { - throw new \Exception('Template path is not allowed.'); - } - - $viewsPath = \realpath($this->path); - if ($viewsPath === false || !$this->relativeStaysInside($file)) { - throw new \Exception('Template path is not allowed.'); - } - - $candidate = $this->path . \DIRECTORY_SEPARATOR . $file; - $resolved = \realpath($candidate); - if ($resolved === false) { - return $candidate; - } + $is_windows = \strtoupper(\substr(PHP_OS, 0, 3)) === 'WIN'; - $root = \rtrim($viewsPath, \DIRECTORY_SEPARATOR) . \DIRECTORY_SEPARATOR; - if (\strpos($resolved, $root) !== 0) { - throw new \Exception('Template path is not allowed.'); + if ((\substr($file, 0, 1) === '/') || ($is_windows && \substr($file, 1, 1) === ':')) { + return $file; } - return $resolved; + return $this->path . DIRECTORY_SEPARATOR . $file; } /** - * True when $file is an absolute filesystem path. + * Displays escaped output. + * + * @param string $str String to escape + * + * @return string Escaped string */ - private function isAbsolutePath(string $file): bool + public function e(string $str): string { - if ($file === '') { - return false; - } - - if ($file[0] === '/' || $file[0] === '\\') { - return true; - } - - return \strlen($file) > 1 && \ctype_alpha($file[0]) && $file[1] === ':'; + $value = \htmlentities($str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + echo $value; + return $value; } /** - * True when relative segments in $file do not climb out of the views directory. + * Checks that an existing template file resolves inside the views path. + * Always true while $restrictToPath is off. Fails closed when either + * path cannot be resolved. */ - private function relativeStaysInside(string $file): bool + protected function isInsidePath(string $template): bool { - $segments = \explode('/', \str_replace('\\', '/', $file)); - $depth = 0; - - foreach ($segments as $segment) { - if ($segment === '' || $segment === '.') { - continue; - } - - // A drive letter or colon is an absolute jump, not a view name. - if (\strpos($segment, ':') !== false) { - return false; - } - - if ($segment === '..') { - if ($depth === 0) { - return false; - } + if ($this->restrictToPath === false) { + return true; + } - $depth--; - continue; - } + $root = \realpath($this->path); + $resolved = \realpath($template); - $depth++; + if ($root === false || $resolved === false) { + return false; } - return true; - } + $root = \rtrim($root, '\\/') . DIRECTORY_SEPARATOR; - /** - * Displays escaped output. - * - * @param string $str String to escape - * - * @return string Escaped string - */ - public function e(string $str): string - { - $value = \htmlentities($str, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); - echo $value; - return $value; + return \strpos($resolved, $root) === 0; } protected static function normalizePath(string $path, string $separator = DIRECTORY_SEPARATOR): string diff --git a/tests/ViewTest.php b/tests/ViewTest.php index ac360d2..b3638d5 100644 --- a/tests/ViewTest.php +++ b/tests/ViewTest.php @@ -111,118 +111,12 @@ class ViewTest extends TestCase $this->expectOutputString("Hello world, Bob!"); } - public function testRenderRelativePathThatStaysInsideViews(): void - { - $this->view->render('layouts/../hello', ['name' => 'Bob']); - - $this->expectOutputString('Hello, Bob!'); - } - public function testGetTemplateAbsolutePath(): void { $tmpfile = tmpfile(); $this->view->extension = ''; $file_path = stream_get_meta_data($tmpfile)['uri']; - - $this->expectException(Exception::class); - $this->expectExceptionMessage('Template path is not allowed.'); - $this->view->getTemplate($file_path); - } - - public function testRejectsDriveLetterTemplatePath(): void - { - $this->expectException(Exception::class); - $this->expectExceptionMessage('Template path is not allowed.'); - $this->view->getTemplate('C:' . DIRECTORY_SEPARATOR . 'outside.php'); - } - - public function testRejectsTemplateThatLeavesViewsDirectory(): void - { - $outside = dirname($this->view->path) . DIRECTORY_SEPARATOR . 'flight-view-outside-' . uniqid(); - mkdir($outside); - $note = $outside . DIRECTORY_SEPARATOR . 'note.php'; - file_put_contents($note, 'expectException(Exception::class); - $this->expectExceptionMessage('Template path is not allowed.'); - $this->view->render($relative); - } finally { - unlink($note); - rmdir($outside); - } - } - - public function testRejectsEmbeddedDriveLetter(): void - { - $this->expectException(Exception::class); - $this->expectExceptionMessage('Template path is not allowed.'); - $this->view->getTemplate('layouts' . DIRECTORY_SEPARATOR . 'C:' . DIRECTORY_SEPARATOR . 'outside'); - } - - public function testRejectsTemplateThatResolvesOutsideViewsDirectory(): void - { - $root = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'flight-view-root-' . uniqid(); - $views = $root . DIRECTORY_SEPARATOR . 'views'; - $outside = $root . DIRECTORY_SEPARATOR . 'outside'; - mkdir($root); - mkdir($views); - mkdir($outside); - $note = $outside . DIRECTORY_SEPARATOR . 'note.php'; - file_put_contents($note, 'removeDir($root); - $this->markTestSkipped('Symlink not available'); - } - - $view = new View($views); - - try { - $this->expectException(Exception::class); - $this->expectExceptionMessage('Template path is not allowed.'); - $view->render('alias'); - } finally { - $this->removeDir($root); - } - } - - public function testRejectsMissingViewsDirectory(): void - { - $view = new View(sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'flight-missing-views-' . uniqid()); - - $this->expectException(Exception::class); - $this->expectExceptionMessage('Template path is not allowed.'); - $view->render('hello'); - } - - - private function removeDir(string $dir): void - { - if (!is_dir($dir)) { - return; - } - - $items = scandir($dir); - if ($items === false) { - return; - } - - foreach ($items as $item) { - if ($item === '.' || $item === '..') { - continue; - } - $path = $dir . DIRECTORY_SEPARATOR . $item; - if (is_link($path) || is_file($path)) { - unlink($path); - continue; - } - $this->removeDir($path); - } - - rmdir($dir); + $this->assertEquals($file_path, $this->view->getTemplate($file_path)); } public function testE(): void @@ -361,4 +255,139 @@ class ViewTest extends TestCase ], ]; } + + public function testAbsolutePathStillRendersByDefault(): void + { + $file = $this->makeOutsideTemplate('outside'); + + try { + $this->expectOutputString('outside'); + $this->view->render($file); + $this->assertTrue($this->view->exists($file)); + } finally { + $this->removeDir(dirname($file)); + } + } + + public function testRestrictToPathIsOffByDefault(): void + { + $this->assertFalse((new View())->restrictToPath); + } + + public function testRestrictToPathRendersViewInsidePath(): void + { + $this->view->restrictToPath = true; + + $this->expectOutputString('Hello, Bob!'); + $this->view->render('hello', ['name' => 'Bob']); + $this->assertTrue($this->view->exists('hello')); + $this->assertTrue($this->view->exists('layouts/layout')); + } + + public function testRestrictToPathAllowsAbsolutePathInsidePath(): void + { + $this->view->restrictToPath = true; + $file = (string) realpath(__DIR__ . '/views/hello.php'); + + $this->expectOutputString('Hello, Bob!'); + $this->view->render($file, ['name' => 'Bob']); + } + + public function testRestrictToPathRejectsRelativePathOutsidePath(): void + { + $this->view->restrictToPath = true; + $dir = __DIR__ . DIRECTORY_SEPARATOR . 'restrict-' . uniqid(); + mkdir($dir); + file_put_contents($dir . DIRECTORY_SEPARATOR . 'note.php', 'outside'); + $name = '..' . DIRECTORY_SEPARATOR . basename($dir) . DIRECTORY_SEPARATOR . 'note'; + + try { + $this->assertFalse($this->view->exists($name)); + $this->expectException(Exception::class); + $this->expectExceptionMessage('Template file is outside the views path.'); + $this->view->render($name); + } finally { + $this->removeDir($dir); + } + } + + public function testRestrictToPathRejectsAbsolutePathOutsidePath(): void + { + $this->view->restrictToPath = true; + $file = $this->makeOutsideTemplate('outside'); + + try { + $this->assertFalse($this->view->exists($file)); + $this->expectException(Exception::class); + $this->expectExceptionMessage('Template file is outside the views path.'); + $this->view->render($file); + } finally { + $this->removeDir(dirname($file)); + } + } + + public function testRestrictToPathRejectsSymlinkOutsidePath(): void + { + $root = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'flight-view-' . uniqid(); + mkdir($root . DIRECTORY_SEPARATOR . 'views', 0777, true); + mkdir($root . DIRECTORY_SEPARATOR . 'outside'); + $target = $root . DIRECTORY_SEPARATOR . 'outside' . DIRECTORY_SEPARATOR . 'note.php'; + file_put_contents($target, 'outside'); + + if (!@symlink($target, $root . DIRECTORY_SEPARATOR . 'views' . DIRECTORY_SEPARATOR . 'alias.php')) { + $this->removeDir($root); + $this->markTestSkipped('Symlinks are not available.'); + } + + $view = new View($root . DIRECTORY_SEPARATOR . 'views'); + $view->restrictToPath = true; + + try { + $this->assertFalse($view->exists('alias')); + $this->expectException(Exception::class); + $this->expectExceptionMessage('Template file is outside the views path.'); + $view->render('alias'); + } finally { + $this->removeDir($root); + } + } + + public function testRestrictToPathKeepsNotFoundMessage(): void + { + $this->view->restrictToPath = true; + + $this->assertFalse($this->view->exists('badfile')); + $this->expectException(Exception::class); + $this->expectExceptionMessage('Template file not found:'); + $this->view->render('badfile'); + } + + private function makeOutsideTemplate(string $content): string + { + $dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'flight-view-' . uniqid(); + mkdir($dir); + $file = $dir . DIRECTORY_SEPARATOR . 'note.php'; + file_put_contents($file, $content); + + return $file; + } + + private function removeDir(string $dir): void + { + foreach ((array) scandir($dir) as $item) { + if ($item === '.' || $item === '..' || $item === false) { + continue; + } + + $path = $dir . DIRECTORY_SEPARATOR . $item; + + if (is_dir($path) && !is_link($path)) { + $this->removeDir($path); + } else { + unlink($path); + } + } + + rmdir($dir); + } } From 9491b939516a7685c53e4cf18f3331295e3071b1 Mon Sep 17 00:00:00 2001 From: Ambrose Casanova <279373485+ambrose5773@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:18:42 -0600 Subject: [PATCH 4/4] feat: drive view path containment via flight.views.restrict_to_path Expose the opt-in as a Flight::set() key in the flight.views.* family. Engine applies it to View::$restrictToPath when the view is created, same as path and extension. Default remains false. --- flight/Engine.php | 2 ++ flight/template/View.php | 1 + tests/EngineTest.php | 11 +++++++++++ 3 files changed, 14 insertions(+) diff --git a/flight/Engine.php b/flight/Engine.php index 8c1d69d..db27c1f 100644 --- a/flight/Engine.php +++ b/flight/Engine.php @@ -188,6 +188,7 @@ class Engine $this->loader->register('view', View::class, [], function (View $view) use ($self) { $view->path = $self->get('flight.views.path'); $view->extension = $self->get('flight.views.extension'); + $view->restrictToPath = (bool) $self->get('flight.views.restrict_to_path'); }); foreach (self::MAPPABLE_METHODS as $name) { @@ -202,6 +203,7 @@ class Engine $this->set('flight.debug', false); $this->set('flight.views.path', './views'); $this->set('flight.views.extension', '.php'); + $this->set('flight.views.restrict_to_path', false); $this->set('flight.content_length', true); $this->set('flight.v2.output_buffering', false); $this->set('flight.allow_method_override', true); diff --git a/flight/template/View.php b/flight/template/View.php index a2fd8f8..4798315 100644 --- a/flight/template/View.php +++ b/flight/template/View.php @@ -25,6 +25,7 @@ class View /** * When true, render(), fetch() and exists() only accept template files * that resolve inside $path. Off by default so existing behavior holds. + * Prefer Flight::set('flight.views.restrict_to_path', true); Engine applies it. */ public bool $restrictToPath = false; diff --git a/tests/EngineTest.php b/tests/EngineTest.php index 7c8226e..ed345d8 100644 --- a/tests/EngineTest.php +++ b/tests/EngineTest.php @@ -1380,4 +1380,15 @@ class EngineTest extends TestCase $this->assertSame('HEAD', $engine->request()->method); } + + public function testViewsRestrictToPathConfigIsAppliedToView(): void + { + $engine = new Engine(); + $this->assertFalse($engine->view()->restrictToPath); + + $engine = new Engine(); + $engine->set('flight.views.restrict_to_path', true); + $this->assertTrue($engine->view()->restrictToPath); + } + }