Files
Grav-Admin-Plugin/admin.php

1376 lines
46 KiB
PHP
Raw Permalink Normal View History

2014-08-05 13:06:38 -07:00
<?php
namespace Grav\Plugin;
use Composer\Autoload\ClassLoader;
use Grav\Common\Cache;
2020-04-18 17:45:11 -06:00
use Grav\Common\Data\Data;
use Grav\Common\Debugger;
2015-12-10 12:07:36 +01:00
use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Grav;
2019-01-31 18:39:38 -07:00
use Grav\Common\Helpers\LogViewer;
use Grav\Common\Inflector;
use Grav\Common\Language\Language;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Page;
use Grav\Common\Page\Pages;
use Grav\Common\Plugin;
use Grav\Common\Plugins;
2019-06-18 12:08:57 +03:00
use Grav\Common\Processors\Events\RequestHandlerEvent;
2018-12-05 08:20:38 +02:00
use Grav\Common\Session;
use Grav\Common\Twig\Twig;
use Grav\Common\Uri;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\Utils;
2020-05-08 17:50:03 -06:00
use Grav\Common\Yaml;
2020-01-21 09:46:42 +02:00
use Grav\Events\PermissionsRegisterEvent;
2020-01-10 20:28:30 +02:00
use Grav\Framework\Acl\PermissionsReader;
2019-06-03 13:17:16 +03:00
use Grav\Framework\Psr7\Response;
use Grav\Framework\Session\Exceptions\SessionException;
use Grav\Plugin\Admin\Admin;
use Grav\Plugin\Admin\AdminFormFactory;
use Grav\Plugin\Admin\Popularity;
2019-06-18 12:08:57 +03:00
use Grav\Plugin\Admin\Router;
use Grav\Plugin\Admin\Themes;
use Grav\Plugin\Admin\AdminController;
use Grav\Plugin\Admin\SafeUpgradeManager;
2018-05-10 10:14:18 +03:00
use Grav\Plugin\Admin\Twig\AdminTwigExtension;
2020-04-20 09:57:44 -06:00
use Grav\Plugin\Admin\WhiteLabel;
use Grav\Plugin\Form\Form;
use Grav\Plugin\Form\Forms;
2017-08-31 12:46:51 -06:00
use Grav\Plugin\Login\Login;
use Pimple\Container;
use Psr\Http\Message\ResponseInterface;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
2014-08-05 13:06:38 -07:00
/**
* Class AdminPlugin
* @package Grav\Plugin\Admin
*/
2014-08-05 13:06:38 -07:00
class AdminPlugin extends Plugin
{
2016-01-22 10:42:38 +02:00
public $features = [
'blueprints' => 1000,
];
/** @var bool */
2014-08-05 13:06:38 -07:00
protected $active = false;
/** @var string */
2014-08-05 13:06:38 -07:00
protected $template;
/** @var string */
2014-09-03 22:22:03 -06:00
protected $theme;
/** @var string */
2014-08-05 13:06:38 -07:00
protected $route;
/** @var string */
2016-09-27 18:21:11 +03:00
protected $admin_route;
/** @var Uri */
2014-08-05 13:06:38 -07:00
protected $uri;
/** @var Admin */
2014-08-05 13:06:38 -07:00
protected $admin;
/** @var Session */
2014-10-01 22:28:16 +03:00
protected $session;
/** @var Popularity */
protected $popularity;
/** @var string */
2014-10-01 22:28:16 +03:00
protected $base;
/** @var string */
2016-04-26 12:57:29 -06:00
protected $version;
/**
* @return array
*/
public static function getSubscribedEvents()
{
2017-09-25 17:10:54 -06:00
return [
'onPluginsInitialized' => [
['setup', 100000],
['onPluginsInitialized', 1001]
2020-11-26 15:55:02 +02:00
],
2019-06-18 12:08:57 +03:00
'onRequestHandlerInit' => [
['onRequestHandlerInit', 100000]
],
'onFormRegisterTypes' => ['onFormRegisterTypes', 0],
2017-09-25 17:10:54 -06:00
'onPageInitialized' => ['onPageInitialized', 0],
'onShutdown' => ['onShutdown', 1000],
2020-01-21 09:46:42 +02:00
PermissionsRegisterEvent::class => ['onRegisterPermissions', 1000],
2017-09-25 17:10:54 -06:00
];
2015-12-10 12:07:36 +01:00
}
/**
* Get list of form field types specified in this plugin. Only special types needs to be listed.
*
* @return array
*/
public function getFormFieldTypes()
{
return [
'column' => [
'input@' => false
],
'columns' => [
'input@' => false
],
'fieldset' => [
'input@' => false
],
'section' => [
'input@' => false
],
'list' => [
'array' => true
],
2022-02-04 19:23:13 +02:00
'elements' => [
'input@' => true
2022-02-04 19:23:13 +02:00
],
'element' => [
'input@' => false
],
'file' => [
'array' => true,
'media_field' => true,
'validate' => [
'type' => 'ignore'
]
],
'pagemedia' => [
'array' => true,
'media_field' => true,
'validate' => [
'type' => 'ignore'
]
],
'filepicker' => [
'media_picker_field' => true
],
'pagemediaselect' => [
'media_picker_field' => true
],
'permissions' => [
'ignore_empty' => true,
'validate' => [
'type' => 'array'
],
'filter' => [
'type' => 'flatten_array',
'value_type' => 'bool',
]
2020-01-28 22:24:29 +02:00
],
'acl_picker' => [
'ignore_empty' => true,
'validate' => [
'type' => 'array'
],
'filter' => [
'type' => 'array',
'key_type' => 'string',
'value_type' => 'bool',
]
],
'taxonomy' => [
'multiple' => true,
'validate' => [
'type' => 'array'
]
]
];
}
/**
* @return ClassLoader
*/
public function autoload(): ClassLoader
{
return require __DIR__ . '/vendor/autoload.php';
}
/**
* @param Event $event
* @return void
*/
public function onFormRegisterTypes(Event $event): void
{
/** @var Forms $forms */
$forms = $event['forms'];
$forms->registerType('admin', new AdminFormFactory());
}
2015-12-10 11:02:19 -07:00
/**
* [onPluginsInitialized:100000]
*
2015-12-10 11:02:19 -07:00
* If the admin path matches, initialize the Login plugin configuration and set the admin
* as active.
*
* @return void
2015-12-10 11:02:19 -07:00
*/
public function setup()
{
// Only enable admin if it has a route.
2015-12-10 11:02:19 -07:00
$route = $this->config->get('plugins.admin.route');
if (!$route) {
return;
}
/** @var Uri uri */
$this->uri = $this->grav['uri'];
$this->base = '/' . trim($route, '/');
2016-09-27 18:21:11 +03:00
$this->admin_route = rtrim($this->grav['pages']->base(), '/') . $this->base;
2015-12-10 11:02:19 -07:00
$inAdmin = $this->isAdminPath();
2015-12-10 11:02:19 -07:00
// If no users found, go to register.
if (!$inAdmin && !Admin::doAnyUsersExist()) {
$this->grav->redirect($this->admin_route);
2015-12-10 11:02:19 -07:00
}
// Only setup admin if we're inside the admin path.
if ($inAdmin) {
$this->setupAdmin();
2015-12-10 11:02:19 -07:00
}
}
2015-12-10 12:07:36 +01:00
/**
* [onPluginsInitialized:1001]
2015-12-10 12:07:36 +01:00
*
* If the admin plugin is set as active, initialize the admin
*
* @return void
2015-12-10 12:07:36 +01:00
*/
public function onPluginsInitialized()
2015-12-10 12:07:36 +01:00
{
// Only activate admin if we're inside the admin path.
if ($this->active) {
$this->initializeAdmin();
}
// Always initialize popularity.
$this->popularity = new Popularity();
}
2019-06-18 12:08:57 +03:00
/**
* [onRequestHandlerInit:100000]
*
* @param RequestHandlerEvent $event
* @return void
2019-06-18 12:08:57 +03:00
*/
public function onRequestHandlerInit(RequestHandlerEvent $event)
{
// Store this version.
$this->version = $this->getBlueprint()->get('version');
2019-06-18 12:08:57 +03:00
$route = $event->getRoute();
$base = $route->getRoute(0, 1);
if ($base === $this->base) {
/** @var Debugger $debugger */
$debugger = $this->grav['debugger'];
$debugger->addMessage('Admin v' . $this->version);
$event->addMiddleware('admin_router', new Router($this->grav, $this->admin));
2019-06-18 12:08:57 +03:00
}
}
/**
* @param Event $event
* @return void
*/
public function onAdminControllerInit(Event $event): void
{
$eventController = $event['controller'];
// Blacklist login related views.
$loginViews = ['login', 'forgot', 'register', 'reset'];
$eventController->blacklist_views = array_merge($eventController->blacklist_views, $loginViews);
}
2020-04-18 17:45:11 -06:00
/**
* Force compile during save if admin plugin save
*
* @param Event $event
* @return void
2020-04-18 17:45:11 -06:00
*/
public function onAdminSave(Event $event)
{
$obj = $event['object'];
if ($obj instanceof Data
&& ($blueprint = $obj->blueprints()) && $blueprint && $blueprint->getFilename() === 'admin/blueprints') {
[$status, $msg] = $this->grav['admin-whitelabel']->compilePresetScss($obj);
2020-04-19 18:31:11 -06:00
if (!$status) {
$this->grav['messages']->add($msg, 'error');
}
2020-04-18 17:45:11 -06:00
}
}
/**
* [onPageInitialized:0]
*
* @return void
*/
public function onPageInitialized()
{
$template = $this->uri->param('tmpl');
if ($template) {
/** @var PageInterface $page */
$page = $this->grav['page'];
$page->template($template);
}
2015-12-10 12:07:36 +01:00
}
/**
* [onShutdown:1000]
*
* Handles the shutdown
*
* @return void
*/
public function onShutdown()
{
if ($this->active) {
//only activate when Admin is active
if ($this->admin->shouldLoadAdditionalFilesInBackground()) {
$this->admin->loadAdditionalFilesInBackground();
}
} elseif ($this->popularity && $this->config->get('plugins.admin.popularity.enabled')) {
//if popularity is enabled, track non-admin hits
$this->popularity->trackHit();
2014-08-05 13:06:38 -07:00
}
}
2014-09-09 14:03:01 -06:00
/**
* [onAdminDashboard:0]
*
* @return void
*/
public function onAdminDashboard()
{
$lang = $this->grav['language'];
$this->grav['twig']->plugins_hooked_dashboard_widgets_top[] = [
'name' => $lang->translate('PLUGIN_ADMIN.MAINTENANCE'),
'template' => 'dashboard-maintenance',
];
$this->grav['twig']->plugins_hooked_dashboard_widgets_top[] = [
'name' => $lang->translate('PLUGIN_ADMIN.VIEWS_STATISTICS'),
'template' => 'dashboard-statistics',
];
$this->grav['twig']->plugins_hooked_dashboard_widgets_top[] = [
'name' => $lang->translate('PLUGIN_ADMIN.NOTIFICATIONS'),
'template' => 'dashboard-notifications',
];
$this->grav['twig']->plugins_hooked_dashboard_widgets_top[] = [
'name' => $lang->translate('PLUGIN_ADMIN.NEWS_FEED'),
'template' => 'dashboard-feed',
];
$this->grav['twig']->plugins_hooked_dashboard_widgets_main[] = [
'name' => $lang->translate('PLUGIN_ADMIN.LATEST_PAGE_UPDATES'),
'template' => 'dashboard-pages',
];
2014-08-05 13:06:38 -07:00
}
/**
* [onAdminTools:0]
*
* Provide the tools for the Tools page, currently only direct install
*
* @return void
*/
public function onAdminTools(Event $event)
2016-07-07 18:55:52 +02:00
{
$event['tools'] = array_merge($event['tools'], [
'backups' => [['admin.maintenance', 'admin.super'], 'PLUGIN_ADMIN.BACKUPS'],
'scheduler' => [['admin.super'], 'PLUGIN_ADMIN.SCHEDULER'],
'logs' => [['admin.super'], 'PLUGIN_ADMIN.LOGS'],
'reports' => [['admin.super'], 'PLUGIN_ADMIN.REPORTS'],
'direct-install' => [['admin.super'], 'PLUGIN_ADMIN.DIRECT_INSTALL'],
]);
try {
$manifestFiles = glob(GRAV_ROOT . '/user/data/upgrades/*.json') ?: [];
if (!$manifestFiles) {
$manager = new SafeUpgradeManager(Grav::instance());
$manifestFiles = $manager->hasSnapshots() ? [true] : [];
}
2025-10-18 12:14:52 -06:00
$tools = $event['tools'];
Grav::instance()['log']->debug('[Admin] Tools before restore grav: ' . implode(',', array_keys($tools)));
2025-10-18 12:14:52 -06:00
if ($manifestFiles) {
2025-10-18 12:14:52 -06:00
$tools['restore-grav'] = [['admin.super'], 'PLUGIN_ADMIN.RESTORE_GRAV'];
Grav::instance()['log']->debug('[Admin] Restore Grav tool enabled');
}
2025-10-18 12:14:52 -06:00
$event['tools'] = $tools;
Grav::instance()['log']->debug('[Admin] Tools after register: ' . implode(',', array_keys($tools)));
} catch (\Throwable $e) {
// ignore availability errors, snapshots tool will simply stay hidden
Grav::instance()['log']->warning('[Admin] Restore Grav detection failed: ' . $e->getMessage());
}
}
2014-08-05 13:06:38 -07:00
/**
* Sets longer path to the home page allowing us to have list of pages when we enter to pages section.
*
* @return void
2014-08-05 13:06:38 -07:00
*/
public function onPagesInitialized()
2014-08-05 13:06:38 -07:00
{
$config = $this->config;
// Force SSL with redirect if required
if ($config->get('system.force_ssl')) {
$scheme = $this->uri->scheme(true);
if ($scheme !== 'https') {
Admin::DEBUG && Admin::addDebugMessage('Admin SSL forced on, redirect');
$url = 'https://' . $this->uri->host() . $this->uri->uri();
$this->grav->redirect($url);
}
}
$this->session = $this->grav['session'];
// set session variable if it's passed via the url
if (!$this->session->user->authorize('admin.super') || $this->uri->param('mode') === 'normal') {
$this->session->expert = false;
} elseif ($this->uri->param('mode') === 'expert') {
$this->session->expert = true;
2018-12-05 08:20:38 +02:00
} else {
// set the default if not set before
$this->session->expert = $this->session->expert ?? false;
}
// make sure page is not frozen!
unset($this->grav['page']);
2014-08-05 13:06:38 -07:00
// Call the controller if it has been set.
$adminParams = $this->admin->request->getAttribute('admin');
$page = null;
if (isset($adminParams['controller'])) {
$controllerParams = $adminParams['controller'];
$class = $controllerParams['class'];
if (!class_exists($class)) {
throw new \RuntimeException(sprintf('Admin controller %s does not exist', $class));
}
2014-08-05 13:06:38 -07:00
/** @var \Grav\Plugin\Admin\Controllers\AdminController $controller */
$controller = new $class($this->grav);
$method = $controllerParams['method'];
$params = $controllerParams['params'] ?? [];
2019-06-03 13:17:16 +03:00
if (!is_callable([$controller, $method])) {
throw new \RuntimeException(sprintf('Admin controller method %s() does not exist', $method));
}
2014-08-05 13:06:38 -07:00
/** @var ResponseInterface $response */
$response = $controller->{$method}(...$params);
if ($response->getStatusCode() !== 418) {
$this->grav->close($response);
}
2014-08-05 13:06:38 -07:00
$page = $controller->getPage();
if (!$page) {
throw new \RuntimeException('Not Found', 404);
}
2015-12-09 21:37:34 -07:00
$this->grav['page'] = $page;
$this->admin->form = $controller->getActiveForm();
$legacyController = false;
} else {
$legacyController = true;
}
2021-12-08 17:38:50 +02:00
/** @var UserInterface $user */
$user = $this->grav['user'];
// Replace page service with admin.
if (empty($this->grav['page'])) {
$this->grav['page'] = function () use ($user) {
$page = new Page();
// Plugins may not have the correct Cache-Control header set, force no-store for the proxies.
$page->expires(0);
if ($user->authorize('admin.login')) {
$event = new Event(['page' => $page]);
$event = $this->grav->fireEvent('onAdminPage', $event);
/** @var PageInterface $page */
$page = $event['page'];
if ($page->slug()) {
Admin::DEBUG && Admin::addDebugMessage('Admin page: from event');
return $page;
}
}
// Look in the pages provided by the Admin plugin itself
if (file_exists(__DIR__ . "/pages/admin/{$this->template}.md")) {
Admin::DEBUG && Admin::addDebugMessage("Admin page: {$this->template}");
$page->init(new \SplFileInfo(__DIR__ . "/pages/admin/{$this->template}.md"));
$page->slug(Utils::basename($this->template));
2015-10-26 21:30:33 +01:00
return $page;
}
/** @var UniformResourceLocator $locator */
$locator = $this->grav['locator'];
2015-10-26 21:30:33 +01:00
// If not provided by Admin, lookup pages added by other plugins
/** @var Plugins $plugins */
$plugins = $this->grav['plugins'];
foreach ($plugins as $plugin) {
if ($this->config->get("plugins.{$plugin->name}.enabled") !== true) {
continue;
}
$path = $locator->findResource("plugins://{$plugin->name}/admin/pages/{$this->template}.md");
if ($path) {
Admin::DEBUG && Admin::addDebugMessage("Admin page: plugin {$plugin->name}/{$this->template}");
2016-07-07 18:55:52 +02:00
$page->init(new \SplFileInfo($path));
$page->slug(Utils::basename($this->template));
return $page;
}
}
2016-01-21 09:46:38 +02:00
return null;
};
}
if (empty($this->grav['page'])) {
if ($user->authenticated) {
Admin::DEBUG && Admin::addDebugMessage('Admin page: fire onPageNotFound event');
$event = new Event(['page' => null]);
$event->page = null;
$event = $this->grav->fireEvent('onPageNotFound', $event);
/** @var PageInterface $page */
$page = $event->page;
if (!$page || !$page->routable()) {
Admin::DEBUG && Admin::addDebugMessage('Admin page: 404 Not Found');
$error_file = $this->grav['locator']->findResource('plugins://admin/pages/admin/error.md');
$page = new Page();
$page->init(new \SplFileInfo($error_file));
$page->slug(Utils::basename($this->route));
$page->routable(true);
}
unset($this->grav['page']);
$this->grav['page'] = $page;
} else {
Admin::DEBUG && Admin::addDebugMessage('Admin page: login');
// Not Found and not logged in: Display login page.
$login_file = $this->grav['locator']->findResource('plugins://admin/pages/admin/login.md');
$page = new Page();
$page->init(new \SplFileInfo($login_file));
$page->slug(Utils::basename($this->route));
unset($this->grav['page']);
$this->grav['page'] = $page;
}
}
if ($legacyController) {
// Handle tasks.
$this->admin->task = $task = $this->grav['task'] ?? $this->grav['action'];
if ($task) {
Admin::DEBUG && Admin::addDebugMessage("Admin task: {$task}");
// Make local copy of POST.
$post = $this->grav['uri']->post();
$this->initializeController($task, $post);
} elseif ($this->template === 'logs' && $this->route) {
// Display RAW error message.
$response = new Response(200, [], $this->admin->logEntry());
$this->grav->close($response);
}
}
// Explicitly set a timestamp on assets
2016-07-07 18:55:52 +02:00
$this->grav['assets']->setTimestamp(substr(md5(GRAV_VERSION . $this->grav['config']->checksum()), 0, 10));
2014-08-05 13:06:38 -07:00
}
2016-01-10 17:17:04 +01:00
/**
* Handles initializing the assets
*
* @return void
2016-01-10 17:17:04 +01:00
*/
public function onAssetsInitialized()
{
// Disable Asset pipelining
$assets = $this->grav['assets'];
$assets->setJsPipeline(false);
$assets->setCssPipeline(false);
2020-04-18 17:45:11 -06:00
}
2014-08-05 13:06:38 -07:00
/**
* Add twig paths to plugin templates.
*
* @return void
2014-08-05 13:06:38 -07:00
*/
public function onTwigTemplatePaths()
2014-08-05 13:06:38 -07:00
{
$twig_paths = [];
$this->grav->fireEvent('onAdminTwigTemplatePaths', new Event(['paths' => &$twig_paths]));
$twig_paths[] = __DIR__ . '/themes/' . $this->theme . '/templates';
$this->grav['twig']->twig_paths = $twig_paths;
2021-04-15 14:13:33 -06:00
2014-08-05 13:06:38 -07:00
}
/**
* Set all twig variables for generating output.
*
* @return void
2014-08-05 13:06:38 -07:00
*/
public function onTwigSiteVariables()
2014-08-05 13:06:38 -07:00
{
/** @var Twig $twig */
$twig = $this->grav['twig'];
/** @var PageInterface $page */
2017-04-13 14:50:27 -06:00
$page = $this->grav['page'];
2014-08-05 13:06:38 -07:00
$twig->twig_vars['location'] = $this->template;
$twig->twig_vars['nav_route'] = trim($this->template . '/' . $this->route, '/') . '/';
$twig->twig_vars['base_url_relative_frontend'] = $twig->twig_vars['base_url_relative'] ?: '/';
2016-09-27 18:21:11 +03:00
$twig->twig_vars['admin_route'] = trim($this->admin_route, '/');
2019-02-11 14:33:48 -07:00
$twig->twig_vars['template_route'] = $this->template;
2018-05-23 22:33:08 +03:00
$twig->twig_vars['current_route'] = '/' . $twig->twig_vars['admin_route'] . '/' . $this->template . '/' . $this->route;
2016-07-07 18:55:52 +02:00
$twig->twig_vars['base_url_relative'] = $twig->twig_vars['base_url_simple'] . '/' . $twig->twig_vars['admin_route'];
$twig->twig_vars['current_url'] = rtrim($twig->twig_vars['base_url_relative'] . '/' . $this->template . '/' . $this->route, '/');
2016-07-15 17:39:13 +02:00
$theme_url = '/' . ltrim($this->grav['locator']->findResource('plugin://admin/themes/' . $this->theme,
false), '/');
$twig->twig_vars['theme_url'] = $theme_url;
2014-08-29 11:59:43 +03:00
$twig->twig_vars['base_url'] = $twig->twig_vars['base_url_relative'];
$twig->twig_vars['base_path'] = GRAV_ROOT;
2014-08-05 13:06:38 -07:00
$twig->twig_vars['admin'] = $this->admin;
2020-01-30 12:32:10 +02:00
$twig->twig_vars['user'] = $this->admin->user;
2016-04-26 12:57:29 -06:00
$twig->twig_vars['admin_version'] = $this->version;
2019-01-31 18:39:38 -07:00
$twig->twig_vars['logviewer'] = new LogViewer();
2019-03-12 06:51:53 -06:00
$twig->twig_vars['form_max_filesize'] = Utils::getUploadLimit() / 1024 / 1024;
2014-08-05 13:06:38 -07:00
2020-04-20 09:57:44 -06:00
// Start white label functionality
$twig->twig_vars['whitelabel_presets'] = $this->getPresets();
2020-04-18 17:45:11 -06:00
2020-04-20 09:57:44 -06:00
$custom_logo = $this->config->get('plugins.admin.whitelabel.logo_custom', false);
$custom_login_logo = $this->config->get('plugins.admin.whitelabel.logo_login', false);
$custom_footer = $this->config->get('plugins.admin.whitelabel.custom_footer', false);
2020-04-18 17:45:11 -06:00
if ($custom_logo && is_array($custom_logo)) {
$custom_logo = array_keys($custom_logo);
$path = array_shift($custom_logo);
$twig->twig_vars['custom_admin_logo'] = $path;
}
if ($custom_login_logo && is_array($custom_login_logo)) {
$custom_login_logo = array_keys($custom_login_logo);
$path = array_shift($custom_login_logo);
$twig->twig_vars['custom_login_logo'] = $path;
}
if ($custom_footer) {
$footer = Utils::processMarkdown($custom_footer);
$twig->twig_vars['custom_admin_footer'] = $footer;
}
2020-04-20 09:57:44 -06:00
$custom_css = $this->config->get('plugins.admin.whitelabel.custom_css', false);
2020-04-18 17:45:11 -06:00
if ($custom_css) {
$this->grav['assets']->addInlineCss($custom_css);
}
2020-04-20 09:57:44 -06:00
// End white label functionality
2020-04-18 17:45:11 -06:00
$fa_icons_file = CompiledYamlFile::instance($this->grav['locator']->findResource('plugin://admin/themes/grav/templates/forms/fields/iconpicker/icons' . YAML_EXT));
$fa_icons = $fa_icons_file->content();
$fa_icons = array_map(function ($icon) {
//only pick used values
return ['id' => $icon['id'], 'unicode' => $icon['unicode']];
}, $fa_icons['icons']);
$twig->twig_vars['fa_icons'] = $fa_icons;
2017-04-13 14:50:27 -06:00
// add form if it exists in the page
$header = $page->header();
$forms = [];
if (isset($header->forms)) foreach ($header->forms as $key => $form) {
$forms[$key] = new Form($page, null, $form);
}
$twig->twig_vars['forms'] = $forms;
// preserve form validation
if ($this->admin->form) {
$twig->twig_vars['form'] = $this->admin->form;
} elseif (!isset($twig->twig_vars['form'])) {
if (isset($header->form)) {
$twig->twig_vars['form'] = new Form($page);
} elseif (isset($header->forms)) {
$twig->twig_vars['form'] = new Form($page, null, reset($header->forms));
}
2017-04-13 14:50:27 -06:00
}
// Gather all nav items
$this->grav['twig']->plugins_hooked_nav = [];
2015-10-26 21:30:33 +01:00
$this->grav->fireEvent('onAdminMenu');
uasort($this->grav['twig']->plugins_hooked_nav, function ($a, $b) {
$ac = $a['priority'] ?? 0;
$bc = $b['priority'] ?? 0;
return $bc <=> $ac;
});
2015-10-26 21:30:33 +01:00
// Gather Plugin-hooked dashboard items
$this->grav->fireEvent('onAdminDashboard');
2014-08-05 13:06:38 -07:00
switch ($this->template) {
case 'dashboard':
$twig->twig_vars['popularity'] = $this->popularity;
2014-08-05 13:06:38 -07:00
break;
}
2017-12-01 16:08:52 -07:00
$flashData = $this->grav['session']->getFlashCookieObject(Admin::TMP_COOKIE_NAME);
if (isset($flashData->message)) {
$this->grav['messages']->add($flashData->message, $flashData->status);
}
2014-08-05 13:06:38 -07:00
}
2014-09-03 22:22:03 -06:00
/**
* Add images to twig template paths to allow inclusion of SVG files
*
* @return void
*/
public function onTwigLoader()
{
/** @var Twig $twig */
$twig = $this->grav['twig'];
/** @var UniformResourceLocator $locator */
$locator = Grav::instance()['locator'];
$theme_paths = $locator->findResources('plugins://admin/themes/' . $this->theme . '/images');
foreach($theme_paths as $images_path) {
$twig->addPath($images_path, 'admin-images');
}
}
/**
* Add the Admin Twig Extensions
*
* @return void
*/
public function onTwigExtensions()
{
/** @var Twig $twig */
$twig = $this->grav['twig'];
$twig->twig->addExtension(new AdminTwigExtension);
}
/**
* @param Event $event
* @return void
*/
public function onAdminAfterSave(Event $event)
2014-09-03 22:22:03 -06:00
{
// Special case to redirect after changing the admin route to avoid 'breaking'
$obj = $event['object'];
2014-09-03 22:22:03 -06:00
if (null !== $obj && method_exists($obj, 'blueprints')) {
$blueprint = $obj->blueprints()->getFilename();
if ($blueprint === 'admin/blueprints' && isset($obj->route) && $this->admin_route !== $obj->route) {
$redirect = preg_replace('/^' . str_replace('/','\/',$this->admin_route) . '/',$obj->route,$this->uri->path());
$this->grav->redirect($redirect);
}
}
}
/**
* Convert some types where we want to process out of the standard config path
*
* @param Event $e
* @return void
*/
public function onAdminData(Event $e)
{
$type = $e['type'] ?? null;
switch ($type) {
case 'config':
2020-02-03 13:21:19 +02:00
$e['type'] = $this->admin->authorize(['admin.configuration.system', 'admin.super']) ? 'config/system' : 'config/site';
break;
case 'tools/scheduler':
$e['type'] = 'config/scheduler';
break;
case 'tools':
case 'tools/backups':
$e['type'] = 'config/backups';
break;
}
}
/**
* @return void
*/
public function onOutputGenerated()
{
// Clear flash objects for previously uploaded files whenever the user switches page or reloads
// ignoring any JSON / extension call
if ($this->admin->task !== 'save' && empty($this->uri->extension())) {
// Discard any previously uploaded files session and remove all uploaded files.
if ($flash = $this->session->getFlashObject('files-upload')) {
$flash = new \RecursiveIteratorIterator(new \RecursiveArrayIterator($flash));
foreach ($flash as $key => $value) {
if ($key !== 'tmp_name') {
continue;
}
@unlink($value);
}
}
}
}
2020-01-10 20:28:30 +02:00
/**
* Initial stab at registering permissions (WIP)
*
2020-01-21 09:46:42 +02:00
* @param PermissionsRegisterEvent $event
* @return void
2020-01-10 20:28:30 +02:00
*/
2020-01-21 09:46:42 +02:00
public function onRegisterPermissions(PermissionsRegisterEvent $event): void
2020-01-10 20:28:30 +02:00
{
$actions = PermissionsReader::fromYaml("plugin://{$this->name}/permissions.yaml");
$permissions = $event->permissions;
$permissions->addActions($actions);
}
/**
* @return void
*/
public function onAdminMenu()
{
/** @var Twig $twig */
$twig = $this->grav['twig'];
// Dashboard
$twig->plugins_hooked_nav['PLUGIN_ADMIN.DASHBOARD'] = [
'route' => 'dashboard',
'icon' => 'fa-th',
'authorize' => ['admin.login', 'admin.super'],
'priority' => 10
];
// Configuration
$twig->plugins_hooked_nav['PLUGIN_ADMIN.CONFIGURATION'] = [
'route' => 'config',
'icon' => 'fa-wrench',
2020-02-03 13:21:19 +02:00
'authorize' => [
'admin.configuration.system',
'admin.configuration.site',
'admin.configuration.media',
'admin.configuration.security',
'admin.configuration.info',
2020-02-03 13:21:19 +02:00
'admin.super'],
'priority' => 9
];
// Pages
$count = new Container(['count' => function () { return $this->admin->pagesCount(); }]);
$twig->plugins_hooked_nav['PLUGIN_ADMIN.PAGES'] = [
'route' => 'pages',
'icon' => 'fa-file-text-o',
'authorize' => ['admin.pages', 'admin.super'],
'badge' => $count,
'priority' => 5
];
// Plugins
$count = new Container(['updates' => 0, 'count' => function () { return count($this->admin->plugins()); }]);
$twig->plugins_hooked_nav['PLUGIN_ADMIN.PLUGINS'] = [
'route' => 'plugins',
'icon' => 'fa-plug',
'authorize' => ['admin.plugins', 'admin.super'],
'badge' => $count,
'priority' => -8
];
// Themes
$count = new Container(['updates' => 0, 'count' => function () { return count($this->admin->themes()); }]);
$twig->plugins_hooked_nav['PLUGIN_ADMIN.THEMES'] = [
'route' => 'themes',
'icon' => 'fa-tint',
'authorize' => ['admin.themes', 'admin.super'],
'badge' => $count,
'priority' => -9
];
// Tools
$twig->plugins_hooked_nav['PLUGIN_ADMIN.TOOLS'] = [
'route' => 'tools',
'icon' => 'fa-briefcase',
'authorize' => $this->admin::toolsPermissions(),
'priority' => -10
];
}
/**
* Check if the current route is under the admin path
*
* @return bool
*/
public function isAdminPath()
{
$route = $this->uri->route();
return $route === $this->base || 0 === strpos($route, $this->base . '/');
}
/**
* Helper function to replace Pages::Types() and to provide an event to manipulate the data
*
* Dispatches 'onAdminPageTypes' event with 'types' data member which is a reference to the data
*
* @param bool $keysOnly
* @return array
*/
public static function pagesTypes(bool $keysOnly = false)
{
$types = Pages::types();
// First filter by configuration
$hideTypes = (array)Grav::instance()['config']->get('plugins.admin.hide_page_types');
foreach ($hideTypes as $hide) {
if (isset($types[$hide])) {
unset($types[$hide]);
} else {
foreach ($types as $key => $type) {
$match = preg_match('#' . $hide . '#i', $key);
if ($match) {
unset($types[$key]);
}
}
}
}
// Allow manipulating of the data by event
$e = new Event(['types' => &$types]);
Grav::instance()->fireEvent('onAdminPageTypes', $e);
return $keysOnly ? array_keys($types) : $types;
}
/**
* Helper function to replace Pages::modularTypes() and to provide an event to manipulate the data
*
* Dispatches 'onAdminModularPageTypes' event with 'types' data member which is a reference to the data
*
* @param bool $keysOnly
* @return array
*/
public static function pagesModularTypes(bool $keysOnly = false)
{
$types = Pages::modularTypes();
// First filter by configuration
$hideTypes = (array)Grav::instance()['config']->get('plugins.admin.hide_modular_page_types');
foreach ($hideTypes as $hide) {
if (isset($types[$hide])) {
unset($types[$hide]);
} else {
foreach ($types as $key => $type) {
$match = preg_match('#' . $hide . '#i', $key);
if ($match) {
unset($types[$key]);
}
}
}
}
// Allow manipulating of the data by event
$e = new Event(['types' => &$types]);
Grav::instance()->fireEvent('onAdminModularPageTypes', $e);
return $keysOnly ? array_keys($types) : $types;
}
/**
* Validate a value. Currently validates
*
* - 'user' for username format and username availability.
* - 'password1' for password format
* - 'password2' for equality to password1
*
* @param string $type The field type
* @param string $value The field value
* @param string $extra Any extra value required
*
* @return bool
*/
protected function validate($type, $value, $extra = '')
{
/** @var Login $login */
$login = $this->grav['login'];
return $login->validateField($type, $value, $extra);
}
/**
* @param string $task
* @param array|null $post
* @return void
*/
protected function initializeController($task, $post = null): void
{
Admin::DEBUG && Admin::addDebugMessage('Admin controller: execute');
$controller = new AdminController();
$controller->initialize($this->grav, $this->template, $task, $this->route, $post);
$controller->execute();
$controller->redirect();
}
/**
* @return void
*/
protected function setupAdmin()
{
// Set cache based on admin_cache option.
/** @var Cache $cache */
$cache = $this->grav['cache'];
$cache->setEnabled($this->config->get('plugins.admin.cache_enabled'));
/** @var Pages $pages */
$pages = $this->grav['pages'];
// Disable frontend pages in admin.
$pages->disablePages();
// Force file hash checks to fix caching on moved and deleted pages.
$pages->setCheckMethod('hash');
/** @var Session $session */
$session = $this->grav['session'];
// Make sure that the session has been initialized.
try {
$session->init();
} catch (SessionException $e) {
$session->init();
$message = 'Session corruption detected, restarting session...';
/** @var Debugger $debugger */
$debugger = $this->grav['debugger'];
$debugger->addMessage($message);
$this->grav['messages']->add($message, 'error');
}
$this->active = true;
}
/**
* Initialize the admin.
*
* @return void
* @throws \RuntimeException
*/
protected function initializeAdmin()
{
2015-07-27 12:56:16 -06:00
// Check for required plugins
2016-07-07 18:55:52 +02:00
if (!$this->grav['config']->get('plugins.login.enabled') || !$this->grav['config']->get('plugins.form.enabled') || !$this->grav['config']->get('plugins.email.enabled')) {
2015-07-27 12:56:16 -06:00
throw new \RuntimeException('One of the required plugins is missing or not enabled');
}
/** @var Cache $cache */
$cache = $this->grav['cache'];
// Have a unique Admin-only Cache key
$cache_key = $cache->getKey();
$cache->setKey($cache_key . '$');
/** @var Session $session */
$session = $this->grav['session'];
/** @var Language $language */
$language = $this->grav['language'];
/** @var UniformResourceLocator $locator */
$locator = $this->grav['locator'];
// Turn on Twig autoescaping
if ($this->uri->param('task') !== 'processmarkdown') {
$this->grav['twig']->setAutoescape(true);
}
// Initialize Admin Language if needed
if ($language->enabled() && empty($session->admin_lang)) {
$session->admin_lang = $language->getLanguage();
}
// Decide admin template and route.
$path = trim(substr($this->uri->route(), strlen($this->base)), '/');
2015-12-10 11:02:19 -07:00
if (empty($this->template)) {
$this->template = 'dashboard';
}
// Can't access path directly...
2018-05-10 10:14:18 +03:00
if ($path && $path !== 'register') {
$array = explode('/', $path, 2);
$this->template = array_shift($array);
$this->route = array_shift($array);
}
2014-09-03 22:22:03 -06:00
// Initialize admin class (also registers it to Grav services).
2016-09-27 18:21:11 +03:00
$this->admin = new Admin($this->grav, $this->admin_route, $this->template, $this->route);
// Get theme for admin
$this->theme = $this->config->get('plugins.admin.theme', 'grav');
// Replace themes service with admin.
$this->grav['themes'] = function () {
return new Themes($this->grav);
};
// Initialize white label functionality
$this->grav['admin-whitelabel'] = new WhiteLabel();
2021-04-15 14:13:33 -06:00
// Compile a missing preset.css file
$preset_css = 'asset://admin-preset.css';
$preset_path = $this->grav['locator']->findResource($preset_css);
if (!$preset_path) {
$this->grav['admin-whitelabel']->compilePresetScss($this->config->get('plugins.admin.whitelabel'));
}
// These events are needed for login.
$this->enable([
'onTwigExtensions' => ['onTwigExtensions', 1000],
'onPagesInitialized' => ['onPagesInitialized', 1000],
'onTwigLoader' => ['onTwigLoader', 1000],
'onTwigTemplatePaths' => ['onTwigTemplatePaths', 1000],
'onTwigSiteVariables' => ['onTwigSiteVariables', 1000],
'onAssetsInitialized' => ['onAssetsInitialized', 1000],
]);
// Do not do more if user isn't logged in.
if (!$this->admin->user->authorize('admin.login')) {
return;
}
// These events are not needed during login.
$this->enable([
'onAdminControllerInit' => ['onAdminControllerInit', 1001],
'onAdminDashboard' => ['onAdminDashboard', 0],
'onAdminMenu' => ['onAdminMenu', 1000],
'onAdminTools' => ['onAdminTools', 0],
'onAdminSave' => ['onAdminSave', 0],
'onAdminAfterSave' => ['onAdminAfterSave', 0],
'onAdminData' => ['onAdminData', 0],
'onOutputGenerated' => ['onOutputGenerated', 0],
]);
// Double check we have system.yaml, site.yaml etc
$config_path = $locator->findResource('user://config');
2018-12-05 08:20:38 +02:00
foreach ($this->admin::configurations() as $config_file) {
if ($config_file === 'info') {
continue;
}
$config_file = "{$config_path}/{$config_file}.yaml";
if (!file_exists($config_file)) {
touch($config_file);
}
}
$assets = $this->grav['assets'];
2016-07-07 18:55:52 +02:00
$translations = 'this.GravAdmin = this.GravAdmin || {}; if (!this.GravAdmin.translations) this.GravAdmin.translations = {}; ' . PHP_EOL . 'this.GravAdmin.translations.PLUGIN_ADMIN = {';
// Enable language translations
$translations_actual_state = $this->config->get('system.languages.translations');
$this->config->set('system.languages.translations', true);
2016-07-07 18:55:52 +02:00
$strings = [
'EVERYTHING_UP_TO_DATE',
'UPDATES_ARE_AVAILABLE',
'IS_AVAILABLE_FOR_UPDATE',
'AND',
'IS_NOW_AVAILABLE',
'CURRENT',
'UPDATE_GRAV_NOW',
'TASK_COMPLETED',
'UPDATE',
'UPDATING_PLEASE_WAIT',
'GRAV_SYMBOLICALLY_LINKED',
'OF_YOUR',
'OF_THIS',
'HAVE_AN_UPDATE_AVAILABLE',
'UPDATE_AVAILABLE',
'UPDATES_AVAILABLE',
'FULLY_UPDATED',
2015-10-21 19:54:10 +02:00
'DAYS',
'PAGE_MODES',
'PAGE_TYPES',
2016-02-01 11:27:34 -08:00
'ACCESS_LEVELS',
2016-02-01 16:34:50 -08:00
'NOTHING_TO_SAVE',
'FILE_UNSUPPORTED',
'FILE_ERROR_ADD',
'FILE_ERROR_UPLOAD',
'DROP_FILES_HERE_TO_UPLOAD',
'DELETE',
'UNSET',
'INSERT',
'METADATA',
'VIEW',
'UNDO',
'REDO',
'HEADERS',
'BOLD',
'ITALIC',
'STRIKETHROUGH',
'SUMMARY_DELIMITER',
'LINK',
'IMAGE',
'BLOCKQUOTE',
'UNORDERED_LIST',
'ORDERED_LIST',
'EDITOR',
'PREVIEW',
2016-05-06 18:17:26 +02:00
'FULLSCREEN',
'MODULE',
'NON_MODULE',
2016-05-06 18:17:26 +02:00
'VISIBLE',
'NON_VISIBLE',
'ROUTABLE',
'NON_ROUTABLE',
'PUBLISHED',
'NON_PUBLISHED',
'PLUGINS',
'THEMES',
'ALL',
'FROM',
[WIP] Ajax Files Upload (#748) * Reworked the `file` field. All files get uploaded via Ajax and are stored upon Save This improves the Save task tremendously as now there is no longer the need of waiting for the files to finish uploading. Fully backward compatible, `file` field now includes also a `limit` and `filesize` option in the blueprints. The former determines how many files are allowed to be uploaded when in combination with `multiple: true` (default: 10), the latter determines the file size limit (in MB) allowed for each file (default: 5MB) * Added support for `accept: [‘*’]` to allow any file type * Minor tweaks in the comments and messages * Delete any orphan file when discarding the uploaded files session * Minor optimization * Fixed issue with `_json` elements where nested fields merging would get stored in an unexpected way * Potential fix for wrong order of value in Datetime * Fixed nested fields for files * Fixed tmp streams * Minor cleanup * Update JSON data when removing a file. Implemented task to remove files that haven’t been saved yet, from the flash object session * Ensure temporary files are deleted when removing un-saved files from the flash object session * Fixed wrong reference of HTML file field when clicking on the drop zone area to pick a file * Added JSON template for pages * fix a CSS issue in page order * More CSS fixes * Trigger file field mutation when adding or removing a file * Recompiled JS * Removed twig templates that are no longer needed * Fixed issue with nested header fields in a page, not properly merging data * [internal] Fixed issue with collections not capable of handling both param and dot notations at the same time * Reorganized FileField structure to be more consistent with the other fields * Added support for dynamically created file fields (ie, autoinitialization on new lists items) * Added translationable strings for file uploads errors * Added translasions for all Dropzone available strings * Changed default values
2016-08-29 11:12:09 -07:00
'TO',
'DROPZONE_CANCEL_UPLOAD',
'DROPZONE_CANCEL_UPLOAD_CONFIRMATION',
'DROPZONE_DEFAULT_MESSAGE',
'DROPZONE_FALLBACK_MESSAGE',
'DROPZONE_FALLBACK_TEXT',
'DROPZONE_FILE_TOO_BIG',
'DROPZONE_INVALID_FILE_TYPE',
'DROPZONE_MAX_FILES_EXCEEDED',
'DROPZONE_REMOVE_FILE',
'DROPZONE_RESPONSE_ERROR'
2015-10-21 19:54:10 +02:00
];
2016-07-07 18:55:52 +02:00
foreach ($strings as $string) {
2016-02-01 11:27:34 -08:00
$separator = (end($strings) === $string) ? '' : ',';
2018-12-05 08:20:38 +02:00
$translations .= '"' . $string . '": "' . htmlspecialchars($this->admin::translate('PLUGIN_ADMIN.' . $string)) . '"' . $separator;
}
2016-02-01 11:27:34 -08:00
$translations .= '};';
$translations .= 'this.GravAdmin.translations.PLUGIN_FORM = {';
$strings = ['RESOLUTION_MIN', 'RESOLUTION_MAX'];
foreach ($strings as $string) {
$separator = (end($strings) === $string) ? '' : ',';
2018-12-05 08:20:38 +02:00
$translations .= '"' . $string . '": "' . $this->admin::translate('PLUGIN_FORM.' . $string) . '"' . $separator;
}
$translations .= '};';
$translations .= 'this.GravAdmin.translations.GRAV_CORE = {';
$strings = [
'NICETIME.SECOND',
'NICETIME.MINUTE',
'NICETIME.HOUR',
'NICETIME.DAY',
'NICETIME.WEEK',
'NICETIME.MONTH',
'NICETIME.YEAR',
'CRON.EVERY',
'CRON.EVERY_HOUR',
'CRON.EVERY_MINUTE',
'CRON.EVERY_DAY_OF_WEEK',
'CRON.EVERY_DAY_OF_MONTH',
'CRON.EVERY_MONTH',
'CRON.TEXT_PERIOD',
'CRON.TEXT_MINS',
'CRON.TEXT_TIME',
'CRON.TEXT_DOW',
'CRON.TEXT_MONTH',
'CRON.TEXT_DOM',
'CRON.ERROR1',
'CRON.ERROR2',
'CRON.ERROR3',
2018-10-24 14:46:24 -06:00
'CRON.ERROR4',
'MONTHS_OF_THE_YEAR',
'DAYS_OF_THE_WEEK'
];
foreach ($strings as $string) {
$separator = (end($strings) === $string) ? '' : ',';
2018-12-05 08:20:38 +02:00
$translations .= '"' . $string . '": ' . json_encode($this->admin::translate('GRAV.'.$string)) . $separator;
}
$translations .= '};';
// set the actual translations state back
$this->config->set('system.languages.translations', $translations_actual_state);
$assets->addInlineJs($translations);
// Fire even to register permissions from other plugins
$this->grav->fireEvent('onAdminRegisterPermissions', new Event(['admin' => $this->admin]));
2014-09-03 22:22:03 -06:00
}
2020-04-18 17:45:11 -06:00
/**
* @return array
*/
2020-05-08 17:50:03 -06:00
public static function themeOptions()
{
static $options;
2020-05-08 17:50:03 -06:00
if (null === $options) {
$options = [];
2020-05-08 17:50:03 -06:00
$theme_files = glob(__dir__ . '/themes/grav/css/codemirror/themes/*.css');
foreach ($theme_files as $theme_file) {
$theme = Utils::basename(Utils::basename($theme_file, '.css'));
2020-05-08 17:50:03 -06:00
$options[$theme] = Inflector::titleize($theme);
}
}
return $options;
}
/**
* @return array
*/
2020-04-18 17:45:11 -06:00
public function getPresets()
{
2020-04-19 16:38:13 -06:00
$filename = $this->grav['locator']->findResource('plugin://admin/presets.yaml', false);
2020-04-18 17:45:11 -06:00
$file = CompiledYamlFile::instance($filename);
$presets = (array)$file->content();
2020-05-08 17:50:03 -06:00
$custom_presets = $this->config->get('plugins.admin.whitelabel.custom_presets');
if (isset($custom_presets)) {
$custom_presets = Yaml::parse($custom_presets);
if (is_array($custom_presets)) {
if (isset($custom_presets['name'], $custom_presets['colors'], $custom_presets['accents'])) {
2020-05-08 17:50:03 -06:00
$preset = [Inflector::hyphenize($custom_presets['name']) => $custom_presets];
$presets = $preset + $presets;
} else {
2020-05-09 14:21:46 -06:00
foreach ($custom_presets as $value) {
if (isset($value['name'], $value['colors'], $value['accents'])) {
2020-05-09 14:21:46 -06:00
$preset = [Inflector::hyphenize($value['name']) => $value];
$presets = $preset + $presets;
2020-05-09 14:21:03 -06:00
}
2020-05-08 17:50:03 -06:00
}
}
}
}
2020-04-18 17:45:11 -06:00
return $presets;
}
2014-08-05 13:06:38 -07:00
}