Administration/src/FuzeWorks/Administration/AdminPlugin.php

420 lines
16 KiB
PHP
Executable File

<?php
/**
* FuzeWorks Framework Administration Plugin.
*
* The FuzeWorks PHP FrameWork
*
* Copyright (C) 2013-2020 i15
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* @author i15
* @copyright Copyright (c) 2013 - 2020, i15. (https://i15.nl)
* @license https://opensource.org/licenses/MIT MIT License
*
* @since Version 1.3.0
*
* @version Version 1.3.0
*/
namespace FuzeWorks\Administration;
use FuzeWorks\Administration\Attributes\DisplayAttribute;
use FuzeWorks\Administration\Attributes\FooterCodeMethodAttribute;
use FuzeWorks\Administration\Attributes\IconAttribute;
use FuzeWorks\Administration\Attributes\PermissionAttribute;
use FuzeWorks\Authentication\AuthenticationPlugin;
use FuzeWorks\Authentication\Model\Session;
use FuzeWorks\Config;
use FuzeWorks\ConfigORM\ConfigORM;
use FuzeWorks\Controllers;
use FuzeWorks\Core;
use FuzeWorks\Event\LayoutLoadEvent;
use FuzeWorks\Event\RouterCallViewEvent;
use FuzeWorks\Event\RouteWebRequestEvent;
use FuzeWorks\Events;
use FuzeWorks\Exception\ConfigException;
use FuzeWorks\Exception\EventException;
use FuzeWorks\Exception\Exception;
use FuzeWorks\Exception\FactoryException;
use FuzeWorks\Exception\HaltException;
use FuzeWorks\Exception\LayoutException;
use FuzeWorks\Exception\OutputException;
use FuzeWorks\Exception\WebException;
use FuzeWorks\Factory;
use FuzeWorks\iPluginHeader;
use FuzeWorks\Layout;
use FuzeWorks\Logger;
use FuzeWorks\Models;
use FuzeWorks\Output;
use FuzeWorks\Priority;
use FuzeWorks\Resources;
use FuzeWorks\Router;
use FuzeWorks\View;
use FuzeWorks\Views;
use ReflectionClass;
use ReflectionException;
class AdminPlugin implements iPluginHeader
{
private Config $config;
private Router $router;
private Models $models;
private Views $views;
private Controllers $controllers;
private AuthenticationPlugin $authPlugin;
private string $pluginKey = 'admin';
private string $pluginPath;
private string $adminURL;
private string $apiRoute;
private string $webRoute;
private ?Session $session = null;
private bool $loadAsync = false;
/**
* @inheritDoc
* @throws EventException
* @throws FactoryException
* @throws ConfigException
*/
public function init()
{
// Make a listener for a web request
$this->pluginPath = dirname(__DIR__, 3);
// Load the admin configuration
$this->config = Factory::getInstance('config');
$this->config->addComponentPath($this->pluginPath, Priority::LOWEST);
$adminCFG = $this->config->getConfig('admin');
// If admin is not enabled, stop here
if (!$adminCFG->get('admin_enabled'))
return;
// Register the event
Events::addListener([$this, 'routeWebRequestEventListener'], 'routeWebRequestEvent', Priority::NORMAL);
// Now load the pluginKey
$this->pluginKey = $adminCFG->get('admin_url');
// Settle admin URL
$webURL = $this->config->getConfig("web")->get("base_url");
$this->adminURL = $webURL . "/" . $this->pluginKey;
// Determine routing paths
$this->webRoute = '^' . $this->pluginKey . '(|\/(?P<viewName>.*?)(|\/(?P<viewMethod>.*?)(|\/(?P<viewParameters>.*?))))';
$this->apiRoute = '^' . $this->pluginKey . '(|\/(?P<viewName>.*?)(|\/(?P<viewMethod>.*?)(|\/(?P<viewParameters>.*?)))).json';
// Load the dependencies
$this->router = Factory::getInstance('router');
$this->models = Factory::getInstance('models');
$this->views = Factory::getInstance('views');
$this->controllers = Factory::getInstance('controllers');
}
/**
* @param RouteWebRequestEvent $event
* @return void
* @throws FactoryException
* @throws WebException
*/
public function routeWebRequestEventListener(RouteWebRequestEvent $event): void
{
Logger::log("Administration: observed web request. Activating plugin.");
// If it does, register everything
/** @var Resources $resources */
$resources = Factory::getInstance('resources');
// Serve the AdminLTE distribution files
$adminLTE = dirname(Core::$coreDir, 4) . DS . 'vendor' . DS . 'almasaeed2010' . DS . 'adminlte';
$resources->registerResources('admin/lte_dist', $adminLTE . DS . 'dist');
$resources->registerResources('admin/lte_plugins', $adminLTE . DS . 'plugins');
$resources->registerResources("admin/admin_dist", $this->pluginPath . DS . 'www' . DS . 'dist');
// @todo TEMPORARY!!
//$resources->registerResources("lte_top", $adminLTE);
// And serve the actual pages
$this->router->addRoute('^' . $this->pluginKey, ['viewName' => 'dashboard', 'callable' => [$this, 'adminCallable']], Priority::HIGHEST);
$this->router->addRoute($this->apiRoute, ['callable' => [$this, 'adminCallable']], Priority::HIGH);
$this->router->addRoute($this->webRoute, ['callable' => [$this, 'adminCallable']], Priority::HIGH);
}
/**
* @param array $matches
* @param array $routeData
* @param string $routeString
* @return string|null
* @throws EventException
* @throws FactoryException
* @throws LayoutException
* @throws OutputException
*/
public function adminCallable(array $matches, array $routeData, string $routeString): ?string
{
Logger::log("AdminCallable called. Loading admin page.");
// Add componentPaths for models, views, controllers
$this->models->addComponentPath($this->pluginPath . DS . 'models' . DS . 'main', Priority::LOW);
$this->views->addComponentPath($this->pluginPath . DS . 'views' . DS . 'main', Priority::LOW);
$this->controllers->addComponentPath($this->pluginPath . DS . 'controllers' . DS . 'main', Priority::LOW);
// Load layouts and assign componentPath, in case the loaded view needs access to it
/** @var Layout $layouts */
$layouts = Factory::getInstance('layouts');
$layouts->addComponentPath($this->pluginPath . DS . 'layouts', Priority::LOW);
// And add a layoutLoadEventListener, to add global administration variables
Events::addListener([$this, 'layoutLoadEventListener'], 'layoutLoadEvent', Priority::HIGH);
Events::addListener([$this, 'verifyViewPermissions'], 'routerCallViewEvent', Priority::HIGH);
Events::addListener([$this, 'logViewNameAndIcon'], 'routerCallViewEvent', Priority::NORMAL);
if (class_exists("\FuzeWorks\Async\Tasks"))
{
// Mark async to be loaded
$this->loadAsync = true;
// Add componentPaths for models, views, controllers
$this->models->addComponentPath($this->pluginPath . DS . 'models' . DS . 'async', Priority::LOW);
$this->views->addComponentPath($this->pluginPath . DS . 'views' . DS . 'async', Priority::LOW);
$this->controllers->addComponentPath($this->pluginPath . DS . 'controllers' . DS . 'async', Priority::LOW);
}
// Load the current session
/** @var Output $output */
$this->authPlugin = Factory::getInstance("plugins")->get('auth');
$output = Factory::getInstance("output");
// Redirect the user to login if they're not logged in
$this->session = $this->authPlugin->sessions->start();
if ($this->session->user->id === "0")
{
$output->setHeader("Location: " . $this->authPlugin->getAuthenticationURL() . "/login?location=" . $this->getAdminURL());
return "";
}
// Let's generate the sidebar
$finder = new PageFinder();
$sidebar = $finder->generateSidebar($this->session);
$footer = "";
// Afterwards, pass on to the admin view and its contents
Logger::log("Forwarding request to Router::defaultCallable().");
// Determine viewType
if ($routeString === $this->apiRoute)
$routeData['viewType'] = 'AdminAPI';
else
$routeData['viewType'] = 'admin';
try {
// Fetch content from requested view
$content = $this->router->defaultCallable($matches, $routeData, $routeString);
// If content is false, nothing is found and should return 404
if (is_bool($content) && $content === false)
{
$output->setStatusHeader(404);
$content = $layouts->get("main/error404");
$this->selectedMethodName = "Not found";
}
// And check for footer code
if (!empty($this->footerMethod))
{
if (!method_exists($this->selectedView, $this->footerMethod))
throw new Exception("Could not load view. Requested footerMethod not found.");
$footer = call_user_func_array([$this->selectedView, $this->footerMethod], []);
}
} catch (HaltException $e) {
$output->setStatusHeader(403);
$content = $layouts->get("main/error403");
$this->selectedMethodName = "Unauthorized";
} catch (Exception|\Throwable $e) {
Logger::exceptionHandler($e, false);
$output->setStatusHeader(500);
$content = $layouts->get("main/error500");
$this->selectedMethodName = "Fatal error";
}
// If an api is requested, return the content as a json object
if ($routeData['viewType'] === 'AdminAPI')
{
$output->setContentType('json');
return json_encode($content);
}
// Otherwise, load the panel and return that
Logger::log("Generating panel wrapper.");
$layouts->reset(false);
$layouts->assign("pageTitle", $this->selectedMethodName);
$layouts->assign("pageIcon", $this->selectedMethodIcon);
$layouts->assign('sidebar', $sidebar);
$layouts->assign('content', $content);
$layouts->assign("footer", $footer);
return $layouts->get('main/panel');
}
public function getAdminURL(): string
{
return $this->adminURL;
}
/**
* Listener for LayoutLoadEvent
*
* Assigns variables to Layout that this plugin's layouts may require.
*
* @param LayoutLoadEvent $event
*/
public function layoutLoadEventListener(LayoutLoadEvent $event)
{
$event->assign('adminKey', $this->pluginKey);
$event->assign('session', $this->session);
$event->assign('authURL', $this->authPlugin->getAuthenticationURL());
$event->assign("loadAsync", $this->loadAsync);
}
/**
* Listener for RouterCallViewEvent
*
* Verifies that the current user has access to the requested methods.
*
* @param RouterCallViewEvent $event
* @return RouterCallViewEvent
* @throws ReflectionException
* @throws HaltException
*/
public function verifyViewPermissions(RouterCallViewEvent $event): RouterCallViewEvent
{
// Load reflector
$reflector = new ReflectionClass(get_class($event->view));
// Pass over each requested method
for ($i = Priority::getHighestPriority(); $i <= Priority::getLowestPriority(); $i++) {
if (!isset($event->viewMethods[$i]))
continue;
foreach ($event->viewMethods[$i] as $key => $method)
{
// If the method doesn't exist, skip it in general
if (!method_exists($event->view, $method))
{
unset($event->viewMethods[$i][$key]);
continue;
}
// If the method exists, verify permission
$methodReflector = $reflector->getMethod($method);
$permissionAttributes = $methodReflector->getAttributes(PermissionAttribute::class);
if (!empty($permissionAttributes))
{
// Fetch nodes
$nodes = $permissionAttributes[0]->newInstance()->getValue();
// Check if any of the nodes are permitted
$found = false;
foreach ($nodes as $node)
if ($this->session->hasPermission($node))
$found = true;
// If not, skip
if (!$found)
{
Logger::logWarning("Current user does not have permission for the requested method. Blocking.");
$event->setCancelled(true);
}
}
}
}
return $event;
}
protected string $selectedMethodName = "";
protected string $selectedMethodIcon = "";
protected string $footerMethod = "";
protected View $selectedView;
public function logViewNameAndIcon(RouterCallViewEvent $event): RouterCallViewEvent
{
$reflector = new ReflectionClass(get_class($event->view));
$this->selectedView = $event->view;
for ($i = Priority::getHighestPriority(); $i <= Priority::getLowestPriority(); $i++)
{
if (!isset($event->viewMethods[$i]))
continue;
foreach ($event->viewMethods[$i] as $key => $method)
{
$methodReflector = $reflector->getMethod($method);
// Check for display attribute.
$displayAttributes = $methodReflector->getAttributes(DisplayAttribute::class);
$iconAttributes = $methodReflector->getAttributes(IconAttribute::class);
$footerAttributes = $methodReflector->getAttributes(FooterCodeMethodAttribute::class);
$this->selectedMethodName = !empty($displayAttributes) ? $displayAttributes[0]->newInstance()->getValue() : ucfirst($methodReflector->getName());
$this->selectedMethodIcon = !empty($iconAttributes) ? $iconAttributes[0]->newInstance()->getValue() : "";
$this->footerMethod = !empty($footerAttributes) ? $footerAttributes[0]->newInstance()->getvalue() : "";
}
}
return $event;
}
/**
* @inheritDoc
*/
public function getName(): string
{
return 'admin';
}
/**
* @inheritDoc
*/
public function getClassesPrefix(): ?string
{
return null;
}
/**
* @inheritDoc
*/
public function getSourceDirectory(): ?string
{
return null;
}
/**
* @inheritDoc
*/
public function getPluginClass(): ?string
{
return null;
}
}