posts/plugins/flex-objects/flex-objects.php

806 lines
25 KiB
PHP

<?php
namespace Grav\Plugin;
use Composer\Autoload\ClassLoader;
use Grav\Common\Debugger;
use Grav\Common\Grav;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Pages;
use Grav\Common\Page\Types;
use Grav\Common\Plugin;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\Utils;
use Grav\Events\FlexRegisterEvent;
use Grav\Events\PermissionsRegisterEvent;
use Grav\Events\PluginsLoadedEvent;
use Grav\Framework\Acl\PermissionsReader;
use Grav\Framework\Flex\FlexDirectory;
use Grav\Framework\Flex\FlexForm;
use Grav\Framework\Flex\Interfaces\FlexAuthorizeInterface;
use Grav\Framework\Flex\Interfaces\FlexInterface;
use Grav\Framework\Form\Interfaces\FormInterface;
use Grav\Framework\Route\Route;
use Grav\Plugin\Admin\Admin;
use Grav\Plugin\FlexObjects\Controllers\ObjectController;
use Grav\Plugin\FlexObjects\FlexFormFactory;
use Grav\Plugin\Form\Forms;
use Grav\Plugin\FlexObjects\Admin\AdminController;
use Grav\Plugin\FlexObjects\Flex;
use Psr\Http\Message\ServerRequestInterface;
use RocketTheme\Toolbox\Event\Event;
use function is_array;
use function is_callable;
/**
* Class FlexObjectsPlugin
* @package Grav\Plugin
*/
class FlexObjectsPlugin extends Plugin
{
/** @var string */
protected const MIN_GRAV_VERSION = '1.7.0';
/** @var int[] */
public $features = [
'blueprints' => 1000,
];
/** @var AdminController */
protected $controller;
/**
* @return bool
*/
public static function checkRequirements(): bool
{
return version_compare(GRAV_VERSION, static::MIN_GRAV_VERSION, '>=');
}
/**
* @return array
*
* The getSubscribedEvents() gives the core a list of events
* that the plugin wants to listen to. The key of each
* array section is the event that the plugin listens to
* and the value (in the form of an array) contains the
* callable (or function) as well as the priority. The
* higher the number the higher the priority.
*/
public static function getSubscribedEvents(): array
{
if (!static::checkRequirements()) {
return [];
}
return [
PluginsLoadedEvent::class => [
['initializeFlex', 10]
],
PermissionsRegisterEvent::class => [
['onRegisterPermissions', 100]
],
FlexRegisterEvent::class => [
['onRegisterFlex', 100]
],
'onCliInitialize' => [
['autoload', 100000],
['initializeFlex', 10]
],
'onPluginsInitialized' => [
['onPluginsInitialized', 0],
],
'onFormRegisterTypes' => [
['onFormRegisterTypes', 0]
]
];
}
/**
* Get list of form field types specified in this plugin. Only special types needs to be listed.
*
* @return array
*/
public function getFormFieldTypes()
{
return [
'list' => [
'array' => true
],
'pagemedia' => [
'array' => true,
'media_field' => true,
'validate' => [
'type' => 'ignore'
]
],
'filepicker' => [
'media_picker_field' => true
],
];
}
/**
* @return ClassLoader
*/
public function autoload(): ClassLoader
{
return require __DIR__ . '/vendor/autoload.php';
}
/**
* [PluginsLoadedEvent:10]: Initialize Flex
*
* @return void
*/
public function initializeFlex(): void
{
$config = $this->config->get('plugins.flex-objects.directories') ?? [];
// Add to DI container
$this->grav['flex_objects'] = static function (Grav $grav) use ($config) {
/** @var FlexInterface $flex */
$flex = $grav['flex'];
$flexObjects = new Flex($flex, $config);
// This event is for backwards compatibility only, do not use it!
$grav->fireEvent('onFlexInit', new Event(['flex' => $flexObjects]));
return $flexObjects;
};
}
/**
* Initialize the plugin
*
* @return void
*/
public function onPluginsInitialized(): void
{
if ($this->isAdmin()) {
/** @var UserInterface|null $user */
$user = $this->grav['user'] ?? null;
if (null === $user || !$user->authorize('login', 'admin')) {
return;
}
$this->enable([
'onAdminTwigTemplatePaths' => [
['onAdminTwigTemplatePaths', 10]
],
'onAdminMenu' => [
['onAdminMenu', 0]
],
'onAdminPage' => [
['onAdminPage', 0]
],
'onAdminCompilePresetSCSS' => [
['onAdminCompilePresetSCSS', 0]
],
'onDataTypeExcludeFromDataManagerPluginHook' => [
['onDataTypeExcludeFromDataManagerPluginHook', 0]
],
'onAdminControllerInit' => [
['onAdminControllerInit', 0]
],
'onThemeInitialized' => [
['onThemeInitialized', 0]
],
'onPageInitialized' => [
['onAdminPageInitialized', 0]
],
'onTwigSiteVariables' => [
['onTwigAdminVariables', 0]
],
'onGetPageTemplates' =>
['onGetPageTemplates', 0]
]);
} else {
$this->enable([
'onTwigTemplatePaths' => [
['onTwigTemplatePaths', 0]
],
'onPagesInitialized' => [
['onPagesInitialized', -10000]
],
'onPageInitialized' => [
['authorizePage', 10000]
],
'onBeforeFlexFormInitialize' => [
['onBeforeFlexFormInitialize', -10]
],
'onPageTask' => [
['onPageTask', -10]
],
]);
}
}
/**
* @param FlexRegisterEvent $event
* @return void
*/
public function onRegisterFlex(FlexRegisterEvent $event): void
{
/** @var \Grav\Framework\Flex\Flex $flex */
$flex = $event->flex;
$types = (array)$this->config->get('plugins.flex-objects.directories', []);
$this->registerDirectories($flex, $types);
}
/**
* @return void
*/
public function onThemeInitialized(): void
{
// Register directories defined in the theme.
/** @var \Grav\Framework\Flex\Flex $flex */
$flex = $this->grav['flex'];
$types = (array)$this->config->get('plugins.flex-objects.directories', []);
$this->registerDirectories($flex, $types, true);
$this->controller = new AdminController();
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$names = implode(', ', array_keys($flex->getDirectories()));
$debugger->addMessage(sprintf('Registered flex types: %s', $names), 'debug');
}
/**
* @param Event $event
*/
public function onBeforeFlexFormInitialize(Event $event): void
{
/** @var array $form */
$form = $event['form'];
$edit = $form['actions']['edit'] ?? false;
if (!isset($form['flex']['key']) && $edit === true) {
/** @var Route $route */
$route = $this->grav['route'];
$id = rawurldecode($route->getGravParam('id'));
if (null !== $id) {
$form['flex']['key'] = $id;
$event['form'] = $form;
}
}
}
/**
* [onPagesInitialized:-10000] Default router for flex pages.
*
* @param Event $event
*/
public function onPagesInitialized(Event $event): void
{
/** @var Route|null $route */
$route = $event['route'] ?? null;
if (null === $route) {
// Stop if in CLI.
return;
}
/** @var PageInterface|null $page */
$page = $this->grav['page'] ?? null;
$base = '';
$path = [];
if (!$page->routable() || $page->template() === 'notfound') {
/** @var Pages $pages */
$pages = $this->grav['pages'];
// Find first existing and routable parent page.
$parts = explode('/', $route->getRoute());
array_shift($parts);
$page = null;
while (!$page && $parts) {
$path[] = array_pop($parts);
$base = '/' . implode('/', $parts);
$page = $pages->find($base);
if ($page && !$page->routable()) {
$page = null;
}
}
}
// If page is found, check if it contains flex directory router.
if ($page) {
$flex = $this->grav['flex'];
$options = $page->header()->flex ?? null;
$router = $options['router'] ?? null;
$type = $options['directory'] ?? null;
$directory = $type ? $flex->getDirectory($type) : null;
if (\is_string($router)) {
$path = implode('/', array_reverse($path));
$response = null;
$flexEvent = new Event([
'flex' => $flex,
'directory' => $directory,
'parent' => $page,
'page' => $page,
'base' => $base,
'path' => $path,
'route' => $route,
'options' => $options,
'request' => $event['request'],
'response' => &$response,
]);
$flexEvent = $this->grav->fireEvent("flex.router.{$router}", $flexEvent);
if ($response) {
$this->grav->close($response);
}
/** @var PageInterface|null $routedPage */
$routedPage = $flexEvent['page'];
if ($routedPage) {
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->addMessage(sprintf('Flex uses page %s', $routedPage->route()));
unset($this->grav['page']);
$this->grav['page'] = $routedPage;
$event->stopPropagation();
}
}
}
}
/**
* [onPageInitialized:10000] Authorize Flex Objects Page
*
* @param Event $event
*/
public function authorizePage(Event $event): void
{
/** @var PageInterface|null $page */
$page = $event['page'];
if (!$page instanceof PageInterface) {
return;
}
$header = $page->header();
$forms = $page->getForms();
// Update dynamic flex forms from the page.
$form = null;
foreach ($forms as $name => $test) {
$type = $form['type'] ?? null;
if ($type === 'flex') {
$form = $test;
// Update the form and add it back to the page.
$this->grav->fireEvent('onBeforeFlexFormInitialize', new Event(['page' => $page, 'name' => $name, 'form' => &$form]));
$page->addForms([$form], true);
}
}
// Make sure the page contains flex.
$config = $header->flex ?? null;
if (!is_array($config) && !$form) {
return;
}
/** @var Route $route */
$route = $this->grav['route'];
$type = $form['flex']['type'] ?? $config['directory'] ?? $route->getGravParam('directory') ?? null;
$key = $form['flex']['key'] ?? $config['id'] ?? $route->getGravParam('id') ?? '';
if (\is_string($type)) {
/** @var Flex $flex */
$flex = $this->grav['flex_objects'];
$directory = $flex->getDirectory($type);
} else {
$directory = null;
}
if (!$directory) {
return;
}
$create = (bool)($form['actions']['create'] ?? false);
$edit = (bool)($form['actions']['edit'] ?? false);
$scope = $config['access']['scope'] ?? null;
$object = $key !== '' ? $directory->getObject($key) : null;
$hasAccess = null;
$action = $config['access']['action'] ?? null;
if (null === $action) {
if (!$form) {
$action = $key !== '' ? 'read' : 'list';
if (null === $scope) {
$hasAccess = true;
}
} elseif ($object) {
if ($edit) {
$scope = $scope ?? 'admin';
$action = 'update';
} else {
$hasAccess = false;
}
} elseif ($create) {
$object = $directory->createObject([], $key);
$scope = $scope ?? 'admin';
$action = 'create';
} else {
$hasAccess = false;
}
}
if ($action && $hasAccess === null) {
if ($object instanceof FlexAuthorizeInterface) {
$hasAccess = $object->isAuthorized($action, $scope);
} else {
$hasAccess = $directory->isAuthorized($action, $scope);
}
}
if (!$hasAccess) {
// Hide the page (404).
$page->routable(false);
$page->visible(false);
// If page is not a module, replace the current page with unauthorized page.
if (!$page->isModule()) {
$login = $this->grav['login'] ?? null;
$unauthorized = $login ? $login->addPage('unauthorized') : null;
if ($unauthorized) {
unset($this->grav['page']);
$this->grav['page'] = $unauthorized;
}
}
} elseif ($config['access']['override'] ?? false) {
// Override page access settings (allow).
$page->modifyHeader('access', []);
}
}
/**
* @param Event $event
*/
public function onPageTask(Event $event): void
{
/** @var FormInterface|null $form */
$form = $event['form'] ?? null;
if (!$form instanceof FlexForm) {
return;
}
$object = $form->getObject();
/** @var ServerRequestInterface $request */
$request = $event['request'];
$request = $request
->withAttribute('type', $object->getFlexType())
->withAttribute('key', $object->getKey())
->withAttribute('object', $object)
->withAttribute('form', $form);
$controller = new ObjectController();
$response = $controller->handle($request);
if ($response->getStatusCode() !== 418) {
$this->grav->close($response);
}
}
/**
* @param \Grav\Framework\Flex\Flex $flex
* @param array $types
* @param bool $report
*/
protected function registerDirectories(\Grav\Framework\Flex\Flex $flex, array $types, bool $report = false): void
{
$map = Flex::getLegacyBlueprintMap(false);
foreach ($types as $blueprint) {
// Backwards compatibility to v1.0.0-rc.3
$blueprint = $map[$blueprint] ?? $blueprint;
$type = Utils::basename((string)$blueprint, '.yaml');
if (!$type) {
continue;
}
if (!file_exists($blueprint)) {
if ($report) {
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->addMessage(sprintf('Flex: blueprint for flex type %s is missing', $type), 'error');
}
continue;
}
$directory = $flex->getDirectory($type);
if (!$directory || !$directory->isEnabled()) {
$flex->addDirectoryType($type, $blueprint);
}
}
}
/**
* Initial stab at registering permissions (WIP)
*
* @param PermissionsRegisterEvent $event
* @return void
*/
public function onRegisterPermissions(PermissionsRegisterEvent $event): void
{
/** @var Flex $flex */
$flex = $this->grav['flex_objects'];
$directories = $flex->getDirectories();
$permissions = $event->permissions;
$actions = [];
foreach ($directories as $directory) {
$data = $directory->getConfig('admin.permissions', []);
$actions[] = PermissionsReader::fromArray($data, $permissions->getTypes());
}
$actions[] = PermissionsReader::fromYaml("plugin://{$this->name}/permissions.yaml");
$permissions->addActions(array_replace(...$actions));
}
/**
* @param Event $event
* @return void
*/
public function onFormRegisterTypes(Event $event): void
{
/** @var Forms $forms */
$forms = $event['forms'];
$forms->registerType('flex', new FlexFormFactory());
}
/**
* @param Event $event
* @return void
*/
public function onAdminPage(Event $event): void
{
if ($this->controller->isActive()) {
$event->stopPropagation();
/** @var PageInterface $page */
$page = $event['page'];
$page->init(new \SplFileInfo(__DIR__ . '/admin/pages/flex-objects.md'));
$page->slug($this->controller->getLocation());
$header = $page->header();
$header->access = ['admin.login'];
$header->controller = $this->controller->getInfo();
}
}
/**
* [onPageInitialized:0]: Run controller
*
* @return void
*/
public function onAdminPageInitialized(): void
{
if ($this->controller->isActive()) {
$this->controller->execute();
$this->controller->redirect();
}
}
/**
* @param Event $event
* @return void
*/
public function onAdminControllerInit(Event $event): void
{
$eventController = $event['controller'];
// Blacklist all admin routes, including aliases and redirects.
$eventController->blacklist_views[] = 'flex-objects';
foreach ($this->controller->getAdminRoutes() as $route => $info) {
$eventController->blacklist_views[] = trim($route, '/');
}
}
/**
* Add Flex-Object's preset.scss to the Admin Preset SCSS compile process
*
* @param Event $event
* @return void
*/
public function onAdminCompilePresetSCSS(Event $event): void
{
$event['scss']->add($this->grav['locator']->findResource('plugins://flex-objects/scss/_preset.scss'));
}
/**
* @param Event $event
* @return void
*/
public function onGetPageTemplates(Event $event): void
{
/** @var Types $types */
$types = $event->types;
$types->register('flex-objects', 'plugins://flex-objects/blueprints/pages/flex-objects.yaml');
}
/**
* Form select options listing all enabled directories.
*
* @return array
*/
public static function directoryOptions(): array
{
/** @var Flex $flex */
$flex = Grav::instance()['flex_objects'];
$directories = $flex->getDirectories();
$list = [];
/**
* @var string $type
* @var FlexDirectory $directory
*/
foreach ($directories as $type => $directory) {
if (!$directory->getConfig('site.hidden')) {
$list[$type] = $directory->getTitle();
}
}
return $list;
}
/**
* @return array
*/
public function getAdminMenu(): array
{
/** @var Flex $flex */
$flex = $this->grav['flex_objects'];
$list = [];
foreach ($flex->getAdminMenuItems() as $name => $item) {
$route = trim($item['route'] ?? $name, '/');
$list[$route] = $item;
}
return $list;
}
/**
* Add Flex Directory to admin menu
*
* @return void
*/
public function onAdminMenu(): void
{
/** @var Flex $flex */
$flex = $this->grav['flex_objects'];
/** @var Admin $admin */
$admin = $this->grav['admin'];
foreach ($this->getAdminMenu() as $route => $item) {
$directory = null;
if (isset($item['directory'])) {
$directory = $flex->getDirectory($item['directory']);
if (!$directory || !$directory->isEnabled()) {
continue;
}
}
$title = $item['title'] ?? 'PLUGIN_FLEX_OBJECTS.TITLE';
$index = $item['index'] ?? 0;
if (($this->grav['twig']->plugins_hooked_nav[$title]['index'] ?? 1000) <= $index) {
continue;
}
$location = $item['location'] ?? $route;
$hidden = $item['hidden'] ?? false;
$icon = $item['icon'] ?? 'fa-list';
$authorize = $item['authorize'] ?? ($directory ? null : ['admin.flex-objects', 'admin.super']);
if ($hidden || (null === $authorize && $directory->isAuthorized('list', 'admin', $admin->user))) {
continue;
}
$cache = $directory ? $directory->getCache('index') : null;
$count = $cache ? $cache->get('admin-count-' . md5($admin->user->username)) : false;
if (null === $count) {
try {
$collection = $directory->getCollection();
if (is_callable([$collection, 'isAuthorized'])) {
$count = $collection->isAuthorized('list', 'admin', $admin->user)->count();
} else {
$count = $collection->count();
}
$cache->set('admin-count-' . md5($admin->user->username), $count);
} catch (\InvalidArgumentException $e) {
continue;
}
}
$badge = $directory ? ['badge' => ['count' => $count]] : [];
$priority = $item['priority'] ?? 0;
$this->grav['twig']->plugins_hooked_nav[$title] = [
'location' => $location,
'route' => $route,
'index' => $index,
'icon' => $icon,
'authorize' => $authorize,
'priority' => $priority
] + $badge;
}
}
/**
* Exclude Flex Directory data from the Data Manager plugin
*
* @return void
*/
public function onDataTypeExcludeFromDataManagerPluginHook(): void
{
$this->grav['admin']->dataTypesExcludedFromDataManagerPlugin[] = 'flex-objects';
}
/**
* Add current directory to twig lookup paths.
*
* @return void
*/
public function onTwigTemplatePaths(): void
{
$extra_site_twig_path = $this->config->get('plugins.flex-objects.extra_site_twig_path');
$extra_path = $extra_site_twig_path ? $this->grav['locator']->findResource($extra_site_twig_path) : null;
if ($extra_path) {
$this->grav['twig']->twig_paths[] = $extra_path;
}
$this->grav['twig']->twig_paths[] = __DIR__ . '/templates';
}
/**
* Add plugin templates path
*
* @param Event $event
* @return void
*/
public function onAdminTwigTemplatePaths(Event $event): void
{
$extra_admin_twig_path = $this->config->get('plugins.flex-objects.extra_admin_twig_path');
$extra_path = $extra_admin_twig_path ? $this->grav['locator']->findResource($extra_admin_twig_path) : null;
$paths = $event['paths'];
if ($extra_path) {
$paths[] = $extra_path;
}
$paths[] = __DIR__ . '/admin/templates';
$event['paths'] = $paths;
}
/**
* Set needed variables to display directory.
*
* @return void
*/
public function onTwigAdminVariables(): void
{
if ($this->controller->isActive()) {
// Twig shortcuts
$this->grav['twig']->twig_vars['controller'] = $this->controller;
$this->grav['twig']->twig_vars['action'] = $this->controller->getAction();
$this->grav['twig']->twig_vars['task'] = $this->controller->getTask();
$this->grav['twig']->twig_vars['target'] = $this->controller->getTarget();
$this->grav['twig']->twig_vars['key'] = $this->controller->getId();
$this->grav['twig']->twig_vars['flex'] = $this->grav['flex_objects'];
$this->grav['twig']->twig_vars['directory'] = $this->controller->getDirectory();
$this->grav['twig']->twig_vars['collection'] = $this->controller->getCollection();
$this->grav['twig']->twig_vars['object'] = $this->controller->getObject();
// CSS / JS Assets
$this->grav['assets']->addCss('plugin://flex-objects/css/admin.css');
$this->grav['assets']->addCss('plugin://admin/themes/grav/css/codemirror/codemirror.css');
}
}
}