xiaowang/kirby/src/Panel/Home.php
2022-08-31 16:08:03 +02:00

264 lines
6.8 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Kirby\Panel;
use Kirby\Cms\App;
use Kirby\Cms\User;
use Kirby\Exception\InvalidArgumentException;
use Kirby\Exception\NotFoundException;
use Kirby\Http\Router;
use Kirby\Http\Uri;
use Kirby\Toolkit\Str;
use Throwable;
/**
* The Home class creates the secure redirect
* URL after logins. The URL can either come
* from the session to remember the last view
* before the automatic logout, or from a user
* blueprint to redirect to custom views.
*
* The Home class also makes sure to check access
* before a redirect happens and avoids redirects
* to inaccessible views.
* @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 Home
{
/**
* Returns an alternative URL if access
* to the first choice is blocked.
*
* It will go through the entire menu and
* take the first area which is not disabled
* or locked in other ways
*
* @param \Kirby\Cms\User $user
* @return string
*/
public static function alternative(User $user): string
{
$permissions = $user->role()->permissions();
// no access to the panel? The only good alternative is the main url
if ($permissions->for('access', 'panel') === false) {
return App::instance()->site()->url();
}
// needed to create a proper menu
$areas = Panel::areas();
$menu = View::menu($areas, $permissions->toArray());
// go through the menu and search for the first
// available view we can go to
foreach ($menu as $menuItem) {
// skip separators
if ($menuItem === '-') {
continue;
}
// skip disabled items
if (($menuItem['disabled'] ?? false) === true) {
continue;
}
// skip the logout button
if ($menuItem['id'] === 'logout') {
continue;
}
return Panel::url($menuItem['link']);
}
throw new NotFoundException('Theres no available Panel page to redirect to');
}
/**
* Checks if the user has access to the given
* panel path. This is quite tricky, because we
* need to call a trimmed down router to check
* for available routes and their firewall status.
*
* @param \Kirby\Cms\User
* @param string $path
* @return bool
*/
public static function hasAccess(User $user, string $path): bool
{
$areas = Panel::areas();
$routes = Panel::routes($areas);
// Remove fallback routes. Otherwise a route
// would be found even if the view does
// not exist at all.
foreach ($routes as $index => $route) {
if ($route['pattern'] === '(:all)') {
unset($routes[$index]);
}
}
// create a dummy router to check if we can access this route at all
try {
return Router::execute($path, 'GET', $routes, function ($route) use ($user) {
$auth = $route->attributes()['auth'] ?? true;
$areaId = $route->attributes()['area'] ?? null;
$type = $route->attributes()['type'] ?? 'view';
// only allow redirects to views
if ($type !== 'view') {
return false;
}
// if auth is not required the redirect is allowed
if ($auth === false) {
return true;
}
// check the firewall
return Panel::hasAccess($user, $areaId);
});
} catch (Throwable $e) {
return false;
}
}
/**
* Checks if the given Uri has the same domain
* as the index URL of the Kirby installation.
* This is used to block external URLs to third-party
* domains as redirect options.
*
* @param \Kirby\Http\Uri $uri
* @return bool
*/
public static function hasValidDomain(Uri $uri): bool
{
$rootUrl = App::instance()->site()->url();
return $uri->domain() === (new Uri($rootUrl))->domain();
}
/**
* Checks if the given URL is a Panel Url.
*
* @param string $url
* @return bool
*/
public static function isPanelUrl(string $url): bool
{
return Str::startsWith($url, App::instance()->url('panel'));
}
/**
* Returns the path after /panel/ which can then
* be used in the router or to find a matching view
*
* @param string $url
* @return string|null
*/
public static function panelPath(string $url): ?string
{
$after = Str::after($url, App::instance()->url('panel'));
return trim($after, '/');
}
/**
* Returns the Url that has been stored in the session
* before the last logout. We take this Url if possible
* to redirect the user back to the last point where they
* left before they got logged out.
*
* @return string|null
*/
public static function remembered(): ?string
{
// check for a stored path after login
$remembered = App::instance()->session()->pull('panel.path');
// convert the result to an absolute URL if available
return $remembered ? Panel::url($remembered) : null;
}
/**
* Tries to find the best possible Url to redirect
* the user to after the login.
*
* When the user got logged out, we try to send them back
* to the point where they left.
*
* If they have a custom redirect Url defined in their blueprint
* via the `home` option, we send them there if no Url is stored
* in the session.
*
* If none of the options above find any result, we try to send
* them to the site view.
*
* Before the redirect happens, the final Url is sanitized, the query
* and params are removed to avoid any attacks and the domain is compared
* to avoid redirects to external Urls.
*
* Afterwards, we also check for permissions before the redirect happens
* to avoid redirects to inaccessible Panel views. In such a case
* the next best accessible view is picked from the menu.
*
* @return string
*/
public static function url(): string
{
$user = App::instance()->user();
// if there's no authenticated user, all internal
// redirects will be blocked and the user is redirected
// to the login instead
if (!$user) {
return Panel::url('login');
}
// get the last visited url from the session or the custom home
$url = static::remembered() ?? $user->panel()->home();
// inspect the given URL
$uri = new Uri($url);
// compare domains to avoid external redirects
if (static::hasValidDomain($uri) !== true) {
throw new InvalidArgumentException('External URLs are not allowed for Panel redirects');
}
// remove all params to avoid
// possible attack vectors
$uri->params = '';
$uri->query = '';
// get a clean version of the URL
$url = $uri->toString();
// Don't further inspect URLs outside of the Panel
if (static::isPanelUrl($url) === false) {
return $url;
}
// get the plain panel path
$path = static::panelPath($url);
// a redirect to login, logout or installation
// views would lead to an infinite redirect loop
if (in_array($path, ['', 'login', 'logout', 'installation'], true) === true) {
$path = 'site';
}
// Check if the user can access the URL
if (static::hasAccess($user, $path) === true) {
return Panel::url($path);
}
// Try to find an alternative
return static::alternative($user);
}
}