]> BookStack Code Mirror - bookstack/commitdiff
Theme: Added handling for functions.php file load error
authorDan Brown <redacted>
Tue, 12 Sep 2023 11:34:02 +0000 (12:34 +0100)
committerDan Brown <redacted>
Tue, 12 Sep 2023 11:34:02 +0000 (12:34 +0100)
This adds specific handling for functions.php error loading to re-throw
errors wrapped in a more descriptive message, to make it clear the error
is due to an issue in their functions.php file.

Decided to throw and stop, rather than ignore & continue, to be on the
safe side in the event auth-level (or other security level) customizations
have been made via functions.php.

Adds test to cover.
Closes #4504

app/Exceptions/ThemeException.php [new file with mode: 0644]
app/Theming/ThemeService.php
resources/views/layouts/base.blade.php
tests/ThemeTest.php

diff --git a/app/Exceptions/ThemeException.php b/app/Exceptions/ThemeException.php
new file mode 100644 (file)
index 0000000..b721eff
--- /dev/null
@@ -0,0 +1,7 @@
+<?php
+
+namespace BookStack\Exceptions;
+
+class ThemeException extends \Exception
+{
+}
index bb91643e308114b567119fed99349b7e6951f98e..31a7d3c64d32c778020474fbcc43837467304c61 100644 (file)
@@ -3,19 +3,23 @@
 namespace BookStack\Theming;
 
 use BookStack\Access\SocialAuthService;
+use BookStack\Exceptions\ThemeException;
 use Illuminate\Console\Application;
 use Illuminate\Console\Application as Artisan;
 use Symfony\Component\Console\Command\Command;
 
 class ThemeService
 {
-    protected $listeners = [];
+    /**
+     * @var array<string, callable[]>
+     */
+    protected array $listeners = [];
 
     /**
      * Listen to a given custom theme event,
      * setting up the action to be ran when the event occurs.
      */
-    public function listen(string $event, callable $action)
+    public function listen(string $event, callable $action): void
     {
         if (!isset($this->listeners[$event])) {
             $this->listeners[$event] = [];
@@ -31,10 +35,8 @@ class ThemeService
      *
      * If a callback returns a non-null value, this method will
      * stop and return that value itself.
-     *
-     * @return mixed
      */
-    public function dispatch(string $event, ...$args)
+    public function dispatch(string $event, ...$args): mixed
     {
         foreach ($this->listeners[$event] ?? [] as $action) {
             $result = call_user_func_array($action, $args);
@@ -49,7 +51,7 @@ class ThemeService
     /**
      * Register a new custom artisan command to be available.
      */
-    public function registerCommand(Command $command)
+    public function registerCommand(Command $command): void
     {
         Artisan::starting(function (Application $application) use ($command) {
             $application->addCommands([$command]);
@@ -59,18 +61,22 @@ class ThemeService
     /**
      * Read any actions from the set theme path if the 'functions.php' file exists.
      */
-    public function readThemeActions()
+    public function readThemeActions(): void
     {
         $themeActionsFile = theme_path('functions.php');
         if ($themeActionsFile && file_exists($themeActionsFile)) {
-            require $themeActionsFile;
+            try {
+                require $themeActionsFile;
+            } catch (\Error $exception) {
+                throw new ThemeException("Failed loading theme functions file at \"{$themeActionsFile}\" with error: {$exception->getMessage()}");
+            }
         }
     }
 
     /**
      * @see SocialAuthService::addSocialDriver
      */
-    public function addSocialDriver(string $driverName, array $config, string $socialiteHandler, callable $configureForRedirect = null)
+    public function addSocialDriver(string $driverName, array $config, string $socialiteHandler, callable $configureForRedirect = null): void
     {
         $socialAuthService = app()->make(SocialAuthService::class);
         $socialAuthService->addSocialDriver($driverName, $config, $socialiteHandler, $configureForRedirect);
index e0a6f46d0166c3b9698a4bed4bc389ec63438b38..8875788a63fd3e40be232f0f8fbef707ab843f39 100644 (file)
@@ -65,7 +65,9 @@
     </div>
 
     @yield('bottom')
-    <script src="{{ versioned_asset('dist/app.js') }}" nonce="{{ $cspNonce }}"></script>
+    @if($cspNonce ?? false)
+        <script src="{{ versioned_asset('dist/app.js') }}" nonce="{{ $cspNonce }}"></script>
+    @endif
     @yield('scripts')
 
     @include('layouts.parts.base-body-end')
index 08c99d297724e249402bb15bb9ad98372b89dd34..f0266cd0c1349420fdd4c07724b68d9b77b7ef56 100644 (file)
@@ -8,6 +8,7 @@ use BookStack\Activity\Models\Webhook;
 use BookStack\Entities\Models\Book;
 use BookStack\Entities\Models\Page;
 use BookStack\Entities\Tools\PageContent;
+use BookStack\Exceptions\ThemeException;
 use BookStack\Facades\Theme;
 use BookStack\Theming\ThemeEvents;
 use BookStack\Users\Models\User;
@@ -51,6 +52,19 @@ class ThemeTest extends TestCase
         });
     }
 
+    public function test_theme_functions_loads_errors_are_caught_and_logged()
+    {
+        $this->usingThemeFolder(function ($themeFolder) {
+            $functionsFile = theme_path('functions.php');
+            file_put_contents($functionsFile, "<?php\n\\BookStack\\Biscuits::eat();");
+
+            $this->expectException(ThemeException::class);
+            $this->expectExceptionMessageMatches('/Failed loading theme functions file at ".*?" with error: Class "BookStack\\\\Biscuits" not found/');
+
+            $this->runWithEnv('APP_THEME', $themeFolder, fn() => null);
+        });
+    }
+
     public function test_event_commonmark_environment_configure()
     {
         $callbackCalled = false;