593 lines
14 KiB
PHP
593 lines
14 KiB
PHP
<?php
|
|
|
|
namespace Kirby\Panel;
|
|
|
|
use Closure;
|
|
use Kirby\Cms\App;
|
|
use Kirby\Cms\Url as CmsUrl;
|
|
use Kirby\Cms\User;
|
|
use Kirby\Exception\Exception;
|
|
use Kirby\Exception\NotFoundException;
|
|
use Kirby\Exception\PermissionException;
|
|
use Kirby\Http\Response;
|
|
use Kirby\Http\Router;
|
|
use Kirby\Http\Uri;
|
|
use Kirby\Http\Url;
|
|
use Kirby\Toolkit\Str;
|
|
use Kirby\Toolkit\Tpl;
|
|
use Throwable;
|
|
|
|
/**
|
|
* The Panel class is only responsible to create
|
|
* a working panel view with all the right URLs
|
|
* and other panel options. The view template is
|
|
* located in `kirby/views/panel.php`
|
|
* @since 3.6.0
|
|
*
|
|
* @package Kirby Panel
|
|
* @author Bastian Allgeier <bastian@getkirby.com>
|
|
* @link https://getkirby.com
|
|
* @copyright Bastian Allgeier
|
|
* @license https://getkirby.com/license
|
|
*/
|
|
class Panel
|
|
{
|
|
/**
|
|
* Normalize a panel area
|
|
*/
|
|
public static function area(string $id, array $area): array
|
|
{
|
|
$area['id'] = $id;
|
|
$area['label'] ??= $id;
|
|
$area['breadcrumb'] ??= [];
|
|
$area['breadcrumbLabel'] ??= $area['label'];
|
|
$area['title'] = $area['label'];
|
|
$area['menu'] ??= false;
|
|
$area['link'] ??= $id;
|
|
$area['search'] ??= null;
|
|
|
|
return $area;
|
|
}
|
|
|
|
/**
|
|
* Collect all registered areas
|
|
*/
|
|
public static function areas(): array
|
|
{
|
|
$kirby = App::instance();
|
|
$system = $kirby->system();
|
|
$user = $kirby->user();
|
|
$areas = $kirby->load()->areas();
|
|
|
|
// the system is not ready
|
|
if (
|
|
$system->isOk() === false ||
|
|
$system->isInstalled() === false
|
|
) {
|
|
return [
|
|
'installation' => static::area(
|
|
'installation',
|
|
$areas['installation']
|
|
),
|
|
];
|
|
}
|
|
|
|
// not yet authenticated
|
|
if (!$user) {
|
|
return [
|
|
'logout' => static::area('logout', $areas['logout']),
|
|
// login area last because it defines a fallback route
|
|
'login' => static::area('login', $areas['login']),
|
|
];
|
|
}
|
|
|
|
unset($areas['installation'], $areas['login']);
|
|
|
|
// Disable the language area for single-language installations
|
|
// This does not check for installed languages. Otherwise you'd
|
|
// not be able to add the first language through the view
|
|
if (!$kirby->option('languages')) {
|
|
unset($areas['languages']);
|
|
}
|
|
|
|
$result = [];
|
|
|
|
foreach ($areas as $id => $area) {
|
|
$result[$id] = static::area($id, $area);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Check for access permissions
|
|
*/
|
|
public static function firewall(
|
|
User|null $user = null,
|
|
string|null $areaId = null
|
|
): bool {
|
|
// a user has to be logged in
|
|
if ($user === null) {
|
|
throw new PermissionException(['key' => 'access.panel']);
|
|
}
|
|
|
|
// get all access permissions for the user role
|
|
$permissions = $user->role()->permissions()->toArray()['access'];
|
|
|
|
// check for general panel access
|
|
if (($permissions['panel'] ?? true) !== true) {
|
|
throw new PermissionException(['key' => 'access.panel']);
|
|
}
|
|
|
|
// don't check if the area is not defined
|
|
if (empty($areaId) === true) {
|
|
return true;
|
|
}
|
|
|
|
// undefined area permissions means access
|
|
if (isset($permissions[$areaId]) === false) {
|
|
return true;
|
|
}
|
|
|
|
// no access
|
|
if ($permissions[$areaId] !== true) {
|
|
throw new PermissionException(['key' => 'access.view']);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
|
|
/**
|
|
* Redirect to a Panel url
|
|
*
|
|
* @throws \Kirby\Panel\Redirect
|
|
* @codeCoverageIgnore
|
|
*/
|
|
public static function go(string|null $url = null, int $code = 302): void
|
|
{
|
|
throw new Redirect(static::url($url), $code);
|
|
}
|
|
|
|
/**
|
|
* Check if the given user has access to the panel
|
|
* or to a given area
|
|
*/
|
|
public static function hasAccess(
|
|
User|null $user = null,
|
|
string|null $area = null
|
|
): bool {
|
|
try {
|
|
static::firewall($user, $area);
|
|
return true;
|
|
} catch (Throwable) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks for a Fiber request
|
|
* via get parameters or headers
|
|
*/
|
|
public static function isFiberRequest(): bool
|
|
{
|
|
$request = App::instance()->request();
|
|
|
|
if ($request->method() === 'GET') {
|
|
return
|
|
(bool)($request->get('_json') ??
|
|
$request->header('X-Fiber'));
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns a JSON response
|
|
* for Fiber calls
|
|
*/
|
|
public static function json(array $data, int $code = 200): Response
|
|
{
|
|
$request = App::instance()->request();
|
|
|
|
return Response::json($data, $code, $request->get('_pretty'), [
|
|
'X-Fiber' => 'true',
|
|
'Cache-Control' => 'no-store, private'
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Checks for a multilanguage installation
|
|
*/
|
|
public static function multilang(): bool
|
|
{
|
|
// multilang setup check
|
|
$kirby = App::instance();
|
|
return $kirby->option('languages') || $kirby->multilang();
|
|
}
|
|
|
|
/**
|
|
* Returns the referrer path if present
|
|
*/
|
|
public static function referrer(): string
|
|
{
|
|
$request = App::instance()->request();
|
|
|
|
$referrer = $request->header('X-Fiber-Referrer')
|
|
?? $request->get('_referrer')
|
|
?? '';
|
|
|
|
return '/' . trim($referrer, '/');
|
|
}
|
|
|
|
/**
|
|
* Creates a Response object from the result of
|
|
* a Panel route call
|
|
*/
|
|
public static function response($result, array $options = []): Response
|
|
{
|
|
// pass responses directly down to the Kirby router
|
|
if ($result instanceof Response) {
|
|
return $result;
|
|
}
|
|
|
|
// interpret missing/empty results as not found
|
|
if ($result === null || $result === false) {
|
|
$result = new NotFoundException('The data could not be found');
|
|
|
|
// interpret strings as errors
|
|
} elseif (is_string($result) === true) {
|
|
$result = new Exception($result);
|
|
}
|
|
|
|
// handle different response types (view, dialog, ...)
|
|
return match ($options['type'] ?? null) {
|
|
'dialog' => Dialog::response($result, $options),
|
|
'drawer' => Drawer::response($result, $options),
|
|
'dropdown' => Dropdown::response($result, $options),
|
|
'request' => Request::response($result, $options),
|
|
'search' => Search::response($result, $options),
|
|
default => View::response($result, $options)
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Router for the Panel views
|
|
*/
|
|
public static function router(string|null $path = null): Response|null
|
|
{
|
|
$kirby = App::instance();
|
|
|
|
if ($kirby->option('panel') === false) {
|
|
return null;
|
|
}
|
|
|
|
// set the translation for Panel UI before
|
|
// gathering areas and routes, so that the
|
|
// `t()` helper can already be used
|
|
static::setTranslation();
|
|
|
|
// set the language in multi-lang installations
|
|
static::setLanguage();
|
|
|
|
$areas = static::areas();
|
|
$routes = static::routes($areas);
|
|
|
|
// create a micro-router for the Panel
|
|
return Router::execute($path, $method = $kirby->request()->method(), $routes, function ($route) use ($areas, $kirby, $method, $path) {
|
|
// route needs authentication?
|
|
$auth = $route->attributes()['auth'] ?? true;
|
|
$areaId = $route->attributes()['area'] ?? null;
|
|
$type = $route->attributes()['type'] ?? 'view';
|
|
$area = $areas[$areaId] ?? null;
|
|
|
|
// call the route action to check the result
|
|
try {
|
|
// trigger hook
|
|
$route = $kirby->apply(
|
|
'panel.route:before',
|
|
compact('route', 'path', 'method'),
|
|
'route'
|
|
);
|
|
|
|
// check for access before executing area routes
|
|
if ($auth !== false) {
|
|
static::firewall($kirby->user(), $areaId);
|
|
}
|
|
|
|
$result = $route->action()->call($route, ...$route->arguments());
|
|
} catch (Throwable $e) {
|
|
$result = $e;
|
|
}
|
|
|
|
$response = static::response($result, [
|
|
'area' => $area,
|
|
'areas' => $areas,
|
|
'path' => $path,
|
|
'type' => $type
|
|
]);
|
|
|
|
return $kirby->apply(
|
|
'panel.route:after',
|
|
compact('route', 'path', 'method', 'response'),
|
|
'response'
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Extract the routes from the given array
|
|
* of active areas.
|
|
*/
|
|
public static function routes(array $areas): array
|
|
{
|
|
$kirby = App::instance();
|
|
|
|
// the browser incompatibility
|
|
// warning is always needed
|
|
$routes = [
|
|
[
|
|
'pattern' => 'browser',
|
|
'auth' => false,
|
|
'action' => fn () => new Response(
|
|
Tpl::load($kirby->root('kirby') . '/views/browser.php')
|
|
),
|
|
]
|
|
];
|
|
|
|
// register all routes from areas
|
|
foreach ($areas as $areaId => $area) {
|
|
$routes = array_merge(
|
|
$routes,
|
|
static::routesForViews($areaId, $area),
|
|
static::routesForSearches($areaId, $area),
|
|
static::routesForDialogs($areaId, $area),
|
|
static::routesForDrawers($areaId, $area),
|
|
static::routesForDropdowns($areaId, $area),
|
|
static::routesForRequests($areaId, $area),
|
|
);
|
|
}
|
|
|
|
// if the Panel is already installed and/or the
|
|
// user is authenticated, those areas won't be
|
|
// included, which is why we add redirect routes
|
|
// to main Panel view as fallbacks
|
|
$routes[] = [
|
|
'pattern' => [
|
|
'/',
|
|
'installation',
|
|
'login',
|
|
],
|
|
'action' => fn () => Panel::go(Home::url()),
|
|
'auth' => false
|
|
];
|
|
|
|
// catch all route
|
|
$routes[] = [
|
|
'pattern' => '(:all)',
|
|
'action' => fn (string $pattern) => 'Could not find Panel view for route: ' . $pattern
|
|
];
|
|
|
|
return $routes;
|
|
}
|
|
|
|
/**
|
|
* Extract all routes from an area
|
|
*/
|
|
public static function routesForDialogs(string $areaId, array $area): array
|
|
{
|
|
$dialogs = $area['dialogs'] ?? [];
|
|
$routes = [];
|
|
|
|
foreach ($dialogs as $dialogId => $dialog) {
|
|
$routes = array_merge($routes, Dialog::routes(
|
|
id: $dialogId,
|
|
areaId: $areaId,
|
|
prefix: 'dialogs',
|
|
options: $dialog
|
|
));
|
|
}
|
|
|
|
return $routes;
|
|
}
|
|
|
|
/**
|
|
* Extract all routes from an area
|
|
*/
|
|
public static function routesForDrawers(string $areaId, array $area): array
|
|
{
|
|
$drawers = $area['drawers'] ?? [];
|
|
$routes = [];
|
|
|
|
foreach ($drawers as $drawerId => $drawer) {
|
|
$routes = array_merge($routes, Drawer::routes(
|
|
id: $drawerId,
|
|
areaId: $areaId,
|
|
prefix: 'drawers',
|
|
options: $drawer
|
|
));
|
|
}
|
|
|
|
return $routes;
|
|
}
|
|
|
|
/**
|
|
* Extract all routes for dropdowns
|
|
*/
|
|
public static function routesForDropdowns(string $areaId, array $area): array
|
|
{
|
|
$dropdowns = $area['dropdowns'] ?? [];
|
|
$routes = [];
|
|
|
|
foreach ($dropdowns as $dropdownId => $dropdown) {
|
|
$routes = array_merge($routes, Dropdown::routes(
|
|
id: $dropdownId,
|
|
areaId: $areaId,
|
|
prefix: 'dropdowns',
|
|
options: $dropdown
|
|
));
|
|
}
|
|
|
|
return $routes;
|
|
}
|
|
|
|
/**
|
|
* Extract all routes from an area
|
|
*/
|
|
public static function routesForRequests(string $areaId, array $area): array
|
|
{
|
|
$routes = $area['requests'] ?? [];
|
|
|
|
foreach ($routes as $key => $route) {
|
|
$routes[$key]['area'] = $areaId;
|
|
$routes[$key]['type'] = 'request';
|
|
}
|
|
|
|
return $routes;
|
|
}
|
|
|
|
/**
|
|
* Extract all routes for searches
|
|
*/
|
|
public static function routesForSearches(string $areaId, array $area): array
|
|
{
|
|
$searches = $area['searches'] ?? [];
|
|
$routes = [];
|
|
|
|
foreach ($searches as $name => $params) {
|
|
// create the full routing pattern
|
|
$pattern = 'search/' . $name;
|
|
|
|
// load event
|
|
$routes[] = [
|
|
'pattern' => $pattern,
|
|
'type' => 'search',
|
|
'area' => $areaId,
|
|
'action' => function () use ($params) {
|
|
$kirby = App::instance();
|
|
$request = $kirby->request();
|
|
$query = $request->get('query');
|
|
$limit = (int)$request->get('limit', $kirby->option('panel.search.limit', 10));
|
|
$page = (int)$request->get('page', 1);
|
|
|
|
return $params['query']($query, $limit, $page);
|
|
}
|
|
];
|
|
}
|
|
|
|
return $routes;
|
|
}
|
|
|
|
/**
|
|
* Extract all views from an area
|
|
*/
|
|
public static function routesForViews(string $areaId, array $area): array
|
|
{
|
|
$views = $area['views'] ?? [];
|
|
$routes = [];
|
|
|
|
foreach ($views as $view) {
|
|
$view['area'] = $areaId;
|
|
$view['type'] = 'view';
|
|
|
|
$when = $view['when'] ?? null;
|
|
unset($view['when']);
|
|
|
|
// enable the route by default, but if there is a
|
|
// when condition closure, it must return `true`
|
|
if (
|
|
$when instanceof Closure === false ||
|
|
$when($view, $area) === true
|
|
) {
|
|
$routes[] = $view;
|
|
}
|
|
}
|
|
|
|
return $routes;
|
|
}
|
|
|
|
/**
|
|
* Set the current language in multi-lang
|
|
* installations based on the session or the
|
|
* query language query parameter
|
|
*/
|
|
public static function setLanguage(): string|null
|
|
{
|
|
$kirby = App::instance();
|
|
|
|
// language switcher
|
|
if (static::multilang()) {
|
|
$fallback = 'en';
|
|
|
|
if ($defaultLanguage = $kirby->defaultLanguage()) {
|
|
$fallback = $defaultLanguage->code();
|
|
}
|
|
|
|
$session = $kirby->session();
|
|
$sessionLanguage = $session->get('panel.language', $fallback);
|
|
$language = $kirby->request()->get('language') ?? $sessionLanguage;
|
|
|
|
// keep the language for the next visit
|
|
if ($language !== $sessionLanguage) {
|
|
$session->set('panel.language', $language);
|
|
}
|
|
|
|
// activate the current language in Kirby
|
|
$kirby->setCurrentLanguage($language);
|
|
|
|
return $language;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Set the currently active Panel translation
|
|
* based on the current user or config
|
|
*/
|
|
public static function setTranslation(): string
|
|
{
|
|
$kirby = App::instance();
|
|
|
|
// use the user language for the default translation or
|
|
// fall back to the language from the config
|
|
$translation = $kirby->user()?->language() ??
|
|
$kirby->panelLanguage();
|
|
|
|
$kirby->setCurrentTranslation($translation);
|
|
|
|
return $translation;
|
|
}
|
|
|
|
/**
|
|
* Creates an absolute Panel URL
|
|
* independent of the Panel slug config
|
|
*/
|
|
public static function url(string|null $url = null, array $options = []): string
|
|
{
|
|
// only touch relative paths
|
|
if (Url::isAbsolute($url) === false) {
|
|
$kirby = App::instance();
|
|
$slug = $kirby->option('panel.slug', 'panel');
|
|
$path = trim($url, '/');
|
|
|
|
$baseUri = new Uri($kirby->url());
|
|
$basePath = trim($baseUri->path()->toString(), '/');
|
|
|
|
// removes base path if relative path contains it
|
|
if (empty($basePath) === false && Str::startsWith($path, $basePath) === true) {
|
|
$path = Str::after($path, $basePath);
|
|
}
|
|
// add the panel slug prefix if it it's not
|
|
// included in the path yet
|
|
elseif (Str::startsWith($path, $slug . '/') === false) {
|
|
$path = $slug . '/' . $path;
|
|
}
|
|
|
|
// create an absolute URL
|
|
$url = CmsUrl::to($path, $options);
|
|
}
|
|
|
|
return $url;
|
|
}
|
|
}
|