modif comtabilite

This commit is contained in:
2019-01-16 16:23:31 +01:00
parent 6b741a9666
commit 9312edc3ae
1844 changed files with 158848 additions and 55874 deletions
+18
View File
@@ -0,0 +1,18 @@
# EditorConfig is awesome: http://EditorConfig.org
# top-most EditorConfig file
root = true
# Unix-style newlines with a newline ending every file
[*]
charset = utf-8
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
indent_style = space
indent_size = 4
# 2 space indentation
[*.yaml, *.yml]
indent_style = space
indent_size = 2
+14
View File
@@ -0,0 +1,14 @@
themes/grav/.sass-cache
.DS_Store
# Node Modules
**/node_modules/**
themes/grav/js/admin.js
themes/grav/js/vendor.js
themes/grav/js/*.map
.idea
tests/_output/*
tests/_support/_generated/*
tests/cache/*
tests/error.log
+140 -7
View File
@@ -1,3 +1,136 @@
# v1.8.14
## 11/12/2018
1. [](#bugfix)
* Fixed Grav core update potentially spinning forever because of an error which happens after a successful upgrade
* Saving in expert mode can cause `undefined index: header` error [#1537](https://github.com/getgrav/grav-plugin-admin/issues/1537)
# v1.8.13
## 11/05/2018
1. [](#new)
* Added new `|nested()` Twig filter to access array objects with dot notation syntax
1. [](#bugfix)
* Fixed issue with complex lists structure and nested dot-notation [admin#2236](https://github.com/getgrav/grav/issues/2236)
# v1.8.12
## 10/24/2018
1. [](#improved)
* Updated various lang strings
* Removed duplicate lang strings
1. [](#bugfix)
* Fix XSS checking when empty content [#1533](https://github.com/getgrav/grav-plugin-admin/issues/1533)
* Fix DirectInstall not working [#1535](https://github.com/getgrav/grav-plugin-admin/issues/1535)
# v1.8.11
## 10/08/2018
1. [](#improved)
* Change usage of basename where possible [#1480](https://github.com/getgrav/grav-plugin-admin/pull/1480)
* Improved filename validation (requires Grav 1.5.3)
* Updated various lang codes
1. [](#bugfix)
* File Uploads: Do not trust mimetype sent by the browser
* Fixed file extension detection
* Fix for HTML entities in page slug [#1524](https://github.com/getgrav/grav-plugin-admin/issues/1524)
* Fix for port in backup download links [#1521](https://github.com/getgrav/grav-plugin-admin/issues/1521)
# v1.8.10
## 10/01/2018
1. [](#new)
* IMPORTANT: Non `admin.super` users are now subject to XSS validation in Page content. Configurable via Configuration / Security
* New XSS content warnings and integration into page save
* Added new event `onAdminPage()` which allows plugins to customize `Page` object in `$event['page']`
1. [](#improved)
* Use `Url:post()` to get the `$_POST` variable (allows common security checks/filtering for the POST data)
* Requires Grav 1.5.2
1. [](#bugfix)
* Fixed redirect to correct URL after failed login
* Fixed issue in `filepicker` where missing images would cause a loop to try to load them
* Twig 2 compatibility fixes for macros
* Updated `composer.json` to better match Grav 1.5
* Remove `package-lock.json` as it was referencing an insecure JS package
# v1.8.9
## 08/23/2018
1. [](#improved)
* Make order field to use context, not data
* Switched to new Grav Yaml class to support Native + Fallback YAML libraries
* Minor fix for `file` thumbnails display
* Requires Grav 1.5.1
# v1.8.8
## 08/17/2018
1. [](#improved)
* Support URI Params and Query attributes in Login redirect
* Added support for textarea value type in `array` field
* Added some new lang strings for Grav 1.5.0
1. [](#bugfix)
* Support params and querystring in login redirect
* Added field name nesting with tab field
# v1.8.7
## 07/31/2018
1. [](#bugfix)
* Fix for deleting 'extra' media files [grav#2100](https://githubcom/getgrav/grav/issues/2100)
# v1.8.6
## 07/13/2018
1. [](#bugfix)
* Force `html` for markdown preview [grav#2066](https://github.com/getgrav/grav/issues/2066)
* Add missing `authorizeTask()` checks in controller [#1483](https://github.com/getgrav/grav/issues/1483)
* Add support for `force_ssl` to admin URLs [#1479](https://github.com/getgrav/grav-plugin-admin/issues/1479)
# v1.8.5
## 06/20/2018
1. [](#bugfix)
* Fixed broken folder attribute on filepicker [#1465](https://github.com/getgrav/grav-plugin-admin/issues/1465)
* Added translation for system.session.initialize
* Slight updates on new translation strings
# v1.8.4
## 06/11/2018
1. [](#improved)
* Including EXIF JS library in the modules dependencies to fix orientation when uploading images
1. [](#bugfix)
* Initialize session on setup [#1451](https://github.com/getgrav/grav-plugin-admin/issues/1451)
* Force a `null` order when empty in the post request
* Fixed some 2FA form styling issues
# v1.8.3
## 05/31/2018
1. [](#new)
* Added support for selectize plugins as options in the selectize field
1. [](#bugfix)
* Fixed deep linking in admin after login [#1456](https://github.com/getgrav/grav-plugin-admin/issues/1456)
* Fixed Undefined property: `stdClass::$image` in v1.8.2 [#1454](https://github.com/getgrav/grav-plugin-admin/issues/1454)
* Pass media order when calling `task:listmedia`
# v1.8.2
## 05/24/2018
1. [](#new)
* Added custom object support for filepicker field
* Don't allow saving of a user with no local account file
* Controls for `list` field were not in sync between top and bottom
1. [](#improved)
* More subtle `fieldset` styling
1. [](#bugfix)
* Check if `$object->blueprints()` exists in `onAdminAfterSave`
* When creating first user, check `admin.login` not `site.login`
* Fix admin login redirects for multisite setups
* Fixed issue with filepicker field where images wouldn't properly merge with the current value if in a page header
* Fixed media delete for streams
# v1.8.1
## 05/15/2018
@@ -18,7 +151,7 @@
1. [](#improved)
* Added support for JavaScript `bottom` block [#1425](https://github.com/getgrav/grav-plugin-admin/pull/1425)
* Added better typography styling for blockquote and markdown in `display` field
* Vendor updates
* Vendor updates
1. [](#bugfix)
* Added missing MarkdownExtra strings [#1385](https://github.com/getgrav/grav-plugin-admin/pull/1385)
* Updated `blueprints.yaml` with missing `step` attribute [#1415](https://github.com/getgrav/grav-plugin-admin/pull/1415)
@@ -46,7 +179,7 @@
* Implemented workaround for required text fields [#1390](https://github.com/getgrav/grav-plugin-admin/issues/1390)
* Fixed highlight color in Firefox [getgrav/grav#1949](https://github.com/getgrav/grav/issues/1949)
* Fix for bad redirect on saving simplesearch (possibly others)
# v1.7.2
## 03/21/2018
@@ -58,8 +191,8 @@
* Optimization to stop admin for looking for pages in disabled plugins
* Added configuration option to choose if you want to use new 'inline' preview or `new tab'
1. [](#bugfix)
* Fix redirect bug when changing admin route to `admin-*`
* Changed Twig `|count` to `|length` filter [#1391](https://github.com/getgrav/grav-plugin-admin/issues/1391)
* Fix redirect bug when changing admin route to `admin-*`
* Changed Twig `|count` to `|length` filter [#1391](https://github.com/getgrav/grav-plugin-admin/issues/1391)
* Fix for page preview when `HTTP_REFERRER` is not set [grav#1930](https://github.com/getgrav/grav/issues/1930)
# v1.7.1
@@ -94,7 +227,7 @@
* Fixed issue with default lang translation in admin [#1361](https://github.com/getgrav/grav-plugin-admin/issues/1361)
* Typos in `Tools` -> `Direct Install` page [#1345](https://github.com/getgrav/grav-plugin-admin/issues/1345)
* Fixed bug with frontmatter being killed when in `Expert Mode` [#1354](https://github.com/getgrav/grav-plugin-admin/issues/1354)
# v1.7.0-rc.3
## 02/15/2018
@@ -119,7 +252,7 @@
* Tidied Tools panel and added translations
* Tooltip and new icon for site preview
* Updated JS library dependencies
* Changed CodeMirror editor to use sans-serif font for readability
* Changed CodeMirror editor to use sans-serif font for readability
1. [](#bugfix)
* Fixed z-index issue in fullscreen mode [#1317](https://github.com/getgrav/grav-plugin-admin/issues/1317)
@@ -215,7 +348,7 @@
* Fixed fields not working in Microsoft Edge with Selectize.js [#1222](https://github.com/getgrav/grav-plugin-admin/pull/1222)
* Replaced a left-over "is empty" check [#1232](https://github.com/getgrav/grav-plugin-admin/pull/1232)
* Fixed headers drop-down in editor to align properly
# v1.6.0
## 09/07/2017
+31 -4
View File
@@ -144,6 +144,7 @@ class AdminPlugin extends Plugin
// Only activate admin if we're inside the admin path.
if ($this->isAdminPath()) {
$this->grav['session']->init();
$this->active = true;
// Set cache based on admin_cache option
@@ -249,7 +250,7 @@ class AdminPlugin extends Plugin
unset($this->grav['user']);
$this->grav['user'] = $user;
$user->authenticated = true;
$user->authorized = $user->authorize('site.login');
$user->authorized = $user->authorize('admin.login');
$messages = $this->grav['messages'];
$messages->add($this->grav['language']->translate('PLUGIN_ADMIN.LOGIN_LOGGED_IN'), 'info');
@@ -316,6 +317,16 @@ class AdminPlugin extends Plugin
*/
public function onPagesInitialized()
{
$config = $this->config;
// Force SSL with redirect if required
if ($config->get('system.force_ssl')) {
if (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] !== 'on') {
$url = 'https://' . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
$this->grav->redirect($url);
}
}
$this->session = $this->grav['session'];
// Set original route for the home page.
@@ -349,7 +360,10 @@ class AdminPlugin extends Plugin
}
// Make local copy of POST.
$post = !empty($_POST) ? $_POST : [];
$post = $this->grav['uri']->post();
// Initialize Page Types
Pages::types();
// Handle tasks.
$this->admin->task = $task = !empty($post['task']) ? $post['task'] : $this->uri->param('task');
@@ -373,7 +387,17 @@ class AdminPlugin extends Plugin
$page = new Page;
$page->expires(0);
// First look in the pages provided by the Admin plugin itself
if ($this->grav['user']->authorize('admin.login')) {
$event = new Event(['page' => $page]);
$event = $this->grav->fireEvent('onAdminPage', $event);
$page = $event['page'];
if ($page->slug()) {
return $page;
}
}
// Look in the pages provided by the Admin plugin itself
if (file_exists(__DIR__ . "/pages/admin/{$self->template}.md")) {
$page->init(new \SplFileInfo(__DIR__ . "/pages/admin/{$self->template}.md"));
$page->slug(basename($self->template));
@@ -456,7 +480,9 @@ class AdminPlugin extends Plugin
$twig->twig_vars['location'] = $this->template;
$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['current_route'] = '/' . $twig->twig_vars['admin_route'] . '/' . $this->template . '/' . $this->route;
$twig->twig_vars['base_url_relative'] = $twig->twig_vars['base_url_simple'] . '/' . $twig->twig_vars['admin_route'];
$twig->twig_vars['current_url'] = rtrim($twig->twig_vars['base_url_relative'] . '/' . $this->template . '/' . $this->route, '/');
$theme_url = '/' . ltrim($this->grav['locator']->findResource('plugin://admin/themes/' . $this->theme,
false), '/');
$twig->twig_vars['theme_url'] = $theme_url;
@@ -762,7 +788,8 @@ class AdminPlugin extends Plugin
{
// Special case to redirect after changing the admin route to avoid 'breaking'
$obj = $event['object'];
if (null !== $obj) {
if (null !== $obj && method_exists($obj, 'blueprints')) {
$blueprint = $obj->blueprints()->getFilename();
if ($blueprint === 'admin/blueprints' && isset($obj->route) && $this->admin_route !== $obj->route) {
+2 -2
View File
@@ -1,5 +1,5 @@
name: Admin Panel
version: 1.8.1
version: 1.8.14
description: Adds an advanced administration panel to manage your site
icon: empire
author:
@@ -13,7 +13,7 @@ docs: https://github.com/getgrav/grav-plugin-admin/blob/develop/README.md
license: MIT
dependencies:
- { name: grav, version: '>=1.4.5' }
- { name: grav, version: '>=1.5.3' }
- { name: form, version: '>=2.14.0' }
- { name: login, version: '>=2.7.0' }
- { name: email, version: '>=2.7.0' }
@@ -18,6 +18,9 @@ form:
title: PLUGIN_ADMIN.CONTENT
fields:
xss_check:
type: xss
frontmatter:
classes: frontmatter
type: editor
@@ -2,10 +2,9 @@
namespace Grav\Plugin\Admin\Twig;
use Grav\Common\Grav;
use Grav\Common\Yaml;
use Grav\Common\Language\Language;
use Grav\Common\Page\Page;
use Symfony\Component\Yaml\Yaml;
use Symfony\Component\Yaml\Parser;
class AdminTwigExtension extends \Twig_Extension
{
@@ -32,6 +31,7 @@ class AdminTwigExtension extends \Twig_Extension
new \Twig_SimpleFilter('toYaml', [$this, 'toYamlFilter']),
new \Twig_SimpleFilter('fromYaml', [$this, 'fromYamlFilter']),
new \Twig_SimpleFilter('adminNicetime', [$this, 'adminNicetimeFilter']),
new \Twig_SimpleFilter('nested', [$this, 'nestedFilter']),
];
}
@@ -43,6 +43,23 @@ class AdminTwigExtension extends \Twig_Extension
];
}
public function nestedFilter($current, $name)
{
$path = explode('.', trim($name, '.'));
foreach ($path as $field) {
if (is_object($current) && isset($current->{$field})) {
$current = $current->{$field};
} elseif (is_array($current) && isset($current[$field])) {
$current = $current[$field];
} else {
return null;
}
}
return $current;
}
public function cloneFunc($obj)
{
return clone $obj;
@@ -81,7 +98,7 @@ class AdminTwigExtension extends \Twig_Extension
return $this->grav['admin']->translate($args, $lang);
}
public function toYamlFilter($value, $inline = true)
public function toYamlFilter($value, $inline = null)
{
return Yaml::dump($value, $inline);
@@ -89,8 +106,7 @@ class AdminTwigExtension extends \Twig_Extension
public function fromYamlFilter($value)
{
$yaml = new Parser();
return $yaml->parse($value);
return Yaml::parse($value);
}
public function adminNicetimeFilter($date, $long_strings = true)
+16 -14
View File
@@ -27,7 +27,7 @@ use RocketTheme\Toolbox\ResourceLocator\UniformResourceIterator;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use RocketTheme\Toolbox\Session\Message;
use RocketTheme\Toolbox\Session\Session;
use Symfony\Component\Yaml\Yaml;
use Grav\Common\Yaml;
use Composer\Semver\Semver;
use PicoFeed\Reader\Reader;
@@ -170,11 +170,11 @@ class Admin
/** @var \DirectoryIterator $directory */
foreach (new \DirectoryIterator($path) as $file) {
if ($file->isDir() || $file->isDot() || Utils::startsWith($file->getBasename(), '.')) {
if ($file->isDir() || $file->isDot() || Utils::startsWith($file->getFilename(), '.')) {
continue;
}
$lang = basename($file->getBasename(), '.yaml');
$lang = $file->getBasename('.yaml');
$languages[$lang] = LanguageCodes::getNativeName($lang);
@@ -202,7 +202,7 @@ class Admin
if ($file->isDir() || !preg_match('/^[^.].*.yaml$/', $file->getFilename())) {
continue;
}
$configurations[] = basename($file->getBasename(), '.yaml');
$configurations[] = $file->getBasename('.yaml');
}
return $configurations;
@@ -365,7 +365,7 @@ class Admin
$userKey = isset($credentials['username']) ? (string)$credentials['username'] : '';
$ipKey = Uri::ip();
$redirect = isset($post['redirect']) ? $post['redirect'] : $this->uri->route();
$redirect = isset($post['redirect']) ? $post['redirect'] : $this->base . $this->route;
// Check if the current IP has been used in failed login attempts.
$attempts = count($rateLimiter->getAttempts($ipKey, 'ip'));
@@ -392,11 +392,9 @@ class Admin
if ($user->authorized) {
$event->defMessage('PLUGIN_ADMIN.LOGIN_LOGGED_IN', 'info');
$event->defRedirect($redirect);
$event->defRedirect(isset($post['redirect']) ? $post['redirect'] : $redirect);
} else {
$this->session->redirect = $redirect;
$event->defRedirect($this->uri->route());
}
} else {
if ($user->authorized) {
@@ -406,7 +404,7 @@ class Admin
}
}
$event->defRedirect($this->uri->route());
$event->defRedirect($redirect);
$message = $event->getMessage();
if ($message) {
@@ -597,7 +595,8 @@ class Admin
}
if (!$post) {
$post = isset($_POST['data']) ? $_POST['data'] : [];
$post = $this->grav['uri']->post();
$post = isset($post['data']) ? $post['data'] : [];
}
// Check to see if a data type is plugin-provided, before looking into core ones
@@ -671,10 +670,10 @@ class Admin
$obj->file = $file;
$obj->page = $this->grav['pages']->get(dirname($obj->path));
$filename = pathinfo($obj->title)['filename'];
$filename = str_replace(['@3x', '@2x'], '', $filename);
if (isset(pathinfo($obj->title)['extension'])) {
$filename .= '.' . pathinfo($obj->title)['extension'];
$fileInfo = pathinfo($obj->title);
$filename = str_replace(['@3x', '@2x'], '', $fileInfo['filename']);
if (isset($fileInfo['extension'])) {
$filename .= '.' . $fileInfo['extension'];
}
if ($obj->page && isset($obj->page->media()[$filename])) {
@@ -1420,6 +1419,9 @@ class Admin
$path = "/{$path}";
}
// Fix for entities in path causing looping...
$path = urldecode($path);
$page = $path ? $pages->dispatch($path, true) : $pages->root();
if (!$page) {
@@ -2,9 +2,12 @@
namespace Grav\Plugin\Admin;
use Grav\Common\Config\Config;
use Grav\Common\Data\Data;
use Grav\Common\Filesystem\Folder;
use Grav\Common\Grav;
use Grav\Common\Media\Interfaces\MediaInterface;
use Grav\Common\Page\Media;
use Grav\Common\Page\Pages;
use Grav\Common\Utils;
use Grav\Common\Plugin;
use Grav\Common\Theme;
@@ -199,6 +202,29 @@ class AdminBaseController
$this->redirectCode = $code;
}
/**
* Sends JSON response and terminates the call.
*
* @param array $response
* @param int $code
* @return bool
*/
protected function sendJsonResponse(array $response, $code = 200)
{
// 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');
echo json_encode($response);
exit();
}
/**
* Handles ajax upload for files.
* Stores in a flash object the temporary file and deals with potential file errors.
@@ -225,10 +251,10 @@ class AdminBaseController
$upload = $this->normalizeFiles($_FILES['data'], $settings->name);
$filename = trim($upload->file->name);
$filename = $upload->file->name;
// Handle bad filenames.
if (strtr($filename, "\t\n\r\0\x0b", '_____') !== $filename || rtrim($filename, '. ') !== $filename || preg_match('|\.php|', $filename)) {
if (!Utils::checkFilename($filename)) {
$this->admin->json_response = [
'status' => 'error',
'message' => sprintf($this->admin->translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_UPLOAD', null),
@@ -259,7 +285,7 @@ class AdminBaseController
}
// Handle errors and breaks without proceeding further
if ($upload->file->error != UPLOAD_ERR_OK) {
if ($upload->file->error !== UPLOAD_ERR_OK) {
$this->admin->json_response = [
'status' => 'error',
'message' => sprintf($this->admin->translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_UPLOAD', null),
@@ -285,6 +311,9 @@ class AdminBaseController
$accepted = false;
$errors = [];
// Do not trust mimetype sent by the browser
$mime = Utils::getMimeByFilename($upload->file->name);
foreach ((array)$settings->accept as $type) {
// Force acceptance of any file when star notation
if ($type === '*') {
@@ -293,15 +322,24 @@ class AdminBaseController
}
$isMime = strstr($type, '/');
$find = str_replace('*', '.*', $type);
$find = str_replace(['.', '*'], ['\.', '.*'], $type);
$match = preg_match('#' . $find . '$#', $isMime ? $upload->file->type : $upload->file->name);
if (!$match) {
$message = $isMime ? 'The MIME type "' . $upload->file->type . '"' : 'The File Extension';
$errors[] = $message . ' for the file "' . $upload->file->name . '" is not an accepted.';
$accepted |= false;
if ($isMime) {
$match = preg_match('#' . $find . '$#', $mime);
if (!$match) {
$errors[] = 'The MIME type "' . $mime . '" for the file "' . $upload->file->name . '" is not an accepted.';
} else {
$accepted = true;
break;
}
} else {
$accepted |= true;
$match = preg_match('#' . $find . '$#', $upload->file->name);
if (!$match) {
$errors[] = 'The File Extension for the file "' . $upload->file->name . '" is not an accepted.';
} else {
$accepted = true;
break;
}
}
}
@@ -366,7 +404,7 @@ class AdminBaseController
// Generate random name if required
if ($settings->random_name) { // TODO: document
$extension = pathinfo($upload->file->name)['extension'];
$extension = pathinfo($upload->file->name, PATHINFO_EXTENSION);
$upload->file->name = Utils::generateRandomString(15) . '.' . $extension;
}
@@ -717,9 +755,9 @@ class AdminBaseController
} else {
$new_data = $files;
}
if (isset($data['header'][$init_key])) {
if (isset($obj->header()->{$init_key})) {
$obj->modifyHeader($init_key,
array_replace_recursive([], $data['header'][$init_key], $new_data));
array_replace_recursive([], $obj->header()->{$init_key}, $new_data));
} else {
$obj->modifyHeader($init_key, $new_data);
}
@@ -743,30 +781,44 @@ class AdminBaseController
return false;
}
$data = $this->view === 'pages' ? $this->admin->page(true) : $this->prepareData([]);
$settings = $data->blueprints()->schema()->getProperty($this->post['name']);
$data = $this->view === 'pages' ? $this->admin->page(true) : $this->prepareData([]);
if (null === $data) {
return false;
}
if (method_exists($data, 'blueprints')) {
$settings = $data->blueprints()->schema()->getProperty($this->post['name']);
} elseif (method_exists($data, 'getBlueprint')) {
$settings = $data->getBlueprint()->schema()->getProperty($this->post['name']);
}
if (isset($settings['folder'])) {
$folder = $settings['folder'];
} else {
$folder = '@self';
$folder = 'self@';
}
// Do not use self@ outside of pages
if ($this->view !== 'pages' && in_array($folder, ['@self', 'self@', '@self@'])) {
$this->admin->json_response = [
'status' => 'error',
'message' => sprintf($this->admin->translate('PLUGIN_ADMIN.FILEUPLOAD_PREVENT_SELF', null), $folder)
];
if (!$data instanceof MediaInterface) {
$this->admin->json_response = [
'status' => 'error',
'message' => sprintf($this->admin->translate('PLUGIN_ADMIN.FILEUPLOAD_PREVENT_SELF', null), $folder)
];
return false;
return false;
}
$media = $data->getMedia();
} else {
// Set destination
$folder = Folder::getRelativePath(rtrim($folder, '/'));
$folder = $this->admin->getPagePathFromToken($folder);
$media = new Media($folder);
}
// Set destination
$folder = Folder::getRelativePath(rtrim($folder, '/'));
$folder = $this->admin->getPagePathFromToken($folder);
$media = new Media($folder);
$available_files = [];
$metadata = [];
$thumbs = [];
@@ -859,7 +911,29 @@ class AdminBaseController
$type = $uri->param('type');
$field = $uri->param('field');
$this->taskRemoveMedia();
// Get Blueprint
$settings = (object) $this->admin->blueprints($blueprint)->schema()->getProperty($field);
// Get destination
if ($this->grav['locator']->isStream($settings->destination)) {
$destination = $this->grav['locator']->findResource($settings->destination, false, true);
} else {
$destination = Folder::getRelativePath(rtrim($settings->destination, '/'));
$destination = $this->admin->getPagePathFromToken($destination);
}
// Not in path
if (!Utils::startsWith($path, $destination)) {
$this->admin->json_response = [
'status' => 'error',
'message' => 'Path not valid for this data type'
];
return false;
}
// Only remove files from correct destination...
$this->taskRemoveMedia($destination . '/' . basename($path));
if ($type === 'pages') {
$page = $this->admin->page(true, $proute);
@@ -876,9 +950,11 @@ class AdminBaseController
$page->save();
} else {
$blueprint_prefix = $type === 'config' ? '' : $type . '.';
$blueprint_name = str_replace(['config/', '/blueprints'], '', $blueprint);
$blueprint_field = $blueprint_prefix . $blueprint_name . '.' . $field;
$files = $this->grav['config']->get($blueprint_field);
if ($files) {
@@ -919,15 +995,17 @@ class AdminBaseController
*
* @return bool True if the action was performed
*/
public function taskRemoveMedia()
public function taskRemoveMedia($filename = null)
{
if (!$this->canEditMedia()) {
return false;
}
$filename = base64_decode($this->grav['uri']->param('route'));
if (!$filename) {
$filename = base64_decode($this->route);
if (is_null($filename)) {
$filename = base64_decode($this->grav['uri']->param('route'));
if (!$filename) {
$filename = base64_decode($this->route);
}
}
$file = File::instance($filename);
+199 -97
View File
@@ -14,17 +14,18 @@ use Grav\Common\Page\Medium\Medium;
use Grav\Common\Page\Page;
use Grav\Common\Page\Pages;
use Grav\Common\Page\Collection;
use Grav\Common\Security;
use Grav\Common\User\User;
use Grav\Common\Utils;
use Grav\Common\Backup\ZipBackup;
use Grav\Plugin\Admin\Twig\AdminTwigExtension;
use Grav\Plugin\Login\TwoFactorAuth\TwoFactorAuth;
use Grav\Common\Yaml;
use RocketTheme\Toolbox\Event\Event;
use RocketTheme\Toolbox\File\File;
use RocketTheme\Toolbox\File\JsonFile;
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
/**
* Class AdminController
@@ -465,7 +466,7 @@ class AdminController extends AdminBaseController
/**
* Handles updating Grav
*
* @return bool True if the action was performed
* @return bool False if user has no permissions.
*/
public function taskUpdategrav()
{
@@ -478,14 +479,14 @@ class AdminController extends AdminBaseController
$result = Gpm::selfupgrade();
if ($result) {
$this->admin->json_response = [
$json_response = [
'status' => 'success',
'type' => 'updategrav',
'version' => $version,
'message' => $this->admin->translate('PLUGIN_ADMIN.GRAV_WAS_SUCCESSFULLY_UPDATED_TO') . ' ' . $version
];
} else {
$this->admin->json_response = [
$json_response = [
'status' => 'error',
'type' => 'updategrav',
'version' => GRAV_VERSION,
@@ -493,7 +494,7 @@ class AdminController extends AdminBaseController
];
}
return true;
return $this->sendJsonResponse($json_response);
}
/**
@@ -611,14 +612,18 @@ class AdminController extends AdminBaseController
$reorder = true;
$data = (array)$this->data;
$this->grav['twig']->twig_vars['current_form_data'] = $data;
// Special handler for user data.
if ($this->view === 'user') {
if (!$this->grav['user']->exists()) {
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.NO_USER_EXISTS'),'error');
return false;
}
if (!$this->admin->authorize(['admin.super', 'admin.users'])) {
//not admin.super or admin.users
// no user file or not admin.super or admin.users
if ($this->prepareData($data)->username !== $this->grav['user']->username) {
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' save.',
'error');
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK') . ' save.','error');
return false;
}
}
@@ -643,13 +648,25 @@ class AdminController extends AdminBaseController
// Ensure route is prefixed with a forward slash.
$route = '/' . ltrim($route, '/');
// Check for valid frontmatter
if (isset($data['frontmatter']) && !$this->checkValidFrontmatter($data['frontmatter'])) {
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.INVALID_FRONTMATTER_COULD_NOT_SAVE'),
'error');
return false;
}
// XSS Checks for page content
$xss_whitelist = $this->grav['config']->get('security.xss_whitelist', 'admin.super');
if (!$this->admin->authorize($xss_whitelist)) {
$check_what = ['header' => isset($data['header']) ? $data['header'] : '', 'frontmatter' => isset($data['frontmatter']) ? $data['frontmatter'] : '', 'content' => isset($data['content']) ? $data['content'] : ''];
$results = Security::detectXssFromArray($check_what);
if (!empty($results)) {
$this->admin->setMessage('<i class="fa fa-ban"></i> ' . $this->admin->translate('PLUGIN_ADMIN.XSS_ONSAVE_ISSUE'),
'error');
return false;
}
}
$parent = $route && $route !== '/' && $route !== '.' && $route !== '/.' ? $pages->dispatch($route, true) : $pages->root();
$original_order = (int)trim($obj->order(), '.');
@@ -758,16 +775,8 @@ class AdminController extends AdminBaseController
public function checkValidFrontmatter($frontmatter)
{
try {
// Try native PECL YAML PHP extension first if available.
if (function_exists('yaml_parse')) {
$saved = @ini_get('yaml.decode_php');
@ini_set('yaml.decode_php', 0);
@yaml_parse("---\n" . $frontmatter . "\n...");
@ini_set('yaml.decode_php', $saved);
} else {
Yaml::parse($frontmatter);
}
} catch (ParseException $e) {
Yaml::parse($frontmatter);
} catch (\RuntimeException $e) {
return false;
}
@@ -874,6 +883,10 @@ class AdminController extends AdminBaseController
protected function taskGetNewsFeed()
{
if (!$this->authorizeTask('dashboard', ['admin.login', 'admin.super'])) {
return false;
}
$cache = $this->grav['cache'];
if ($this->post['refresh'] === 'true') {
@@ -910,11 +923,13 @@ class AdminController extends AdminBaseController
} catch (\Exception $e) {
$this->admin->json_response = ['status' => 'error', 'message' => $e->getMessage()];
return;
return false;
}
}
$this->admin->json_response = ['status' => 'success', 'feed_data' => $feed_data];
return true;
}
/**
@@ -922,6 +937,10 @@ class AdminController extends AdminBaseController
*/
protected function taskGetUpdates()
{
if (!$this->authorizeTask('dashboard', ['admin.login', 'admin.super'])) {
return false;
}
$data = $this->post;
$flush = (isset($data['flush']) && $data['flush'] == true) ? true : false;
@@ -954,12 +973,17 @@ class AdminController extends AdminBaseController
];
} else {
$this->admin->json_response = ['status' => 'error', 'message' => 'Cannot connect to the GPM'];
return false;
}
} catch (\Exception $e) {
$this->admin->json_response = ['status' => 'error', 'message' => $e->getMessage()];
return false;
}
return true;
}
/**
@@ -968,12 +992,16 @@ class AdminController extends AdminBaseController
*/
protected function taskGetNotifications()
{
if (!$this->authorizeTask('dashboard', ['admin.login', 'admin.super'])) {
return false;
}
$cache = $this->grav['cache'];
if (!(bool)$this->grav['config']->get('system.cache.enabled') || !$notifications = $cache->fetch('notifications')) {
//No notifications cache (first time)
$this->admin->json_response = ['status' => 'success', 'notifications' => [], 'need_update' => true];
return;
return true;
}
$need_update = false;
@@ -990,7 +1018,7 @@ class AdminController extends AdminBaseController
} catch (\Exception $e) {
$this->admin->json_response = ['status' => 'error', 'message' => $e->getMessage()];
return;
return false;
}
$this->admin->json_response = [
@@ -998,6 +1026,8 @@ class AdminController extends AdminBaseController
'notifications' => $notifications,
'need_update' => $need_update
];
return true;
}
/**
@@ -1007,6 +1037,10 @@ class AdminController extends AdminBaseController
*/
protected function taskProcessNotifications()
{
if (!$this->authorizeTask('notifications', ['admin.login', 'admin.super'])) {
return false;
}
$cache = $this->grav['cache'];
$data = $this->post;
@@ -1156,8 +1190,8 @@ class AdminController extends AdminBaseController
'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
];
echo json_encode($json_response);
exit;
return $this->sendJsonResponse($json_response, 403);
}
//check if there are packages that have this as a dependency. Abort and show which ones
@@ -1172,8 +1206,8 @@ class AdminController extends AdminBaseController
}
$json_response = ['status' => 'error', 'message' => $message];
echo json_encode($json_response);
exit;
return $this->sendJsonResponse($json_response, 200);
}
try {
@@ -1181,8 +1215,8 @@ class AdminController extends AdminBaseController
$result = Gpm::uninstall($package, []);
} catch (\Exception $e) {
$json_response = ['status' => 'error', 'message' => $e->getMessage()];
echo json_encode($json_response);
exit;
return $this->sendJsonResponse($json_response, 200);
}
if ($result) {
@@ -1191,16 +1225,16 @@ class AdminController extends AdminBaseController
'dependencies' => $dependencies,
'message' => $this->admin->translate(is_string($result) ? $result : 'PLUGIN_ADMIN.UNINSTALL_SUCCESSFUL')
];
echo json_encode($json_response);
exit;
return $this->sendJsonResponse($json_response, 200);
}
$json_response = [
'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.UNINSTALL_FAILED')
];
echo json_encode($json_response);
exit;
return $this->sendJsonResponse($json_response, 200);
}
/**
@@ -1215,6 +1249,15 @@ class AdminController extends AdminBaseController
$package_name = isset($data['package_name']) ? $data['package_name'] : '';
$current_version = isset($data['current_version']) ? $data['current_version'] : '';
if (!$this->authorizeTask('install ' . $type, ['admin.' . $type, 'admin.super'])) {
$json_response = [
'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.INSUFFICIENT_PERMISSIONS_FOR_TASK')
];
$this->sendJsonResponse($json_response, 403);
}
$url = "https://getgrav.org/download/{$type}s/$slug/$current_version";
$result = Gpm::directInstall($url);
@@ -1344,7 +1387,7 @@ class AdminController extends AdminBaseController
}
$download = urlencode(base64_encode($backup));
$url = rtrim($this->grav['uri']->rootUrl(true), '/') . '/' . trim($this->admin->base,
$url = rtrim($this->grav['uri']->rootUrl(false), '/') . '/' . trim($this->admin->base,
'/') . '/task' . $param_sep . 'backup/download' . $param_sep . $download . '/admin-nonce' . $param_sep . Utils::getNonce('admin-form');
$log->content([
@@ -1574,6 +1617,8 @@ class AdminController extends AdminBaseController
{
$this->uri = $this->uri ?: $this->grav['uri'];
$uri = $this->uri->post('uri');
$order = $this->uri->post('order') ?: null;
if ($uri) {
/** @var UniformResourceLocator $locator */
$locator = $this->grav['locator'];
@@ -1584,8 +1629,11 @@ class AdminController extends AdminBaseController
$media_path = $page ? $page->path() : null;
}
if ($order) {
$order = array_map('trim', explode(',', $order));
}
return $media_path ? new Media($media_path) : null;
return $media_path ? new Media($media_path, $order) : null;
}
/**
@@ -1646,6 +1694,19 @@ class AdminController extends AdminBaseController
return false;
}
$filename = $_FILES['file']['name'];
// Handle bad filenames.
if (!Utils::checkFilename($filename)) {
$this->admin->json_response = [
'status' => 'error',
'message' => sprintf($this->admin->translate('PLUGIN_ADMIN.FILEUPLOAD_UNABLE_TO_UPLOAD'),
$filename, 'Bad filename')
];
return false;
}
$grav_limit = $config->get('system.media.upload_limit', 0);
// You should also check filesize here.
if ($grav_limit > 0 && $_FILES['file']['size'] > $grav_limit) {
@@ -1659,18 +1720,13 @@ class AdminController extends AdminBaseController
// Check extension
$fileParts = pathinfo($_FILES['file']['name']);
$fileExt = '';
if (isset($fileParts['extension'])) {
$fileExt = strtolower($fileParts['extension']);
}
$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
// If not a supported type, return
if (!$fileExt || !$config->get("media.types.{$fileExt}")) {
if (!$extension || !$config->get("media.types.{$extension}")) {
$this->admin->json_response = [
'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.UNSUPPORTED_FILE_TYPE') . ': ' . $fileExt
'message' => $this->admin->translate('PLUGIN_ADMIN.UNSUPPORTED_FILE_TYPE') . ': ' . $extension
];
return false;
@@ -1687,9 +1743,16 @@ class AdminController extends AdminBaseController
return false;
}
/** @var UniformResourceLocator $locator */
$locator = $this->grav['locator'];
$path = $media->path();
if ($locator->isStream($path)) {
$path = $locator->findResource($path, true, true);
}
// Upload it
if (!move_uploaded_file($_FILES['file']['tmp_name'],
sprintf('%s/%s', $media->path(), $_FILES['file']['name']))
sprintf('%s/%s', $path, $filename))
) {
$this->admin->json_response = [
'status' => 'error',
@@ -1701,13 +1764,12 @@ class AdminController extends AdminBaseController
// Add metadata if needed
$include_metadata = Grav::instance()['config']->get('system.media.auto_metadata_exif', false);
$filename = $fileParts['basename'];
$filename = str_replace(['@3x', '@2x'], '', $filename);
$basename = str_replace(['@3x', '@2x'], '', pathinfo($filename, PATHINFO_BASENAME));
$metadata = [];
if ($include_metadata && isset($media[$filename])) {
$img_metadata = $media[$filename]->metadata();
if ($include_metadata && isset($media[$basename])) {
$img_metadata = $media[$basename]->metadata();
if ($img_metadata) {
$metadata = $img_metadata;
}
@@ -1750,6 +1812,11 @@ class AdminController extends AdminBaseController
$filename = !empty($this->post['filename']) ? $this->post['filename'] : null;
// Handle bad filenames.
if (!Utils::checkFilename($filename)) {
$filename = null;
}
if (!$filename) {
$this->admin->json_response = [
'status' => 'error',
@@ -1759,7 +1826,13 @@ class AdminController extends AdminBaseController
return false;
}
/** @var UniformResourceLocator $locator */
$locator = $this->grav['locator'];
$targetPath = $media->path() . '/' . $filename;
if ($locator->isStream($targetPath)) {
$targetPath = $locator->findResource($targetPath, true, true);
}
$fileParts = pathinfo($filename);
$found = false;
@@ -1781,7 +1854,13 @@ class AdminController extends AdminBaseController
// Remove Extra Files
foreach (scandir($media->path(), SCANDIR_SORT_NONE) as $file) {
if (preg_match("/{$fileParts['filename']}@\d+x\.{$fileParts['extension']}(?:\.meta\.yaml)?$|{$filename}\.meta\.yaml$/", $file)) {
$result = unlink($media->path() . '/' . $file);
$targetPath = $media->path() . '/' . $file;
if ($locator->isStream($targetPath)) {
$targetPath = $locator->findResource($targetPath, true, true);
}
$result = unlink($targetPath);
if (!$result) {
$this->admin->json_response = [
@@ -1825,9 +1904,9 @@ class AdminController extends AdminBaseController
*/
protected function taskProcessMarkdown()
{
/*if (!$this->authorizeTask('process markdown', ['admin.pages', 'admin.super'])) {
return;
}*/
if (!$this->authorizeTask('process markdown', ['admin.pages', 'admin.super'])) {
return false;
}
try {
$page = $this->admin->page(true);
@@ -1843,6 +1922,7 @@ class AdminController extends AdminBaseController
$this->preparePage($page, true);
$page->header();
$page->templateFormat('html');
// Add theme template paths to Twig loader
$template_paths = $this->grav['locator']->findResources('theme://templates');
@@ -2153,6 +2233,10 @@ class AdminController extends AdminBaseController
*/
protected function taskSwitchlanguage()
{
if (!$this->authorizeTask('switch language', ['admin.pages', 'admin.super'])) {
return false;
}
$data = (array)$this->data;
if (isset($data['lang'])) {
@@ -2176,6 +2260,68 @@ class AdminController extends AdminBaseController
$admin_route = $this->admin->base;
$this->setRedirect('/' . $language . $admin_route . '/' . $redirect);
return true;
}
/**
* Handle direct install.
*/
protected function taskDirectInstall()
{
if (!$this->authorizeTask('install', ['admin.super'])) {
return false;
}
$file_path = isset($this->data['file_path']) ? $this->data['file_path'] : null ;
if (isset($_FILES['uploaded_file'])) {
// Check $_FILES['file']['error'] value.
switch ($_FILES['uploaded_file']['error']) {
case UPLOAD_ERR_OK:
break;
case UPLOAD_ERR_NO_FILE:
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.NO_FILES_SENT'), 'error');
return false;
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.EXCEEDED_FILESIZE_LIMIT'), 'error');
return false;
case UPLOAD_ERR_NO_TMP_DIR:
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.UPLOAD_ERR_NO_TMP_DIR'), 'error');
return false;
default:
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.UNKNOWN_ERRORS'), 'error');
return false;
}
$file_path = $_FILES['uploaded_file']['tmp_name'];
// Handle bad filenames.
if (!Utils::checkFilename(basename($file_path))) {
$this->admin->json_response = [
'status' => 'error',
'message' => $this->admin->translate('PLUGIN_ADMIN.UNKNOWN_ERRORS')
];
return false;
}
}
$result = Gpm::directInstall($file_path);
if ($result === true) {
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.INSTALLATION_SUCCESSFUL'), 'info');
} else {
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.INSTALLATION_FAILED') . ': ' . $result,
'error');
}
$this->setRedirect('/tools');
return true;
}
/**
@@ -2249,49 +2395,5 @@ class AdminController extends AdminBaseController
return $filename . '.md';
}
/**
* Handle direct install.
*/
protected function taskDirectInstall()
{
$file_path = isset($this->data['file_path']) ? $this->data['file_path'] : null ;
if (isset($_FILES['uploaded_file'])) {
// Check $_FILES['file']['error'] value.
switch ($_FILES['uploaded_file']['error']) {
case UPLOAD_ERR_OK:
break;
case UPLOAD_ERR_NO_FILE:
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.NO_FILES_SENT'), 'error');
return false;
case UPLOAD_ERR_INI_SIZE:
case UPLOAD_ERR_FORM_SIZE:
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.EXCEEDED_FILESIZE_LIMIT'), 'error');
return false;
case UPLOAD_ERR_NO_TMP_DIR:
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.UPLOAD_ERR_NO_TMP_DIR'), 'error');
return false;
default:
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.UNKNOWN_ERRORS'), 'error');
return false;
}
$file_path = $_FILES['uploaded_file']['tmp_name'];
}
$result = Gpm::directInstall($file_path);
if ($result === true) {
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.INSTALLATION_SUCCESSFUL'), 'info');
} else {
$this->admin->setMessage($this->admin->translate('PLUGIN_ADMIN.INSTALLATION_FAILED') . ': ' . $result,
'error');
}
$this->setRedirect('/tools');
return true;
}
}
+24 -4
View File
@@ -1,15 +1,35 @@
{
"name": "grav-plugin-admin",
"type": "grav-plugin",
"description": "Admin plugin for Grav CMS",
"keywords": ["admin", "plugin", "manager", "panel"],
"homepage": "https://github.com/trilbymedia/grav-plugin-admin",
"license": "MIT",
"authors": [
{
"name": "Team Grav",
"email": "devs@getgrav.org",
"homepage": "http://getgrav.org",
"role": "Developer"
}
],
"require": {
"php": ">=5.6.4",
"composer/semver": "^1.4",
"fguillot/picofeed": "@stable"
},
"require-dev": {
"codeception/codeception": "^2.1",
"fzaninotto/faker": "^1.5",
"symfony/yaml": "~2.8",
"symfony/console": "~2.8",
"symfony/finder": "~2.8",
"symfony/event-dispatcher": "~2.8"
"symfony/yaml": "~3.4",
"symfony/console": "~3.4",
"symfony/finder": "~3.4",
"symfony/event-dispatcher": "~3.4"
},
"config": {
"platform": {
"php": "5.6.4"
}
},
"scripts": {
"test": "vendor/bin/codecept run unit",
+372 -478
View File
File diff suppressed because it is too large Load Diff
+15
View File
@@ -0,0 +1,15 @@
{
"project":"grav-plugin-admin",
"platforms":{
"grav":{
"nodes":{
"plugin":[
{
"source":"/",
"destination":"/user/plugins/admin"
}
]
}
}
}
}
+28 -2
View File
@@ -392,6 +392,10 @@ PLUGIN_ADMIN:
DISPLAY_ERRORS_HELP: "Display full backtrace-style error page"
LOG_ERRORS: "Log errors"
LOG_ERRORS_HELP: "Log errors to /logs folder"
LOG_HANDLER: "Log handler"
LOG_HANDLER_HELP: "Where to output the logs"
SYSLOG_FACILITY: "Syslog facility"
SYSLOG_FACILITY_HELP: "Syslog facility for output"
DEBUGGER: "Debugger"
DEBUGGER_HELP: "Enable Grav debugger and following settings"
DEBUG_TWIG: "Debug Twig"
@@ -441,6 +445,7 @@ PLUGIN_ADMIN:
PAGE_FILE: "Page Template"
PAGE_FILE_HELP: "Page template file name, and by default the display template for this page"
NO_USER_ACCOUNTS: "No user accounts found, please create one first..."
NO_USER_EXISTS: "No local user exists for this account, cannot save..."
REDIRECT_TRAILING_SLASH: "Redirect trailing slash"
REDIRECT_TRAILING_SLASH_HELP: "Perform a 301 redirect rather than transparently handling trailing slash URIs."
DEFAULT_DATE_FORMAT: "Page date format"
@@ -502,7 +507,6 @@ PLUGIN_ADMIN:
ADD_FOLDER: "Add Folder"
PROXY_URL: "Proxy URL"
PROXY_URL_HELP: "Enter the proxy HOST or IP and PORT"
INVALID_FRONTMATTER_COULD_NOT_SAVE: "Invalid frontmatter, could not save"
NOTHING_TO_SAVE: "Nothing to Save"
FILE_ERROR_ADD: "An error occurred while trying to add the file"
FILE_ERROR_UPLOAD: "An error occurred while trying to upload the file"
@@ -554,7 +558,6 @@ PLUGIN_ADMIN:
CANNOT_ADD_FILES_PAGE_NOT_SAVED: "NOTE: Page must be saved before you can upload files to it."
DROP_FILES_HERE_TO_UPLOAD: "Drop your files here or <strong>click in this area</strong>"
INSERT: "Insert"
METADATA: "Metadata"
UNDO: "Undo"
REDO: "Redo"
HEADERS: "Headers"
@@ -713,3 +716,26 @@ PLUGIN_ADMIN:
ROUTE_ALIASES: "Route Aliases"
OPEN_NEW_TAB: "Open in new tab"
PAGEMEDIA_RESIZER: "> The following settings apply to images uploaded through the page media. Resize Width / Height will automatically resize down and proportionally an image if exceeds the limits set in the inputs. Resolution min and max values define the size ranges for uploaded images. Set the fields to 0 to prevent any manipulation."
SESSION_INITIALIZE: "Initialize Session"
SESSION_INITIALIZE_HELP: "Makes Grav to start a session. This feature is needed to make any user interaction to work, such as login, forms etc. Admin plugin isn't affected by this setting."
STRICT_YAML_COMPAT: "YAML Compatibility"
STRICT_YAML_COMPAT_HELP: "Falls back to Symfony 2.4 YAML parser if Native or 3.4 parser fails"
STRICT_TWIG_COMPAT: "Twig Compatibility"
STRICT_TWIG_COMPAT_HELP: "Enables deprecated Twig autoescape setting. When disabled, |raw filter is required to output HTML as Twig will autoescape output"
SECURITY: "Security"
XSS_SECURITY: "XSS Security"
XSS_WHITELIST_PERMISSIONS: "Whitelist Permissions"
XSS_WHITELIST_PERMISSIONS_HELP: "Users with these permissions will skip the XSS rules when saving content"
XSS_ON_EVENTS: "On-events"
XSS_INVALID_PROTOCOLS: "Enable Invalid protocols"
XSS_MOZ_BINDINGS: "Moz bindings"
XSS_HTML_INLINE_STYLES: "HTML inline styles"
XSS_DANGEROUS_TAGS: "Dangerous HTML tags"
XSS_DANGEROUS_TAGS_LIST: "Dangerous HTML tags list"
XSS_ONSAVE_ISSUE: "Save failed: XSS issue detected..."
XSS_ISSUE: "<strong>NOTICE:</strong> Grav found potential XSS issues in <strong>%s</strong>"
UPLOADS_SECURITY: "Uploads Security"
UPLOADS_DANGEROUS_EXTENSIONS: "Dangerous Extensions"
UPLOADS_DANGEROUS_EXTENSIONS_HELP: "Block these extensions from being uploaded no matter the accepted MIME types"
@@ -28,6 +28,10 @@ class Template {
return this.container.find('[data-grav-array-mode="value_only"]:first').length || false;
}
isTextArea() {
return this.container.data('grav-array-textarea') || false;
}
shouldBeDisabled() {
// check for toggleables, if field is toggleable and it's not enabled, render disabled
let toggle = this.container.closest('.form-field').find('[data-grav-field="toggleable"] input[type="checkbox"]');
@@ -35,20 +39,22 @@ class Template {
}
getNewRow() {
let tpl = '';
let tpl = '';const value = this.isTextArea()
? `<textarea ${this.shouldBeDisabled() ? 'disabled="disabled"' : ''} data-grav-array-type="value" name="" placeholder="${this.getValuePlaceholder()}"></textarea>`
: `<input ${this.shouldBeDisabled() ? 'disabled="disabled"' : ''} data-grav-array-type="value" type="text" name="" value="" placeholder="${this.getValuePlaceholder()}" />`;
if (this.isValueOnly()) {
tpl += `
<div class="form-row array-field-value_only" data-grav-array-type="row">
<span data-grav-array-action="sort" class="fa fa-bars"></span>
<input ${this.shouldBeDisabled() ? 'disabled="disabled"' : ''} data-grav-array-type="value" type="text" value="" placeholder="${this.getValuePlaceholder()}" />
${value}
`;
} else {
tpl += `
<div class="form-row" data-grav-array-type="row">
<span data-grav-array-action="sort" class="fa fa-bars"></span>
<input ${this.shouldBeDisabled() ? 'disabled="disabled"' : ''} data-grav-array-type="key" type="text" value="" placeholder="${this.getKeyPlaceholder()}" />
<input ${this.shouldBeDisabled() ? 'disabled="disabled"' : ''} data-grav-array-type="value" type="text" name="" value="" placeholder="${this.getValuePlaceholder()}" />
${value}
`;
}
@@ -1,8 +1,8 @@
import $ from 'jquery';
import { config } from 'grav-config';
import { config, uri_params } from 'grav-config';
import request from '../../utils/request';
const insertTextAt = (string, index, text) => [string.slice(0, index), text, string.slice(index)].join('');
// const insertTextAt = (string, index, text) => [string.slice(0, index), text, string.slice(index)].join('');
export default class FilePickerField {
@@ -45,10 +45,11 @@ export default class FilePickerField {
let parent = field.closest('[data-grav-filepicker]');
let name = parent.data('name');
let value = parent.data('value');
let params = JSON.stringify(uri_params || '{}');
request(url, {
method: 'post',
body: { name }
body: { name, params }
}, (response) => {
if (typeof response.files === 'undefined') {
return;
@@ -78,14 +79,12 @@ export default class FilePickerField {
let renderOption = function renderOption(item, escape) {
let image = '';
if (imagesPreview && folder && (!item.status || item.status === 'available') && item.name.match(/\.(jpg|jpeg|png|gif)$/i)) {
const fallback2x = insertTextAt(`${config.base_url_relative}/../${folder}/${item.name}`, -4, '@2x');
const fallback3x = insertTextAt(`${config.base_url_relative}/../${folder}/${item.name}`, -4, '@3x');
// const fallback2x = insertTextAt(`${config.base_url_relative}/../${folder}/${item.name}`, -4, '@2x');
// const fallback3x = insertTextAt(`${config.base_url_relative}/../${folder}/${item.name}`, -4, '@3x');
const source = thumbs[item.name] || `${config.base_url_relative}/../${folder}/${item.name}`;
image = `
<img class="filepicker-field-image"
src="${source}"
onerror="if(this.src==='${fallback2x}'){this.src='${fallback3x}';this.onerror='';}else{this.src='${fallback2x}'}" />`;
// onerror="if(this.src==='${fallback2x}'){this.src='${fallback3x}';}else{this.src='${fallback2x}'}"
image = `<img class="filepicker-field-image" src="${source}" />`;
}
return `<div>
@@ -1,5 +1,6 @@
import $ from 'jquery';
import Dropzone from 'dropzone';
import EXIF from 'exif-js';
import request from '../../utils/request';
import { config, translations } from 'grav-config';
@@ -70,6 +71,8 @@ const DropzoneMediaConfig = {
</div>`.trim()
};
global.EXIF = EXIF;
const ACCEPT_FUNC = function(file, done, settings) {
const resolution = settings.resolution;
if (!resolution) return done();
@@ -162,7 +165,6 @@ export default class FilesField {
}
getURI() {
console.log(this.container.data('mediaUri'));
return this.container.data('mediaUri') || '';
}
View File
@@ -20,9 +20,8 @@ export default class SelectizeField {
let field = (isInput ? element : element.find('input, select'));
if (!field.length || field.get(0).selectize) { return; }
field.selectize($.extend({}, data, {
plugins: ['required-fix']
}));
const plugins = $.merge(data.plugins ? data.plugins : [], ['required-fix']);
field.selectize($.extend({}, data, { plugins }));
this.elements.push(field.data('selectize'));
}
+2 -3
View File
@@ -14,9 +14,8 @@ import request from './utils/request';
import './utils/2fa';
// bootstrap jQuery extensions
import 'bootstrap/js/transition';
import 'bootstrap/js/dropdown';
import 'bootstrap/js/collapse';
import './utils/bootstrap-transition';
import './utils/bootstrap-collapse';
// tabs memory
import './utils/tabs-memory';
@@ -74,7 +74,8 @@ export default class PageMedia extends FilesField {
}
fetchMedia() {
const body = { uri: this.getURI() };
const order = this.container.closest('.form-field').find('[name="data[header][media_order]"]').val();
const body = { uri: this.getURI(), order };
let url = this.urls.fetch;
request(url, { method: 'post', body }, (response) => {
@@ -0,0 +1,210 @@
import jQuery from 'jquery';
/* ========================================================================
* Bootstrap: collapse.js v3.4.0
* http://getbootstrap.com/javascript/#collapse
* ========================================================================
* Copyright 2011-2016 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* ======================================================================== */
/* jshint latedef: false */
+(function($) {
'use strict';
// COLLAPSE PUBLIC CLASS DEFINITION
// ================================
var Collapse = function(element, options) {
this.$element = $(element);
this.options = $.extend({}, Collapse.DEFAULTS, options);
this.$trigger = $('[data-toggle="collapse"][href="#' + element.id + '"],' +
'[data-toggle="collapse"][data-target="#' + element.id + '"]');
this.transitioning = null;
if (this.options.parent) {
this.$parent = this.getParent();
} else {
this.addAriaAndCollapsedClass(this.$element, this.$trigger);
}
if (this.options.toggle) this.toggle();
};
Collapse.VERSION = '3.4.0';
Collapse.TRANSITION_DURATION = 350;
Collapse.DEFAULTS = {
toggle: true
};
Collapse.prototype.dimension = function() {
var hasWidth = this.$element.hasClass('width');
return hasWidth ? 'width' : 'height';
};
Collapse.prototype.show = function() {
if (this.transitioning || this.$element.hasClass('in')) return;
var activesData;
var actives = this.$parent && this.$parent.children('.panel').children('.in, .collapsing');
if (actives && actives.length) {
activesData = actives.data('bs.collapse');
if (activesData && activesData.transitioning) return;
}
var startEvent = $.Event('show.bs.collapse');
this.$element.trigger(startEvent);
if (startEvent.isDefaultPrevented()) return;
if (actives && actives.length) {
Plugin.call(actives, 'hide');
activesData || actives.data('bs.collapse', null);
}
var dimension = this.dimension();
this.$element
.removeClass('collapse')
.addClass('collapsing')[dimension](0)
.attr('aria-expanded', true);
this.$trigger
.removeClass('collapsed')
.attr('aria-expanded', true);
this.transitioning = 1;
var complete = function() {
this.$element
.removeClass('collapsing')
.addClass('collapse in')[dimension]('');
this.transitioning = 0;
this.$element
.trigger('shown.bs.collapse');
};
if (!$.support.transition) return complete.call(this);
var scrollSize = $.camelCase(['scroll', dimension].join('-'));
this.$element
.one('bsTransitionEnd', $.proxy(complete, this))
.emulateTransitionEnd(Collapse.TRANSITION_DURATION)[dimension](this.$element[0][scrollSize]);
};
Collapse.prototype.hide = function() {
if (this.transitioning || !this.$element.hasClass('in')) return;
var startEvent = $.Event('hide.bs.collapse');
this.$element.trigger(startEvent);
if (startEvent.isDefaultPrevented()) return;
var dimension = this.dimension();
this.$element[dimension](this.$element[dimension]())[0].offsetHeight;
this.$element
.addClass('collapsing')
.removeClass('collapse in')
.attr('aria-expanded', false);
this.$trigger
.addClass('collapsed')
.attr('aria-expanded', false);
this.transitioning = 1;
var complete = function() {
this.transitioning = 0;
this.$element
.removeClass('collapsing')
.addClass('collapse')
.trigger('hidden.bs.collapse');
};
if (!$.support.transition) return complete.call(this);
this.$element[dimension](0)
.one('bsTransitionEnd', $.proxy(complete, this))
.emulateTransitionEnd(Collapse.TRANSITION_DURATION);
};
Collapse.prototype.toggle = function() {
this[this.$element.hasClass('in') ? 'hide' : 'show']();
};
Collapse.prototype.getParent = function() {
return $(this.options.parent)
.find('[data-toggle="collapse"][data-parent="' + this.options.parent + '"]')
.each($.proxy(function(i, element) {
var $element = $(element);
this.addAriaAndCollapsedClass(getTargetFromTrigger($element), $element);
}, this))
.end();
};
Collapse.prototype.addAriaAndCollapsedClass = function($element, $trigger) {
var isOpen = $element.hasClass('in');
$element.attr('aria-expanded', isOpen);
$trigger
.toggleClass('collapsed', !isOpen)
.attr('aria-expanded', isOpen);
};
function getTargetFromTrigger($trigger) {
var href;
var target = $trigger.attr('data-target') ||
(href = $trigger.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, ''); // strip for ie7
return $(target);
}
// COLLAPSE PLUGIN DEFINITION
// ==========================
function Plugin(option) {
return this.each(function() {
var $this = $(this);
var data = $this.data('bs.collapse');
var options = $.extend({}, Collapse.DEFAULTS, $this.data(), typeof option === 'object' && option);
if (!data && options.toggle && /show|hide/.test(option)) options.toggle = false;
if (!data) $this.data('bs.collapse', (data = new Collapse(this, options)));
if (typeof option === 'string') data[option]();
});
}
var old = $.fn.collapse;
$.fn.collapse = Plugin;
$.fn.collapse.Constructor = Collapse;
// COLLAPSE NO CONFLICT
// ====================
$.fn.collapse.noConflict = function() {
$.fn.collapse = old;
return this;
};
// COLLAPSE DATA-API
// =================
$(document).on('click.bs.collapse.data-api', '[data-toggle="collapse"]', function(e) {
var $this = $(this);
if (!$this.attr('data-target')) e.preventDefault();
var $target = getTargetFromTrigger($this);
var data = $target.data('bs.collapse');
var option = data ? 'toggle' : $this.data();
Plugin.call($target, option);
});
}(jQuery));
@@ -0,0 +1,52 @@
import jQuery from 'jquery';
+(function($) {
'use strict';
// CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/)
// ============================================================
function transitionEnd() {
var el = document.createElement('bootstrap');
var transEndEventNames = {
WebkitTransition: 'webkitTransitionEnd',
MozTransition: 'transitionend',
OTransition: 'oTransitionEnd otransitionend',
transition: 'transitionend'
};
for (var name in transEndEventNames) {
if (el.style[name] !== undefined) {
return { end: transEndEventNames[name] };
}
}
return false; // explicit for ie8 ( ._.)
}
// http://blog.alexmaccaw.com/css-transitions
$.fn.emulateTransitionEnd = function(duration) {
var called = false;
var $el = this;
$(this).one('bsTransitionEnd', function() { called = true; });
var callback = function() { if (!called) $($el).trigger($.support.transition.end); };
setTimeout(callback, duration);
return this;
};
$(function() {
$.support.transition = transitionEnd();
if (!$.support.transition) return;
$.event.special.bsTransitionEnd = {
bindType: $.support.transition.end,
delegateType: $.support.transition.end,
handle: function(e) {
if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments);
}
};
});
}(jQuery));
+1 -1
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:"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 */
/*# sourceMappingURL=fonts.css.map */
@@ -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,cAAU,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\n\n\n\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,uGAAI,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
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,3 +1,3 @@
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{font-family:"Monaco", "Consolas", "Lucida Console", monospace}
/*# sourceMappingURL=../css-compiled/simple-fonts.css.map */
/*# sourceMappingURL=simple-fonts.css.map */
@@ -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,AAAE,CAClB,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#admin-menu li, .form-tabs > label, .label,\nbody .CodeMirror pre {\n font-family: \"Helvetica Neue\", \"Helvetica\", \"Tahoma\", \"Geneva\", \"Arial\", sans-serif;\n}\ncode, kbd, pre, samp {\n font-family: \"Monaco\", \"Consolas\", \"Lucida Console\", monospace;\n}\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,WAAW,CAAC,EAAE,CAAE,UAAU,CAAG,KAAK,CAAE,MAAM,CAC1C,IAAI,CAAC,WAAW,CAAC,GAAG,AAAC,CACjB,WAAW,CAAE,sEAAsE,CACtF,AACD,AAAA,IAAI,CAAE,GAAG,CAAE,GAAG,CAAE,IAAI,AAAE,CAClB,WAAW,CAAE,iDAAiD,CACjE"}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
View File
View File
File diff suppressed because one or more lines are too long
+80
View File
@@ -0,0 +1,80 @@
(function($) {
$(function() {
/**
* polyfill for html5 form attr
*/
// detect if browser supports this
var sampleElement = $('[form]').get(0);
var isIE11 = !(window.ActiveXObject) && "ActiveXObject" in window;
if (sampleElement && window.HTMLFormElement && sampleElement.form instanceof HTMLFormElement && !isIE11) {
// browser supports it, no need to fix
return;
}
/**
* Append a field to a form
*
*/
$.fn.appendField = function(data) {
// for form only
if (!this.is('form')) return;
// wrap data
if (!$.isArray(data) && data.name && data.value) {
data = [data];
}
var $form = this;
// attach new params
$.each(data, function(i, item) {
$('<input/>')
.attr('type', 'hidden')
.attr('name', item.name)
.val(item.value).appendTo($form);
});
return $form;
};
/**
* Find all input fields with form attribute point to jQuery object
*
*/
$('form[id]').submit(function(e) {
// serialize data
var data = $('[form=' + this.id + ']').serializeArray();
// append data to form
$(this).appendField(data);
}).each(function() {
var form = this,
$fields = $('[form=' + this.id + ']');
$fields.filter('button, input')
.filter('[type=reset],[type=submit],[type=image],button')
.click(function() {
var type = this.type.toLowerCase();
if (type === 'reset') {
// reset form
form.reset();
// for elements outside form
$fields.each(function() {
this.value = this.defaultValue;
this.checked = this.defaultChecked;
}).filter('select').each(function() {
$(this).find('option').each(function() {
this.selected = this.defaultSelected;
});
});
} else {
$(form).appendField({
name: this.name,
value: this.value
}).submit();
}
});
});
});
})(jQuery);
File diff suppressed because one or more lines are too long
+2 -1
View File
@@ -13,7 +13,6 @@
"author": "RocketTheme, LLC",
"license": "MIT",
"dependencies": {
"bootstrap": "^3.3.7",
"chartist": "0.11.0",
"codemirror": "^5.30.0",
"cookies-js": "^1.2.3",
@@ -22,11 +21,13 @@
"eonasdan-bootstrap-datetimepicker": "^4.17.47",
"es6-map": "^0.1.5",
"es6-promise": "^4.1.1",
"exif-js": "^2.3.0",
"gemini-scrollbar": "^1.5.2",
"immutable": "^3.8.1",
"immutablediff": "^0.4.4",
"js-yaml": "^3.10.0",
"mout": "^1.0.0",
"popper.js": "^1.14.4",
"rangetouch": "^1.0.5",
"remodal": "^1.1.1",
"selectize": "^0.12.4",
View File
@@ -251,7 +251,7 @@ tr {
&:focus, &:hover {
color: $button-text;
background-color: $button-bg;
background-color: darken($button-bg, 5%);
}
}
@@ -298,7 +298,7 @@ tr {
}
}
.instructions {
.instructions, p {
color: rgba($nav-text,0.8);
}
@@ -306,6 +306,10 @@ tr {
background: darken($nav-bg, 5%) url(../images/logo.svg) 50% 50% no-repeat;
}
h5 {
color: $nav-text;
}
form {
input {
background: lighten($nav-bg, 3%);
@@ -674,6 +678,10 @@ form {
input {
color: $form-field-text;
}
a.remove {
color: $page-bg !important;
}
}
}
}
@@ -909,16 +917,16 @@ form {
}
.form-fieldset {
background-color: #f7f7f7;
border: 2px solid #e1e1e1;
background-color: darken($content-bg, 1%);
border: 1px solid $form-border;
}
.form-fieldset--label {
background-color: #f3f3f3;
background-color: darken($content-bg, 4%);
&:hover,
.form-fieldset input:checked + & {
background-color: #eee;
background-color: darken($content-bg, 6%);
}
}
#admin-main {
@@ -252,14 +252,14 @@ $preview-height: 150px;
overflow: hidden;
img {
position: absolute;
left: 50%;
top: 50%;
height: auto;
width: 100%;
-webkit-transform: translate(-50%,-50%);
-ms-transform: translate(-50%,-50%);
transform: translate(-50%,-50%);
//position: absolute;
//left: 50%;
//top: 50%;
//height: auto;
//width: 100%;
//-webkit-transform: translate(-50%,-50%);
//-ms-transform: translate(-50%,-50%);
//transform: translate(-50%,-50%);
}
}
}
@@ -272,7 +272,7 @@ $preview-height: 150px;
img {
position: absolute;
object-fit: cover;
object-fit: contain;
top: 0;
left: 0;
width: $preview-width;
@@ -604,7 +604,7 @@ textarea.frontmatter {
letter-spacing: normal;
}
.form-fieldset {
margin: 1rem 2rem;
margin: 1rem 1.5rem;
}
.form-fieldset--label {
@@ -672,5 +672,3 @@ textarea.frontmatter {
}
}
@@ -127,6 +127,10 @@
right: 0;
padding: 1.5rem 3rem;
.button {
margin-bottom: 2px;
}
button:first-child {
margin-right: 1rem;
}
@@ -1,46 +1,56 @@
{% extends "forms/field.html.twig" %}
{% macro renderer(key, text, field, scope) %}
{% if text is not iterable %}
<div class="form-row{% if field.value_only %} array-field-value_only{% endif %}"
data-grav-array-type="row">
<span data-grav-array-action="sort" class="fa fa-bars"></span>
{% if field.value_only != true %}
{% if key == '0' and text == '' %}
{% set key = '' %}
{% endif %}
<input
data-grav-array-type="key"
type="text" value="{{ key }}"
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
placeholder="{{ field.placeholder_key|e|tu }}" />
{% endif %}
{% if field.value_type == 'textarea' %}
<textarea
data-grav-array-type="value"
name="{{ ((scope ~ field.name)|fieldName) ~ '[' ~ key ~ ']' }}"
placeholder="{{ field.placeholder_value|e|tu }}"
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}>{{ text }}</textarea>
{% else %}
<input
data-grav-array-type="value"
type="text"
name="{{ ((scope ~ field.name)|fieldName) ~ '[' ~ key ~ ']' }}"
placeholder="{{ field.placeholder_value|e|tu }}"
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
value={% if text == 'true' %}true{% elseif text == 'false' %}false{% else %}"{{ text|join(', ')|e }}"{% endif %} />
{% endif %}
<span data-grav-array-action="rem" class="fa fa-minus"></span>
<span data-grav-array-action="add" class="fa fa-plus"></span>
</div>
{% endif %}
{% endmacro %}
{% import _self as array_field %}
{% block global_attributes %}
data-grav-array-name="{{ (scope ~ field.name)|fieldName }}"
data-grav-array-keyname="{{ field.placeholder_key|e|tu }}"
data-grav-array-valuename="{{ field.placeholder_value|e|tu }}"
data-grav-array-textarea="{{ field.value_type == 'textarea' }}"
{{ parent() }}
{% endblock %}
{% macro renderer(key, text, field, scope) %}
{% if text is not iterable %}
<div class="form-row{% if field.value_only %} array-field-value_only{% endif %}"
data-grav-array-type="row">
<span data-grav-array-action="sort" class="fa fa-bars"></span>
{% if field.value_only != true %}
{% if key == '0' and text == '' %}
{% set key = '' %}
{% endif %}
<input
data-grav-array-type="key"
type="text" value="{{ key }}"
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
placeholder="{{ field.placeholder_key|e|tu }}" />
{% endif %}
<input
data-grav-array-type="value"
type="text"
name="{{ ((scope ~ field.name)|fieldName) ~ '[' ~ key ~ ']' }}"
placeholder="{{ field.placeholder_value|e|tu }}"
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
value={% if text == 'true' %}true{% elseif text == 'false' %}false{% else %}"{{ text|join(', ')|e }}"{% endif %} />
<span data-grav-array-action="rem" class="fa fa-minus"></span>
<span data-grav-array-action="add" class="fa fa-plus"></span>
</div>
{% endif %}
{% endmacro %}
{% block input %}
{% import _self as array_field %}
<div class="{{ field.size }}" data-grav-array-type="container"{% if field.value_only %} data-grav-array-mode="value_only"{% endif %}{{ value|length <= 1 ? ' class="one-child"' : '' }}>
{% if value|length %}
{% for key, text in value -%}
@@ -58,18 +68,26 @@
<div class="form-row" data-grav-array-type="row">
<span data-grav-array-action="sort" class="fa fa-bars"></span>
{% if field.value_only != true %}
<input
data-grav-array-type="key"
type="text"
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
placeholder="{{ field.placeholder_key|e|tu }}" />
<input
data-grav-array-type="key"
type="text"
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
placeholder="{{ field.placeholder_key|e|tu }}" />
{% endif %}
{% if field.value_type == 'textarea' %}
<textarea
data-grav-array-type="value"
name="{{ (scope ~ field.name)|fieldName }}"
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
placeholder="{{ field.placeholder_value|e|tu }}"></textarea>
{% else %}
<input
data-grav-array-type="value"
type="text"
name="{{ (scope ~ field.name)|fieldName }}"
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
placeholder="{{ field.placeholder_value|e|tu }}" />
{% endif %}
<input
data-grav-array-type="value"
type="text"
name="{{ (scope ~ field.name)|fieldName }}"
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
placeholder="{{ field.placeholder_value|e|tu }}" />
<span data-grav-array-action="rem" class="fa fa-minus"></span>
<span data-grav-array-action="add" class="fa fa-plus"></span>
</div>
@@ -32,13 +32,13 @@
{% if field.resizer is not defined or field.resizer not in ['off', 'false', 0] %}<div class="grav-editor-resizer"></div>{% endif %}
{% if field.description %}
<div class="form-extra-wrapper {{ field.size }} {{ field.wrapper_classes }}">
<span class="form-description">
{% if field.markdown %}
{{ field.description|tu|markdown(false)|raw }}
{% else %}
{{ field.description|tu|raw }}
{% endif %}
</span>
<span class="form-description">
{% if field.markdown %}
{{ field.description|tu|markdown(false)|raw }}
{% else %}
{{ field.description|tu|raw }}
{% endif %}
</span>
</div>
{% endif %}
</div>
@@ -1,9 +1,4 @@
{% extends "forms/field.html.twig" %}
{% set defaults = config.plugins.form %}
{% set files = defaults.files|merge(field|default([])) %}
{% set limit = not field.multiple ? 1 : files.limit %}
{% macro bytesToSize(bytes) -%}
{% spaceless %}
@@ -43,20 +38,26 @@
{% set real_path = global.admin.getPagePathFromToken(path) %}
{% set remove = global.file_url_remove ? global.file_url_remove : (global.base_url_relative ~ '/media.json') %}
{% set remove = uri.addNonce(
remove ~
'/route' ~ config.system.param_sep ~ base64_encode(global.base_path ~ '/' ~ real_path) ~
'/task' ~ config.system.param_sep ~ 'removeFileFromBlueprint' ~
'/proute' ~ config.system.param_sep ~ base64_encode(route) ~
'/blueprint' ~ config.system.param_sep ~ blueprint ~
'/type' ~ config.system.param_sep ~ type ~
'/field' ~ config.system.param_sep ~ files.name ~
'/path' ~ config.system.param_sep ~ base64_encode(value.path), 'admin-form', 'admin-nonce') %}
remove ~
'/route' ~ config.system.param_sep ~ base64_encode(global.base_path ~ '/' ~ real_path) ~
'/task' ~ config.system.param_sep ~ 'removeFileFromBlueprint' ~
'/proute' ~ config.system.param_sep ~ base64_encode(route) ~
'/blueprint' ~ config.system.param_sep ~ blueprint ~
'/type' ~ config.system.param_sep ~ type ~
'/field' ~ config.system.param_sep ~ files.name ~
'/path' ~ config.system.param_sep ~ base64_encode(value.path), 'admin-form', 'admin-nonce') %}
{% set file = value|merge({remove: remove, path: (uri.rootUrl == '/' ? '/' : uri.rootUrl ~ '/' ~ real_path) }) %}
<div class="hidden" data-file="{{ file|json_encode|e('html_attr') }}"></div>
{% endif %}
{% endmacro %}
{% import _self as macro %}
{% set defaults = config.plugins.form %}
{% set files = defaults.files|merge(field|default([])) %}
{% set limit = not field.multiple ? 1 : files.limit %}
{% block input %}
{% set upload_limit = config.system.media.upload_limit / 1024 / 1024 %}
{% set page_can_upload = exists or (type == 'page' and not exists and not (field.destination starts with '@self' or field.destination starts with 'self@')) %}
@@ -78,7 +79,7 @@
/>
{% for path, file in value %}
{{ _self.preview(path, file, _context) }}
{{ macro.preview(path, file, _context) }}
{% endfor %}
{% include 'forms/fields/hidden/hidden.html.twig' with {field: {name: '_json.' ~ field.name}, value:value|raw|json_encode} %}
</div>
@@ -45,16 +45,18 @@
{% if field.max is defined and not field.selectunique %}data-max="{{ field.max }}"{% endif %}
>
{% if fieldControls in ['top', 'both'] %}
<div class="collection-actions{{ not value|length ? ' hidden' : '' }}">
<button class="button" type="button" data-action="expand_all"
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}><i class="fa fa-chevron-circle-down"></i> {{ "PLUGIN_ADMIN.EXPAND_ALL"|e|tu }}</button>
<button class="button" type="button" data-action="collapse_all"
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}><i class="fa fa-chevron-circle-right"></i> {{ "PLUGIN_ADMIN.COLLAPSE_ALL"|e|tu }}</button>
<div class="collection-actions">
{% if collapsible %}
<button class="button" type="button" data-action="expand_all"
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}><i class="fa fa-chevron-circle-down"></i> {{ "PLUGIN_ADMIN.EXPAND_ALL"|e|tu }}</button>
<button class="button" type="button" data-action="collapse_all"
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}><i class="fa fa-chevron-circle-right"></i> {{ "PLUGIN_ADMIN.COLLAPSE_ALL"|e|tu }}</button>
{% endif %}
{% if field.sortby %}
<button class="button{{ not value|length ? ' hidden' : '' }}" type="button" data-action="sort" data-action-sort="{{ field.sortby }}" data-action-sort-dir="{{ field.sortby_dir|default('asc') }}"
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}><i class="fa fa-sort-amount-{{ field.sortby_dir|default('asc') }}"></i> {{ btnSortLabel|e|tu }} '{{ field.sortby }}'</button>
{% endif %}
<button class="button" type="button" data-action="add" data-action-add="top"
<button class="button" type="button" data-action="add" data-action-add="bottom"
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}><i class="fa fa-plus"></i> {{ btnLabel|e|tu }}</button>
</div>
{% endif %}
@@ -75,7 +77,7 @@
{% set childName = itemName -%}
{%- elseif childName starts with '.' -%}
{% set childKey = childName|trim('.') %}
{% set childValue = val[childName[1:]] %}
{% set childValue = val|nested(childName) %}
{% set childName = itemName ~ childName %}
{% else %}
{% set childKey = childName %}
@@ -1,13 +1,8 @@
{% extends "forms/field.html.twig" %}
{% block global_attributes %}
data-grav-array-name="{{ (scope ~ field.name)|fieldName }}"
data-grav-array-keyname="{{ field.placeholder_key|e|tu }}"
data-grav-array-valuename="{{ field.placeholder_value|e|tu }}"
{{ parent() }}
{% endblock %}
{% macro renderer(key, content, field, scope, level, parent_key, up_level) %}
{% import _self as self %}
{% macro field(value, key, level, globalvars, disable_name, hidden) %}
{% set name = 'data[' ~ globalvars.field.name|replace({'.': ']['}) ~ ']' ~ key %}
<div class="form-row array-field-value_only js__multilevel-field {{ level == 0 ? 'top' : '' }}"
@@ -28,14 +23,14 @@
{% if level == 0 %}
{{ _self.field(key, '', level, _context, true, (is_numeric(key) ? true : false)) }}
{{ self.field(key, '', level, _context, true, (is_numeric(key) ? true : false)) }}
{% if content is not iterable %}
{% set level2 = level + 1 %}
<div class="children-wrapper">
<div class="element-wrapper">
{{ _self.field(content, '[' ~ key ~ ']', level2, _context) }}
{{ self.field(content, '[' ~ key ~ ']', level2, _context) }}
</div>
</div>
{% endif %}
@@ -51,9 +46,9 @@
<div class="element-wrapper">
{% if not is_numeric(inner_key) %}
{% if (content|length > 1) %}
{{ _self.field(inner_key, parent_key, level, _context, true) }}
{{ self.field(inner_key, parent_key, level, _context, true) }}
{% else %}
{{ _self.field(inner_key, parent_key, level, _context) }}
{{ self.field(inner_key, parent_key, level, _context) }}
{% endif %}
{% set level2 = level + 1 %}
{% set up_level = true %}
@@ -70,7 +65,7 @@
{% endif %}
{% set last_key = (is_numeric(inner_key)) ? '' : inner_key %}
{{ _self.field(inner_content, parent_key ~ '[' ~ inner_key ~ ']', level2, _context) }}
{{ self.field(inner_content, parent_key ~ '[' ~ inner_key ~ ']', level2, _context) }}
{% if not is_numeric(inner_key) %}
</div>
@@ -79,19 +74,28 @@
{% else %}
{% set inner_parent_key = parent_key ~ '[' ~ inner_key ~ ']' %}
{{ _self.renderer(inner_key, inner_content, field, scope, level, inner_parent_key, up_level) }}
{{ self.renderer(inner_key, inner_content, field, scope, level, inner_parent_key, up_level) }}
{% endif %}
</div>
{% endfor %}
</div>
{% endmacro %}
{% import _self as macro %}
{% block global_attributes %}
data-grav-array-name="{{ (scope ~ field.name)|fieldName }}"
data-grav-array-keyname="{{ field.placeholder_key|e|tu }}"
data-grav-array-valuename="{{ field.placeholder_value|e|tu }}"
{{ parent() }}
{% endblock %}
{% block input %}
<div data-id="{{random_string()}}" data-grav-multilevel-field data-grav-array-type="container" data-grav-array-mode="value_only"{{ value|length <= 1 ? ' class="one-child"' : '' }}>
{% if value|length %}
{% for key, content in value -%}
<div class="element-wrapper">
{{ _self.renderer(key, content, field, scope, 0, '[' ~ key ~ ']', true) }}
{{ macro.renderer(key, content, field, scope, 0, '[' ~ key ~ ']', true) }}
</div>
{% endfor %}
{%- else -%}
@@ -1,6 +1,6 @@
{% set value = (value is null ? field.default : value) %}
{% set siblings = data.parent.children %}
{% set canOrder = data.order %}
{% set siblings = context.parent.children %}
{% set canOrder = context.order %}
{% set vertical = field.style == 'vertical' %}
<div class="form-field grid pure-g{% if vertical %} vertical{% endif %}">
@@ -14,6 +14,8 @@
{% endfor %}
{% endmacro %}
{% import _self as macro %}
{% block global_attributes %}
data-grav-selectize="{{ (field.selectize is defined ? field.selectize : {})|json_encode|e('html_attr') }}"
data-grav-field="select"
@@ -43,7 +45,7 @@
{% if field.show_root %}
<option value="/">/ (root)</option>
{% endif %}
{{ _self.page_options(_context, page_list) }}
{{ macro.page_options(_context, page_list) }}
</select>
</div>
{% endblock %}
@@ -1,5 +1,12 @@
{% extends "forms/field.html.twig" %}
{% macro spanToggle(input, length) %}
{% set space = repeat('&nbsp;&nbsp;', (length - input|length) / 2) %}
{{ (space ~ input ~ space)|raw }}
{% endmacro %}
{% import _self as macro %}
{% set value = (value is null ? field.default : value) %}
{% set value = (value is same as(false) ? 0 : value) %}
@@ -8,11 +15,6 @@
data-grav-default="{{ field.default|json_encode()|e('html_attr') }}"
{% endblock %}
{% macro spanToggle(input, length) %}
{% set space = repeat('&nbsp;&nbsp;', (length - input|length) / 2) %}
{{ (space ~ input ~ space)|raw }}
{% endmacro %}
{% block input %}
<div class="permissions-container">
{% set permissions = admin.getPermissions %}
@@ -56,7 +58,7 @@
{% if field.validate.required in ['on', 'true', 1] %}required="required"{% endif %}
/>
<label for="{{ id }}">{{ (_self.spanToggle(translation, maxLen)|trim)|raw }}</label>
<label for="{{ id }}">{{ (macro.spanToggle(translation, maxLen)|trim)|raw }}</label>
{% endfor %}
<a></a>
</div>
@@ -7,11 +7,11 @@
{% if field.validate.max %}max="{{ field.validate.max }}"{% endif %}
{% if field.validate.step %}step="{{ field.validate.step }}"{% endif %}
{% if field.id is defined %}
id="range_{{ field.id|e|replace('.', '_') }}"
oninput="number_{{ field.id|e|replace('.', '_') }}_output.value = this.value"
id="range_{{ field.id|e|replace({'.': '_'}) }}"
oninput="number_{{ field.id|e|replace({'.': '_'}) }}_output.value = this.value"
{% else %}
id="range_{{ field.name|e|replace('.', '_') }}"
oninput="number_{{ field.name|e|replace('.', '_') }}_output.value = this.value"
id="range_{{ field.name|e|replace({'.': '_'}) }}"
oninput="number_{{ field.name|e|replace({'.': '_'}) }}_output.value = this.value"
{% endif %}
{{ parent() }}
{% endblock %}
@@ -30,11 +30,11 @@
value="0"
{% endif %}
{% if field.id is defined %}
id="number_{{ field.id|e|replace('.', '_') }}_output"
oninput="range_{{ field.id|e|replace('.', '_') }}.value = this.value"
id="number_{{ field.id|e|replace({'.': '_'}) }}_output"
oninput="range_{{ field.id|e|replace({'.': '_'}) }}.value = this.value"
{% else %}
id="number_{{ field.name|e|replace('.', '_') }}_output"
oninput="range_{{ field.name|e|replace('.', '_') }}.value = this.value"
id="number_{{ field.name|e|replace({'.': '_'}) }}_output"
oninput="range_{{ field.name|e|replace({'.': '_'}) }}.value = this.value"
{% endif %}
/>
<span class="range-append">{{ field.append }}</span>
@@ -1,7 +1,7 @@
{% extends "forms/field.html.twig" %}
{% block global_attributes %}
data-grav-selectize="{{ (field.selectize is defined ? field.selectize|merge({'create': true})|merge({'items':value}) : {'create': true})|json_encode()|e('html_attr') }}"
data-grav-selectize="{{ (field.selectize is defined ? field.selectize|merge({'create': true}) : {'create': true})|json_encode()|e('html_attr') }}"
{{ parent() }}
{% endblock %}
@@ -1,9 +1,13 @@
{% if field.fields %}
<div class="form-tab">
{% for field in field.fields %}
{% for childName, child in field.fields %}
{% if field.type %}
{% set value = field.name ? data.value(field.name) : data.toArray %}
{% include ["forms/fields/#{field.type}/#{field.type}.html.twig", 'forms/fields/text/text.html.twig'] %}
{%- if childName starts with '.' -%}
{% set child = child|merge({ name: field.name ~ childName }) %}
{% endif %}
{% set value = child.name ? data.value(child.name) : data.toArray %}
{% include ["forms/fields/#{child.type}/#{child.type}.html.twig", 'forms/fields/text/text.html.twig'] with { field: child, value: data.value(child.name) } %}
{% endif %}
{% endfor %}
</div>
@@ -1,5 +1,12 @@
{% extends "forms/field.html.twig" %}
{% macro spanToggle(input, length) %}
{% set space = repeat('&nbsp;&nbsp;', (length - input|length) / 2) %}
{{ (space ~ input ~ space)|raw }}
{% endmacro %}
{% import _self as macro %}
{% set value = (value is null ? field.default : value) %}
{% set value = (value is same as(false) ? 0 : value) %}
@@ -15,11 +22,6 @@
data-grav-field-name="{{ (scope ~ field.name)|fieldName }}"
{% endblock %}
{% macro spanToggle(input, length) %}
{% set space = repeat('&nbsp;&nbsp;', (length - input|length) / 2) %}
{{ (space ~ input ~ space)|raw }}
{% endmacro %}
{% block input %}
<div class="switch-toggle switch-grav {{ field.size }} switch-{{ field.options|length }} {{ field.classes }}">
@@ -54,7 +56,7 @@
{% endif %}
{% if field.validate.required in ['on', 'true', 1] %}required="required"{% endif %}
/>
<label for="{{ id }}">{{ (_self.spanToggle(translation, maxLen)|trim)|raw }}</label>
<label for="{{ id }}">{{ (macro.spanToggle(translation, maxLen)|trim)|raw }}</label>
{% endfor %}
</div>
{% endblock %}
@@ -0,0 +1,6 @@
{% set xss_header = data.value('header')|array %}
{% set xss_content = data.value('content') %}
{% set xss_status = xss({header: xss_header, content: xss_content}) %}
{% if xss_status is not empty %}
<div class="notice alert">{{ "PLUGIN_ADMIN.XSS_ISSUE"|tu([xss_status])|raw }}</div>
{% endif %}
@@ -4,6 +4,87 @@
{{ (repeat('&nbsp;&nbsp;', (length - input|length) / 2) ~ input ~ repeat('&nbsp;&nbsp;', (length - input|length) / 2))|raw }}
{% endmacro %}
{% macro loop(page, depth, twig_vars) %}
{% import _self as self %}
{% set config = twig_vars['config'] %}
{% set separator = config.system.param_sep %}
{% set display_field = config.plugins.admin.pages_list_display_field %}
{% set base_url = twig_vars['base_url_relative'] %}
{% set base_url_relative_frontend = twig_vars['base_url_relative_frontend'] %}
{% set base_url_simple = twig_vars['base_url_simple'] %}
{% set admin_route = twig_vars['admin_route'] %}
{% set admin_lang = twig_vars['admin_lang'] %}
{% set warn = twig_vars['warn'] %}
{% set uri = twig_vars['uri'] %}
{% if page.header.admin.children_display_order == 'collection' and page.header.content.order.by %}
{% if page.header.content.order.custom %}
{% set pcol = page.children().order(page.header.content.order.by, page.header.content.order.dir|default('asc'), page.header.content.order.custom) %}
{% else %}
{% set pcol = page.children().order(page.header.content.order.by, page.header.content.order.dir|default('asc')) %}
{% endif %}
{% else %}
{% set pcol = page.children() %}
{% endif %}
{% for p in pcol %}
{% set description = (not p.page ? "PLUGIN_ADMIN.FOLDER"|tu ~ ' &bull; ' : "PLUGIN_ADMIN.PAGE"|tu ~ ' &bull; ') ~
(p.modular ? "PLUGIN_ADMIN.MODULAR"|tu ~ ' &bull; ' : '') ~
(p.routable ? "PLUGIN_ADMIN.ROUTABLE"|tu ~ ' &bull; ' : "PLUGIN_ADMIN.NON_ROUTABLE"|tu ~ ' &bull; ') ~
(p.visible ? "PLUGIN_ADMIN.VISIBLE"|tu ~ ' &bull; ' : "PLUGIN_ADMIN.NON_VISIBLE"|tu ~ ' &bull; ') ~
(p.published ? "PLUGIN_ADMIN.PUBLISHED"|tu ~ ' &bull; ' : "PLUGIN_ADMIN.NON_PUBLISHED"|tu ~ ' &bull; ') %}
{% set page_url = getPageUrl(p) %}
<li class="page-item" data-nav-id="{{ p.route }}">
<div class="row page-item__row">
<span class="page-item__toggle" {{ p.children(0).count > 0 ? 'data-toggle="children"' : ''}}>
<i class="page-icon fa fa-fw fa-circle-o {{ p.children(0).count > 0 ? 'children-closed' : ''}} {{ p.modular ? 'modular' : (not p.routable ? 'not-routable' : (not p.visible ? 'not-visible' : (not p.page ? 'folder' : ''))) }}"></i>
</span>
<div class="page-item__content">
<div class="page-item__content-name">
<span data-hint="{{ description|trim(' &bull; ')|raw }}" class="hint--top page-item__content-hint">
{% set page_label = attribute(p.header, display_field)|defined(attribute(p, display_field))|defined(p.title) %}
<a href="{{ page_url }}" class="page-edit">{{ page_label|e }}</a>
</span>
{% if p.language %}
<span class="badge lang {% if p.language == admin_lang %}info{% endif %}">{{p.language}}</span>
{% endif %}
{% if p.home %}
<span class="page-home"><i class="fa fa-home"></i></span>
{% endif %}
</div>
<p class="page-route">{{ p.header.routes.default ?: p.route }} <span class="spacer"><i class="fa fa-long-arrow-right"></i></span> {{ p.template() }}</p>
</div>
<span class="page-item__tools">
{% if config.plugins.admin.frontend_preview_target != 'inline' %}
{% set preview_target = config.plugins.admin.frontend_preview_target %}
{% set preview_html = (base_url_relative_frontend|rtrim('/') ~ (p.home ? '' : p.route)) ?: '/' %}
{% set preview_link = p.routable ? '<a class="page-view" target="' ~ preview_target ~ '" href="' ~ preview_html ~ '" title="' ~ "PLUGIN_ADMIN.PREVIEW"|tu ~ '"> <i class="fa fa-fw fa-eye"></i></a>' : '' %}
{% else %}
{% set preview_html = (base_url|rtrim('/') ~ '/preview' ~ (p.home ? '' : p.route)) ?: '/' %}
{% set preview_link = p.routable ? '<a class="page-view" href="' ~ preview_html ~ '" title="' ~ "PLUGIN_ADMIN.PREVIEW"|tu ~ '"> <i class="fa fa-fw fa-eye"></i></a>' : '' %}
{% endif %}
{{ preview_link|raw }}
{% if warn %}
<a href="#delete" data-remodal-target="delete" data-delete-url="{{ uri.addNonce(page_url ~ '/task' ~ separator ~ 'delete', 'admin-form', 'admin-nonce') }}" class="page-delete" ><i class="fa fa-close"></i></a>
{% else %}
<a href="{{ uri.addNonce(page_url ~ '/task' ~ separator ~ 'delete', 'admin-form', 'admin-nonce') }}" class="page-delete" ><i class="fa fa-close"></i></a>
{% endif %}
</span>
</div>
{% if p.children().count > 0 %}
<ul class="depth-{{ depth + 1 }}" style="display:none;">
{{ self.loop(p, depth + 1, twig_vars) }}
</ul>
{% endif %}
</li>
{% endfor %}
{% endmacro %}
{% import _self as macro %}
{% if admin.route %}
{% set context = admin.page(true) %}
{% endif %}
@@ -50,85 +131,8 @@
{% set preview_link = context.routable ? '<a class="button" href="' ~ preview_html ~ '" title="' ~ "PLUGIN_ADMIN.PREVIEW"|tu ~ '"> <i class="fa fa-fw fa-eye" style="font-size:18px;margin-right:0;"></i></a>' : '' %}
{% endif %}
{% macro loop(page, depth, twig_vars) %}
{% set config = twig_vars['config'] %}
{% set separator = config.system.param_sep %}
{% set display_field = config.plugins.admin.pages_list_display_field %}
{% set base_url = twig_vars['base_url_relative'] %}
{% set base_url_relative_frontend = twig_vars['base_url_relative_frontend'] %}
{% set base_url_simple = twig_vars['base_url_simple'] %}
{% set admin_route = twig_vars['admin_route'] %}
{% set admin_lang = twig_vars['admin_lang'] %}
{% set warn = twig_vars['warn'] %}
{% set uri = twig_vars['uri'] %}
{% if page.header.admin.children_display_order == 'collection' and page.header.content.order.by %}
{% if page.header.content.order.custom %}
{% set pcol = page.children().order(page.header.content.order.by, page.header.content.order.dir|default('asc'), page.header.content.order.custom) %}
{% else %}
{% set pcol = page.children().order(page.header.content.order.by, page.header.content.order.dir|default('asc')) %}
{% endif %}
{% else %}
{% set pcol = page.children() %}
{% endif %}
{% for p in pcol %}
{% set description = (not p.page ? "PLUGIN_ADMIN.FOLDER"|tu ~ ' &bull; ' : "PLUGIN_ADMIN.PAGE"|tu ~ ' &bull; ') ~
(p.modular ? "PLUGIN_ADMIN.MODULAR"|tu ~ ' &bull; ' : '') ~
(p.routable ? "PLUGIN_ADMIN.ROUTABLE"|tu ~ ' &bull; ' : "PLUGIN_ADMIN.NON_ROUTABLE"|tu ~ ' &bull; ') ~
(p.visible ? "PLUGIN_ADMIN.VISIBLE"|tu ~ ' &bull; ' : "PLUGIN_ADMIN.NON_VISIBLE"|tu ~ ' &bull; ') ~
(p.published ? "PLUGIN_ADMIN.PUBLISHED"|tu ~ ' &bull; ' : "PLUGIN_ADMIN.NON_PUBLISHED"|tu ~ ' &bull; ') %}
{% set page_url = getPageUrl(p) %}
<li class="page-item" data-nav-id="{{ p.route }}">
<div class="row page-item__row">
<span class="page-item__toggle" {{ p.children(0).count > 0 ? 'data-toggle="children"' : ''}}>
<i class="page-icon fa fa-fw fa-circle-o {{ p.children(0).count > 0 ? 'children-closed' : ''}} {{ p.modular ? 'modular' : (not p.routable ? 'not-routable' : (not p.visible ? 'not-visible' : (not p.page ? 'folder' : ''))) }}"></i>
</span>
<div class="page-item__content">
<div class="page-item__content-name">
<span data-hint="{{ description|trim(' &bull; ')|raw }}" class="hint--top page-item__content-hint">
{% set page_label = attribute(p.header, display_field)|defined(attribute(p, display_field))|defined(p.title) %}
<a href="{{ page_url }}" class="page-edit">{{ page_label|e }}</a>
</span>
{% if p.language %}
<span class="badge lang {% if p.language == admin_lang %}info{% endif %}">{{p.language}}</span>
{% endif %}
{% if p.home %}
<span class="page-home"><i class="fa fa-home"></i></span>
{% endif %}
</div>
<p class="page-route">{{ p.header.routes.default ?: p.route }} <span class="spacer"><i class="fa fa-long-arrow-right"></i></span> {{ p.template() }}</p>
</div>
<span class="page-item__tools">
{% if config.plugins.admin.frontend_preview_target != 'inline' %}
{% set preview_target = config.plugins.admin.frontend_preview_target %}
{% set preview_html = (base_url_relative_frontend|rtrim('/') ~ (p.home ? '' : p.route)) ?: '/' %}
{% set preview_link = p.routable ? '<a class="page-view" target="' ~ preview_target ~ '" href="' ~ preview_html ~ '" title="' ~ "PLUGIN_ADMIN.PREVIEW"|tu ~ '"> <i class="fa fa-fw fa-eye"></i></a>' : '' %}
{% else %}
{% set preview_html = (base_url|rtrim('/') ~ '/preview' ~ (p.home ? '' : p.route)) ?: '/' %}
{% set preview_link = p.routable ? '<a class="page-view" href="' ~ preview_html ~ '" title="' ~ "PLUGIN_ADMIN.PREVIEW"|tu ~ '"> <i class="fa fa-fw fa-eye"></i></a>' : '' %}
{% endif %}
{{ preview_link|raw }}
{% if warn %}
<a href="#delete" data-remodal-target="delete" data-delete-url="{{ uri.addNonce(page_url ~ '/task' ~ separator ~ 'delete', 'admin-form', 'admin-nonce') }}" class="page-delete" ><i class="fa fa-close"></i></a>
{% else %}
<a href="{{ uri.addNonce(page_url ~ '/task' ~ separator ~ 'delete', 'admin-form', 'admin-nonce') }}" class="page-delete" ><i class="fa fa-close"></i></a>
{% endif %}
</span>
</div>
{% if p.children().count > 0 %}
<ul class="depth-{{ depth + 1 }}" style="display:none;">
{{ _self.loop(p, depth + 1, twig_vars) }}
</ul>
{% endif %}
</li>
{% endfor %}
{% endmacro %}
{% block titlebar %}
<div class="button-bar">
{% if mode == 'list' %}
<a class="button" href="{{ base_url }}"><i class="fa fa-reply"></i> {{ "PLUGIN_ADMIN.BACK"|tu }}</a>
@@ -301,8 +305,8 @@
{% set normalText = "PLUGIN_ADMIN.NORMAL"|tu %}
{% set expertText = "PLUGIN_ADMIN.EXPERT"|tu %}
{% set maxLen = max([normalText|length, expertText|length]) %}
{% set normalText = _self.spanToggle(normalText, maxLen) %}
{% set expertText = _self.spanToggle(expertText, maxLen) %}
{% set normalText = macro.spanToggle(normalText, maxLen) %}
{% set expertText = macro.spanToggle(expertText, maxLen) %}
<form id="admin-mode-toggle">
<div class="switch-toggle switch-grav">
<input type="radio" value="normal" data-leave-url="{{ base_url }}/pages/{{ admin.route|trim('/') }}/mode{{ config.system.param_sep }}normal" id="normal" name="mode-switch" class="highlight" {% if admin.session.expert == '0' %} checked="checked"{% endif %}>
@@ -316,6 +320,11 @@
</div>
{# Set current form data back into page content #}
{% if current_form_data %}
{% do context.header(current_form_data.header) %}
{% do context.content(current_form_data.content) %}
{% endif %}
{% if context.blueprints.fields and admin.session.expert == '0' %}
{% include 'partials/blueprints.html.twig' with { blueprints: context.blueprints, data: context } %}
{% else %}
@@ -337,7 +346,7 @@
</form>
<div class="pages-list">
<ul class="depth-0">
{{ _self.loop(pages, 0, _context) }}
{{ macro.loop(pages, 0, _context) }}
</ul>
{% include 'partials/page-legend.html.twig' %}
</div>
@@ -1,7 +1,7 @@
{% if authorize(['admin.pages', 'admin.super']) %}
<div id="latest">
<div class="button-bar">
<a class="button" href="{{ uri.route(true) }}/pages"><i class="fa fa-fw fa-file-text-o"></i>{{ "PLUGIN_ADMIN.MANAGE_PAGES"|tu }}</a>
<a class="button" href="{{ base_url_relative }}/pages"><i class="fa fa-fw fa-file-text-o"></i>{{ "PLUGIN_ADMIN.MANAGE_PAGES"|tu }}</a>
</div>
<h1>{{ "PLUGIN_ADMIN.LATEST_PAGE_UPDATES"|tu }}</h1>
<table>
View File
@@ -16,7 +16,7 @@
{% endfor %}
<div class="{{ form_button_outer_classes ?: 'form-actions secondary-accent' }}">
<div class="form-actionst">
<div class="form-actions primary-accent">
<button type="submit" class="button primary" name="task" value="twofa">{{ 'PLUGIN_LOGIN.BTN_LOGIN'|t }}</button>
</div>
</div>
@@ -15,7 +15,7 @@
{% block integration %}{% endblock %}
{% set redirect = redirect ?: uri.route(false) %}
{% set redirect = redirect ?: uri.path ~ uri.params ~ (uri.query ? '?' ~ uri.query : '') %}
<form method="post" action="{{ base_url_relative }}">
<div class="padding">
@@ -1,13 +1,6 @@
<div class="pages-list-container clear block size-1-4">
<h5>{{ "PLUGIN_ADMIN.PAGES"|tu|e }}</h5>
<div class="mediapicker-scroll">
<ul class="pages-list depth-0">
{{ _self.loop(pages, 0, _context) }}
</ul>
</div>
</div>
{% macro loop(page, depth, twig_vars) %}
{% import _self as self %}
{% set separator = twig_vars['config'].system.param_sep %}
{% set base_url = twig_vars['base_url_relative'] %}
{% set base_url_simple = twig_vars['base_url_simple'] %}
@@ -37,10 +30,21 @@
</div>
{% if p.children().count > 0 %}
<ul class="depth-{{ depth + 1 }}" style="display:none;">
{{ _self.loop(p, depth + 1, twig_vars) }}
</ul>
<ul class="depth-{{ depth + 1 }}" style="display:none;">
{{ self.loop(p, depth + 1, twig_vars) }}
</ul>
{% endif %}
</li>
{% endfor %}
{% endmacro %}
{% endmacro %}
{% import _self as macro %}
<div class="pages-list-container clear block size-1-4">
<h5>{{ "PLUGIN_ADMIN.PAGES"|tu|e }}</h5>
<div class="mediapicker-scroll">
<ul class="pages-list depth-0">
{{ macro.loop(pages, 0, _context) }}
</ul>
</div>
</div>
@@ -48,7 +48,7 @@
{% set gumroad_loaded = true %}
<script src="//gumroad.com/js/gumroad.js"></script>
{% endif %}
<a class="gumroad-button button" href="https://gum.co/{{ plugin.premium.product_permalink }}" target="_blank" data-gumroad-single-product="true"><i class="fa fa-shopping-cart"></i> {{ "Buy Now"|tu }}</a>
<a class="gumroad-button button" href="https://gum.co/{{ plugin.premium.permalink }}" target="_blank" data-gumroad-single-product="true"><i class="fa fa-shopping-cart"></i> {{ "Buy Now"|tu }}</a>
{% else %}
<a class="button" href="#" data-remodal-target="add-package" data-packages-slugs="{{ plugin.slug }}" data-plugin-action="start-package-installation"><i class="fa fa-plus"></i> {{ "PLUGIN_ADMIN.INSTALL_PLUGIN"|tu }}</a>
{% endif %}
@@ -52,7 +52,7 @@
{% set gumroad_loaded = true %}
<script src="//gumroad.com/js/gumroad.js"></script>
{% endif %}
<a class="gumroad-button button" href="https://gum.co/{{ plugin.premium.product_permalink }}" target="_blank" data-gumroad-single-product="true"><i class="fa fa-shopping-cart"></i> {{ "Buy Now"|tu }}</a>
<a class="gumroad-button button" href="https://gum.co/{{ plugin.premium.permalink }}" target="_blank" data-gumroad-single-product="true"><i class="fa fa-shopping-cart"></i> {{ "Buy Now"|tu }}</a>
{% else %}
<a class="button" href="#" data-remodal-target="add-package" data-packages-slugs="{{ plugin.slug }}" data-plugin-action="start-package-installation"><i class="fa fa-plus"></i> {{ "PLUGIN_ADMIN.INSTALL"|tu }}</a>
{% endif %}
View File
File diff suppressed because it is too large Load Diff
Vendored Executable → Regular
+1 -135
View File
@@ -1,135 +1 @@
#!/usr/bin/env php
<?php
require_once 'vendor/autoload.php';
use PicoFeed\Config\Config;
use PicoFeed\Reader\Favicon;
use PicoFeed\Scraper\Scraper;
use PicoFeed\Reader\Reader;
use PicoFeed\Logging\Logger;
use PicoFeed\PicoFeedException;
Logger::enable();
function get_feed($url, $disable_filtering = false)
{
try {
$reader = new Reader;
$resource = $reader->discover($url);
$parser = $reader->getParser(
$resource->getUrl(),
$resource->getContent(),
$resource->getEncoding()
);
if ($disable_filtering) {
$parser->disableContentFiltering();
}
return $parser->execute();
}
catch (PicoFeedException $e) {
echo 'Exception thrown ===> "'.$e->getMessage().'"'.PHP_EOL;
return false;
}
}
function get_item($feed, $item_id)
{
foreach ($feed->items as $item) {
if ($item->getId() === $item_id) {
echo $item;
echo "============= CONTENT ================\n";
echo $item->getContent();
echo "\n============= CONTENT ================\n";
break;
}
}
}
function dump_feed($url)
{
$feed = get_feed($url);
echo $feed;
}
function debug_feed($url)
{
get_feed($url);
print_r(Logger::getMessages());
}
function dump_item($url, $item_id)
{
$feed = get_feed($url);
if ($feed !== false) {
get_item($feed, $item_id);
}
}
function nofilter_item($url, $item_id)
{
$feed = get_feed($url, true);
if ($feed !== false) {
get_item($feed, $item_id);
}
}
function grabber($url)
{
$grabber = new Scraper(new Config);
$grabber->setUrl($url);
$grabber->execute();
print_r(Logger::getMessages());
echo "============= CONTENT ================\n";
echo $grabber->getRelevantContent().PHP_EOL;
echo "============= FILTERED ================\n";
echo $grabber->getFilteredContent().PHP_EOL;
}
function fetch_favicon($url)
{
$favicon = new Favicon();
echo $favicon->find($url) . PHP_EOL;
}
// Parse command line arguments
if ($argc === 4) {
switch ($argv[1]) {
case 'item':
dump_item($argv[2], $argv[3]);
die;
case 'nofilter':
nofilter_item($argv[2], $argv[3]);
die;
}
} else if ($argc === 3) {
switch ($argv[1]) {
case 'feed':
dump_feed($argv[2]);
die;
case 'debug':
debug_feed($argv[2]);
die;
case 'grabber':
grabber($argv[2]);
die;
case 'favicon':
fetch_favicon($argv[2]);
die;
}
}
printf("Usage:\n");
printf("%s feed <feed-url>\n", $argv[0]);
printf("%s debug <feed-url>\n", $argv[0]);
printf("%s item <feed-url> <item-id>\n", $argv[0]);
printf("%s nofilter <feed-url> <item-id>\n", $argv[0]);
printf("%s grabber <url>\n", $argv[0]);
printf("%s favicon <url>\n", $argv[0]);
../fguillot/picofeed/picofeed
View File
@@ -1,54 +0,0 @@
{
"name": "zendframework/zendxml",
"description": "Utility library for XML usage, best practices, and security in PHP",
"license": "BSD-3-Clause",
"keywords": [
"zf",
"zendframework",
"xml",
"security"
],
"support": {
"issues": "https://github.com/zendframework/ZendXml/issues",
"source": "https://github.com/zendframework/ZendXml",
"rss": "https://github.com/zendframework/ZendXml/releases.atom",
"chat": "https://zendframework-slack.herokuapp.com",
"forum": "https://discourse.zendframework.com/c/questions/components"
},
"require": {
"php": "^5.6 || ^7.0"
},
"require-dev": {
"zendframework/zend-coding-standard": "~1.0.0",
"phpunit/phpunit": "^5.7.27 || ^6.5.8 || ^7.1.4"
},
"autoload": {
"psr-4": {
"ZendXml\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"ZendXmlTest\\": "test/"
}
},
"config": {
"sort-packages": true
},
"extra": {
"branch-alias": {
"dev-master": "1.1.x-dev",
"dev-develop": "1.2.x-dev"
}
},
"scripts": {
"check": [
"@cs-check",
"@test"
],
"cs-check": "phpcs",
"cs-fix": "phpcbf",
"test": "phpunit --colors=always",
"test-coverage": "phpunit --colors=always --coverage-clover clover.xml"
}
}
+5
View File
@@ -0,0 +1,5 @@
# OS Generated
.DS_Store*
/.idea
node_modules
*.js.map
+48
View File
@@ -1,3 +1,51 @@
# v2.16.3
## 09/21/2018
1. [](#improved)
* Use `Url:post()` to get the `$_POST` variable (allows common security checks/filtering for the POST data)
* Various JS tweaks and enhancements
1. [](#bugfix)
* Fixed issue where `select` set up as `multiple` and with `selectize: create: true` would not properly merge newly created values on rendering.
# v2.16.2
## 08/23/2018
1. [](#improved)
* Switched to new Grav `Yaml` class to support Native + Fallback YAML libraries
* Simple styling fixes for `array` field
1. [](#bugfix)
* Fixed issue with translations of placeholder text in `array` field
# v2.16.1
## 08/20/2018
1. [](#new)
* Fixed a regression issue with `file` & `array` field
# v2.16.0
## 08/20/2018
1. [](#new)
* Added new `form.keep_alive` option to keep session alive [#275](https://github.com/getgrav/grav-plugin-form/issues/275)
* Added `array` field for frontend use
1. [](#improved)
* Improving compatibility `autocomplete` spec [#274](https://github.com/getgrav/grav-plugin-form/pull/274)
# v2.15.1
## 06/20/2018
1. [](#improved)
* Including EXIF JS library in the modules dependencies to fix orientation when uploading images
1. [](#bugfix)
* Fix HTML data template for checkboxes fields where 'use' property is "keys" [#258](https://github.com/getgrav/grav-plugin-form/pull/258)
# v2.15.0
## 05/31/2018
1. [](#new)
* Added support for `Uri::post()`
* Added support for `autocapitalize`, `inputmode`, and `spellcheck` options in field definitions
# v2.14.1
## 05/15/2018
+194
View File
@@ -0,0 +1,194 @@
import $ from 'jquery';
import Sortable from 'sortablejs';
let body = $('body');
class Template {
constructor(container) {
this.container = $(container);
if (this.getName() === undefined) {
this.container = this.container.closest('[data-grav-array-name]');
}
}
getName() {
return this.container.data('grav-array-name') || '';
}
getKeyPlaceholder() {
return this.container.data('grav-array-keyname') || 'Key';
}
getValuePlaceholder() {
return this.container.data('grav-array-valuename') || 'Value';
}
isValueOnly() {
return this.container.find('[data-grav-array-mode="value_only"]:first').length || false;
}
isTextArea() {
return this.container.data('grav-array-textarea') || false;
}
shouldBeDisabled() {
// check for toggleables, if field is toggleable and it's not enabled, render disabled
let toggle = this.container.closest('.form-field').find('[data-grav-field="toggleable"] input[type="checkbox"]');
return toggle.length && toggle.is(':not(:checked)');
}
getNewRow() {
let tpl = '';
const value = this.isTextArea()
? `<textarea ${this.shouldBeDisabled() ? 'disabled="disabled"' : ''} data-grav-array-type="value" name="" placeholder="${this.getValuePlaceholder()}"></textarea>`
: `<input ${this.shouldBeDisabled() ? 'disabled="disabled"' : ''} data-grav-array-type="value" type="text" name="" value="" placeholder="${this.getValuePlaceholder()}" />`;
if (this.isValueOnly()) {
tpl += `
<div class="form-row array-field-value_only" data-grav-array-type="row">
<span data-grav-array-action="sort" class="fa fa-bars"></span>
${value}
`;
} else {
tpl += `
<div class="form-row" data-grav-array-type="row">
<span data-grav-array-action="sort" class="fa fa-bars"></span>
<input ${this.shouldBeDisabled() ? 'disabled="disabled"' : ''} data-grav-array-type="key" type="text" value="" placeholder="${this.getKeyPlaceholder()}" />
${value}
`;
}
tpl += `
<span data-grav-array-action="rem" class="fa fa-minus"></span>
<span data-grav-array-action="add" class="fa fa-plus"></span>
</div>`;
return tpl;
}
}
export default class ArrayField {
constructor() {
body.on('input', '[data-grav-array-type="key"], [data-grav-array-type="value"]', (event) => this.actionInput(event));
body.on('click touch', '[data-grav-array-action]:not([data-grav-array-action="sort"])', (event) => this.actionEvent(event));
this.arrays = $();
$('[data-grav-field="array"]').each((index, list) => this.addArray(list));
$('body').on('mutation._grav', this._onAddedNodes.bind(this));
}
addArray(list) {
list = $(list);
list.find('[data-grav-array-type="container"]').each((index, container) => {
container = $(container);
if (container.data('array-sort') || container[0].hasAttribute('data-array-nosort')) { return; }
container.data('array-sort', new Sortable(container.get(0), {
handle: '.fa-bars',
animation: 150
}));
});
}
actionInput(event) {
let element = $(event.target);
let type = element.data('grav-array-type');
this._setTemplate(element);
let template = element.data('array-template');
let keyElement = type === 'key' ? element : element.siblings('[data-grav-array-type="key"]:first');
let valueElement = type === 'value' ? element : element.siblings('[data-grav-array-type="value"]:first');
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);
this.refreshNames(template);
}
actionEvent(event) {
event && event.preventDefault();
let element = $(event.target);
let action = element.data('grav-array-action');
let container = element.parents('[data-grav-array-type="container"]');
this._setTemplate(element);
this[`${action}Action`](element);
let siblings = container.find('> div');
container[siblings.length > 1 ? 'removeClass' : 'addClass']('one-child');
}
addAction(element) {
let template = element.data('array-template');
let row = element.closest('[data-grav-array-type="row"]');
row.after(template.getNewRow());
}
remAction(element) {
let template = element.data('array-template');
let row = element.closest('[data-grav-array-type="row"]');
let isLast = !row.siblings().length;
if (isLast) {
let newRow = $(template.getNewRow());
row.after(newRow);
newRow.find('[data-grav-array-type="value"]:last').attr('name', template.getName());
}
row.remove();
this.refreshNames(template);
}
refreshNames(template) {
if (!template.isValueOnly()) { return; }
let row = template.container.find('> div > [data-grav-array-type="row"]');
let inputs = row.find('[name]:not([name=""])');
inputs.each((index, input) => {
input = $(input);
let name = input.attr('name');
name = name.replace(/\[\d+\]$/, `[${index}]`);
input.attr('name', name);
});
if (!inputs.length) {
row.find('[data-grav-array-type="value"]').attr('name', template.getName());
}
}
getIndexFor(element) {
let template = element.data('array-template');
let row = element.closest('[data-grav-array-type="row"]');
return template.container.find(`${template.isValueOnly() ? '> div ' : ''} > [data-grav-array-type="row"]`).index(row);
}
_setTemplate(element) {
if (!element.data('array-template')) {
element.data('array-template', new Template(element.closest('[data-grav-array-name]')));
}
}
_onAddedNodes(event, target/* , record, instance */) {
let arrays = $(target).find('[data-grav-field="array"]');
if (!arrays.length) { return; }
arrays.each((index, list) => {
list = $(list);
if (!~this.arrays.index(list)) {
this.addArray(list);
}
});
}
}
export let Instance = new ArrayField();
+299
View File
@@ -0,0 +1,299 @@
import $ from 'jquery';
import Dropzone from 'dropzone';
import EXIF from 'exif-js';
import { config, translations } from 'grav-form';
// translations
const Dictionary = {
dictCancelUpload: translations.PLUGIN_FORM.DROPZONE_CANCEL_UPLOAD,
dictCancelUploadConfirmation: translations.PLUGIN_FORM.DROPZONE_CANCEL_UPLOAD_CONFIRMATION,
dictDefaultMessage: translations.PLUGIN_FORM.DROPZONE_DEFAULT_MESSAGE,
dictFallbackMessage: translations.PLUGIN_FORM.DROPZONE_FALLBACK_MESSAGE,
dictFallbackText: translations.PLUGIN_FORM.DROPZONE_FALLBACK_TEXT,
dictFileTooBig: translations.PLUGIN_FORM.DROPZONE_FILE_TOO_BIG,
dictInvalidFileType: translations.PLUGIN_FORM.DROPZONE_INVALID_FILE_TYPE,
dictMaxFilesExceeded: translations.PLUGIN_FORM.DROPZONE_MAX_FILES_EXCEEDED,
dictRemoveFile: translations.PLUGIN_FORM.DROPZONE_REMOVE_FILE,
dictRemoveFileConfirmation: translations.PLUGIN_FORM.DROPZONE_REMOVE_FILE_CONFIRMATION,
dictResponseError: translations.PLUGIN_FORM.DROPZONE_RESPONSE_ERROR
};
Dropzone.autoDiscover = false;
const DropzoneMediaConfig = {
createImageThumbnails: { thumbnailWidth: 150 },
addRemoveLinks: false,
dictDefaultMessage: Dictionary.dictDefaultMessage,
dictRemoveFileConfirmation: Dictionary.dictRemoveFileConfirmation,
previewTemplate: ''
};
window.EXIF = EXIF;
export default class FilesField {
constructor({ container = '.dropzone.files-upload', options = {} } = {}) {
this.container = $(container);
if (!this.container.length) { return; }
this.urls = {};
DropzoneMediaConfig.previewTemplate = $('#dropzone-template').html();
this.options = Object.assign({}, Dictionary, DropzoneMediaConfig, {
klass: this,
url: this.container.data('file-url-add') || config.current_url,
acceptedFiles: this.container.data('media-types'),
init: this.initDropzone
}, this.container.data('dropzone-options'), options);
this.dropzone = new Dropzone(container, this.options);
this.dropzone.on('complete', this.onDropzoneComplete.bind(this));
this.dropzone.on('success', this.onDropzoneSuccess.bind(this));
this.dropzone.on('removedfile', this.onDropzoneRemovedFile.bind(this));
this.dropzone.on('sending', this.onDropzoneSending.bind(this));
this.dropzone.on('error', this.onDropzoneError.bind(this));
}
initDropzone() {
let files = this.options.klass.container.find('[data-file]');
let dropzone = this;
if (!files.length) { return; }
files.each((index, file) => {
file = $(file);
let data = file.data('file');
let mock = {
name: data.name,
size: data.size,
type: data.type,
status: Dropzone.ADDED,
accepted: true,
url: this.options.url,
removeUrl: data.remove
};
dropzone.files.push(mock);
dropzone.options.addedfile.call(dropzone, mock);
if (mock.type.match(/^image\//)) dropzone.options.thumbnail.call(dropzone, mock, data.path);
file.remove();
});
}
getURI() {
return this.container.data('mediaUri') || '';
}
onDropzoneSending(file, xhr, formData) {
formData.append('__form-name__', this.container.closest('form').find('[name="__form-name__"]').val());
formData.append('__form-file-uploader__', 1);
formData.append('name', this.options.dotNotation);
formData.append('form-nonce', config.form_nonce);
formData.append('task', 'filesupload');
formData.append('uri', this.getURI());
}
onDropzoneSuccess(file, response, xhr) {
if (this.options.reloadPage) {
global.location.reload();
}
// store params for removing file from session before it gets saved
if (response.session) {
file.sessionParams = response.session;
file.removeUrl = this.options.url;
// Touch field value to force a mutation detection
const input = this.container.find('[name][type="hidden"]');
const value = input.val();
input.val(value + ' ');
}
return this.handleError({
file,
data: response,
mode: 'removeFile',
msg: `<p>${translations.PLUGIN_FORM.FILE_ERROR_UPLOAD} <strong>${file.name}</strong></p>
<pre>${response.message}</pre>`
});
}
onDropzoneComplete(file) {
if (!file.accepted && !file.rejected) {
let data = {
status: 'error',
message: `${translations.PLUGIN_FORM.FILE_UNSUPPORTED}: ${file.name.match(/\..+/).join('')}`
};
return this.handleError({
file,
data,
mode: 'removeFile',
msg: `<p>${translations.PLUGIN_FORM.FILE_ERROR_ADD} <strong>${file.name}</strong></p>
<pre>${data.message}</pre>`
});
}
if (this.options.reloadPage) {
global.location.reload();
}
}
onDropzoneRemovedFile(file, ...extra) {
if (!file.accepted || file.rejected) { return; }
let url = file.removeUrl || this.urls.delete || `${location.href}.json`;
let path = (url || '').match(/path:(.*)\//);
let data = new FormData();
data.append('filename', file.name);
data.append('__form-name__', this.container.closest('form').find('[name="__form-name__"]').val());
data.append('name', this.options.dotNotation);
data.append('form-nonce', config.form_nonce);
data.append('uri', this.getURI());
if (file.sessionParams) {
data.append('__form-file-remover__', '1');
data.append('session', file.sessionParams);
}
$.ajax({
url,
data,
method: 'POST',
contentType: false,
processData: false,
success: () => {
if (!path) { return; }
path = global.atob(path[1]);
let input = this.container.find('[name][type="hidden"]');
let data = JSON.parse(input.val() || '{}');
delete data[path];
input.val(JSON.stringify(data));
}
});
}
onDropzoneError(file, response, xhr) {
let message = xhr && response.error ? response.error.message : response;
$(file.previewElement).find('[data-dz-errormessage]').html(message);
return this.handleError({
file,
data: { status: 'error' },
msg: `<pre>${message}</pre>`
});
}
handleError(options) {
return true;
/* let { file, data, mode, msg } = options;
if (data.status !== 'error' && data.status !== 'unauthorized') { return; }
switch (mode) {
case 'addBack':
if (file instanceof File) {
this.dropzone.addFile.call(this.dropzone, file);
} else {
this.dropzone.files.push(file);
this.dropzone.options.addedfile.call(this.dropzone, file);
this.dropzone.options.thumbnail.call(this.dropzone, file, file.extras.url);
}
break;
case 'removeFile':
default:
if (~this.dropzone.files.indexOf(file)) {
file.rejected = true;
this.dropzone.removeFile.call(this.dropzone, file, { silent: true });
}
break;
}
let modal = $('[data-remodal-id="generic"]');
modal.find('.error-content').html(msg);
$.remodal.lookup[modal.data('remodal')].open(); */
}
}
export function UriToMarkdown(uri) {
uri = uri.replace(/@3x|@2x|@1x/, '');
uri = uri.replace(/\(/g, '%28');
uri = uri.replace(/\)/g, '%29');
return uri.match(/\.(jpe?g|png|gif|svg)$/i) ? `![](${uri})` : `[${decodeURI(uri)}](${uri})`;
}
let instances = [];
let cache = $();
const onAddedNodes = (event, target/* , record, instance */) => {
let files = $(target).find('.dropzone.files-upload');
if (!files.length) { return; }
files.each((index, file) => {
file = $(file);
if (!~cache.index(file)) {
addNode(file);
}
});
};
const addNode = (container) => {
container = $(container);
let input = container.find('input[type="file"]');
let settings = container.data('grav-file-settings') || {};
if (settings.accept && ~settings.accept.indexOf('*')) {
settings.accept = [''];
}
let options = {
url: container.data('file-url-add') || (container.closest('form').attr('action') || config.current_url) + '.json',
paramName: settings.paramName || 'file',
dotNotation: settings.name || 'file',
acceptedFiles: settings.accept ? settings.accept.join(',') : input.attr('accept') || container.data('media-types'),
maxFilesize: settings.filesize || 256,
maxFiles: settings.limit || null,
resizeWidth: settings.resizeWidth || null,
resizeHeight: settings.resizeHeight || null,
resizeQuality: settings.resizeQuality || null,
accept: function(file, done) {
const resolution = settings.resolution;
if (!resolution) return done();
setTimeout(() => {
let error = '';
if (resolution.min) {
Object.keys(resolution.min).forEach((attr) => {
if (file[attr] < resolution.min[attr]) {
error += translations.PLUGIN_FORM.RESOLUTION_MIN.replace(/{{attr}}/g, attr).replace(/{{min}}/g, resolution.min[attr]);
}
});
}
if (!(settings.resizeWidth || settings.resizeHeight)) {
if (resolution.max) {
Object.keys(resolution.max).forEach((attr) => {
if (file[attr] > resolution.max[attr]) {
error += translations.PLUGIN_FORM.RESOLUTION_MAX.replace(/{{attr}}/g, attr).replace(/{{max}}/g, resolution.max[attr]);
}
});
}
}
return done(error);
}, 50);
}
};
cache = cache.add(container);
container = container[0];
instances.push(new FilesField({ container, options }));
};
export let Instances = (() => {
$(document).ready(() => {
$('.dropzone.files-upload').each((i, container) => addNode(container));
$('body').on('mutation._grav', onAddedNodes);
});
return instances;
})();
+4
View File
@@ -0,0 +1,4 @@
import FileInstances from './file';
import ArrayInstances from './array';
export default { FileInstances, ArrayInstances };
+3 -294
View File
@@ -1,295 +1,4 @@
import $ from 'jquery';
import Dropzone from 'dropzone';
import { config, translations } from 'grav-form';
// translations
const Dictionary = {
dictCancelUpload: translations.PLUGIN_FORM.DROPZONE_CANCEL_UPLOAD,
dictCancelUploadConfirmation: translations.PLUGIN_FORM.DROPZONE_CANCEL_UPLOAD_CONFIRMATION,
dictDefaultMessage: translations.PLUGIN_FORM.DROPZONE_DEFAULT_MESSAGE,
dictFallbackMessage: translations.PLUGIN_FORM.DROPZONE_FALLBACK_MESSAGE,
dictFallbackText: translations.PLUGIN_FORM.DROPZONE_FALLBACK_TEXT,
dictFileTooBig: translations.PLUGIN_FORM.DROPZONE_FILE_TOO_BIG,
dictInvalidFileType: translations.PLUGIN_FORM.DROPZONE_INVALID_FILE_TYPE,
dictMaxFilesExceeded: translations.PLUGIN_FORM.DROPZONE_MAX_FILES_EXCEEDED,
dictRemoveFile: translations.PLUGIN_FORM.DROPZONE_REMOVE_FILE,
dictRemoveFileConfirmation: translations.PLUGIN_FORM.DROPZONE_REMOVE_FILE_CONFIRMATION,
dictResponseError: translations.PLUGIN_FORM.DROPZONE_RESPONSE_ERROR
};
Dropzone.autoDiscover = false;
const DropzoneMediaConfig = {
createImageThumbnails: { thumbnailWidth: 150 },
addRemoveLinks: false,
dictDefaultMessage: Dictionary.dictDefaultMessage,
dictRemoveFileConfirmation: Dictionary.dictRemoveFileConfirmation,
previewTemplate: ''
};
export default class FilesField {
constructor({ container = '.dropzone.files-upload', options = {} } = {}) {
this.container = $(container);
if (!this.container.length) { return; }
this.urls = {};
DropzoneMediaConfig.previewTemplate = $('#dropzone-template').html();
this.options = Object.assign({}, Dictionary, DropzoneMediaConfig, {
klass: this,
url: this.container.data('file-url-add') || config.current_url,
acceptedFiles: this.container.data('media-types'),
init: this.initDropzone
}, this.container.data('dropzone-options'), options);
this.dropzone = new Dropzone(container, this.options);
this.dropzone.on('complete', this.onDropzoneComplete.bind(this));
this.dropzone.on('success', this.onDropzoneSuccess.bind(this));
this.dropzone.on('removedfile', this.onDropzoneRemovedFile.bind(this));
this.dropzone.on('sending', this.onDropzoneSending.bind(this));
this.dropzone.on('error', this.onDropzoneError.bind(this));
}
initDropzone() {
let files = this.options.klass.container.find('[data-file]');
let dropzone = this;
if (!files.length) { return; }
files.each((index, file) => {
file = $(file);
let data = file.data('file');
let mock = {
name: data.name,
size: data.size,
type: data.type,
status: Dropzone.ADDED,
accepted: true,
url: this.options.url,
removeUrl: data.remove
};
dropzone.files.push(mock);
dropzone.options.addedfile.call(dropzone, mock);
if (mock.type.match(/^image\//)) dropzone.options.thumbnail.call(dropzone, mock, data.path);
file.remove();
});
}
getURI() {
return this.container.data('mediaUri') || '';
}
onDropzoneSending(file, xhr, formData) {
formData.append('__form-name__', this.container.closest('form').find('[name="__form-name__"]').val());
formData.append('__form-file-uploader__', 1);
formData.append('name', this.options.dotNotation);
formData.append('form-nonce', config.form_nonce);
formData.append('task', 'filesupload');
formData.append('uri', this.getURI());
}
onDropzoneSuccess(file, response, xhr) {
if (this.options.reloadPage) {
global.location.reload();
}
// store params for removing file from session before it gets saved
if (response.session) {
file.sessionParams = response.session;
file.removeUrl = this.options.url;
// Touch field value to force a mutation detection
const input = this.container.find('[name][type="hidden"]');
const value = input.val();
input.val(value + ' ');
}
return this.handleError({
file,
data: response,
mode: 'removeFile',
msg: `<p>${translations.PLUGIN_FORM.FILE_ERROR_UPLOAD} <strong>${file.name}</strong></p>
<pre>${response.message}</pre>`
});
}
onDropzoneComplete(file) {
if (!file.accepted && !file.rejected) {
let data = {
status: 'error',
message: `${translations.PLUGIN_FORM.FILE_UNSUPPORTED}: ${file.name.match(/\..+/).join('')}`
};
return this.handleError({
file,
data,
mode: 'removeFile',
msg: `<p>${translations.PLUGIN_FORM.FILE_ERROR_ADD} <strong>${file.name}</strong></p>
<pre>${data.message}</pre>`
});
}
if (this.options.reloadPage) {
global.location.reload();
}
}
onDropzoneRemovedFile(file, ...extra) {
if (!file.accepted || file.rejected) { return; }
let url = file.removeUrl || this.urls.delete || `${location.href}.json`;
let path = (url || '').match(/path:(.*)\//);
let data = new FormData();
data.append('filename', file.name);
data.append('__form-name__', this.container.closest('form').find('[name="__form-name__"]').val());
data.append('name', this.options.dotNotation);
data.append('form-nonce', config.form_nonce);
data.append('uri', this.getURI());
if (file.sessionParams) {
data.append('__form-file-remover__', '1');
data.append('session', file.sessionParams);
}
$.ajax({
url,
data,
method: 'POST',
contentType: false,
processData: false,
success: () => {
if (!path) { return; }
path = global.atob(path[1]);
let input = this.container.find('[name][type="hidden"]');
let data = JSON.parse(input.val() || '{}');
delete data[path];
input.val(JSON.stringify(data));
}
});
}
onDropzoneError(file, response, xhr) {
let message = xhr && response.error ? response.error.message : response;
$(file.previewElement).find('[data-dz-errormessage]').html(message);
return this.handleError({
file,
data: { status: 'error' },
msg: `<pre>${message}</pre>`
});
}
handleError(options) {
return true;
/* let { file, data, mode, msg } = options;
if (data.status !== 'error' && data.status !== 'unauthorized') { return; }
switch (mode) {
case 'addBack':
if (file instanceof File) {
this.dropzone.addFile.call(this.dropzone, file);
} else {
this.dropzone.files.push(file);
this.dropzone.options.addedfile.call(this.dropzone, file);
this.dropzone.options.thumbnail.call(this.dropzone, file, file.extras.url);
}
break;
case 'removeFile':
default:
if (~this.dropzone.files.indexOf(file)) {
file.rejected = true;
this.dropzone.removeFile.call(this.dropzone, file, { silent: true });
}
break;
}
let modal = $('[data-remodal-id="generic"]');
modal.find('.error-content').html(msg);
$.remodal.lookup[modal.data('remodal')].open(); */
}
}
export function UriToMarkdown(uri) {
uri = uri.replace(/@3x|@2x|@1x/, '');
uri = uri.replace(/\(/g, '%28');
uri = uri.replace(/\)/g, '%29');
return uri.match(/\.(jpe?g|png|gif|svg)$/i) ? `![](${uri})` : `[${decodeURI(uri)}](${uri})`;
}
let instances = [];
let cache = $();
const onAddedNodes = (event, target/* , record, instance */) => {
let files = $(target).find('.dropzone.files-upload');
if (!files.length) { return; }
files.each((index, file) => {
file = $(file);
if (!~cache.index(file)) {
addNode(file);
}
});
};
const addNode = (container) => {
container = $(container);
let input = container.find('input[type="file"]');
let settings = container.data('grav-file-settings') || {};
if (settings.accept && ~settings.accept.indexOf('*')) {
settings.accept = [''];
}
let options = {
url: container.data('file-url-add') || (container.closest('form').attr('action') || config.current_url) + '.json',
paramName: settings.paramName || 'file',
dotNotation: settings.name || 'file',
acceptedFiles: settings.accept ? settings.accept.join(',') : input.attr('accept') || container.data('media-types'),
maxFilesize: settings.filesize || 256,
maxFiles: settings.limit || null,
resizeWidth: settings.resizeWidth || null,
resizeHeight: settings.resizeHeight || null,
resizeQuality: settings.resizeQuality || null,
accept: function(file, done) {
const resolution = settings.resolution;
if (!resolution) return done();
setTimeout(() => {
let error = '';
if (resolution.min) {
Object.keys(resolution.min).forEach((attr) => {
if (file[attr] < resolution.min[attr]) {
error += translations.PLUGIN_FORM.RESOLUTION_MIN.replace(/{{attr}}/g, attr).replace(/{{min}}/g, resolution.min[attr]);
}
});
}
if (!(settings.resizeWidth || settings.resizeHeight)) {
if (resolution.max) {
Object.keys(resolution.max).forEach((attr) => {
if (file[attr] > resolution.max[attr]) {
error += translations.PLUGIN_FORM.RESOLUTION_MAX.replace(/{{attr}}/g, attr).replace(/{{max}}/g, resolution.max[attr]);
}
});
}
}
return done(error);
}, 50);
}
};
cache = cache.add(container);
container = container[0];
instances.push(new FilesField({ container, options }));
};
export let Instances = (() => {
$('.dropzone.files-upload').each((i, container) => addNode(container));
$('body').on('mutation._grav', onAddedNodes);
return instances;
})();
import Instances from './fields';
import './utils/keep-alive';
export { Instances };
+15
View File
@@ -0,0 +1,15 @@
import $ from 'jquery';
import {config} from 'grav-form';
$(document).ready(() => {
const keepAlive = $('[data-grav-keepalive]');
if (keepAlive.length) {
const timeout = config.session_timeout / 1.5 * 1000;
setInterval(() => {
$.ajax({
url: `${config.base_url_relative}/task${config.param_sep}keep-alive`
});
}, timeout);
}
});
+14 -2
View File
@@ -23,8 +23,6 @@
margin: 0;
}
.form-input-file input {
display: none;
}
@@ -122,3 +120,17 @@
justify-content: space-between;
margin-top: 8px;
}
[data-grav-field="array"] .form-row {
display: flex;
align-items: center;
margin-bottom: 0.5rem;
}
[data-grav-field="array"] .form-row > input,
[data-grav-field="array"] .form-row > textarea
{
margin: 0 0.5rem;
display: inline-block;
}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
View File
+2 -2
View File
@@ -1,5 +1,5 @@
name: Form
version: 2.14.1
version: 2.16.3
description: Enables the forms handling
icon: check-square
author:
@@ -12,7 +12,7 @@ bugs: https://github.com/getgrav/grav-plugin-form/issues
license: MIT
dependencies:
- { name: grav, version: '>=1.3.9' }
- { name: grav, version: '>=1.5.1' }
form:
validation: strict
+22 -10
View File
@@ -384,11 +384,12 @@ class Form extends Iterator implements \Serializable
*/
public function uploadFiles()
{
$post = $_POST;
$grav = Grav::instance();
$uri = $grav['uri']->url;
$config = $grav['config'];
$session = $grav['session'];
$uri = $grav['uri'];
$url = $uri->url;
$post = $uri->post();
$settings = $this->data->blueprints()->schema()->getProperty($post['name']);
$settings = (object) array_merge(
@@ -402,7 +403,10 @@ class Form extends Iterator implements \Serializable
(array) $settings,
['name' => $post['name']]
);
// Allow plugins to adapt settings for a given post name
// Useful if schema retrieval is not an option, e.g. dynamically created forms
$grav->fireEvent('onFormUploadSettings', new Event(['settings' => &$settings, 'post' => $post]));
$upload = $this->normalizeFiles($_FILES['data'], $settings->name);
// Handle errors and breaks without proceeding further
@@ -494,7 +498,7 @@ class Form extends Iterator implements \Serializable
// Retrieve the current session of the uploaded files for the field
// and initialize it if it doesn't exist
$sessionField = base64_encode($uri);
$sessionField = base64_encode($url);
$flash = $session->getFlashObject('files-upload');
if (!$flash) {
$flash = [];
@@ -544,7 +548,7 @@ class Form extends Iterator implements \Serializable
$json_response = [
'status' => 'success',
'session' => \json_encode([
'sessionField' => base64_encode($uri),
'sessionField' => base64_encode($url),
'path' => $upload->file->path,
'field' => $settings->name
])
@@ -564,8 +568,10 @@ class Form extends Iterator implements \Serializable
public function filesSessionRemove()
{
$grav = Grav::instance();
$post = $_POST;
$session = $grav['session'];
$uri = $grav['uri'];
$post = $uri->post();
// Retrieve the current session of the uploaded files for the field
// and initialize it if it doesn't exist
$sessionField = base64_encode($grav['uri']->url(true));
@@ -625,11 +631,13 @@ class Form extends Iterator implements \Serializable
public function post()
{
$grav = Grav::instance();
$uri = $grav['uri']->url;
$session = $grav['session'];
$uri = $grav['uri'];
$url = $uri->url;
$post = $uri->post();
if (isset($_POST)) {
$this->values = new Data(isset($_POST) ? (array)$_POST : []);
if ($post) {
$this->values = new Data((array)$post);
$data = $this->values->get('data');
// Add post data to form dataset
@@ -694,8 +702,12 @@ class Form extends Iterator implements \Serializable
// Process previously uploaded files for the current URI
// and finally store them. Everything else will get discarded
$queue = $session->getFlashObject('files-upload');
$queue = $queue[base64_encode($uri)];
$queue = $queue[base64_encode($url)];
if (is_array($queue)) {
// Allow plugins to implement additional / alternative logic
// Add post to event data
$grav->fireEvent('onFormStoreUploads', new Event(['queue' => &$queue, 'form' => $this, 'post' => $post]));
foreach ($queue as $key => $files) {
foreach ($files as $destination => $file) {
if (!rename($file['tmp_name'], $destination)) {
+19 -7
View File
@@ -11,10 +11,10 @@ use Grav\Common\Plugin;
use Grav\Common\Twig\Twig;
use Grav\Common\Utils;
use Grav\Common\Uri;
use Grav\Common\Yaml;
use Grav\Plugin\Form\Form;
use RocketTheme\Toolbox\File\JsonFile;
use RocketTheme\Toolbox\File\YamlFile;
use Symfony\Component\Yaml\Yaml;
use RocketTheme\Toolbox\File\File;
use RocketTheme\Toolbox\Event\Event;
@@ -94,6 +94,12 @@ class FormPlugin extends Plugin
'onTwigSiteVariables' => ['onTwigVariables', 0],
'onFormValidationProcessed' => ['onFormValidationProcessed', 0],
]);
// Mini Keep-Alive Logic
$task = $this->grav['uri']->param('task');
if ($task && $task === 'keep-alive') {
exit;
}
}
public function onGetPageTemplates(Event $event)
@@ -183,11 +189,14 @@ class FormPlugin extends Plugin
'onFormFieldTypes' => ['onFormFieldTypes', 0],
]);
$uri = $this->grav['uri'];
// Post the form
if ($this->form) {
if (isset($_POST['__form-file-uploader__']) && $this->grav['uri']->extension() === 'json') {
$isJson = $uri->extension() === 'json';
if ($isJson && $uri->post('__form-file-uploader__')) {
$this->json_response = $this->form->uploadFiles();
} else if ($this->form && isset($_POST['__form-file-remover__']) && $this->grav['uri']->extension() === 'json') {
} elseif ($isJson && $uri->post('__form-file-remover__')) {
$this->json_response = $this->form->filesSessionRemove();
} else {
$this->form->post();
@@ -198,7 +207,7 @@ class FormPlugin extends Plugin
// Clear flash objects for previously uploaded files
// whenever the user switches page / reloads
// ignoring any JSON / extension call
if (!$submitted && null === $this->grav['uri']->extension()) {
if (!$submitted && null === $uri->extension()) {
// Discard any previously uploaded files session.
// and if there were any uploaded file, remove them from the filesystem
if ($flash = $this->grav['session']->getFlashObject('files-upload')) {
@@ -711,7 +720,9 @@ class FormPlugin extends Plugin
*/
protected function shouldProcessForm()
{
$status = isset($_POST['form-nonce']) ? true : false; // php72 quirk?
$uri = $this->grav['uri'];
$nonce = $uri->post('form-nonce');
$status = $nonce ? true : false; // php72 quirk?
$refresh_prevention = null;
if ($status && $this->form()) {
@@ -726,7 +737,7 @@ class FormPlugin extends Plugin
$refresh_prevention = $this->config->get('plugins.form.refresh_prevention', false);
}
$unique_form_id = filter_input(INPUT_POST, '__unique_form_id__', FILTER_SANITIZE_STRING);
$unique_form_id = $uri->post('__unique_form_id__', FILTER_SANITIZE_STRING);
if ($refresh_prevention && $unique_form_id) {
if ($this->grav['session']->unique_form_id !== $unique_form_id) {
@@ -769,7 +780,8 @@ class FormPlugin extends Plugin
$page = $this->grav['page'];
}
$form_name = filter_input(INPUT_POST, '__form-name__');
$form_name = $this->grav['uri']->post('__form-name__', FILTER_SANITIZE_STRING);
if (!$form_name) {
$form_name = $page ? $page->slug() : null;
}
+15
View File
@@ -0,0 +1,15 @@
{
"project":"grav-plugin-form",
"platforms":{
"grav":{
"nodes":{
"plugin":[
{
"source":"/",
"destination":"/user/plugins/form"
}
]
}
}
}
}
+40
View File
@@ -66,12 +66,15 @@ de:
ACCEPT_HELP: "Eine Liste von MIME-Typen, die hochgeladen werden dürfen."
ERROR_VALIDATING_CAPTCHA: "Die Überprüfung des Captcha ist fehlgeschlagen."
DATA_SUMMARY: "Hier die Zusammenfassung von dem, was Sie geschrieben haben:"
YES: "Ja"
NO: "Nein"
es:
PLUGIN_FORM:
NOT_VALIDATED: "Falló la validación del formulario. Uno o más campos obligatorios no fueron cubiertos."
NONCE_NOT_VALIDATED: "Oops, hay un problema, por favor revise la información e intente enviar el formulario otra vez."
FILES: "Subida de Ficheros"
FORM_ALREADY_SUBMITTED: "Este formulario ya ha sido enviado"
ALLOW_MULTIPLE: "Permitir más de un fichero"
ALLOW_MULTIPLE_HELP: "Permitir seleccionar más de un fichero para subir."
DESTINATION: "Destino"
@@ -79,11 +82,48 @@ es:
ACCEPT: "MIME Types permitidos"
ACCEPT_HELP: "Una lista de MIME Types que se permiten subir."
ERROR_VALIDATING_CAPTCHA: "Error al comprobar el Captcha"
DATA_SUMMARY: "Este es el resumen de lo que nos has escrito:"
NO_FORM_DATA: "No hay datos disponibles del formulario"
RECAPTCHA: "ReCaptcha"
RECAPTCHA_SITE_KEY: "Site key"
RECAPTCHA_SITE_KEY_HELP: "Para más información visita https://developers.google.com/recaptcha"
RECAPTCHA_SECRET_KEY: "Secret key"
RECAPTCHA_SECRET_KEY_HELP: "Para más información visita https://developers.google.com/recaptcha"
GENERAL: "General"
USE_BUILT_IN_CSS: "Utilizar CSS incorporado"
USE_INLINE_CSS: "Utilizar CSS en línea"
FILEUPLOAD_PREVENT_SELF: 'No se puede utilizar "%s" fuera de las páginas'
FILEUPLOAD_UNABLE_TO_UPLOAD: 'No se puede subir el fichero %s: %s'
FILEUPLOAD_UNABLE_TO_MOVE: 'No se puede mover el fichero %s to "%s"'
DROPZONE_CANCEL_UPLOAD: 'Cancelar subida'
DROPZONE_CANCEL_UPLOAD_CONFIRMATION: '¿Estas seguro que quieres cancelar esta subida?'
DROPZONE_DEFAULT_MESSAGE: 'Arrastra tus ficheros aquí o <strong>haz click en esta área</strong>'
DROPZONE_FALLBACK_MESSAGE: 'Tu navegador no soporta el arrastrar y soltar ficheros de subida.'
DROPZONE_FALLBACK_TEXT: 'Por favor utiliza el formulario alternativo que esta debajo, para subir ficheros como antiguamente.'
DROPZONE_FILE_TOO_BIG: 'Fichero demasiado grande ({{filesize}}MiB). Máximo tamaño: {{maxFilesize}}MiB.'
DROPZONE_INVALID_FILE_TYPE: "No puedes subir ficheros de este tipo."
DROPZONE_MAX_FILES_EXCEEDED: "No puedes subir mas ficheros."
DROPZONE_REMOVE_FILE: "Borrar fichero"
DROPZONE_REMOVE_FILE_CONFIRMATION: '¿Estas seguro de que quieres borrar este fichero?'
DROPZONE_RESPONSE_ERROR: "El servidor respondió con el código de estado {{statusCode}}."
YES: "Si"
NO: "No"
REFRESH_PREVENTION: "Prevenir refresco"
REFRESH_PREVENTION_HELP: "Utiliza el ID único del formulario para garantizar que el este no se vuelve a procesar cuando se refresca el navegador"
LIMIT: "Límite"
LIMIT_HELP: "Máximo número de ficheros permitidos por campo (múltiples requeridos)"
FILESIZE: "Tamaño máximo de fichero"
FILESIZE_HELP: "Tamaño máximo de fichero permitido (en MB), 0 = Utiliza el de por defecto del sistema"
AVOID_OVERWRITING: "Evitar sobreescribir"
AVOID_OVERWRITING_HELP: "Evita que los ficheros con el mismo nombre sean sobreescritos. Se agregará el prefijo de fecha"
RANDOM_NAME: "Nombre aleatorio"
RANDOM_NAME_HELP: "Genera una cadela aleatoria de 15 caracteres como nombre de fichero a subir"
CLIENT_SIDE_VALIDATION: "Validación del lado del cliente"
CLIENT_SIDE_VALIDATION_HELP: "Por defecto los formularios usarán validación HTML5 del lado del cliente como primera línea de defensa"
INLINE_ERRORS: "Errores en línea"
INLINE_ERRORS_HELP: "Útil con 'validación del lado del cliente' desactivado, muestra errores en el formulario contextual en línea"
RESOLUTION_MIN: "{{attr}} fué menor que el mínimo {{min}}px <br />"
RESOLUTION_MAX: "{{attr}} fué mayor que el máximo {{max}}px <br />"
fr:
PLUGIN_FORM:
+29 -21
View File
@@ -5,31 +5,39 @@
"repository": "https://github.com/getgrav/grav-plugin-form",
"main": "app/main.js",
"scripts": {
"watch": "webpack --watch --progress --colors --config webpack.conf.js",
"dev": "webpack --progress --colors --config webpack.conf.js",
"prod": "NODE_ENV=production webpack -p --config webpack.conf.js",
"test": "echo \"Error: no test specified\" && exit 1"
"watch": "NODE_ENV=development webpack --watch --progress --colors --mode development --config webpack.conf.js",
"dev": "NODE_ENV=development webpack --progress --colors --config webpack.conf.js",
"prod": "NODE_ENV=production-wip webpack --mode development --config webpack.conf.js"
},
"author": "RocketTheme, LLC",
"author": "Team Grav",
"license": "MIT",
"dependencies": {
"dropzone": "^5.3.0"
"dropzone": "^5.5.1",
"exif-js": "^2.3.0",
"sortablejs": "^1.7.0"
},
"devDependencies": {
"babel-core": "^6.14.0",
"babel-loader": "^6.2.5",
"babel-polyfill": "^6.13.0",
"babel-preset-es2015": "^6.14.0",
"eslint": "^3.4.0",
"eslint-loader": "^1.5.0",
"exports-loader": "^0.6.3",
"gulp": "^3.9.1",
"gulp-sourcemaps": "^1.6.0",
"gulp-util": "^3.0.7",
"gulp-webpack": "^1.5.0",
"immutable": "^3.8.1",
"imports-loader": "^0.6.5",
"json-loader": "^0.5.4",
"webpack": "^1.13.2"
"babel-core": "^6.26.3",
"babel-eslint": "^8.2.6",
"babel-loader": "^7.1.5",
"babel-polyfill": "^6.26.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-stage-3": "^6.24.1",
"css-loader": "^1.0.0",
"eslint": "^5.6.0",
"eslint-loader": "^2.1.1",
"exports-loader": "^0.7.0",
"gulp": "latest",
"gulp-autoprefixer": "latest",
"gulp-clean-css": "^3.10.0",
"gulp-csscomb": "^3.0.8",
"gulp-rename": "^1.4.0",
"gulp-sass": "^4.0.1",
"gulp-sourcemaps": "^2.6.4",
"imports-loader": "^0.8.0",
"json-loader": "^0.5.7",
"style-loader": "^0.23.0",
"webpack": "^4.19.1",
"webpack-cli": "^3.1.0"
}
}
@@ -14,8 +14,10 @@
{% block field_value %}
{% if field.type == 'checkboxes' %}
<ul>
{% for value in form.value(scope ~ field.name) %}
<li>{{ field.options[value]|e }}</li>
{% set use_keys = field.use is defined and field.use == 'keys' %}
{% for key,value in form.value(scope ~ field.name) %}
{% set index = (use_keys ? key : value) %}
<li>{{ field.options[index]|e }}</li>
{% endfor %}
</ul>
{% elseif field.type == 'checkbox' %}
@@ -63,7 +63,11 @@
{% if autofocus %}autofocus="autofocus"{% endif %}
{% if field.novalidate in ['on', 'true', 1] %}novalidate="novalidate"{% endif %}
{% if field.readonly in ['on', 'true', 1] %}readonly="readonly"{% endif %}
{% if field.autocomplete in ['on', 'off', 'new-password'] %}autocomplete="{{ field.autocomplete }}"{% endif %}
{% if field.autocomplete is defined %}autocomplete="{{ field.autocomplete }}"{% endif %}
{% if field.autocapitalize in ['off', 'characters', 'words', 'sentences'] %}autocapitalize="{{ field.autocapitalize }}"{% endif %}
{% if field.inputmode in ['none', 'text', 'decimal', 'numeric', 'tel', 'search', 'email', 'url'] %}inputmode="{{ field.inputmode }}"{% endif %}
{% if field.spellcheck in ['true', 'false'] %}spellcheck="{{ field.spellcheck }}"{% endif %}
{% if field.attributes is defined %}
{% for attribute in field.attributes %}
{{ attribute.name }}="{{ attribute.value|e }}"
@@ -22,6 +22,24 @@
{% set action = base_url_relative ~ '/' ~ page.slug %}
{% endif %}
{% if form.keep_alive %}
{% do assets.addJs('plugin://form/assets/form.vendor.js', { 'group': 'bottom', 'loading': 'defer' }) %}
{% do assets.addJs('plugin://form/assets/form.min.js', { 'group': 'bottom', 'loading': 'defer' }) %}
{% endif %}
{% do assets.addInlineJs("
window.GravForm = window.GravForm || {};
window.GravForm.config = {
current_url: '" ~ uri.route(true) ~"',
base_url_relative: '" ~ base_url_relative ~ "',
param_sep: '"~ config.system.param_sep ~ "',
form_nonce: '" ~ form.getNonce ~ "',
session_timeout: " ~ config.system.session.timeout ~ "
};
window.GravForm.translations = jQuery.extend({}, window.GravForm.translations || {}, { PLUGIN_FORM: {} });
", {'group': 'bottom'}) %}
<form name="{{ form.name }}"
action="{{ action | trim('/', 'right') }}"
method="{{ method }}"{{ multipart }}
@@ -30,14 +48,13 @@
class="{{ form_outer_classes }} {{ form.classes }}"
{% endblock %}
{% if form.novalidate %}novalidate{% endif %}
{% if form.keep_alive %}data-grav-keepalive="true"{% endif %}
>
{% block inner_markup_fields_start %}{% endblock %}
{% set data = data ?? form.data %}
{% for field in form.fields %}
{% if field.type == 'file' %}
{% do assets.addJs('plugin://form/assets/form.min.js') %}
{% endif %}
{% set value = form.value(field.name) %}
{% include "forms/fields/#{field.type}/#{field.type}.html.twig" ignore missing %}
{% endfor %}
@@ -0,0 +1,98 @@
{% extends "forms/field.html.twig" %}
{% do assets.addJs('plugin://form/assets/form.vendor.js', { 'group': 'bottom', 'loading': 'defer' }) %}
{% do assets.addJs('plugin://form/assets/form.min.js', { 'group': 'bottom', 'loading': 'defer' }) %}
{% block global_attributes %}
data-grav-array-name="{{ (scope ~ field.name)|fieldName }}"
data-grav-array-keyname="{{ field.placeholder_key|e|t }}"
data-grav-array-valuename="{{ field.placeholder_value|e|t }}"
data-grav-array-textarea="{{ field.value_type == 'textarea' }}"
{{ parent() }}
{% endblock %}
{% macro renderer(key, text, field, scope) %}
{% if text is not iterable %}
<div class="form-row{% if field.value_only %} array-field-value_only{% endif %}"
data-grav-array-type="row">
<span data-grav-array-action="sort" class="fa fa-bars"></span>
{% if field.value_only != true %}
{% if key == '0' and text == '' %}
{% set key = '' %}
{% endif %}
<input
data-grav-array-type="key"
type="text" value="{{ key }}"
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
placeholder="{{ field.placeholder_key|e|t }}" />
{% endif %}
{% if field.value_type == 'textarea' %}
<textarea
data-grav-array-type="value"
name="{{ ((scope ~ field.name)|fieldName) ~ '[' ~ key ~ ']' }}"
placeholder="{{ field.placeholder_value|e|t }}"
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}>{{ text }}</textarea>
{% else %}
<input
data-grav-array-type="value"
type="text"
name="{{ ((scope ~ field.name)|fieldName) ~ '[' ~ key ~ ']' }}"
placeholder="{{ field.placeholder_value|e|t }}"
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
value={% if text == 'true' %}true{% elseif text == 'false' %}false{% else %}"{{ text|join(', ')|e }}"{% endif %} />
{% endif %}
<span data-grav-array-action="rem" class="fa fa-minus"></span>
<span data-grav-array-action="add" class="fa fa-plus"></span>
</div>
{% endif %}
{% endmacro %}
{% block input %}
{% import _self as array_field %}
<div class="{{ field.size }}" data-grav-array-type="container"{% if field.value_only %} data-grav-array-mode="value_only"{% endif %}{{ value|length <= 1 ? ' class="one-child"' : '' }}>
{% if value|length %}
{% for key, text in value -%}
{% if text is not iterable %}
{{ array_field.renderer(key, text, field, scope) }}
{% else %}
{# Backward compatibility for nested arrays (metas) which are not supported anymore #}
{% for subkey, subtext in text -%}
{{ array_field.renderer(key ~ '[' ~ subkey ~ ']', subtext, field, scope) }}
{% endfor %}
{% endif %}
{% endfor %}
{%- else -%}
{# Empty value, mock the entry field#}
<div class="form-row" data-grav-array-type="row">
<span data-grav-array-action="sort" class="fa fa-bars"></span>
{% if field.value_only != true %}
<input
data-grav-array-type="key"
type="text"
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
placeholder="{{ field.placeholder_key|e|t }}" />
{% endif %}
{% if field.value_type == 'textarea' %}
<textarea
data-grav-array-type="value"
name="{{ (scope ~ field.name)|fieldName }}"
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
placeholder="{{ field.placeholder_value|e|t }}"></textarea>
{% else %}
<input
data-grav-array-type="value"
type="text"
name="{{ (scope ~ field.name)|fieldName }}"
{% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %}
placeholder="{{ field.placeholder_value|e|t }}" />
{% endif %}
<span data-grav-array-action="rem" class="fa fa-minus"></span>
<span data-grav-array-action="add" class="fa fa-plus"></span>
</div>
{%- endif %}
</div>
{% endblock %}
@@ -81,32 +81,30 @@
</div>
{% do assets.addJs('jquery', 101) %}
{% do assets.addJs('plugin://form/assets/form.min.js', { 'group': 'bottom', 'loading': 'defer' }) %}
{% do assets.addCss('plugin://form/assets/dropzone.min.css', { 'group': 'form' }) %}
{% do assets.addJs('plugin://form/assets/form.vendor.js', { 'group': 'bottom', 'loading': 'defer' }) %}
{% do assets.addJs('plugin://form/assets/form.min.js', { 'group': 'bottom', 'loading': 'defer' }) %}
{% do assets.addCss('plugin://form/assets/dropzone.min.css', { 'group': 'form'}) %}
{{ assets.css('form')|raw }}
{% do assets.addInlineJs("
window.GravForm = window.GravForm || {};
window.GravForm.config = {
current_url: '" ~ uri.route(true) ~"',
base_url_relative: '" ~ base_url_relative ~ "',
param_sep: '"~ config.system.param_sep ~ "',
form_nonce: '" ~ form.getNonce ~ "',
};
window.GravForm.translations = {};
window.GravForm.translations['PLUGIN_FORM'] = {
'DROPZONE_CANCEL_UPLOAD': " ~ 'PLUGIN_FORM.DROPZONE_CANCEL_UPLOAD'|t|json_encode ~ ",
'DROPZONE_CANCEL_UPLOAD_CONFIRMATION': " ~ 'PLUGIN_FORM.DROPZONE_CANCEL_UPLOAD_CONFIRMATION'|t|json_encode ~ ",
'DROPZONE_DEFAULT_MESSAGE': " ~ 'PLUGIN_FORM.DROPZONE_DEFAULT_MESSAGE'|t|json_encode ~ ",
'DROPZONE_FALLBACK_MESSAGE': " ~ 'PLUGIN_FORM.DROPZONE_FALLBACK_MESSAGE'|t|json_encode ~ ",
'DROPZONE_FALLBACK_TEXT': " ~ 'PLUGIN_FORM.DROPZONE_FALLBACK_TEXT'|t|json_encode ~ ",
'DROPZONE_FILE_TOO_BIG': " ~ 'PLUGIN_FORM.DROPZONE_FILE_TOO_BIG'|t|json_encode ~ ",
'DROPZONE_INVALID_FILE_TYPE': " ~ 'PLUGIN_FORM.DROPZONE_INVALID_FILE_TYPE'|t|json_encode ~ ",
'DROPZONE_MAX_FILES_EXCEEDED': " ~ 'PLUGIN_FORM.DROPZONE_MAX_FILES_EXCEEDED'|t|json_encode ~ ",
'DROPZONE_REMOVE_FILE': " ~ 'PLUGIN_FORM.DROPZONE_REMOVE_FILE'|t|json_encode ~ ",
'DROPZONE_REMOVE_FILE_CONFIRMATION': " ~ 'PLUGIN_FORM.DROPZONE_REMOVE_FILE_CONFIRMATION'|t|json_encode ~ ",
'DROPZONE_RESPONSE_ERROR': " ~ 'PLUGIN_FORM.DROPZONE_RESPONSE_ERROR'|t|json_encode ~ ",
'RESOLUTION_MIN': " ~ 'PLUGIN_FORM.RESOLUTION_MIN'|t|json_encode ~ ",
'RESOLUTION_MAX': " ~ 'PLUGIN_FORM.RESOLUTION_MAX'|t|json_encode ~ "
};
", { 'group': 'bottom' }) %}
window.GravForm = jQuery.extend({}, window.GravForm, {
translations: {
PLUGIN_FORM: {
'DROPZONE_CANCEL_UPLOAD': " ~ 'PLUGIN_FORM.DROPZONE_CANCEL_UPLOAD'|t|json_encode ~ ",
'DROPZONE_CANCEL_UPLOAD_CONFIRMATION': " ~ 'PLUGIN_FORM.DROPZONE_CANCEL_UPLOAD_CONFIRMATION'|t|json_encode ~ ",
'DROPZONE_DEFAULT_MESSAGE': " ~ 'PLUGIN_FORM.DROPZONE_DEFAULT_MESSAGE'|t|json_encode ~ ",
'DROPZONE_FALLBACK_MESSAGE': " ~ 'PLUGIN_FORM.DROPZONE_FALLBACK_MESSAGE'|t|json_encode ~ ",
'DROPZONE_FALLBACK_TEXT': " ~ 'PLUGIN_FORM.DROPZONE_FALLBACK_TEXT'|t|json_encode ~ ",
'DROPZONE_FILE_TOO_BIG': " ~ 'PLUGIN_FORM.DROPZONE_FILE_TOO_BIG'|t|json_encode ~ ",
'DROPZONE_INVALID_FILE_TYPE': " ~ 'PLUGIN_FORM.DROPZONE_INVALID_FILE_TYPE'|t|json_encode ~ ",
'DROPZONE_MAX_FILES_EXCEEDED': " ~ 'PLUGIN_FORM.DROPZONE_MAX_FILES_EXCEEDED'|t|json_encode ~ ",
'DROPZONE_REMOVE_FILE': " ~ 'PLUGIN_FORM.DROPZONE_REMOVE_FILE'|t|json_encode ~ ",
'DROPZONE_REMOVE_FILE_CONFIRMATION': " ~ 'PLUGIN_FORM.DROPZONE_REMOVE_FILE_CONFIRMATION'|t|json_encode ~ ",
'DROPZONE_RESPONSE_ERROR': " ~ 'PLUGIN_FORM.DROPZONE_RESPONSE_ERROR'|t|json_encode ~ ",
'RESOLUTION_MIN': " ~ 'PLUGIN_FORM.RESOLUTION_MIN'|t|json_encode ~ ",
'RESOLUTION_MAX': " ~ 'PLUGIN_FORM.RESOLUTION_MAX'|t|json_encode ~ "
}
}
});
", {'group': 'bottom'}) %}
{% endblock %}
@@ -24,7 +24,12 @@
>
{% if field.placeholder %}<option value="" disabled selected>{% if grav.twig.twig.filters['tu'] is defined %}{{ field.placeholder|tu|raw }}{% else %}{{ field.placeholder|t|raw }}{% endif %}</option>{% endif %}
{% for key, item_value in field.options %}
{% set options = field.options %}
{% if field.selectize.create and field.multiple %}
{% set options = options|merge(value|default([]))|array_unique %}
{% endif %}
{% for key, item_value in options %}
{% if item_value is iterable %}
<optgroup label="{{ key }}">
{%for subkey, suboption in item_value %}
+60 -18
View File
@@ -1,23 +1,65 @@
var webpack = require('webpack'),
path = require('path'),
exec = require('child_process').execSync,
pwd = exec('pwd').toString();
path = require('path'),
UglifyJsPlugin = require('uglifyjs-webpack-plugin'),
isProd = process.env.NODE_ENV === 'production' || process.env.NODE_ENV === 'production-wip';
module.exports = {
entry: {
app: './app/main.js'
},
externals: {
jquery: 'jQuery',
'grav-form': 'GravForm'
},
module: {
preLoaders: [
{ test: /\.json$/, loader: 'json' },
{ test: /\.js$/, loader: 'eslint', exclude: /node_modules/ }
entry: {
site: './app/main.js'
},
devtool: isProd ? false : 'eval-source-map',
target: 'web',
output: {
path: path.resolve(__dirname, 'assets'),
filename: 'form.min.js',
chunkFilename: 'form.vendor.js'
},
optimization: {
minimize: isProd,
minimizer: [
new UglifyJsPlugin({
uglifyOptions: {
compress: {
drop_console: true
},
dead_code: true
}
})
],
splitChunks: {
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: 1,
name: 'vendor',
enforce: true,
chunks: 'all'
}
}
}
},
plugins: [
new webpack.ProvidePlugin({
'fetch': 'imports-loader?this=>global!exports-loader?global.fetch!whatwg-fetch'
})
],
loaders: [
{ test: /\.js$/, loader: 'babel', exclude: /node_modules/, query: { presets: ['es2015'] } }
]
}
externals: {
jquery: 'jQuery',
'grav-form': 'GravForm'
},
module: {
rules: [
{ enforce: 'pre', test: /\.json$/, loader: 'json-loader' },
{ enforce: 'pre', test: /\.js$/, loader: 'eslint-loader', exclude: /node_modules/ },
{ test: /\.css$/, loader: 'style-loader!css-loader' },
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/,
query: {
presets: ['es2015', 'stage-3']
}
}
]
}
};
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,21 @@
# v1.1.0
## 11/23/2018
1. [](#improved)
* Add pull-request [#7](https://github.com/clemdesign/grav-plugin-language-selector/pull/7): Need jQuery 1.8 or later.
# v1.0.1
## 08/10/2018
1. [](#improved)
* Add pull-request [#1](https://github.com/clemdesign/grav-plugin-language-selector/pull/1)
2. [](#bugfix)
* Fix issue [#2](https://github.com/clemdesign/grav-plugin-language-selector/issues/2)
-> Improve jQuery integration
# v1.0.0
## 07/22/2017
+27 -4
View File
@@ -6,7 +6,15 @@
# Installation
Installing the Language Selector plugin can be done only manually for this moment. GPM installation is not yet available
Installing the Language Selector plugin can be done in one of two ways. GPM (Grav Package Manager) installation method enables you to quickly and easily install the plugin with a simple terminal command, while the manual method enables you to do so via a zip file or GIT.
## GPM Installation (Preferred)
The simplest way to install this plugin is via the [Grav Package Manager (GPM)](http://learn.getgrav.org/advanced/grav-gpm) through your system's Terminal (also called the command line). From the root of your Grav install type:
bin/gpm install language-selector
This will install the Language Selector plugin into your `/user/plugins` directory within Grav. Its files can be found under `/yoursite/user/plugins/language-selector`.
## Manual Installation (Download)
@@ -28,7 +36,20 @@ This will clone this repository into the _language-selector_ folder.
# Usage
The `Language Selector` plugin doesn't require any configuration. You do however need to add the included Twig partials template into your own theme somewhere you want the available languages to be displayed.
## 1. Define the supported languages
In `system.yaml`, add the supported languages in `languages.supported` parameter.
Example:
languages:
supported:
- fr
- en
## 2. Integration
You do need to add the included Twig partials template into your own theme somewhere you want the available languages to be displayed.
```
{% include 'partials/language-selector.html.twig' %}
@@ -44,7 +65,9 @@ Copy the template file [language-selector.html.twig](templates/partials/language
You can now edit the override and tweak it however you prefer.
## Usage of the `hreflang` partial
`language-selector` need jQuery (1.8 -> later) to display dropdown language menu.
## 3. Usage of the `hreflang` partial
A second template is available for `hreflang` annotations in the header of the page. In order to emit language annotations for the available languages of a page you need to add the corrsponding Twig partial template into the `<head>` section of your page, which can typically be found in `base.html.twig`:
@@ -95,7 +118,7 @@ pages:
# Contribute
# Languages
## Languages
For Administration panel, `Language Selector` is available in English and French. You could contribute for another languages.
@@ -1,5 +1,5 @@
name: Language Selector
version: 1.0.0
version: 1.1.0
description: Language Selector is a [Grav](http://github.com/getgrav/grav) plugin that provides native language selector with flags to switch between [multiple languages](http://learn.getgrav.org/content/multi-language).
icon: globe
author:
@@ -0,0 +1,87 @@
.language-selector {
/* float: right;
position: relative;
top: 50%;
vertical-align: middle;
-webkit-transform: translateY(-50%);
-moz-transform: translateY(-50%);
-o-transform: translateY(-50%);
-ms-transform: translateY(-50%);
transform: translateY(-50%);
margin: 0 1rem !important;
display: inline-block; */
}
.language-selector>.btn:first-child {
margin-left: 0;
}
.language-selector .btn {
float: left;
position: relative;
display: inline-block;
margin-bottom: 0;
font-weight: 400;
text-align: center;
white-space: nowrap;
vertical-align: middle;
touch-action: manipulation;
cursor: pointer;
user-select: none;
background-image: none;
border: 1px solid transparent;
/* size */
padding: 5px 10px;
font-size: 0.8rem;
line-height: 1.5;
border-radius: 3px;
/* Aspect */
color: #333;
background-color: #fff;
border-color: #ccc;
}
.language-selector .dropdown-menu {
position: absolute;
top: 100%;
left: 0;
z-index: 1000;
display: none;
float: left;
padding: 5px 0;
margin: 2px 0 0;
font-size: 1rem;
text-align: left;
list-style: none;
background-color: #fff;
background-clip: padding-box;
border-radius: 4px;
box-shadow: 0 6px 12px rgba(0,0,0,.175);
}
.language-selector .dropdown-menu img {
width: 24px;
height: 12px;
max-width: inherit;
}
.language-selector .dropdown-menu li {
line-height: 1rem;
font-color: #c0c0c0;
display: list-item;
text-align: -webkit-match-parent;
}
.language-selector .dropdown-menu li:hover {
background-color: #eee;
}
.language-selector .dropdown-menu>li>a {
display: block;
padding: 3px 15px;
clear: both;
font-weight: 400;
line-height: 1.42857143;
color: #333;
white-space: nowrap;
}
@@ -1,7 +1,7 @@
/*!
* Language Selector JS plugin
* Copyright 2017 Clement G., Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
* Licensed under MIT
*/
var dropdownmenu={
@@ -13,76 +13,78 @@ var dropdownmenu={
stubboxenable: false,
showbox:function($, $dropdown){
this.stubboxenable = false
clearTimeout($dropdown.data('timers').hidetimer)
this.stubboxenable = false;
clearTimeout($dropdown.data('timers').hidetimer);
$dropdown.data('timers').showtimer=setTimeout(function(){$dropdown.show(dropdownmenu.animspeed)}, this.showhidedelay[0])
},
hidebox:function($, $dropdown){
if(this.stubboxenable == false) {
clearTimeout($dropdown.data('timers').showtimer)
if(this.stubboxenable === false) {
clearTimeout($dropdown.data('timers').showtimer);
$dropdown.data('timers').hidetimer=setTimeout(function(){$dropdown.hide(100)}, this.showhidedelay[1]) //hide dropdown plus all of its sub ULs
}
},
stubbox:function($, $dropdown){
this.stubboxenable = true
clearTimeout($dropdown.data('timers').hidetimer)
this.stubboxenable = true;
clearTimeout($dropdown.data('timers').hidetimer);
$dropdown.data('timers').showtimer=setTimeout(function(){$dropdown.show(dropdownmenu.animspeed)}, this.showhidedelay[0])
},
builddropdown:function($, $menu, $target){
$menu.css({display:'none'}).addClass('jqdropdown')
$menu.css({display:'none'}).addClass('jqdropdown');
$menu.bind('mouseenter', function(){
clearTimeout($menu.data('timers').hidetimer)
})
});
$menu.bind('mouseleave', function(){ //hide menu when mouse moves out of it
dropdownmenu.hidebox($, $menu)
})
$menu.data('dimensions', {w:$menu.outerWidth(), h:$menu.outerHeight()}) //remember main menu's dimensions
$menu.data('timers', {})
});
$menu.data('dimensions', {w:$menu.outerWidth(), h:$menu.outerHeight()}); //remember main menu's dimensions
$menu.data('timers', {});
this.builtdropdownids.push($menu.get(0).id) //remember id of dropdown that was just built
},
init:function($, $target, $dropdown){
if (this.builtdropdownids.length==0){ //only bind click event to document once
if (this.builtdropdownids.length === 0){ //only bind click event to document once
$(document).bind("click", function(e){
if (e.button==0){ //hide all dropdown (and their sub ULs) when left mouse button is clicked
$('.jqdropdown').find('ul').andSelf().hide()
if (e.button === 0){ //hide all dropdown (and their sub ULs) when left mouse button is clicked
$('.jqdropdown').find('ul').addBack().hide()
}
})
}
if (jQuery.inArray($dropdown.get(0).id, this.builtdropdownids)==-1) //if this dropdown hasn't been built yet
this.builddropdown($, $dropdown, $target)
if (jQuery.inArray($dropdown.get(0).id, this.builtdropdownids) === -1) //if this dropdown hasn't been built yet
this.builddropdown($, $dropdown, $target);
if ($target.parents().filter('ul.jqdropdown').length>0) //if $target matches an element within the dropdown markup, don't bind ondropdown to that element
return
return;
$target.bind("mouseenter", function(e){
dropdownmenu.showbox($, $dropdown)
})
});
$target.bind("mouseleave", function(e){
dropdownmenu.hidebox($, $dropdown)
})
});
$target.bind("click", function(e){
dropdownmenu.stubbox($, $dropdown)
})
}
};
jQuery.fn.adddropdown=function(dropdownid){
var $=jQuery
return this.each(function(){ //return jQuery obj
var $target=$(this)
if ($('#'+dropdownid).length==1) //check dropdown is defined
dropdownmenu.init($, $target, $('#'+dropdownid))
})
};
//By default, add dropdown to anchor links with attribute "data-dropdown"
jQuery(document).ready(function($){
var $anchors=$('*[data-dropdown]')
jQuery.fn.adddropdown=function(dropdownid){
var $=jQuery;
return this.each(function(){ //return jQuery obj
var $target=$(this);
var $dropdownId = $('#'+dropdownid);
if ($dropdownId.length === 1) //check dropdown is defined
dropdownmenu.init($, $target, $dropdownId)
})
};
var $anchors=$('*[data-dropdown]');
$anchors.each(function(){
$(this).adddropdown(this.getAttribute('data-dropdown'))
})

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