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'); } } }