Theme: Added handling for functions.php file load error
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
This commit is contained in:
parent
8e3f8de627
commit
6e098905d4
4 changed files with 39 additions and 10 deletions
7
app/Exceptions/ThemeException.php
Normal file
7
app/Exceptions/ThemeException.php
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Exceptions;
|
||||||
|
|
||||||
|
class ThemeException extends \Exception
|
||||||
|
{
|
||||||
|
}
|
|
@ -3,19 +3,23 @@
|
||||||
namespace BookStack\Theming;
|
namespace BookStack\Theming;
|
||||||
|
|
||||||
use BookStack\Access\SocialAuthService;
|
use BookStack\Access\SocialAuthService;
|
||||||
|
use BookStack\Exceptions\ThemeException;
|
||||||
use Illuminate\Console\Application;
|
use Illuminate\Console\Application;
|
||||||
use Illuminate\Console\Application as Artisan;
|
use Illuminate\Console\Application as Artisan;
|
||||||
use Symfony\Component\Console\Command\Command;
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
|
||||||
class ThemeService
|
class ThemeService
|
||||||
{
|
{
|
||||||
protected $listeners = [];
|
/**
|
||||||
|
* @var array<string, callable[]>
|
||||||
|
*/
|
||||||
|
protected array $listeners = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Listen to a given custom theme event,
|
* Listen to a given custom theme event,
|
||||||
* setting up the action to be ran when the event occurs.
|
* 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])) {
|
if (!isset($this->listeners[$event])) {
|
||||||
$this->listeners[$event] = [];
|
$this->listeners[$event] = [];
|
||||||
|
@ -31,10 +35,8 @@ class ThemeService
|
||||||
*
|
*
|
||||||
* If a callback returns a non-null value, this method will
|
* If a callback returns a non-null value, this method will
|
||||||
* stop and return that value itself.
|
* 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) {
|
foreach ($this->listeners[$event] ?? [] as $action) {
|
||||||
$result = call_user_func_array($action, $args);
|
$result = call_user_func_array($action, $args);
|
||||||
|
@ -49,7 +51,7 @@ class ThemeService
|
||||||
/**
|
/**
|
||||||
* Register a new custom artisan command to be available.
|
* 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) {
|
Artisan::starting(function (Application $application) use ($command) {
|
||||||
$application->addCommands([$command]);
|
$application->addCommands([$command]);
|
||||||
|
@ -59,18 +61,22 @@ class ThemeService
|
||||||
/**
|
/**
|
||||||
* Read any actions from the set theme path if the 'functions.php' file exists.
|
* 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');
|
$themeActionsFile = theme_path('functions.php');
|
||||||
if ($themeActionsFile && file_exists($themeActionsFile)) {
|
if ($themeActionsFile && file_exists($themeActionsFile)) {
|
||||||
|
try {
|
||||||
require $themeActionsFile;
|
require $themeActionsFile;
|
||||||
|
} catch (\Error $exception) {
|
||||||
|
throw new ThemeException("Failed loading theme functions file at \"{$themeActionsFile}\" with error: {$exception->getMessage()}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see SocialAuthService::addSocialDriver
|
* @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 = app()->make(SocialAuthService::class);
|
||||||
$socialAuthService->addSocialDriver($driverName, $config, $socialiteHandler, $configureForRedirect);
|
$socialAuthService->addSocialDriver($driverName, $config, $socialiteHandler, $configureForRedirect);
|
||||||
|
|
|
@ -65,7 +65,9 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@yield('bottom')
|
@yield('bottom')
|
||||||
|
@if($cspNonce ?? false)
|
||||||
<script src="{{ versioned_asset('dist/app.js') }}" nonce="{{ $cspNonce }}"></script>
|
<script src="{{ versioned_asset('dist/app.js') }}" nonce="{{ $cspNonce }}"></script>
|
||||||
|
@endif
|
||||||
@yield('scripts')
|
@yield('scripts')
|
||||||
|
|
||||||
@include('layouts.parts.base-body-end')
|
@include('layouts.parts.base-body-end')
|
||||||
|
|
|
@ -8,6 +8,7 @@ use BookStack\Activity\Models\Webhook;
|
||||||
use BookStack\Entities\Models\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Models\Page;
|
use BookStack\Entities\Models\Page;
|
||||||
use BookStack\Entities\Tools\PageContent;
|
use BookStack\Entities\Tools\PageContent;
|
||||||
|
use BookStack\Exceptions\ThemeException;
|
||||||
use BookStack\Facades\Theme;
|
use BookStack\Facades\Theme;
|
||||||
use BookStack\Theming\ThemeEvents;
|
use BookStack\Theming\ThemeEvents;
|
||||||
use BookStack\Users\Models\User;
|
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()
|
public function test_event_commonmark_environment_configure()
|
||||||
{
|
{
|
||||||
$callbackCalled = false;
|
$callbackCalled = false;
|
||||||
|
|
Loading…
Reference in a new issue