Merge branch 'release/1.10.0'

This commit is contained in:
Andy Miller
2021-01-19 12:00:35 -07:00
329 changed files with 154602 additions and 7326 deletions

2
.gitattributes vendored
View File

@@ -1,7 +1,7 @@
# Linguist Normalizer
*.yaml linguistic-language=PHP
*.twig linguistic-language=PHP
**/gulpfile.js linguist-vendored
**/gulpfile.babel.js linguist-vendored
**/webpack.conf.js linguist-vendored
**/js/*.js linguist-vendored
**/js/*.json linguist-vendored

1
.gitignore vendored
View File

@@ -13,3 +13,4 @@ tests/_output/*
tests/_support/_generated/*
tests/cache/*
tests/error.log
/crowdin.yaml

View File

@@ -1,3 +1,354 @@
# v1.10.0
## 01/19/2021
1. [](#new)
* Requires **Grav 1.7 and PHP 7.3.6**
* Read about this release in the [Grav 1.7 Released](https://getgrav.org/blog/grav-1.7-released) blog post
* Read the full list of changes in the [Changelog on GitHub](https://github.com/getgrav/grav-plugin-admin/blob/1.10.0/CHANGELOG.md)
* Please read [Grav 1.7 Upgrade Guide](https://learn.getgrav.org/17/advanced/grav-development/grav-17-upgrade-guide) before upgrading!
1. [](#improved)
* Various notifications improvements
1. [](#bugfix)
* Fixed missed highlight on the selected page in Parents field
* Fixed notifications that would not be remembered as hidden
* Fixed taxonomy field not listing existing options in Flex Pages
* Fixed taxonomy field not working outside pages
* Fixed fatal error when moving a page using the old implementation [#2019](https://github.com/getgrav/grav-plugin-admin/issues/2019)
* Fixed evaluating default value in `hidden` field (thanks @NicoHood)
# v1.10.0-rc.20
## 12/14/2020
1. [](#improved)
* Cookies now explicitly set `SameSite` to `Lax` unless otherwise specified [#1998](https://github.com/getgrav/grav-plugin-admin/issues/1998)
* Exposed **Cookies** class (`Grav.default.Utils.Cookies`) for developers that need it in Admin.
1. [](#bugfix)
* Fixed Plugins references in Themes details page.
* Fixed issue preventing purchase of Themes within Admin and redirecting instead.
* Regression: Values inside Fieldset do not display [#1995](https://github.com/getgrav/grav-plugin-admin/issues/1995)
# v1.10.0-rc.19
## 12/02/2020
1. [](#improved)
* Just keeping sync with Grav rc.19
# v1.10.0-rc.18
## 12/02/2020
1. [](#new)
* Retired "Secure Delete" and "Warn on page delete". You are now always warned and asked to confirm a deletion.
1. [](#improved)
* Auto-link a plugin/theme license in details if it starts with `http`
* Allow to fallback to `docs:` instead of `readme:`
* Forward a `sid` to GPM when downloading a premium package
* Better support for array field key/value when either key or value is stored empty [#1972](https://github.com/getgrav/grav-plugin-admin/issues/1972)
* Remember the open state of the sidebar [#1973](https://github.com/getgrav/grav-plugin-admin/issues/1973)
* Upgraded node dependencies to latest version. Improved speed of JS compilation.
* Added modal to confirm updating Grav as well as cool down counter before enabling Update button [#1257](https://github.com/getgrav/grav-plugin-admin/issues/1257)
* Better handling of offline/intranet mode when the repository index is missing. Faster admin. [#1916](https://github.com/getgrav/grav-plugin-admin/issues/1916)
* Statistics is now Page View Statistics [#1885](https://github.com/getgrav/grav-plugin-admin/issues/1885)
* It is now possible to use regex as values for "Hide page types in Admin" and "Hide modular page types in Admin" settings [#1828](https://github.com/getgrav/grav-plugin-admin/issues/1828)
* Default to `disabled` state for all cron-jobs
1. [](#bugfix)
* Fixed Safari issue with new ACL picker field [#1955](https://github.com/getgrav/grav-plugin-admin/issues/1955)
* Stop propagation of ACL add button in ACL picker [flex-objects#83](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/83)
* Fixed missing special groups `authors` and `defaults` for pages
* Fixed Page Move action and selection highlight in Parents selector modal [flex-objects#80](https://github.com/trilbymedia/grav-plugin-flex-objects/issues/80)
* Fixed folder auto-naming in Add Module [#1937](https://github.com/getgrav/grav-plugin-admin/issues/1937)
* Fixed remodal issue triggering close when selecting a dropdown item ending outside of scope [#1682](https://github.com/getgrav/grav-plugin-admin/issues/1682)
* Reworked how collapsed lists work so the tooltip is not cut off [#1928](https://github.com/getgrav/grav-plugin-admin/issues/1928)
* Fixed KeepAlive issue where too large of a session value would fire the keep alive immediately [#1860](https://github.com/getgrav/grav-plugin-admin/issues/1860)
* Fixed stringable objects breaking the inputs
* Fixed filepicker, pagemediaselect fields with `multiple: true` and `array: true` [#1580](https://github.com/getgrav/grav-plugin-admin/issues/1580)
# v1.10.0-rc.17
## 10/07/2020
1. [](#new)
* Support premium themes
1. [](#improved)
* Improved some error messages for better readability
* Strip tags from browser title
1. [](#bugfix)
* More multi-site routing fixes
* Fixed issue that would force a page reload when failing to install/update a plugin or theme.
* Fixed proxy/browser caching issues in admin pages
# v1.10.0-rc.16
## 09/01/2020
1. [](#improved)
* Made all the `onAdmin*` CRUD events to pass `object` (and backwards compatible `page`) to make them easier to use
* Updated vendor libraries including `SCSSPHP` to v1.2
1. [](#bugfix)
* Fixed issue with File field being used in Theme/Plugins
* Fixed bad redirection after successful admin login in subdirectory multisite [#1487](https://github.com/getgrav/grav-plugin-admin/issues/1487)
# v1.10.0-rc.15
## 07/22/2020
1. [](#bugfix)
* Disabled the EXIF library for Dropzone for fixing the orientation as it was getting applied twice [#1923](https://github.com/getgrav/grav-plugin-admin/issues/1923)
* Forked Dropzone fo fix issue with Resize + Exif orientation [#1923](https://github.com/getgrav/grav-plugin-admin/issues/1923)
* Fixed URI encode for the preview of images names
# v1.10.0-rc.14
## 07/09/2020
1. [](#improved)
* Completely removed old Google font support for upgrade compatibility
1. [](#bugfix)
* Fixed bad `use` reference to `UserObject`
# v1.10.0-rc.13
## 07/01/2020
1. [](#improved)
* Improved color picker field
* Trim login route for safety
* Composer update to grab latest vendor libs
# v1.10.0-rc.12
## 06/08/2020
1. [](#new)
* Added ability to set a preferred markdown editor in user profile
* Added new `onAdminListContentEditors` event to add a custom editor to the list of available
1. [](#bugfix)
* Fixed issue deleting file from a plugin's configuration
* Use `Pages::find()` instead of `Pages::dispatch()` as we do not want to redirect out of admin
* Fixed broken `parent` field when using the old pages
* Fixed broken `file` field preview when using streams in the path
# v1.10.0-rc.11
## 05/14/2020
1. [](#new)
* Major enhancements to "White Label" functionality including ability to export/import presets
* New horizontal scroller for theme presets
* Codemirror Fontsize / Preset / Font preference options
1. [](#improved)
* Fixed lots of styling issues related to "White Label" presets
* Changed out "One Light" theme for new "Firewatch Light" theme
* New scrolling system based on `SimpleBar` + native CSS scrollbar styling
# v1.10.0-rc.10
## 04/30/2020
1. [](#new)
* Addd new `taskConvertUrls` method for use with 3rd party editors
# v1.10.0-rc.9
## 04/27/2020
1. [](#new)
* Added new "White Label" functionality to customize admin colors + logos
* Added badge count for children in the Parents field
1. [](#improved)
* Added markdown support to `text` in `section` field
1. [](#bugfix)
* Prevent loading Pages in Parents field if they don't have children
* Fixed custom folder in `mediapicker` field not working with streams
* Fixed language redirect adding extra language prefix in Flex
* Fixed `Invalid input in "Parent"` when saving page in raw mode [#1869](https://github.com/getgrav/grav-plugin-admin/issues/1869)
# v1.10.0-rc.8
## 03/19/2020
1. [](#new)
* Added `has-children` flag in parent field data response
* Added `RESET` en lang string
1. [](#bugfix)
* Fixed parent field not working with regular pages
# v1.10.0-rc.7
## 03/05/2020
1. [](#new)
* Enable admin cache by default (for existing sites, check `Plugins > Admin Panel > Enable Admin Caching`)
1. [](#improved)
* Removed old `scss.sh` and `watch.sh` scripts, use `gulp watch-css`
* Added keysOnly parameter to `AdminPlugin::pagesTypes()` and `AdminPlugin::pagesModularTypes()` methods
* Added ignore parameter to `Admin::types()` and `Admin::modularTypes()` methods
* Improved configuration fields for hiding page types in Admin
1. [](#bugfix)
* Fixed minor UI padding in Flex pages [#1825](https://github.com/getgrav/grav-plugin-admin/issues/1825)
* Fixed `column` and `section` fields loosing user entered value when form submit fails
* Fixed `order` field not working with a new Flex Page
# v1.10.0-rc.6
## 02/11/2020
1. [](#new)
* Pass phpstan level 1 tests
* Updated semver library to v1.5
* Require flex-objects plugin
1. [](#improved)
* Added some debugging messages (turned off by default)
# v1.10.0-rc.5
## 02/03/2020
1. [](#new)
* No changes, just keeping things in sync with Grav RC version
# v1.10.0-rc.4
## 02/03/2020
1. [](#new)
* Added message to dashboard to install Flex Objects plugin if it is missing
* Updated `permissions` field to use new `$grav['permissions']`
* DEPRECATED `onAdminRegisterPermissions` event, use `PermissionsRegisterEvent::class` event instead
* DEPRECATED `Admin::setPermissions()` and `Admin::addPermissions()`, use `PermissionsRegisterEvent::class` event instead
* DEPRECATED `Admin::getPermissions()`, use `$grav['permissions']->getInstances()` instead
1. [](#improved)
* Added `field.show_label` and `field.label` display logic from frontend forms
1. [](#bugfix)
* Fixed user profile when using `Flex Users` only in admin
* Fixed saving data with empty field, default value (from config, plugin, theme) was used instead
* Fixed JS bug is using empty Grav URI param key
* Fixed bug in toggleable field being disabled with empty value (`''` `0`, `false`, `[]`...)
* Fixed `admin_route()` twig function to work properly with Grav 1.7.0-rc.4, which fixes `Route` base
* Fixed misleading 'Show sensitive data' configuration option wording [#1818](https://github.com/getgrav/grav-plugin-admin/issues/1818)
# v1.10.0-rc.3
## 01/02/2020
1. [](#new)
* Added ability to display **Changelogs** for `Grav`, `Plugins` and `Themes`
* Added raw root page support for `Flex Pages`
# v1.10.0-rc.2
## 12/04/2019
1. [](#new)
* Added support for hiding parts of admin by `Deny` permissions (`Flex Users` only)
* Optimized `parent` field for Flex Pages
1. [](#improved)
* Improved `permissions` field to add support for displaying calculated permissions
* Grav 1.7: Updated deprecated `$page->modular()` method calls to `$page->isModule()`
* Output the current process user name in Scheduler instructions
* Translations: rename MODULAR to MODULE everywhere
1. [](#bugfix)
* Fixed `permissions` field with nested permissions
* Fixed Save Shortcut (CTRL + S / CMD + S) not working with new Flex Pages [#1787](https://github.com/getgrav/grav-plugin-admin/issues/1787)
# v1.10.0-rc.1
## 11/06/2019
1. [](#new)
* Added a new `onAdminLogFiles()` event for 3rd party plugins to register log files for log viewer [#1765](https://github.com/getgrav/grav-plugin-admin/issues/1765)
1. [](#improved)
* Improved delete button UI [#1769](https://github.com/getgrav/grav-plugin-admin/issues/1769)
* Ability to configure display of 3rd party dashboard widgets [#1766](https://github.com/getgrav/grav-plugin-admin/issues/1766)
1. [](#bugfix)
* Fixed administrator user creation when `Flex Users` is enabled
* Fixed minor button alignment in FF [#1760](https://github.com/getgrav/grav-plugin-admin/issues/1760)
# v1.10.0-beta.10
## 10/03/2019
1. [](#bugfix)
* Regression: Fixed language assignments for the pages without set language
# v1.10.0-beta.9
## 09/26/2019
1. [](#bugfix)
* Make pages field to work with Flex Pages
# v1.10.0-beta.8
## 09/19/2019
1. [](#new)
* Add ability to Sanitize SVGs on file upload
* Add ability to Sanitize SVGs in Page media
1. [](#improved)
* YAML linter report now supports multi-language
* Better colors/placement of toolbar buttons in page edit view
1. [](#bugfix)
* Fixed missing language for AJAX requests
* Fixed redirect with absolute language URL
* Fixed issue with user avatar reference not being deleted when image removed
# v1.10.0-beta.7
## 08/30/2019
1. [](#bugfix)
* Fixed regression: Do not require Flex Objects plugin [grav#2653](https://github.com/getgrav/grav/issues/2653)
# v1.10.0-beta.6
## 08/29/2019
1. [](#improved)
* Optimized admin for speed (only load frontend pages on demand)
* Updated navigation menu to be fully controlled and overrideable by `onAdminMenu` event
* Lots of Flex Page speed improvements
# v1.10.0-beta.5
## 08/11/2019
1. [](#new)
* Added `data()` twig function to create data object from an array
1. [](#improved)
* Better support for `array` field into `list` field
* Made RAW blueprints (expert mode) to work properly with Flex Form
* Better support for `clockwork` logs
1. [](#bugfix)
* Fixed issue with nested `list` fields both utilizing the custom `key` functionality
* Regression: Page Preview not working, bad url [#1715](https://github.com/getgrav/grav-plugin-admin/issues/1715)
* Fixed '+New Folder' to work with new parent picker
* Fixed missing XSS check field when editing modular page as raw
* Fixed minor CSS layout issue [#1717](https://github.com/getgrav/grav-plugin-admin/issues/1717)
# v1.10.0-beta.4
## 07/01/2019
1. [](#new)
* Added `Admin::redirect()` method to allow redirects to be used outside of controllers
* Added `$admin->adminRoute()` method and `admin_route()` twig function to create language aware admin page links
* Renamed `Admin::route()` to `Admin::getCurrentRoute()` and deprecated the old call
1. [](#improved)
* Much improved multi-language support for pages
* Admin redirects should now work better with multiple languages enabled
1. [](#bugfix)
* Fixed default language being renamed to `page.en.md` (English) instead of keeping existing `page.md` filename
* Fixed possibility to override already existing translation by `Save As Language`
* Fixed missing default translation if page used plain `.md` file extension without language code
* Fixed wrong translation showing up as page fallback language
* Integrated Admin 1.9.8 bug fixes
# v1.10.0-beta.3
## 06/24/2019
1. [](#improved)
* Smarter handling of symlinks in parent field
1. [](#bugfix)
* Fixed issue with windows paths in `parent` field [#1699](https://github.com/getgrav/grav-plugin-admin/issues/1699)
# v1.10.0-beta.2
## 06/21/2019
1. [](#improved)
* Moved Remodal in-house and added support for stackable modals [#1698](https://github.com/getgrav/grav-plugin-admin/issues/1698), [#1699](https://github.com/getgrav/grav-plugin-admin/issues/1699)
1. [](#bugfix)
* Fixed missing check for maximum allowed files in `files` field
# v1.10.0-beta.1
## 06/14/2019
1. [](#new)
* New Parent/Move field using Ajax for better performance
* Improvements to cache clearing when admin cache is enabled
* Require Grav v1.7
* Use PSR-4 for plugin classes
* Added support for Twig 2.11 (compatible with Twig 1.40+)
1. [](#improved)
* Various admin performance improvements
1. [](#bugfix)
* Fixed admin caching issues
# v1.9.19
## 12/14/2020

View File

@@ -105,6 +105,47 @@ The **standard free version**, is very powerful, and has more functionality than
We also intend to release in the near future a more feature-rich **pro version** that will include enhanced functionality, as well as some additional nice-to-have capabilities. This pro version will be a **paid** plugin the price of which is not yet 100% finalized.
# Admin Events
## General events
- onAdminRegisterPermissions - (admin)
- onAdminThemeInitialized
- onAdminPage - (page)
- onAdminMenu
- onAdminTwigTemplatePaths - (paths)
## Page specific events
- onAdminDashboard
- onAdminTools - (tools)
- onAdminLogFiles - (logs)
- onAdminGenerateReports - (reports)
## Tasks
- onAdminControllerInit - (controller)
- onAdminTaskExecute - (controller, method)
## Editing
- onAdminData
- onAdminSave - (object)
- onAdminAfterSave - (object)
## Pages
- onAdminPageTypes - (types)
- onAdminModularPageTypes
- onAdminSave - (page)
- onAdminAfterSaveAs - (path)
- onAdminAfterSave - (page)
- onAdminAfterDelete - (page)
- onAdminAfterAddMedia - (page)
- onAdminAfterDelMedia - (page)
- onAdminCreatePageFrontmatter - (header, data)
# Running Tests
First install the dev dependencies by running `composer update` from the Grav root.

6
UPGRADE.md Normal file
View File

@@ -0,0 +1,6 @@
# Upgrading to Admin 1.10
Twig:
* **Admin link**: When linking to another admin page, use `{{ admin_route('/config/site') }}` instead of any other method, such as `{{ base_url_relative }}/config/site` (fixes multi-language issues)

411
admin.php
View File

@@ -3,6 +3,7 @@ namespace Grav\Plugin;
use Composer\Autoload\ClassLoader;
use Grav\Common\Cache;
use Grav\Common\Data\Data;
use Grav\Common\Debugger;
use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Grav;
@@ -18,6 +19,10 @@ use Grav\Common\Session;
use Grav\Common\Uri;
use Grav\Common\User\Interfaces\UserCollectionInterface;
use Grav\Common\Utils;
use Grav\Common\Yaml;
use Grav\Events\PermissionsRegisterEvent;
use Grav\Framework\Acl\PermissionsReader;
use Grav\Framework\Psr7\Response;
use Grav\Framework\Session\Exceptions\SessionException;
use Grav\Plugin\Admin\Admin;
use Grav\Plugin\Admin\Popularity;
@@ -25,8 +30,10 @@ use Grav\Plugin\Admin\Router;
use Grav\Plugin\Admin\Themes;
use Grav\Plugin\Admin\AdminController;
use Grav\Plugin\Admin\Twig\AdminTwigExtension;
use Grav\Plugin\Admin\WhiteLabel;
use Grav\Plugin\Form\Form;
use Grav\Plugin\Login\Login;
use Pimple\Container;
use RocketTheme\Toolbox\Event\Event;
class AdminPlugin extends Plugin
@@ -87,6 +94,8 @@ class AdminPlugin extends Plugin
'onShutdown' => ['onShutdown', 1000],
'onAdminDashboard' => ['onAdminDashboard', 0],
'onAdminTools' => ['onAdminTools', 0],
'onAdminSave' => ['onAdminSave', 0],
PermissionsRegisterEvent::class => ['onRegisterPermissions', 1000],
];
}
@@ -115,6 +124,33 @@ class AdminPlugin extends Plugin
],
'file' => [
'array' => true
],
'permissions' => [
'ignore_empty' => true,
'validate' => [
'type' => 'array'
],
'filter' => [
'type' => 'flatten_array',
'value_type' => 'bool',
]
],
'acl_picker' => [
'ignore_empty' => true,
'validate' => [
'type' => 'array'
],
'filter' => [
'type' => 'array',
'key_type' => 'string',
'value_type' => 'bool',
]
],
'taxonomy' => [
'multiple' => true,
'validate' => [
'type' => 'array'
]
]
];
}
@@ -158,6 +194,10 @@ class AdminPlugin extends Plugin
// Only activate admin if we're inside the admin path.
if ($this->isAdminPath()) {
$pages = $this->grav['pages'];
if (method_exists($pages, 'disablePages')) {
$pages->disablePages();
}
try {
$this->grav['session']->init();
} catch (SessionException $e) {
@@ -200,7 +240,7 @@ class AdminPlugin extends Plugin
}
// Turn on Twig autoescaping
if (method_exists($this->grav['twig'], 'setAutoescape') && $this->grav['uri']->param('task') !== 'processmarkdown') {
if ($this->grav['uri']->param('task') !== 'processmarkdown') {
$this->grav['twig']->setAutoescape(true);
}
@@ -216,6 +256,9 @@ class AdminPlugin extends Plugin
$this->grav['themes'] = function () {
return new Themes($this->grav);
};
// Initialize white label functionality
$this->grav['admin-whitelabel'] = new WhiteLabel();
}
// We need popularity no matter what
@@ -244,6 +287,25 @@ class AdminPlugin extends Plugin
}
}
/**
* Force compile during save if admin plugin save
*
* @param Event $event
*/
public function onAdminSave(Event $event)
{
$obj = $event['object'];
if ($obj instanceof Data && $obj->blueprints()->getFilename() === 'admin/blueprints') {
[$status, $msg] = $this->grav['admin-whitelabel']->compilePresetScss($obj);
if (!$status) {
$this->grav['messages']->add($msg, 'error');
}
}
}
/**
* [onPageInitialized:0]
*/
@@ -270,9 +332,9 @@ class AdminPlugin extends Plugin
$form = $event['form'];
$action = $event['action'];
Admin::DEBUG && Admin::addDebugMessage('Admin Form: ' . $action);
switch ($action) {
case 'register_admin_user':
if (Admin::doAnyUsersExist()) {
throw new \RuntimeException('A user account already exists, please create an admin account manually.');
}
@@ -320,7 +382,6 @@ class AdminPlugin extends Plugin
$data['fullname'] = $data['fullname'] ?? $inflector->titleize($username);
$data['title'] = $data['title'] ?? 'Administrator';
$data['state'] = 'enabled';
$data['access'] = ['admin' => ['login' => true, 'super' => true], 'site' => ['login' => true]];
/** @var UserCollectionInterface $users */
$users = $this->grav['accounts'];
@@ -328,14 +389,15 @@ class AdminPlugin extends Plugin
// Create user object and save it
$user = $users->load($username);
$user->update($data);
$user->set('access', ['admin' => ['login' => true, 'super' => true], 'site' => ['login' => true]]);
$user->save();
//Login user
// Login user
$this->grav['session']->user = $user;
unset($this->grav['user']);
$this->grav['user'] = $user;
$user->authenticated = true;
$user->authorized = $user->authorize('admin.login');
$user->authorized = $user->authorize('admin.login') ?? false;
$messages = $this->grav['messages'];
$messages->add($this->grav['language']->translate('PLUGIN_ADMIN.LOGIN_LOGGED_IN'), 'info');
@@ -370,11 +432,27 @@ class AdminPlugin extends Plugin
*/
public function onAdminDashboard()
{
$this->grav['twig']->plugins_hooked_dashboard_widgets_top[] = ['template' => 'dashboard-maintenance'];
$this->grav['twig']->plugins_hooked_dashboard_widgets_top[] = ['template' => 'dashboard-statistics'];
$this->grav['twig']->plugins_hooked_dashboard_widgets_top[] = ['template' => 'dashboard-notifications'];
$this->grav['twig']->plugins_hooked_dashboard_widgets_top[] = ['template' => 'dashboard-feed'];
$this->grav['twig']->plugins_hooked_dashboard_widgets_main[] = ['template' => 'dashboard-pages'];
$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',
];
}
@@ -387,12 +465,13 @@ class AdminPlugin extends Plugin
*/
public function onAdminTools(Event $event)
{
$lang = $this->grav['language'];
$event['tools'] = array_merge($event['tools'], [
'backups' => [['admin.maintenance', 'admin.super'], $this->grav['language']->translate('PLUGIN_ADMIN.BACKUPS')],
'scheduler' => [['admin.super'], $this->grav['language']->translate('PLUGIN_ADMIN.SCHEDULER')],
'logs' => [['admin.super'], $this->grav['language']->translate('PLUGIN_ADMIN.LOGS')],
'reports' => [['admin.super'], $this->grav['language']->translate('PLUGIN_ADMIN.REPORTS')],
'direct-install' => [['admin.super'], $this->grav['language']->translate('PLUGIN_ADMIN.DIRECT_INSTALL')],
'backups' => [['admin.maintenance', 'admin.super'], $lang->translate('PLUGIN_ADMIN.BACKUPS')],
'scheduler' => [['admin.super'], $lang->translate('PLUGIN_ADMIN.SCHEDULER')],
'logs' => [['admin.super'], $lang->translate('PLUGIN_ADMIN.LOGS')],
'reports' => [['admin.super'], $lang->translate('PLUGIN_ADMIN.REPORTS')],
'direct-install' => [['admin.super'], $lang->translate('PLUGIN_ADMIN.DIRECT_INSTALL')],
]);
return $event;
@@ -408,6 +487,7 @@ class AdminPlugin extends Plugin
// Force SSL with redirect if required
if ($config->get('system.force_ssl')) {
if (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') {
Admin::DEBUG && Admin::addDebugMessage('Admin SSL forced on, redirect');
$url = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
$this->grav->redirect($url);
}
@@ -415,9 +495,6 @@ class AdminPlugin extends Plugin
$this->session = $this->grav['session'];
// Set original route for the home page.
$home = '/' . trim($this->config->get('system.home.alias'), '/');
// set session variable if it's passed via the url
if ($this->uri->param('mode') === 'expert') {
$this->session->expert = true;
@@ -428,37 +505,19 @@ class AdminPlugin extends Plugin
$this->session->expert = $this->session->expert ?? false;
}
/** @var Pages $pages */
$pages = $this->grav['pages'];
$this->grav['admin']->routes = $pages->routes();
// Remove default route from routes.
if (isset($this->grav['admin']->routes['/'])) {
unset($this->grav['admin']->routes['/']);
}
$page = $pages->dispatch('/', true);
// If page is null, the default page does not exist, and we cannot route to it
if ($page) {
$page->route($home);
}
// Make local copy of POST.
$post = $this->grav['uri']->post();
// Initialize Page Types
Pages::types();
// Handle tasks.
$this->admin->task = $task = $this->grav['task'] ?? $this->grav['action'];
if ($task) {
Admin::DEBUG && Admin::addDebugMessage("Admin task: {$task}");
$this->initializeController($task, $post);
} elseif ($this->template === 'logs' && $this->route) {
// Display RAW error message.
echo $this->admin->logEntry();
exit();
$response = new Response(200, [], $this->admin->logEntry());
$this->grav->close($response);
}
$self = $this;
@@ -466,8 +525,6 @@ class AdminPlugin extends Plugin
// make sure page is not frozen!
unset($this->grav['page']);
$this->admin->pagesCount();
// Replace page service with admin.
$this->grav['page'] = function () use ($self) {
$page = new Page();
@@ -481,12 +538,15 @@ class AdminPlugin extends Plugin
$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/{$self->template}.md")) {
Admin::DEBUG && Admin::addDebugMessage("Admin page: {$self->template}");
$page->init(new \SplFileInfo(__DIR__ . "/pages/admin/{$self->template}.md"));
$page->slug(basename($self->template));
@@ -505,6 +565,8 @@ class AdminPlugin extends Plugin
$path = $locator->findResource("plugins://{$plugin->name}/admin/pages/{$self->template}.md");
if ($path) {
Admin::DEBUG && Admin::addDebugMessage("Admin page: plugin {$plugin->name}/{$self->template}");
$page->init(new \SplFileInfo($path));
$page->slug(basename($self->template));
@@ -517,6 +579,7 @@ class AdminPlugin extends Plugin
if (empty($this->grav['page'])) {
if ($this->grav['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);
@@ -524,6 +587,7 @@ class AdminPlugin extends Plugin
$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));
@@ -534,6 +598,7 @@ class AdminPlugin extends Plugin
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();
@@ -557,6 +622,14 @@ class AdminPlugin extends Plugin
$assets = $this->grav['assets'];
$assets->setJsPipeline(false);
$assets->setCssPipeline(false);
// 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'));
}
}
/**
@@ -581,6 +654,7 @@ class AdminPlugin extends Plugin
$page = $this->grav['page'];
$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'] ?: '/';
$twig->twig_vars['admin_route'] = trim($this->admin_route, '/');
$twig->twig_vars['template_route'] = $this->template;
@@ -590,14 +664,45 @@ class AdminPlugin extends Plugin
$theme_url = '/' . ltrim($this->grav['locator']->findResource('plugin://admin/themes/' . $this->theme,
false), '/');
$twig->twig_vars['theme_url'] = $theme_url;
$twig->twig_vars['preset_url'] = $twig->twig_vars['preset_url'] ?? $theme_url;
$twig->twig_vars['base_url'] = $twig->twig_vars['base_url_relative'];
$twig->twig_vars['base_path'] = GRAV_ROOT;
$twig->twig_vars['admin'] = $this->admin;
$twig->twig_vars['user'] = $this->admin->user;
$twig->twig_vars['admin_version'] = $this->version;
$twig->twig_vars['logviewer'] = new LogViewer();
$twig->twig_vars['form_max_filesize'] = Utils::getUploadLimit() / 1024 / 1024;
// Start white label functionality
$twig->twig_vars['whitelabel_presets'] = $this->getPresets();
$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);
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;
}
$custom_css = $this->config->get('plugins.admin.whitelabel.custom_css', false);
if ($custom_css) {
$this->grav['assets']->addInlineCss($custom_css);
}
// End white label functionality
$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) {
@@ -625,16 +730,22 @@ class AdminPlugin extends Plugin
}
}
// Gather Plugin-hooked nav items
// Gather all nav items
$this->grav['twig']->plugins_hooked_nav = [];
$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;
});
// Gather Plugin-hooked dashboard items
$this->grav->fireEvent('onAdminDashboard');
switch ($this->template) {
case 'dashboard':
$twig->twig_vars['popularity'] = $this->popularity;
// Gather Plugin-hooked dashboard items
$this->grav->fireEvent('onAdminDashboard');
break;
}
@@ -659,8 +770,6 @@ class AdminPlugin extends Plugin
*/
public function onTwigExtensions()
{
require_once __DIR__ . '/classes/Twig/AdminTwigExtension.php';
$this->grav['twig']->twig->addExtension(new AdminTwigExtension);
}
@@ -688,10 +797,13 @@ class AdminPlugin extends Plugin
{
$type = $e['type'] ?? null;
switch ($type) {
case 'config':
$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':
case 'tools/backups':
$e['type'] = 'config/backups';
break;
@@ -719,30 +831,74 @@ class AdminPlugin extends Plugin
/**
* Initial stab at registering permissions (WIP)
*
* @param Event $e
* @param PermissionsRegisterEvent $event
*/
public function onAdminRegisterPermissions(Event $e)
public function onRegisterPermissions(PermissionsRegisterEvent $event): void
{
$admin = $e['admin'];
$permissions = [
'admin.super' => 'boolean',
'admin.login' => 'boolean',
'admin.cache' => 'boolean',
'admin.configuration' => 'boolean',
'admin.configuration_system' => 'boolean',
'admin.configuration_site' => 'boolean',
'admin.configuration_media' => 'boolean',
'admin.configuration_info' => 'boolean',
'admin.settings' => 'boolean',
'admin.pages' => 'boolean',
'admin.maintenance' => 'boolean',
'admin.statistics' => 'boolean',
'admin.plugins' => 'boolean',
'admin.themes' => 'boolean',
'admin.tools' => 'boolean',
'admin.users' => 'boolean',
$actions = PermissionsReader::fromYaml("plugin://{$this->name}/permissions.yaml");
$permissions = $event->permissions;
$permissions->addActions($actions);
}
public function onAdminMenu()
{
// Dashboard
$this->grav['twig']->plugins_hooked_nav['PLUGIN_ADMIN.DASHBOARD'] = [
'route' => 'dashboard',
'icon' => 'fa-th',
'authorize' => ['admin.login', 'admin.super'],
'priority' => 10
];
// Configuration
$this->grav['twig']->plugins_hooked_nav['PLUGIN_ADMIN.CONFIGURATION'] = [
'route' => 'config',
'icon' => 'fa-wrench',
'authorize' => [
'admin.configuration.system',
'admin.configuration.site',
'admin.super'],
'priority' => 9
];
// Pages
$count = new Container(['count' => function () { return $this->admin->pagesCount(); }]);
$this->grav['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()); }]);
$this->grav['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()); }]);
$this->grav['twig']->plugins_hooked_nav['PLUGIN_ADMIN.THEMES'] = [
'route' => 'themes',
'icon' => 'fa-tint',
'authorize' => ['admin.themes', 'admin.super'],
'badge' => $count,
'priority' => -9
];
// Tools
$this->grav['twig']->plugins_hooked_nav['PLUGIN_ADMIN.TOOLS'] = [
'route' => 'tools',
'icon' => 'fa-briefcase',
'authorize' => $this->admin::toolsPermissions(),
'priority' => -10
];
$admin->addPermissions($permissions);
}
/**
@@ -758,53 +914,71 @@ class AdminPlugin extends Plugin
}
/**
* Helper function to replace Pages::Types()
* and to provide an event to manipulate the data
* 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
* Dispatches 'onAdminPageTypes' event with 'types' data member which is a reference to the data
*
* @param bool $keysOnly
* @return array
*/
public static function pagesTypes()
public static function pagesTypes(bool $keysOnly = false)
{
$types = Pages::types();
// First filter by configuration
$hideTypes = Grav::instance()['config']->get('plugins.admin.hide_page_types', []);
foreach ((array) $hideTypes as $type) {
unset($types[$type]);
foreach ((array) $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 $types;
return $keysOnly ? array_keys($types) : $types;
}
/**
* Helper function to replace Pages::modularTypes()
* and to provide an event to manipulate the data
* 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
* Dispatches 'onAdminModularPageTypes' event with 'types' data member which is a reference to the data
*
* @param bool $keysOnly
* @return array
*/
public static function pagesModularTypes()
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 $type) {
unset($types[$type]);
foreach ((array) $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 $types;
return $keysOnly ? array_keys($types) : $types;
}
/**
@@ -830,6 +1004,8 @@ class AdminPlugin extends Plugin
protected function initializeController($task, $post)
{
Admin::DEBUG && Admin::addDebugMessage('Admin controller: execute');
$controller = new AdminController();
$controller->initialize($this->grav, $this->template, $task, $this->route, $post);
$controller->execute();
@@ -850,16 +1026,12 @@ class AdminPlugin extends Plugin
'onTwigTemplatePaths' => ['onTwigTemplatePaths', 1000],
'onTwigSiteVariables' => ['onTwigSiteVariables', 1000],
'onAssetsInitialized' => ['onAssetsInitialized', 1000],
'onAdminRegisterPermissions' => ['onAdminRegisterPermissions', 0],
'onOutputGenerated' => ['onOutputGenerated', 0],
'onAdminAfterSave' => ['onAdminAfterSave', 0],
'onAdminData' => ['onAdminData', 0],
'onAdminMenu' => ['onAdminMenu', 1000],
]);
// Autoload classes
require_once __DIR__ . '/vendor/autoload.php';
// Check for required plugins
if (!$this->grav['config']->get('plugins.login.enabled') || !$this->grav['config']->get('plugins.form.enabled') || !$this->grav['config']->get('plugins.email.enabled')) {
throw new \RuntimeException('One of the required plugins is missing or not enabled');
@@ -955,8 +1127,8 @@ class AdminPlugin extends Plugin
'EDITOR',
'PREVIEW',
'FULLSCREEN',
'MODULAR',
'NON_MODULAR',
'MODULE',
'NON_MODULE',
'VISIBLE',
'NON_VISIBLE',
'ROUTABLE',
@@ -1034,4 +1206,49 @@ class AdminPlugin extends Plugin
$assets->addInlineJs($translations);
}
public static function themeOptions()
{
static $options = [];
if (empty($options)) {
$theme_files = glob(__dir__ . '/themes/grav/css/codemirror/themes/*.css');
foreach ($theme_files as $theme_file) {
$theme = basename(basename($theme_file, '.css'));
$options[$theme] = Inflector::titleize($theme);
}
}
return $options;
}
public function getPresets()
{
$filename = $this->grav['locator']->findResource('plugin://admin/presets.yaml', false);
$file = CompiledYamlFile::instance($filename);
$presets = (array)$file->content();
$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']) && isset($custom_presets['colors']) && isset($custom_presets['accents'])) {
$preset = [Inflector::hyphenize($custom_presets['name']) => $custom_presets];
$presets = $preset + $presets;
} else {
foreach ($custom_presets as $value) {
if (isset($value['name']) && isset($value['colors']) && isset($value['accents'])) {
$preset = [Inflector::hyphenize($value['name']) => $value];
$presets = $preset + $presets;
}
}
}
}
}
return $presets;
}
}

View File

@@ -1,19 +1,18 @@
enabled: true
route: '/admin'
cache_enabled: false
cache_enabled: true
theme: grav
logo_text: ''
body_classes: ''
content_padding: true
twofa_enabled: true
log_viewer_files: ['grav', 'email']
sidebar:
activate: tab
hover_delay: 100
size: auto
dashboard:
days_of_stats: 7
widgets:
widgets_display:
dashboard-maintenance: true
dashboard-statistics: true
dashboard-notifications: true
@@ -24,14 +23,10 @@ pages:
show_modular: true
session:
timeout: 1800
warnings:
delete_page: true
secure_delete: false
edit_mode: normal
frontend_preview_target: inline
show_github_msg: true
pages_list_display_field: title
google_fonts: false
admin_icons: line-awesome
enable_auto_updates_check: true
notifications:
@@ -46,3 +41,45 @@ popularity:
daily: 30
monthly: 12
visitors: 20
whitelabel:
quicktray_recompile: false
codemirror_theme: paper
codemirror_fontsize: md
codemirror_md_font: sans
logo_custom:
logo_login:
color_scheme:
accents:
primary-accent: button
secondary-accent: notice
tertiary-accent: critical
colors:
logo-bg: '#323640'
logo-link: '#FFFFFF'
nav-bg: '#3D424E'
nav-text: '#B7B9BD'
nav-link: '#ffffff'
nav-selected-bg: '#323640'
nav-selected-link: '#ffffff'
nav-hover-bg: '#434753'
nav-hover-link: '#ffffff'
toolbar-bg: '#ffffff'
toolbar-text: '#3D424E'
page-bg: '#F6F6F6'
page-text: '#6f7b8a'
page-link: '#0090D9'
content-bg: '#ffffff'
content-text: '#6f7b8a'
content-link: '#0090D9'
content-link2: '#da4b46'
content-header: '#414147'
content-tabs-bg: '#e6e6e6'
content-tabs-text: '#808080'
button-bg: '#0090D9'
button-text: '#ffffff'
notice-bg: '#06A599'
notice-text: '#ffffff'
update-bg: '#77559D'
update-text: '#ffffff'
critical-bg: '#F45857'
critical-text: '#ffffff'

File diff suppressed because it is too large Load Diff

View File

@@ -10,7 +10,7 @@ form:
section:
type: section
title: PLUGIN_ADMIN.ADD_MODULAR_CONTENT
title: PLUGIN_ADMIN.ADD_MODULE_CONTENT
title:
type: text
@@ -35,7 +35,7 @@ form:
name:
type: select
classes: fancy
label: PLUGIN_ADMIN.MODULAR_TEMPLATE
label: PLUGIN_ADMIN.MODULE_TEMPLATE
help: PLUGIN_ADMIN.PAGE_FILE_HELP
default: default
data-options@: '\Grav\Plugin\AdminPlugin::pagesModularTypes'

View File

@@ -18,6 +18,9 @@ form:
title: PLUGIN_ADMIN.CONTENT
fields:
xss_check:
type: xss
frontmatter:
classes: frontmatter
type: editor
@@ -39,7 +42,7 @@ form:
header.media_order:
type: pagemedia
label: PLUGIN_ADMIN.PAGE_MEDIA
options:
type: tab
title: PLUGIN_ADMIN.OPTIONS
@@ -83,7 +86,7 @@ form:
name:
type: select
classes: fancy
label: PLUGIN_ADMIN.MODULAR_TEMPLATE
label: PLUGIN_ADMIN.MODULE_TEMPLATE
default: default
data-options@: '\Grav\Plugin\AdminPlugin::pagesModularTypes'
validate:

View File

@@ -2,6 +2,4 @@ form:
validation: loose
fields:
route:
type: parents
label: PLUGIN_ADMIN.PARENT
classes: fancy
type: hidden

View File

@@ -20,7 +20,7 @@ form:
fields:
xss_check:
type: xss
frontmatter:
classes: frontmatter
type: editor
@@ -37,7 +37,7 @@ form:
lint: true
content:
type: markdown
type: codemirror
header.media_order:
type: pagemedia
@@ -81,8 +81,6 @@ form:
type: parents
label: PLUGIN_ADMIN.PARENT
classes: fancy
validate:
required: true
name:
type: select

View File

@@ -0,0 +1,34 @@
rules:
slug:
pattern: '[a-zA-Zа-яA-Я0-9_\-]+'
min: 1
max: 200
form:
validation: loose
fields:
tabs:
type: tabs
active: 1
fields:
content:
type: tab
title: PLUGIN_ADMIN.CONTENT
fields:
frontmatter:
classes: frontmatter
type: editor
label: PLUGIN_ADMIN.FRONTMATTER
autofocus: true
codemirror:
mode: 'yaml'
indentUnit: 4
autofocus: true
indentWithTabs: false
lineNumbers: true
styleActiveLine: true
gutters: ['CodeMirror-lint-markers']
lint: true

View File

@@ -1,31 +0,0 @@
<?php
namespace Grav\Plugin\Admin;
use Grav\Common\Processors\ProcessorBase;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
class Router extends ProcessorBase
{
public $id = 'admin_router';
public $title = 'Admin Panel';
/**
* Admin router.
*
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
{
$this->startTimer();
$response = $handler->handle($request);
$this->stopTimer();
// Never allow admin pages to be rendered in <frame>, <iframe>, <embed> or <object> for improved security.
return $response->withHeader('X-Frame-Options', 'NONE');
}
}

View File

@@ -4,12 +4,16 @@ namespace Grav\Plugin\Admin;
use DateTime;
use Grav\Common\Data;
use Grav\Common\Data\Data as GravData;
use Grav\Common\Debugger;
use Grav\Common\File\CompiledYamlFile;
use Grav\Common\Flex\Types\Users\UserObject;
use Grav\Common\GPM\GPM;
use Grav\Common\GPM\Licenses;
use Grav\Common\GPM\Response;
use Grav\Common\Grav;
use Grav\Common\Helpers\YamlLinter;
use Grav\Common\Language\Language;
use Grav\Common\Language\LanguageCodes;
use Grav\Common\Page\Collection;
use Grav\Common\Page\Interfaces\PageInterface;
@@ -21,9 +25,18 @@ use Grav\Common\Session;
use Grav\Common\Themes;
use Grav\Common\Uri;
use Grav\Common\User\Interfaces\UserCollectionInterface;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\User\User;
use Grav\Common\Utils;
use Grav\Framework\Acl\Action;
use Grav\Framework\Acl\Permissions;
use Grav\Framework\Collection\ArrayCollection;
use Grav\Framework\Flex\Flex;
use Grav\Framework\Flex\Interfaces\FlexInterface;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Route\Route;
use Grav\Framework\Route\RouteFactory;
use Grav\Plugin\AdminPlugin;
use Grav\Plugin\Login\Login;
use Grav\Plugin\Login\TwoFactorAuth\TwoFactorAuth;
use PicoFeed\Parser\MalformedXmlException;
@@ -41,8 +54,9 @@ define('LOGIN_REDIRECT_COOKIE', 'grav-login-redirect');
class Admin
{
const MEDIA_PAGINATION_INTERVAL = 20;
const TMP_COOKIE_NAME = 'tmp-admin-message';
public const DEBUG = 1;
public const MEDIA_PAGINATION_INTERVAL = 20;
public const TMP_COOKIE_NAME = 'tmp-admin-message';
/** @var Grav */
public $grav;
@@ -74,8 +88,11 @@ class Admin
/** @var bool */
public $multilang;
/** @var string */
public $language;
/** @var array */
public $languages_enabled;
public $languages_enabled = [];
/** @var Uri $uri */
protected $uri;
@@ -95,9 +112,6 @@ class Admin
/** @var int */
protected $pages_count;
/** @var array */
protected $permissions;
/** @var bool */
protected $load_additional_files_in_background = false;
@@ -126,29 +140,74 @@ class Admin
$this->route = $route;
$this->uri = $grav['uri'];
$this->session = $grav['session'];
$this->user = $grav['user'];
$this->permissions = [];
$language = $grav['language'];
/** @var FlexInterface|null $flex */
$flex = $grav['flex_objects'] ?? null;
/** @var UserInterface $user */
$user = $grav['user'];
// Convert old user to Flex User if Flex Objects plugin has been enabled.
if ($flex && !$user instanceof FlexObjectInterface) {
$managed = !method_exists($flex, 'isManaged') || $flex->isManaged('user-accounts');
$directory = $managed ? $flex->getDirectory('user-accounts') : null;
/** @var UserObject|null $test */
$test = $directory ? $directory->getObject($user->username) : null;
if ($test) {
$test = clone $test;
$test->access = $user->access;
$test->groups = $user->groups;
$test->authenticated = $user->authenticated;
$test->authorized = $user->authorized;
$user = $test;
}
}
$this->user = $user;
/** @var Language $language */
$language = $grav['language'];
$this->multilang = $language->enabled();
// Load utility class
if ($language->enabled()) {
$this->multilang = true;
if ($this->multilang) {
$this->language = $language->getActive() ?? '';
$this->languages_enabled = (array)$this->grav['config']->get('system.languages.supported', []);
//Set the currently active language for the admin
$language = $this->grav['uri']->param('lang');
if (!$language) {
if (!$this->session->admin_lang) {
$this->session->admin_lang = $this->grav['language']->getLanguage();
}
$language = $this->session->admin_lang;
$languageCode = $this->grav['uri']->param('lang');
if (null === $languageCode && !$this->session->admin_lang) {
$this->session->admin_lang = $language->getActive() ?? '';
}
$this->grav['language']->setActive($language ?: 'en');
} else {
$this->grav['language']->setActive('en');
$this->multilang = false;
$this->language = '';
}
// Set admin route language.
RouteFactory::setLanguage($this->language);
}
/**
* @param string $message
* @param array $data
*/
public static function addDebugMessage(string $message, $data = [])
{
/** @var Debugger $debugger */
$debugger = Grav::instance()['debugger'];
$debugger->addMessage($message, 'debug', $data);
}
public static function contentEditor()
{
$options = [
'default' => 'Default',
'codemirror' => 'CodeMirror'
];
$event = new Event(['options' => &$options]);
Grav::instance()->fireEvent('onAdminListContentEditors', $event);
return $options;
}
/**
@@ -162,7 +221,6 @@ class Admin
$path = Grav::instance()['locator']->findResource('plugins://admin/languages');
/** @var \DirectoryIterator $directory */
foreach (new \DirectoryIterator($path) as $file) {
if ($file->isDir() || $file->isDot() || Utils::startsWith($file->getFilename(), '.')) {
continue;
@@ -180,6 +238,11 @@ class Admin
return $languages;
}
public function getLanguage(): string
{
return $this->language ?: $this->grav['language']->getLanguage() ?: 'en';
}
/**
* Return the found configuration blueprints
*
@@ -273,21 +336,74 @@ class Admin
*/
public static function getLastPageRoute()
{
return Grav::instance()['session']->lastPageRoute ?: self::route();
/** @var Session $session */
$session = Grav::instance()['session'];
$route = $session->lastPageRoute;
if ($route) {
return $route;
}
/** @var Admin $admin */
$admin = Grav::instance()['admin'];
return $admin->getCurrentRoute();
}
public function getAdminRoute(string $path = '', $languageCode = null): Route
{
/** @var Language $language */
$language = $this->grav['language'];
$languageCode = $languageCode ?? $language->getActive();
$languagePrefix = $languageCode ? '/' . $languageCode : '';
$root = $this->grav['uri']->rootUrl();
$subRoute = rtrim($this->grav['pages']->base(), '/');
$adminRoute = rtrim($this->grav['config']->get('plugins.admin.route'), '/');
$parts = [
'path' => $path,
'query' => '',
'query_params' => [],
'grav' => [
// TODO: Make URL to be /admin/en, not /en/admin.
'root' => preg_replace('`//+`', '/', $root . $subRoute . $languagePrefix . $adminRoute),
'language' => '', //$languageCode,
'route' => ltrim($path, '/'),
'params' => ''
],
];
return RouteFactory::createFromParts($parts);
}
public function adminUrl(string $route = '', $languageCode = null)
{
return $this->getAdminRoute($route, $languageCode)->toString(true);
}
/**
* Static helper method to return current route.
*
* @return string
* @deprecated 1.10 Use $admin->getCurrentRoute() instead
*/
public static function route()
{
$pages = Grav::instance()['pages'];
$route = '/' . ltrim(Grav::instance()['admin']->route, '/');
user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Admin 1.9.7, use $admin->getCurrentRoute() instead', E_USER_DEPRECATED);
$admin = Grav::instance()['admin'];
return $admin->getCurrentRoute();
}
public function getCurrentRoute()
{
$pages = static::enablePages();
$route = '/' . ltrim($this->route, '/');
/** @var PageInterface $page */
$page = $pages->dispatch($route);
$page = $pages->find($route);
$parent_route = null;
if ($page) {
/** @var PageInterface $parent */
@@ -298,6 +414,73 @@ class Admin
return $parent_route;
}
/**
* Redirect to the route stored in $this->redirect
*
* Route may or may not be prefixed by /en or /admin or /en/admin.
*
* @param string $redirect
* @param int$redirectCode
*/
public function redirect($redirect, $redirectCode = 303)
{
// No redirect, do nothing.
if (!$redirect) {
return;
}
Admin::DEBUG && Admin::addDebugMessage("Admin redirect: {$redirectCode} {$redirect}");
$redirect = '/' . ltrim(preg_replace('`//+`', '/', $redirect), '/');
$base = $this->base;
$root = Grav::instance()['uri']->rootUrl();
if ($root === '/') {
$root = '';
}
$pattern = '`^((' . preg_quote($root, '`') . ')?(/[^/]+)?)' . preg_quote($base, '`') . '`ui';
// Check if we already have an admin path: /admin, /en/admin, /root/admin or /root/en/admin.
if (preg_match($pattern, $redirect)) {
$redirect = preg_replace('|^' . preg_quote($root, '|') . '|', '', $redirect);
$this->grav->redirect($redirect, $redirectCode);
}
if ($this->isMultilang()) {
// Check if URL does not have language prefix.
if (!Utils::pathPrefixedByLangCode($redirect)) {
/** @var Language $language */
$language = $this->grav['language'];
// Prefix path with language prefix: /en
// TODO: Use /admin/en instead of /en/admin in the future.
$redirect = $language->getLanguageURLPrefix($this->grav['session']->admin_lang) . $base . $redirect;
} else {
// TODO: Use /admin/en instead of /en/admin in the future.
//$redirect = preg_replace('`^(/[^/]+)/admin`', '\\1', $redirect);
// Check if we already have language prefixed admin path: /en/admin
$this->grav->redirect($redirect, $redirectCode);
}
} else {
// TODO: Use /admin/en instead of /en/admin in the future.
// Prefix path with /admin
$redirect = $base . $redirect;
}
$this->grav->redirect($redirect, $redirectCode);
}
/**
* Return true if multilang is active
*
* @return bool True if multilang is active
*/
protected function isMultilang()
{
return count($this->grav['config']->get('system.languages.supported', [])) > 1;
}
public static function getTempDir()
{
try {
@@ -314,11 +497,12 @@ class Admin
$files = [];
$grav = Grav::instance();
$pages = $grav['pages'];
$pages = static::enablePages();
$route = '/' . ltrim($grav['admin']->route, '/');
/** @var PageInterface $page */
$page = $pages->dispatch($route);
$page = $pages->find($route);
$parent_route = null;
if ($page) {
$media = $page->media()->all();
@@ -383,11 +567,15 @@ class Admin
// Check rate limit for both IP and user, but allow each IP a single try even if user is already rate limited.
if ($rateLimiter->isRateLimited($ipKey, 'ip') || ($attempts && $rateLimiter->isRateLimited($userKey))) {
Admin::DEBUG && Admin::addDebugMessage('Admin login: rate limit, redirecting', $credentials);
$this->setMessage(static::translate(['PLUGIN_LOGIN.TOO_MANY_LOGIN_ATTEMPTS', $rateLimiter->getInterval()]), 'error');
$this->grav->redirect('/');
}
Admin::DEBUG && Admin::addDebugMessage('Admin login', $credentials);
// Fire Login process.
$event = $login->login(
$credentials,
@@ -396,6 +584,8 @@ class Admin
);
$user = $event->getUser();
Admin::DEBUG && Admin::addDebugMessage('Admin login: user', $user);
if ($user->authenticated) {
$rateLimiter->resetRateLimit($ipKey, 'ip')->resetRateLimit($userKey);
if ($user->authorized) {
@@ -420,7 +610,9 @@ class Admin
$this->setMessage(static::translate($message), $event->getMessageType());
}
$redirect = $event->getRedirect();
/** @var Pages $pages */
$pages = $this->grav['pages'];
$redirect = $pages->baseRoute() . $event->getRedirect();
$this->grav->redirect($redirect, $event->getRedirectCode());
}
@@ -430,6 +622,10 @@ class Admin
*/
public function twoFa($data, $post)
{
/** @var Pages $pages */
$pages = $this->grav['pages'];
$baseRoute = $pages->baseRoute();
/** @var Login $login */
$login = $this->grav['login'];
@@ -446,14 +642,16 @@ class Admin
$this->grav['session']->setFlashCookieObject(Admin::TMP_COOKIE_NAME, ['message' => $this->translate('PLUGIN_ADMIN.2FA_FAILED'), 'status' => 'error']);
$this->grav->redirect($this->uri->route(), 303);
$this->grav->redirect($baseRoute . $this->uri->route(), 303);
}
$this->setMessage($this->translate('PLUGIN_ADMIN.LOGIN_LOGGED_IN'), 'info');
$user->authorized = true;
$this->grav->redirect($post['redirect']);
$redirect = $baseRoute . $post['redirect'];
$this->grav->redirect($redirect);
}
/**
@@ -541,7 +739,7 @@ class Admin
} else {
$languages = (array)$grav['language']->getDefault();
}
$languages = $grav['user']->authenticated ? [ $grav['user']->language ] : $languages;
$languages = $grav['user']->authenticated ? [$grav['user']->language] : $languages;
} else {
$languages = (array)$languages;
}
@@ -582,8 +780,14 @@ class Admin
{
$action = (array)$action;
$user = $this->user;
foreach ($action as $a) {
if ($this->user->authorize($a)) {
// Ignore 'admin.super' if it's not the only value to be checked.
if ($a === 'admin.super' && count($action) > 1 && $user instanceof FlexObjectInterface) {
continue;
}
if ($user->authorize($a)) {
return true;
}
}
@@ -682,11 +886,13 @@ class Admin
$file = File::instance($filename);
$pages = static::enablePages();
$obj = new \stdClass();
$obj->title = $file->basename();
$obj->path = $file->filename();
$obj->file = $file;
$obj->page = $this->grav['pages']->get(dirname($obj->path));
$obj->page = $pages->get(dirname($obj->path));
$fileInfo = pathinfo($obj->title);
$filename = str_replace(['@3x', '@2x'], '', $fileInfo['filename']);
@@ -775,8 +981,7 @@ class Admin
*/
public function routes($unique = false)
{
/** @var Pages $pages */
$pages = $this->grav['pages'];
$pages = static::enablePages();
if ($unique) {
$routes = array_unique($pages->routes());
@@ -795,7 +1000,8 @@ class Admin
public function pagesCount()
{
if (!$this->pages_count) {
$this->pages_count = count($this->grav['pages']->all());
$pages = static::enablePages();
$this->pages_count = count($pages->all());
}
return $this->pages_count;
@@ -804,21 +1010,35 @@ class Admin
/**
* Get all template types
*
* @param array|null $ignore
* @return array
*/
public function types()
public function types(?array $ignore = [])
{
return Pages::types();
if (null === $ignore) {
return AdminPlugin::pagesTypes();
}
$types = Pages::types();
return $ignore ? array_diff_key($types, array_flip($ignore)) : $types;
}
/**
* Get all modular template types
*
* @param array|null $ignore
* @return array
*/
public function modularTypes()
public function modularTypes(?array $ignore = [])
{
return Pages::modularTypes();
if (null === $ignore) {
return AdminPlugin::pagesModularTypes();
}
$types = Pages::modularTypes();
return $ignore ? array_diff_key($types, array_flip($ignore)) : $types;
}
/**
@@ -828,8 +1048,10 @@ class Admin
*/
public function accessLevels()
{
if (method_exists($this->grav['pages'], 'accessLevels')) {
return $this->grav['pages']->accessLevels();
$pages = static::enablePages();
if (method_exists($pages, 'accessLevels')) {
return $pages->accessLevels();
}
return [];
@@ -1032,8 +1254,14 @@ class Admin
*/
public function latestPages($count = 10)
{
/** @var Pages $pages */
$pages = $this->grav['pages'];
/** @var Flex $flex */
$flex = $this->grav['flex_objects'] ?? null;
$directory = $flex ? $flex->getDirectory('pages') : null;
if ($directory) {
return $directory->getIndex()->sort(['timestamp' => 'DESC'])->slice(0, $count);
}
$pages = static::enablePages();
$latest = [];
@@ -1042,7 +1270,7 @@ class Admin
}
foreach ($pages->routes() as $url => $path) {
$page = $pages->dispatch($url, true);
$page = $pages->find($url, true);
if ($page && $page->routable()) {
$latest[$page->route()] = ['modified' => $page->modified(), 'page' => $page];
}
@@ -1145,8 +1373,7 @@ class Admin
if (function_exists('phpinfo')) {
ob_start();
phpinfo();
$pinfo = ob_get_contents();
ob_end_clean();
$pinfo = ob_get_clean();
$pinfo = preg_replace('%^.*<body>(.*)</body>.*$%ms', '$1', $pinfo);
@@ -1184,22 +1411,21 @@ class Admin
];
if (!isset($guess[$date])) {
$guess[$date] = 'd-m-Y H:i';
foreach ($date_formats as $date_format) {
foreach ($time_formats as $time_format) {
if ($this->validateDate($date, "$date_format $time_format")) {
$guess[$date] = "$date_format $time_format";
$full_format = "{$date_format} {$time_format}";
if ($this->validateDate($date, $full_format)) {
$guess[$date] = $full_format;
break 2;
}
if ($this->validateDate($date, "$time_format $date_format")) {
$guess[$date] = "$time_format $date_format";
$full_format = "{$time_format} {$date_format}";
if ($this->validateDate($date, $full_format)) {
$guess[$date] = $full_format;
break 2;
}
}
}
if (!isset($guess[$date])) {
$guess[$date] = 'd-m-Y H:i';
}
}
return $guess[$date];
@@ -1299,40 +1525,65 @@ class Admin
* Gets the entire permissions array
*
* @return array
* @deprecated 1.10 Use $grav['permissions']->getInstances() instead.
*/
public function getPermissions()
{
return $this->permissions;
user_error(__METHOD__ . '() is deprecated since Admin 1.10, use $grav[\'permissions\']->getInstances() instead', E_USER_DEPRECATED);
$grav = $this->grav;
/** @var Permissions $permissions */
$permissions = $grav['permissions'];
return array_fill_keys(array_keys($permissions->getInstances()), 'boolean');
}
/**
* Sets the entire permissions array
*
* @param array $permissions
* @deprecated 1.10 Use PermissionsRegisterEvent::class event instead.
*/
public function setPermissions($permissions)
{
$this->permissions = $permissions;
user_error(__METHOD__ . '() is deprecated since Admin 1.10, use PermissionsRegisterEvent::class event instead', E_USER_DEPRECATED);
$this->addPermissions($permissions);
}
/**
* Adds a permission to the permissions array
*
* @param array $permissions
* @deprecated 1.10 Use RegisterPermissionsEvent::class event instead.
*/
public function addPermissions($permissions)
{
$this->permissions = array_merge($this->permissions, $permissions);
user_error(__METHOD__ . '() is deprecated since Admin 1.10, use RegisterPermissionsEvent::class event instead', E_USER_DEPRECATED);
$grav = $this->grav;
/** @var Permissions $object */
$object = $grav['permissions'];
foreach ($permissions as $name => $type) {
if (!$object->hasAction($name)) {
$action = new Action($name);
$object->addAction($action);
}
}
}
public function getNotifications($force = false)
{
$last_checked = null;
$filename = $this->grav['locator']->findResource('user://data/notifications/' . md5($this->grav['user']->username) . YAML_EXT, true, true);
$userStatus = $this->grav['locator']->findResource('user://data/notifications/' . $this->grav['user']->username . YAML_EXT, true, true);
$notifications_file = CompiledYamlFile::instance($filename);
$notifications_content = (array)$notifications_file->content();
$userStatus_file = CompiledYamlFile::instance($userStatus);
$userStatus_content = (array)$userStatus_file->content();
$last_checked = $notifications_content['last_checked'] ?? null;
$notifications = $notifications_content['data'] ?? array();
$timeout = $this->grav['config']->get('system.session.timeout', 1800);
@@ -1361,16 +1612,16 @@ class Admin
foreach ($notification['dependencies'] as $dependency => $constraints) {
if ($dependency === 'grav') {
if (!Semver::satisfies(GRAV_VERSION, $constraints)) {
continue;
continue 2;
}
} else {
$packages = array_merge($this->plugins()->toArray(), $this->themes()->toArray());
if (!isset($packages[$dependency])) {
continue;
continue 2;
} else {
$version = $packages[$dependency]['version'];
if (!Semver::satisfies($version, $constraints)) {
continue;
continue 2;
}
}
}
@@ -1395,6 +1646,27 @@ class Admin
$notifications_file->save();
}
foreach ($notifications as $location => $list) {
$notifications[$location] = array_filter($list, function ($notification) use ($userStatus_content) {
$element = $userStatus_content[$notification['id']] ?? null;
if (isset($element)) {
if (isset($notification['reappear_after'])) {
$now = new \DateTime();
$hidden_on = new \DateTime($element);
$hidden_on->modify($notification['reappear_after']);
if ($now >= $hidden_on) {
return true;
}
}
return false;
}
return true;
});
}
return $notifications;
}
@@ -1581,8 +1853,7 @@ class Admin
*/
public function getPage($path)
{
/** @var Pages $pages */
$pages = $this->grav['pages'];
$pages = static::enablePages();
if ($path && $path[0] !== '/') {
$path = "/{$path}";
@@ -1591,7 +1862,7 @@ class Admin
// Fix for entities in path causing looping...
$path = urldecode($path);
$page = $path ? $pages->dispatch($path, true) : $pages->root();
$page = $path ? $pages->find($path, true) : $pages->root();
if (!$page) {
$slug = basename($path);
@@ -1647,7 +1918,7 @@ class Admin
$header['body_classes'] = 'modular';
}
$name = $page->modular() ? str_replace('modular/', '', $data['name']) : $data['name'];
$name = $page->isModule() ? str_replace('modular/', '', $data['name']) : $data['name'];
$page->name($name . '.md');
// Fire new event to allow plugins to manipulate page frontmatter
@@ -1671,8 +1942,7 @@ class Admin
{
$reports = new ArrayCollection();
/** @var Pages $pages */
$pages = $this->grav['pages'];
$pages = static::enablePages();
// Default to XSS Security Report
$result = Security::detectXssFromPages($pages, true);
@@ -1742,10 +2012,10 @@ class Admin
if (!$page_files) {
$page_files = [];
$pages = $this->grav['pages'];
$pages = static::enablePages();
if ($param_page) {
$page = $pages->dispatch($param_page);
$page = $pages->find($param_page);
$page_files = $this->getFiles('images', $page, $page_files, $filtered);
$page_files = $this->getFiles('videos', $page, $page_files, $filtered);
@@ -1946,11 +2216,12 @@ class Admin
*/
public function pages()
{
/** @var Collection $pages */
$pages = $this->grav['pages']->all();
$pages = static::enablePages();
$collection = $pages->all();
$pagesWithFiles = [];
foreach ($pages as $page) {
foreach ($collection as $page) {
if (count($page->media()->all())) {
$pagesWithFiles[] = $page;
}
@@ -1959,6 +2230,43 @@ class Admin
return $pagesWithFiles;
}
/**
* @return Pages
*/
public static function enablePages()
{
static $pages;
if ($pages) {
return $pages;
}
$grav = Grav::instance();
$admin = $grav['admin'];
/** @var Pages $pages */
$pages = Grav::instance()['pages'];
$pages->enablePages();
// If page is null, the default page does not exist, and we cannot route to it
$page = $pages->find('/', true);
if ($page) {
// Set original route for the home page.
$home = '/' . trim($grav['config']->get('system.home.alias'), '/');
$page->route($home);
}
$admin->routes = $pages->routes();
// Remove default route from routes.
if (isset($admin->routes['/'])) {
unset($admin->routes['/']);
}
return $pages;
}
/**
* Return HTTP_REFERRER if set
*
@@ -1969,8 +2277,41 @@ class Admin
return $_SERVER['HTTP_REFERER'] ?? null;
}
public static function enablePages()
/**
* Get Grav system log files
*
* @return array
*/
public function getLogFiles()
{
return Grav::instance()['pages'];
$logs = new GravData(['grav.log' => 'Grav System Log', 'email.log' => 'Email Log']);
Grav::instance()->fireEvent('onAdminLogFiles', new Event(['logs' => &$logs]));
return $logs->toArray();
}
/**
* Get changelog for a given GPM package based on slug
*
* @param null $slug
* @return array
*/
public function getChangelog($slug = null)
{
$gpm = $this->gpm();
$changelog = [];
if (!empty($slug)) {
$package = $gpm->findPackage($slug);
} else {
$package = $gpm->grav;
}
if ($package) {
$changelog = $package->getChangelog();
}
return $changelog;
}
}

View File

@@ -2,20 +2,28 @@
namespace Grav\Plugin\Admin;
use Grav\Common\Cache;
use Grav\Common\Config\Config;
use Grav\Common\Data\Data;
use Grav\Common\Debugger;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Common\Media\Interfaces\MediaInterface;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Page\Media;
use Grav\Common\Security;
use Grav\Common\Uri;
use Grav\Common\User\Interfaces\UserInterface;
use Grav\Common\Utils;
use Grav\Common\Plugin;
use Grav\Common\Theme;
use Grav\Framework\Controller\Traits\ControllerResponseTrait;
use Grav\Framework\RequestHandler\Exception\RequestException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\File\File;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
/**
* Class AdminController
@@ -24,6 +32,8 @@ use RocketTheme\Toolbox\File\File;
*/
class AdminBaseController
{
use ControllerResponseTrait;
/**
* @var Grav
*/
@@ -107,16 +117,30 @@ class AdminBaseController
if (method_exists($this, $method)) {
try {
$success = $this->{$method}();
$response = $this->{$method}();
} catch (RequestException $e) {
/** @var Debugger $debugger */
$debugger = $this->grav['debugger'];
$debugger->addException($e);
$response = $this->createErrorResponse($e);
} catch (\RuntimeException $e) {
$success = true;
/** @var Debugger $debugger */
$debugger = $this->grav['debugger'];
$debugger->addException($e);
$response = true;
$this->admin->setMessage($e->getMessage(), 'error');
}
} else {
$success = $this->grav->fireEvent('onAdminTaskExecute',
$response = $this->grav->fireEvent('onAdminTaskExecute',
new Event(['controller' => $this, 'method' => $method]));
}
if ($response instanceof ResponseInterface) {
$this->close($response);
}
// Grab redirect parameter.
$redirect = $this->post['_redirect'] ?? null;
unset($this->post['_redirect']);
@@ -126,7 +150,7 @@ class AdminBaseController
$this->setRedirect($redirect);
}
return $success;
return $response;
}
protected function validateNonce()
@@ -208,24 +232,23 @@ class AdminBaseController
/**
* Sends JSON response and terminates the call.
*
* @param array $response
* @param array $json
* @param int $code
* @return bool
*/
protected function sendJsonResponse(array $response, $code = 200)
protected function sendJsonResponse(array $json, $code = 200): void
{
// Make sure nothing extra gets written to the response.
while (ob_get_level()) {
ob_end_clean();
}
// JSON response.
http_response_code($code);
header('Content-Type: application/json');
header('Cache-Control: no-cache, no-store, must-revalidate');
$response = $this->createJsonResponse($json, $code);
echo json_encode($response);
exit();
$this->close($response);
}
/**
* @param ResponseInterface $response
*/
protected function close(ResponseInterface $response): void
{
$this->grav->close($response);
}
/**
@@ -376,6 +399,11 @@ class AdminBaseController
return false;
}
// Special Sanitization for SVG
if (Utils::contains($mime, 'svg', false)) {
Security::sanitizeSVG($tmp);
}
$upload->file->tmp_name = $tmp;
// Retrieve the current session of the uploaded files for the field
@@ -467,6 +495,21 @@ class AdminBaseController
return true;
}
/**
* Checks if the user is allowed to perform the given task with its associated permissions.
* Throws exception if the check fails.
*
* @param string $task The task to execute
* @param array $permissions The permissions given
* @throws RequestException
*/
public function checkTaskAuthorization($task = '', $permissions = [])
{
if (!$this->admin->authorize($permissions)) {
throw new RequestException($this->getRequest(), $this->admin::translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' ' . $task . '.', 403);
}
}
/**
* Gets the permissions needed to access a given view
*
@@ -481,11 +524,11 @@ class AdminBaseController
case 'configuration':
case 'config':
case 'system':
$permissions[] = 'admin.configuration';
$permissions[] = 'admin.configuration.system';
break;
case 'settings':
case 'site':
$permissions[] = 'admin.settings';
$permissions[] = 'admin.configuration.site';
break;
case 'plugins':
$permissions[] = 'admin.plugins';
@@ -503,6 +546,9 @@ class AdminBaseController
case 'pages':
$permissions[] = 'admin.pages';
break;
default:
$permissions[] = 'admin.configuration.' . $type;
$permissions[] = 'admin.configuration_' . $type;
}
return $permissions;
@@ -609,48 +655,12 @@ class AdminBaseController
/**
* Redirect to the route stored in $this->redirect
*
* Route may or may not be prefixed by /en or /admin or /en/admin.
*/
public function redirect()
{
if (!$this->redirect) {
return;
}
$base = $this->admin->base;
$this->redirect = '/' . ltrim($this->redirect, '/');
$multilang = $this->isMultilang();
$redirect = '';
if ($multilang) {
// if base path does not already contain the lang code, add it
$langPrefix = '/' . $this->grav['session']->admin_lang;
if (!Utils::startsWith($base, $langPrefix . '/')) {
$base = $langPrefix . $base;
}
// now the first 4 chars of base contain the lang code.
// if redirect path already contains the lang code, and is != than the base lang code, then use redirect path as-is
if (Utils::pathPrefixedByLangCode($base) && Utils::pathPrefixedByLangCode($this->redirect)
&& !Utils::startsWith($this->redirect, $base)
) {
$redirect = $this->redirect;
} else {
if (!Utils::startsWith($this->redirect, $base)) {
$this->redirect = $base . $this->redirect;
}
}
} else {
if (!Utils::startsWith($this->redirect, $base)) {
$this->redirect = $base . $this->redirect;
}
}
if (!$redirect) {
$redirect = $this->redirect;
}
$this->grav->redirect($redirect, $this->redirectCode);
$this->admin->redirect($this->redirect, $this->redirectCode);
}
/**
@@ -817,6 +827,12 @@ class AdminBaseController
$media = $data->getMedia();
} else {
/** @var UniformResourceLocator $locator */
$locator = $this->grav['locator'];
if ($locator->isStream($folder)) {
$folder = $locator->findResource($folder);
}
// Set destination
$folder = Folder::getRelativePath(rtrim($folder, '/'));
$folder = $this->admin->getPagePathFromToken($folder);
@@ -974,10 +990,10 @@ class AdminBaseController
$this->taskRemoveMedia($destination . '/' . $filename);
if ($page) {
$keys = explode('.', preg_replace('/^header./', '', $field));
$header = (array)$page->header();
$keys = explode('.', preg_replace('/^header./', '', $field));
$header = (array)$page->header();
$data_path = implode('.', $keys);
$data = Utils::getDotNotation($header, $data_path);
$data = Utils::getDotNotation($header, $data_path);
if (isset($data[$path])) {
unset($data[$path]);
@@ -986,6 +1002,10 @@ class AdminBaseController
}
$page->save();
} elseif ($type === 'user') {
$user = Grav::instance()['user'];
unset($user->avatar);
$user->save();
} else {
$blueprint_prefix = $type === 'config' ? '' : $type . '.';
@@ -1019,6 +1039,8 @@ class AdminBaseController
}
}
Cache::clearCache('invalidate');
$this->admin->json_response = [
'status' => 'success',
'message' => $this->admin::translate('PLUGIN_ADMIN.REMOVE_SUCCESSFUL')
@@ -1116,4 +1138,32 @@ class AdminBaseController
return true;
}
/**
* @param string $message
* @param string $type
* @return $this
*/
protected function setMessage($message, $type = 'info')
{
$this->admin->setMessage($message, $type);
return $this;
}
/**
* @return Config
*/
protected function getConfig(): Config
{
return $this->grav['config'];
}
/**
* @return ServerRequestInterface
*/
protected function getRequest(): ServerRequestInterface
{
return $this->grav['request'];
}
}

View File

@@ -0,0 +1,407 @@
<?php
declare(strict_types=1);
namespace Grav\Plugin\Admin\Controllers;
use Grav\Common\Debugger;
use Grav\Common\Grav;
use Grav\Common\Inflector;
use Grav\Common\Language\Language;
use Grav\Common\Utils;
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
use Grav\Framework\Form\Interfaces\FormInterface;
use Grav\Framework\Psr7\Response;
use Grav\Framework\RequestHandler\Exception\NotFoundException;
use Grav\Framework\RequestHandler\Exception\PageExpiredException;
use Grav\Framework\RequestHandler\Exception\RequestException;
use Grav\Framework\Route\Route;
use Grav\Framework\Session\SessionInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\Session\Message;
abstract class AbstractController implements RequestHandlerInterface
{
/** @var string */
protected $nonce_action = 'admin-form';
/** @var string */
protected $nonce_name = 'admin-nonce';
/** @var ServerRequestInterface */
protected $request;
/** @var Grav */
protected $grav;
/** @var string */
protected $type;
/** @var string */
protected $key;
/**
* Handle request.
*
* Fires event: admin.[directory].[task|action].[command]
*
* @param ServerRequestInterface $request
* @return Response
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
$attributes = $request->getAttributes();
$this->request = $request;
$this->grav = $attributes['grav'] ?? Grav::instance();
$this->type = $attributes['type'] ?? null;
$this->key = $attributes['key'] ?? null;
/** @var Route $route */
$route = $attributes['route'];
$post = $this->getPost();
if ($this->isFormSubmit()) {
$form = $this->getForm();
$this->nonce_name = $attributes['nonce_name'] ?? $form->getNonceName();
$this->nonce_action = $attributes['nonce_action'] ?? $form->getNonceAction();
}
try {
$task = $request->getAttribute('task') ?? $post['task'] ?? $route->getParam('task');
if ($task) {
if (empty($attributes['forwarded'])) {
$this->checkNonce($task);
}
$type = 'task';
$command = $task;
} else {
$type = 'action';
$command = $request->getAttribute('action') ?? $post['action'] ?? $route->getParam('action') ?? 'display';
}
$command = strtolower($command);
$event = new Event(
[
'controller' => $this,
'response' => null
]
);
$this->grav->fireEvent("admin.{$this->type}.{$type}.{$command}", $event);
$response = $event['response'];
if (!$response) {
/** @var Inflector $inflector */
$inflector = $this->grav['inflector'];
$method = $type . $inflector::camelize($command);
if ($method && method_exists($this, $method)) {
$response = $this->{$method}($request);
} else {
throw new NotFoundException($request);
}
}
} catch (\Exception $e) {
/** @var Debugger $debugger */
$debugger = $this->grav['debugger'];
$debugger->addException($e);
$response = $this->createErrorResponse($e);
}
if ($response instanceof Response) {
return $response;
}
return $this->createJsonResponse($response);
}
/**
* Get request.
*
* @return ServerRequestInterface
*/
public function getRequest(): ServerRequestInterface
{
return $this->request;
}
/**
* @param string|null $name
* @param mixed $default
* @return mixed
*/
public function getPost(string $name = null, $default = null)
{
$body = $this->request->getParsedBody();
if ($name) {
return $body[$name] ?? $default;
}
return $body;
}
/**
* Check if a form has been submitted.
*
* @return bool
*/
public function isFormSubmit(): bool
{
return (bool)$this->getPost('__form-name__');
}
/**
* Get form.
*
* @param string|null $type
* @return FormInterface
*/
public function getForm(string $type = null): FormInterface
{
$object = $this->getObject();
if (!$object) {
throw new \RuntimeException('Not Found', 404);
}
$formName = $this->getPost('__form-name__');
$uniqueId = $this->getPost('__unique_form_id__') ?: $formName;
$form = $object->getForm($type ?? 'edit');
if ($uniqueId) {
$form->setUniqueId($uniqueId);
}
return $form;
}
/**
* @return FlexObjectInterface
*/
abstract public function getObject();
/**
* Get Grav instance.
*
* @return Grav
*/
public function getGrav(): Grav
{
return $this->grav;
}
/**
* Get session.
*
* @return SessionInterface
*/
public function getSession(): SessionInterface
{
return $this->getGrav()['session'];
}
/**
* Display the current admin page.
*
* @return Response
*/
public function createDisplayResponse(): ResponseInterface
{
return new Response(418);
}
/**
* Create custom HTML response.
*
* @param string $content
* @param int $code
* @return Response
*/
public function createHtmlResponse(string $content, int $code = null): ResponseInterface
{
return new Response($code ?: 200, [], $content);
}
/**
* Create JSON response.
*
* @param array $content
* @return Response
*/
public function createJsonResponse(array $content): ResponseInterface
{
$code = $content['code'] ?? 200;
if ($code >= 301 && $code <= 307) {
$code = 200;
}
return new Response($code, ['Content-Type' => 'application/json'], json_encode($content));
}
/**
* Create redirect response.
*
* @param string $url
* @param int $code
* @return Response
*/
public function createRedirectResponse(string $url, int $code = null): ResponseInterface
{
if (null === $code || $code < 301 || $code > 307) {
$code = $this->grav['config']->get('system.pages.redirect_default_code', 302);
}
$accept = $this->getAccept(['application/json', 'text/html']);
if ($accept === 'application/json') {
return $this->createJsonResponse(['code' => $code, 'status' => 'redirect', 'redirect' => $url]);
}
return new Response($code, ['Location' => $url]);
}
/**
* Create error response.
*
* @param \Exception $exception
* @return Response
*/
public function createErrorResponse(\Exception $exception): ResponseInterface
{
$validCodes = [
400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418,
422, 423, 424, 425, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 511
];
if ($exception instanceof RequestException) {
$code = $exception->getHttpCode();
$reason = $exception->getHttpReason();
} else {
$code = $exception->getCode();
$reason = null;
}
if (!in_array($code, $validCodes, true)) {
$code = 500;
}
$message = $exception->getMessage();
$response = [
'code' => $code,
'status' => 'error',
'message' => $message
];
$accept = $this->getAccept(['application/json', 'text/html']);
if ($accept === 'text/html') {
$method = $this->getRequest()->getMethod();
// On POST etc, redirect back to the previous page.
if ($method !== 'GET' && $method !== 'HEAD') {
$this->setMessage($message, 'error');
$referer = $this->request->getHeaderLine('Referer');
return $this->createRedirectResponse($referer, 303);
}
// TODO: improve error page
return $this->createHtmlResponse($response['message']);
}
return new Response($code, ['Content-Type' => 'application/json'], json_encode($response), '1.1', $reason);
}
/**
* Translate a string.
*
* @param string $string
* @return string
*/
public function translate(string $string): string
{
/** @var Language $language */
$language = $this->grav['language'];
return $language->translate($string);
}
/**
* Set message to be shown in the admin.
*
* @param string $message
* @param string $type
* @return $this
*/
public function setMessage($message, $type = 'info')
{
/** @var Message $messages */
$messages = $this->grav['messages'];
$messages->add($message, $type);
return $this;
}
/**
* Check if request nonce is valid.
*
* @param string $task
* @throws PageExpiredException If nonce is not valid.
*/
protected function checkNonce(string $task): void
{
$nonce = null;
if (\in_array(strtoupper($this->request->getMethod()), ['POST', 'PUT', 'PATCH', 'DELETE'])) {
$nonce = $this->getPost($this->nonce_name);
}
if (!$nonce) {
$nonce = $this->grav['uri']->param($this->nonce_name);
}
if (!$nonce) {
$nonce = $this->grav['uri']->query($this->nonce_name);
}
if (!$nonce || !Utils::verifyNonce($nonce, $this->nonce_action)) {
throw new PageExpiredException($this->request);
}
}
/**
* Return the best matching mime type for the request.
*
* @param string[] $compare
* @return string|null
*/
protected function getAccept(array $compare): ?string
{
$accepted = [];
foreach ($this->request->getHeader('Accept') as $accept) {
foreach (explode(',', $accept) as $item) {
if (!$item) {
continue;
}
$split = explode(';q=', $item);
$mime = array_shift($split);
$priority = array_shift($split) ?? 1.0;
$accepted[$mime] = $priority;
}
}
arsort($accepted);
// TODO: add support for image/* etc
$list = array_intersect($compare, array_keys($accepted));
if (!$list && (isset($accepted['*/*']) || isset($accepted['*']))) {
return reset($compare) ?: null;
}
return reset($list) ?: null;
}
}

59
classes/plugin/Router.php Normal file
View File

@@ -0,0 +1,59 @@
<?php
namespace Grav\Plugin\Admin;
use Grav\Common\Processors\ProcessorBase;
use Grav\Framework\Route\Route;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
class Router extends ProcessorBase
{
public $id = 'admin_router';
public $title = 'Admin Panel';
/**
* Handle routing to the dashboard, group and build objects.
*
* @param ServerRequestInterface $request
* @param RequestHandlerInterface $handler
* @return ResponseInterface
*/
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
{
$this->startTimer();
$context = $request->getAttributes();
/** @var Route $route */
$route = $context['route'];
$normalized = mb_strtolower(trim($route->getRoute(), '/'));
$parts = explode('/', $normalized);
array_shift($parts);
$key = array_shift($parts);
$path = implode('/', $parts);
$request = $request->withAttribute('admin', ['path' => $path, 'parts' => $parts]);
$response = null;
/*
if ($key === '__TODO__') {
$controller = new TodoController();
$response = $controller->handle($request);
}
*/
if (!$response) {
// Fallback to the old admin behavior.
$response = $handler->handle($request);
}
$this->stopTimer();
// Never allow admin pages to be rendered in <frame>, <iframe>, <embed> or <object> for improved security.
return $response->withHeader('X-Frame-Options', 'NONE');
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Grav\Plugin\Admin;
use ScssPhp\ScssPhp\Compiler;
class ScssCompiler
{
protected $compiler;
public function compiler()
{
if ($this->compiler === null) {
$this->reset();
}
return $this->compiler;
}
public function reset()
{
$this->compiler = new Compiler();
return $this;
}
public function setVariables(array $variables)
{
$this->compiler()->setVariables($variables);
return $this;
}
public function setImportPaths(array $paths)
{
$this->compiler()->setImportPaths($paths);
return $this;
}
public function compile(string $input_file, string $output_file)
{
$input = file_get_contents($input_file);
$output = $this->compiler()->compile($input);
file_put_contents($output_file, $output);
return $this;
}
public function compileAll(array $input_paths, string $output_file)
{
$input = '';
foreach ($input_paths as $input_file) {
$input .= trim(file_get_contents($input_file)) . "\n\n";
}
$output = $this->compiler()->compile($input);
file_put_contents($output_file, $output);
return $this;
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Grav\Plugin\Admin;
class ScssList
{
/** @var string[] */
protected $list = [];
/**
* ScssList constructor.
* @param string|null $item
*/
public function __construct($item = null)
{
if ($item) {
$this->add($item);
}
}
/**
* @return array
*/
public function all(): array
{
return $this->list;
}
/**
* @param string $item
* @return void
*/
public function add($item): void
{
if ($item) {
$this->list[] = $item;
}
}
/**
* @param string $item
* @return void
*/
public function remove($item): void
{
$pos = array_search($item, $this->list, true);
if ($pos) {
unset($this->list[$pos]);
}
}
}

View File

@@ -2,21 +2,23 @@
namespace Grav\Plugin\Admin\Twig;
use Grav\Common\Data\Data;
use Grav\Common\Grav;
use Grav\Common\Page\Interfaces\PageInterface;
use Grav\Common\Utils;
use Grav\Common\Yaml;
use Grav\Common\Language\Language;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
use Twig\TwigFunction;
use Grav\Plugin\Admin\Admin;
class AdminTwigExtension extends \Twig_Extension
class AdminTwigExtension extends AbstractExtension
{
/**
* @var Grav
*/
/** @var Grav */
protected $grav;
/**
* @var Language $lang
*/
/** @var Language $lang */
protected $lang;
public function __construct()
@@ -25,22 +27,25 @@ class AdminTwigExtension extends \Twig_Extension
$this->lang = $this->grav['user']->language;
}
public function getFilters()
public function getFilters(): array
{
return [
new \Twig_SimpleFilter('tu', [$this, 'tuFilter']),
new \Twig_SimpleFilter('toYaml', [$this, 'toYamlFilter']),
new \Twig_SimpleFilter('fromYaml', [$this, 'fromYamlFilter']),
new \Twig_SimpleFilter('adminNicetime', [$this, 'adminNicetimeFilter']),
new \Twig_SimpleFilter('nested', [$this, 'nestedFilter']),
new TwigFilter('tu', [$this, 'tuFilter']),
new TwigFilter('toYaml', [$this, 'toYamlFilter']),
new TwigFilter('fromYaml', [$this, 'fromYamlFilter']),
new TwigFilter('adminNicetime', [$this, 'adminNicetimeFilter']),
new TwigFilter('nested', [$this, 'nestedFilter']),
new TwigFilter('flatten', [$this, 'flattenFilter']),
];
}
public function getFunctions()
public function getFunctions(): array
{
return [
new \Twig_SimpleFunction('getPageUrl', [$this, 'getPageUrl'], ['needs_context' => true]),
new \Twig_SimpleFunction('clone', [$this, 'cloneFunc']),
new TwigFunction('admin_route', [$this, 'adminRouteFunc']),
new TwigFunction('getPageUrl', [$this, 'getPageUrl']),
new TwigFunction('clone', [$this, 'cloneFunc']),
new TwigFunction('data', [$this, 'dataFunc']),
];
}
@@ -61,26 +66,30 @@ class AdminTwigExtension extends \Twig_Extension
return $current;
}
public function flattenFilter($array)
{
return Utils::arrayFlattenDotNotation($array);
}
public function cloneFunc($obj)
{
return clone $obj;
}
public function getPageUrl($context, PageInterface $page)
public function adminRouteFunc(string $route = '', string $languageCode = null)
{
$page_route = trim($page->rawRoute(), '/');
$page_lang = $page->language();
$base_url = $context['base_url'];
$base_url_simple = $context['base_url_simple'];
$admin_lang = Grav::instance()['session']->admin_lang ?: 'en';
/** @var Admin $admin */
$admin = Grav::instance()['admin'];
if ($page_lang && $page_lang !== $admin_lang) {
$page_url = $base_url_simple . '/' . $page_lang . '/' . $context['admin_route'] . '/pages/' . $page_route;
} else {
$page_url = $base_url . '/pages/' . $page_route;
}
return $admin->getAdminRoute($route, $languageCode)->toString(true);
}
return $page_url;
public function getPageUrl(PageInterface $page)
{
/** @var Admin $admin */
$admin = Grav::instance()['admin'];
return $admin->getAdminRoute('/pages' . $page->rawRoute(), $page->language())->toString(true);
}
public static function tuFilter()
@@ -115,4 +124,8 @@ class AdminTwigExtension extends \Twig_Extension
return Grav::instance()['admin']->adminNiceTime($date, $long_strings);
}
public function dataFunc(array $data, $blueprints = null)
{
return new Data($data, $blueprints);
}
}

View File

@@ -1,54 +1,54 @@
<?php
namespace Grav\Plugin\Admin;
use Grav\Common\Grav;
use Grav\Common\User\Interfaces\UserCollectionInterface;
use Grav\Common\User\Interfaces\UserInterface;
/**
* Admin utils class
*
* @license MIT
*/
class Utils
{
/**
* Matches an email to a user
*
* @param string $email
*
* @return UserInterface
*/
public static function findUserByEmail(string $email)
{
$grav = Grav::instance();
/** @var UserCollectionInterface $users */
$users = $grav['accounts'];
return $users->find($email, ['email']);
}
/**
* Generates a slug of the given string
*
* @param string $str
* @return string
*/
public static function slug(string $str)
{
if (function_exists('transliterator_transliterate')) {
$str = transliterator_transliterate('Any-Latin; NFD; [:Nonspacing Mark:] Remove; NFC; [:Punctuation:] Remove;', $str);
} else {
$str = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $str);
}
$str = strtolower($str);
$str = preg_replace('/[-\s]+/', '-', $str);
$str = preg_replace('/[^a-z0-9-]/i', '', $str);
$str = trim($str, '-');
return $str;
}
}
<?php
namespace Grav\Plugin\Admin;
use Grav\Common\Grav;
use Grav\Common\User\Interfaces\UserCollectionInterface;
use Grav\Common\User\Interfaces\UserInterface;
/**
* Admin utils class
*
* @license MIT
*/
class Utils
{
/**
* Matches an email to a user
*
* @param string $email
*
* @return UserInterface
*/
public static function findUserByEmail(string $email)
{
$grav = Grav::instance();
/** @var UserCollectionInterface $users */
$users = $grav['accounts'];
return $users->find($email, ['email']);
}
/**
* Generates a slug of the given string
*
* @param string $str
* @return string
*/
public static function slug(string $str)
{
if (function_exists('transliterator_transliterate')) {
$str = transliterator_transliterate('Any-Latin; NFD; [:Nonspacing Mark:] Remove; NFC; [:Punctuation:] Remove;', $str);
} else {
$str = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $str);
}
$str = strtolower($str);
$str = preg_replace('/[-\s]+/', '-', $str);
$str = preg_replace('/[^a-z0-9-]/i', '', $str);
$str = trim($str, '-');
return $str;
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace Grav\Plugin\Admin;
use Grav\Common\Data\Data;
use Grav\Common\Grav;
use Grav\Framework\File\File;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use Symfony\Component\Yaml\Yaml;
class WhiteLabel
{
protected $grav;
protected $scss;
public function __construct()
{
$this->grav = Grav::instance();
$this->scss = new ScssCompiler();
}
public function compilePresetScss($config, $options = [
'input' => 'plugin://admin/themes/grav/scss/preset.scss',
'output' => 'asset://admin-preset.css'
])
{
if (is_array($config)) {
$color_scheme = $config['color_scheme'];
} else {
$color_scheme = $config->get('whitelabel.color_scheme');
}
if ($color_scheme) {
/** @var UniformResourceLocator $locator */
$locator = $this->grav['locator'];
// Use ScssList object to make it easier ot handle in event
$scss_list = new ScssList($locator->findResource($options['input']));
$output_css = $locator->findResource(($options['output']), true, true);
Grav::instance()->fireEvent('onAdminCompilePresetSCSS', new Event(['scss' => $scss_list]));
// Convert bak to regular array now we have run the event
$input_scss = $scss_list->all();
$imports = [$locator->findResource('plugin://admin/themes/grav/scss')];
foreach ($input_scss as $scss) {
$input_path = dirname($scss);
if (!in_array($input_path, $imports)) {
$imports[] = $input_path;
}
}
try {
$compiler = $this->scss->reset();
$compiler->setVariables($color_scheme['colors'] + $color_scheme['accents']);
$compiler->setImportPaths($imports);
$compiler->compileAll($input_scss, $output_css);
} catch (\Exception $e) {
return [false, $e->getMessage()];
}
return [true, 'Recompiled successfully'];
}
return [false, ' Could not be recompiled, missing color scheme...'];
}
public function exportPresetScsss($config, $location = 'asset://admin-theme-export.yaml')
{
if (isset($config['color_scheme'])) {
$color_scheme = $config['color_scheme'];
$body = Yaml::dump($color_scheme);
$file = new File($location);
$file->save($body);
// todo: handle errors/exceptions?
return [true, 'File created successfully'];
} else {
return [false, ' Could not export, missing color scheme...'];
}
}
}

View File

@@ -20,28 +20,31 @@
"docs": "https://github.com/getgrav/grav-plugin-admin/blob/master/README.md"
},
"require": {
"php": ">=7.1.3",
"php": "^7.3.6 || ^8.0",
"ext-json": "*",
"composer/semver": "^1.4",
"p3k/picofeed": "@stable"
"p3k/picofeed": "@stable",
"scssphp/scssphp": "^1.4"
},
"require-dev": {
"codeception/codeception": "^2.4",
"fzaninotto/faker": "^1.8",
"symfony/yaml": "~4.1",
"symfony/console": "~4.1",
"symfony/finder": "~4.1",
"symfony/event-dispatcher": "~4.1"
"symfony/yaml": "~4.4",
"symfony/console": "~4.4",
"symfony/finder": "~4.4",
"symfony/event-dispatcher": "~4.4"
},
"autoload": {
"psr-4": {
"Grav\\Plugin\\Admin\\": "classes/plugin"
},
"classmap": [
"classes/",
"admin.php"
]
},
"config": {
"platform": {
"php": "7.1.3"
"php": "7.3.6"
}
},
"scripts": {

458
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -34,7 +34,6 @@ PLUGIN_ADMIN:
NEXT: "Next"
PREVIOUS: "Previous"
ADD_PAGE: "Add Page"
ADD_MODULAR: "Add Modular"
MOVE: "Move"
DELETE: "Delete"
UNSET: "Unset"
@@ -44,7 +43,6 @@ PLUGIN_ADMIN:
EXPERT: "Expert"
EXPAND_ALL: "Expand All"
COLLAPSE_ALL: "Collapse All"
SECURE_DELETE_DESC: "Type DELETE and confirm"
ERROR: "Error"
CLOSE: "Close"
CANCEL: "Cancel"
@@ -55,6 +53,7 @@ PLUGIN_ADMIN:
MODAL_CHANGED_DETECTED_DESC: "You have unsaved changes. Are you sure you want to leave without saving?"
MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_TITLE: "Confirmation Required"
MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_DESC: "Are you sure you want to delete this file? This action cannot be undone."
MODAL_UPDATE_GRAV_CONFIRMATION_REQUIRED_DESC: "You are about to upgrade Grav to the latest version available. Would you like to continue?"
ADD_FILTERS: "Add Filters"
SEARCH_PAGES: "Search Pages"
VERSION: "Version"
@@ -112,6 +111,7 @@ PLUGIN_ADMIN:
BACKUP_DATE: "Backup Date"
STATISTICS: "Statistics"
VIEWS_STATISTICS: "Page View Statistics"
TODAY: "Today"
WEEK: "Week"
MONTH: "Month"
@@ -305,11 +305,9 @@ PLUGIN_ADMIN:
FOLDER: "Folder"
ASCENDING: "Ascending"
DESCENDING: "Descending"
ADD_MODULAR_CONTENT: "Add Modular Content"
PAGE_TITLE: "Page Title"
PAGE_TITLE_HELP: "The title of the page"
PAGE: "Page"
MODULAR_TEMPLATE: "Modular Template"
FRONTMATTER: "Frontmatter"
FILENAME: "Filename"
PARENT_PAGE: "Parent Page"
@@ -342,8 +340,6 @@ PLUGIN_ADMIN:
LANGUAGES: "Languages"
SUPPORTED: "Supported"
SUPPORTED_HELP: "Comma separated list of 2 letter language codes (for example 'en,fr,de')"
TRANSLATIONS_ENABLED: "Translations enabled"
TRANSLATIONS_ENABLED_HELP: "Support translations in Grav, plugins and extensions"
TRANSLATIONS_FALLBACK: "Translations fallback"
TRANSLATIONS_FALLBACK_HELP: "Fallback through supported translations if active language doesn't exist"
ACTIVE_LANGUAGE_IN_SESSION: "Active language in session"
@@ -443,6 +439,8 @@ PLUGIN_ADMIN:
CACHE_ALL_HELP: "Run all images through Grav's cache system even if they have no media manipulations"
IMAGES_DEBUG: "Image debug watermark"
IMAGES_DEBUG_HELP: "Show an overlay over images indicating the pixel depth of the image when working with retina for example"
IMAGES_LOADING: "Image loading behavior"
IMAGES_LOADING_HELP: "The loading attribute allows a browser to defer loading offscreen images and iframes until users scroll near them. loading supports three values: auto, lazy, eager"
IMAGES_SEOFRIENDLY: "SEO-Friendly Image names"
IMAGES_SEOFRIENDLY_HELP: "When enabled, the image name is displayed first, then a smaller hash to reflect processed operations"
UPLOAD_LIMIT: "File upload limit"
@@ -505,6 +503,8 @@ PLUGIN_ADMIN:
PLUGIN_STATUS: "Plugin status"
INCLUDE_DEFAULT_LANG: "Include default language"
INCLUDE_DEFAULT_LANG_HELP: "This will prepend all URLs in the default language with the default language. e.g. `/en/blog/my-post`"
INCLUDE_DEFAULT_LANG_FILE_EXTENSION: "Include default language in file extension"
INCLUDE_DEFAULT_LANG_HELP_FILE_EXTENSION: "If enabled, it will prepend the default language to the file extension (e.g. `.en.md`). Disable it to keep the default language using `.md` file extension."
PAGES_FALLBACK_ONLY: "Pages fallback only"
PAGES_FALLBACK_ONLY_HELP: "Only 'fallback' to find page content through supported languages, default behavior is to display any language found if active language is missing"
ALLOW_URL_TAXONOMY_FILTERS: "URL Taxonomy Filters"
@@ -617,10 +617,7 @@ PLUGIN_ADMIN:
EDITOR: "Editor"
PREVIEW: "Preview"
FULLSCREEN: "Fullscreen"
MODULAR: "Modular"
MODULAR_SETUP: "Modular Setup"
NON_ROUTABLE: "Non-Routable"
NON_MODULAR: "Non-Modular"
NON_VISIBLE: "Non-Visible"
NON_PUBLISHED: "Non-Published"
CHARACTERS: "characters"
@@ -771,11 +768,11 @@ PLUGIN_ADMIN:
SCHEDULER: "Scheduler"
SCHEDULER_INSTALL_INSTRUCTIONS: "Install Instructions"
SCHEDULER_INSTALLED_READY: "Installed and Ready"
SCHEDULER_CRON_NA: "Cron Not Available"
SCHEDULER_NOT_ENABLED: "Not Enabled"
SCHEDULER_CRON_NA: "Cron Not Available for user: <b>%s</b>"
SCHEDULER_NOT_ENABLED: "Not Enabled for user: <b>%s</b>"
SCHEDULER_SETUP: "Scheduler Setup"
SCHEDULER_INSTRUCTIONS: "The <b>Grav Scheduler</b> allows you to create and schedule custom jobs. It also provides a method for Grav plugins to integrate programmatically and dynamically add jobs to be run periodically."
SCHEDULER_POST_INSTRUCTIONS: "To enable the Scheduler's functionality, you must add the <b>Grav Scheduler</b> to your system's crontab file. Run the command above from the terminal to add it automatically. Once saved, refresh this page to see the status Ready."
SCHEDULER_POST_INSTRUCTIONS: "To enable the Scheduler's functionality, you must add the <b>Grav Scheduler</b> to your system's crontab file for the <b>%s</b> user. Run the command above from the terminal to add it automatically. Once saved, refresh this page to see the status."
SCHEDULER_JOBS: "Custom Scheduler Jobs"
SCHEDULER_STATUS: "Scheduler Status"
SCHEDULER_RUNAT: "Run At"
@@ -830,11 +827,140 @@ PLUGIN_ADMIN:
DEBUGGER_PROVIDER_HELP: "Default is PHP Debug Bar, but Clockwork browser extension provides for a less intrusive approach"
DEBUGGER_DEBUGBAR: "PHP Debug Bar"
DEBUGGER_CLOCKWORK: "Clockwork Browser Extension"
PAGE_ROUTE_NOT_FOUND: "Page route not found"
PAGE_ROUTE_FOUND: "Page route found"
NO_ROUTE_PROVIDED: "No route provided"
CONTENT_LANGUAGE_FALLBACKS: "Content Language Fallback"
CONTENT_LANGUAGE_FALLBACKS_HELP: "By default if the content isn't translated, Grav will display content in the default language. Use this setting to override that behavior per language basis."
CONTENT_LANGUAGE_FALLBACK: "Fallback Languages"
CONTENT_LANGUAGE_FALLBACK_HELP: "Please enter a list of language codes. Note that if you omit default language code, it will not be used."
CONTENT_FALLBACK_LANGUAGE_HELP: "Specify language code which you want to customize."
EXPERIMENTAL: "Experimental"
PAGES_TYPE: "Frontend Page Type"
PAGES_TYPE_HELP: "This option enables Flex Object pages on the front-end. Admin Flex Pages requires Flex Objects plugin"
ACCOUNTS_TYPE: "Accounts Type"
ACCOUNTS_TYPE_HELP: "Flex Object system to store user accounts"
ACCOUNTS_STORAGE: "Account Storage"
ACCOUNTS_STORAGE_HELP: "The storage mechanism to be used for Flex Object Account type. Files is the traditional approach where account are stored in a YAML file in a single folder, while Folder creates a new folder for each account"
FLEX: "Flex Object (EXPERIMENTAL)"
REGULAR: "Regular"
FILE: "File"
SANITIZE_SVG: "Sanitize SVG"
SANITIZE_SVG_HELP: "Removes any XSS code from SVG"
ACCOUNTS: "Accounts"
USER_ACCOUNTS: "User Accounts"
USER_GROUPS: "User Groups"
GROUP_NAME: "Group Name"
DISPLAY_NAME: "Display Name"
ICON: "Icon"
ACCESS: "Access"
NO_ACCESS: "No Access"
SUPER_USER: "Super User"
ALLOWED: "Allowed"
DENIED: "Denied"
MODULE: "Module"
NON_MODULE: "Non-Module"
ADD_MODULE: "Add Module"
MODULE_SETUP: "Module Setup"
MODULE_TEMPLATE: "Module Template"
ADD_MODULE_CONTENT: "Add Module Content"
CHANGELOG: "Changelog"
PAGE_ACCESS: "Page Access"
PAGE PERMISSIONS: "Page Permissions"
PAGE_ACCESS_HELP: "User with following access permissions can access the page."
PAGE_VISIBILITY_REQUIRES_ACCESS: "Menu Visibility Requires Access"
PAGE_VISIBILITY_REQUIRES_ACCESS_HELP: "Set to Yes if page should be shown in menus only if user can access them."
PAGE_INHERIT_PERMISSIONS: "Inherit Permissions"
PAGE_INHERIT_PERMISSIONS_HELP: "Inherit ACL from parent page."
PAGE_AUTHORS: "Page Authors"
PAGE_AUTHORS_HELP: "Members of Page Authors have owner level access to this page defined in special 'Authors' page group."
PAGE_GROUPS: "Page Groups"
PAGE_GROUPS_HELP: "Members of Page Groups have special access to this page."
READ: "Read"
PUBLISH: "Publish"
LIST: "List"
ACCESS_SITE: "Site"
ACCESS_SITE_LOGIN: "Login to Site"
ACCESS_ADMIN: "Admin"
ACCESS_ADMIN_LOGIN: "Login to Admin"
ACCESS_ADMIN_SUPER: "Super User"
ACCESS_ADMIN_CACHE: "Clear Cache"
ACCESS_ADMIN_CONFIGURATION: "Configuration"
ACCESS_ADMIN_CONFIGURATION_SYSTEM: "Manage System Configuration"
ACCESS_ADMIN_CONFIGURATION_SITE: "Manage Site Configuration"
ACCESS_ADMIN_CONFIGURATION_MEDIA: "Manage Media Configuration"
ACCESS_ADMIN_CONFIGURATION_INFO: "See Server Information"
ACCESS_ADMIN_SETTINGS: "Settings"
ACCESS_ADMIN_PAGES: "Manage Pages"
ACCESS_ADMIN_MAINTENANCE: "Site Maintenance"
ACCESS_ADMIN_STATISTICS: "Site Statistics"
ACCESS_ADMIN_PLUGINS: "Manage Plugins"
ACCESS_ADMIN_THEMES: "Manage Themes"
ACCESS_ADMIN_TOOLS: "Access to Tools"
ACCESS_ADMIN_USERS: "Manage Users"
USERS: "Users"
ACL: "Access Control"
FLEX_CACHING: "Flex Caching"
FLEX_INDEX_CACHE_ENABLED: "Enable Index Caching"
FLEX_INDEX_CACHE_LIFETIME: "Index Cache Lifetime (seconds)"
FLEX_OBJECT_CACHE_ENABLED: "Enable Object Caching"
FLEX_OBJECT_CACHE_LIFETIME: "Object Cache Lifetime (seconds)"
FLEX_RENDER_CACHE_ENABLED: "Enable Render Caching"
FLEX_RENDER_CACHE_LIFETIME: "Render Cache Lifetime (seconds)"
DEBUGGER_CENSORED: "Censor Sensitive Data"
DEBUGGER_CENSORED_HELP: "Clockwork Provider ONLY: If Yes, censor potentially sensitive information (POST parameters, cookies, files, configuration and most array/object data in log messages)"
LANGUAGE_TRANSLATIONS: "Translations"
LANGUAGE_TRANSLATIONS_HELP: "If false, translation keys are used instead of translated strings. This feature can be used to help to fix bad translations or to find hardcoded English strings."
STRICT_BLUEPRINT_COMPAT: "Blueprint Compatibility"
STRICT_BLUEPRINT_COMPAT_HELP: "Enables backward compatible strict support for blueprints. If turned off, the new behavior makes the form validation to fail if there is extra data which is not defined in the blueprint."
RESET: "Reset"
LOGOS: "Logos"
CUSTOMIZATION: "Customization"
PRESETS: "Presets"
COLOR_SCHEME_LABEL: "Color Scheme"
COLOR_SCHEME_HELP: "Choose a color scheme from a list of predefined combinations, or add your own style"
COLOR_SCHEME_NAME: "Custom Color Scheme Name"
COLOR_SCHEME_NAME_HELP: "Give a name to your custom theme for exporting and sharing"
COLOR_SCHEME_NAME_PLACEHOLDER: "Shades of Blue"
PRIMARY_ACCENT_LABEL: "Primary Accent"
PRIMARY_ACCENT_HELP: "Select which color set the primary accent should use for it's color scheme"
SECONDARY_ACCENT_LABEL: "Secondary Accent"
SECONDARY_ACCENT_HELP: "Select which color set the secondary accent should use for it's color scheme"
TERTIARY_ACCENT_LABEL: "Tertiary Accent"
TERTIARY_ACCENT_HELP: "Select which color set the tertiary accent should use for it's color scheme"
WEB_FONTS_LABEL: "Web Fonts"
WEB_FONTS_HELP: "Use custom web fonts"
HEADER_FONT_LABEL: "Header Font"
HEADER_FONT_HELP: "Font used for headers, side nav and section titles"
BODY_FONT_LABEL: "Body Font"
BODY_FONT_HELP: "Primary font used throughout the body of the theme"
CUSTOM_CSS_LABEL: "Custom CSS"
CUSTOM_CSS_PLACEHOLDER: "This is some placeholder text"
CUSTOM_CSS_HELP: "Custom CSS that will be added to every admin page"
CUSTOM_FOOTER: "Custom Footer"
CUSTOM_FOOTER_HELP: "You can use HTML and/or Markdown syntax here"
LOGIN_SCREEN_CUSTOM_LOGO_LABEL: "Login Custom Logo"
LOGIN_SCREEN_CUSTOM_LOGO_HELP: ""
TOP_LEFT_CUSTOM_LOGO_LABEL: "Primary Custom Logo"
TOP_LEFT_CUSTOM_LOGO_HELP: ""
LOAD_PRESET: "Load Preset"
RECOMPILE: "Recompile"
EXPORT: "Export"
QUICKTRAY_RECOMPILE: "QuickTray Recompile Icon"
QUICKTRAY_RECOMPILE_HELP: "Will recompile the preset SCSS to pickup any changes or new plugins"
CODEMIRROR: "CodeMirror Editor"
CODEMIRROR_THEME: "Editor Theme"
CODEMIRROR_THEME_DESC: "**NOTE:** Use the [CodeMirror Themes Demo](https://codemirror.net/demo/theme.html?target=_blank) to see these in action. **_Paper_** is the default Grav theme."
CODEMIRROR_FONTSIZE: "Editor Font Size"
CODEMIRROR_MD_FONT: "Markdown Editor Font"
CUSTOM_PRESETS: "Custom Presets"
CUSTOM_PRESETS_HELP: "Drag-n-drop a theme .yaml file here, or you can create an array of presets with text based keys"
GENERAL: "General"
CONTENT_EDITOR: "Content Editor"
CONTENT_EDITOR_HELP: "Custom editors can be preferred for content editing"
BAD_FILENAME: "Bad filename"
SHOW_SENSITIVE: "Show Sensitive Data"
SHOW_SENSITIVE_HELP: "Clockwork Provider ONLY: Censor potentially sensitive information (POST parameters, cookies, files, configuration and most array/object data in log messages)"
VALID_LINK_ATTRIBUTES: "Valid Link Attributes"
VALID_LINK_ATTRIBUTES_HELP: "Attributes that will be automatically added to the media HTML element"
PAGE_ROUTE_NOT_FOUND: "Page route not found"
PAGE_ROUTE_FOUND: "Page route found"
NO_ROUTE_PROVIDED: "No route provided"

View File

@@ -43,7 +43,6 @@ PLUGIN_ADMIN:
EXPERT: "Esperto"
EXPAND_ALL: "Expandir Tudo"
COLLAPSE_ALL: "Recolher Tudo"
SECURE_DELETE_DESC: "Digite DELETE e confirme"
ERROR: "Erro"
CLOSE: "Fechar"
CANCEL: "Cancelar"

9
pages/admin/changelog.md Normal file
View File

@@ -0,0 +1,9 @@
---
title: Changelog
access:
admin.plugins: true
admin.maintenance: true
admin.themes: true
admin.super: true
---

View File

@@ -3,6 +3,7 @@ title: Config
expires: 0
access:
admin.configuration: true
admin.configuration.system: true
admin.configuration.site: true
admin.super: true
---

View File

@@ -4,6 +4,6 @@ template: config
expires: 0
access:
admin.settings: true
admin.configuration.info: true
admin.super: true
---

View File

@@ -4,6 +4,6 @@ template: config
expires: 0
access:
admin.settings: true
admin.configuration.site: true
admin.super: true
---

View File

@@ -4,6 +4,6 @@ template: config
expires: 0
access:
admin.configuration: true
admin.configuration.system: true
admin.super: true
---

46
permissions.yaml Normal file
View File

@@ -0,0 +1,46 @@
actions:
site:
label: PLUGIN_ADMIN.ACCESS_SITE
actions:
login:
label: PLUGIN_ADMIN.ACCESS_SITE_LOGIN
admin:
label: PLUGIN_ADMIN.ACCESS_ADMIN
actions:
login:
label: PLUGIN_ADMIN.ACCESS_ADMIN_LOGIN
super:
label: PLUGIN_ADMIN.ACCESS_ADMIN_SUPER
cache:
label: PLUGIN_ADMIN.ACCESS_ADMIN_CACHE
configuration:
label: PLUGIN_ADMIN.ACCESS_ADMIN_CONFIGURATION
actions:
system:
label: PLUGIN_ADMIN.ACCESS_ADMIN_CONFIGURATION_SYSTEM
site:
label: PLUGIN_ADMIN.ACCESS_ADMIN_CONFIGURATION_SITE
media:
label: PLUGIN_ADMIN.ACCESS_ADMIN_CONFIGURATION_MEDIA
info:
label: PLUGIN_ADMIN.ACCESS_ADMIN_CONFIGURATION_INFO
pages:
label: PLUGIN_ADMIN.ACCESS_ADMIN_PAGES
maintenance:
label: PLUGIN_ADMIN.ACCESS_ADMIN_MAINTENANCE
statistics:
label: PLUGIN_ADMIN.ACCESS_ADMIN_STATISTICS
plugins:
label: PLUGIN_ADMIN.ACCESS_ADMIN_PLUGINS
themes:
label: PLUGIN_ADMIN.ACCESS_ADMIN_THEMES
tools:
label: PLUGIN_ADMIN.ACCESS_ADMIN_TOOLS
users:
label: PLUGIN_ADMIN.ACCESS_ADMIN_USERS
types:
default:
type: access

360
presets.yaml Normal file
View File

@@ -0,0 +1,360 @@
default:
name: Default
accents:
primary-accent: 'button'
secondary-accent: 'notice'
tertiary-accent: 'critical'
colors:
logo-bg: '#323640'
logo-link: '#FFFFFF'
nav-bg: '#3D424E'
nav-text: '#B7B9BD'
nav-link: '#ffffff'
nav-selected-bg: '#323640'
nav-selected-link: '#ffffff'
nav-hover-bg: '#434753'
nav-hover-link: '#ffffff'
toolbar-bg: '#ffffff'
toolbar-text: '#3D424E'
page-bg: '#F6F6F6'
page-text: '#6f7b8a'
page-link: '#0090D9'
content-bg: '#ffffff'
content-text: '#6f7b8a'
content-link: '#0090D9'
content-link2: '#da4b46'
content-header: '#414147'
content-tabs-bg: '#e6e6e6'
content-tabs-text: '#808080'
button-bg: '#0090D9'
button-text: '#ffffff'
notice-bg: '#06A599'
notice-text: '#ffffff'
update-bg: '#77559D'
update-text: '#ffffff'
critical-bg: '#F45857'
critical-text: '#ffffff'
original:
name: Original
accents:
primary-accent: 'button'
secondary-accent: 'notice'
tertiary-accent: 'critical'
colors:
logo-bg: '#1e333e'
logo-link: '#ffffff'
nav-bg: '#253a47'
nav-text: '#afc7d5'
nav-link: '#d1dee7'
nav-selected-bg: '#2d4d5b'
nav-selected-link: '#ffffff'
nav-hover-bg: '#1e333e'
nav-hover-link: '#ffffff'
toolbar-bg: '#349886'
toolbar-text: '#ffffff'
page-bg: '#314d5b'
page-text: '#81a5b5'
page-link: '#aad9ed'
content-bg: '#eeeeee'
content-text: '#737c81'
content-link: '#0082ba'
content-link2: '#da4b46'
content-header: '#314d5b'
content-tabs-bg: '#D4D4D4'
content-tabs-text: '#737c81'
button-bg: '#41bea8'
button-text: '#ffffff'
notice-bg: '#00a6cf'
notice-text: '#ffffff'
update-bg: '#8f5aad'
update-text: '#ffffff'
critical-bg: '#da4b46'
critical-text: '#ffffff'
modern:
name: Modern
accents:
primary-accent: 'logo'
secondary-accent: 'update'
tertiary-accent: 'critical'
colors:
logo-bg: '#3494cf'
logo-link: '#ffffff'
nav-bg: '#354052'
nav-text: '#8c98ad'
nav-link: '#b4bed6'
nav-selected-bg: '#30394A'
nav-selected-link: '#ffffff'
nav-hover-bg: '#3c485e'
nav-hover-link: '#ffffff'
toolbar-bg: '#ffffff'
toolbar-text: '#636468'
page-bg: '#EDF2F6'
page-text: '#929699'
page-link: '#3EA7E6'
content-bg: '#ffffff'
content-text: '#787b7d'
content-link: '#3EA7E6'
content-link2: '#3cb8ba'
content-header: '#636468'
content-tabs-bg: '#f0f0f0'
content-tabs-text: '#929699'
button-bg: '#3EA7E6'
button-text: '#ffffff'
notice-bg: '#3EA7E6'
notice-text: '#ffffff'
update-bg: '#A652AB'
update-text: '#ffffff'
critical-bg: '#b84256'
critical-text: '#ffffff'
modern-dark:
name: Modern Dark
accents:
primary-accent: 'logo'
secondary-accent: 'update'
tertiary-accent: 'critical'
colors:
logo-bg: '#3494cf'
logo-link: '#ffffff'
nav-bg: '#354052'
nav-text: '#8c98ad'
nav-link: '#b4bed6'
nav-selected-bg: '#30394A'
nav-selected-link: '#ffffff'
nav-hover-bg: '#3c485e'
nav-hover-link: '#ffffff'
toolbar-bg: '#2c79b0'
toolbar-text: '#ffffff'
page-bg: '#30394A'
page-text: '#8c98ad'
page-link: '#ffffff'
content-bg: '#354052'
content-text: '#8c98ad'
content-link: '#3EA7E6'
content-link2: '#3cb8ba'
content-header: '#b4bed6'
content-tabs-bg: '#4a5873'
content-tabs-text: '#d1ddf0'
button-bg: '#3EA7E6'
button-text: '#ffffff'
notice-bg: '#3EA7E6'
notice-text: '#ffffff'
update-bg: '#A652AB'
update-text: '#ffffff'
critical-bg: '#b84256'
critical-text: '#ffffff'
modern-lite:
name: Modern Light
accents:
primary-accent: 'update'
secondary-accent: 'logo'
tertiary-accent: 'critical'
colors:
logo-bg: '#3696d1'
logo-link: '#ffffff'
nav-bg: '#ffffff'
nav-text: '#70798a'
nav-link: '#5b6270'
nav-selected-bg: '#edf2f6'
nav-selected-link: '#4b505c'
nav-hover-bg: '#cadbeb'
nav-hover-link: '#5b6270'
toolbar-bg: '#ffffff'
toolbar-text: '#636468'
page-bg: '#EDF2F6'
page-text: '#929699'
page-link: '#3EA7E6'
content-bg: '#ffffff'
content-text: '#787b7d'
content-link: '#3EA7E6'
content-link2: '#3cb8ba'
content-header: '#636468'
content-tabs-bg: '#f0f0f0'
content-tabs-text: '#929699'
button-bg: '#3EA7E6'
button-text: '#ffffff'
notice-bg: '#f24d77'
notice-text: '#ffffff'
update-bg: '#3cb8ba'
update-text: '#ffffff'
critical-bg: '#f45857'
critical-text: '#ffffff'
one-dark:
name: One Dark
accents:
primary-accent: nav
secondary-accent: nav
tertiary-accent: tabs
colors:
logo-bg: '#2A2C33'
logo-link: '#d8dadf'
nav-bg: '#333842'
nav-text: '#858991'
nav-link: '#CED0D4'
nav-selected-bg: '#282c34'
nav-selected-link: '#d8dadf'
nav-hover-bg: '#3A404B'
nav-hover-link: '#d8dadf'
toolbar-bg: '#2A2C33'
toolbar-text: '#9ea5b3'
page-bg: '#1E2127'
page-text: '#ABB2BF'
page-link: '#61AFEE'
content-bg: '#282C34'
content-text: '#abb2bf'
content-link: '#61AFEE'
content-link2: '#b960d4'
content-header: '#727C8C'
content-tabs-bg: '#25272E'
content-tabs-text: '#9ea5b3'
button-bg: '#383F4C'
button-text: '#d8dadf'
notice-bg: '#18a4ba'
notice-text: '#dfe3ed'
update-bg: '#4fa814'
update-text: '#dfe3ed'
critical-bg: '#d13e46'
critical-text: '#dfe3ed'
firewatch-light:
name: 'Firewatch Light'
colors:
logo-bg: '#f3efe7'
logo-link: '#075289'
nav-bg: '#f3efe7'
nav-text: '#6f6a5e'
nav-link: '#075289'
nav-selected-bg: '#eeeae3'
nav-selected-link: '#06497a'
nav-hover-bg: '#FAF8F5'
nav-hover-link: '#075289'
toolbar-bg: '#f3efe7'
toolbar-text: '#6f6a5e'
page-bg: '#e8e5dd'
page-text: '#6f6a5e'
page-link: '#718ECD'
content-bg: '#f3efe7'
content-text: '#6f6a5e'
content-link: '#718ecd'
content-link2: '#896723'
content-header: '#635e53'
content-tabs-bg: '#D6D3CC'
content-tabs-text: '#6f6a5e'
button-bg: '#075289'
button-text: '#faf8f5'
notice-bg: '#718ECD'
notice-text: '#faf8f5'
update-bg: '#DD672C'
update-text: '#faf8f5'
critical-bg: '#B0555C'
critical-text: '#faf8f5'
accents:
primary-accent: notice
secondary-accent: button
tertiary-accent: critical
remedy:
name: Remedy
accents:
primary-accent: update
secondary-accent: notice
tertiary-accent: tabs
colors:
logo-bg: '#282726'
logo-link: '#f9e7c4'
nav-bg: '#2C2B2A'
nav-text: '#938977'
nav-link: '#938977'
nav-selected-bg: '#272625'
nav-selected-link: '#f9e7c4'
nav-hover-bg: '#333130'
nav-hover-link: '#f9e7c4'
toolbar-bg: '#282726'
toolbar-text: '#f9e7c4'
page-bg: '#272625'
page-text: '#f9e7c4'
page-link: '#B294BB'
content-bg: '#353433'
content-text: '#F9E7C4'
content-link: '#b294bb'
content-link2: '#81a2be'
content-header: '#F9E7C4'
content-tabs-bg: '#272625'
content-tabs-text: '#938977'
button-bg: '#5a7185'
button-text: '#f9e7c4'
notice-bg: '#5a7185'
notice-text: '#f9e7c4'
update-bg: '#83638c'
update-text: '#f9e7c4'
critical-bg: '#943136'
critical-text: '#f9e7c4'
ocean:
name: Ocean
accents:
primary-accent: update
secondary-accent: button
tertiary-accent: notice
colors:
logo-bg: '#007FD4'
logo-link: '#ffffff'
nav-bg: '#007acc'
nav-text: '#B5D6F4'
nav-link: '#b5d6f4'
nav-selected-bg: '#006bbd'
nav-selected-link: '#c9e3ff'
nav-hover-bg: '#0072c9'
nav-hover-link: '#c9e3ff'
toolbar-bg: '#006bbd'
toolbar-text: '#c9e3ff'
page-bg: '#e9f3ff'
page-text: '#2a587a'
page-link: '#3EA7E6'
content-bg: '#f5faff'
content-text: '#2A587A'
content-link: '#006bbd'
content-link2: '#519173'
content-header: '#636468'
content-tabs-bg: '#eef6ff'
content-tabs-text: '#41627a'
button-bg: '#027FD4'
button-text: '#ffffff'
notice-bg: '#8e5b8f'
notice-text: '#ffffff'
update-bg: '#519173'
update-text: '#ffffff'
critical-bg: '#f45857'
critical-text: '#ffffff'
cobalt2:
name: Cobalt2
accents:
primary-accent: notice
secondary-accent: update
tertiary-accent: tabs
colors:
logo-bg: '#0e1d2b'
logo-link: '#ffffff'
nav-bg: '#122739'
nav-text: '#88929c'
nav-link: '#ffffff'
nav-selected-bg: '#0E1D2B'
nav-selected-link: '#ffc600'
nav-hover-bg: '#142B40'
nav-hover-link: '#ffffff'
toolbar-bg: '#122739'
toolbar-text: '#acb8c5'
page-bg: '#0e1d2b'
page-text: '#E1EFFE'
page-link: '#0088FF'
content-bg: '#193549'
content-text: '#E1EFFE'
content-link: '#0088ff'
content-link2: '#ffc600'
content-header: '#ffc600'
content-tabs-bg: '#173043'
content-tabs-text: '#acb8c5'
button-bg: '#0476e0'
button-text: '#ffffff'
notice-bg: '#0476e0'
notice-text: '#ffffff'
update-bg: '#ffc600'
update-text: '#0e1d2b'
critical-bg: '#d13e46'
critical-text: '#dfe3ed'

3
themes/grav/.babelrc Normal file
View File

@@ -0,0 +1,3 @@
{
"presets": [ "@babel/preset-env" ]
}

View File

@@ -1,4 +1,5 @@
{
"root": true,
"env": {
"browser": true,
"node": true

View File

@@ -34,13 +34,13 @@ export default class Cache {
enable() {
this.element
.removeAttr('disabled')
.find('> .fa').removeClass('fa-refresh fa-spin').addClass('fa-trash');
.find('> .fa').removeClass('fa-refresh fa-spin fa-retweet').addClass('fa-retweet');
}
disable() {
this.element
.attr('disabled', 'disabled')
.find('> .fa').removeClass('fa-trash').addClass('fa-refresh fa-spin');
.find('> .fa').removeClass('fa-retweet').addClass('fa-refresh fa-spin');
}
}

View File

@@ -0,0 +1,89 @@
import $ from 'jquery';
const body = $('body');
body.on('change', '[data-acl_picker] select', (event) => {
const target = $(event.currentTarget);
const value = target.val();
const item = target.closest('.permissions-item');
const inputs = item.find('input[type="checkbox"], input[type="radio"]');
const hidden = item.find('input[type="hidden"][name]');
const wrapper = target.closest('[data-acl_picker_id]');
const type = item.data('fieldType');
if (type === 'access') {
inputs.each((index, input) => {
input = $(input);
const name = input.prop('name');
input.prop('name', name.replace(/(.*)(\[[^\]]*\])/, `$1[${value}]`));
});
} else if (type === 'permissions') {
const crudpContainer = item.find('[data-field-name]');
inputs.each((index, input) => {
input = $(input);
const rand = Math.round(Math.random() * 500);
const name = crudpContainer.data('fieldName');
const id = input.prop('id').split('_').slice(0, -1).join('_') + `_${value}+${rand}`;
// const key = input.data('crudpKey');
hidden.prop('name', name.replace(/(.*)(\[[^\]]*\])/, `$1[${value}]`));
input.prop('id', id);
input.next('label').prop('for', id);
});
}
wrapper.find('.permissions-item .button.add-item')[!value ? 'addClass' : 'removeClass']('disabled').prop('disabled', !value ? 'disabled' : null);
});
body.on('input', 'input[data-crudp-key]', (event) => {
const target = $(event.currentTarget);
const container = target.closest('.crudp-container');
const hidden = container.find('input[type="hidden"][name]');
const key = target.data('crudpKey');
const json = JSON.parse(hidden.val() || '{}');
json[key] = target.val();
hidden.val(JSON.stringify(json));
});
body.on('click', '[data-acl_picker] .remove-item', (event) => {
event.preventDefault();
const target = $(event.currentTarget);
const container = target.closest('.permissions-item');
const wrapper = target.closest('[data-acl_picker_id]');
container.remove();
const empty = wrapper.find('.permissions-item').length === 1;
// show the initial + button
if (empty) {
wrapper.find('.permissions-item.empty-list').removeClass('hidden');
}
});
body.on('click', '[data-acl_picker] .add-item', (event) => {
event.preventDefault();
const target = $(event.currentTarget);
const item = target.closest('.permissions-item');
const wrapper = target.closest('[data-acl_picker_id]');
const ID = wrapper.data('acl_picker_id');
const template = document.querySelector(`template[data-id="acl_picker-${ID}"]`);
const clone = $(template.content.firstElementChild).clone();
clone.insertAfter(item);
// randomize ids
clone.find('.switch-toggle input[type="radio"]').each((index, input) => {
input = $(input);
const id = input.prop('id');
const label = input.next('label');
const rand = (Date.now().toString(36) + Math.random().toString(36).substr(2, 5)).toLowerCase();
input.prop('id', `${id}${rand}`);
label.prop('for', `${id}${rand}`);
});
// hide the initial + button
wrapper.find('.permissions-item.empty-list').addClass('hidden');
// disable all + buttons until one is selected
wrapper.find('.permissions-item .button.add-item').addClass('disabled').prop('disabled', 'disabled');
});

View File

@@ -87,7 +87,13 @@ export default class ArrayField {
container.data('array-sort', new Sortable(container.get(0), {
handle: '.fa-bars',
animation: 150
animation: 150,
onUpdate: () => {
const item = container.find('[data-grav-array-type="row"]:first');
this._setTemplate(item);
const template = item.data('array-template');
this.refreshNames(template);
}
}));
});
}
@@ -105,7 +111,13 @@ export default class ArrayField {
let escaped_name = !template.isValueOnly() ? keyElement.val() : this.getIndexFor(element);
escaped_name = escaped_name.toString().replace(/\[/g, '%5B').replace(/]/g, '%5D');
let name = `${template.getName()}[${escaped_name}]`;
valueElement.attr('name', !valueElement.val() ? template.getName() : name);
if (!template.isValueOnly() && (!keyElement.val() && !valueElement.val())) {
valueElement.attr('name', '');
} else {
// valueElement.attr('name', !valueElement.val() ? template.getName() : name);
valueElement.attr('name', name);
}
this.refreshNames(template);
}
@@ -154,8 +166,8 @@ export default class ArrayField {
inputs.each((index, input) => {
input = $(input);
let name = input.attr('name');
name = name.replace(/\[\d+\]$/, `[${index}]`);
const preserved_name = input.closest('[data-grav-array-name]');
const name = `${preserved_name.attr('data-grav-array-name')}[${index}]`;
input.attr('name', name);
});

View File

@@ -186,7 +186,7 @@ export default class CollectionsField {
// special case to preserve array field index keys
if (prop === 'name' && element.data('gravArrayType')) {
const match_index = element.attr(prop).match(/\[[0-9]{1,}\]$/);
const pattern = element.closest('[data-grav-array-name]').data('gravArrayName');
const pattern = element[0].closest('[data-grav-array-name]').dataset.gravArrayName;
if (match_index && pattern) {
array_index = match_index[0];
element.attr(prop, `${pattern}${match_index[0]}`);

View File

@@ -82,24 +82,22 @@ export default class ColorpickerField {
this.wrapper.addClass('cp-visible');
this.updateFromInput();
let mainContainer = $('#admin-main .content-wrapper').data('scrollbar').getViewElement();
this.wrapper.on(MOUSEDOWN, '.cp-grid, .cp-slider, .cp-opacity-slider', this.bound('bodyDown'));
body.on(MOUSEMOVE, this.bound('bodyMove'));
body.on(MOUSEDOWN, this.bound('bodyClick'));
body.on(MOUSEUP, this.bound('targetReset'));
$(mainContainer).on('scroll', this.bound('reposition'));
$('#admin-main > .content-wrapper').on('scroll', this.bound('reposition'));
}
hide() {
if (!this.built) { return; }
this.wrapper.removeClass('cp-visible');
let mainContainer = $('#admin-main .content-wrapper').data('scrollbar').getViewElement();
this.wrapper.undelegate(MOUSEDOWN, '.cp-grid, .cp-slider, .cp-opacity-slider', this.bound('bodyDown'));
body.off(MOUSEMOVE, this.bound('bodyMove'));
body.off(MOUSEDOWN, this.bound('bodyClick'));
body.off(MOUSEUP, this.bound('targetReset'));
$(mainContainer).off('scroll', this.bound('reposition'));
$('#admin-main > .content-wrapper').on('scroll', this.bound('reposition'));
}
build() {

View File

@@ -83,63 +83,3 @@ export default class CronField {
}
export let Instance = new CronField();
/*
// cron-selector
$(document).ready(function() {
$('.cron-selector').each(function(index, wrapper) {
wrapper = $(wrapper);
const input = wrapper.closest('[data-grav-field]').find('input');
wrapper.jqCron({
enabled_minute: true,
multiple_dom: true,
multiple_month: true,
multiple_mins: true,
multiple_dow: true,
multiple_time_hours: true,
multiple_time_minutes: true,
default_period: 'week',
default_value: input.val() || '3 * * * *',
no_reset_button: false,
bind_to: input,
bind_method: {
set: function($element, value) {
$element.val(value);
}
},
texts: {
en: {
empty: 'every',
empty_minutes: 'every',
empty_time_hours: 'every hour',
empty_time_minutes: 'every minute',
empty_day_of_week: 'every day of the week',
empty_day_of_month: 'every day of the month',
empty_month: 'every month',
name_minute: 'minute',
name_hour: 'hour',
name_day: 'day',
name_week: 'week',
name_month: 'month',
name_year: 'year',
text_period: 'Every <b />',
text_mins: ' at <b /> minute(s) past the hour',
text_time: ' at <b />:<b />',
text_dow: ' on <b />',
text_month: ' of <b />',
text_dom: ' on <b />',
error1: 'The tag %s is not supported !',
error2: 'Bad number of elements',
error3: 'The jquery_element should be set into jqCron settings',
error4: 'Unrecognized expression',
weekdays: ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'],
months: ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december']
}
}
});
});
});
*/

View File

@@ -1,6 +1,6 @@
import $ from 'jquery';
import { config } from 'grav-config';
import 'eonasdan-bootstrap-datetimepicker';
import '../../utils/bootstrap-datetimepicker';
export default class DateTimeField {

View File

@@ -118,8 +118,8 @@ export default class EditorField {
this.editors = this.editors.add(textarea);
if (theme && !~ThemesMap.indexOf(theme)) {
ThemesMap.push(theme);
let themeCSS = `https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.12.0/theme/${theme}.min.css`;
$('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', themeCSS));
// let themeCSS = `https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.12.0/theme/${theme}.min.css`;
// $('head').append($('<link rel="stylesheet" type="text/css" />').attr('href', themeCSS));
}
if (options.mode === 'yaml') {

View File

@@ -38,6 +38,8 @@ export default class FilePickerField {
let folder = '';
let thumbs = {};
let onDemand = field.closest('[data-ondemand]').length > 0;
if (!field.length || field.get(0).selectize) { return; }
let getData = function getData(field, callback, mode = 'all') {
@@ -118,7 +120,9 @@ export default class FilePickerField {
},
onInitialize: function() {
this.load((callback) => getData(field, (data) => callback(data), 'selected'));
if (!onDemand) {
this.load((callback) => getData(field, (data) => callback(data), 'selected'));
}
},
onLoad: function(/* data */) {

View File

@@ -1,6 +1,6 @@
import $ from 'jquery';
import Dropzone from 'dropzone';
import EXIF from 'exif-js';
// import EXIF from 'exif-js';
import request from '../../utils/request';
import { config, translations } from 'grav-config';
@@ -71,7 +71,7 @@ const DropzoneMediaConfig = {
</div>`.trim()
};
global.EXIF = EXIF;
// global.EXIF = EXIF;
const ACCEPT_FUNC = function(file, done, settings) {
const resolution = settings.resolution;
@@ -375,7 +375,7 @@ const addNode = (container) => {
instances.push(new FilesField({ container, options }));
};
export let Instances = (() => {
export let Instance = (() => {
$('.dropzone.files-upload').each((i, container) => addNode(container));
$('body').on('mutation._grav', onAddedNodes);

View File

@@ -0,0 +1,52 @@
import $ from 'jquery';
document.addEventListener('click', (event) => {
if (document.querySelector('#pages-filters')) {
return true;
}
const wrapper = event.target.closest('.checkboxes.indeterminate');
if (wrapper) {
event.preventDefault();
const checkbox = wrapper.querySelector('input[type="checkbox"]:not([disabled])');
const checkStatus = wrapper.dataset._checkStatus;
wrapper.classList.remove('status-checked', 'status-unchecked', 'status-indeterminate');
switch (checkStatus) {
// checked, going indeterminate
case '1':
wrapper.dataset._checkStatus = '2';
checkbox.indeterminate = true;
checkbox.checked = false;
checkbox.value = 0;
wrapper.classList.add('status-indeterminate');
break;
// indeterminate, going unchecked
case '2':
wrapper.dataset._checkStatus = '0';
checkbox.indeterminate = false;
checkbox.checked = false;
checkbox.value = '';
wrapper.classList.add('status-unchecked');
break;
// unchecked, going checked
case '0':
default:
wrapper.dataset._checkStatus = '1';
checkbox.indeterminate = false;
checkbox.checked = true;
checkbox.value = 1;
wrapper.classList.add('status-checked');
break;
}
// const input = new CustomEvent('input', { detail: { target: checkbox }});
// document.dispatchEvent(input);
$(checkbox).trigger('input');
}
});
(document.querySelectorAll('input[type="checkbox"][indeterminate="true"]') || []).forEach((input) => { input.indeterminate = true; });

View File

@@ -6,13 +6,17 @@ import DateTimeField, { Instance as DateTimeFieldInstance } from './datetime';
import EditorField, { Instance as EditorFieldInstance } from './editor';
import ColorpickerField, { Instance as ColorpickerFieldInstance } from './colorpicker';
import FilesField, { Instance as FilesFieldInstance } from './files';
import MediapickerField, { Instance as MediapickerInstance } from './mediapicker';
import MultilevelField, { Instance as MultilevelInstance } from './multilevel';
import SelectUniqueField, { Instance as SelectUniqueInstance } from './selectunique';
import IconpickerField, { Instance as IconpickerInstance } from './iconpicker';
import CronField, { Instance as CronFieldInstance } from './cron';
import TextField, { Instance as TextFieldInstance } from './text';
import ParentsField, { Instance as ParentsFieldInstance } from './parents';
import ParentsField, { Instances as ParentsFieldInstance } from './parents';
import './acl-picker';
import './permissions';
import './indeterminate';
import './mediapicker';
import './multilevel';
import './text';
export default {
FilepickerField: {
@@ -51,23 +55,13 @@ export default {
SelectUniqueField,
Instance: SelectUniqueInstance
},
MediapickerField: {
MediapickerField,
Instance: MediapickerInstance
},
MultilevelField: {
MultilevelField,
Instance: MultilevelInstance
},
IconpickerField: {
IconpickerField,
Instance: IconpickerInstance
},
CronField: {CronField,
Insance: CronFieldInstance
},
TextField: {TextField,
Insance: TextFieldInstance
CronField: {
CronField,
Instance: CronFieldInstance
},
ParentsField: {
ParentsField,

View File

@@ -1,30 +1,10 @@
import $ from 'jquery';
import Scrollbar from '../../utils/scrollbar';
import { Instance as pagesTree } from '../../pages/tree';
const queries = {
tree: '.pages-list-container .mediapicker-scroll',
thumb: '.thumbs-list-container .mediapicker-scroll'
};
$(function() {
let modal = '';
let body = $('body');
let treescrolls = [];
let thumbscrolls = [];
$(queries.tree).each((index, element) => {
treescrolls.push(new Scrollbar(element, { autoshow: true }));
});
$(queries.thumb).each((index, element) => {
thumbscrolls.push(new Scrollbar(element, { autoshow: true }));
});
// let treescroll = new Scrollbar('.pages-list-container .mediapicker-scroll', { autoshow: true });
// let thumbscroll = new Scrollbar('.thumbs-list-container .mediapicker-scroll', { autoshow: true });
// Thumb Resizer
$(document).on('input change', '.media-container .media-range', function(event) {
const target = $(event.currentTarget);
@@ -34,16 +14,6 @@ $(function() {
cards.each(function() {
$(this).css('width', width);
});
treescrolls.forEach((tree) => tree.update());
thumbscrolls.forEach((thumb) => thumb.update());
});
$(document).on('opened', '.remodal', function() {
setTimeout(function() {
treescrolls.forEach((tree) => tree.update());
thumbscrolls.forEach((thumb) => thumb.update());
}, 10);
});
body.on('click', '[data-mediapicker-modal-trigger]', function(event) {
@@ -57,20 +27,9 @@ $(function() {
modal = $.remodal.lookup[modal_element.data('remodal')];
}
$(queries.tree).filter((index, item) => !$(item).data('scrollbar')).each((index, item) => {
treescrolls.push(new Scrollbar(item, { autoshow: true }));
});
$(queries.thumb).filter((index, item) => !$(item).data('scrollbar')).each((index, item) => {
thumbscrolls.push(new Scrollbar(item, { autoshow: true }));
});
modal.open();
modal.dataField = element.find('input');
treescrolls.forEach((tree) => tree.update());
thumbscrolls.forEach((thumb) => thumb.update());
// load all media
modal_element.find('.js__files').trigger('fillView');

View File

@@ -5,7 +5,7 @@ import { config as gravConfig } from 'grav-config';
let XHRUUID = 0;
export const Instances = {};
export class Parents {
export default class Parents {
constructor(container, field, data) {
this.container = $(container);
this.fieldName = field.attr('name');

View File

@@ -0,0 +1,46 @@
import $ from 'jquery';
const body = $('body');
const radioSelector = '.permission-container.parent-section input[type="radio"]';
const handleParent = (event) => {
const target = $(event.currentTarget);
const value = target.val();
const container = target.closest('.parent-section');
const fieldset = container.next('fieldset');
const radios = fieldset.find(`input[type="radio"][value="${value}"]`);
if (container.data('isLocked') !== false) {
container.data('isUpdating', true);
radios.each((index, radio) => {
const ID = radio.id;
$(radio).siblings(`[for="${ID}"]`).trigger('click');
});
container.data('isUpdating', false);
}
};
const boundHandleParent = handleParent.bind(handleParent);
body.on('click', '.permission-container.parent-section label', (event) => {
const target = $(event.currentTarget);
const container = target.closest('.parent-section');
container.data('isLocked', true);
});
body.on('input', radioSelector, boundHandleParent);
body.on('input', '.permissions-container input[type="radio"][data-parent-id]', (event) => {
const target = $(event.currentTarget);
const parent = $(`[for="${target.data('parentId')}"]`);
const container = target.closest('fieldset').prev('.permission-container.parent-section');
if (container.data('isUpdating') === true) {
return true;
}
body.off('input', radioSelector, boundHandleParent);
container.data('isLocked', false);
parent.trigger('click');
body.on('input', radioSelector, boundHandleParent);
});

View File

@@ -30,6 +30,11 @@ export default class SelectizeField {
add(element) {
element = $(element);
if (element.closest('template').length) {
return false;
}
let tag = element.prop('tagName').toLowerCase();
let isInput = tag === 'input' || tag === 'select';
@@ -48,7 +53,10 @@ export default class SelectizeField {
}
_onAddedNodes(event, target/* , record, instance */) {
let fields = $(target).find('select.fancy, input.fancy, [data-grav-selectize]');
let fields = $(target).find('select.fancy, input.fancy, [data-grav-selectize]').filter((index, element) => {
return !$(element).closest('template').length;
});
if (!fields.length) { return; }
fields.each((index, field) => this.add(field));

View File

@@ -31,7 +31,7 @@ export default class Form {
_attachShortcuts() {
// CTRL + S / CMD + S - shortcut for [Save] when available
let saveTask = $('#titlebar [name="task"][value="save"]');
let saveTask = $('#titlebar [name="task"][value="save"][form="blueprints"]');
if (saveTask.length) {
$(global).on('keydown', function(event) {

View File

@@ -1,47 +1,44 @@
// polyfills
import '@babel/polyfill';
import $ from 'jquery';
import './utils/remodal';
import 'simplebar';
import GPM, { Instance as gpm } from './utils/gpm';
import KeepAlive from './utils/keepalive';
import Updates, { Instance as updates, Notifications, Feed } from './updates';
import Dashboard from './dashboard';
import Pages from './pages';
import Forms from './forms';
import Scrollbar, { Instance as contentScrollbar } from './utils/scrollbar';
import Cookies from './utils/cookies';
import './plugins';
import './themes';
import { Filter as MediaFilter, Instance as MediaFilterInstance} from './media';
import MediaFilter, { Instance as MediaFilterInstance} from './media';
import toastr from './utils/toastr';
import request from './utils/request';
import './utils/2fa';
import './tools';
import './whitelabel';
// bootstrap jQuery extensions
import './utils/bootstrap-transition';
import './utils/bootstrap-collapse';
import './utils/bootstrap-dropdown';
// tabs memory
import './utils/tabs-memory';
// changelog
import './utils/changelog';
// Main Sidebar
import Sidebar, { Instance as sidebar } from './utils/sidebar';
// starts the keep alive, auto runs every X seconds
KeepAlive.start();
// Sidebar auto-refresh
global.setInterval(() => {
contentScrollbar.update();
sidebar.scroller.update();
}, 150);
// global event to catch sidebar_state changes
$(global).on('sidebar_state._grav', () => {
/* Should not be needed since Gemini Scrollbar v1.4.0
* - Auto-update scrollbar on resize
*
* $('.admin-menu-wrapper').data('scrollbar').update();
* $('#admin-main .content-wrapper').data('scrollbar').update();
*/
Object.keys(Dashboard.Chart.Instances).forEach((chart) => {
setTimeout(() => Dashboard.Chart.Instances[chart].chart.update(), 10);
});
@@ -56,10 +53,6 @@ export default {
Dashboard,
Pages,
Forms,
Scrollbar: {
Scrollbar,
Instance: contentScrollbar
},
Updates: {
Updates,
Notifications,
@@ -74,5 +67,6 @@ export default {
MediaFilter,
Instance: MediaFilterInstance
},
Utils: { request, toastr }
Scrollbar: { Scrollbar: { deprecated: true }, Instance: { deprecated: true } },
Utils: { request, toastr, Cookies }
};

View File

@@ -166,14 +166,10 @@ $('body').on('click', '.js__reset-pages-filter', (event) => {
var enableInfiniteScrolling = function enableInfiniteScrolling() {
$('.spinning-wheel').hide();
var view = $('.mediapicker-scroll').last();
var gemini = view.data('scrollbar');
if (gemini) {
gemini = gemini.getViewElement();
}
if (!gemini || !gemini.length && !view.length) { return; }
if (!view.length) { return; }
$(gemini || view).on('scroll', function() {
$(view).on('scroll', function() {
if (($(this).scrollTop() + $(this).innerHeight() + 100) >= $(this)[0].scrollHeight) {
fillView();
}

View File

@@ -10,11 +10,11 @@ import '../utils/storage';
/* @formatter:off */
/* eslint-disable */
const options = [
{ flag: translations.PLUGIN_ADMIN.MODULAR, key: 'Modular', cat: 'mode' },
{ flag: translations.PLUGIN_ADMIN.MODULE, key: 'Module', cat: 'mode' },
{ flag: translations.PLUGIN_ADMIN.VISIBLE, key: 'Visible', cat: 'mode' },
{ flag: translations.PLUGIN_ADMIN.ROUTABLE, key: 'Routable', cat: 'mode' },
{ flag: translations.PLUGIN_ADMIN.PUBLISHED, key: 'Published', cat: 'mode' },
{ flag: translations.PLUGIN_ADMIN.NON_MODULAR, key: 'NonModular', cat: 'mode' },
{ flag: translations.PLUGIN_ADMIN.NON_MODULE, key: 'NonModule', cat: 'mode' },
{ flag: translations.PLUGIN_ADMIN.NON_VISIBLE, key: 'NonVisible', cat: 'mode' },
{ flag: translations.PLUGIN_ADMIN.NON_ROUTABLE, key: 'NonRoutable', cat: 'mode' },
{ flag: translations.PLUGIN_ADMIN.NON_PUBLISHED, key: 'NonPublished', cat: 'mode' }

View File

@@ -4,8 +4,8 @@ import request from '../../utils/request';
import { config } from 'grav-config';
let custom = false;
let folder = $('[data-remodal-id="modal"] input[name="data[folder]"], [data-remodal-id="modular"] input[name="data[folder]"]');
let title = $('[data-remodal-id="modal"] input[name="data[title]"], [data-remodal-id="modular"] input[name="data[title]"]');
let folder = $('[data-remodal-id="modal"] input[name="data[folder]"], [data-remodal-id="module"] input[name="data[folder]"]');
let title = $('[data-remodal-id="modal"] input[name="data[title]"], [data-remodal-id="module"] input[name="data[title]"]');
let getFields = (type, target) => {
target = $(target);
let query = `[data-remodal-id="${target.closest('[data-remodal-id]').data('remodal-id')}"]`;

View File

@@ -139,9 +139,9 @@ export default class PageMedia extends FilesField {
let file = target.parent('.dz-preview').find('.dz-filename');
let filename = encodeURI(file.text());
let URL = target.closest('[data-media-path]').data('media-path');
let original = this.dropzone.files.filter((file) => encodeURIComponent(file.name) === filename).shift();
let original = this.dropzone.files.filter((file) => encodeURI(file.name) === filename).shift();
original = original.extras.original || encodeURIComponent(original.name);
original = original && ((original.extras && original.extras.original) || encodeURI(original.name));
target.attr('href', `${URL}/${original}`);
});

View File

@@ -1,7 +1,7 @@
import $ from 'jquery';
$('[data-page-move] button[name="task"][value="save"]').on('click', function() {
let route = $('form#blueprints:first select[name="data[route]"]');
$(document).on('click', '[data-page-move] button[name="task"][value="save"]', (event) => {
/* let route = $('form#blueprints:first select[name="data[route]"]');
let moveTo = $('[data-page-move] select').val();
if (route.length && route.val() !== moveTo) {
@@ -9,5 +9,55 @@ $('[data-page-move] button[name="task"][value="save"]').on('click', function() {
route.val(moveTo);
if (selectize) selectize.setValue(moveTo);
}
}*/
const modal = $(event.currentTarget).closest('[data-remodal-id]');
const parents = modal.data('parents') || {};
const finder = parents.finder;
if (!parents || !finder) { return true; }
const field = parents.field;
const parentLabel = parents.parentLabel;
const parentName = parents.parentName;
const selection = finder.findLastActive().item[0];
const value = selection._item[finder.config.valueKey];
const name = selection._item[finder.config.labelKey];
field.val(value);
parentLabel.text(value);
parentName.text(name);
finder.config.defaultPath = value;
$('<div />').css({
backgroundColor: 'rgba(255, 255, 255, 0.1)',
position: 'fixed',
top: 0,
left: 0,
width: '100vw',
height: '100vh',
zIndex: 15000
}).appendTo($('body'));
});
/*
$(document).on('click', '[data-remodal-id="parents"] [data-parents-select]', (event) => {
const modal = $(event.currentTarget).closest('[data-remodal-id]');
const parents = modal.data('parents');
const finder = parents.finder;
const field = parents.field;
const parentLabel = parents.parentLabel;
const parentName = parents.parentName;
const selection = finder.findLastActive().item[0];
const value = selection._item[finder.config.valueKey];
const name = selection._item[finder.config.labelKey];
field.val(value);
parentLabel.text(value);
parentName.text(name);
finder.config.defaultPath = value;
const remodal = $.remodal.lookup[$(`[data-remodal-id="${modal.data('remodalId')}"]`).data('remodal')];
remodal.close();
});
*/

View File

@@ -64,11 +64,6 @@ export default class PagesTree {
}
});
const scroller = elements.closest('.mediapicker-scroll');
if (scroller.length && scroller.data('scrollbar')) {
scroller.data('scrollbar').update();
}
if (!dontStore) { this.save(); }
}
@@ -99,11 +94,6 @@ export default class PagesTree {
}
});
const scroller = elements.closest('.mediapicker-scroll');
if (scroller.length && scroller.data('scrollbar')) {
scroller.data('scrollbar').update();
}
if (!dontStore) { this.save(); }
}

View File

@@ -1,9 +1,9 @@
import $ from 'jquery';
import unique from 'mout/array/unique';
import { config, translations } from 'grav-config';
import formatBytes from '../utils/formatbytes';
import { Instance as gpm } from '../utils/gpm';
import Notifications from './notifications';
import Feed from './feed';
import './check';
import './update';
@@ -13,6 +13,7 @@ export default class Updates {
constructor(payload = {}) {
this.setPayload(payload);
this.task = `task${config.param_sep}`;
this.updateURL = '';
}
setPayload(payload = {}) {
@@ -47,7 +48,8 @@ export default class Updates {
let bar = '';
if (!payload.isSymlink) {
bar += `<button data-maintenance-update="${config.base_url_relative}/update.json/${task}updategrav/admin-nonce${config.param_sep}${config.admin_nonce}" class="button button-small secondary" id="grav-update-button">${translations.PLUGIN_ADMIN.UPDATE_GRAV_NOW}</button>`;
this.updateURL = `${config.base_url_relative}/update.json/${task}updategrav/admin-nonce${config.param_sep}${config.admin_nonce}`;
bar += `<button data-remodal-target="update-grav" class="button button-small secondary pointer-events-none" id="grav-update-button">${translations.PLUGIN_ADMIN.UPDATE_GRAV_NOW} <span class="cnt-down">(5s)</span></button>`;
} else {
bar += `<span class="hint--left" style="float: right;" data-hint="${translations.PLUGIN_ADMIN.GRAV_SYMBOLICALLY_LINKED}"><i class="fa fa-fw fa-link"></i></span>`;
}
@@ -65,14 +67,22 @@ export default class Updates {
element
.addClass('grav')
.html(`${bar}`)
.slideDown(150)
.slideDown(150, function() {
var c = 5;
var x = setInterval(function() {
c -= 1;
element.find('.pointer-events-none .cnt-down').text('(' + c + 's)');
}, 1000);
setTimeout(function() {
clearInterval(x);
element.find('.pointer-events-none .cnt-down').remove();
element.find('.pointer-events-none').removeClass('pointer-events-none');
}, 5000);
})
.parent('#messages').addClass('default-box-shadow');
}
$('#grav-update-button').on('click', function() {
$(this).html(`${translations.PLUGIN_ADMIN.UPDATING_PLEASE_WAIT} ${formatBytes(payload.assets['grav-update'].size)}..`);
});
return this;
}

View File

@@ -138,7 +138,7 @@ export default notifications;
if (canFetchNotifications()) {
notifications.fetch();
/* Hide a notification and store it hidden
/* Hide a notification and store it hidden */
// <a href="#" data-notification-action="hide-notification" data-notification-id="${notification.id}" class="close hide-notification"><i class="fa fa-close"></i></a>
$(document).on('click', '[data-notification-action="hide-notification"]', (event) => {
let notification_id = $(event.target).parents('.hide-notification').data('notification-id');
@@ -149,7 +149,6 @@ if (canFetchNotifications()) {
$(event.target).parents('.single-notification').hide();
});
*/
$(document).on('click', '[data-notification-action="hide-notification"]', (event) => {
const target = $(event.currentTarget);

View File

@@ -1,14 +1,17 @@
import $ from 'jquery';
import { translations } from 'grav-config';
import formatBytes from '../utils/formatbytes';
import request from '../utils/request';
import { Instance as Update } from './index';
// Dashboard update and Grav update
$('body').on('click', '[data-maintenance-update]', function() {
let element = $(this);
let url = element.data('maintenanceUpdate');
$(document).on('click.remodal', '[data-remodal-id="update-grav"] [data-remodal-action="confirm"]', () => {
const element = $('#grav-update-button');
element.html(`${translations.PLUGIN_ADMIN.UPDATING_PLEASE_WAIT} ${formatBytes(Update.payload.grav.assets['grav-update'].size)}..`);
element.attr('disabled', 'disabled').find('> .fa').removeClass('fa-cloud-download').addClass('fa-refresh fa-spin');
request(url, (response) => {
request(Update.updateURL, (response) => {
if (response.type === 'updategrav') {
$('[data-gpm-grav]').remove();
$('#footer .grav-version').html(response.version);

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,169 @@
import jQuery from 'jquery';
/* ========================================================================
* Bootstrap: dropdown.js v3.4.1
* https://getbootstrap.com/docs/3.4/javascript/#dropdowns
* ========================================================================
* Copyright 2011-2019 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/v3-dev/LICENSE)
* ======================================================================== */
+(function($) {
'use strict';
// DROPDOWN CLASS DEFINITION
// =========================
const backdrop = '.dropdown-backdrop';
const toggle = '[data-toggle="dropdown"]';
const Dropdown = function(element) {
$(element).on('click.bs.dropdown', this.toggle);
};
Dropdown.VERSION = '3.4.1';
function getParent($this) {
let selector = $this.attr('data-target');
if (!selector) {
selector = $this.attr('href');
selector = selector && /#[A-Za-z]/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, ''); // strip for ie7
}
const $parent = selector !== '#' ? $(document).find(selector) : null;
return $parent && $parent.length ? $parent : $this.parent();
}
function clearMenus(e) {
if (e && e.which === 3) { return; }
$(backdrop).remove();
$(toggle).each(function() {
const $this = $(this);
const $parent = getParent($this);
const relatedTarget = { relatedTarget: this };
if (!$parent.hasClass('open')) { return; }
if (e && e.type === 'click' && /input|textarea/i.test(e.target.tagName) && $.contains($parent[0], e.target)) { return; }
$parent.trigger(e = $.Event('hide.bs.dropdown', relatedTarget));
if (e.isDefaultPrevented()) { return; }
$this.attr('aria-expanded', 'false');
$parent.removeClass('open').trigger($.Event('hidden.bs.dropdown', relatedTarget));
});
}
Dropdown.prototype.toggle = function(e) {
const $this = $(this);
if ($this.is('.disabled, :disabled')) { return; }
const $parent = getParent($this);
const isActive = $parent.hasClass('open');
clearMenus();
if (!isActive) {
if ('ontouchstart' in document.documentElement && !$parent.closest('.navbar-nav').length) {
// if mobile we use a backdrop because click events don't delegate
$(document.createElement('div'))
.addClass('dropdown-backdrop')
.insertAfter($(this))
.on('click', clearMenus);
}
const relatedTarget = { relatedTarget: this };
$parent.trigger(e = $.Event('show.bs.dropdown', relatedTarget));
if (e.isDefaultPrevented()) { return; }
$this
.trigger('focus')
.attr('aria-expanded', 'true');
$parent
.toggleClass('open')
.trigger($.Event('shown.bs.dropdown', relatedTarget));
}
return false;
};
Dropdown.prototype.keydown = function(e) {
if (!/(38|40|27|32)/.test(e.which) || /input|textarea/i.test(e.target.tagName)) return;
const $this = $(this);
e.preventDefault();
e.stopPropagation();
if ($this.is('.disabled, :disabled')) {
return;
}
const $parent = getParent($this);
const isActive = $parent.hasClass('open');
if (!isActive && e.which !== 27 || isActive && e.which === 27) {
if (e.which === 27) {
$parent.find(toggle).trigger('focus');
}
return $this.trigger('click');
}
const desc = ' li:not(.disabled):visible a';
const $items = $parent.find('.dropdown-menu' + desc);
if (!$items.length) {
return;
}
let index = $items.index(e.target);
if (e.which === 38 && index > 0) { index--; } // up
if (e.which === 40 && index < $items.length - 1) { index++; } // down
if (!~index) { index = 0; }
$items.eq(index).trigger('focus');
};
// DROPDOWN PLUGIN DEFINITION
// ==========================
function Plugin(option) {
return this.each(function() {
const $this = $(this);
let data = $this.data('bs.dropdown');
if (!data) $this.data('bs.dropdown', (data = new Dropdown(this)));
if (typeof option === 'string') data[option].call($this);
});
}
const old = $.fn.dropdown;
$.fn.dropdown = Plugin;
$.fn.dropdown.Constructor = Dropdown;
// DROPDOWN NO CONFLICT
// ====================
$.fn.dropdown.noConflict = function() {
$.fn.dropdown = old;
return this;
};
// APPLY TO STANDARD DROPDOWN ELEMENTS
// ===================================
$(document)
.on('click.bs.dropdown.data-api', clearMenus)
.on('click.bs.dropdown.data-api', '.dropdown form', function(e) { e.stopPropagation(); })
.on('click.bs.dropdown.data-api', toggle, Dropdown.prototype.toggle)
.on('keydown.bs.dropdown.data-api', toggle, Dropdown.prototype.keydown)
.on('keydown.bs.dropdown.data-api', '.dropdown-menu', Dropdown.prototype.keydown);
}(jQuery));

View File

@@ -0,0 +1,25 @@
/* eslint-disable */
import $ from 'jquery';
let TRIGGER = null;
$(document).on('click', '[data-remodal-changelog]', (event) => {
TRIGGER = event.currentTarget;
});
$(document).on('opened', '[data-remodal-id="changelog"]', () => {
const instance = $.remodal.lookup[$('[data-remodal-id=changelog]').data('remodal')];
instance.$modal.html('<div class="changelog-overflow center" style="padding:5rem 0;text-align:center;"><i class="fa fa-spinner fa-spin fa-3x fa-fw"></i></div>');
if (!TRIGGER) { return true; }
const url = $(TRIGGER).data('remodalChangelog');
$.ajax({url: url}).done(function(data) {
instance.$modal.html(data);
});
});
$(document).on('closed', '[data-remodal-id="changelog"]', () => {
const instance = $.remodal.lookup[$('[data-remodal-id=changelog]').data('remodal')];
instance.$modal.html('');
});

View File

@@ -0,0 +1,165 @@
/*
* Cookies.js - 1.2.3-grav
* https://github.com/ScottHamper/Cookies
*
* With SameSite support by Grav
*
* This is free and unencumbered software released into the public domain.
*/
const factory = function(window) {
if (typeof window.document !== 'object') {
throw new Error('Cookies.js requires a `window` with a `document` object');
}
const Cookies = (key, value, options) => {
alert('a');
return arguments.length === 1
? Cookies.get(key)
: Cookies.set(key, value, options);
};
// Allows for setter injection in unit tests
Cookies._document = window.document;
// Used to ensure cookie keys do not collide with
// built-in `Object` properties
Cookies._cacheKeyPrefix = 'cookey.'; // Hurr hurr, :)
Cookies._maxExpireDate = new Date('Fri, 31 Dec 9999 23:59:59 UTC');
Cookies.defaults = {
path: '/',
secure: false,
sameSite: 'Lax'
};
Cookies.get = (key) => {
if (Cookies._cachedDocumentCookie !== Cookies._document.cookie) {
Cookies._renewCache();
}
const value = Cookies._cache[Cookies._cacheKeyPrefix + key];
return value === undefined ? undefined : decodeURIComponent(value);
};
Cookies.set = (key, value, options) => {
options = Cookies._getExtendedOptions(options);
options.expires = Cookies._getExpiresDate(value === undefined ? -1 : options.expires);
Cookies._document.cookie = Cookies._generateCookieString(key, value, options);
return Cookies;
};
Cookies.expire = (key, options) => {
return Cookies.set(key, undefined, options);
};
Cookies._getExtendedOptions = (options) => {
return {
path: options && options.path || Cookies.defaults.path,
domain: options && options.domain || Cookies.defaults.domain,
expires: options && options.expires || Cookies.defaults.expires,
secure: options && options.secure !== undefined ? options.secure : Cookies.defaults.secure,
sameSite: options && options.sameSite || Cookies.defaults.sameSite
};
};
Cookies._isValidDate = (date) => {
return Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date.getTime());
};
Cookies._getExpiresDate = (expires, now) => {
now = now || new Date();
if (typeof expires === 'number') {
expires = expires === Infinity
? Cookies._maxExpireDate
: new Date(now.getTime() + expires * 1000);
} else if (typeof expires === 'string') {
expires = new Date(expires);
}
if (expires && !Cookies._isValidDate(expires)) {
throw new Error('`expires` parameter cannot be converted to a valid Date instance');
}
return expires;
};
Cookies._generateCookieString = (key, value, options) => {
key = key.replace(/[^#$&+\^`|]/g, encodeURIComponent);
key = key.replace(/\(/g, '%28').replace(/\)/g, '%29');
value = (value + '').replace(/[^!#$&-+\--:<-\[\]-~]/g, encodeURIComponent);
options = options || {};
let cookieString = key + '=' + value;
cookieString += options.path ? ';path=' + options.path : '';
cookieString += options.domain ? ';domain=' + options.domain : '';
cookieString += options.expires ? ';expires=' + options.expires.toUTCString() : '';
cookieString += options.secure ? ';secure' : '';
cookieString += options.sameSite ? ';SameSite=' + options.sameSite : '';
return cookieString;
};
Cookies._getCacheFromString = (documentCookie) => {
let cookieCache = {};
const cookiesArray = documentCookie ? documentCookie.split('; ') : [];
for (let i = 0; i < cookiesArray.length; i++) {
const cookieKvp = Cookies._getKeyValuePairFromCookieString(cookiesArray[i]);
if (cookieCache[Cookies._cacheKeyPrefix + cookieKvp.key] === undefined) {
cookieCache[Cookies._cacheKeyPrefix + cookieKvp.key] = cookieKvp.value;
}
}
return cookieCache;
};
Cookies._getKeyValuePairFromCookieString = (cookieString) => {
// "=" is a valid character in a cookie value according to RFC6265, so cannot `split('=')`
let separatorIndex = cookieString.indexOf('=');
// IE omits the "=" when the cookie value is an empty string
separatorIndex = separatorIndex < 0 ? cookieString.length : separatorIndex;
const key = cookieString.substr(0, separatorIndex);
let decodedKey;
try {
decodedKey = decodeURIComponent(key);
} catch (e) {
if (console && typeof console.error === 'function') {
console.error('Could not decode cookie with key "' + key + '"', e);
}
}
return {
key: decodedKey,
value: cookieString.substr(separatorIndex + 1) // Defer decoding value until accessed
};
};
Cookies._renewCache = () => {
Cookies._cache = Cookies._getCacheFromString(Cookies._document.cookie);
Cookies._cachedDocumentCookie = Cookies._document.cookie;
};
Cookies._areEnabled = () => {
const testKey = 'cookies.js';
const areEnabled = Cookies.set(testKey, 1).get(testKey) === '1';
Cookies.expire(testKey);
return areEnabled;
};
Cookies.enabled = Cookies._areEnabled();
return Cookies;
};
global.Cookies = (global && typeof global.document === 'object') ? factory(global) : factory;
export default global.Cookies;

View File

@@ -1,6 +1,6 @@
import { parseJSON, parseStatus, userFeedbackError } from './response';
import { config } from 'grav-config';
import { EventEmitter } from 'events';
import EventEmitter from 'events';
export default class GPM extends EventEmitter {
constructor(action = 'getUpdates') {

View File

@@ -1,6 +1,8 @@
import { config } from 'grav-config';
import { userFeedbackError } from './response';
const MAX_SAFE_DELAY = 2147483647;
class KeepAlive {
constructor() {
this.active = false;
@@ -8,7 +10,7 @@ class KeepAlive {
start() {
let timeout = config.admin_timeout / 1.5 * 1000;
this.timer = setInterval(() => this.fetch(), timeout);
this.timer = setInterval(() => this.fetch(), Math.min(timeout, MAX_SAFE_DELAY));
this.active = true;
}

View File

@@ -0,0 +1,820 @@
/* Remodal from https://github.com/vodkabears/Remodal
* With Stackable option from https://github.com/antstorm/Remodal patch
*/
import $ from 'jquery';
!(function(root, factory) {
return factory(root, $);
})(this, function(global, $) {
'use strict';
/**
* Name of the plugin
* @private
* @const
* @type {String}
*/
var PLUGIN_NAME = 'remodal';
/**
* Namespace for CSS and events
* @private
* @const
* @type {String}
*/
var NAMESPACE = window.REMODAL_GLOBALS && window.REMODAL_GLOBALS.NAMESPACE || PLUGIN_NAME;
/**
* Animationstart event with vendor prefixes
* @private
* @const
* @type {String}
*/
var ANIMATIONSTART_EVENTS = $.map(
['animationstart', 'webkitAnimationStart', 'MSAnimationStart', 'oAnimationStart'],
function(eventName) {
return eventName + '.' + NAMESPACE;
}
).join(' ');
/**
* Animationend event with vendor prefixes
* @private
* @const
* @type {String}
*/
var ANIMATIONEND_EVENTS = $.map(
['animationend', 'webkitAnimationEnd', 'MSAnimationEnd', 'oAnimationEnd'],
function(eventName) {
return eventName + '.' + NAMESPACE;
}
).join(' ');
/**
* Default settings
* @private
* @const
* @type {Object}
*/
var DEFAULTS = $.extend({
hashTracking: true,
closeOnConfirm: true,
closeOnCancel: true,
closeOnEscape: true,
closeOnOutsideClick: true,
modifier: '',
stack: false,
appendTo: null
}, window.REMODAL_GLOBALS && window.REMODAL_GLOBALS.DEFAULTS);
/**
* States of the Remodal
* @private
* @const
* @enum {String}
*/
var STATES = {
CLOSING: 'closing',
CLOSED: 'closed',
OPENING: 'opening',
OPENED: 'opened'
};
/**
* Reasons of the state change.
* @private
* @const
* @enum {String}
*/
var STATE_CHANGE_REASONS = {
CONFIRMATION: 'confirmation',
CANCELLATION: 'cancellation'
};
/**
* Is animation supported?
* @private
* @const
* @type {Boolean}
*/
var IS_ANIMATION = (function() {
var style = document.createElement('div').style;
return style.animationName !== undefined ||
style.WebkitAnimationName !== undefined ||
style.MozAnimationName !== undefined ||
style.msAnimationName !== undefined ||
style.OAnimationName !== undefined;
})();
/**
* Is iOS?
* @private
* @const
* @type {Boolean}
*/
var IS_IOS = /iPad|iPhone|iPod/.test(navigator.platform);
/**
* Current modal
* @private
* @type {Remodal}
*/
var openModals = [];
/**
* Scrollbar position
* @private
* @type {Number}
*/
var scrollTop;
/**
* Returns an animation duration
* @private
* @param {jQuery} $elem
* @returns {Number}
*/
function getAnimationDuration($elem) {
if (
IS_ANIMATION &&
$elem.css('animation-name') === 'none' &&
$elem.css('-webkit-animation-name') === 'none' &&
$elem.css('-moz-animation-name') === 'none' &&
$elem.css('-o-animation-name') === 'none' &&
$elem.css('-ms-animation-name') === 'none'
) {
return 0;
}
var duration = $elem.css('animation-duration') ||
$elem.css('-webkit-animation-duration') ||
$elem.css('-moz-animation-duration') ||
$elem.css('-o-animation-duration') ||
$elem.css('-ms-animation-duration') ||
'0s';
var delay = $elem.css('animation-delay') ||
$elem.css('-webkit-animation-delay') ||
$elem.css('-moz-animation-delay') ||
$elem.css('-o-animation-delay') ||
$elem.css('-ms-animation-delay') ||
'0s';
var iterationCount = $elem.css('animation-iteration-count') ||
$elem.css('-webkit-animation-iteration-count') ||
$elem.css('-moz-animation-iteration-count') ||
$elem.css('-o-animation-iteration-count') ||
$elem.css('-ms-animation-iteration-count') ||
'1';
var max;
var len;
var num;
var i;
duration = duration.split(', ');
delay = delay.split(', ');
iterationCount = iterationCount.split(', ');
// The 'duration' size is the same as the 'delay' size
for (i = 0, len = duration.length, max = Number.NEGATIVE_INFINITY; i < len; i++) {
num = parseFloat(duration[i]) * parseInt(iterationCount[i], 10) + parseFloat(delay[i]);
if (num > max) {
max = num;
}
}
return max;
}
/**
* Returns a scrollbar width
* @private
* @returns {Number}
*/
function getScrollbarWidth() {
if ($(document).height() <= $(window).height()) {
return 0;
}
var outer = document.createElement('div');
var inner = document.createElement('div');
var widthNoScroll;
var widthWithScroll;
outer.style.visibility = 'hidden';
outer.style.width = '100px';
document.body.appendChild(outer);
widthNoScroll = outer.offsetWidth;
// Force scrollbars
outer.style.overflow = 'scroll';
// Add inner div
inner.style.width = '100%';
outer.appendChild(inner);
widthWithScroll = inner.offsetWidth;
// Remove divs
outer.parentNode.removeChild(outer);
return widthNoScroll - widthWithScroll;
}
/**
* Locks the screen
* @private
*/
function lockScreen() {
if (IS_IOS) {
return;
}
var $html = $('html');
var lockedClass = namespacify('is-locked');
var paddingRight;
var $body;
if (!$html.hasClass(lockedClass)) {
$body = $(document.body);
// Zepto does not support '-=', '+=' in the `css` method
paddingRight = parseInt($body.css('padding-right'), 10) + getScrollbarWidth();
$body.css('padding-right', paddingRight + 'px');
$html.addClass(lockedClass);
}
}
/**
* Unlocks the screen
* @private
*/
function unlockScreen() {
if (IS_IOS) {
return;
}
var $html = $('html');
var lockedClass = namespacify('is-locked');
var paddingRight;
var $body;
if ($html.hasClass(lockedClass)) {
$body = $(document.body);
// Zepto does not support '-=', '+=' in the `css` method
paddingRight = parseInt($body.css('padding-right'), 10) - getScrollbarWidth();
$body.css('padding-right', paddingRight + 'px');
$html.removeClass(lockedClass);
}
}
/**
* Sets a state for an instance
* @private
* @param {Remodal} instance
* @param {STATES} state
* @param {Boolean} isSilent If true, Remodal does not trigger events
* @param {String} Reason of a state change.
*/
function setState(instance, state, isSilent, reason) {
var newState = namespacify('is', state);
var allStates = [namespacify('is', STATES.CLOSING),
namespacify('is', STATES.OPENING),
namespacify('is', STATES.CLOSED),
namespacify('is', STATES.OPENED)].join(' ');
instance.$bg
.removeClass(allStates)
.addClass(newState);
instance.$overlay
.removeClass(allStates)
.addClass(newState);
instance.$wrapper
.removeClass(allStates)
.addClass(newState);
instance.$modal
.removeClass(allStates)
.addClass(newState);
instance.state = state;
!isSilent && instance.$modal.trigger({
type: state,
reason: reason
}, [{ reason: reason }]);
}
/**
* Synchronizes with the animation
* @param {Function} doBeforeAnimation
* @param {Function} doAfterAnimation
* @param {Remodal} instance
*/
function syncWithAnimation(doBeforeAnimation, doAfterAnimation, instance) {
var runningAnimationsCount = 0;
var handleAnimationStart = function(e) {
if (e.target !== this) {
return;
}
runningAnimationsCount++;
};
var handleAnimationEnd = function(e) {
if (e.target !== this) {
return;
}
if (--runningAnimationsCount === 0) {
// Remove event listeners
$.each(['$bg', '$overlay', '$wrapper', '$modal'], function(index, elemName) {
instance[elemName].off(ANIMATIONSTART_EVENTS + ' ' + ANIMATIONEND_EVENTS);
});
doAfterAnimation();
}
};
$.each(['$bg', '$overlay', '$wrapper', '$modal'], function(index, elemName) {
instance[elemName]
.on(ANIMATIONSTART_EVENTS, handleAnimationStart)
.on(ANIMATIONEND_EVENTS, handleAnimationEnd);
});
doBeforeAnimation();
// If the animation is not supported by a browser or its duration is 0
if (
getAnimationDuration(instance.$bg) === 0 &&
getAnimationDuration(instance.$overlay) === 0 &&
getAnimationDuration(instance.$wrapper) === 0 &&
getAnimationDuration(instance.$modal) === 0
) {
// Remove event listeners
$.each(['$bg', '$overlay', '$wrapper', '$modal'], function(index, elemName) {
instance[elemName].off(ANIMATIONSTART_EVENTS + ' ' + ANIMATIONEND_EVENTS);
});
doAfterAnimation();
}
}
/**
* Closes immediately
* @private
* @param {Remodal} instance
*/
function halt(instance) {
if (instance.state === STATES.CLOSED) {
return;
}
$.each(['$bg', '$overlay', '$wrapper', '$modal'], function(index, elemName) {
instance[elemName].off(ANIMATIONSTART_EVENTS + ' ' + ANIMATIONEND_EVENTS);
});
removeModal(instance);
instance.$bg.removeClass(instance.settings.modifier);
instance.$overlay.removeClass(instance.settings.modifier).hide();
instance.$wrapper.hide();
if (openModals.length === 0) {
unlockScreen();
}
setState(instance, STATES.CLOSED, true);
}
/**
* Parses a string with options
* @private
* @param str
* @returns {Object}
*/
function parseOptions(str) {
var obj = {};
var arr;
var len;
var val;
var i;
// Remove spaces before and after delimiters
str = str.replace(/\s*:\s*/g, ':').replace(/\s*,\s*/g, ',');
// Parse a string
arr = str.split(',');
for (i = 0, len = arr.length; i < len; i++) {
arr[i] = arr[i].split(':');
val = arr[i][1];
// Convert a string value if it is like a boolean
if (typeof val === 'string' || val instanceof String) {
val = val === 'true' || (val === 'false' ? false : val);
}
// Convert a string value if it is like a number
if (typeof val === 'string' || val instanceof String) {
val = !isNaN(val) ? +val : val;
}
obj[arr[i][0]] = val;
}
return obj;
}
/**
* Generates a string separated by dashes and prefixed with NAMESPACE
* @private
* @param {...String}
* @returns {String}
*/
function namespacify() {
var result = NAMESPACE;
for (var i = 0; i < arguments.length; ++i) {
result += '-' + arguments[i];
}
return result;
}
/**
* Handles the hashchange event
* @private
* @listens hashchange
*/
function handleHashChangeEvent() {
var id = location.hash.replace('#', '');
var instance;
var $elem;
var current = currentModal();
if (!id) {
// Check if we have currently opened modal and animation was completed
if (current && current.state === STATES.OPENED && current.settings.hashTracking) {
current.close();
}
} else {
if (!current || current.id !== id) {
// Catch syntax error if your hash is bad
try {
$elem = $(
'[data-' + PLUGIN_NAME + '-id="' + id + '"]'
);
} catch (err) {
}
if ($elem && $elem.length) {
instance = $[PLUGIN_NAME].lookup[$elem.data(PLUGIN_NAME)];
if (instance && instance.settings.hashTracking) {
instance.open();
}
}
}
}
}
function currentModal() {
return openModals[openModals.length - 1];
}
function removeModal(remodal) {
var index = openModals.indexOf(remodal);
if (index >= 0) {
openModals.slice(index, 1);
}
}
/**
* Remodal constructor
* @constructor
* @param {jQuery} $modal
* @param {Object} options
*/
function Remodal($modal, options) {
var $body = $(document.body);
var $appendTo = $body;
var remodal = this;
remodal.id = $modal.attr('data-' + PLUGIN_NAME + '-id');
remodal.settings = $.extend({}, DEFAULTS, options);
remodal.index = $[PLUGIN_NAME].lookup.push(remodal) - 1;
remodal.state = STATES.CLOSED;
// remodal.$overlay = $('.' + namespacify('overlay'));
if (remodal.settings.appendTo !== null && remodal.settings.appendTo.length) {
$appendTo = $(remodal.settings.appendTo);
}
if (!remodal.$overlay) {
remodal.$overlay = $('<div>').addClass(namespacify('overlay') + ' ' + namespacify('is', STATES.CLOSED)).hide();
$appendTo.append(remodal.$overlay);
}
remodal.$bg = $('.' + namespacify('bg')).addClass(namespacify('is', STATES.CLOSED));
remodal.$modal = $modal
.addClass(
NAMESPACE + ' ' +
namespacify('is-initialized') + ' ' +
remodal.settings.modifier + ' ' +
namespacify('is', STATES.CLOSED))
.attr('tabindex', '-1');
remodal.$wrapper = $('<div>')
.addClass(
namespacify('wrapper') + ' ' +
remodal.settings.modifier + ' ' +
namespacify('is', STATES.CLOSED))
.hide()
.append(remodal.$modal);
$appendTo.append(remodal.$wrapper);
// Add the event listener for the close button
remodal.$wrapper.on('click.' + NAMESPACE, '[data-' + PLUGIN_NAME + '-action="close"]', function(e) {
e.preventDefault();
remodal.close();
});
// Add the event listener for the cancel button
remodal.$wrapper.on('click.' + NAMESPACE, '[data-' + PLUGIN_NAME + '-action="cancel"]', function(e) {
e.preventDefault();
remodal.$modal.trigger(STATE_CHANGE_REASONS.CANCELLATION);
if (remodal.settings.closeOnCancel) {
remodal.close(STATE_CHANGE_REASONS.CANCELLATION);
}
});
// Add the event listener for the confirm button
remodal.$wrapper.on('click.' + NAMESPACE, '[data-' + PLUGIN_NAME + '-action="confirm"]', function(e) {
e.preventDefault();
remodal.$modal.trigger(STATE_CHANGE_REASONS.CONFIRMATION);
if (remodal.settings.closeOnConfirm) {
remodal.close(STATE_CHANGE_REASONS.CONFIRMATION);
}
});
// Add the event listener for the overlay
remodal.$wrapper.on('click.' + NAMESPACE, function(e) {
var $target = $(e.target);
var isWrapper = $target.hasClass(namespacify('wrapper'));
var isWithin = $target.closest('.' + namespacify('is', STATES.OPENED)).length;
if (!isWrapper && isWithin) {
return;
}
if (remodal.settings.closeOnOutsideClick) {
remodal.close();
}
});
}
/**
* Opens a modal window
* @public
*/
Remodal.prototype.open = function() {
var remodal = this;
var current;
var modalCount;
// Check if the animation was completed
if (remodal.state === STATES.OPENING || remodal.state === STATES.CLOSING) {
return;
}
// id = remodal.$modal.attr('data-' + PLUGIN_NAME + '-id');
if (remodal.id && remodal.settings.hashTracking) {
scrollTop = $(window).scrollTop();
location.hash = remodal.id;
}
if (!remodal.settings.stack) {
current = currentModal();
if (current && current !== remodal) {
halt(current);
}
}
modalCount = openModals.push(remodal);
remodal.$overlay.css('z-index', function(_, value) { return parseInt(value, 10) + modalCount; });
remodal.$wrapper.css('z-index', function(_, value) { return parseInt(value, 10) + modalCount; });
lockScreen();
remodal.$bg.addClass(remodal.settings.modifier);
remodal.$overlay.addClass(remodal.settings.modifier).show();
remodal.$wrapper.show().scrollTop(0);
remodal.$modal.focus();
syncWithAnimation(
function() {
setState(remodal, STATES.OPENING);
},
function() {
setState(remodal, STATES.OPENED);
},
remodal);
};
/**
* Closes a modal window
* @public
* @param {String} reason
*/
Remodal.prototype.close = function(reason) {
var remodal = this;
var current;
// Check if the animation was completed
if (remodal.state === STATES.OPENING || remodal.state === STATES.CLOSING || remodal.state === STATES.CLOSED) {
return;
}
removeModal(remodal);
if (
remodal.settings.hashTracking &&
remodal.id === location.hash.substr(1)
) {
current = currentModal();
if (current) {
location.hash = current.id;
} else {
location.hash = '';
$(window).scrollTop(scrollTop);
}
}
syncWithAnimation(
function() {
setState(remodal, STATES.CLOSING, false, reason);
},
function() {
remodal.$bg.removeClass(remodal.settings.modifier);
remodal.$overlay.removeClass(remodal.settings.modifier).hide();
remodal.$wrapper.hide();
if (openModals.length === 0) {
unlockScreen();
}
setState(remodal, STATES.CLOSED, false, reason);
},
remodal);
};
/**
* Returns a current state of a modal
* @public
* @returns {STATES}
*/
Remodal.prototype.getState = function() {
return this.state;
};
/**
* Destroys a modal
* @public
*/
Remodal.prototype.destroy = function() {
var lookup = $[PLUGIN_NAME].lookup;
var instanceCount;
halt(this);
this.$wrapper.remove();
delete lookup[this.index];
instanceCount = $.grep(lookup, function(instance) {
return !!instance;
}).length;
if (instanceCount === 0) {
this.$overlay.remove();
this.$bg.removeClass(
namespacify('is', STATES.CLOSING) + ' ' +
namespacify('is', STATES.OPENING) + ' ' +
namespacify('is', STATES.CLOSED) + ' ' +
namespacify('is', STATES.OPENED));
}
};
/**
* Special plugin object for instances
* @public
* @type {Object}
*/
$[PLUGIN_NAME] = {
lookup: []
};
/**
* Plugin constructor
* @constructor
* @param {Object} options
* @returns {JQuery}
*/
$.fn[PLUGIN_NAME] = function(opts) {
var instance;
var $elem;
this.each(function(index, elem) {
$elem = $(elem);
if ($elem.data(PLUGIN_NAME) == null) {
instance = new Remodal($elem, opts);
$elem.data(PLUGIN_NAME, instance.index);
if (
instance.settings.hashTracking &&
instance.id === location.hash.substr(1)
) {
instance.open();
}
} else {
instance = $[PLUGIN_NAME].lookup[$elem.data(PLUGIN_NAME)];
}
});
return instance;
};
$(document).ready(function() {
// data-remodal-target opens a modal window with the special Id
$(document).on('click', '[data-' + PLUGIN_NAME + '-target]', function(e) {
e.preventDefault();
var elem = e.currentTarget;
var id = elem.getAttribute('data-' + PLUGIN_NAME + '-target');
var $target = $('[data-' + PLUGIN_NAME + '-id="' + id + '"]');
$[PLUGIN_NAME].lookup[$target.data(PLUGIN_NAME)].open();
});
// Auto initialization of modal windows
// They should have the 'remodal' class attribute
// Also you can write the `data-remodal-options` attribute to pass params into the modal
$(document).find('.' + NAMESPACE).each(function(i, container) {
var $container = $(container);
var options = $container.data(PLUGIN_NAME + '-options');
if (!options) {
options = {};
} else if (typeof options === 'string' || options instanceof String) {
options = parseOptions(options);
}
$container[PLUGIN_NAME](options);
});
// Handles the keydown event
$(document).on('keydown.' + NAMESPACE, function(e) {
var current = currentModal();
if (current && current.settings.closeOnEscape && current.state === STATES.OPENED && e.keyCode === 27) {
current.close();
}
});
// Handles the hashchange event
$(window).on('hashchange.' + NAMESPACE, handleHashChangeEvent);
});
});

View File

@@ -1,40 +1 @@
import $ from 'jquery';
import GeminiScrollbar from 'gemini-scrollbar';
const defaults = {
autoshow: false,
createElements: true,
forceGemini: false
};
export default class Scrollbar {
constructor(element, options) {
this.element = $(element);
this.created = false;
if (!this.element.length) { return; }
this.options = Object.assign({}, defaults, options, { element: this.element[0] });
this.element.css('overflow', 'auto');
this.instance = new GeminiScrollbar(this.options);
this.create();
this.element.data('scrollbar', this.instance);
}
create() {
this.instance.create();
this.created = true;
}
update() {
if (!this.created) { return false; }
this.instance.update();
}
destroy() {
if (!this.created) { return false; }
this.instance.destroy();
}
}
export let Instance = new Scrollbar('#admin-main .content-wrapper');
import 'simplebar';

View File

@@ -1,6 +1,6 @@
import $ from 'jquery';
import Scrollbar from './scrollbar';
import Map from 'es6-map';
import Cookies from '../utils/cookies';
const MOBILE_BREAKPOINT = 48 - 0.062;
const DESKTOP_BREAKPOINT = 75 + 0.063;
@@ -17,7 +17,6 @@ export default class Sidebar {
this.isOpen = false;
this.body = $('body');
this.matchMedia = global.matchMedia(MOBILE_QUERY);
this.scroller = new Scrollbar('.admin-menu-wrapper', { autoshow: true });
this.enable();
}
@@ -79,7 +78,6 @@ export default class Sidebar {
if (event) { event.preventDefault(); }
let overlay = $('#overlay');
let sidebar = $('#admin-sidebar');
let scrollbar = $('#admin-menu').data('scrollbar');
this.body.addClass('sidebar-mobile-open');
overlay.css('display', 'block');
@@ -94,15 +92,12 @@ export default class Sidebar {
sidebar.css({ display: 'block', opacity: 1 });
this.isOpen = true;
}
if (scrollbar) { scrollbar.update(); }
}
close(event, quick = false) {
if (event) { event.preventDefault(); }
let overlay = $('#overlay');
let sidebar = $('#admin-sidebar');
let scrollbar = $('#admin-menu').data('scrollbar');
this.body.removeClass('sidebar-mobile-open');
overlay.css('display', 'none');
@@ -118,8 +113,6 @@ export default class Sidebar {
sidebar.css({ opacity: 0, display: 'none' });
this.isOpen = false;
}
if (scrollbar) { scrollbar.update(); }
}
toggle(event) {
@@ -131,6 +124,7 @@ export default class Sidebar {
if (event) { event.preventDefault(); }
clearTimeout(this.timeout);
let isDesktop = global.matchMedia(DESKTOP_QUERY).matches;
let cookie = null;
if (isDesktop) {
this.body.removeClass('sidebar-open');
@@ -143,6 +137,14 @@ export default class Sidebar {
this.body.toggleClass(`sidebar-${isDesktop ? 'closed' : 'open'}`);
$(global).trigger('sidebar_state._grav', isDesktop);
if (isDesktop) {
cookie = !this.body.hasClass('sidebar-closed');
} else {
cookie = this.body.hasClass('sidebar-open');
}
Cookies.set('grav-admin-sidebar', cookie, { expires: Infinity });
}
checkMatch(data) {

View File

@@ -1,5 +1,5 @@
import $ from 'jquery';
import Cookies from 'cookies-js';
import Cookies from '../utils/cookies';
import { Instance as Editors } from '../forms/fields/editor';
let Data = JSON.parse(Cookies.get('grav-tabs-state') || '{}');

View File

@@ -0,0 +1,25 @@
import { config } from 'grav-config';
import request from '../utils/request';
export default ({ preview = false, exportScss = false, color_scheme = {}, fonts = {}, callback = () => {} } = {}) => {
let task = exportScss ? 'exportScss' : 'compileScss';
// console.log(config);
const URI = `${config.base_url_relative}.json/task:${task}`;
request(URI, {
method: 'post',
body: Object.assign({}, preview ? { preview } : null, color_scheme)
}, callback);
};
export const prepareElement = (element) => {
element.data('busy_right_now', true);
if (!element.data('current_icon')) {
element.data('current_icon', element.find('.fa').attr('class'));
}
element.find('.fa').attr('class', 'fa fa-fw fa-spin fa-refresh');
};
export const resetElement = (element) => {
element.data('busy_right_now', false);
element.find('.fa').attr('class', element.data('current_icon'));
};

View File

@@ -0,0 +1,93 @@
import $ from 'jquery';
import Compile, { prepareElement, resetElement } from './compile';
import Forms from '../forms';
import { hex2rgb } from '../utils/colors';
import './presets';
const body = $('body');
const FormState = Forms.FormState.Instance;
const compiler = (element, preview = false, exportScss = false, callback = () => {}) => {
prepareElement(element);
let fields = FormState.collect();
Compile({
preview,
exportScss,
color_scheme: !fields ? [] : fields.filter((value, key) => key.match(/^data\[whitelabel]\[color_scheme]/)).toJS(),
callback: (response) => {
callback.call(callback, response);
resetElement(element);
}
});
};
body.on('click', '[data-preview-scss]', (event) => {
event && event.preventDefault();
let element = $(event.currentTarget);
if (element.data('busy_right_now')) { return false; }
compiler(element, true, false, (response) => {
if (response.files) {
Object.keys(response.files).forEach((key) => {
let file = $(`#admin-pro-preview-${key}`);
let timestamp = Date.now();
if (!file.length) {
file = $(`<link id="admin-pro-preview-${key}" type="text/css" rel="stylesheet" />`);
$('head').append(file);
if (!$('[data-reset-scss]').length) {
let reset = $('<button class="button" data-reset-scss style="margin-left: 5px;"><i class="fa fa-fw fa-history"></i> Reset</button>');
reset.insertAfter(element);
}
}
file.attr('href', `${response.files[key]}?${timestamp}`);
});
}
});
});
body.on('click', '[data-recompile-scss]', (event) => {
event && event.preventDefault();
let element = $(event.currentTarget);
if (element.data('busy_right_now')) { return false; }
compiler(element, true, false);
});
body.on('click', '[data-export-scss]', (event) => {
event && event.preventDefault();
let element = $(event.currentTarget);
if (element.data('busy_right_now')) { return false; }
compiler(element, true, true, (response) => {
if (response.files) {
Object.keys(response.files).forEach((key) => {
if (key === 'download') {
let element = document.createElement('a');
element.setAttribute('href', response.files[key]);
element.setAttribute('download', '');
element.style.display = 'none';
document.body.appendChild(element);
element.click();
document.body.removeChild(element);
}
});
}
});
});
body.on('change._grav_colorpicker', '[data-grav-colorpicker]', (event, input, hex, opacity) => {
let RGB = hex2rgb(hex);
let YIQ = ((RGB.r * 299) + (RGB.g * 587) + (RGB.b * 114)) / 1000;
let contrast = YIQ >= 128 || opacity <= 0.50 ? 'dark' : 'light';
input.parent().removeClass('dark-text light-text').addClass(`${contrast}-text`);
});
body.ready(() => {
$('[data-grav-colorpicker]').trigger('keyup');
});

View File

@@ -0,0 +1,169 @@
import $ from 'jquery';
import Forms from '../forms';
let body = $('body');
let fields = [];
const FormState = Forms.FormState.Instance;
const setField = (field, value) => {
let name = field.prop('name');
let tag = field.prop('tagName').toLowerCase();
let type = field.prop('type');
fields.push(name);
switch (tag) {
case 'select':
field.val(value);
field.data('selectize').setValue(value);
field.trigger('change');
break;
case 'input':
if (type === 'radio') {
let strValue = value ? '1' : '0';
field.filter((index, radio) => $(radio).val() === strValue).prop('checked', true);
break;
}
if (type === 'checkbox') {
field.prop('checked', value);
break;
}
field.val(value);
field.trigger('keyup');
}
};
body.on('click', '[data-preset-values]', (event) => {
let target = $(event.currentTarget);
let data = target.data('preset-values');
Object.keys(data).forEach((section) => {
if (typeof data[section] === 'string') {
return;
}
Object.keys(data[section]).forEach((key) => {
let field = $(`[name="data[whitelabel][color_scheme][${section}][${key}]"], [name="data[${section}][${key}]"]`);
let value = data[section][key];
setField(field, value);
});
});
});
body.on('click', '[data-reset-scss]', (event) => {
event && event.preventDefault();
let element = $(event.currentTarget);
let links = $('link[id^=admin-pro-preview-]');
element.remove();
links.remove();
fields.forEach((field) => {
let value = FormState.loadState.get(field);
setField($(`[name="${field}"]`), value);
});
fields = [];
});
// Horizontal Scroll Functionality
$.fn.hscrollarrows = function() {
return this.each(function() {
let navNext = $('<a class="nav-next hide"></a>');
let navPrev = $('<a class="nav-prev hide"></a>');
let scrollTime = null;
let resizeTime = null;
let scrolling = false;
let elm_w = 0;
let elem_data_w = 0;
let max_scroll = 0;
let inc_scroll = 0;
let calcData = function() {
elm_w = elem.width();
elem_data_w = elem_data.get(0).scrollWidth;
max_scroll = elem_data_w - elm_w;
inc_scroll = elm_w * 0.3; // 20%
};
let revalidate = function() {
calcData();
stateNavs();
};
let run = function() {
calcData();
setupNavs();
};
let setupNavs = function() {
elem.parent().prepend(navNext);
elem.parent().prepend(navPrev);
navNext.on('click', next);
navPrev.on('click', prev);
stateNavs();
$(elem).scroll(function() {
if (!scrolling) {
clearTimeout(scrollTime);
scrollTime = setTimeout(function() {
stateNavs();
}, 250);
}
});
$(window).resize(function() {
clearTimeout(resizeTime);
resizeTime = setTimeout(function() {
revalidate();
}, 250);
});
};
let stateNavs = function() {
let current_scroll = elem.scrollLeft();
if (current_scroll < max_scroll) {
navNext.removeClass('hide');
} else {
navNext.addClass('hide');
}
if (current_scroll > 0) {
navPrev.removeClass('hide');
} else {
navPrev.addClass('hide');
}
scrolling = false;
};
let next = function() {
let current_scroll = elem.scrollLeft();
if (current_scroll < max_scroll) {
scrolling = true;
elem.stop().animate({
scrollLeft: (current_scroll + inc_scroll)
}, stateNavs);
}
};
let prev = function() {
let current_scroll = elem.scrollLeft();
if (current_scroll > 0) {
scrolling = true;
elem.stop().animate({
scrollLeft: (current_scroll - inc_scroll)
}, stateNavs);
}
};
let elem = $(this);
let elem_data = $(':first-child', elem);
run();
});
};
$(document).ready(() => {
$('.jquery-horizontal-scroll').hscrollarrows();
});

View File

@@ -1,3 +1,3 @@
@import url("https://fonts.googleapis.com/css?family=Roboto:300,400,500|Inconsolata:400,700|&subset=latin-ext");body,h5,h6,.badge,.note,.grav-mdeditor-preview,input,select,textarea,button,.selectize-input{font-family:"Roboto","Helvetica","Tahoma","Geneva","Arial",sans-serif}h1,h2,h3,h4,.form-tabs>label,.label{font-family:"Roboto","Helvetica","Tahoma","Geneva","Arial",sans-serif}code,kbd,pre,samp,body .CodeMirror{font-family:"Inconsolata","Monaco","Consolas","Lucida Console",monospace !important}
@import url("https://fonts.googleapis.com/css?family=Josefin+Slab:400|Roboto:300,400,500|Inconsolata:400,700&subset=latin-ext");body,h5,h6,.badge,.note,.grav-mdeditor-preview,input,select,textarea,button,.selectize-input{font-family:"Roboto","Helvetica","Tahoma","Geneva","Arial",sans-serif}h1,h2,h3,h4,.form-tabs>label,.label{font-family:"Josefin Slab","Helvetica","Tahoma","Geneva","Arial",sans-serif}code,kbd,pre,samp,body .CodeMirror{font-family:"Inconsolata","Monaco","Consolas","Lucida Console",monospace !important}
/*# sourceMappingURL=../css-compiled/fonts.css.map */

View File

@@ -1,11 +1 @@
{
"version": 3,
"file": "../scss/fonts.css",
"sources": [
"../scss/fonts.scss",
"../hdr0",
"../scss/configuration/fonts/_support.scss"
],
"mappings": "AE+DA,OAAO,CAAC,uGAAI,CAtBR,AAAA,IAAI,CAAE,AAAA,EAAE,CAAE,AAAA,EAAE,CACZ,AAAA,MAAM,CAAE,AAAA,KAAK,CAAE,AAAA,sBAAsB,CACrC,AAAA,KAAK,CAAE,AAAA,MAAM,CAAE,AAAA,QAAQ,CAAE,AAAA,MAAM,CAAE,AAAA,gBAAgB,AAAC,CAC9C,WAAW,CAAE,QAAU,CAAE,WAAW,CAAE,QAAQ,CAAE,QAAQ,CAAE,OAAO,CAAE,UAAU,CAChF,AAID,AAAA,EAAE,CAAE,AAAA,EAAE,CAAE,AAAA,EAAE,CAAE,AAAA,EAAE,CACd,AAAa,UAAH,CAAG,KAAK,CAAE,AAAA,MAAM,AAAC,CACvB,WAAW,CAAE,QAAU,CAAE,WAAW,CAAE,QAAQ,CAAE,QAAQ,CAAE,OAAO,CAAE,UAAU,CAChF,AAID,AAAA,IAAI,CAAE,AAAA,GAAG,CAAE,AAAA,GAAG,CAAE,AAAA,IAAI,CACpB,AAAK,IAAD,CAAC,WAAW,AAAC,CACb,WAAW,CAAE,aAAU,CAAE,QAAQ,CAAE,UAAU,CAAE,gBAAgB,CAAE,SAAS,CAAC,UAAU,CACxF",
"names": []
}
{"version":3,"file":"fonts.css","sources":["fonts.scss","configuration/fonts/_support.scss"],"sourcesContent":["$fonts-header: 'Josefin Slab' !default;\n$fonts-default: 'Roboto' !default;\n$fonts-mono: 'Inconsolata' !default;\n\n$font-definitions: (\n 'Josefin Slab': '400',\n Roboto: '300,400,500',\n Inconsolata: '400,700'\n);\n\n@import \"configuration/fonts/support\";\n","@function str-replace($string, $search, $replace: '') {\n $index: str-index($string, $search);\n\n @if $index {\n @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);\n }\n\n @return $string;\n}\n\n@function admin-font-faces($fonts) {\n $url: \"https://fonts.googleapis.com/css?family=\";\n $nb: 0;\n\n @each $fontname, $weights in $fonts {\n\n @if $fontname == $fonts-default or\n $fontname == $fonts-header or\n $fontname == $fonts-mono {\n\n $nb: $nb + 1;\n $nb-word: 0;\n\n $fontname: str-replace(\"#{$fontname}\", \" \", \"+\");\n\n $url: $url + $fontname;\n\n @if $weights != null {\n $url: $url + \":\" + $weights;\n }\n\n @if $nb < 3 {\n $url: $url + \"|\";\n }\n }\n }\n\n @return $url + \"&subset=latin-ext\";\n}\n\n@mixin body-fonts($font) {\n body, h5, h6,\n .badge, .note, .grav-mdeditor-preview,\n input, select, textarea, button, .selectize-input {\n font-family: \"#{$font}\", \"Helvetica\", \"Tahoma\", \"Geneva\", \"Arial\", sans-serif;\n }\n}\n\n@mixin header-fonts($font) {\n h1, h2, h3, h4,\n .form-tabs > label, .label {\n font-family: \"#{$font}\", \"Helvetica\", \"Tahoma\", \"Geneva\", \"Arial\", sans-serif;\n }\n}\n\n@mixin mono-fonts($font) {\n code, kbd, pre, samp,\n body .CodeMirror {\n font-family: \"#{$font}\", \"Monaco\", \"Consolas\", \"Lucida Console\", monospace !important;\n }\n}\n$font-url: admin-font-faces($font-definitions);\n\n@import url(\"#{$font-url}\");\n\n@include body-fonts($fonts-default);\n\n@include header-fonts($fonts-header);\n\n@include mono-fonts($fonts-mono);\n\n\n\n\n\n"],"names":[],"mappings":"AC+DA,OAAO,CAAC,uHAAI,CAtBR,AAAA,IAAI,CAAE,EAAE,CAAE,EAAE,CACZ,MAAM,CAAE,KAAK,CAAE,sBAAsB,CACrC,KAAK,CAAE,MAAM,CAAE,QAAQ,CAAE,MAAM,CAAE,gBAAgB,AAAC,CAC9C,WAAW,CAAE,QAAU,CAAE,WAAW,CAAE,QAAQ,CAAE,QAAQ,CAAE,OAAO,CAAE,UAAU,CAChF,AAID,AAAA,EAAE,CAAE,EAAE,CAAE,EAAE,CAAE,EAAE,CACd,UAAU,CAAG,KAAK,CAAE,MAAM,AAAC,CACvB,WAAW,CAAE,cAAU,CAAE,WAAW,CAAE,QAAQ,CAAE,QAAQ,CAAE,OAAO,CAAE,UAAU,CAChF,AAID,AAAA,IAAI,CAAE,GAAG,CAAE,GAAG,CAAE,IAAI,CACpB,IAAI,CAAC,WAAW,AAAC,CACb,WAAW,CAAE,aAAU,CAAE,QAAQ,CAAE,UAAU,CAAE,gBAAgB,CAAE,SAAS,CAAC,UAAU,CACxF"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
{"version":3,"file":"paper.css","sources":["paper.scss"],"sourcesContent":["// CodeMirror Theme\n.cm-s-paper {\n $paper-fg: #666;\n $tag: darken($paper-fg, 10%);\n\n &.CodeMirror {\n color: $paper-fg;\n line-height: 1.4;\n font-size: 16px;\n }\n\n .cm-link {color: #0082ba;}\n .cm-comment {color: lighten($paper-fg,10%);}\n .cm-header {color: darken($paper-fg,5%);}\n .cm-strong {color: darken($paper-fg,10%);}\n .cm-em {color: darken($paper-fg, 10%);}\n .cm-string {color: #0082BA;}\n .cm-tag {color: $tag}\n .cm-bracket {color: lighten($tag, 10%);}\n .cm-hr {color: lighten($paper-fg,40%);font-weight: bold;}\n\n .cm-keyword {color: #0082ba;}\n .cm-atom {color: #9055af;}\n .cm-meta {color: $paper-fg;}\n\n .cm-number {color: #7f8c8d;}\n .cm-def {color: #00f;}\n .cm-variable {color: black;}\n //.cm-variable-2 {color: #555;}\n //.cm-variable-3 {color: #085;}\n .cm-property {color: black;}\n .cm-operator {color: black;}\n\n\n .cm-string-2 {color: #f50;}\n .cm-meta {color: #555;}\n .cm-error {color: #f00;}\n .cm-qualifier {color: #555;}\n .cm-builtin {color: #555;}\n\n\n .cm-attribute {color: #7f8c8d;}\n\n .cm-quote {color: #888;}\n\n\n .cm-header-1 {font-size: 140%;}\n .cm-header-2 {font-size: 120%;}\n .cm-header-3 {font-size: 110%;}\n\n .cm-negative {color: #d44;}\n .cm-positive {color: #292;}\n .cm-header, .cm-strong {font-weight: bold;}\n .cm-em {font-style: italic;}\n .cm-link {text-decoration: underline;}\n\n .cm-invalidchar {color: #f00;}\n}\n"],"names":[],"mappings":"AACA,AAII,WAJO,AAIN,WAAW,AAAC,CACT,KAAK,CAJE,IAAI,CAKX,WAAW,CAAE,GAAG,CAChB,SAAS,CAAE,IAAI,CAClB,AARL,AAUI,WAVO,CAUP,QAAQ,AAAC,CAAC,KAAK,CAAE,OAAO,CAAG,AAV/B,AAWI,WAXO,CAWP,WAAW,AAAC,CAAC,KAAK,CAAE,IAAsB,CAAG,AAXjD,AAYI,WAZO,CAYP,UAAU,AAAC,CAAC,KAAK,CAAE,OAAoB,CAAG,AAZ9C,AAaI,WAbO,CAaP,UAAU,AAAC,CAAC,KAAK,CAAE,OAAqB,CAAG,AAb/C,AAcI,WAdO,CAcP,MAAM,AAAC,CAAC,KAAK,CAAE,OAAsB,CAAG,AAd5C,AAeI,WAfO,CAeP,UAAU,AAAC,CAAC,KAAK,CAAE,OAAO,CAAG,AAfjC,AAgBI,WAhBO,CAgBP,OAAO,AAAC,CAAC,KAAK,CAdR,OAAsB,CAcN,AAhB1B,AAiBI,WAjBO,CAiBP,WAAW,AAAC,CAAC,KAAK,CAAE,IAAkB,CAAG,AAjB7C,AAkBI,WAlBO,CAkBP,MAAM,AAAC,CAAC,KAAK,CAAE,IAAsB,CAAC,WAAW,CAAE,IAAI,CAAG,AAlB9D,AAoBI,WApBO,CAoBP,WAAW,AAAC,CAAC,KAAK,CAAE,OAAO,CAAG,AApBlC,AAqBI,WArBO,CAqBP,QAAQ,AAAC,CAAC,KAAK,CAAE,OAAO,CAAG,AArB/B,AAsBI,WAtBO,CAsBP,QAAQ,AAAC,CAAC,KAAK,CArBJ,IAAI,CAqBc,AAtBjC,AAwBI,WAxBO,CAwBP,UAAU,AAAC,CAAC,KAAK,CAAE,OAAO,CAAG,AAxBjC,AAyBI,WAzBO,CAyBP,OAAO,AAAC,CAAC,KAAK,CAAE,IAAI,CAAG,AAzB3B,AA0BI,WA1BO,CA0BP,YAAY,AAAC,CAAC,KAAK,CAAE,KAAK,CAAG,AA1BjC,AA6BI,WA7BO,CA6BP,YAAY,AAAC,CAAC,KAAK,CAAE,KAAK,CAAG,AA7BjC,AA8BI,WA9BO,CA8BP,YAAY,AAAC,CAAC,KAAK,CAAE,KAAK,CAAG,AA9BjC,AAiCI,WAjCO,CAiCP,YAAY,AAAC,CAAC,KAAK,CAAE,IAAI,CAAG,AAjChC,AAkCI,WAlCO,CAkCP,QAAQ,AAAC,CAAC,KAAK,CAAE,IAAI,CAAG,AAlC5B,AAmCI,WAnCO,CAmCP,SAAS,AAAC,CAAC,KAAK,CAAE,IAAI,CAAG,AAnC7B,AAoCI,WApCO,CAoCP,aAAa,AAAC,CAAC,KAAK,CAAE,IAAI,CAAG,AApCjC,AAqCI,WArCO,CAqCP,WAAW,AAAC,CAAC,KAAK,CAAE,IAAI,CAAG,AArC/B,AAwCI,WAxCO,CAwCP,aAAa,AAAC,CAAC,KAAK,CAAE,OAAO,CAAG,AAxCpC,AA0CI,WA1CO,CA0CP,SAAS,AAAC,CAAC,KAAK,CAAE,IAAI,CAAG,AA1C7B,AA6CI,WA7CO,CA6CP,YAAY,AAAC,CAAC,SAAS,CAAE,IAAI,CAAG,AA7CpC,AA8CI,WA9CO,CA8CP,YAAY,AAAC,CAAC,SAAS,CAAE,IAAI,CAAG,AA9CpC,AA+CI,WA/CO,CA+CP,YAAY,AAAC,CAAC,SAAS,CAAE,IAAI,CAAG,AA/CpC,AAiDI,WAjDO,CAiDP,YAAY,AAAC,CAAC,KAAK,CAAE,IAAI,CAAG,AAjDhC,AAkDI,WAlDO,CAkDP,YAAY,AAAC,CAAC,KAAK,CAAE,IAAI,CAAG,AAlDhC,AAmDI,WAnDO,CAmDP,UAAU,CAnDd,WAAW,CAmDK,UAAU,AAAC,CAAC,WAAW,CAAE,IAAI,CAAG,AAnDhD,AAoDI,WApDO,CAoDP,MAAM,AAAC,CAAC,UAAU,CAAE,MAAM,CAAG,AApDjC,AAqDI,WArDO,CAqDP,QAAQ,AAAC,CAAC,eAAe,CAAE,SAAS,CAAG,AArD3C,AAuDI,WAvDO,CAuDP,eAAe,AAAC,CAAC,KAAK,CAAE,IAAI,CAAG"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,3 +1,2 @@
body,h5,h6,.badge,.note,.grav-mdeditor-preview,input,select,textarea,button,.selectize-input,h1,h2,h3,h4,#admin-menu li,.form-tabs>label,.label,body .CodeMirror pre{font-family:"Helvetica Neue", "Helvetica", "Tahoma", "Geneva", "Arial", sans-serif}code,kbd,pre,samp,.mono{font-family:"Monaco", "Consolas", "Lucida Console", monospace}
/*# sourceMappingURL=../css-compiled/simple-fonts.css.map */
#admin-menu li,.badge,.fontfamily-sans .CodeMirror pre,.form-tabs>label,.grav-mdeditor-preview,.label,.note,.selectize-input,body,button,h1,h2,h3,h4,h5,h6,input,select,textarea{font-family:"Helvetica Neue",Helvetica,Tahoma,Geneva,Arial,sans-serif}.CodeMirror pre,.mono,code,kbd,pre,samp{font-family:SFMono-Regular,Consolas,"Liberation Mono",Menlo,Courier,monospace}
/*# sourceMappingURL=data:application/json;charset=utf8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbInNpbXBsZS1mb250cy5zY3NzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUtBLGVBSkEsT0FHQSxpQ0FDZ0IsaUJBSkQsdUJBSXFCLE9BSjVCLE1BQ3lCLGlCQUZqQyxLQUV5QixPQUN6QixHQUFJLEdBQUksR0FBSSxHQUhOLEdBQUksR0FFVixNQUFPLE9BQVEsU0FJWCxZQUFhLGdCQUFBLENBQUEsU0FBQSxDQUFBLE1BQUEsQ0FBQSxNQUFBLENBQUEsS0FBQSxDQUFBLFdBQ2hCLGdCQUdxQixNQUF0QixLQUFNLElBQUssSUFBSyxLQUNaLFlBQWEsY0FBQSxDQUFBLFFBQUEsQ0FBQSxpQkFBQSxDQUFBLEtBQUEsQ0FBQSxPQUFBLENBQUEiLCJmaWxlIjoic2ltcGxlLWZvbnRzLmNzcyIsInNvdXJjZXNDb250ZW50IjpbImJvZHksIGg1LCBoNixcbi5iYWRnZSwgLm5vdGUsIC5ncmF2LW1kZWRpdG9yLXByZXZpZXcsXG5pbnB1dCwgc2VsZWN0LCB0ZXh0YXJlYSwgYnV0dG9uLCAuc2VsZWN0aXplLWlucHV0LFxuaDEsIGgyLCBoMywgaDQsXG4uZm9udGZhbWlseS1zYW5zIC5Db2RlTWlycm9yIHByZSxcbiNhZG1pbi1tZW51IGxpLCAuZm9ybS10YWJzID4gbGFiZWwsIC5sYWJlbCwge1xuICAgIGZvbnQtZmFtaWx5OiBcIkhlbHZldGljYSBOZXVlXCIsIFwiSGVsdmV0aWNhXCIsIFwiVGFob21hXCIsIFwiR2VuZXZhXCIsIFwiQXJpYWxcIiwgc2Fucy1zZXJpZjtcbn1cbi8vLmNrLWVkaXRvciBzaG9ydGNvZGUsIC5jay1lZGl0b3IgdHdpZyxcbi5Db2RlTWlycm9yIHByZSxcbmNvZGUsIGtiZCwgcHJlLCBzYW1wLCAubW9ubyAge1xuICAgIGZvbnQtZmFtaWx5OiBcIlNGTW9uby1SZWd1bGFyXCIsIENvbnNvbGFzLCBcIkxpYmVyYXRpb24gTW9ub1wiLCBNZW5sbywgQ291cmllciwgbW9ub3NwYWNlO1xufVxuIl19 */

View File

@@ -1,10 +1 @@
{
"version": 3,
"file": "../scss/simple-fonts.css",
"sources": [
"../scss/simple-fonts.scss",
"../hdr0"
],
"mappings": "AAAA,AAAA,IAAI,CAAE,AAAA,EAAE,CAAE,AAAA,EAAE,CACZ,AAAA,MAAM,CAAE,AAAA,KAAK,CAAE,AAAA,sBAAsB,CACrC,AAAA,KAAK,CAAE,AAAA,MAAM,CAAE,AAAA,QAAQ,CAAE,AAAA,MAAM,CAAE,AAAA,gBAAgB,CACjD,AAAA,EAAE,CAAE,AAAA,EAAE,CAAE,AAAA,EAAE,CAAE,AAAA,EAAE,CACd,AAAY,WAAD,CAAC,EAAE,CAAE,AAAa,UAAH,CAAG,KAAK,CAAE,AAAA,MAAM,CAC1C,AAAiB,IAAb,CAAC,WAAW,CAAC,GAAG,AAAC,CACjB,WAAW,CAAE,sEAAuE,CACvF,AACD,AAAA,IAAI,CAAE,AAAA,GAAG,CAAE,AAAA,GAAG,CAAE,AAAA,IAAI,CAAE,AAAA,KAAK,AAAE,CACzB,WAAW,CAAE,iDAAkD,CAClE",
"names": []
}
{"version":3,"file":"simple-fonts.css","sources":["simple-fonts.scss"],"sourcesContent":["body, h5, h6,\n.badge, .note, .grav-mdeditor-preview,\ninput, select, textarea, button, .selectize-input,\nh1, h2, h3, h4,\n.fontfamily-sans .CodeMirror pre,\n#admin-menu li, .form-tabs > label, .label, {\n font-family: \"Helvetica Neue\", \"Helvetica\", \"Tahoma\", \"Geneva\", \"Arial\", sans-serif;\n}\n//.ck-editor shortcode, .ck-editor twig,\n.CodeMirror pre,\ncode, kbd, pre, samp, .mono {\n font-family: \"SFMono-Regular\", Consolas, \"Liberation Mono\", Menlo, Courier, monospace;\n}\n"],"names":[],"mappings":"AAAA,AAAA,IAAI,CAAE,EAAE,CAAE,EAAE,CACZ,MAAM,CAAE,KAAK,CAAE,sBAAsB,CACrC,KAAK,CAAE,MAAM,CAAE,QAAQ,CAAE,MAAM,CAAE,gBAAgB,CACjD,EAAE,CAAE,EAAE,CAAE,EAAE,CAAE,EAAE,CACd,gBAAgB,CAAC,WAAW,CAAC,GAAG,CAChC,WAAW,CAAC,EAAE,CAAE,UAAU,CAAG,KAAK,CAAE,MAAM,AAAE,CACxC,WAAW,CAAE,sEAAsE,CACtF,AAED,AAAA,WAAW,CAAC,GAAG,CACf,IAAI,CAAE,GAAG,CAAE,GAAG,CAAE,IAAI,CAAE,KAAK,AAAE,CACzB,WAAW,CAAE,wEAAwE,CACxF"}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,36 +0,0 @@
/*
Name: Base16 Default Light
Author: Chris Kempson (http://chriskempson.com)
CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-chrome-devtools)
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
*/
.cm-s-base16-light.CodeMirror {background: #f5f5f5; color: #202020;}
.cm-s-base16-light div.CodeMirror-selected {background: #e0e0e0 !important;}
.cm-s-base16-light .CodeMirror-gutters {background: #f5f5f5; border-right: 0px;}
.cm-s-base16-light .CodeMirror-guttermarker { color: #ac4142; }
.cm-s-base16-light .CodeMirror-guttermarker-subtle { color: #b0b0b0; }
.cm-s-base16-light .CodeMirror-linenumber {color: #b0b0b0;}
.cm-s-base16-light .CodeMirror-cursor {border-left: 1px solid #505050 !important;}
.cm-s-base16-light span.cm-comment {color: #8f5536;}
.cm-s-base16-light span.cm-atom {color: #aa759f;}
.cm-s-base16-light span.cm-number {color: #aa759f;}
.cm-s-base16-light span.cm-property, .cm-s-base16-light span.cm-attribute {color: #90a959;}
.cm-s-base16-light span.cm-keyword {color: #ac4142;}
.cm-s-base16-light span.cm-string {color: #f4bf75;}
.cm-s-base16-light span.cm-variable {color: #90a959;}
.cm-s-base16-light span.cm-variable-2 {color: #6a9fb5;}
.cm-s-base16-light span.cm-def {color: #d28445;}
.cm-s-base16-light span.cm-bracket {color: #202020;}
.cm-s-base16-light span.cm-tag {color: #ac4142;}
.cm-s-base16-light span.cm-link {color: #aa759f;}
.cm-s-base16-light span.cm-error {background: #ac4142; color: #505050;}
.cm-s-base16-light .CodeMirror-activeline-background {background: #DDDCDC !important;}
.cm-s-base16-light .CodeMirror-matchingbracket { text-decoration: underline; color: white !important;}

View File

@@ -41,11 +41,11 @@
/* CURSOR */
.CodeMirror div.CodeMirror-cursor {
.CodeMirror .CodeMirror-cursor {
border-left: 1px solid black;
}
/* Shown when moving in bi-directional text */
.CodeMirror div.CodeMirror-secondarycursor {
.CodeMirror .CodeMirror-secondarycursor {
border-left: 1px solid silver;
}
.CodeMirror.cm-fat-cursor div.CodeMirror-cursor {

View File

@@ -1,39 +0,0 @@
.cm-s-paper .cm-keyword {color: #555;}
.cm-s-paper .cm-atom {color: #7f8c8d;}
.cm-s-paper .cm-number {color: #7f8c8d;}
.cm-s-paper .cm-def {color: #00f;}
.cm-s-paper .cm-variable {color: black;}
.cm-s-paper .cm-variable-2 {color: #555;}
.cm-s-paper .cm-variable-3 {color: #085;}
.cm-s-paper .cm-property {color: black;}
.cm-s-paper .cm-operator {color: black;}
.cm-s-paper .cm-comment {color: #959595;}
.cm-s-paper .cm-string {color: #7f8c8d;}
.cm-s-paper .cm-string-2 {color: #f50;}
.cm-s-paper .cm-meta {color: #555;}
.cm-s-paper .cm-error {color: #f00;}
.cm-s-paper .cm-qualifier {color: #555;}
.cm-s-paper .cm-builtin {color: #555;}
.cm-s-paper .cm-bracket {color: #997;}
.cm-s-paper .cm-tag {color: #7f8c8d;}
.cm-s-paper .cm-attribute {color: #7f8c8d;}
.cm-s-paper .cm-header {color: #000;}
.cm-s-paper .cm-quote {color: #888;}
.cm-s-paper .cm-hr {color: #999;}
.cm-s-paper .cm-link {color: #7f8c8d;}
.cm-negative {color: #d44;}
.cm-positive {color: #292;}
.cm-header, .cm-strong {font-weight: bold;}
.cm-em {font-style: italic;}
.cm-link {text-decoration: underline;}
.cm-invalidchar {color: #f00;}
.cm-s-paper .cm-header-1 {font-size: 140%;}
.cm-s-paper .cm-header-2 {font-size: 120%;}
.cm-s-paper .cm-header-3 {font-size: 110%;}
div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}

View File

@@ -0,0 +1,41 @@
/*
Name: 3024 day
Author: Jan T. Sott (http://github.com/idleberg)
CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
*/
.cm-s-3024-day.CodeMirror { background: #f7f7f7; color: #3a3432; }
.cm-s-3024-day div.CodeMirror-selected { background: #d6d5d4; }
.cm-s-3024-day .CodeMirror-line::selection, .cm-s-3024-day .CodeMirror-line > span::selection, .cm-s-3024-day .CodeMirror-line > span > span::selection { background: #d6d5d4; }
.cm-s-3024-day .CodeMirror-line::-moz-selection, .cm-s-3024-day .CodeMirror-line > span::-moz-selection, .cm-s-3024-day .CodeMirror-line > span > span::selection { background: #d9d9d9; }
.cm-s-3024-day .CodeMirror-gutters { background: #f7f7f7; border-right: 0px; }
.cm-s-3024-day .CodeMirror-guttermarker { color: #db2d20; }
.cm-s-3024-day .CodeMirror-guttermarker-subtle { color: #807d7c; }
.cm-s-3024-day .CodeMirror-linenumber { color: #807d7c; }
.cm-s-3024-day .CodeMirror-cursor { border-left: 1px solid #5c5855; }
.cm-s-3024-day span.cm-comment { color: #cdab53; }
.cm-s-3024-day span.cm-atom { color: #a16a94; }
.cm-s-3024-day span.cm-number { color: #a16a94; }
.cm-s-3024-day span.cm-property, .cm-s-3024-day span.cm-attribute { color: #01a252; }
.cm-s-3024-day span.cm-keyword { color: #db2d20; }
.cm-s-3024-day span.cm-string { color: #fded02; }
.cm-s-3024-day span.cm-variable { color: #01a252; }
.cm-s-3024-day span.cm-variable-2 { color: #01a0e4; }
.cm-s-3024-day span.cm-def { color: #e8bbd0; }
.cm-s-3024-day span.cm-bracket { color: #3a3432; }
.cm-s-3024-day span.cm-tag { color: #db2d20; }
.cm-s-3024-day span.cm-link { color: #a16a94; }
.cm-s-3024-day span.cm-error { background: #db2d20; color: #5c5855; }
.cm-s-3024-day .CodeMirror-activeline-background { background: #e8f2ff; }
.cm-s-3024-day .CodeMirror-matchingbracket { text-decoration: underline; color: #a16a94 !important; }

View File

@@ -0,0 +1,39 @@
/*
Name: 3024 night
Author: Jan T. Sott (http://github.com/idleberg)
CodeMirror template by Jan T. Sott (https://github.com/idleberg/base16-codemirror)
Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16)
*/
.cm-s-3024-night.CodeMirror { background: #090300; color: #d6d5d4; }
.cm-s-3024-night div.CodeMirror-selected { background: #3a3432; }
.cm-s-3024-night .CodeMirror-line::selection, .cm-s-3024-night .CodeMirror-line > span::selection, .cm-s-3024-night .CodeMirror-line > span > span::selection { background: rgba(58, 52, 50, .99); }
.cm-s-3024-night .CodeMirror-line::-moz-selection, .cm-s-3024-night .CodeMirror-line > span::-moz-selection, .cm-s-3024-night .CodeMirror-line > span > span::-moz-selection { background: rgba(58, 52, 50, .99); }
.cm-s-3024-night .CodeMirror-gutters { background: #090300; border-right: 0px; }
.cm-s-3024-night .CodeMirror-guttermarker { color: #db2d20; }
.cm-s-3024-night .CodeMirror-guttermarker-subtle { color: #5c5855; }
.cm-s-3024-night .CodeMirror-linenumber { color: #5c5855; }
.cm-s-3024-night .CodeMirror-cursor { border-left: 1px solid #807d7c; }
.cm-s-3024-night span.cm-comment { color: #cdab53; }
.cm-s-3024-night span.cm-atom { color: #a16a94; }
.cm-s-3024-night span.cm-number { color: #a16a94; }
.cm-s-3024-night span.cm-property, .cm-s-3024-night span.cm-attribute { color: #01a252; }
.cm-s-3024-night span.cm-keyword { color: #db2d20; }
.cm-s-3024-night span.cm-string { color: #fded02; }
.cm-s-3024-night span.cm-variable { color: #01a252; }
.cm-s-3024-night span.cm-variable-2 { color: #01a0e4; }
.cm-s-3024-night span.cm-def { color: #e8bbd0; }
.cm-s-3024-night span.cm-bracket { color: #d6d5d4; }
.cm-s-3024-night span.cm-tag { color: #db2d20; }
.cm-s-3024-night span.cm-link { color: #a16a94; }
.cm-s-3024-night span.cm-error { background: #db2d20; color: #807d7c; }
.cm-s-3024-night .CodeMirror-activeline-background { background: #2F2F2F; }
.cm-s-3024-night .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; }

View File

@@ -0,0 +1,32 @@
.cm-s-abcdef.CodeMirror { background: #0f0f0f; color: #defdef; }
.cm-s-abcdef div.CodeMirror-selected { background: #515151; }
.cm-s-abcdef .CodeMirror-line::selection, .cm-s-abcdef .CodeMirror-line > span::selection, .cm-s-abcdef .CodeMirror-line > span > span::selection { background: rgba(56, 56, 56, 0.99); }
.cm-s-abcdef .CodeMirror-line::-moz-selection, .cm-s-abcdef .CodeMirror-line > span::-moz-selection, .cm-s-abcdef .CodeMirror-line > span > span::-moz-selection { background: rgba(56, 56, 56, 0.99); }
.cm-s-abcdef .CodeMirror-gutters { background: #555; border-right: 2px solid #314151; }
.cm-s-abcdef .CodeMirror-guttermarker { color: #222; }
.cm-s-abcdef .CodeMirror-guttermarker-subtle { color: azure; }
.cm-s-abcdef .CodeMirror-linenumber { color: #FFFFFF; }
.cm-s-abcdef .CodeMirror-cursor { border-left: 1px solid #00FF00; }
.cm-s-abcdef span.cm-keyword { color: darkgoldenrod; font-weight: bold; }
.cm-s-abcdef span.cm-atom { color: #77F; }
.cm-s-abcdef span.cm-number { color: violet; }
.cm-s-abcdef span.cm-def { color: #fffabc; }
.cm-s-abcdef span.cm-variable { color: #abcdef; }
.cm-s-abcdef span.cm-variable-2 { color: #cacbcc; }
.cm-s-abcdef span.cm-variable-3, .cm-s-abcdef span.cm-type { color: #def; }
.cm-s-abcdef span.cm-property { color: #fedcba; }
.cm-s-abcdef span.cm-operator { color: #ff0; }
.cm-s-abcdef span.cm-comment { color: #7a7b7c; font-style: italic;}
.cm-s-abcdef span.cm-string { color: #2b4; }
.cm-s-abcdef span.cm-meta { color: #C9F; }
.cm-s-abcdef span.cm-qualifier { color: #FFF700; }
.cm-s-abcdef span.cm-builtin { color: #30aabc; }
.cm-s-abcdef span.cm-bracket { color: #8a8a8a; }
.cm-s-abcdef span.cm-tag { color: #FFDD44; }
.cm-s-abcdef span.cm-attribute { color: #DDFF00; }
.cm-s-abcdef span.cm-error { color: #FF0000; }
.cm-s-abcdef span.cm-header { color: aquamarine; font-weight: bold; }
.cm-s-abcdef span.cm-link { color: blueviolet; }
.cm-s-abcdef .CodeMirror-activeline-background { background: #314151; }

View File

@@ -0,0 +1,5 @@
.cm-s-ambiance.CodeMirror {
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,42 @@
/* Based on https://github.com/dempfi/ayu */
.cm-s-ayu-dark.CodeMirror { background: #0a0e14; color: #b3b1ad; }
.cm-s-ayu-dark div.CodeMirror-selected { background: #273747; }
.cm-s-ayu-dark .CodeMirror-line::selection, .cm-s-ayu-dark .CodeMirror-line > span::selection, .cm-s-ayu-dark .CodeMirror-line > span > span::selection { background: rgba(39, 55, 71, 99); }
.cm-s-ayu-dark .CodeMirror-line::-moz-selection, .cm-s-ayu-dark .CodeMirror-line > span::-moz-selection, .cm-s-ayu-dark .CodeMirror-line > span > span::-moz-selection { background: rgba(39, 55, 71, 99); }
.cm-s-ayu-dark .CodeMirror-gutters { background: #0a0e14; border-right: 0px; }
.cm-s-ayu-dark .CodeMirror-guttermarker { color: white; }
.cm-s-ayu-dark .CodeMirror-guttermarker-subtle { color: #3d424d; }
.cm-s-ayu-dark .CodeMirror-linenumber { color: #3d424d; }
.cm-s-ayu-dark .CodeMirror-cursor { border-left: 1px solid #e6b450; }
.cm-s-ayu-dark span.cm-comment { color: #626a73; }
.cm-s-ayu-dark span.cm-atom { color: #ae81ff; }
.cm-s-ayu-dark span.cm-number { color: #e6b450; }
.cm-s-ayu-dark span.cm-comment.cm-attribute { color: #ffb454; }
.cm-s-ayu-dark span.cm-comment.cm-def { color: rgba(57, 186, 230, 80); }
.cm-s-ayu-dark span.cm-comment.cm-tag { color: #39bae6; }
.cm-s-ayu-dark span.cm-comment.cm-type { color: #5998a6; }
.cm-s-ayu-dark span.cm-property, .cm-s-ayu-dark span.cm-attribute { color: #ffb454; }
.cm-s-ayu-dark span.cm-keyword { color: #ff8f40; }
.cm-s-ayu-dark span.cm-builtin { color: #e6b450; }
.cm-s-ayu-dark span.cm-string { color: #c2d94c; }
.cm-s-ayu-dark span.cm-variable { color: #b3b1ad; }
.cm-s-ayu-dark span.cm-variable-2 { color: #f07178; }
.cm-s-ayu-dark span.cm-variable-3 { color: #39bae6; }
.cm-s-ayu-dark span.cm-type { color: #ff8f40; }
.cm-s-ayu-dark span.cm-def { color: #ffee99; }
.cm-s-ayu-dark span.cm-bracket { color: #f8f8f2; }
.cm-s-ayu-dark span.cm-tag { color: rgba(57, 186, 230, 80); }
.cm-s-ayu-dark span.cm-header { color: #c2d94c; }
.cm-s-ayu-dark span.cm-link { color: #39bae6; }
.cm-s-ayu-dark span.cm-error { color: #ff3333; }
.cm-s-ayu-dark .CodeMirror-activeline-background { background: #01060e; }
.cm-s-ayu-dark .CodeMirror-matchingbracket {
text-decoration: underline;
color: white !important;
}

Some files were not shown because too many files have changed in this diff Show More