Compare commits
10 Commits
a2aa986296
...
589b70aae9
| Author | SHA1 | Date | |
|---|---|---|---|
| 589b70aae9 | |||
| 87b0893ebd | |||
| 741f7fe57b | |||
| 1a1fbec6ed | |||
| 9cf5b4eece | |||
| 09208c5a87 | |||
| a7a4db6505 | |||
| 9c708a620e | |||
| 1a4f6a5b69 | |||
| cf487eeed1 |
@@ -2,6 +2,7 @@
|
|||||||
user/accounts/
|
user/accounts/
|
||||||
user/data/
|
user/data/
|
||||||
user/cache/
|
user/cache/
|
||||||
|
user/plugins/
|
||||||
|
|
||||||
# Claude
|
# Claude
|
||||||
.claude/
|
.claude/
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
start:
|
||||||
|
docker compose up -d
|
||||||
|
|
||||||
|
stop:
|
||||||
|
docker compose down
|
||||||
|
|
||||||
|
setup: start install-plugins
|
||||||
|
|
||||||
|
install-plugins:
|
||||||
|
docker exec natascha_grav php /app/www/public/bin/gpm install $(shell cat plugins.txt | tr '\n' ' ') -y
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
# natascha-rieter.nl — Grav CMS
|
||||||
|
|
||||||
|
## Local development
|
||||||
|
|
||||||
|
Requires Docker. To set up from scratch:
|
||||||
|
|
||||||
|
```
|
||||||
|
make setup
|
||||||
|
```
|
||||||
|
|
||||||
|
This starts the container and installs all plugins listed in `plugins.txt` via GPM.
|
||||||
|
|
||||||
|
Other commands:
|
||||||
|
|
||||||
|
```
|
||||||
|
make start # start the container
|
||||||
|
make stop # stop the container
|
||||||
|
make install-plugins # (re)install plugins from plugins.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Site runs at http://localhost:8080.
|
||||||
|
|
||||||
|
## Repository structure
|
||||||
|
|
||||||
|
| Path | Description | In git |
|
||||||
|
|------|-------------|--------|
|
||||||
|
| `user/config/` | Site and plugin configuration | yes |
|
||||||
|
| `user/pages/` | Page content | yes (see deployment note) |
|
||||||
|
| `user/themes/natascha/` | Custom theme | yes |
|
||||||
|
| `user/plugins/` | Plugins (see plugins.txt) | no |
|
||||||
|
| `user/accounts/` | Admin credentials | no |
|
||||||
|
| `user/data/` | Runtime data | no |
|
||||||
|
| `user/cache/` | Generated cache | no |
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
Production is shared hosting. Deploy via FTP.
|
||||||
|
|
||||||
|
### Initial deploy
|
||||||
|
Upload the full `user/` directory (including locally installed plugins from `user/plugins/`).
|
||||||
|
|
||||||
|
### Subsequent deploys
|
||||||
|
**Only upload** `user/themes/` and `user/config/`. Do **not** overwrite `user/pages/` — content is managed online by editors after go-live and a deploy would overwrite their changes.
|
||||||
|
|
||||||
|
> **Future improvement**: once the site is live and content is stable, move `user/pages/` to `.gitignore` entirely (Option 1). Content backups should be handled separately (e.g. a server-side backup solution), not via git.
|
||||||
|
|
||||||
|
## Plugins
|
||||||
|
|
||||||
|
Plugins are not committed to git. The full list is in `plugins.txt`. To install locally, run `make install-plugins`. On the production server, plugins must be uploaded manually (no CLI access on shared hosting).
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
grav:
|
grav:
|
||||||
image: lscr.io/linuxserver/grav:latest
|
image: lscr.io/linuxserver/grav:1.7.49.5-ls244
|
||||||
container_name: natascha_grav
|
container_name: natascha_grav
|
||||||
environment:
|
environment:
|
||||||
- PUID=1000
|
- PUID=1000
|
||||||
|
|||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
admin
|
||||||
|
email
|
||||||
|
error
|
||||||
|
flex-objects
|
||||||
|
form
|
||||||
|
login
|
||||||
|
problems
|
||||||
|
sitemap
|
||||||
|
social-meta-tags
|
||||||
|
draft-preview
|
||||||
|
langswitcher
|
||||||
|
automagic-images
|
||||||
|
admin-media-move
|
||||||
|
admin-media-replace
|
||||||
|
admin-media-actions
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
enabled: false
|
||||||
@@ -2,4 +2,4 @@ enabled: true
|
|||||||
built_in_css: true
|
built_in_css: true
|
||||||
translated_urls: true
|
translated_urls: true
|
||||||
untranslated_pages_behavior: none
|
untranslated_pages_behavior: none
|
||||||
language_display: long
|
language_display: short
|
||||||
@@ -168,7 +168,7 @@ debugger:
|
|||||||
shutdown:
|
shutdown:
|
||||||
close_connection: true
|
close_connection: true
|
||||||
images:
|
images:
|
||||||
adapter: gd
|
adapter: imagick
|
||||||
default_image_quality: 85
|
default_image_quality: 85
|
||||||
cache_all: false
|
cache_all: false
|
||||||
cache_perms: '0755'
|
cache_perms: '0755'
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
title: 'Natascha Rieter'
|
||||||
|
menu: Home
|
||||||
|
published: true
|
||||||
|
sitemap:
|
||||||
|
lastmod: '19-04-2026 00:00'
|
||||||
|
image_main: portret-2.jpg
|
||||||
|
image_secondary: portret-1.jpg
|
||||||
|
image_logo: logo-blauw.png
|
||||||
|
---
|
||||||
|
|
||||||
|
I create small sculptures, reliefs, clay paintings, modelled figures and wheel-thrown work. My work is emotional and poetic with a monumental character. The outdoor objects are frost-resistant. In my clay paintings I combine ceramic and painting techniques. Working with clay is for me the same as writing a poem. In recent years I have been almost exclusively occupied with monumental commissions. I work on commission and give courses and workshops.
|
||||||
|
|
||||||
|
Side activities: Ceramics teacher, Kumulus in Maastricht; gallery owner of ceramic gallery "Groot Welsden"; owner of B&B de Kunstkamer.
|
||||||
@@ -4,6 +4,9 @@ menu: Home
|
|||||||
published: true
|
published: true
|
||||||
sitemap:
|
sitemap:
|
||||||
lastmod: '18-04-2026 21:48'
|
lastmod: '18-04-2026 21:48'
|
||||||
|
image_secondary: portret-1.jpg
|
||||||
|
image_main: portret-2.jpg
|
||||||
|
image_logo: logo-blauw.png
|
||||||
---
|
---
|
||||||
|
|
||||||
Ik maak kleinplastieken, reliëfs, kleischilderijen, geboetseerde beelden en draaiwerk. Mijn werk is emotioneel en poëtisch met een monumentaal karakter. De objecten voor buiten zijn winterhard. In mijn kleischilderijen worden de keramische- en schilderstechniek met elkaar gecombineerd. Het werken met klei is voor mij het zelfde als het schrijven van een gedicht. De laatste jaren ben ik bijna uitsluitend met monumentale opdrachten bezig geweest. Ik werk in opdracht en geef cursussen en workshops.
|
Ik maak kleinplastieken, reliëfs, kleischilderijen, geboetseerde beelden en draaiwerk. Mijn werk is emotioneel en poëtisch met een monumentaal karakter. De objecten voor buiten zijn winterhard. In mijn kleischilderijen worden de keramische- en schilderstechniek met elkaar gecombineerd. Het werken met klei is voor mij het zelfde als het schrijven van een gedicht. De laatste jaren ben ik bijna uitsluitend met monumentale opdrachten bezig geweest. Ik werk in opdracht en geef cursussen en workshops.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
title: Natascha Rieter – Curriculum Vitae
|
||||||
|
menu: CV
|
||||||
|
image_portrait: portret-1.jpg
|
||||||
|
image_logo: logo-blauw.png
|
||||||
|
---
|
||||||
|
|
||||||
|
Natascha Rieter, born in Roermond (1948), has lived since 1988 in Margraten, in the hamlet of Groot Welsden, where she and her husband Siegfried Gorinskat (also a ceramist, who passed away in 2006) established their studios in a typical half-timbered farmhouse. Both artists enjoy great renown in the art world for their ceramic works. In 1989 they opened Ceramic Gallery "Groot Welsden" — with great success. Natascha Rieter is an emotional and poetic artist for whom ceramics is the mirror of the soul.
|
||||||
|
|
||||||
|
Natascha studied monumental design, majoring in painting, at the City Academy in Maastricht (1967–1971). After her studies she worked for two years in a ceramic studio in Switzerland (1971–1972). From 1972 to 1975 she studied ceramics at the Ceramic Hochschule in Höhr-Grenzhausen, Germany. She then moved to Nijswiller (South Limburg), where she established herself as a ceramist (1976–1988). She has exhibited in many galleries in the Netherlands, Germany, Belgium, France and Japan. The themes in her work are nature and humanity: LIFE. She translates her feelings into ceramics — "Working with clay is for me the same as writing a poem."
|
||||||
|
|
||||||
|
Natascha Rieter makes functional ceramics, reliefs, small sculptures, modelled figures and clay paintings. She uses stoneware clay from Germany and porcelain from France. In her clay paintings she combines ceramics with painting techniques on wooden panels.
|
||||||
|
|
||||||
|
In recent years Natascha has been almost exclusively occupied with monumental commissions in applied art — wall reliefs in building lobbies, monumental sculpture groups, and private and corporate commissions.
|
||||||
|
|
||||||
|
Side activities: Ceramics teacher at Kumulus, Centre for the Arts, Maastricht since 1977. Gallery owner of Ceramic Gallery "Groot Welsden" since 1989.
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
---
|
---
|
||||||
title: Natascha Rieter – Curriculum Vitae
|
title: Natascha Rieter – Curriculum Vitae
|
||||||
menu: CV
|
menu: CV
|
||||||
|
image_portrait: portret-1.jpg
|
||||||
|
image_logo: logo-blauw.png
|
||||||
---
|
---
|
||||||
|
|
||||||
Natascha Rieter, geboren te Roermond (1948), woont sinds 1988 te Margraten, in het gehucht Groot Welsden, waar zij met haar man Siegfried Gorinskat (ook keramist) (2006 gestorven) in een typische vakwerkboerderij hun ateliers hebben gevestigd. Beide kunstenaars genieten vanwege hun keramische werken grote bekendheid in de kunstwereld. In 1989 openden zij Keramiek Galerie "Groot Welsden" en niet zonder succes. Natascha Rieter is een emotioneel en poëtisch kunstenaar voor wie keramiek de spiegel van de ziel is.
|
Natascha Rieter, geboren te Roermond (1948), woont sinds 1988 te Margraten, in het gehucht Groot Welsden, waar zij met haar man Siegfried Gorinskat (ook keramist) (2006 gestorven) in een typische vakwerkboerderij hun ateliers hebben gevestigd. Beide kunstenaars genieten vanwege hun keramische werken grote bekendheid in de kunstwereld. In 1989 openden zij Keramiek Galerie "Groot Welsden" en niet zonder succes. Natascha Rieter is een emotioneel en poëtisch kunstenaar voor wie keramiek de spiegel van de ziel is.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 83 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
title: Gallery
|
||||||
|
menu: Gallery
|
||||||
|
---
|
||||||
|
|
||||||
|
A selection of ceramic works by Natascha Rieter — from small sculptures to monumental commissions. Click an image for an enlarged view.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: Work in detail
|
||||||
|
menu: Work in detail
|
||||||
|
---
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
title: Contact & Address
|
||||||
|
menu: Contact
|
||||||
|
---
|
||||||
|
|
||||||
|
**Opening hours**
|
||||||
|
Every Friday and Saturday
|
||||||
|
The first Sunday of the month
|
||||||
|
From 11:00 to 17:00
|
||||||
|
Also by appointment
|
||||||
|
|
||||||
|
Sint Agnesplein 4
|
||||||
|
NL-6241 CA Bunde
|
||||||
|
[+31 (0)43 458 27 51](tel:+31434582751)
|
||||||
|
[info@natascha-rieter.nl](mailto:info@natascha-rieter.nl)
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# v1.0.4
|
|
||||||
## 2/15/2018
|
|
||||||
|
|
||||||
1. [](#new)
|
|
||||||
* Update styles for Grav Admin v1.7.
|
|
||||||
|
|
||||||
# v1.0.3
|
|
||||||
## 2/15/2018
|
|
||||||
|
|
||||||
1. [](#new)
|
|
||||||
* Add column margin to media actions.
|
|
||||||
* Add border-radius to media actions.
|
|
||||||
|
|
||||||
# v1.0.2
|
|
||||||
## 2/14/2018
|
|
||||||
|
|
||||||
1. [](#new)
|
|
||||||
* Update bugs URL.
|
|
||||||
* Update addAction signature
|
|
||||||
|
|
||||||
# v1.0.1
|
|
||||||
## 2/7/2018
|
|
||||||
|
|
||||||
1. [](#new)
|
|
||||||
* Show alert dialog for sample actions.
|
|
||||||
* Update sample action log message.
|
|
||||||
|
|
||||||
# v1.0.0
|
|
||||||
## 1/24/2018
|
|
||||||
|
|
||||||
1. [](#Initial)
|
|
||||||
*
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2018 TwelveTone LLC
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
# Admin Media Actions Plugin
|
|
||||||
|
|
||||||
The **Admin Media Actions** Plugin is for [Grav CMS](http://github.com/getgrav/grav). A plugin which adds an API for adding actions items to media items in the media bin.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Installing the Admin Media Actions plugin can be done in one of two ways. The 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.
|
|
||||||
|
|
||||||
### 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 admin-media-actions
|
|
||||||
|
|
||||||
This will install the Admin Media Actions plugin into your `/user/plugins` directory within Grav. Its files can be found under `/your/site/grav/user/plugins/admin-media-actions`.
|
|
||||||
|
|
||||||
### Manual Installation
|
|
||||||
|
|
||||||
To install this plugin, just download the zip version of this repository and unzip it under `/your/site/grav/user/plugins`. Then, rename the folder to `admin-media-actions`. You can find these files on [GitHub](https://github.com) or via [GetGrav.org](http://getgrav.org/downloads/plugins#extras).
|
|
||||||
|
|
||||||
You should now have all the plugin files under
|
|
||||||
|
|
||||||
/your/site/grav/user/plugins/admin-media-actions
|
|
||||||
|
|
||||||
> NOTE: This plugin is a modular component for Grav which requires [Grav](http://github.com/getgrav/grav) and the [Admin](https://github.com/getgrav/grav-plugin-admin) plugin to operate.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
See online [documentation](https://www.twelvetone.tv/docs/developer-tools/grav-plugins/grav-admin-media-actions-plugin)
|
|
||||||
|
|
||||||
## Credits
|
|
||||||
|
|
||||||
A big thanks to David Szabo and his development on the grav-plugin-admin-addon-media-rename for some clever approaches
|
|
||||||
on tapping into the Grav architecture.
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*
|
|
||||||
* The MIT License (MIT)
|
|
||||||
*
|
|
||||||
* Copyright (c) 2018 TwelveTone LLC
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Grav\Plugin;
|
|
||||||
|
|
||||||
use Grav\Common\Plugin;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class AdminMediaActionsPlugin
|
|
||||||
* @package Grav\Plugin
|
|
||||||
*/
|
|
||||||
class AdminMediaActionsPlugin extends Plugin
|
|
||||||
{
|
|
||||||
|
|
||||||
const ROUTE = '/admin-media-actions';
|
|
||||||
|
|
||||||
public static function getSubscribedEvents()
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'onPluginsInitialized' => ['onPluginsInitialized', 0]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPath()
|
|
||||||
{
|
|
||||||
return '/' . trim($this->grav['admin']->base, '/') . '/' . trim(self::ROUTE, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function buildBaseUrl()
|
|
||||||
{
|
|
||||||
$ret = rtrim($this->grav['uri']->rootUrl(false), '/') . '/' . trim($this->getPath(), '/');
|
|
||||||
return $ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onPluginsInitialized()
|
|
||||||
{
|
|
||||||
if (!$this->isAdmin() || !$this->grav['user']->authenticated) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register the media actions service
|
|
||||||
$this->grav['media-actions'] = function ($c) {
|
|
||||||
return new MediaActionsController();
|
|
||||||
};
|
|
||||||
|
|
||||||
// Ignore requests to the plugin URL
|
|
||||||
if ($this->grav['uri']->path() == $this->getPath()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->config->get('plugins.admin-media-actions.show_samples')) {
|
|
||||||
// Sample Actions
|
|
||||||
$this->grav['media-actions']->addAction("SampleAction1", "Sample Action 1", "play-circle", function ($page, $mediaName, $payload) {
|
|
||||||
return [
|
|
||||||
"path" => $page->path(),
|
|
||||||
"route" => $page->route(),
|
|
||||||
"mediaName" => $mediaName,
|
|
||||||
];
|
|
||||||
});
|
|
||||||
$this->grav['media-actions']->addAction("SampleAction2", "Sample Action 2", "play-circle", function ($page, $mediaName, $payload) {
|
|
||||||
return [
|
|
||||||
"path" => $page->path(),
|
|
||||||
"route" => $page->route(),
|
|
||||||
"mediaName" => $mediaName,
|
|
||||||
];
|
|
||||||
});
|
|
||||||
$this->grav['media-actions']->addAction("SampleAction3", "Sample Action 3", "play-circle", function ($page, $mediaName, $payload) {
|
|
||||||
return [
|
|
||||||
"path" => $page->path(),
|
|
||||||
"route" => $page->route(),
|
|
||||||
"mediaName" => $mediaName,
|
|
||||||
];
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->grav['media-actions']->addAction("SampleForm", "Sample Form", "list", function ($page, $mediaName, $payload) {
|
|
||||||
return "ok";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$this->enable([
|
|
||||||
'onAdminTwigTemplatePaths' => ['onAdminTwigTemplatePaths', 0],
|
|
||||||
'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0],
|
|
||||||
'onPagesInitialized' => ['onTwigExtensions', 0],
|
|
||||||
'onAdminTaskExecute' => ['onAdminTaskExecute', 0],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onAdminTaskExecute($e)
|
|
||||||
{
|
|
||||||
$method = $e['method'];
|
|
||||||
switch ($method) {
|
|
||||||
case "taskMedia-action":
|
|
||||||
|
|
||||||
$page = $this->grav['admin']->page(false);
|
|
||||||
//$route = $page->route();
|
|
||||||
|
|
||||||
$actionId = $_POST['action_id'];
|
|
||||||
$media_name = $_POST['media_name'];
|
|
||||||
$payload = json_decode($_POST['payload'], true);
|
|
||||||
|
|
||||||
$handler = $this->grav['media-actions']->getHandlerForAction($actionId);
|
|
||||||
if ($handler) {
|
|
||||||
$json = $handler($page, $media_name, $payload);
|
|
||||||
die("{\"result\":" . json_encode($json) . "}");
|
|
||||||
} else {
|
|
||||||
die("{\"result\":{\"error\":true}}");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onAdminTwigTemplatePaths()
|
|
||||||
{
|
|
||||||
// $event['paths'] = __DIR__ . '/themes/grav/templates';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onTwigTemplatePaths()
|
|
||||||
{
|
|
||||||
$this->grav['twig']->twig_paths[] = __DIR__ . '/templates';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onTwigExtensions()
|
|
||||||
{
|
|
||||||
$page = $this->grav['admin']->page(true);
|
|
||||||
if (!$page) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->config->get('plugins.admin-media-actions.show_samples')) {
|
|
||||||
$this->grav['assets']->addJs('plugin://admin-media-actions/assets/samples/sample_actions.js', -1000, false);
|
|
||||||
$this->grav['assets']->addJs('plugin://admin-media-actions/assets/samples/sample_form_action.js', -1000, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
$oCopy = [];
|
|
||||||
foreach ($this->grav['media-actions']->actions as $action) {
|
|
||||||
$oCopy[] = [
|
|
||||||
'actionId' => $action['actionId'],
|
|
||||||
'icon' => $action['icon'],
|
|
||||||
'caption' => $action['caption'],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
$this->grav['assets']->addInlineJs('const MEDIA_ACTIONS = ' . json_encode($oCopy) . ';', -1000, false);
|
|
||||||
|
|
||||||
$taskUrl = $this->buildBaseUrl() . $page->route() . '/task:media-action';
|
|
||||||
$this->grav['assets']->addInlineJs('const MEDIA_ACTION_TASK_URL = ' . json_encode($taskUrl) . ';', -1000, false);
|
|
||||||
|
|
||||||
$this->grav['assets']->addJs('plugin://admin-media-actions/assets/admin-media-actions.js', -1000, false);
|
|
||||||
$this->grav['assets']->addCss('plugin://admin-media-actions/assets/admin-media-actions.css', -1000, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function outputError($msg)
|
|
||||||
{
|
|
||||||
header('HTTP/1.1 400 Bad Request');
|
|
||||||
die(json_encode(['error' => ['msg' => $msg]]));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class MediaActionsController
|
|
||||||
{
|
|
||||||
public $actions = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param $actionId A unique id for the action. Must be a valid Javascript function name.
|
|
||||||
* This can also be an array containing keys of the same parameter names.
|
|
||||||
*
|
|
||||||
* @param $caption The caption for the action. Used for the tooltip.
|
|
||||||
* @param $icon The font-awesome icon name. The 'fa-' prefix is optional.
|
|
||||||
* @param $handler A handler for the action. (page, mediaName, payload) => object.
|
|
||||||
*/
|
|
||||||
function addAction($actionId, $caption = null, $icon = null, $handler = null)
|
|
||||||
{
|
|
||||||
if (is_array($actionId)) {
|
|
||||||
if (isset($actionId['caption'])) {
|
|
||||||
$caption = $actionId['caption'];
|
|
||||||
}
|
|
||||||
if (isset($actionId['icon'])) {
|
|
||||||
$icon = $actionId['icon'];
|
|
||||||
}
|
|
||||||
if (isset($actionId['handler'])) {
|
|
||||||
$handler = $actionId['handler'];
|
|
||||||
}
|
|
||||||
// do this last...
|
|
||||||
$actionId = $actionId['actionId'];
|
|
||||||
}
|
|
||||||
$this->actions[$actionId] = [
|
|
||||||
'handler' => $handler,
|
|
||||||
'caption' => $caption,
|
|
||||||
'icon' => $icon,
|
|
||||||
'actionId' => $actionId,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHandlerForAction($actionId)
|
|
||||||
{
|
|
||||||
if (isset($this->actions[$actionId])) {
|
|
||||||
return $this->actions[$actionId]['handler'];
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
#
|
|
||||||
# The MIT License (MIT)
|
|
||||||
#
|
|
||||||
# Copyright (c) 2018 TwelveTone LLC
|
|
||||||
#
|
|
||||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
# of this software and associated documentation files (the "Software"), to deal
|
|
||||||
# in the Software without restriction, including without limitation the rights
|
|
||||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
# copies of the Software, and to permit persons to whom the Software is
|
|
||||||
# furnished to do so, subject to the following conditions:
|
|
||||||
#
|
|
||||||
# The above copyright notice and this permission notice shall be included in all
|
|
||||||
# copies or substantial portions of the Software.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
# SOFTWARE.
|
|
||||||
#
|
|
||||||
|
|
||||||
enabled: true
|
|
||||||
show_samples: false
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
/**
|
|
||||||
* The MIT License (MIT)
|
|
||||||
*
|
|
||||||
* Copyright (c) 2018 TwelveTone LLC
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
span[data-dz-name] {
|
|
||||||
cursor: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dz-rename:hover:after {
|
|
||||||
color: #0082ba;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*.dropzone .dz-preview:hover .dz-rename {*/
|
|
||||||
/*display: block;*/
|
|
||||||
/*}*/
|
|
||||||
|
|
||||||
.dropzone .dz-preview:hover .dz-media-action {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dz-media-action {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
width: 25px;
|
|
||||||
height: 25px;
|
|
||||||
cursor: pointer;
|
|
||||||
background: #e1e1e1;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
border-radius: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.ga-theme-17x .dz-media-action {
|
|
||||||
background: #f2f2f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dz-media-action i {
|
|
||||||
color: #737c81;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dz-media-action i:hover {
|
|
||||||
color: #0082ba;
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
/*
|
|
||||||
* The MIT License (MIT)
|
|
||||||
*
|
|
||||||
* Copyright (c) 2018 TwelveTone LLC
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
// MEDIA_ACTIONS must be set
|
|
||||||
// MEDIA_ACTION_TASK_URL must be set
|
|
||||||
|
|
||||||
|
|
||||||
function _onMediaAction(actionId, mediaName, dz) {
|
|
||||||
let fn = "onMediaAction_" + actionId;
|
|
||||||
if (typeof window[fn] === 'function') {
|
|
||||||
window[fn].apply(null, [actionId, mediaName, dz]);
|
|
||||||
} else {
|
|
||||||
submitMediaAction(actionId, mediaName, "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitMediaAction(actionId, mediaName, payload = "", callback = null, modal = null) {
|
|
||||||
if (modal) {
|
|
||||||
$('.loading', modal).removeClass('hidden');
|
|
||||||
$('.button', modal).addClass('hidden');
|
|
||||||
}
|
|
||||||
var data = new FormData();
|
|
||||||
data.append('admin-nonce', GravAdmin.config.admin_nonce);
|
|
||||||
data.append("action_id", actionId);
|
|
||||||
data.append("media_name", mediaName);
|
|
||||||
data.append("payload", JSON.stringify(payload));
|
|
||||||
fetch(MEDIA_ACTION_TASK_URL, {method: 'POST', body: data, credentials: 'same-origin'})
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(result => {
|
|
||||||
if (modal) {
|
|
||||||
if (!result.error) {
|
|
||||||
modal.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (callback) {
|
|
||||||
callback(result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for new media every 1000 ms and add actions
|
|
||||||
setInterval(function () {
|
|
||||||
const size = 25; // The action icon size
|
|
||||||
const maxRows = 5;
|
|
||||||
const colMargin = 2;
|
|
||||||
|
|
||||||
$('.dz-preview').each(function (i, dz) {
|
|
||||||
if (!dz._actions) {
|
|
||||||
dz._actions = true;
|
|
||||||
let actionsIndex = 3; //TODO hardcoded to standard action count
|
|
||||||
//let top = 72; //TODO get max top of children (they are not in order)
|
|
||||||
//let top = actionsCount * size - size; // the standard icons ar off by 1 pixel!?
|
|
||||||
|
|
||||||
const that = this;
|
|
||||||
MEDIA_ACTIONS.forEach(function (item) {
|
|
||||||
actionsIndex++;
|
|
||||||
let faIcon = item.icon;
|
|
||||||
if (!faIcon) {
|
|
||||||
faIcon = "fa-play-circle";
|
|
||||||
}
|
|
||||||
if (!faIcon.startsWith('fa-')) {
|
|
||||||
faIcon = 'fa-' + faIcon;
|
|
||||||
}
|
|
||||||
const ele = document.createElement('a');
|
|
||||||
ele.className = 'dz-media-action';
|
|
||||||
ele.style.top = (Math.floor(actionsIndex % maxRows) * size - (Math.floor(actionsIndex % maxRows)) - 1) + 'px';
|
|
||||||
let right;
|
|
||||||
const col = Math.floor(actionsIndex / maxRows);
|
|
||||||
if (col === 0) {
|
|
||||||
right = -size;
|
|
||||||
} else {
|
|
||||||
right = -((1 + Math.floor((actionsIndex) / maxRows)) * size) - (col * colMargin);
|
|
||||||
}
|
|
||||||
ele.style.right = right + 'px';
|
|
||||||
ele.href = 'javascript:undefined;';
|
|
||||||
ele.title = item.caption;
|
|
||||||
ele.innerText = "";//item.caption;
|
|
||||||
const nameEle = $(dz).find('[data-dz-name]');
|
|
||||||
ele._file_name = nameEle.text();
|
|
||||||
ele._dz_preview = dz;
|
|
||||||
$(that).append(ele);
|
|
||||||
const i = document.createElement("i");
|
|
||||||
ele.appendChild(i);
|
|
||||||
i.className = 'fa fa-fw ' + faIcon;
|
|
||||||
ele.addEventListener('click', () => _onMediaAction(item.actionId, nameEle.text(), dz));
|
|
||||||
|
|
||||||
//Invisible div to maintain hover when the mouseover is on the column margin
|
|
||||||
const ele2 = document.createElement('div');
|
|
||||||
ele2.className = 'dz-media-action';
|
|
||||||
ele2.style.background = 'transparent';
|
|
||||||
ele2.style.right = (right + colMargin) + "px";
|
|
||||||
ele2.style.top = ele.style.top;
|
|
||||||
$(ele2).insertBefore(ele);
|
|
||||||
});
|
|
||||||
dz.style.marginRight = 15 + Math.floor(actionsIndex / maxRows) * (size + colMargin) + "px";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
/*
|
|
||||||
* The MIT License (MIT)
|
|
||||||
*
|
|
||||||
* Copyright (c) 2018 TwelveTone LLC
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function onMediaAction_SampleAction1(actionId, mediaName, mediaElement) {
|
|
||||||
console.log(`Media action ${actionId} was selected.`);
|
|
||||||
alert(`Media action ${actionId} was selected.`);
|
|
||||||
submitMediaAction(actionId, mediaName, `${actionId} was selected`, function (result) {
|
|
||||||
console.log(result);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMediaAction_SampleAction2(actionId, mediaName, mediaElement) {
|
|
||||||
console.log(`Media action ${actionId} was selected.`);
|
|
||||||
alert(`Media action ${actionId} was selected.`);
|
|
||||||
submitMediaAction(actionId, mediaName, `${actionId} was selected`, function (result) {
|
|
||||||
console.log(result);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMediaAction_SampleAction3(actionId, mediaName, mediaElement) {
|
|
||||||
console.log(`Media action ${actionId} was selected.`);
|
|
||||||
alert(`Media action ${actionId} was selected.`);
|
|
||||||
submitMediaAction(actionId, mediaName, `${actionId} was selected`, function (result) {
|
|
||||||
console.log(result);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
/*
|
|
||||||
* The MIT License (MIT)
|
|
||||||
*
|
|
||||||
* Copyright (c) 2018 TwelveTone LLC
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
// Sample Form Action
|
|
||||||
|
|
||||||
function onMediaAction_SampleForm(actionId, mediaName, mediaElement) {
|
|
||||||
alert("This is a sample form.");
|
|
||||||
submitMediaAction(actionId, mediaName, '{"key1":"value1", "key2":"value2"}');
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
#
|
|
||||||
# The MIT License (MIT)
|
|
||||||
#
|
|
||||||
# Copyright (c) 2018 TwelveTone LLC
|
|
||||||
#
|
|
||||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
# of this software and associated documentation files (the "Software"), to deal
|
|
||||||
# in the Software without restriction, including without limitation the rights
|
|
||||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
# copies of the Software, and to permit persons to whom the Software is
|
|
||||||
# furnished to do so, subject to the following conditions:
|
|
||||||
#
|
|
||||||
# The above copyright notice and this permission notice shall be included in all
|
|
||||||
# copies or substantial portions of the Software.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
# SOFTWARE.
|
|
||||||
#
|
|
||||||
|
|
||||||
name: Admin Media Actions
|
|
||||||
version: 1.0.4
|
|
||||||
description: A plugin that extends Grav with an API for adding actions to media items in the page media bin. This plugin is required by other plugins that add media actions.
|
|
||||||
icon: plug
|
|
||||||
author:
|
|
||||||
name: TwelveTone LLC
|
|
||||||
email: info@twelvetone.tv
|
|
||||||
homepage: https://www.twelvetone.tv/docs/developer-tools/grav-plugins/grav-admin-media-actions-plugin
|
|
||||||
keywords: grav, plugin, admin, media, action
|
|
||||||
bugs: https://github.com/Flamenco/grav-admin-media-actions/issues
|
|
||||||
docs: https://www.twelvetone.tv/docs/developer-tools/grav-plugins/grav-admin-media-actions-plugin
|
|
||||||
license: MIT
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
- { name: grav, version: '>=1.0.0' }
|
|
||||||
- { name: admin, version: '>=1.0.0' }
|
|
||||||
|
|
||||||
form:
|
|
||||||
validation: strict
|
|
||||||
fields:
|
|
||||||
enabled:
|
|
||||||
type: toggle
|
|
||||||
label: Plugin status
|
|
||||||
highlight: 1
|
|
||||||
default: 0
|
|
||||||
options:
|
|
||||||
1: Enabled
|
|
||||||
0: Disabled
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
|
|
||||||
show_samples:
|
|
||||||
type: toggle
|
|
||||||
label: Show sample actions
|
|
||||||
description: For testing purposes, several actions will be added to each media item.
|
|
||||||
highlight: 0
|
|
||||||
default: 0
|
|
||||||
options:
|
|
||||||
1: Enabled
|
|
||||||
0: Disabled
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id("com.github.hierynomus.license").version("0.14.0")
|
|
||||||
}
|
|
||||||
apply plugin:'java'
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
grav {
|
|
||||||
resources {
|
|
||||||
srcDirs += "."
|
|
||||||
include "**/*.yaml"
|
|
||||||
include "**/*.php"
|
|
||||||
include "**/*.css"
|
|
||||||
include "**/*.js"
|
|
||||||
include "**/*.twig"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<!--A generic form dialog that contains a loading indicator, submit button, and message -->
|
|
||||||
<div class="remodal" data-remodal-id="modal-admin-media-actions" data-remodal-options="hashTracking: false">
|
|
||||||
<form method="post" onsubmit='return false;'>
|
|
||||||
{% for field in fields %}
|
|
||||||
{% if field.type %}
|
|
||||||
{% set value = data.value(field.name) %}
|
|
||||||
<div class="block block-{{field.type}}">
|
|
||||||
{% include ["forms/fields/#{field.type}/#{field.type}.html.twig", 'forms/fields/text/text.html.twig'] %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<div class="block page-media-info hidden">
|
|
||||||
<div class="form-field grid">{{ "Page Info" }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="block non-page-media-info hidden">
|
|
||||||
<div class="form-field grid">{{ "Non-Page Info" }}</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="button-bar">
|
|
||||||
<div class="loading">
|
|
||||||
{{ "Moving" }}... <i class="fa fa-spinner fa-spin"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="button primary" style="visibility: hidden">{{ "PLUGIN_ADMIN.CONTINUE"|tu }}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# v1.0.4
|
|
||||||
## 12/11/2018
|
|
||||||
|
|
||||||
1. [](#bugfix)
|
|
||||||
* Update addInlineJs load order
|
|
||||||
|
|
||||||
# v1.0.3
|
|
||||||
## 2/24/2018
|
|
||||||
|
|
||||||
1. [](#new)
|
|
||||||
* Check that dependencies are enabled before loading
|
|
||||||
|
|
||||||
# v1.0.2
|
|
||||||
## 2/24/2018
|
|
||||||
|
|
||||||
1. [](#new)
|
|
||||||
* Check for dependencies before loading
|
|
||||||
|
|
||||||
# v1.0.1
|
|
||||||
## 2/14/2018
|
|
||||||
|
|
||||||
1. [](#new)
|
|
||||||
* Update bugs URL
|
|
||||||
* Update addAction signature
|
|
||||||
|
|
||||||
# v1.0.0
|
|
||||||
## 1/24/2018
|
|
||||||
|
|
||||||
1. [](#initial)
|
|
||||||
*
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2018 TwelveTone LLC
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
# Admin Media Move Plugin
|
|
||||||
|
|
||||||
The **Admin Media Move** Plugin is for [Grav CMS](http://github.com/getgrav/grav). A plugin which adds the option to move media files in the page bin to another page.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Installing the Admin Media Move plugin can be done in one of two ways. The 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.
|
|
||||||
|
|
||||||
### 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 admin-media-move
|
|
||||||
|
|
||||||
This will install the Admin Media Move plugin into your `/user/plugins` directory within Grav. Its files can be found under `/your/site/grav/user/plugins/admin-media-move`.
|
|
||||||
|
|
||||||
### Manual Installation
|
|
||||||
|
|
||||||
To install this plugin, just download the zip version of this repository and unzip it under `/your/site/grav/user/plugins`. Then, rename the folder to `admin-media-move`. You can find these files on [GitHub](https://github.com) or via [GetGrav.org](http://getgrav.org/downloads/plugins#extras).
|
|
||||||
|
|
||||||
You should now have all the plugin files under
|
|
||||||
|
|
||||||
/your/site/grav/user/plugins/admin-media-move
|
|
||||||
|
|
||||||
> NOTE: This plugin is a modular component for Grav which requires [Grav](http://github.com/getgrav/grav) and the [Admin](https://github.com/getgrav/grav-plugin-admin) plugin to operate.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*
|
|
||||||
* The MIT License (MIT)
|
|
||||||
*
|
|
||||||
* Copyright (c) 2018 TwelveTone LLC
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Grav\Plugin;
|
|
||||||
|
|
||||||
use Grav\Common\Grav;
|
|
||||||
use Grav\Common\Plugin;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class AdminMediaMovePlugin
|
|
||||||
* @package Grav\Plugin
|
|
||||||
*/
|
|
||||||
class AdminMediaMovePlugin extends Plugin
|
|
||||||
{
|
|
||||||
|
|
||||||
const ROUTE = '/admin-media-move';
|
|
||||||
|
|
||||||
public static function getSubscribedEvents()
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'onPluginsInitialized' => ['onPluginsInitialized', 0]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPath()
|
|
||||||
{
|
|
||||||
return '/' . trim($this->grav['admin']->base, '/') . '/' . trim(self::ROUTE, '/');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function buildBaseUrl()
|
|
||||||
{
|
|
||||||
$ret = rtrim($this->grav['uri']->rootUrl(false), '/') . '/' . trim($this->getPath(), '/');
|
|
||||||
return $ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onPluginsInitialized()
|
|
||||||
{
|
|
||||||
if (!$this->isAdmin() || !$this->grav['user']->authenticated) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->grav['uri']->path() == $this->getPath()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!self::_checkDependencies($this)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->enable([
|
|
||||||
'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0],
|
|
||||||
'onPagesInitialized' => ['onTwigExtensions', 0],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->grav['media-actions']->addAction([
|
|
||||||
'actionId' => "MediaMove",
|
|
||||||
'caption' => "Move",
|
|
||||||
'icon' => "arrows",
|
|
||||||
'handler' => function ($page, $mediaName, $payload) {
|
|
||||||
|
|
||||||
$destination_route = $payload['destination_route'];
|
|
||||||
|
|
||||||
if (!$destination_route || !$page || !$mediaName || !$payload) {
|
|
||||||
$this->outputError("Invalid input");
|
|
||||||
}
|
|
||||||
|
|
||||||
$basePath = $page->path() . DS;
|
|
||||||
$filePath = $basePath . $mediaName;
|
|
||||||
if (!file_exists($filePath)) {
|
|
||||||
$this->outputError("Media file not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Locate the target page
|
|
||||||
$targetPage = $this->grav['pages']->find($destination_route);
|
|
||||||
if (!$targetPage) {
|
|
||||||
$this->outputError("Page for route $destination_route not found");
|
|
||||||
}
|
|
||||||
|
|
||||||
$path = $targetPage->path();
|
|
||||||
try {
|
|
||||||
rename($filePath, "$path/$mediaName");
|
|
||||||
$this->grav['log']->info("Moved media file '$mediaName' to '$path'");
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->outputError("Could not move file: " . $e);
|
|
||||||
}
|
|
||||||
|
|
||||||
$ret = [
|
|
||||||
"error" => false
|
|
||||||
];
|
|
||||||
|
|
||||||
// Redirects will not work for fetch, so send destination url in result
|
|
||||||
if (get($payload, "go", false)) {
|
|
||||||
// Get the admin edit-page url
|
|
||||||
//$url = $this->grav['twig']->twig->getExtension('Grav\Plugin\Admin\AdminTwigExtension')->getPageUrl($this, $targetPage);
|
|
||||||
$url = $this->grav['uri']->rootUrl(false) . "/admin/pages" . $targetPage->route();
|
|
||||||
$ret["destination_url"] = $url;
|
|
||||||
}
|
|
||||||
|
|
||||||
//header('HTTP/1.1 200 OK');
|
|
||||||
return $ret;
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onTwigTemplatePaths()
|
|
||||||
{
|
|
||||||
$this->grav['twig']->twig_paths[] = __DIR__ . '/templates';
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onTwigExtensions()
|
|
||||||
{
|
|
||||||
$page = $this->grav['admin']->page(true);
|
|
||||||
if (!$page) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$modal_move = $this->grav['twig']->twig()->render('move-modal.twig.html', $this->config->get('plugins.admin-media-move.modal_move'));
|
|
||||||
$jsConfig_move = [
|
|
||||||
'MODAL' => $modal_move
|
|
||||||
];
|
|
||||||
$this->grav['assets']->addInlineJs('var ADMIN_ADDON_MEDIA_MOVE = ' . json_encode($jsConfig_move) . ';', -1000);
|
|
||||||
$this->grav['assets']->addJs('plugin://admin-media-move/assets/media_move_action.js', -1000, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function outputError($msg)
|
|
||||||
{
|
|
||||||
header('HTTP/1.1 400 Bad Request');
|
|
||||||
die(json_encode(['error' => ['msg' => $msg]]));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks plugin dependencies. Call this after all plugins have been loaded and are enabled.
|
|
||||||
*
|
|
||||||
* @param $plugin
|
|
||||||
* @param $issues array Receives issues as strings. If null, grav['messages'] is used.
|
|
||||||
* @return bool true if dependencies are met.
|
|
||||||
*/
|
|
||||||
public static function _checkDependencies($plugin, &$issues = null)
|
|
||||||
{
|
|
||||||
$grav = Grav::instance();
|
|
||||||
$errors = 0;
|
|
||||||
$messages = $grav['messages'];
|
|
||||||
$plugins = $grav['plugins'];
|
|
||||||
|
|
||||||
$deps = $plugin->getBlueprint()->dependencies;
|
|
||||||
if ($deps) {
|
|
||||||
foreach ($deps as $dep) {
|
|
||||||
$name = $dep['name'];
|
|
||||||
if ($name === 'grav') {
|
|
||||||
//TODO check grav version
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$version = $dep['version'];
|
|
||||||
if (!preg_match("#^([<>=]+)?(.*)#", $version, $m)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$compare = $m[1];
|
|
||||||
$version = $m[2];
|
|
||||||
if (!$compare) {
|
|
||||||
$compare = '=';
|
|
||||||
}
|
|
||||||
|
|
||||||
$found = $plugins->get($name);
|
|
||||||
if (!$found) {
|
|
||||||
$msg = "Missing Dependency: '$name'";
|
|
||||||
if (is_array($issues)) {
|
|
||||||
$issues[] = $msg;
|
|
||||||
} else {
|
|
||||||
$messages->add($msg, 'error');
|
|
||||||
}
|
|
||||||
$errors++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!$grav['config']->get("plugins.$name.enabled")) {
|
|
||||||
//BUG admin should always be enabled if installed
|
|
||||||
if ($name !== 'admin') {
|
|
||||||
$msg = "Dependency Not Enabled: '$name'";
|
|
||||||
if (is_array($issues)) {
|
|
||||||
$issues[] = $msg;
|
|
||||||
} else {
|
|
||||||
$messages->add($msg, 'error');
|
|
||||||
}
|
|
||||||
$errors++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$realVersion = $found->blueprints()->version;
|
|
||||||
if (!version_compare($realVersion, $version, $compare)) {
|
|
||||||
$msg = "Missing Dependency: '$name' $version";
|
|
||||||
if (is_array($issues)) {
|
|
||||||
$issues[] = $msg;
|
|
||||||
} else {
|
|
||||||
$messages->add($msg, 'error');
|
|
||||||
}
|
|
||||||
$errors++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($errors > 0) {
|
|
||||||
$msg = "Plugin '$plugin->name' was not loaded due to dependency issues";
|
|
||||||
if (is_array($issues)) {
|
|
||||||
$issues[] = $msg;
|
|
||||||
} else {
|
|
||||||
$messages->add($msg, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $errors === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
#
|
|
||||||
# The MIT License (MIT)
|
|
||||||
#
|
|
||||||
# Copyright (c) 2018 TwelveTone LLC
|
|
||||||
#
|
|
||||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
# of this software and associated documentation files (the "Software"), to deal
|
|
||||||
# in the Software without restriction, including without limitation the rights
|
|
||||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
# copies of the Software, and to permit persons to whom the Software is
|
|
||||||
# furnished to do so, subject to the following conditions:
|
|
||||||
#
|
|
||||||
# The above copyright notice and this permission notice shall be included in all
|
|
||||||
# copies or substantial portions of the Software.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
# SOFTWARE.
|
|
||||||
#
|
|
||||||
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
modal_move:
|
|
||||||
fields:
|
|
||||||
- type: section
|
|
||||||
title: "Move Media"
|
|
||||||
|
|
||||||
- type: text
|
|
||||||
label: Filename
|
|
||||||
name: file_name
|
|
||||||
readonly: true
|
|
||||||
|
|
||||||
- type: pages
|
|
||||||
label: Destination Page
|
|
||||||
name: destination_page
|
|
||||||
|
|
||||||
- type: text
|
|
||||||
label: Destination Route
|
|
||||||
name: destination_route
|
|
||||||
autofocus: on
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
/*
|
|
||||||
* The MIT License (MIT)
|
|
||||||
*
|
|
||||||
* Copyright (c) 2018 TwelveTone LLC
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// This must be in a function call for remodal to register it
|
|
||||||
$(function () {
|
|
||||||
$('body').append(ADMIN_ADDON_MEDIA_MOVE.MODAL);
|
|
||||||
});
|
|
||||||
|
|
||||||
function onMediaAction_MediaMove(actionId, mediaName, mediaElement) {
|
|
||||||
var modal = $.remodal.lookup[$('[data-remodal-id=modal-admin-media-move]').data('remodal')];
|
|
||||||
modal.open();
|
|
||||||
|
|
||||||
var $modal = modal.$modal;
|
|
||||||
// Populate fields
|
|
||||||
$('[name=file_name]', $modal).val(mediaName);
|
|
||||||
$('[name=destination_route]', $modal).val("");
|
|
||||||
$('[name=destination_page]', $modal).val("");
|
|
||||||
|
|
||||||
// Reset loading state
|
|
||||||
$('.loading', $modal).addClass('hidden');
|
|
||||||
$('.button', $modal).removeClass('hidden').css('visibility', 'visible');
|
|
||||||
|
|
||||||
$(document).off('click', '[data-remodal-id=modal-admin-media-move] .button');
|
|
||||||
$(document).on('click', '[data-remodal-id=modal-admin-media-move] .button', function (e) {
|
|
||||||
var destination_route = $('[name=destination_route]').val();
|
|
||||||
if (!destination_route) {
|
|
||||||
destination_route = $('[name=destination_page]').val();
|
|
||||||
}
|
|
||||||
if (destination_route) {
|
|
||||||
const payload = {
|
|
||||||
destination_route
|
|
||||||
};
|
|
||||||
if (e.target.name === 'move_and_go') {
|
|
||||||
payload.go = true;
|
|
||||||
}
|
|
||||||
const callback = function (result) {
|
|
||||||
if (result.error) {
|
|
||||||
alert(result.error.msg);
|
|
||||||
} else {
|
|
||||||
$(mediaElement).remove();
|
|
||||||
if (payload.go) {
|
|
||||||
window.location = result.result.destination_url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
submitMediaAction(actionId, mediaName, payload, callback, modal);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
#
|
|
||||||
# The MIT License (MIT)
|
|
||||||
#
|
|
||||||
# Copyright (c) 2018 TwelveTone LLC
|
|
||||||
#
|
|
||||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
# of this software and associated documentation files (the "Software"), to deal
|
|
||||||
# in the Software without restriction, including without limitation the rights
|
|
||||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
# copies of the Software, and to permit persons to whom the Software is
|
|
||||||
# furnished to do so, subject to the following conditions:
|
|
||||||
#
|
|
||||||
# The above copyright notice and this permission notice shall be included in all
|
|
||||||
# copies or substantial portions of the Software.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
# SOFTWARE.
|
|
||||||
#
|
|
||||||
|
|
||||||
name: Admin Media Move
|
|
||||||
version: 1.0.4
|
|
||||||
description: Moves media from one page to another.
|
|
||||||
icon: plug
|
|
||||||
author:
|
|
||||||
name: TwelveTone LLC
|
|
||||||
email: info@twelvetone.tv
|
|
||||||
homepage: https://www.twelvetone.tv/docs/developer-tools/grav-plugins/grav-admin-media-move-plugin
|
|
||||||
keywords: grav, plugin, admin, media
|
|
||||||
bugs: https://github.com/Flamenco/grav-admin-media-move
|
|
||||||
docs: https://www.twelvetone.tv/docs/developer-tools/grav-plugins/grav-admin-media-move-plugin
|
|
||||||
license: MIT
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
- { name: grav, version: '>=1.0.0' }
|
|
||||||
- { name: admin, version: '>=1.0.0' }
|
|
||||||
- { name: admin-media-actions, version: '>=1.0.0' }
|
|
||||||
|
|
||||||
form:
|
|
||||||
validation: strict
|
|
||||||
fields:
|
|
||||||
enabled:
|
|
||||||
type: toggle
|
|
||||||
label: Plugin status
|
|
||||||
highlight: 1
|
|
||||||
default: 0
|
|
||||||
options:
|
|
||||||
1: Enabled
|
|
||||||
0: Disabled
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id("com.github.hierynomus.license").version("0.14.0")
|
|
||||||
}
|
|
||||||
apply plugin:'java'
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
grav {
|
|
||||||
resources {
|
|
||||||
srcDirs += "."
|
|
||||||
include "**/*.yaml"
|
|
||||||
include "**/*.php"
|
|
||||||
include "**/*.css"
|
|
||||||
include "**/*.js"
|
|
||||||
include "**/*.twig"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
<div class="remodal" data-remodal-id="modal-admin-media-move" data-remodal-options="hashTracking: false">
|
|
||||||
<form method="post" onsubmit='return false;'>
|
|
||||||
{% for field in fields %}
|
|
||||||
{% if field.type %}
|
|
||||||
{% set value = data.value(field.name) %}
|
|
||||||
<div class="block block-{{field.type}}">
|
|
||||||
{% include ["forms/fields/#{field.type}/#{field.type}.html.twig", 'forms/fields/text/text.html.twig'] %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<div class="form-field grid">Select a destination page or enter the destination page route.</div>
|
|
||||||
|
|
||||||
<div class="button-bar">
|
|
||||||
<div class="loading">
|
|
||||||
{{ "Moving" }}... <i class="fa fa-spinner fa-spin"></i>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="button primary" style="visibility: hidden" name="move">{{ "Move" }}</button>
|
|
||||||
<button class="button" style="visibility: hidden" name="move_and_go">{{ "Move And Go" }}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
# v1.0.4
|
|
||||||
## 12/11/2018
|
|
||||||
|
|
||||||
1. [](#feature)
|
|
||||||
* Update documentation
|
|
||||||
|
|
||||||
# v1.0.3
|
|
||||||
## 2/24/2018
|
|
||||||
|
|
||||||
1. [](#new)
|
|
||||||
* Check that dependencies are enabled before loading
|
|
||||||
|
|
||||||
# v1.0.2
|
|
||||||
## 2/24/2018
|
|
||||||
|
|
||||||
1. [](#new)
|
|
||||||
* Check for dependencies before loading
|
|
||||||
|
|
||||||
# v1.0.1
|
|
||||||
## 1/25/2018
|
|
||||||
|
|
||||||
1. [](#new)
|
|
||||||
* Add dialog
|
|
||||||
* Add quicksend option
|
|
||||||
* Enforce extension, image, and file-rename options
|
|
||||||
* Refactor project layout
|
|
||||||
|
|
||||||
# v1.0.0
|
|
||||||
## 1/24/2018
|
|
||||||
|
|
||||||
1. [](#initial)
|
|
||||||
*
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2018 TwelveTone LLC
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
# Admin Media Replace Plugin
|
|
||||||
|
|
||||||
The **Admin Media Replace** Plugin is for [Grav CMS](http://github.com/getgrav/grav). A plugin which adds the option to replace media.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
Installing the Admin Media Replace plugin can be done in one of two ways. The 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.
|
|
||||||
|
|
||||||
### 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 admin-media-replace
|
|
||||||
|
|
||||||
This will install the Admin Media Replace plugin into your `/user/plugins` directory within Grav. Its files can be found under `/your/site/grav/user/plugins/admin-media-replace`.
|
|
||||||
|
|
||||||
### Manual Installation
|
|
||||||
|
|
||||||
To install this plugin, just download the zip version of this repository and unzip it under `/your/site/grav/user/plugins`. Then, rename the folder to `admin-media-replace`. You can find these files on [GitHub](https://github.com) or via [GetGrav.org](http://getgrav.org/downloads/plugins#extras).
|
|
||||||
|
|
||||||
You should now have all the plugin files under
|
|
||||||
|
|
||||||
/your/site/grav/user/plugins/admin-media-replace
|
|
||||||
|
|
||||||
> NOTE: This plugin is a modular component for Grav which requires [Grav](http://github.com/getgrav/grav) and the [Admin](https://github.com/getgrav/grav-plugin-admin) plugin to operate.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
See [Official Documentation](https://www.twelvetone.tv/docs/developer-tools/grav-plugins/admin-media-replace-plugin)
|
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
<?php
|
|
||||||
/*
|
|
||||||
* The MIT License (MIT)
|
|
||||||
*
|
|
||||||
* Copyright (c) 2018 TwelveTone LLC
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Grav\Plugin;
|
|
||||||
|
|
||||||
use Grav\Common\Grav;
|
|
||||||
use Grav\Common\Page\Media;
|
|
||||||
use Grav\Common\Plugin;
|
|
||||||
use Grav\Common\Utils;
|
|
||||||
|
|
||||||
include_once 'classes/DialogUtil.php';
|
|
||||||
|
|
||||||
function array_get($arr, $key, $default = null)
|
|
||||||
{
|
|
||||||
if (!isset($arr[$key])) {
|
|
||||||
if ($default === null) {
|
|
||||||
throw new \Exception("A key is missing: " . $key);
|
|
||||||
} else {
|
|
||||||
return $default;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $arr[$key];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class AdminMediaReplacePlugin
|
|
||||||
* @package Grav\Plugin
|
|
||||||
*/
|
|
||||||
class AdminMediaReplacePlugin extends Plugin
|
|
||||||
{
|
|
||||||
|
|
||||||
const ROUTE = '/admin-media-replace';
|
|
||||||
|
|
||||||
public static function getSubscribedEvents()
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'onPluginsInitialized' => ['onPluginsInitialized', 0]
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onPluginsInitialized()
|
|
||||||
{
|
|
||||||
if (!$this->isAdmin() || !$this->grav['user']->authenticated) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!self::_checkDependencies($this)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->enable([
|
|
||||||
'onAdminTwigTemplatePaths' => ['onAdminTwigTemplatePaths', 0],
|
|
||||||
'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0],
|
|
||||||
'onTwigInitialized' => ['onTwigInitialized', 0],
|
|
||||||
'onTwigExtensions' => ['onTwigExtensions', -1],
|
|
||||||
'onPageNotFound' => ['onPageNotFound', 1],
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->grav['media-actions']->addAction([
|
|
||||||
'actionId' => "MediaReplace",
|
|
||||||
'caption' => "Replace",
|
|
||||||
'icon' => "exchange",
|
|
||||||
'handler' => function ($page, $mediaName, $payload) {
|
|
||||||
$ret = [
|
|
||||||
"error" => false
|
|
||||||
];
|
|
||||||
return $ret;
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function onPageNotFound($e)
|
|
||||||
{
|
|
||||||
if (!$this->isAdmin()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$route = $this->grav['admin']->location . "/" . $this->grav['admin']->route;
|
|
||||||
switch ($route) {
|
|
||||||
case "admin-media-replace/replace":
|
|
||||||
try {
|
|
||||||
$filename = array_get($_POST, "media-new-filename");
|
|
||||||
$route = array_get($_POST, "media-route");
|
|
||||||
$media = array_get($_POST, "media-filename");
|
|
||||||
|
|
||||||
$media_rename = array_get($_POST, "media-rename", "1") === "1";
|
|
||||||
$require_image = array_get($_POST, "media-require-image", "1") === "1";
|
|
||||||
$match_extension = array_get($_POST, "media-match-extension", "1") === "1";
|
|
||||||
|
|
||||||
$page = $this->grav['pages']->find($route);
|
|
||||||
if (!$page) {
|
|
||||||
throw new \Exception("Page not found.");
|
|
||||||
}
|
|
||||||
$mediaPath = $page->path() . "/" . $media;
|
|
||||||
if (!is_file($mediaPath)) {
|
|
||||||
throw new \Exception("Media not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
$tmp_name = $_FILES['mediaupload']['tmp_name'];
|
|
||||||
|
|
||||||
if ($match_extension) {
|
|
||||||
if (basename($filename) !== basename($media)) {
|
|
||||||
throw new \Exception("Media extensions do not match.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($require_image) {
|
|
||||||
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
|
||||||
$mime = finfo_file($finfo, $_FILES['mediaupload']['tmp_name']);
|
|
||||||
finfo_close($finfo);
|
|
||||||
if (!Utils::startsWith($mime, "image/")) {
|
|
||||||
throw new \Exception("Media must be an image.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($media_rename) {
|
|
||||||
// overwrite current file
|
|
||||||
$finalName = basename($mediaPath);
|
|
||||||
} else {
|
|
||||||
// delete current file
|
|
||||||
unlink($page->path() . '/' . $media);
|
|
||||||
$finalName = basename($filename);
|
|
||||||
}
|
|
||||||
|
|
||||||
move_uploaded_file($tmp_name, $page->path() . '/' . $finalName);
|
|
||||||
//$tmp_name = $_FILES["pictures"]["tmp_name"][$key];
|
|
||||||
// basename() may prevent filesystem traversal attacks;
|
|
||||||
// further validation/sanitation of the filename may be appropriate
|
|
||||||
//$name = basename($_FILES["pictures"]["name"][$key]);
|
|
||||||
|
|
||||||
$media1 = new Media($page->path());
|
|
||||||
$medium = $media1[basename($finalName)];
|
|
||||||
$url = $medium->display($medium->get('extension') === 'svg' ? 'source' : 'thumbnail')->cropZoom(400, 300)->url();
|
|
||||||
|
|
||||||
$ret = ["thumbnail" => $url];
|
|
||||||
$ret['newName'] = $finalName;
|
|
||||||
// if (!$media_rename) {
|
|
||||||
// $ret['toast'] = "Refresh the page to update the new page media name.";
|
|
||||||
// }
|
|
||||||
die(json_encode($ret));
|
|
||||||
|
|
||||||
// Get original name
|
|
||||||
//$source = $medium->higherQualityAlternative()->get('filename');
|
|
||||||
//$media_list[$name] = ['url' => $medium->display($medium->get('extension') === 'svg' ? 'source' : 'thumbnail')->cropZoom(400, 300)->url(), 'size' => $medium->get('size'), 'metadata' => $metadata, 'original' => $source->get('filename')];
|
|
||||||
|
|
||||||
} catch
|
|
||||||
(\Exception $exception) {
|
|
||||||
// die(print_r($_SERVER));
|
|
||||||
die(json_encode(["error" => $exception->getMessage()]));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public
|
|
||||||
function onTwigTemplatePaths()
|
|
||||||
{
|
|
||||||
$this->grav['twig']->twig_paths[] = __DIR__ . '/templates';
|
|
||||||
}
|
|
||||||
|
|
||||||
public
|
|
||||||
function onTwigInitialized()
|
|
||||||
{
|
|
||||||
$this->grav['assets']->addJs('plugin://admin-media-replace/assets/dialog_util.js', -1000, false);
|
|
||||||
$this->grav['assets']->addJs('plugin://admin-media-replace/assets/media_replace_action.js', -1000, false);
|
|
||||||
if ($this->config->get("plugins.admin-media-replace.quicksend", false)) {
|
|
||||||
$this->grav['assets']->addInlineJs("const _media_replace_isQuicksend = true;");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public
|
|
||||||
function onTwigExtensions()
|
|
||||||
{
|
|
||||||
if (!$this->isAdmin()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
addModalForm("MediaReplace", "generic-modal.twig.html");
|
|
||||||
}
|
|
||||||
|
|
||||||
public
|
|
||||||
function onAdminTwigTemplatePaths($event)
|
|
||||||
{
|
|
||||||
$event['paths'] = array_merge($event['paths'], [__DIR__ . '/templates']);
|
|
||||||
return $event;
|
|
||||||
}
|
|
||||||
|
|
||||||
public
|
|
||||||
function outputError($msg)
|
|
||||||
{
|
|
||||||
header('HTTP/1.1 400 Bad Request');
|
|
||||||
die(json_encode(['error' => ['msg' => $msg]]));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks plugin dependencies. Call this after all plugins have been loaded and are enabled.
|
|
||||||
*
|
|
||||||
* @param $plugin
|
|
||||||
* @param $issues array Receives issues as strings. If null, grav['messages'] is used.
|
|
||||||
* @return bool true if dependencies are met.
|
|
||||||
*/
|
|
||||||
public static function _checkDependencies($plugin, &$issues = null)
|
|
||||||
{
|
|
||||||
$grav = Grav::instance();
|
|
||||||
$errors = 0;
|
|
||||||
$messages = $grav['messages'];
|
|
||||||
$plugins = $grav['plugins'];
|
|
||||||
|
|
||||||
$deps = $plugin->getBlueprint()->dependencies;
|
|
||||||
if ($deps) {
|
|
||||||
foreach ($deps as $dep) {
|
|
||||||
$name = $dep['name'];
|
|
||||||
if ($name === 'grav') {
|
|
||||||
//TODO check grav version
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$version = $dep['version'];
|
|
||||||
if (!preg_match("#^([<>=]+)?(.*)#", $version, $m)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$compare = $m[1];
|
|
||||||
$version = $m[2];
|
|
||||||
if (!$compare) {
|
|
||||||
$compare = '=';
|
|
||||||
}
|
|
||||||
|
|
||||||
$found = $plugins->get($name);
|
|
||||||
if (!$found) {
|
|
||||||
$msg = "Missing Dependency: '$name'";
|
|
||||||
if (is_array($issues)) {
|
|
||||||
$issues[] = $msg;
|
|
||||||
} else {
|
|
||||||
$messages->add($msg, 'error');
|
|
||||||
}
|
|
||||||
$errors++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!$grav['config']->get("plugins.$name.enabled")) {
|
|
||||||
//BUG admin should always be enabled if installed
|
|
||||||
if ($name !== 'admin') {
|
|
||||||
$msg = "Dependency Not Enabled: '$name'";
|
|
||||||
if (is_array($issues)) {
|
|
||||||
$issues[] = $msg;
|
|
||||||
} else {
|
|
||||||
$messages->add($msg, 'error');
|
|
||||||
}
|
|
||||||
$errors++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$realVersion = $found->blueprints()->version;
|
|
||||||
if (!version_compare($realVersion, $version, $compare)) {
|
|
||||||
$msg = "Missing Dependency: '$name' $version";
|
|
||||||
if (is_array($issues)) {
|
|
||||||
$issues[] = $msg;
|
|
||||||
} else {
|
|
||||||
$messages->add($msg, 'error');
|
|
||||||
}
|
|
||||||
$errors++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if ($errors > 0) {
|
|
||||||
$msg = "Plugin '$plugin->name' was not loaded due to dependency issues";
|
|
||||||
if (is_array($issues)) {
|
|
||||||
$issues[] = $msg;
|
|
||||||
} else {
|
|
||||||
$messages->add($msg, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $errors === 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
#
|
|
||||||
# The MIT License (MIT)
|
|
||||||
#
|
|
||||||
# Copyright (c) 2018 TwelveTone LLC
|
|
||||||
#
|
|
||||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
# of this software and associated documentation files (the "Software"), to deal
|
|
||||||
# in the Software without restriction, including without limitation the rights
|
|
||||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
# copies of the Software, and to permit persons to whom the Software is
|
|
||||||
# furnished to do so, subject to the following conditions:
|
|
||||||
#
|
|
||||||
# The above copyright notice and this permission notice shall be included in all
|
|
||||||
# copies or substantial portions of the Software.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
# SOFTWARE.
|
|
||||||
#
|
|
||||||
|
|
||||||
enabled: true
|
|
||||||
quicksend: false
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
/**
|
|
||||||
* A re-modal wrapper to simplify usage.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function openModalDialog(remodalId) {
|
|
||||||
const dlgElement = $(`[data-remodal-id=${remodalId}]`);
|
|
||||||
const modal = $.remodal.lookup[dlgElement.data('remodal')];
|
|
||||||
modal.open();
|
|
||||||
|
|
||||||
$(dlgElement).find('input[temporary=true]').remove();
|
|
||||||
|
|
||||||
return {
|
|
||||||
show: function () {
|
|
||||||
modal.open();
|
|
||||||
},
|
|
||||||
close: function () {
|
|
||||||
modal.close();
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Replaces a message listener
|
|
||||||
* @param selector
|
|
||||||
* @param message
|
|
||||||
* @param callback
|
|
||||||
*/
|
|
||||||
on: function (selector, message, callback) {
|
|
||||||
const found = dlgElement.find(selector);
|
|
||||||
found.off(message);
|
|
||||||
found.on(message, callback);
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param selector {string}
|
|
||||||
* @returns {element} The HTML element
|
|
||||||
*/
|
|
||||||
get: function (selector) {
|
|
||||||
const found = dlgElement.find(selector);
|
|
||||||
return found[0];
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param selector {string}
|
|
||||||
* @returns {jquery element}
|
|
||||||
*/
|
|
||||||
jget: function (selector) {
|
|
||||||
const found = dlgElement.find(selector);
|
|
||||||
return found;
|
|
||||||
},
|
|
||||||
setHiddenField: function (name, value, temporary = true) {
|
|
||||||
const form = $(dlgElement).find('form');
|
|
||||||
form.append(`<input type='hidden' temporary='${temporary}' name='${name}' value='${value}' />`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// For getting/setting values
|
|
||||||
// var $modal = modal.$modal;
|
|
||||||
}
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
/*
|
|
||||||
* The MIT License (MIT)
|
|
||||||
*
|
|
||||||
* Copyright (c) 2018 TwelveTone LLC
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
* of this software and associated documentation files (the "Software"), to deal
|
|
||||||
* in the Software without restriction, including without limitation the rights
|
|
||||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
* copies of the Software, and to permit persons to whom the Software is
|
|
||||||
* furnished to do so, subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
* SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
//TODO refactor AJAX code
|
|
||||||
|
|
||||||
function onMediaAction_MediaReplace(actionId, mediaName, mediaElement) {
|
|
||||||
|
|
||||||
if (window._media_replace_isQuicksend) {
|
|
||||||
doQuicksend(actionId, mediaName, mediaElement);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const form = openModalDialog('MediaReplace');
|
|
||||||
|
|
||||||
form.on('.button[name=cancel]', 'click', () => {
|
|
||||||
form.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
form.on('.button[name=continue]', 'click', () => {
|
|
||||||
form.close();
|
|
||||||
|
|
||||||
const input = form.get('input[type=file]');
|
|
||||||
if (input.files.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let file = input.files[0];
|
|
||||||
let data = new FormData();
|
|
||||||
data.append('mediaupload', file, file.name);
|
|
||||||
|
|
||||||
let xhr = new XMLHttpRequest();
|
|
||||||
xhr.open("POST", GravAdmin.config.base_url_relative + '/admin-media-replace/replace', true);
|
|
||||||
// xhr.setRequestHeader("X_FILENAME", file.name);
|
|
||||||
// Grav is stripping out X_ from $_SERVER
|
|
||||||
xhr.setRequestHeader("X-MEDIA-NEW-FILENAME", file.name);
|
|
||||||
xhr.setRequestHeader("X-MEDIA-ROUTE", '/' + GravAdmin.config.route);
|
|
||||||
xhr.setRequestHeader("X-MEDIA-FILENAME", mediaName);
|
|
||||||
|
|
||||||
data.append("media-new-filename", file.name);
|
|
||||||
data.append("media-route", '/' + GravAdmin.config.route);
|
|
||||||
data.append("media-filename", mediaName);
|
|
||||||
data.append("media-rename", form.jget('input[name=rename_file]:checked').val());
|
|
||||||
data.append("media-match-extension", form.jget('input[name=match_extension]:checked').val());
|
|
||||||
data.append("media-require-image", form.jget('input[name=require_image]:checked').val());
|
|
||||||
|
|
||||||
xhr.onload = function () {
|
|
||||||
//get response and show the uploading status
|
|
||||||
if (xhr.status === 200) {
|
|
||||||
let response = JSON.parse(xhr.responseText);
|
|
||||||
if (response.error) {
|
|
||||||
alert(response.error);
|
|
||||||
}
|
|
||||||
else if (response.thumbnail) {
|
|
||||||
let img = mediaElement.querySelector('img');
|
|
||||||
if (!img) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
img.src = response.thumbnail + "?refresh=" + new Date().getTime();
|
|
||||||
|
|
||||||
if (response.toast) {
|
|
||||||
Grav.default.Utils.toastr.info(response.toast);
|
|
||||||
}
|
|
||||||
if (response.newName) {
|
|
||||||
const nameEle = $(mediaElement).find('[data-dz-name]');
|
|
||||||
nameEle.text(response.newName);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.send(data);
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function doQuicksend(actionId, mediaName, mediaElement) {
|
|
||||||
let form = document.querySelector('form[id="replace-media"]');
|
|
||||||
if (!form) {
|
|
||||||
form = $("<form id='replace-media' action='MediaReplace' method='post'>" +
|
|
||||||
"<input type='file' style='visibility: hidden; position: absolute; top: 0px; left: 0px; height: 0px; width: 0px;'></input>" +
|
|
||||||
"</form>")[0];
|
|
||||||
document.body.appendChild(form);
|
|
||||||
|
|
||||||
let input = form.querySelector('input[type=file]');
|
|
||||||
|
|
||||||
input.addEventListener('change', function () {
|
|
||||||
if (input.files.length !== 1) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let file = input.files[0];
|
|
||||||
let data = new FormData();
|
|
||||||
data.append('mediaupload', file, file.name);
|
|
||||||
|
|
||||||
let xhr = new XMLHttpRequest();
|
|
||||||
xhr.open("POST", GravAdmin.config.base_url_relative + '/admin-media-replace/replace', true);
|
|
||||||
// xhr.setRequestHeader("X_FILENAME", file.name);
|
|
||||||
// Grav is stripping out X_ from $_SERVER
|
|
||||||
xhr.setRequestHeader("X-MEDIA-NEW-FILENAME", file.name);
|
|
||||||
xhr.setRequestHeader("X-MEDIA-ROUTE", '/' + GravAdmin.config.route);
|
|
||||||
xhr.setRequestHeader("X-MEDIA-FILENAME", mediaName);
|
|
||||||
|
|
||||||
data.append("media-new-filename", file.name);
|
|
||||||
data.append("media-route", '/' + GravAdmin.config.route);
|
|
||||||
data.append("media-filename", mediaName);
|
|
||||||
data.append("media-rename", "1");
|
|
||||||
data.append("media-match-extension", "1");
|
|
||||||
data.append("media-require-image", "1");
|
|
||||||
|
|
||||||
xhr.onload = function () {
|
|
||||||
//get response and show the uploading status
|
|
||||||
if (xhr.status === 200) {
|
|
||||||
let response = JSON.parse(xhr.responseText);
|
|
||||||
if (response.error) {
|
|
||||||
alert(response.error);
|
|
||||||
}
|
|
||||||
else if (response.thumbnail) {
|
|
||||||
let img = mediaElement.querySelector('img');
|
|
||||||
if (!img) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
img.src = response.thumbnail + "?refresh=" + new Date().getTime();
|
|
||||||
} else {
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
xhr.send(data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let input = form.querySelector('input[type=file]');
|
|
||||||
input.click();
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
#
|
|
||||||
# The MIT License (MIT)
|
|
||||||
#
|
|
||||||
# Copyright (c) 2018 TwelveTone LLC
|
|
||||||
#
|
|
||||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
# of this software and associated documentation files (the "Software"), to deal
|
|
||||||
# in the Software without restriction, including without limitation the rights
|
|
||||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
# copies of the Software, and to permit persons to whom the Software is
|
|
||||||
# furnished to do so, subject to the following conditions:
|
|
||||||
#
|
|
||||||
# The above copyright notice and this permission notice shall be included in all
|
|
||||||
# copies or substantial portions of the Software.
|
|
||||||
#
|
|
||||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
# SOFTWARE.
|
|
||||||
#
|
|
||||||
|
|
||||||
name: Admin Media Replace
|
|
||||||
version: 1.0.4
|
|
||||||
description: Replaces media in the page media bin.
|
|
||||||
icon: plug
|
|
||||||
author:
|
|
||||||
name: TwelveTone LLC
|
|
||||||
email: info@twelvetone.tv
|
|
||||||
homepage: https://www.twelvetone.tv/docs/developer-tools/grav-plugins/grav-admin-media-replace-plugin
|
|
||||||
keywords: grav, plugin, admin, media
|
|
||||||
bugs: https://www.twelvetone.tv/docs/developer-tools/grav-plugins/grav-admin-media-replace-plugin
|
|
||||||
docs: https://www.twelvetone.tv/docs/developer-tools/grav-plugins/grav-admin-media-replace-plugin
|
|
||||||
license: MIT
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
- { name: grav, version: '>=1.0.0' }
|
|
||||||
- { name: admin, version: '>=1.0.0' }
|
|
||||||
- { name: admin-media-actions, version: '>=1.0.2' }
|
|
||||||
|
|
||||||
form:
|
|
||||||
validation: strict
|
|
||||||
fields:
|
|
||||||
enabled:
|
|
||||||
type: toggle
|
|
||||||
label: Plugin status
|
|
||||||
highlight: 1
|
|
||||||
default: 0
|
|
||||||
options:
|
|
||||||
1: Enabled
|
|
||||||
0: Disabled
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
|
|
||||||
quicksend:
|
|
||||||
type: toggle
|
|
||||||
label: Quick send
|
|
||||||
description: Bypasses the upload dialog.
|
|
||||||
highlight: 1
|
|
||||||
default: 1
|
|
||||||
options:
|
|
||||||
1: Enabled
|
|
||||||
0: Disabled
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
form:
|
|
||||||
fields:
|
|
||||||
|
|
||||||
foobar:
|
|
||||||
type: section
|
|
||||||
title: Replace Media
|
|
||||||
|
|
||||||
rename_file:
|
|
||||||
type: toggle
|
|
||||||
label: Rename file
|
|
||||||
description: Renames the uploaded file to the target file name. This includes the basename and extension.
|
|
||||||
highlight: 1
|
|
||||||
default: 1
|
|
||||||
options:
|
|
||||||
1: Yes
|
|
||||||
0: No
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
|
|
||||||
require_image:
|
|
||||||
type: toggle
|
|
||||||
label: Require image
|
|
||||||
description: Require upload to be an image MIME type.
|
|
||||||
highlight: 0
|
|
||||||
default: 0
|
|
||||||
options:
|
|
||||||
1: Yes
|
|
||||||
0: No
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
|
|
||||||
match_extension:
|
|
||||||
type: toggle
|
|
||||||
label: Match Extension
|
|
||||||
description: Require new file extension to match current file extension.
|
|
||||||
highlight: 0
|
|
||||||
default: 0
|
|
||||||
options:
|
|
||||||
1: Yes
|
|
||||||
0: No
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
|
|
||||||
file:
|
|
||||||
type: singlefile
|
|
||||||
label: File
|
|
||||||
description: You can also drop a file on the <em>choose file button</em>.
|
|
||||||
|
|
||||||
spacer:
|
|
||||||
type: spacer
|
|
||||||
text: To bypass this dialog and simply <em>pick and send</em> the file using default values, go to the plugin settings and enable <em>quicksend</em>.
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
plugins {
|
|
||||||
id("com.github.hierynomus.license").version("0.14.0")
|
|
||||||
}
|
|
||||||
apply plugin:'java'
|
|
||||||
|
|
||||||
sourceSets {
|
|
||||||
grav {
|
|
||||||
resources {
|
|
||||||
srcDirs += "."
|
|
||||||
include "**/*.yaml"
|
|
||||||
include "**/*.php"
|
|
||||||
include "**/*.css"
|
|
||||||
include "**/*.js"
|
|
||||||
include "**/*.twig"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
// Do not call until Grav 'twig' is initialized and the base twig extensions are loaded.
|
|
||||||
function addModalForm($jsid, $twigName)
|
|
||||||
{
|
|
||||||
//TODO parametize
|
|
||||||
$bpNewPage = new \Grav\Common\Data\Blueprint("media-replace.yaml");
|
|
||||||
$bpNewPage->setContext(__DIR__ . "/../blueprints");
|
|
||||||
$bpNewPage = $bpNewPage->load()->init();
|
|
||||||
|
|
||||||
$params = [];
|
|
||||||
$params["remodalId"] = $jsid;
|
|
||||||
$params["fields"] = $bpNewPage->toArray()['form']['fields'];
|
|
||||||
|
|
||||||
$grav = \Grav\Common\Grav::instance();
|
|
||||||
$rendered = $grav['twig']->twig()->render($twigName, $params);
|
|
||||||
$arr = [
|
|
||||||
'MODAL' => $rendered
|
|
||||||
];
|
|
||||||
|
|
||||||
$grav['assets']->addInlineJs("var $jsid = " . json_encode($arr) . ';', -1000, false);
|
|
||||||
|
|
||||||
$modalReg = "
|
|
||||||
$(function () {
|
|
||||||
$('body').append($jsid.MODAL);
|
|
||||||
});";
|
|
||||||
$grav['assets']->addInlineJs($modalReg, -999, false);
|
|
||||||
|
|
||||||
}
|
|
||||||
-9
@@ -1,9 +0,0 @@
|
|||||||
{% extends "forms/field.html.twig" %}
|
|
||||||
|
|
||||||
{% block label %}
|
|
||||||
{{ label ?: "File" }}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block input %}
|
|
||||||
<input name="{{ name }}" type="file" style="width: 100%"/>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<div class="remodal" data-remodal-id="{{remodalId}}" data-remodal-options="hashTracking: false">
|
|
||||||
<form method="post" onsubmit='return false;'>
|
|
||||||
{% for name, field in fields %}
|
|
||||||
{% if field.type %}
|
|
||||||
{% set value = data.value(name) %}
|
|
||||||
<div class="block block-{{field.type}}">
|
|
||||||
{% include ["forms/fields/#{field.type}/#{field.type}.html.twig", 'forms/fields/text/text.html.twig'] %}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
<div class="form-field grid">{{message}}</div>
|
|
||||||
|
|
||||||
<div class="button-bar">
|
|
||||||
<!--<div class="loading">-->
|
|
||||||
<!--{{ "Moving" }}... <i class="fa fa-spinner fa-spin"></i>-->
|
|
||||||
<!--</div>-->
|
|
||||||
<button class="button" style="visibility: visible" name="cancel">{{ "Cancel" }}</button>
|
|
||||||
<button class="button primary" style="visibility: visible" name="continue">{{ "Continue" }}</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
-8
@@ -1,8 +0,0 @@
|
|||||||
# These are supported funding model platforms
|
|
||||||
|
|
||||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
|
||||||
patreon: # Replace with a single Patreon username
|
|
||||||
open_collective: grav
|
|
||||||
ko_fi: # Replace with a single Ko-fi username
|
|
||||||
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
|
|
||||||
custom: # Replace with a single custom sponsorship URL
|
|
||||||
-6
@@ -1,6 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "github-actions"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1 +0,0 @@
|
|||||||
Please read the <a href="https://github.com/getgrav/grav/blob/develop/CONTRIBUTING.md" target="_blank">Contributing Guidelines of the Grav Project</a>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2017 Grav
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
# Grav Standard Administration Panel Plugin
|
|
||||||
|
|
||||||
This **admin plugin** for [Grav](https://github.com/getgrav/grav) is an HTML user interface that provides a convenient way to configure Grav and easily create and modify pages. This will remain a totally optional plugin, and is not in any way required or needed to use Grav effectively. In fact, the admin provides an intentionally limited view to ensure it remains easy to use and not overwhelming. I'm sure power users will still prefer to work with the configuration files directly.
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
# Features
|
|
||||||
|
|
||||||
* User login with automatic password encryption
|
|
||||||
* Forgot password functionality
|
|
||||||
* Logged-in-user management
|
|
||||||
* One click Grav core updates
|
|
||||||
* Dashboard with maintenance status, site activity and latest page updates
|
|
||||||
* Notifications system for latest news, blogs, and announcements
|
|
||||||
* Ajax-powered backup capability
|
|
||||||
* Ajax-powered clear-cache capability
|
|
||||||
* System configuration management
|
|
||||||
* Site configuration management
|
|
||||||
* Normal and Expert modes which allow editing via forms or YAML
|
|
||||||
* Page listing with filtering and search
|
|
||||||
* Page creation, editing, moving, copying, and deleting
|
|
||||||
* Powerful syntax highlighting code editor with instant Grav-powered preview
|
|
||||||
* Editor features, hot keys, toolbar, and distraction-free fullscreen mode
|
|
||||||
* Drag-n-drop upload of page media files including drag-n-drop placement in the editor
|
|
||||||
* One click theme and plugin updates
|
|
||||||
* Plugin manager that allows listing and configuration of installed plugins
|
|
||||||
* Theme manager that allows listing and configuration of installed themes
|
|
||||||
* GPM-powered installation of new plugins and themes
|
|
||||||
|
|
||||||
# Support
|
|
||||||
|
|
||||||
#### Support
|
|
||||||
|
|
||||||
We have tested internally, but we hope to use this public beta phase to identify, isolate, and fix issues related to the plugin to ensure it is as solid and reliable as possible.
|
|
||||||
|
|
||||||
For **live chatting**, please use the dedicated [Discord Chat Room](https://getgrav.org/discord) for discussions directly related to Grav.
|
|
||||||
|
|
||||||
For **bugs, features, improvements**, please ensure you [create issues in the admin plugin GitHub repository](https://github.com/getgrav/grav-plugin-admin).
|
|
||||||
|
|
||||||
# Installation
|
|
||||||
|
|
||||||
First ensure you are running the latest **Grav 1.6.7 or later**. This is required for the admin plugin to run properly (`-f` forces a refresh of the GPM index).
|
|
||||||
|
|
||||||
```
|
|
||||||
$ bin/gpm selfupgrade -f
|
|
||||||
```
|
|
||||||
|
|
||||||
The admin plugin actually requires the help of 3 other plugins, so to get the admin plugin to work you first need to install **admin**, **login**, **forms**, and **email** plugins. These are available via GPM, and because the plugin has dependencies you just need to proceed and install the admin plugin, and agree when prompted to install the others:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ bin/gpm install admin
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manual Installation
|
|
||||||
|
|
||||||
Manual installation is not the recommended method of installation, however, it is still possible to install the admin plugin manually. Basically, you need to download each of the following plugins individually:
|
|
||||||
|
|
||||||
* [admin](https://github.com/getgrav/grav-plugin-admin/archive/develop.zip)
|
|
||||||
* [login](https://github.com/getgrav/grav-plugin-login/archive/develop.zip)
|
|
||||||
* [form](https://github.com/getgrav/grav-plugin-form/archive/develop.zip)
|
|
||||||
* [email](https://github.com/getgrav/grav-plugin-email/archive/develop.zip)
|
|
||||||
|
|
||||||
Extract each archive file into your `user/plugins` folder, then ensure the folders are renamed to just `admin/`, `login/`, `form/`, and `email/`. Then proceed with the **Usage instructions below**.
|
|
||||||
|
|
||||||
# Usage
|
|
||||||
|
|
||||||
### Create User with CLI
|
|
||||||
|
|
||||||
After this you need to create a user account with admin privileges:
|
|
||||||
|
|
||||||
```
|
|
||||||
$ bin/plugin login new-user
|
|
||||||
```
|
|
||||||
|
|
||||||
### Create User Manually
|
|
||||||
|
|
||||||
Alternatively, you can create a user account manually, in a file called `user/accounts/admin.yaml`. This **filename** is actually the **username** that you will use to login. The contents will contain the other information for the user.
|
|
||||||
|
|
||||||
```
|
|
||||||
password: 'password'
|
|
||||||
email: 'youremail@mail.com'
|
|
||||||
fullname: 'Johnny Appleseed'
|
|
||||||
title: 'Site Administrator'
|
|
||||||
access:
|
|
||||||
admin:
|
|
||||||
login: true
|
|
||||||
super: true
|
|
||||||
```
|
|
||||||
|
|
||||||
Of course you should edit your `email`, `password`, `fullname`, and `title` to suit your needs.
|
|
||||||
|
|
||||||
> You can use any password when you manually put it in this `.yaml` file. However, when you change your password in the admin, it must contain at least one number and one uppercase and lowercase letter, and at least 8 or more characters.
|
|
||||||
|
|
||||||
# Accessing the Admin
|
|
||||||
|
|
||||||
By default, you can access the admin by pointing your browser to `http://yoursite.com/admin`. You can simply log in with the `username` and `password` set in the YAML file you configured earlier.
|
|
||||||
|
|
||||||
> After logging in, your **plaintext password** will be removed and replaced by an **encrypted** one.
|
|
||||||
|
|
||||||
# Standard Free & Paid Pro Versions
|
|
||||||
|
|
||||||
If you have been following the [blog](https://getgrav.org/blog), [Twitter](https://twitter.com/getgrav), [Discord chat](https://getgrav.org/discord), etc., you probably already know now that our intention is to provide two versions of this plugin.
|
|
||||||
|
|
||||||
The **standard free version**, is very powerful, and has more functionality than most commercial flat-file CMS systems.
|
|
||||||
|
|
||||||
We also intend to release in the near future a more feature-rich **pro version** that will include enhanced functionality, as well as some additional nice-to-have capabilities. This pro version will be a **paid** plugin the price of which is not yet 100% finalized.
|
|
||||||
|
|
||||||
# Admin Events
|
|
||||||
|
|
||||||
## General events
|
|
||||||
|
|
||||||
- onAdminRegisterPermissions - (admin)
|
|
||||||
- onAdminThemeInitialized
|
|
||||||
- onAdminPage - (page)
|
|
||||||
- onAdminMenu
|
|
||||||
- onAdminTwigTemplatePaths - (paths)
|
|
||||||
|
|
||||||
## Page specific events
|
|
||||||
|
|
||||||
- onAdminDashboard
|
|
||||||
- onAdminTools - (tools)
|
|
||||||
- onAdminLogFiles - (logs)
|
|
||||||
- onAdminGenerateReports - (reports)
|
|
||||||
|
|
||||||
## Tasks
|
|
||||||
|
|
||||||
- onAdminControllerInit - (controller)
|
|
||||||
- onAdminTaskExecute - (controller, method)
|
|
||||||
|
|
||||||
## Editing
|
|
||||||
|
|
||||||
- onAdminData
|
|
||||||
- onAdminSave - (object)
|
|
||||||
- onAdminAfterSave - (object)
|
|
||||||
|
|
||||||
## Pages
|
|
||||||
|
|
||||||
- onAdminPageTypes - (types)
|
|
||||||
- onAdminModularPageTypes
|
|
||||||
- onAdminSave - (page)
|
|
||||||
- onAdminAfterSaveAs - (path)
|
|
||||||
- onAdminAfterSave - (page)
|
|
||||||
- onAdminAfterDelete - (page)
|
|
||||||
- onAdminAfterAddMedia - (page)
|
|
||||||
- onAdminAfterDelMedia - (page)
|
|
||||||
- onAdminCreatePageFrontmatter - (header, data)
|
|
||||||
|
|
||||||
|
|
||||||
# Running Tests
|
|
||||||
|
|
||||||
First install the dev dependencies by running `composer update` from the Grav root.
|
|
||||||
Then `composer test` will run the Unit Tests, which should be always executed successfully on any site.
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# Upgrading to Admin 1.10
|
|
||||||
|
|
||||||
Twig:
|
|
||||||
|
|
||||||
* **Admin link**: When linking to another admin page, use `{{ admin_route('/config/site') }}` instead of any other method, such as `{{ base_url_relative }}/config/site` (fixes multi-language issues)
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,84 +0,0 @@
|
|||||||
enabled: true
|
|
||||||
route: '/admin'
|
|
||||||
cache_enabled: true
|
|
||||||
theme: grav
|
|
||||||
logo_text: ''
|
|
||||||
body_classes: ''
|
|
||||||
content_padding: true
|
|
||||||
twofa_enabled: true
|
|
||||||
sidebar:
|
|
||||||
activate: tab
|
|
||||||
hover_delay: 100
|
|
||||||
size: auto
|
|
||||||
dashboard:
|
|
||||||
days_of_stats: 7
|
|
||||||
widgets_display:
|
|
||||||
dashboard-maintenance: true
|
|
||||||
dashboard-statistics: true
|
|
||||||
dashboard-notifications: true
|
|
||||||
dashboard-feed: true
|
|
||||||
dashboard-pages: true
|
|
||||||
pages:
|
|
||||||
show_parents: both
|
|
||||||
show_modular: true
|
|
||||||
session:
|
|
||||||
timeout: 1800
|
|
||||||
edit_mode: normal
|
|
||||||
frontend_preview_target: inline
|
|
||||||
show_github_msg: true
|
|
||||||
admin_icons: line-awesome
|
|
||||||
enable_auto_updates_check: false
|
|
||||||
notifications:
|
|
||||||
feed: true
|
|
||||||
dashboard: true
|
|
||||||
plugins: true
|
|
||||||
themes: true
|
|
||||||
popularity:
|
|
||||||
enabled: true
|
|
||||||
ignore: ['/test*','/modular']
|
|
||||||
history:
|
|
||||||
daily: 30
|
|
||||||
monthly: 12
|
|
||||||
visitors: 20
|
|
||||||
whitelabel:
|
|
||||||
quicktray_recompile: false
|
|
||||||
codemirror_theme: paper
|
|
||||||
codemirror_fontsize: md
|
|
||||||
codemirror_md_font: sans
|
|
||||||
logo_custom:
|
|
||||||
logo_login:
|
|
||||||
color_scheme:
|
|
||||||
accents:
|
|
||||||
primary-accent: button
|
|
||||||
secondary-accent: notice
|
|
||||||
tertiary-accent: critical
|
|
||||||
colors:
|
|
||||||
logo-bg: '#323640'
|
|
||||||
logo-link: '#FFFFFF'
|
|
||||||
nav-bg: '#3D424E'
|
|
||||||
nav-text: '#B7B9BD'
|
|
||||||
nav-link: '#ffffff'
|
|
||||||
nav-selected-bg: '#323640'
|
|
||||||
nav-selected-link: '#ffffff'
|
|
||||||
nav-hover-bg: '#434753'
|
|
||||||
nav-hover-link: '#ffffff'
|
|
||||||
toolbar-bg: '#ffffff'
|
|
||||||
toolbar-text: '#3D424E'
|
|
||||||
page-bg: '#F6F6F6'
|
|
||||||
page-text: '#6f7b8a'
|
|
||||||
page-link: '#0090D9'
|
|
||||||
content-bg: '#ffffff'
|
|
||||||
content-text: '#6f7b8a'
|
|
||||||
content-link: '#0090D9'
|
|
||||||
content-link2: '#da4b46'
|
|
||||||
content-header: '#414147'
|
|
||||||
content-tabs-bg: '#e6e6e6'
|
|
||||||
content-tabs-text: '#808080'
|
|
||||||
button-bg: '#0090D9'
|
|
||||||
button-text: '#ffffff'
|
|
||||||
notice-bg: '#06A599'
|
|
||||||
notice-text: '#ffffff'
|
|
||||||
update-bg: '#77559D'
|
|
||||||
update-text: '#ffffff'
|
|
||||||
critical-bg: '#F45857'
|
|
||||||
critical-text: '#ffffff'
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 236 KiB |
@@ -1,781 +0,0 @@
|
|||||||
name: Admin Panel
|
|
||||||
slug: admin
|
|
||||||
type: plugin
|
|
||||||
version: 1.10.49.4
|
|
||||||
description: Adds an advanced administration panel to manage your site
|
|
||||||
icon: empire
|
|
||||||
author:
|
|
||||||
name: Team Grav
|
|
||||||
email: devs@getgrav.org
|
|
||||||
url: https://getgrav.org
|
|
||||||
homepage: https://github.com/getgrav/grav-plugin-admin
|
|
||||||
keywords: admin, plugin, manager, panel
|
|
||||||
bugs: https://github.com/getgrav/grav-plugin-admin/issues
|
|
||||||
docs: https://github.com/getgrav/grav-plugin-admin/blob/develop/README.md
|
|
||||||
license: MIT
|
|
||||||
|
|
||||||
dependencies:
|
|
||||||
- { name: grav, version: '>=1.7.49' }
|
|
||||||
- { name: form, version: '>=6.0.1' }
|
|
||||||
- { name: login, version: '>=3.7.8' }
|
|
||||||
- { name: email, version: '>=3.1.6' }
|
|
||||||
- { name: flex-objects, version: '>=1.2.0' }
|
|
||||||
|
|
||||||
form:
|
|
||||||
validation: loose
|
|
||||||
fields:
|
|
||||||
admin_tabs:
|
|
||||||
type: tabs
|
|
||||||
fields:
|
|
||||||
config_tab:
|
|
||||||
type: tab
|
|
||||||
title: PLUGIN_ADMIN.CONFIGURATION
|
|
||||||
|
|
||||||
fields:
|
|
||||||
|
|
||||||
Basics:
|
|
||||||
type: section
|
|
||||||
title: PLUGIN_ADMIN.BASICS
|
|
||||||
underline: false
|
|
||||||
|
|
||||||
enabled:
|
|
||||||
type: hidden
|
|
||||||
label: PLUGIN_ADMIN.PLUGIN_STATUS
|
|
||||||
highlight: 1
|
|
||||||
default: 0
|
|
||||||
options:
|
|
||||||
1: PLUGIN_ADMIN.ENABLED
|
|
||||||
0: PLUGIN_ADMIN.DISABLED
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
|
|
||||||
cache_enabled:
|
|
||||||
type: toggle
|
|
||||||
label: PLUGIN_ADMIN.ADMIN_CACHING
|
|
||||||
help: PLUGIN_ADMIN.ADMIN_CACHING_HELP
|
|
||||||
highlight: 1
|
|
||||||
options:
|
|
||||||
1: PLUGIN_ADMIN.YES
|
|
||||||
0: PLUGIN_ADMIN.NO
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
|
|
||||||
twofa_enabled:
|
|
||||||
type: toggle
|
|
||||||
label: PLUGIN_LOGIN.2FA_TITLE
|
|
||||||
help: PLUGIN_LOGIN.2FA_ENABLED_HELP
|
|
||||||
default: 1
|
|
||||||
highlight: 1
|
|
||||||
options:
|
|
||||||
1: PLUGIN_ADMIN.YES
|
|
||||||
0: PLUGIN_ADMIN.NO
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
|
|
||||||
route:
|
|
||||||
type: text
|
|
||||||
label: PLUGIN_ADMIN.ADMIN_PATH
|
|
||||||
size: medium
|
|
||||||
placeholder: ADMIN_PATH_PLACEHOLDER
|
|
||||||
help: ADMIN_PATH_HELP
|
|
||||||
|
|
||||||
logo_text:
|
|
||||||
type: text
|
|
||||||
label: PLUGIN_ADMIN.LOGO_TEXT
|
|
||||||
size: medium
|
|
||||||
placeholder: "Grav"
|
|
||||||
help: PLUGIN_ADMIN.LOGO_TEXT_HELP
|
|
||||||
|
|
||||||
content_padding:
|
|
||||||
type: toggle
|
|
||||||
label: PLUGIN_ADMIN.CONTENT_PADDING
|
|
||||||
help: PLUGIN_ADMIN.CONTENT_PADDING_HELP
|
|
||||||
highlight: 1
|
|
||||||
options:
|
|
||||||
1: PLUGIN_ADMIN.YES
|
|
||||||
0: PLUGIN_ADMIN.NO
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
|
|
||||||
body_classes:
|
|
||||||
type: text
|
|
||||||
label: PLUGIN_ADMIN.BODY_CLASSES
|
|
||||||
size: medium
|
|
||||||
help: PLUGIN_ADMIN.BODY_CLASSES_HELP
|
|
||||||
|
|
||||||
sidebar.activate:
|
|
||||||
type: select
|
|
||||||
label: PLUGIN_ADMIN.SIDEBAR_ACTIVATION
|
|
||||||
help: PLUGIN_ADMIN.SIDEBAR_ACTIVATION_HELP
|
|
||||||
size: small
|
|
||||||
default: tab
|
|
||||||
options:
|
|
||||||
tab: PLUGIN_ADMIN.SIDEBAR_ACTIVATION_TAB
|
|
||||||
hover: PLUGIN_ADMIN.SIDEBAR_ACTIVATION_HOVER
|
|
||||||
|
|
||||||
sidebar.hover_delay:
|
|
||||||
type: text
|
|
||||||
size: x-small
|
|
||||||
append: PLUGIN_ADMIN.SIDEBAR_HOVER_DELAY_APPEND
|
|
||||||
label: PLUGIN_ADMIN.SIDEBAR_HOVER_DELAY
|
|
||||||
default: 500
|
|
||||||
validate:
|
|
||||||
type: number
|
|
||||||
min: 1
|
|
||||||
|
|
||||||
|
|
||||||
sidebar.size:
|
|
||||||
type: select
|
|
||||||
label: PLUGIN_ADMIN.SIDEBAR_SIZE
|
|
||||||
help: PLUGIN_ADMIN.SIDEBAR_SIZE_HELP
|
|
||||||
size: medium
|
|
||||||
default: auto
|
|
||||||
options:
|
|
||||||
auto: PLUGIN_ADMIN.SIDEBAR_SIZE_AUTO
|
|
||||||
small: PLUGIN_ADMIN.SIDEBAR_SIZE_SMALL
|
|
||||||
|
|
||||||
theme:
|
|
||||||
type: hidden
|
|
||||||
label: PLUGIN_ADMIN.THEME
|
|
||||||
default: grav
|
|
||||||
|
|
||||||
edit_mode:
|
|
||||||
type: select
|
|
||||||
label: PLUGIN_ADMIN.EDIT_MODE
|
|
||||||
size: small
|
|
||||||
default: normal
|
|
||||||
options:
|
|
||||||
normal: PLUGIN_ADMIN.NORMAL
|
|
||||||
expert: PLUGIN_ADMIN.EXPERT
|
|
||||||
help: PLUGIN_ADMIN.EDIT_MODE_HELP
|
|
||||||
|
|
||||||
frontend_preview_target:
|
|
||||||
type: select
|
|
||||||
label: PLUGIN_ADMIN.FRONTEND_PREVIEW_TARGET
|
|
||||||
size: medium
|
|
||||||
default: inline
|
|
||||||
options:
|
|
||||||
inline: PLUGIN_ADMIN.FRONTEND_PREVIEW_TARGET_INLINE
|
|
||||||
_blank: PLUGIN_ADMIN.FRONTEND_PREVIEW_TARGET_NEW
|
|
||||||
_self: PLUGIN_ADMIN.FRONTEND_PREVIEW_TARGET_CURRENT
|
|
||||||
|
|
||||||
pages.show_parents:
|
|
||||||
type: select
|
|
||||||
size: medium
|
|
||||||
label: PLUGIN_ADMIN.PARENT_DROPDOWN
|
|
||||||
highlight: 1
|
|
||||||
options:
|
|
||||||
both: PLUGIN_ADMIN.PARENT_DROPDOWN_BOTH
|
|
||||||
folder: PLUGIN_ADMIN.PARENT_DROPDOWN_FOLDER
|
|
||||||
fullpath: PLUGIN_ADMIN.PARENT_DROPDOWN_FULLPATH
|
|
||||||
|
|
||||||
pages.parents_levels:
|
|
||||||
type: text
|
|
||||||
label: PLUGIN_ADMIN.PARENTS_LEVELS
|
|
||||||
size: small
|
|
||||||
help: PLUGIN_ADMIN.PARENTS_LEVELS_HELP
|
|
||||||
|
|
||||||
pages.show_modular:
|
|
||||||
type: toggle
|
|
||||||
label: PLUGIN_ADMIN.MODULAR_PARENTS
|
|
||||||
highlight: 1
|
|
||||||
default: 1
|
|
||||||
options:
|
|
||||||
1: PLUGIN_ADMIN.ENABLED
|
|
||||||
0: PLUGIN_ADMIN.DISABLED
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
help: PLUGIN_ADMIN.MODULAR_PARENTS_HELP
|
|
||||||
|
|
||||||
show_beta_msg:
|
|
||||||
type: hidden
|
|
||||||
|
|
||||||
show_github_msg:
|
|
||||||
type: toggle
|
|
||||||
label: PLUGIN_ADMIN.SHOW_GITHUB_LINK
|
|
||||||
highlight: 1
|
|
||||||
default: 1
|
|
||||||
options:
|
|
||||||
1: PLUGIN_ADMIN.ENABLED
|
|
||||||
0: PLUGIN_ADMIN.DISABLED
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
help: PLUGIN_ADMIN.SHOW_GITHUB_LINK_HELP
|
|
||||||
|
|
||||||
enable_auto_updates_check:
|
|
||||||
type: toggle
|
|
||||||
label: PLUGIN_ADMIN.AUTO_UPDATES
|
|
||||||
highlight: 1
|
|
||||||
default: 1
|
|
||||||
options:
|
|
||||||
1: PLUGIN_ADMIN.ENABLED
|
|
||||||
0: PLUGIN_ADMIN.DISABLED
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
help: PLUGIN_ADMIN.AUTO_UPDATES_HELP
|
|
||||||
|
|
||||||
session.timeout:
|
|
||||||
type: text
|
|
||||||
size: small
|
|
||||||
label: PLUGIN_ADMIN.TIMEOUT
|
|
||||||
append: GRAV.NICETIME.SECOND_PLURAL
|
|
||||||
help: PLUGIN_ADMIN.TIMEOUT_HELP
|
|
||||||
validate:
|
|
||||||
type: number
|
|
||||||
min: 1
|
|
||||||
|
|
||||||
hide_page_types:
|
|
||||||
type: select
|
|
||||||
size: large
|
|
||||||
label: PLUGIN_ADMIN.HIDE_PAGE_TYPES
|
|
||||||
classes: fancy
|
|
||||||
multiple: true
|
|
||||||
array: true
|
|
||||||
selectize:
|
|
||||||
create: true
|
|
||||||
data-options@: ['\Grav\Plugin\AdminPlugin::pagesTypes', true]
|
|
||||||
|
|
||||||
hide_modular_page_types:
|
|
||||||
type: select
|
|
||||||
size: large
|
|
||||||
label: PLUGIN_ADMIN.HIDE_MODULAR_PAGE_TYPES
|
|
||||||
classes: fancy
|
|
||||||
multiple: true
|
|
||||||
array: true
|
|
||||||
selectize:
|
|
||||||
create: true
|
|
||||||
data-options@: ['\Grav\Plugin\AdminPlugin::pagesModularTypes', true]
|
|
||||||
|
|
||||||
Dashboard:
|
|
||||||
type: section
|
|
||||||
title: PLUGIN_ADMIN.DASHBOARD
|
|
||||||
underline: true
|
|
||||||
|
|
||||||
widgets_display:
|
|
||||||
type: widgets
|
|
||||||
label: PLUGIN_ADMIN.WIDGETS_DISPLAY
|
|
||||||
validate:
|
|
||||||
type: array
|
|
||||||
|
|
||||||
Notifications:
|
|
||||||
type: section
|
|
||||||
title: PLUGIN_ADMIN.NOTIFICATIONS
|
|
||||||
underline: true
|
|
||||||
|
|
||||||
notifications.feed:
|
|
||||||
type: toggle
|
|
||||||
label: PLUGIN_ADMIN.FEED_NOTIFICATIONS
|
|
||||||
highlight: 1
|
|
||||||
default: 1
|
|
||||||
options:
|
|
||||||
1: PLUGIN_ADMIN.ENABLED
|
|
||||||
0: PLUGIN_ADMIN.DISABLED
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
help: PLUGIN_ADMIN.FEED_NOTIFICATIONS_HELP
|
|
||||||
|
|
||||||
notifications.dashboard:
|
|
||||||
type: toggle
|
|
||||||
label: PLUGIN_ADMIN.DASHBOARD_NOTIFICATIONS
|
|
||||||
highlight: 1
|
|
||||||
default: 1
|
|
||||||
options:
|
|
||||||
1: PLUGIN_ADMIN.ENABLED
|
|
||||||
0: PLUGIN_ADMIN.DISABLED
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
help: PLUGIN_ADMIN.DASHBOARD_NOTIFICATIONS_HELP
|
|
||||||
|
|
||||||
notifications.plugins:
|
|
||||||
type: toggle
|
|
||||||
label: PLUGIN_ADMIN.PLUGINS_NOTIFICATIONS
|
|
||||||
highlight: 1
|
|
||||||
default: 1
|
|
||||||
options:
|
|
||||||
1: PLUGIN_ADMIN.ENABLED
|
|
||||||
0: PLUGIN_ADMIN.DISABLED
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
help: PLUGIN_ADMIN.PLUGINS_NOTIFICATIONS_HELP
|
|
||||||
|
|
||||||
notifications.themes:
|
|
||||||
type: toggle
|
|
||||||
label: PLUGIN_ADMIN.THEMES_NOTIFICATIONS
|
|
||||||
highlight: 1
|
|
||||||
default: 1
|
|
||||||
options:
|
|
||||||
1: PLUGIN_ADMIN.ENABLED
|
|
||||||
0: PLUGIN_ADMIN.DISABLED
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
help: PLUGIN_ADMIN.THEMES_NOTIFICATIONS_HELP
|
|
||||||
|
|
||||||
customization_tab:
|
|
||||||
type: tab
|
|
||||||
title: PLUGIN_ADMIN.CUSTOMIZATION
|
|
||||||
|
|
||||||
fields:
|
|
||||||
whitelabel.logos:
|
|
||||||
type: section
|
|
||||||
underline: true
|
|
||||||
title: PLUGIN_ADMIN.LOGOS
|
|
||||||
|
|
||||||
whitelabel.logo_login:
|
|
||||||
type: file
|
|
||||||
label: PLUGIN_ADMIN.LOGIN_SCREEN_CUSTOM_LOGO_LABEL
|
|
||||||
destination: 'user://assets'
|
|
||||||
accept:
|
|
||||||
- image/*
|
|
||||||
|
|
||||||
whitelabel.logo_custom:
|
|
||||||
type: file
|
|
||||||
label: PLUGIN_ADMIN.TOP_LEFT_CUSTOM_LOGO_LABEL
|
|
||||||
destination: 'user://assets'
|
|
||||||
accept:
|
|
||||||
- image/*
|
|
||||||
|
|
||||||
codemirror_section:
|
|
||||||
type: section
|
|
||||||
underline: true
|
|
||||||
title: PLUGIN_ADMIN.CODEMIRROR
|
|
||||||
|
|
||||||
whitelabel.codemirror_theme:
|
|
||||||
type: select
|
|
||||||
label: PLUGIN_ADMIN.CODEMIRROR_THEME
|
|
||||||
default: paper
|
|
||||||
markdown: true
|
|
||||||
data-options@: '\Grav\Plugin\AdminPlugin::themeOptions'
|
|
||||||
description: PLUGIN_ADMIN.CODEMIRROR_THEME_DESC
|
|
||||||
|
|
||||||
whitelabel.codemirror_fontsize:
|
|
||||||
type: select
|
|
||||||
label: PLUGIN_ADMIN.CODEMIRROR_FONTSIZE
|
|
||||||
default: md
|
|
||||||
options:
|
|
||||||
sm: PLUGIN_ADMIN.CODEMIRROR_FONTSIZE_SM
|
|
||||||
md: PLUGIN_ADMIN.CODEMIRROR_FONTSIZE_MD
|
|
||||||
lg: PLUGIN_ADMIN.CODEMIRROR_FONTSIZE_LG
|
|
||||||
|
|
||||||
whitelabel.codemirror_md_font:
|
|
||||||
type: select
|
|
||||||
label: PLUGIN_ADMIN.CODEMIRROR_MD_FONT
|
|
||||||
default: sans
|
|
||||||
options:
|
|
||||||
sans: PLUGIN_ADMIN.CODEMIRROR_MD_FONT_SANS
|
|
||||||
mono: PLUGIN_ADMIN.CODEMIRROR_MD_FONT_MONO
|
|
||||||
|
|
||||||
customization_section:
|
|
||||||
type: section
|
|
||||||
underline: true
|
|
||||||
title: PLUGIN_ADMIN.CUSTOMIZATION
|
|
||||||
|
|
||||||
whitelabel.quicktray_recompile:
|
|
||||||
type: toggle
|
|
||||||
label: PLUGIN_ADMIN.QUICKTRAY_RECOMPILE
|
|
||||||
help: PLUGIN_ADMIN.QUICKTRAY_RECOMPILE_HELP
|
|
||||||
highlight: 0
|
|
||||||
default: 0
|
|
||||||
options:
|
|
||||||
1: PLUGIN_ADMIN.ENABLED
|
|
||||||
0: PLUGIN_ADMIN.DISABLED
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
whitelabel.color_scheme.name:
|
|
||||||
type: text
|
|
||||||
label: PLUGIN_ADMIN.COLOR_SCHEME_NAME
|
|
||||||
help: PLUGIN_ADMIN.COLOR_SCHEME_NAME_HELP
|
|
||||||
placeholder: PLUGIN_ADMIN.COLOR_SCHEME_NAME_PLACEHOLDER
|
|
||||||
|
|
||||||
themes-preview:
|
|
||||||
type: themepreview
|
|
||||||
ignore: true;
|
|
||||||
label: PLUGIN_ADMIN.PRESETS
|
|
||||||
style: vertical
|
|
||||||
|
|
||||||
colorschemes:
|
|
||||||
type: colorscheme
|
|
||||||
label: PLUGIN_ADMIN.COLOR_SCHEME_LABEL
|
|
||||||
style: vertical
|
|
||||||
help: PLUGIN_ADMIN.COLOR_SCHEME_HELP
|
|
||||||
|
|
||||||
fields:
|
|
||||||
whitelabel.color_scheme.colors.logo-bg:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#1e333e'
|
|
||||||
help: PLUGIN_ADMIN.LOGO_BG_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.logo-link:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#ffffff'
|
|
||||||
help: PLUGIN_ADMIN.LOGO_LINK_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.nav-bg:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#253a47'
|
|
||||||
help: PLUGIN_ADMIN.NAV_BG_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.nav-text:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#afc7d5'
|
|
||||||
help: PLUGIN_ADMIN.NAV_TEXT_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.nav-link:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#d1dee7'
|
|
||||||
help: PLUGIN_ADMIN.NAV_LINK_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.nav-selected-bg:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#2d4d5b'
|
|
||||||
help: PLUGIN_ADMIN.NAV_SELECTED_BG_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.nav-selected-link:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#ffffff'
|
|
||||||
help: PLUGIN_ADMIN.NAV_SELECTED_LINK_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.nav-hover-bg:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#1e333e'
|
|
||||||
help: PLUGIN_ADMIN.NAV_HOVER_BG_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.nav-hover-link:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#ffffff'
|
|
||||||
help: PLUGIN_ADMIN.NAV_HOVER_LINK_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.toolbar-bg:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#349886'
|
|
||||||
help: PLUGIN_ADMIN.TOOLBAR_BG_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.toolbar-text:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#ffffff'
|
|
||||||
help: PLUGIN_ADMIN.TOOLBAR_TEXT_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.page-bg:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#314d5b'
|
|
||||||
help: PLUGIN_ADMIN.PAGE_BG_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.page-text:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#81a5b5'
|
|
||||||
help: PLUGIN_ADMIN.PAGE_TEXT_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.page-link:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#aad9ed'
|
|
||||||
help: PLUGIN_ADMIN.PAGE_LINK_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.content-bg:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#eeeeee'
|
|
||||||
help: PLUGIN_ADMIN.CONTENT_BG_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.content-text:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#737c81'
|
|
||||||
help: PLUGIN_ADMIN.CONTENT_TEXT_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.content-link:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#0082ba'
|
|
||||||
help: PLUGIN_ADMIN.CONTENT_LINK_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.content-link2:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#da4b46'
|
|
||||||
help: PLUGIN_ADMIN.CONTENT_LINK2_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.content-header:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#314d5b'
|
|
||||||
help: PLUGIN_ADMIN.CONTENT_HEADER_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.content-tabs-bg:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#223a47'
|
|
||||||
help: PLUGIN_ADMIN.CONTENT_TABS_BG_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.content-tabs-text:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#d1dee7'
|
|
||||||
help: PLUGIN_ADMIN.CONTENT_TABS_TEXT_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.content-highlight:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#ffffd7'
|
|
||||||
help: PLUGIN_ADMIN.CONTENT_HIGHLIGHT_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.button-bg:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#41bea8'
|
|
||||||
help: PLUGIN_ADMIN.BUTTON_BG_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.button-text:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#ffffff'
|
|
||||||
help: PLUGIN_ADMIN.BUTTON_TEXT_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.notice-bg:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#00a6cf'
|
|
||||||
help: PLUGIN_ADMIN.NOTICE_BG_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.notice-text:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#ffffff'
|
|
||||||
help: PLUGIN_ADMIN.NOTICE_TEXT_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.update-bg:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#8f5aad'
|
|
||||||
help: PLUGIN_ADMIN.UPDATES_BG_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.update-text:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#ffffff'
|
|
||||||
help: PLUGIN_ADMIN.UPDATES_TEXT_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.critical-bg:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#da4b46'
|
|
||||||
help: PLUGIN_ADMIN.CRITICAL_BG_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.colors.critical-text:
|
|
||||||
type: colorscheme.color
|
|
||||||
default: '#ffffff'
|
|
||||||
help: PLUGIN_ADMIN.CRITICAL_TEXT_HELP
|
|
||||||
|
|
||||||
whitelabel.color_scheme.accents.primary-accent:
|
|
||||||
type: select
|
|
||||||
size: medium
|
|
||||||
classes: fancy
|
|
||||||
label: PLUGIN_ADMIN.PRIMARY_ACCENT_LABEL
|
|
||||||
help: PLUGIN_ADMIN.PRIMARY_ACCENT_HELP
|
|
||||||
options:
|
|
||||||
button: PLUGIN_ADMIN.BUTTON_COLORS
|
|
||||||
content: PLUGIN_ADMIN.CONTENT_COLORS
|
|
||||||
tabs: PLUGIN_ADMIN.TABS_COLORS
|
|
||||||
critical: PLUGIN_ADMIN.CRITICAL_COLORS
|
|
||||||
logo: PLUGIN_ADMIN.LOGO_COLORS
|
|
||||||
nav: PLUGIN_ADMIN.NAV_COLORS
|
|
||||||
notice: PLUGIN_ADMIN.NOTICE_COLORS
|
|
||||||
page: PLUGIN_ADMIN.PAGE_COLORS
|
|
||||||
toolbar: PLUGIN_ADMIN.TOOLBAR_COLORS
|
|
||||||
update: PLUGIN_ADMIN.UPDATE_COLORS
|
|
||||||
|
|
||||||
whitelabel.color_scheme.accents.secondary-accent:
|
|
||||||
type: select
|
|
||||||
size: medium
|
|
||||||
classes: fancy
|
|
||||||
label: PLUGIN_ADMIN.SECONDARY_ACCENT_LABEL
|
|
||||||
help: PLUGIN_ADMIN.SECONDARY_ACCENT_HELP
|
|
||||||
options:
|
|
||||||
button: PLUGIN_ADMIN.BUTTON_COLORS
|
|
||||||
content: PLUGIN_ADMIN.CONTENT_COLORS
|
|
||||||
tabs: PLUGIN_ADMIN.TABS_COLORS
|
|
||||||
critical: PLUGIN_ADMIN.CRITICAL_COLORS
|
|
||||||
logo: PLUGIN_ADMIN.LOGO_COLORS
|
|
||||||
nav: PLUGIN_ADMIN.NAV_COLORS
|
|
||||||
notice: PLUGIN_ADMIN.NOTICE_COLORS
|
|
||||||
page: PLUGIN_ADMIN.PAGE_COLORS
|
|
||||||
toolbar: PLUGIN_ADMIN.TOOLBAR_COLORS
|
|
||||||
update: PLUGIN_ADMIN.UPDATE_COLORS
|
|
||||||
|
|
||||||
whitelabel.color_scheme.accents.tertiary-accent:
|
|
||||||
type: select
|
|
||||||
size: medium
|
|
||||||
classes: fancy
|
|
||||||
label: PLUGIN_ADMIN.TERTIARY_ACCENT_LABEL
|
|
||||||
help: PLUGIN_ADMIN.TERTIARY_ACCENT_HELP
|
|
||||||
options:
|
|
||||||
button: PLUGIN_ADMIN.BUTTON_COLORS
|
|
||||||
content: PLUGIN_ADMIN.CONTENT_COLORS
|
|
||||||
tabs: PLUGIN_ADMIN.TABS_COLORS
|
|
||||||
critical: PLUGIN_ADMIN.CRITICAL_COLORS
|
|
||||||
logo: PLUGIN_ADMIN.LOGO_COLORS
|
|
||||||
nav: PLUGIN_ADMIN.NAV_COLORS
|
|
||||||
notice: PLUGIN_ADMIN.NOTICE_COLORS
|
|
||||||
page: PLUGIN_ADMIN.PAGE_COLORS
|
|
||||||
toolbar: PLUGIN_ADMIN.TOOLBAR_COLORS
|
|
||||||
update: PLUGIN_ADMIN.UPDATE_COLORS
|
|
||||||
|
|
||||||
whitelabel.custom_footer:
|
|
||||||
type: textarea
|
|
||||||
rows: 2
|
|
||||||
label: PLUGIN_ADMIN.CUSTOM_FOOTER
|
|
||||||
help: PLUGIN_ADMIN.CUSTOM_FOOTER_HELP
|
|
||||||
placeholder: PLUGIN_ADMIN.CUSTOM_FOOTER_PLACEHOLDER
|
|
||||||
|
|
||||||
|
|
||||||
whitelabel.custom_css:
|
|
||||||
label: PLUGIN_ADMIN.CUSTOM_CSS_LABEL
|
|
||||||
placeholder: PLUGIN_ADMIN.CUSTOM_CSS_PLACEHOLDER
|
|
||||||
help: PLUGIN_ADMIN.CUSTOM_CSS_HELP
|
|
||||||
type: editor
|
|
||||||
codemirror:
|
|
||||||
mode: 'css'
|
|
||||||
indentUnit: 2
|
|
||||||
autofocus: true
|
|
||||||
indentWithTabs: true
|
|
||||||
lineNumbers: true
|
|
||||||
styleActiveLine: true
|
|
||||||
|
|
||||||
whitelabel.custom_presets:
|
|
||||||
label: PLUGIN_ADMIN.CUSTOM_PRESETS
|
|
||||||
help: PLUGIN_ADMIN.CUSTOM_PRESETS_HELP
|
|
||||||
placeholder: PLUGIN_ADMIN.CUSTOM_PRESETS_PLACEHOLDER
|
|
||||||
type: editor
|
|
||||||
codemirror:
|
|
||||||
mode: 'yaml'
|
|
||||||
indentUnit: 2
|
|
||||||
autofocus: true
|
|
||||||
indentWithTabs: false
|
|
||||||
lineNumbers: true
|
|
||||||
styleActiveLine: true
|
|
||||||
gutters: ['CodeMirror-lint-markers']
|
|
||||||
lint: true
|
|
||||||
|
|
||||||
extras_tab:
|
|
||||||
type: tab
|
|
||||||
title: PLUGIN_ADMIN.EXTRAS
|
|
||||||
|
|
||||||
fields:
|
|
||||||
|
|
||||||
Popularity:
|
|
||||||
type: section
|
|
||||||
title: PLUGIN_ADMIN.POPULARITY
|
|
||||||
underline: true
|
|
||||||
|
|
||||||
popularity.enabled:
|
|
||||||
type: toggle
|
|
||||||
label: PLUGIN_ADMIN.VISITOR_TRACKING
|
|
||||||
highlight: 1
|
|
||||||
default: 1
|
|
||||||
options:
|
|
||||||
1: PLUGIN_ADMIN.ENABLED
|
|
||||||
0: PLUGIN_ADMIN.DISABLED
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
help: PLUGIN_ADMIN.VISITOR_TRACKING_HELP
|
|
||||||
|
|
||||||
dashboard.days_of_stats:
|
|
||||||
type: text
|
|
||||||
label: PLUGIN_ADMIN.DAYS_OF_STATS
|
|
||||||
append: days
|
|
||||||
size: x-small
|
|
||||||
default: 7
|
|
||||||
help: PLUGIN_ADMIN.DAYS_OF_STATS_HELP
|
|
||||||
validate:
|
|
||||||
type: int
|
|
||||||
|
|
||||||
popularity.ignore:
|
|
||||||
type: array
|
|
||||||
label: PLUGIN_ADMIN.IGNORE_URLS
|
|
||||||
size: large
|
|
||||||
help: PLUGIN_ADMIN.IGNORE_URLS_HELP
|
|
||||||
default: ['/test*','/modular']
|
|
||||||
value_only: true
|
|
||||||
placeholder_value: /ignore-this-route
|
|
||||||
|
|
||||||
popularity.history.daily:
|
|
||||||
type: hidden
|
|
||||||
label: PLUGIN_ADMIN.DAILY_HISTORY
|
|
||||||
default: 30
|
|
||||||
|
|
||||||
popularity.history.monthly:
|
|
||||||
type: hidden
|
|
||||||
label: PLUGIN_ADMIN.MONTHLY_HISTORY
|
|
||||||
default: 12
|
|
||||||
|
|
||||||
popularity.history.visitors:
|
|
||||||
type: hidden
|
|
||||||
label: PLUGIN_ADMIN.VISITORS_HISTORY
|
|
||||||
default: 20
|
|
||||||
|
|
||||||
MediaResize:
|
|
||||||
type: section
|
|
||||||
title: PLUGIN_ADMIN.MEDIA_RESIZE
|
|
||||||
underline: true
|
|
||||||
|
|
||||||
MediaResizeNote:
|
|
||||||
type: spacer
|
|
||||||
text: PLUGIN_ADMIN.PAGEMEDIA_RESIZER
|
|
||||||
markdown: true
|
|
||||||
|
|
||||||
pagemedia.resize_width:
|
|
||||||
type: number
|
|
||||||
size: x-small
|
|
||||||
append: PLUGIN_ADMIN.PIXELS
|
|
||||||
label: PLUGIN_ADMIN.RESIZE_WIDTH
|
|
||||||
default: 0
|
|
||||||
validate:
|
|
||||||
type: number
|
|
||||||
help: PLUGIN_ADMIN.RESIZE_WIDTH_HELP
|
|
||||||
|
|
||||||
pagemedia.resize_height:
|
|
||||||
type: number
|
|
||||||
size: x-small
|
|
||||||
append: PLUGIN_ADMIN.PIXELS
|
|
||||||
label: PLUGIN_ADMIN.RESIZE_HEIGHT
|
|
||||||
default: 0
|
|
||||||
validate:
|
|
||||||
type: number
|
|
||||||
help: PLUGIN_ADMIN.RESIZE_HEIGHT_HELP
|
|
||||||
|
|
||||||
pagemedia.res_min_width:
|
|
||||||
type: number
|
|
||||||
size: x-small
|
|
||||||
append: PLUGIN_ADMIN.PIXELS
|
|
||||||
label: PLUGIN_ADMIN.RES_MIN_WIDTH
|
|
||||||
default: 0
|
|
||||||
validate:
|
|
||||||
type: number
|
|
||||||
help: PLUGIN_ADMIN.RES_MIN_WIDTH_HELP
|
|
||||||
|
|
||||||
pagemedia.res_min_height:
|
|
||||||
type: number
|
|
||||||
size: x-small
|
|
||||||
append: PLUGIN_ADMIN.PIXELS
|
|
||||||
label: PLUGIN_ADMIN.RES_MIN_HEIGHT
|
|
||||||
default: 0
|
|
||||||
validate:
|
|
||||||
type: number
|
|
||||||
help: PLUGIN_ADMIN.RES_MIN_HEIGHT_HELP
|
|
||||||
|
|
||||||
pagemedia.res_max_width:
|
|
||||||
type: number
|
|
||||||
size: x-small
|
|
||||||
append: PLUGIN_ADMIN.PIXELS
|
|
||||||
label: PLUGIN_ADMIN.RES_MAX_WIDTH
|
|
||||||
default: 0
|
|
||||||
validate:
|
|
||||||
type: number
|
|
||||||
help: PLUGIN_ADMIN.RES_MAX_WIDTH_HELP
|
|
||||||
|
|
||||||
pagemedia.res_max_height:
|
|
||||||
type: number
|
|
||||||
size: x-small
|
|
||||||
append: PLUGIN_ADMIN.PIXELS
|
|
||||||
label: PLUGIN_ADMIN.RES_MAX_HEIGHT
|
|
||||||
default: 0
|
|
||||||
validate:
|
|
||||||
type: number
|
|
||||||
help: PLUGIN_ADMIN.RES_MAX_HEIGHT_HELP
|
|
||||||
|
|
||||||
pagemedia.resize_quality:
|
|
||||||
type: number
|
|
||||||
size: x-small
|
|
||||||
append: 0...1
|
|
||||||
label: PLUGIN_ADMIN.RESIZE_QUALITY
|
|
||||||
default: 0.8
|
|
||||||
validate:
|
|
||||||
type: number
|
|
||||||
step: 0.01
|
|
||||||
help: PLUGIN_ADMIN.RESIZE_QUALITY_HELP
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
rules:
|
|
||||||
slug:
|
|
||||||
pattern: '[a-zA-Zа-яA-Я0-9_\-]+'
|
|
||||||
min: 1
|
|
||||||
max: 200
|
|
||||||
|
|
||||||
form:
|
|
||||||
validation: loose
|
|
||||||
fields:
|
|
||||||
|
|
||||||
section:
|
|
||||||
type: section
|
|
||||||
title: PLUGIN_ADMIN.COPY_PAGE
|
|
||||||
|
|
||||||
title:
|
|
||||||
type: text
|
|
||||||
label: PLUGIN_ADMIN.PAGE_TITLE
|
|
||||||
help: PLUGIN_ADMIN.PAGE_TITLE_HELP
|
|
||||||
validate:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
folder:
|
|
||||||
type: text
|
|
||||||
label: PLUGIN_ADMIN.FOLDER_NAME
|
|
||||||
help: PLUGIN_ADMIN.FOLDER_NAME_HELP
|
|
||||||
validate:
|
|
||||||
rule: slug
|
|
||||||
required: true
|
|
||||||
|
|
||||||
header.published:
|
|
||||||
id: move-header-published
|
|
||||||
type: toggle
|
|
||||||
label: PLUGIN_ADMIN.PUBLISHED
|
|
||||||
help: PLUGIN_ADMIN.PUBLISHED_HELP
|
|
||||||
highlight: ''
|
|
||||||
default: ''
|
|
||||||
size: medium
|
|
||||||
options:
|
|
||||||
'': PLUGIN_ADMIN.AUTO
|
|
||||||
1: PLUGIN_ADMIN.YES
|
|
||||||
0: PLUGIN_ADMIN.NO
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
rules:
|
|
||||||
slug:
|
|
||||||
pattern: '[a-zA-Zа-яA-Я0-9_\-]+'
|
|
||||||
min: 1
|
|
||||||
max: 200
|
|
||||||
|
|
||||||
form:
|
|
||||||
validation: loose
|
|
||||||
fields:
|
|
||||||
|
|
||||||
section:
|
|
||||||
type: section
|
|
||||||
title: PLUGIN_ADMIN.ADD_MODULE_CONTENT
|
|
||||||
|
|
||||||
title:
|
|
||||||
type: text
|
|
||||||
label: PLUGIN_ADMIN.PAGE_TITLE
|
|
||||||
validate:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
folder:
|
|
||||||
type: text
|
|
||||||
label: PLUGIN_ADMIN.FOLDER_NAME
|
|
||||||
validate:
|
|
||||||
rule: slug
|
|
||||||
required: true
|
|
||||||
|
|
||||||
route:
|
|
||||||
type: parents
|
|
||||||
label: PLUGIN_ADMIN.PAGE
|
|
||||||
classes: fancy
|
|
||||||
validate:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
name:
|
|
||||||
type: select
|
|
||||||
classes: fancy
|
|
||||||
label: PLUGIN_ADMIN.MODULE_TEMPLATE
|
|
||||||
help: PLUGIN_ADMIN.PAGE_FILE_HELP
|
|
||||||
default: default
|
|
||||||
data-options@: '\Grav\Plugin\AdminPlugin::pagesModularTypes'
|
|
||||||
validate:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
modular:
|
|
||||||
type: hidden
|
|
||||||
default: 1
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
|
|
||||||
blueprint:
|
|
||||||
type: blueprint
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
rules:
|
|
||||||
slug:
|
|
||||||
pattern: '[a-zA-Zа-яA-Я0-9_\-]+'
|
|
||||||
min: 1
|
|
||||||
max: 200
|
|
||||||
|
|
||||||
form:
|
|
||||||
validation: loose
|
|
||||||
fields:
|
|
||||||
|
|
||||||
tabs:
|
|
||||||
type: tabs
|
|
||||||
active: 1
|
|
||||||
|
|
||||||
fields:
|
|
||||||
content:
|
|
||||||
type: tab
|
|
||||||
title: PLUGIN_ADMIN.CONTENT
|
|
||||||
|
|
||||||
fields:
|
|
||||||
xss_check:
|
|
||||||
type: xss
|
|
||||||
|
|
||||||
frontmatter:
|
|
||||||
classes: frontmatter
|
|
||||||
type: editor
|
|
||||||
label: PLUGIN_ADMIN.FRONTMATTER
|
|
||||||
autofocus: true
|
|
||||||
codemirror:
|
|
||||||
mode: 'yaml'
|
|
||||||
indentUnit: 4
|
|
||||||
autofocus: true
|
|
||||||
indentWithTabs: false
|
|
||||||
lineNumbers: true
|
|
||||||
styleActiveLine: true
|
|
||||||
gutters: ['CodeMirror-lint-markers']
|
|
||||||
lint: true
|
|
||||||
|
|
||||||
content:
|
|
||||||
type: markdown
|
|
||||||
|
|
||||||
header.media_order:
|
|
||||||
type: pagemedia
|
|
||||||
label: PLUGIN_ADMIN.PAGE_MEDIA
|
|
||||||
|
|
||||||
options:
|
|
||||||
type: tab
|
|
||||||
title: PLUGIN_ADMIN.OPTIONS
|
|
||||||
|
|
||||||
fields:
|
|
||||||
|
|
||||||
columns:
|
|
||||||
type: columns
|
|
||||||
|
|
||||||
fields:
|
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
|
|
||||||
fields:
|
|
||||||
|
|
||||||
ordering:
|
|
||||||
type: toggle
|
|
||||||
label: PLUGIN_ADMIN.FOLDER_NUMERIC_PREFIX
|
|
||||||
help: PLUGIN_ADMIN.FOLDER_NUMERIC_PREFIX_HELP
|
|
||||||
highlight: 1
|
|
||||||
options:
|
|
||||||
1: PLUGIN_ADMIN.ENABLED
|
|
||||||
0: PLUGIN_ADMIN.DISABLED
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
|
|
||||||
folder:
|
|
||||||
type: text
|
|
||||||
label: PLUGIN_ADMIN.FILENAME
|
|
||||||
validate:
|
|
||||||
rule: slug
|
|
||||||
required: true
|
|
||||||
|
|
||||||
route:
|
|
||||||
type: parents
|
|
||||||
label: PLUGIN_ADMIN.PARENT
|
|
||||||
classes: fancy
|
|
||||||
validate:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
name:
|
|
||||||
type: select
|
|
||||||
classes: fancy
|
|
||||||
label: PLUGIN_ADMIN.MODULE_TEMPLATE
|
|
||||||
default: default
|
|
||||||
data-options@: '\Grav\Plugin\AdminPlugin::pagesModularTypes'
|
|
||||||
validate:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
|
|
||||||
fields:
|
|
||||||
order:
|
|
||||||
type: order
|
|
||||||
label: PLUGIN_ADMIN.ORDERING
|
|
||||||
|
|
||||||
blueprint:
|
|
||||||
type: blueprint
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
form:
|
|
||||||
validation: loose
|
|
||||||
fields:
|
|
||||||
route:
|
|
||||||
type: hidden
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
rules:
|
|
||||||
slug:
|
|
||||||
pattern: '[a-zA-Zа-яA-Я0-9_\-]+'
|
|
||||||
min: 1
|
|
||||||
max: 200
|
|
||||||
|
|
||||||
form:
|
|
||||||
validation: loose
|
|
||||||
fields:
|
|
||||||
|
|
||||||
section:
|
|
||||||
type: section
|
|
||||||
title: PLUGIN_ADMIN.ADD_PAGE
|
|
||||||
|
|
||||||
title:
|
|
||||||
type: text
|
|
||||||
label: PLUGIN_ADMIN.PAGE_TITLE
|
|
||||||
help: PLUGIN_ADMIN.PAGE_TITLE_HELP
|
|
||||||
validate:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
folder:
|
|
||||||
type: text
|
|
||||||
label: PLUGIN_ADMIN.FOLDER_NAME
|
|
||||||
help: PLUGIN_ADMIN.FOLDER_NAME_HELP
|
|
||||||
validate:
|
|
||||||
rule: slug
|
|
||||||
required: true
|
|
||||||
|
|
||||||
route:
|
|
||||||
type: parents
|
|
||||||
label: PLUGIN_ADMIN.PARENT_PAGE
|
|
||||||
classes: fancy
|
|
||||||
validate:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
name:
|
|
||||||
type: select
|
|
||||||
classes: fancy
|
|
||||||
label: PLUGIN_ADMIN.PAGE_FILE
|
|
||||||
help: PLUGIN_ADMIN.PAGE_FILE_HELP
|
|
||||||
data-options@: '\Grav\Plugin\AdminPlugin::pagesTypes'
|
|
||||||
data-default@: '\Grav\Plugin\Admin\Admin::getLastPageName'
|
|
||||||
validate:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
visible:
|
|
||||||
type: toggle
|
|
||||||
label: PLUGIN_ADMIN.VISIBLE
|
|
||||||
help: PLUGIN_ADMIN.VISIBLE_HELP
|
|
||||||
highlight: ''
|
|
||||||
default: ''
|
|
||||||
options:
|
|
||||||
'': PLUGIN_ADMIN.AUTO
|
|
||||||
1: PLUGIN_ADMIN.YES
|
|
||||||
0: PLUGIN_ADMIN.NO
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
required: true
|
|
||||||
|
|
||||||
blueprint:
|
|
||||||
type: blueprint
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
rules:
|
|
||||||
slug:
|
|
||||||
pattern: '[a-zA-Zа-яA-Я0-9_\-]+'
|
|
||||||
min: 1
|
|
||||||
max: 200
|
|
||||||
|
|
||||||
form:
|
|
||||||
validation: loose
|
|
||||||
fields:
|
|
||||||
|
|
||||||
section:
|
|
||||||
type: section
|
|
||||||
title: PLUGIN_ADMIN.ADD_FOLDER
|
|
||||||
|
|
||||||
folder:
|
|
||||||
type: text
|
|
||||||
label: PLUGIN_ADMIN.FOLDER_NAME
|
|
||||||
help: PLUGIN_ADMIN.FOLDER_NAME_HELP
|
|
||||||
validate:
|
|
||||||
rule: slug
|
|
||||||
required: true
|
|
||||||
|
|
||||||
route:
|
|
||||||
type: parents
|
|
||||||
label: PLUGIN_ADMIN.PARENT_PAGE
|
|
||||||
classes: fancy
|
|
||||||
validate:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
blueprint:
|
|
||||||
type: blueprint
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
rules:
|
|
||||||
slug:
|
|
||||||
pattern: '[a-zA-Zа-яA-Я0-9_\-]+'
|
|
||||||
min: 1
|
|
||||||
max: 200
|
|
||||||
|
|
||||||
form:
|
|
||||||
validation: loose
|
|
||||||
fields:
|
|
||||||
|
|
||||||
tabs:
|
|
||||||
type: tabs
|
|
||||||
active: 1
|
|
||||||
|
|
||||||
fields:
|
|
||||||
content:
|
|
||||||
type: tab
|
|
||||||
title: PLUGIN_ADMIN.CONTENT
|
|
||||||
|
|
||||||
fields:
|
|
||||||
xss_check:
|
|
||||||
type: xss
|
|
||||||
|
|
||||||
frontmatter:
|
|
||||||
classes: frontmatter
|
|
||||||
type: editor
|
|
||||||
label: PLUGIN_ADMIN.FRONTMATTER
|
|
||||||
autofocus: true
|
|
||||||
codemirror:
|
|
||||||
mode: 'yaml'
|
|
||||||
indentUnit: 4
|
|
||||||
autofocus: true
|
|
||||||
indentWithTabs: false
|
|
||||||
lineNumbers: true
|
|
||||||
styleActiveLine: true
|
|
||||||
gutters: ['CodeMirror-lint-markers']
|
|
||||||
lint: true
|
|
||||||
|
|
||||||
content:
|
|
||||||
type: codemirror
|
|
||||||
|
|
||||||
header.media_order:
|
|
||||||
type: pagemedia
|
|
||||||
label: PLUGIN_ADMIN.PAGE_MEDIA
|
|
||||||
|
|
||||||
options:
|
|
||||||
type: tab
|
|
||||||
title: PLUGIN_ADMIN.OPTIONS
|
|
||||||
|
|
||||||
fields:
|
|
||||||
|
|
||||||
columns:
|
|
||||||
type: columns
|
|
||||||
|
|
||||||
fields:
|
|
||||||
column1:
|
|
||||||
type: column
|
|
||||||
|
|
||||||
fields:
|
|
||||||
|
|
||||||
ordering:
|
|
||||||
type: toggle
|
|
||||||
label: PLUGIN_ADMIN.FOLDER_NUMERIC_PREFIX
|
|
||||||
help: PLUGIN_ADMIN.FOLDER_NUMERIC_PREFIX_HELP
|
|
||||||
highlight: 1
|
|
||||||
options:
|
|
||||||
1: PLUGIN_ADMIN.ENABLED
|
|
||||||
0: PLUGIN_ADMIN.DISABLED
|
|
||||||
validate:
|
|
||||||
type: bool
|
|
||||||
|
|
||||||
folder:
|
|
||||||
type: text
|
|
||||||
label: PLUGIN_ADMIN.FOLDER_NAME
|
|
||||||
help: PLUGIN_ADMIN.FOLDER_NAME_HELP
|
|
||||||
validate:
|
|
||||||
rule: slug
|
|
||||||
required: true
|
|
||||||
|
|
||||||
route:
|
|
||||||
type: parents
|
|
||||||
label: PLUGIN_ADMIN.PARENT
|
|
||||||
classes: fancy
|
|
||||||
|
|
||||||
name:
|
|
||||||
type: select
|
|
||||||
classes: fancy
|
|
||||||
label: PLUGIN_ADMIN.DISPLAY_TEMPLATE
|
|
||||||
help: PLUGIN_ADMIN.DISPLAY_TEMPLATE_HELP
|
|
||||||
default: default
|
|
||||||
data-options@: '\Grav\Plugin\AdminPlugin::pagesTypes'
|
|
||||||
validate:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
column2:
|
|
||||||
type: column
|
|
||||||
|
|
||||||
fields:
|
|
||||||
order:
|
|
||||||
type: order
|
|
||||||
label: PLUGIN_ADMIN.ORDERING
|
|
||||||
|
|
||||||
blueprint:
|
|
||||||
type: blueprint
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
rules:
|
|
||||||
slug:
|
|
||||||
pattern: '[a-zA-Zа-яA-Я0-9_\-]+'
|
|
||||||
min: 1
|
|
||||||
max: 200
|
|
||||||
|
|
||||||
form:
|
|
||||||
validation: loose
|
|
||||||
fields:
|
|
||||||
|
|
||||||
tabs:
|
|
||||||
type: tabs
|
|
||||||
active: 1
|
|
||||||
|
|
||||||
fields:
|
|
||||||
content:
|
|
||||||
type: tab
|
|
||||||
title: PLUGIN_ADMIN.CONTENT
|
|
||||||
|
|
||||||
fields:
|
|
||||||
frontmatter:
|
|
||||||
classes: frontmatter
|
|
||||||
type: editor
|
|
||||||
label: PLUGIN_ADMIN.FRONTMATTER
|
|
||||||
autofocus: true
|
|
||||||
codemirror:
|
|
||||||
mode: 'yaml'
|
|
||||||
indentUnit: 4
|
|
||||||
autofocus: true
|
|
||||||
indentWithTabs: false
|
|
||||||
lineNumbers: true
|
|
||||||
styleActiveLine: true
|
|
||||||
gutters: ['CodeMirror-lint-markers']
|
|
||||||
lint: true
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
title: PLUGIN_ADMIN.MEDIA
|
|
||||||
form:
|
|
||||||
validation: loose
|
|
||||||
fields:
|
|
||||||
'types':
|
|
||||||
name: medias
|
|
||||||
type: list
|
|
||||||
label: PLUGIN_ADMIN.MEDIA_TYPES
|
|
||||||
style: vertical
|
|
||||||
key: extension
|
|
||||||
controls: both
|
|
||||||
collapsed: true
|
|
||||||
|
|
||||||
fields:
|
|
||||||
.extension:
|
|
||||||
type: key
|
|
||||||
label: PLUGIN_ADMIN.FILE_EXTENSION
|
|
||||||
.type:
|
|
||||||
type: text
|
|
||||||
label: PLUGIN_ADMIN.TYPE
|
|
||||||
.thumb:
|
|
||||||
type: text
|
|
||||||
label: PLUGIN_ADMIN.THUMB
|
|
||||||
.mime:
|
|
||||||
type: text
|
|
||||||
label: PLUGIN_ADMIN.MIME_TYPE
|
|
||||||
validate:
|
|
||||||
type: lower
|
|
||||||
.image:
|
|
||||||
type: textarea
|
|
||||||
yaml: true
|
|
||||||
label: PLUGIN_ADMIN.IMAGE_OPTIONS
|
|
||||||
validate:
|
|
||||||
type: yaml
|
|
||||||
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,182 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package Grav\Plugin\Admin
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
|
||||||
* @license MIT License; see LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Grav\Plugin\Admin;
|
|
||||||
|
|
||||||
use ArrayAccess;
|
|
||||||
use Exception;
|
|
||||||
use Grav\Common\Data\Blueprint;
|
|
||||||
use Grav\Common\Data\Data;
|
|
||||||
use Grav\Framework\Form\Interfaces\FormFlashInterface;
|
|
||||||
use Grav\Framework\Form\Interfaces\FormInterface;
|
|
||||||
use Grav\Framework\Form\Traits\FormTrait;
|
|
||||||
use InvalidArgumentException;
|
|
||||||
use JsonSerializable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class AdminForm
|
|
||||||
* @package Grav\Plugin\Admin
|
|
||||||
*/
|
|
||||||
class AdminForm implements FormInterface, JsonSerializable
|
|
||||||
{
|
|
||||||
use FormTrait;
|
|
||||||
|
|
||||||
/** @var string */
|
|
||||||
protected $nonce_name;
|
|
||||||
/** @var string */
|
|
||||||
protected $nonce_action;
|
|
||||||
/** @var callable */
|
|
||||||
protected $submitMethod;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AdminForm constructor.
|
|
||||||
*
|
|
||||||
* @param string $name
|
|
||||||
* @param array $options
|
|
||||||
*/
|
|
||||||
public function __construct(string $name, array $options)
|
|
||||||
{
|
|
||||||
$this->name = $name;
|
|
||||||
$this->nonce_name = $options['nonce_name'] ?? 'admin-nonce';
|
|
||||||
$this->nonce_action = $options['nonce_action'] ?? 'admin-form';
|
|
||||||
|
|
||||||
$this->setId($options['id'] ?? $this->getName());
|
|
||||||
$this->setUniqueId($options['unique_id'] ?? $this->getName());
|
|
||||||
$this->setBlueprint($options['blueprint']);
|
|
||||||
$this->setSubmitMethod($options['submit_method'] ?? null);
|
|
||||||
$this->setFlashLookupFolder('tmp://admin/forms/[SESSIONID]');
|
|
||||||
|
|
||||||
if (!empty($options['reset'])) {
|
|
||||||
$this->getFlash()->delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function initialize(): AdminForm
|
|
||||||
{
|
|
||||||
$this->messages = [];
|
|
||||||
$this->submitted = false;
|
|
||||||
$this->unsetFlash();
|
|
||||||
|
|
||||||
/** @var FormFlashInterface $flash */
|
|
||||||
$flash = $this->getFlash();
|
|
||||||
if ($flash->exists()) {
|
|
||||||
$data = $flash->getData();
|
|
||||||
if (null !== $data) {
|
|
||||||
$data = new Data($data, $this->getBlueprint());
|
|
||||||
$data->setKeepEmptyValues(true);
|
|
||||||
$data->setMissingValuesAsNull(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->data = $data;
|
|
||||||
$this->files = $flash->getFilesByFields(false);
|
|
||||||
} else {
|
|
||||||
$this->data = new Data([], $this->getBlueprint());
|
|
||||||
$this->files = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getNonceName(): string
|
|
||||||
{
|
|
||||||
return $this->nonce_name;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getNonceAction(): string
|
|
||||||
{
|
|
||||||
return $this->nonce_action;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getScope(): string
|
|
||||||
{
|
|
||||||
return 'data.';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Blueprint $blueprint
|
|
||||||
*/
|
|
||||||
public function setBlueprint(Blueprint $blueprint): void
|
|
||||||
{
|
|
||||||
if (null === $blueprint) {
|
|
||||||
throw new InvalidArgumentException('Blueprint is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->blueprint = $blueprint;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $field
|
|
||||||
* @param mixed $value
|
|
||||||
*/
|
|
||||||
public function setData(string $field, $value): void
|
|
||||||
{
|
|
||||||
$this->getData()->set($field, $value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Blueprint
|
|
||||||
*/
|
|
||||||
public function getBlueprint(): Blueprint
|
|
||||||
{
|
|
||||||
return $this->blueprint;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param callable|null $submitMethod
|
|
||||||
*/
|
|
||||||
public function setSubmitMethod(?callable $submitMethod): void
|
|
||||||
{
|
|
||||||
if (null === $submitMethod) {
|
|
||||||
throw new InvalidArgumentException('Submit method is required');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->submitMethod = $submitMethod;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array $data
|
|
||||||
* @param array $files
|
|
||||||
* @return void
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
protected function doSubmit(array $data, array $files): void
|
|
||||||
{
|
|
||||||
$method = $this->submitMethod;
|
|
||||||
$method($data, $files);
|
|
||||||
|
|
||||||
$this->reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter validated data.
|
|
||||||
*
|
|
||||||
* @param ArrayAccess|Data|null $data
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function filterData($data = null): void
|
|
||||||
{
|
|
||||||
if ($data instanceof Data) {
|
|
||||||
$data->filter(true, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package Grav\Plugin\Admin
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
|
||||||
* @license MIT License; see LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Grav\Plugin\Admin;
|
|
||||||
|
|
||||||
use Grav\Common\Grav;
|
|
||||||
use Grav\Common\Page\Interfaces\PageInterface;
|
|
||||||
use Grav\Common\Page\Page;
|
|
||||||
use Grav\Framework\Form\Interfaces\FormFactoryInterface;
|
|
||||||
use Grav\Framework\Form\Interfaces\FormInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class FlexFormFactory
|
|
||||||
* @package Grav\Plugin\FlexObjects
|
|
||||||
*/
|
|
||||||
class AdminFormFactory implements FormFactoryInterface
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* @param Page $page
|
|
||||||
* @param string $name
|
|
||||||
* @param array $form
|
|
||||||
* @return FormInterface|null
|
|
||||||
*/
|
|
||||||
public function createPageForm(Page $page, string $name, array $form): ?FormInterface
|
|
||||||
{
|
|
||||||
return $this->createFormForPage($page, $name, $form);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param PageInterface $page
|
|
||||||
* @param string $name
|
|
||||||
* @param array $form
|
|
||||||
* @return FormInterface|null
|
|
||||||
*/
|
|
||||||
public function createFormForPage(PageInterface $page, string $name, array $form): ?FormInterface
|
|
||||||
{
|
|
||||||
/** @var Admin|null $admin */
|
|
||||||
$admin = Grav::instance()['admin'] ?? null;
|
|
||||||
$object = $admin->form ?? null;
|
|
||||||
|
|
||||||
return $object && $object->getName() === $name ? $object : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,414 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package Grav\Plugin\Admin
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
|
||||||
* @license MIT License; see LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Grav\Plugin\Admin\Controllers;
|
|
||||||
|
|
||||||
use Grav\Common\Debugger;
|
|
||||||
use Grav\Common\Grav;
|
|
||||||
use Grav\Common\Inflector;
|
|
||||||
use Grav\Common\Language\Language;
|
|
||||||
use Grav\Common\Utils;
|
|
||||||
use Grav\Framework\Flex\Interfaces\FlexObjectInterface;
|
|
||||||
use Grav\Framework\Form\Interfaces\FormInterface;
|
|
||||||
use Grav\Framework\Psr7\Response;
|
|
||||||
use Grav\Framework\RequestHandler\Exception\NotFoundException;
|
|
||||||
use Grav\Framework\RequestHandler\Exception\PageExpiredException;
|
|
||||||
use Grav\Framework\RequestHandler\Exception\RequestException;
|
|
||||||
use Grav\Framework\Route\Route;
|
|
||||||
use Grav\Framework\Session\SessionInterface;
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
|
||||||
use RocketTheme\Toolbox\Event\Event;
|
|
||||||
use RocketTheme\Toolbox\Session\Message;
|
|
||||||
|
|
||||||
abstract class AbstractController implements RequestHandlerInterface
|
|
||||||
{
|
|
||||||
/** @var string */
|
|
||||||
protected $nonce_action = 'admin-form';
|
|
||||||
|
|
||||||
/** @var string */
|
|
||||||
protected $nonce_name = 'admin-nonce';
|
|
||||||
|
|
||||||
/** @var ServerRequestInterface */
|
|
||||||
protected $request;
|
|
||||||
|
|
||||||
/** @var Grav */
|
|
||||||
protected $grav;
|
|
||||||
|
|
||||||
/** @var string */
|
|
||||||
protected $type;
|
|
||||||
|
|
||||||
/** @var string */
|
|
||||||
protected $key;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle request.
|
|
||||||
*
|
|
||||||
* Fires event: admin.[directory].[task|action].[command]
|
|
||||||
*
|
|
||||||
* @param ServerRequestInterface $request
|
|
||||||
* @return Response
|
|
||||||
*/
|
|
||||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
|
||||||
{
|
|
||||||
$attributes = $request->getAttributes();
|
|
||||||
$this->request = $request;
|
|
||||||
$this->grav = $attributes['grav'] ?? Grav::instance();
|
|
||||||
$this->type = $attributes['type'] ?? null;
|
|
||||||
$this->key = $attributes['key'] ?? null;
|
|
||||||
|
|
||||||
/** @var Route $route */
|
|
||||||
$route = $attributes['route'];
|
|
||||||
$post = $this->getPost();
|
|
||||||
|
|
||||||
if ($this->isFormSubmit()) {
|
|
||||||
$form = $this->getForm();
|
|
||||||
$this->nonce_name = $attributes['nonce_name'] ?? $form->getNonceName();
|
|
||||||
$this->nonce_action = $attributes['nonce_action'] ?? $form->getNonceAction();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$task = $request->getAttribute('task') ?? $post['task'] ?? $route->getParam('task');
|
|
||||||
if ($task) {
|
|
||||||
if (empty($attributes['forwarded'])) {
|
|
||||||
$this->checkNonce($task);
|
|
||||||
}
|
|
||||||
$type = 'task';
|
|
||||||
$command = $task;
|
|
||||||
} else {
|
|
||||||
$type = 'action';
|
|
||||||
$command = $request->getAttribute('action') ?? $post['action'] ?? $route->getParam('action') ?? 'display';
|
|
||||||
}
|
|
||||||
$command = strtolower($command);
|
|
||||||
|
|
||||||
$event = new Event(
|
|
||||||
[
|
|
||||||
'controller' => $this,
|
|
||||||
'response' => null
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
$this->grav->fireEvent("admin.{$this->type}.{$type}.{$command}", $event);
|
|
||||||
|
|
||||||
$response = $event['response'];
|
|
||||||
if (!$response) {
|
|
||||||
/** @var Inflector $inflector */
|
|
||||||
$inflector = $this->grav['inflector'];
|
|
||||||
$method = $type . $inflector::camelize($command);
|
|
||||||
if ($method && method_exists($this, $method)) {
|
|
||||||
$response = $this->{$method}($request);
|
|
||||||
} else {
|
|
||||||
throw new NotFoundException($request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
/** @var Debugger $debugger */
|
|
||||||
$debugger = $this->grav['debugger'];
|
|
||||||
$debugger->addException($e);
|
|
||||||
|
|
||||||
$response = $this->createErrorResponse($e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($response instanceof Response) {
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->createJsonResponse($response);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get request.
|
|
||||||
*
|
|
||||||
* @return ServerRequestInterface
|
|
||||||
*/
|
|
||||||
public function getRequest(): ServerRequestInterface
|
|
||||||
{
|
|
||||||
return $this->request;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string|null $name
|
|
||||||
* @param mixed $default
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function getPost(?string $name = null, $default = null)
|
|
||||||
{
|
|
||||||
$body = $this->request->getParsedBody();
|
|
||||||
|
|
||||||
if ($name) {
|
|
||||||
return $body[$name] ?? $default;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $body;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a form has been submitted.
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isFormSubmit(): bool
|
|
||||||
{
|
|
||||||
return (bool)$this->getPost('__form-name__');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get form.
|
|
||||||
*
|
|
||||||
* @param string|null $type
|
|
||||||
* @return FormInterface
|
|
||||||
*/
|
|
||||||
public function getForm(?string $type = null): FormInterface
|
|
||||||
{
|
|
||||||
$object = $this->getObject();
|
|
||||||
if (!$object) {
|
|
||||||
throw new \RuntimeException('Not Found', 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$formName = $this->getPost('__form-name__');
|
|
||||||
$uniqueId = $this->getPost('__unique_form_id__') ?: $formName;
|
|
||||||
|
|
||||||
$form = $object->getForm($type ?? 'edit');
|
|
||||||
if ($uniqueId) {
|
|
||||||
$form->setUniqueId($uniqueId);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $form;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return FlexObjectInterface
|
|
||||||
*/
|
|
||||||
abstract public function getObject();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get Grav instance.
|
|
||||||
*
|
|
||||||
* @return Grav
|
|
||||||
*/
|
|
||||||
public function getGrav(): Grav
|
|
||||||
{
|
|
||||||
return $this->grav;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get session.
|
|
||||||
*
|
|
||||||
* @return SessionInterface
|
|
||||||
*/
|
|
||||||
public function getSession(): SessionInterface
|
|
||||||
{
|
|
||||||
return $this->getGrav()['session'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display the current admin page.
|
|
||||||
*
|
|
||||||
* @return Response
|
|
||||||
*/
|
|
||||||
public function createDisplayResponse(): ResponseInterface
|
|
||||||
{
|
|
||||||
return new Response(418);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create custom HTML response.
|
|
||||||
*
|
|
||||||
* @param string $content
|
|
||||||
* @param int $code
|
|
||||||
* @return Response
|
|
||||||
*/
|
|
||||||
public function createHtmlResponse(string $content, ?int $code = null): ResponseInterface
|
|
||||||
{
|
|
||||||
return new Response($code ?: 200, [], $content);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create JSON response.
|
|
||||||
*
|
|
||||||
* @param array $content
|
|
||||||
* @return Response
|
|
||||||
*/
|
|
||||||
public function createJsonResponse(array $content): ResponseInterface
|
|
||||||
{
|
|
||||||
$code = $content['code'] ?? 200;
|
|
||||||
if ($code >= 301 && $code <= 307) {
|
|
||||||
$code = 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response($code, ['Content-Type' => 'application/json'], json_encode($content));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create redirect response.
|
|
||||||
*
|
|
||||||
* @param string $url
|
|
||||||
* @param int $code
|
|
||||||
* @return Response
|
|
||||||
*/
|
|
||||||
public function createRedirectResponse(string $url, ?int $code = null): ResponseInterface
|
|
||||||
{
|
|
||||||
if (null === $code || $code < 301 || $code > 307) {
|
|
||||||
$code = $this->grav['config']->get('system.pages.redirect_default_code', 302);
|
|
||||||
}
|
|
||||||
|
|
||||||
$accept = $this->getAccept(['application/json', 'text/html']);
|
|
||||||
|
|
||||||
if ($accept === 'application/json') {
|
|
||||||
return $this->createJsonResponse(['code' => $code, 'status' => 'redirect', 'redirect' => $url]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response($code, ['Location' => $url]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create error response.
|
|
||||||
*
|
|
||||||
* @param \Exception $exception
|
|
||||||
* @return Response
|
|
||||||
*/
|
|
||||||
public function createErrorResponse(\Exception $exception): ResponseInterface
|
|
||||||
{
|
|
||||||
$validCodes = [
|
|
||||||
400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418,
|
|
||||||
422, 423, 424, 425, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 511
|
|
||||||
];
|
|
||||||
|
|
||||||
if ($exception instanceof RequestException) {
|
|
||||||
$code = $exception->getHttpCode();
|
|
||||||
$reason = $exception->getHttpReason();
|
|
||||||
} else {
|
|
||||||
$code = $exception->getCode();
|
|
||||||
$reason = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!in_array($code, $validCodes, true)) {
|
|
||||||
$code = 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
$message = $exception->getMessage();
|
|
||||||
$response = [
|
|
||||||
'code' => $code,
|
|
||||||
'status' => 'error',
|
|
||||||
'message' => htmlspecialchars($message, ENT_QUOTES | ENT_HTML5, 'UTF-8')
|
|
||||||
];
|
|
||||||
|
|
||||||
$accept = $this->getAccept(['application/json', 'text/html']);
|
|
||||||
|
|
||||||
if ($accept === 'text/html') {
|
|
||||||
$method = $this->getRequest()->getMethod();
|
|
||||||
|
|
||||||
// On POST etc, redirect back to the previous page.
|
|
||||||
if ($method !== 'GET' && $method !== 'HEAD') {
|
|
||||||
$this->setMessage($message, 'error');
|
|
||||||
$referer = $this->request->getHeaderLine('Referer');
|
|
||||||
return $this->createRedirectResponse($referer, 303);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: improve error page
|
|
||||||
return $this->createHtmlResponse($response['message']);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response($code, ['Content-Type' => 'application/json'], json_encode($response), '1.1', $reason);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translate a string.
|
|
||||||
*
|
|
||||||
* @param string $string
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function translate(string $string): string
|
|
||||||
{
|
|
||||||
/** @var Language $language */
|
|
||||||
$language = $this->grav['language'];
|
|
||||||
|
|
||||||
return $language->translate($string);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set message to be shown in the admin.
|
|
||||||
*
|
|
||||||
* @param string $message
|
|
||||||
* @param string $type
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function setMessage($message, $type = 'info')
|
|
||||||
{
|
|
||||||
/** @var Message $messages */
|
|
||||||
$messages = $this->grav['messages'];
|
|
||||||
$messages->add($message, $type);
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if request nonce is valid.
|
|
||||||
*
|
|
||||||
* @param string $task
|
|
||||||
* @throws PageExpiredException If nonce is not valid.
|
|
||||||
*/
|
|
||||||
protected function checkNonce(string $task): void
|
|
||||||
{
|
|
||||||
$nonce = null;
|
|
||||||
|
|
||||||
if (\in_array(strtoupper($this->request->getMethod()), ['POST', 'PUT', 'PATCH', 'DELETE'])) {
|
|
||||||
$nonce = $this->getPost($this->nonce_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$nonce) {
|
|
||||||
$nonce = $this->grav['uri']->param($this->nonce_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$nonce) {
|
|
||||||
$nonce = $this->grav['uri']->query($this->nonce_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$nonce || !Utils::verifyNonce($nonce, $this->nonce_action)) {
|
|
||||||
throw new PageExpiredException($this->request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the best matching mime type for the request.
|
|
||||||
*
|
|
||||||
* @param string[] $compare
|
|
||||||
* @return string|null
|
|
||||||
*/
|
|
||||||
protected function getAccept(array $compare): ?string
|
|
||||||
{
|
|
||||||
$accepted = [];
|
|
||||||
foreach ($this->request->getHeader('Accept') as $accept) {
|
|
||||||
foreach (explode(',', $accept) as $item) {
|
|
||||||
if (!$item) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$split = explode(';q=', $item);
|
|
||||||
$mime = array_shift($split);
|
|
||||||
$priority = array_shift($split) ?? 1.0;
|
|
||||||
|
|
||||||
$accepted[$mime] = $priority;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
arsort($accepted);
|
|
||||||
|
|
||||||
// TODO: add support for image/* etc
|
|
||||||
$list = array_intersect($compare, array_keys($accepted));
|
|
||||||
if (!$list && (isset($accepted['*/*']) || isset($accepted['*']))) {
|
|
||||||
return reset($compare) ?: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return reset($list) ?: null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,359 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package Grav\Plugin\Admin
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
|
||||||
* @license MIT License; see LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Grav\Plugin\Admin\Controllers;
|
|
||||||
|
|
||||||
use Grav\Common\Config\Config;
|
|
||||||
use Grav\Common\Data\Blueprint;
|
|
||||||
use Grav\Common\Grav;
|
|
||||||
use Grav\Common\Language\Language;
|
|
||||||
use Grav\Common\Page\Interfaces\PageInterface;
|
|
||||||
use Grav\Common\Page\Page;
|
|
||||||
use Grav\Common\Page\Pages;
|
|
||||||
use Grav\Common\Uri;
|
|
||||||
use Grav\Common\User\Interfaces\UserInterface;
|
|
||||||
use Grav\Common\Utils;
|
|
||||||
use Grav\Framework\Controller\Traits\ControllerResponseTrait;
|
|
||||||
use Grav\Framework\RequestHandler\Exception\PageExpiredException;
|
|
||||||
use Grav\Framework\Session\SessionInterface;
|
|
||||||
use Grav\Plugin\Admin\Admin;
|
|
||||||
use Grav\Plugin\Admin\AdminForm;
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
|
||||||
use RocketTheme\Toolbox\Session\Message;
|
|
||||||
|
|
||||||
abstract class AdminController
|
|
||||||
{
|
|
||||||
use ControllerResponseTrait {
|
|
||||||
createRedirectResponse as traitCreateRedirectResponse;
|
|
||||||
getErrorJson as traitGetErrorJson;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var string */
|
|
||||||
protected $nonce_action = 'admin-form';
|
|
||||||
/** @var string */
|
|
||||||
protected $nonce_name = 'admin-nonce';
|
|
||||||
/** @var Grav */
|
|
||||||
protected $grav;
|
|
||||||
/** @var PageInterface */
|
|
||||||
protected $page;
|
|
||||||
/** @var AdminForm|null */
|
|
||||||
protected $form;
|
|
||||||
|
|
||||||
public function __construct(Grav $grav)
|
|
||||||
{
|
|
||||||
$this->grav = $grav;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return PageInterface|null
|
|
||||||
*/
|
|
||||||
public function getPage(): ?PageInterface
|
|
||||||
{
|
|
||||||
return $this->page;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get currently active form.
|
|
||||||
*
|
|
||||||
* @return AdminForm|null
|
|
||||||
*/
|
|
||||||
public function getActiveForm(): ?AdminForm
|
|
||||||
{
|
|
||||||
if (null === $this->form) {
|
|
||||||
$post = $this->getPost();
|
|
||||||
|
|
||||||
$active = $post['__form-name__'] ?? null;
|
|
||||||
|
|
||||||
$this->form = $active ? $this->getForm($active) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->form;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a form.
|
|
||||||
*
|
|
||||||
* @param string $name
|
|
||||||
* @param array $options
|
|
||||||
* @return AdminForm|null
|
|
||||||
*/
|
|
||||||
public function getForm(string $name, array $options = []): ?AdminForm
|
|
||||||
{
|
|
||||||
$post = $this->getPost();
|
|
||||||
$page = $this->getPage();
|
|
||||||
$forms = $page ? $page->forms() : [];
|
|
||||||
$blueprint = $forms[$name] ?? null;
|
|
||||||
if (null === $blueprint) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$active = $post['__form-name__'] ?? null;
|
|
||||||
$unique_id = $active && $active === $name ? ($post['__unique_form_id__'] ?? null) : null;
|
|
||||||
|
|
||||||
$options += [
|
|
||||||
'unique_id' => $unique_id,
|
|
||||||
'blueprint' => new Blueprint(null, ['form' => $blueprint]),
|
|
||||||
'submit_method' => $this->getFormSubmitMethod($name),
|
|
||||||
'nonce_name' => $this->nonce_name,
|
|
||||||
'nonce_action' => $this->nonce_action,
|
|
||||||
];
|
|
||||||
|
|
||||||
return new AdminForm($name, $options);
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract protected function getFormSubmitMethod(string $name): callable;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $route
|
|
||||||
* @param string|null $lang
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getAdminUrl(string $route, ?string $lang = null): string
|
|
||||||
{
|
|
||||||
/** @var Pages $pages */
|
|
||||||
$pages = $this->grav['pages'];
|
|
||||||
$admin = $this->getAdmin();
|
|
||||||
|
|
||||||
return $pages->baseUrl($lang) . $admin->base . $route;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $route
|
|
||||||
* @param string|null $lang
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getAbsoluteAdminUrl(string $route, ?string $lang = null): string
|
|
||||||
{
|
|
||||||
/** @var Pages $pages */
|
|
||||||
$pages = $this->grav['pages'];
|
|
||||||
$admin = $this->getAdmin();
|
|
||||||
|
|
||||||
return $pages->baseUrl($lang, true) . $admin->base . $route;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get session.
|
|
||||||
*
|
|
||||||
* @return SessionInterface
|
|
||||||
*/
|
|
||||||
public function getSession(): SessionInterface
|
|
||||||
{
|
|
||||||
return $this->grav['session'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Admin
|
|
||||||
*/
|
|
||||||
protected function getAdmin(): Admin
|
|
||||||
{
|
|
||||||
return $this->grav['admin'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return UserInterface
|
|
||||||
*/
|
|
||||||
protected function getUser(): UserInterface
|
|
||||||
{
|
|
||||||
return $this->getAdmin()->user;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return ServerRequestInterface
|
|
||||||
*/
|
|
||||||
public function getRequest(): ServerRequestInterface
|
|
||||||
{
|
|
||||||
return $this->getAdmin()->request;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function getPost(): array
|
|
||||||
{
|
|
||||||
return (array)($this->getRequest()->getParsedBody() ?? []);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Translate a string.
|
|
||||||
*
|
|
||||||
* @param string $string
|
|
||||||
* @param mixed ...$args
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function translate(string $string, ...$args): string
|
|
||||||
{
|
|
||||||
/** @var Language $language */
|
|
||||||
$language = $this->grav['language'];
|
|
||||||
|
|
||||||
array_unshift($args, $string);
|
|
||||||
|
|
||||||
return $language->translate($args);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set message to be shown in the admin.
|
|
||||||
*
|
|
||||||
* @param string $message
|
|
||||||
* @param string $type
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function setMessage(string $message, string $type = 'info'): AdminController
|
|
||||||
{
|
|
||||||
/** @var Message $messages */
|
|
||||||
$messages = $this->grav['messages'];
|
|
||||||
$messages->add($message, $type);
|
|
||||||
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Config
|
|
||||||
*/
|
|
||||||
protected function getConfig(): Config
|
|
||||||
{
|
|
||||||
return $this->grav['config'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if request nonce is valid.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
* @throws PageExpiredException If nonce is not valid.
|
|
||||||
*/
|
|
||||||
protected function checkNonce(): void
|
|
||||||
{
|
|
||||||
$nonce = null;
|
|
||||||
|
|
||||||
$nonce_name = $this->form ? $this->form->getNonceName() : $this->nonce_name;
|
|
||||||
$nonce_action = $this->form ? $this->form->getNonceAction() : $this->nonce_action;
|
|
||||||
|
|
||||||
if (\in_array(strtoupper($this->getRequest()->getMethod()), ['POST', 'PUT', 'PATCH', 'DELETE'])) {
|
|
||||||
$post = $this->getPost();
|
|
||||||
$nonce = $post[$nonce_name] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var Uri $uri */
|
|
||||||
$uri = $this->grav['uri'];
|
|
||||||
if (!$nonce) {
|
|
||||||
$nonce = $uri->param($nonce_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$nonce) {
|
|
||||||
$nonce = $uri->query($nonce_name);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$nonce || !Utils::verifyNonce($nonce, $nonce_action)) {
|
|
||||||
throw new PageExpiredException($this->getRequest());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the best matching mime type for the request.
|
|
||||||
*
|
|
||||||
* @param string[] $compare
|
|
||||||
* @return string|null
|
|
||||||
*/
|
|
||||||
protected function getAccept(array $compare): ?string
|
|
||||||
{
|
|
||||||
$accepted = [];
|
|
||||||
foreach ($this->getRequest()->getHeader('Accept') as $accept) {
|
|
||||||
foreach (explode(',', $accept) as $item) {
|
|
||||||
if (!$item) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$split = explode(';q=', $item);
|
|
||||||
$mime = array_shift($split);
|
|
||||||
$priority = array_shift($split) ?? 1.0;
|
|
||||||
|
|
||||||
$accepted[$mime] = $priority;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
arsort($accepted);
|
|
||||||
|
|
||||||
// TODO: add support for image/* etc
|
|
||||||
$list = array_intersect($compare, array_keys($accepted));
|
|
||||||
if (!$list && (isset($accepted['*/*']) || isset($accepted['*']))) {
|
|
||||||
return reset($compare) ?: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return reset($list) ?: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $template
|
|
||||||
* @return PageInterface
|
|
||||||
*/
|
|
||||||
protected function createPage(string $template): PageInterface
|
|
||||||
{
|
|
||||||
$page = new Page();
|
|
||||||
|
|
||||||
// Plugins may not have the correct Cache-Control header set, force no-store for the proxies.
|
|
||||||
$page->expires(0);
|
|
||||||
|
|
||||||
$filename = "plugin://admin/pages/admin/{$template}.md";
|
|
||||||
if (!file_exists($filename)) {
|
|
||||||
throw new \RuntimeException(sprintf('Creating admin page %s failed: not found', $template));
|
|
||||||
}
|
|
||||||
|
|
||||||
Admin::DEBUG && Admin::addDebugMessage("Admin page: {$template}");
|
|
||||||
|
|
||||||
$page->init(new \SplFileInfo($filename));
|
|
||||||
$page->slug($template);
|
|
||||||
|
|
||||||
return $page;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string|null $url
|
|
||||||
* @param int|null $code
|
|
||||||
* @return ResponseInterface
|
|
||||||
*/
|
|
||||||
protected function createRedirectResponse(?string $url = null, ?int $code = null): ResponseInterface
|
|
||||||
{
|
|
||||||
$request = $this->getRequest();
|
|
||||||
|
|
||||||
if (null === $url || '' === $url) {
|
|
||||||
$url = (string)$request->getUri();
|
|
||||||
} elseif (mb_strpos($url, '/') === 0) {
|
|
||||||
$url = $this->getAbsoluteAdminUrl($url);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (null === $code) {
|
|
||||||
if (in_array($request->getMethod(), ['GET', 'HEAD'])) {
|
|
||||||
$code = 302;
|
|
||||||
} else {
|
|
||||||
$code = 303;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->traitCreateRedirectResponse($url, $code);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \Throwable $e
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
protected function getErrorJson(\Throwable $e): array
|
|
||||||
{
|
|
||||||
$json = $this->traitGetErrorJson($e);
|
|
||||||
$code = $e->getCode();
|
|
||||||
if ($code === 401) {
|
|
||||||
$json['redirect'] = $this->getAbsoluteAdminUrl('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $json;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,644 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package Grav\Plugin\Admin
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
|
||||||
* @license MIT License; see LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Grav\Plugin\Admin\Controllers\Login;
|
|
||||||
|
|
||||||
use Grav\Common\Debugger;
|
|
||||||
use Grav\Common\Grav;
|
|
||||||
use Grav\Common\Page\Pages;
|
|
||||||
use Grav\Common\Uri;
|
|
||||||
use Grav\Common\User\Interfaces\UserCollectionInterface;
|
|
||||||
use Grav\Common\User\Interfaces\UserInterface;
|
|
||||||
use Grav\Common\Utils;
|
|
||||||
use Grav\Framework\RequestHandler\Exception\PageExpiredException;
|
|
||||||
use Grav\Framework\RequestHandler\Exception\RequestException;
|
|
||||||
use Grav\Plugin\Admin\Admin;
|
|
||||||
use Grav\Plugin\Admin\Controllers\AdminController;
|
|
||||||
use Grav\Plugin\Email\Email;
|
|
||||||
use Grav\Plugin\Login\Login;
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
|
||||||
use RobThree\Auth\TwoFactorAuthException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class LoginController
|
|
||||||
* @package Grav\Plugin\Admin\Controllers\Login
|
|
||||||
*/
|
|
||||||
class LoginController extends AdminController
|
|
||||||
{
|
|
||||||
/** @var string */
|
|
||||||
protected $nonce_action = 'admin-login';
|
|
||||||
/** @var string */
|
|
||||||
protected $nonce_name = 'login-nonce';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return ResponseInterface
|
|
||||||
*/
|
|
||||||
public function displayLogin(): ResponseInterface
|
|
||||||
{
|
|
||||||
$this->page = $this->createPage('login');
|
|
||||||
|
|
||||||
$user = $this->getUser();
|
|
||||||
if ($this->is2FA($user)) {
|
|
||||||
$this->form = $this->getForm('login-twofa', ['reset' => true]);
|
|
||||||
} else {
|
|
||||||
$this->form = $this->getForm('login', ['reset' => true]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->createDisplayResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return ResponseInterface
|
|
||||||
*/
|
|
||||||
public function displayForgot(): ResponseInterface
|
|
||||||
{
|
|
||||||
$this->page = $this->createPage('forgot');
|
|
||||||
$this->form = $this->getForm('admin-login-forgot', ['reset' => true]);
|
|
||||||
|
|
||||||
return $this->createDisplayResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the reset password action.
|
|
||||||
*
|
|
||||||
* @param string|null $username
|
|
||||||
* @param string|null $token
|
|
||||||
* @return ResponseInterface
|
|
||||||
*/
|
|
||||||
public function displayReset(?string $username = null, ?string $token = null): ResponseInterface
|
|
||||||
{
|
|
||||||
if ('' === (string)$username || '' === (string)$token) {
|
|
||||||
$this->setMessage($this->translate('PLUGIN_ADMIN.RESET_INVALID_LINK'), 'error');
|
|
||||||
|
|
||||||
return $this->createRedirectResponse('/forgot');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->page = $this->createPage('reset');
|
|
||||||
$this->form = $this->getForm('admin-login-reset', ['reset' => true]);
|
|
||||||
$this->form->setData('username', $username);
|
|
||||||
$this->form->setData('token', $token);
|
|
||||||
|
|
||||||
$this->setMessage($this->translate('PLUGIN_ADMIN.RESET_NEW_PASSWORD'));
|
|
||||||
|
|
||||||
return $this->createDisplayResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return ResponseInterface
|
|
||||||
*/
|
|
||||||
public function displayRegister(): ResponseInterface
|
|
||||||
{
|
|
||||||
$route = $this->getRequest()->getAttribute('admin')['route'] ?? '';
|
|
||||||
if ('' !== $route) {
|
|
||||||
return $this->createRedirectResponse('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->page = $this->createPage('register');
|
|
||||||
$this->form = $this->getForm('admin-login-register');
|
|
||||||
|
|
||||||
return $this->createDisplayResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return ResponseInterface
|
|
||||||
*/
|
|
||||||
public function displayUnauthorized(): ResponseInterface
|
|
||||||
{
|
|
||||||
$uri = (string)$this->getRequest()->getUri();
|
|
||||||
|
|
||||||
$ext = Utils::pathinfo($uri, PATHINFO_EXTENSION);
|
|
||||||
$accept = $this->getAccept(['application/json', 'text/html']);
|
|
||||||
if ($ext === 'json' || $accept === 'application/json') {
|
|
||||||
return $this->createErrorResponse(new RequestException($this->getRequest(), $this->translate('PLUGIN_ADMIN.LOGGED_OUT'), 401));
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->setMessage($this->translate('PLUGIN_ADMIN.LOGGED_OUT'), 'warning');
|
|
||||||
|
|
||||||
return $this->createRedirectResponse('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle login.
|
|
||||||
*
|
|
||||||
* @return ResponseInterface
|
|
||||||
*/
|
|
||||||
public function taskLogin(): ResponseInterface
|
|
||||||
{
|
|
||||||
$this->page = $this->createPage('login');
|
|
||||||
$this->form = $this->getActiveForm() ?? $this->getForm('login');
|
|
||||||
try {
|
|
||||||
$this->checkNonce();
|
|
||||||
} catch (PageExpiredException $e) {
|
|
||||||
$this->setMessage($this->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'), 'error');
|
|
||||||
|
|
||||||
return $this->createDisplayResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
$post = $this->getPost();
|
|
||||||
$credentials = (array)($post['data'] ?? []);
|
|
||||||
$login = $this->getLogin();
|
|
||||||
$config = $this->getConfig();
|
|
||||||
|
|
||||||
$userKey = (string)($credentials['username'] ?? '');
|
|
||||||
// Pseudonymization of the IP.
|
|
||||||
$ipKey = sha1(Uri::ip() . $config->get('security.salt'));
|
|
||||||
|
|
||||||
$rateLimiter = $login->getRateLimiter('login_attempts');
|
|
||||||
|
|
||||||
// Check if the current IP has been used in failed login attempts.
|
|
||||||
$attempts = count($rateLimiter->getAttempts($ipKey, 'ip'));
|
|
||||||
|
|
||||||
$rateLimiter->registerRateLimitedAction($ipKey, 'ip')->registerRateLimitedAction($userKey);
|
|
||||||
|
|
||||||
// Check rate limit for both IP and user, but allow each IP a single try even if user is already rate limited.
|
|
||||||
if ($rateLimiter->isRateLimited($ipKey, 'ip') || ($attempts && $rateLimiter->isRateLimited($userKey))) {
|
|
||||||
Admin::DEBUG && Admin::addDebugMessage('Admin login: rate limit, redirecting', $credentials);
|
|
||||||
|
|
||||||
$this->setMessage($this->translate('PLUGIN_LOGIN.TOO_MANY_LOGIN_ATTEMPTS', $rateLimiter->getInterval()), 'error');
|
|
||||||
|
|
||||||
$this->form->reset();
|
|
||||||
|
|
||||||
/** @var Pages $pages */
|
|
||||||
$pages = $this->grav['pages'];
|
|
||||||
|
|
||||||
// Redirect to the home page of the site.
|
|
||||||
return $this->createRedirectResponse($pages->homeUrl(null, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
Admin::DEBUG && Admin::addDebugMessage('Admin login', $credentials);
|
|
||||||
|
|
||||||
// Fire Login process.
|
|
||||||
$event = $login->login(
|
|
||||||
$credentials,
|
|
||||||
['admin' => true, 'twofa' => $config->get('plugins.admin.twofa_enabled', false)],
|
|
||||||
['authorize' => 'admin.login', 'return_event' => true]
|
|
||||||
);
|
|
||||||
$user = $event->getUser();
|
|
||||||
|
|
||||||
Admin::DEBUG && Admin::addDebugMessage('Admin login: user', $user);
|
|
||||||
|
|
||||||
$redirect = (string)$this->getRequest()->getUri();
|
|
||||||
|
|
||||||
if ($user->authenticated) {
|
|
||||||
$rateLimiter->resetRateLimit($ipKey, 'ip')->resetRateLimit($userKey);
|
|
||||||
if ($user->authorized) {
|
|
||||||
$event->defMessage('PLUGIN_ADMIN.LOGIN_LOGGED_IN', 'info');
|
|
||||||
}
|
|
||||||
|
|
||||||
$event->defRedirect($redirect);
|
|
||||||
} elseif ($user->authorized) {
|
|
||||||
$event->defMessage('PLUGIN_LOGIN.ACCESS_DENIED', 'error');
|
|
||||||
} else {
|
|
||||||
$event->defMessage('PLUGIN_LOGIN.LOGIN_FAILED', 'error');
|
|
||||||
}
|
|
||||||
|
|
||||||
$event->defRedirect($redirect);
|
|
||||||
|
|
||||||
$message = $event->getMessage();
|
|
||||||
if ($message) {
|
|
||||||
$this->setMessage($this->translate($message), $event->getMessageType());
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->form->reset();
|
|
||||||
|
|
||||||
return $this->createRedirectResponse($event->getRedirect());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle logout when user isn't fully logged in or clicks logout after the session has been expired.
|
|
||||||
*
|
|
||||||
* @return ResponseInterface
|
|
||||||
*/
|
|
||||||
public function taskLogout(): ResponseInterface
|
|
||||||
{
|
|
||||||
// We do not need to check the nonce here as user session has been expired or user hasn't fully logged in (2FA).
|
|
||||||
// Just be sure we terminate the current session.
|
|
||||||
$login = $this->getLogin();
|
|
||||||
$event = $login->logout(['admin' => true], ['return_event' => true]);
|
|
||||||
|
|
||||||
$event->defMessage('PLUGIN_ADMIN.LOGGED_OUT', 'info');
|
|
||||||
$message = $event->getMessage();
|
|
||||||
if ($message) {
|
|
||||||
$this->getSession()->setFlashCookieObject(Admin::TMP_COOKIE_NAME, ['message' => $this->translate($message), 'status' => $event->getMessageType()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->createRedirectResponse('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle 2FA verification.
|
|
||||||
*
|
|
||||||
* @return ResponseInterface
|
|
||||||
*/
|
|
||||||
public function taskTwofa(): ResponseInterface
|
|
||||||
{
|
|
||||||
$user = $this->getUser();
|
|
||||||
if (!$this->is2FA($user)) {
|
|
||||||
Admin::DEBUG && Admin::addDebugMessage('Admin login: user is not logged in or does not have 2FA enabled', $user);
|
|
||||||
|
|
||||||
// Task is visible only for users who have enabled 2FA.
|
|
||||||
return $this->createRedirectResponse('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
$login = $this->getLogin();
|
|
||||||
|
|
||||||
$this->page = $this->createPage('login');
|
|
||||||
$this->form = $this->getForm('login-twofa');
|
|
||||||
try {
|
|
||||||
$this->checkNonce();
|
|
||||||
} catch (PageExpiredException $e) {
|
|
||||||
$this->setMessage($this->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'), 'error');
|
|
||||||
|
|
||||||
// Failed 2FA nonce check, logout and redirect.
|
|
||||||
$login->logout(['admin' => true]);
|
|
||||||
$this->form->reset();
|
|
||||||
|
|
||||||
return $this->createRedirectResponse('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$post = $this->getPost();
|
|
||||||
$data = $post['data'] ?? [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
$twoFa = $login->twoFactorAuth();
|
|
||||||
} catch (TwoFactorAuthException $e) {
|
|
||||||
/** @var Debugger $debugger */
|
|
||||||
$debugger = $this->grav['debugger'];
|
|
||||||
$debugger->addException($e);
|
|
||||||
|
|
||||||
$twoFa = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
$code = $data['2fa_code'] ?? '';
|
|
||||||
$secret = $user->twofa_secret ?? '';
|
|
||||||
// Strip any whitespace from secret (fixes corrupted secrets)
|
|
||||||
$secret = preg_replace('/\s+/', '', $secret);
|
|
||||||
$twofa_valid = $twoFa->verifyCode($secret, $code);
|
|
||||||
|
|
||||||
$yubikey_otp = $data['yubikey_otp'] ?? '';
|
|
||||||
$yubikey_id = $user->yubikey_id ?? '';
|
|
||||||
$yubikey_valid = $twoFa->verifyYubikeyOTP($yubikey_id, $yubikey_otp);
|
|
||||||
|
|
||||||
$redirect = (string)$this->getRequest()->getUri();
|
|
||||||
|
|
||||||
if (null === $twoFa || !$user->authenticated || (!$twofa_valid && !$yubikey_valid) ) {
|
|
||||||
Admin::DEBUG && Admin::addDebugMessage('Admin login: 2FA check failed, log out!');
|
|
||||||
|
|
||||||
// Failed 2FA auth, logout and redirect to the current page.
|
|
||||||
$login->logout(['admin' => true]);
|
|
||||||
|
|
||||||
$this->grav['session']->setFlashCookieObject(Admin::TMP_COOKIE_NAME, ['message' => $this->translate('PLUGIN_ADMIN.2FA_FAILED'), 'status' => 'error']);
|
|
||||||
|
|
||||||
$this->form->reset();
|
|
||||||
|
|
||||||
return $this->createRedirectResponse($redirect);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Successful 2FA, authorize user and redirect.
|
|
||||||
Grav::instance()['user']->authorized = true;
|
|
||||||
|
|
||||||
Admin::DEBUG && Admin::addDebugMessage('Admin login: 2FA check succeeded, authorize user and redirect');
|
|
||||||
|
|
||||||
$this->setMessage($this->translate('PLUGIN_ADMIN.LOGIN_LOGGED_IN'));
|
|
||||||
|
|
||||||
$this->form->reset();
|
|
||||||
|
|
||||||
return $this->createRedirectResponse($redirect);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the reset password action.
|
|
||||||
*
|
|
||||||
* @param string|null $username
|
|
||||||
* @param string|null $token
|
|
||||||
* @return ResponseInterface
|
|
||||||
*/
|
|
||||||
public function taskReset(?string $username = null, ?string $token = null): ResponseInterface
|
|
||||||
{
|
|
||||||
$this->page = $this->createPage('reset');
|
|
||||||
$this->form = $this->getForm('admin-login-reset');
|
|
||||||
try {
|
|
||||||
$this->checkNonce();
|
|
||||||
} catch (PageExpiredException $e) {
|
|
||||||
$this->setMessage($this->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'), 'error');
|
|
||||||
|
|
||||||
return $this->createDisplayResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$post = $this->getPost();
|
|
||||||
$data = $post['data'] ?? [];
|
|
||||||
$users = $this->getAccounts();
|
|
||||||
|
|
||||||
$username = $username ?? $data['username'] ?? null;
|
|
||||||
$token = $token ?? $data['token'] ?? null;
|
|
||||||
|
|
||||||
$user = $username ? $users->load($username) : null;
|
|
||||||
$password = $data['password'];
|
|
||||||
|
|
||||||
if ($user && $user->exists() && !empty($user->get('reset'))) {
|
|
||||||
[$good_token, $expire] = explode('::', $user->get('reset'));
|
|
||||||
|
|
||||||
if ($good_token === $token) {
|
|
||||||
if (time() > $expire) {
|
|
||||||
$this->setMessage($this->translate('PLUGIN_ADMIN.RESET_LINK_EXPIRED'), 'error');
|
|
||||||
|
|
||||||
$this->form->reset();
|
|
||||||
|
|
||||||
return $this->createRedirectResponse('/forgot');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set new password.
|
|
||||||
$login = $this->getLogin();
|
|
||||||
try {
|
|
||||||
$login->validateField('password1', $password);
|
|
||||||
} catch (\RuntimeException $e) {
|
|
||||||
$this->setMessage($this->translate($e->getMessage()), 'error');
|
|
||||||
|
|
||||||
return $this->createRedirectResponse("/reset/u/{$username}/{$token}");
|
|
||||||
}
|
|
||||||
|
|
||||||
$user->undef('hashed_password');
|
|
||||||
$user->undef('reset');
|
|
||||||
$user->update(['password' => $password]);
|
|
||||||
$user->save();
|
|
||||||
|
|
||||||
$this->form->reset();
|
|
||||||
|
|
||||||
$this->setMessage($this->translate('PLUGIN_ADMIN.RESET_PASSWORD_RESET'));
|
|
||||||
|
|
||||||
return $this->createRedirectResponse('/login');
|
|
||||||
}
|
|
||||||
|
|
||||||
Admin::DEBUG && Admin::addDebugMessage(sprintf('Failed to reset password: Token %s is not good', $token));
|
|
||||||
} else {
|
|
||||||
Admin::DEBUG && Admin::addDebugMessage(sprintf('Failed to reset password: User %s does not exist or has not requested reset', $username));
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->setMessage($this->translate('PLUGIN_ADMIN.RESET_INVALID_LINK'), 'error');
|
|
||||||
|
|
||||||
$this->form->reset();
|
|
||||||
|
|
||||||
return $this->createRedirectResponse('/forgot');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the email password recovery procedure.
|
|
||||||
*
|
|
||||||
* Sends email to the user.
|
|
||||||
*
|
|
||||||
* @return ResponseInterface
|
|
||||||
*/
|
|
||||||
public function taskForgot(): ResponseInterface
|
|
||||||
{
|
|
||||||
$this->page = $this->createPage('forgot');
|
|
||||||
$this->form = $this->getForm('admin-login-forgot');
|
|
||||||
try {
|
|
||||||
$this->checkNonce();
|
|
||||||
} catch (PageExpiredException $e) {
|
|
||||||
$this->setMessage($this->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'), 'error');
|
|
||||||
|
|
||||||
return $this->createDisplayResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$post = $this->getPost();
|
|
||||||
$data = $post['data'] ?? [];
|
|
||||||
$login = $this->getLogin();
|
|
||||||
$users = $this->getAccounts();
|
|
||||||
$email = $this->getEmail();
|
|
||||||
|
|
||||||
$current = (string)$this->getRequest()->getUri();
|
|
||||||
|
|
||||||
$search = isset($data['username']) ? strip_tags($data['username']) : '';
|
|
||||||
$user = !empty($search) ? $users->load($search) : null;
|
|
||||||
$username = $user->username ?? null;
|
|
||||||
$to = $user->email ?? null;
|
|
||||||
|
|
||||||
// Only send email to users which are enabled and have an email address.
|
|
||||||
if (null === $user || $user->state !== 'enabled' || !$to) {
|
|
||||||
Admin::DEBUG && Admin::addDebugMessage(sprintf('Failed sending email: %s <%s> was not found or is blocked', $search, $to ?? 'N/A'));
|
|
||||||
|
|
||||||
$this->form->reset();
|
|
||||||
|
|
||||||
$this->setMessage($this->translate('PLUGIN_ADMIN.FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL'));
|
|
||||||
|
|
||||||
return $this->createRedirectResponse($current);
|
|
||||||
}
|
|
||||||
|
|
||||||
$config = $this->getConfig();
|
|
||||||
|
|
||||||
// Check rate limit for the user.
|
|
||||||
$rateLimiter = $login->getRateLimiter('pw_resets');
|
|
||||||
$rateLimiter->registerRateLimitedAction($username);
|
|
||||||
if ($rateLimiter->isRateLimited($username)) {
|
|
||||||
Admin::DEBUG && Admin::addDebugMessage(sprintf('Failed sending email: user %s <%s> is rate limited', $search, $to));
|
|
||||||
|
|
||||||
$this->form->reset();
|
|
||||||
|
|
||||||
$interval = $config->get('plugins.login.max_pw_resets_interval', 2);
|
|
||||||
|
|
||||||
$this->setMessage($this->translate('PLUGIN_LOGIN.FORGOT_CANNOT_RESET_IT_IS_BLOCKED', $to, $interval), 'error');
|
|
||||||
|
|
||||||
return $this->createRedirectResponse($current);
|
|
||||||
}
|
|
||||||
|
|
||||||
$token = md5(uniqid(mt_rand(), true));
|
|
||||||
$expire = time() + 3600; // 1 hour
|
|
||||||
|
|
||||||
$user->set('reset', $token . '::' . $expire);
|
|
||||||
$user->save();
|
|
||||||
|
|
||||||
$from = $config->get('plugins.email.from');
|
|
||||||
if (empty($from)) {
|
|
||||||
Admin::DEBUG && Admin::addDebugMessage('Failed sending email: from address is not configured in email plugin');
|
|
||||||
|
|
||||||
$this->form->reset();
|
|
||||||
|
|
||||||
$this->setMessage($this->translate('PLUGIN_ADMIN.FORGOT_EMAIL_NOT_CONFIGURED'), 'error');
|
|
||||||
|
|
||||||
return $this->createRedirectResponse($current);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do not trust username from the request.
|
|
||||||
$fullname = $user->fullname ?: $username;
|
|
||||||
$author = $config->get('site.author.name', '');
|
|
||||||
$sitename = $config->get('site.title', 'Website');
|
|
||||||
$reset_route = "/reset/u/{$username}/{$token}";
|
|
||||||
|
|
||||||
$site_host = $config->get('plugins.login.site_host');
|
|
||||||
if (!empty($site_host)) {
|
|
||||||
$admin = $this->getAdmin();
|
|
||||||
$reset_link = rtrim($site_host, '/') . '/' . trim($admin->base, '/') . '/' . ltrim($reset_route, '/');
|
|
||||||
} else {
|
|
||||||
$reset_link = $this->getAbsoluteAdminUrl($reset_route);
|
|
||||||
}
|
|
||||||
|
|
||||||
// For testing only!
|
|
||||||
//Admin::DEBUG && Admin::addDebugMessage(sprintf('Reset link: %s', $reset_link));
|
|
||||||
|
|
||||||
$subject = $this->translate('PLUGIN_ADMIN.FORGOT_EMAIL_SUBJECT', $sitename);
|
|
||||||
$content = $this->translate('PLUGIN_ADMIN.FORGOT_EMAIL_BODY', $fullname, $reset_link, $author, $sitename);
|
|
||||||
|
|
||||||
$this->grav['twig']->init();
|
|
||||||
$body = $this->grav['twig']->processTemplate('email/base.html.twig', ['content' => $content]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
$message = $email->message($subject, $body, 'text/html')->setFrom($from)->setTo($to);
|
|
||||||
$sent = $email->send($message);
|
|
||||||
if ($sent < 1) {
|
|
||||||
throw new \RuntimeException('Sending email failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->setMessage($this->translate('PLUGIN_ADMIN.FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL'));
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$rateLimiter->resetRateLimit($username);
|
|
||||||
|
|
||||||
/** @var Debugger $debugger */
|
|
||||||
$debugger = $this->grav['debugger'];
|
|
||||||
$debugger->addException($e);
|
|
||||||
|
|
||||||
$this->form->reset();
|
|
||||||
|
|
||||||
$this->setMessage($this->translate('PLUGIN_ADMIN.FORGOT_FAILED_TO_EMAIL'), 'error');
|
|
||||||
|
|
||||||
return $this->createRedirectResponse('/forgot');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->form->reset();
|
|
||||||
|
|
||||||
return $this->createRedirectResponse('/login');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return ResponseInterface
|
|
||||||
*/
|
|
||||||
public function taskRegister(): ResponseInterface
|
|
||||||
{
|
|
||||||
$this->page = $this->createPage('register');
|
|
||||||
$this->form = $form = $this->getForm('admin-login-register');
|
|
||||||
try {
|
|
||||||
$this->checkNonce();
|
|
||||||
} catch (PageExpiredException $e) {
|
|
||||||
$this->setMessage($this->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'), 'error');
|
|
||||||
|
|
||||||
return $this->createDisplayResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: Calls $this->doRegistration() to perform the user registration.
|
|
||||||
$form->handleRequest($this->getRequest());
|
|
||||||
$error = $form->getError();
|
|
||||||
$errors = $form->getErrors();
|
|
||||||
if ($error || $errors) {
|
|
||||||
foreach ($errors as $field => $list) {
|
|
||||||
foreach ((array)$list as $message) {
|
|
||||||
if ($message !== $error) {
|
|
||||||
$this->setMessage($message, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->createDisplayResponse();
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->setMessage($this->translate('PLUGIN_ADMIN.LOGIN_LOGGED_IN'));
|
|
||||||
|
|
||||||
return $this->createRedirectResponse('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param UserInterface $user
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
protected function is2FA(UserInterface $user): bool
|
|
||||||
{
|
|
||||||
return $user && $user->authenticated && !$user->authorized && $user->get('twofa_enabled');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $name
|
|
||||||
* @return callable
|
|
||||||
*/
|
|
||||||
protected function getFormSubmitMethod(string $name): callable
|
|
||||||
{
|
|
||||||
switch ($name) {
|
|
||||||
case 'login':
|
|
||||||
case 'login-twofa':
|
|
||||||
case 'admin-login-forgot':
|
|
||||||
case 'admin-login-reset':
|
|
||||||
return static function(array $data, array $files) {};
|
|
||||||
case 'admin-login-register':
|
|
||||||
return function(array $data, array $files) {
|
|
||||||
$this->doRegistration($data, $files);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new \RuntimeException('Unknown form');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called by registration form when calling handleRequest().
|
|
||||||
*
|
|
||||||
* @param array $data
|
|
||||||
* @param array $files
|
|
||||||
*/
|
|
||||||
private function doRegistration(array $data, array $files): void
|
|
||||||
{
|
|
||||||
if (Admin::doAnyUsersExist()) {
|
|
||||||
throw new \RuntimeException('A user account already exists, please create an admin account manually.', 400);
|
|
||||||
}
|
|
||||||
|
|
||||||
$login = $this->getLogin();
|
|
||||||
if (!$login) {
|
|
||||||
throw new \RuntimeException($this->grav['language']->translate('PLUGIN_LOGIN.PLUGIN_LOGIN_DISABLED', 500));
|
|
||||||
}
|
|
||||||
|
|
||||||
$data['title'] = $data['title'] ?? 'Administrator';
|
|
||||||
|
|
||||||
// Do not allow form to set the following fields (make super user):
|
|
||||||
$data['state'] = 'enabled';
|
|
||||||
$data['access'] = ['admin' => ['login' => true, 'super' => true], 'site' => ['login' => true]];
|
|
||||||
unset($data['groups']);
|
|
||||||
|
|
||||||
// Create user.
|
|
||||||
$user = $login->register($data, $files);
|
|
||||||
|
|
||||||
// Log in the new super admin user.
|
|
||||||
unset($this->grav['user']);
|
|
||||||
$this->grav['user'] = $user;
|
|
||||||
$this->grav['session']->user = $user;
|
|
||||||
$user->authenticated = true;
|
|
||||||
$user->authorized = $user->authorize('admin.login') ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Login
|
|
||||||
*/
|
|
||||||
private function getLogin(): Login
|
|
||||||
{
|
|
||||||
return $this->grav['login'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return Email
|
|
||||||
*/
|
|
||||||
private function getEmail(): Email
|
|
||||||
{
|
|
||||||
return $this->grav['Email'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return UserCollectionInterface
|
|
||||||
*/
|
|
||||||
private function getAccounts(): UserCollectionInterface
|
|
||||||
{
|
|
||||||
return $this->grav['accounts'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,461 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package Grav\Plugin\Admin
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
|
||||||
* @license MIT License; see LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Grav\Plugin\Admin;
|
|
||||||
|
|
||||||
use Grav\Common\Cache;
|
|
||||||
use Grav\Common\Grav;
|
|
||||||
use Grav\Common\GPM\GPM as GravGPM;
|
|
||||||
use Grav\Common\GPM\Licenses;
|
|
||||||
use Grav\Common\GPM\Installer;
|
|
||||||
use Grav\Common\GPM\Upgrader;
|
|
||||||
use Grav\Common\HTTP\Response;
|
|
||||||
use Grav\Common\Filesystem\Folder;
|
|
||||||
use Grav\Common\GPM\Common\Package;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class Gpm
|
|
||||||
*
|
|
||||||
* @package Grav\Plugin\Admin
|
|
||||||
*/
|
|
||||||
class Gpm
|
|
||||||
{
|
|
||||||
// Probably should move this to Grav DI container?
|
|
||||||
/** @var GravGPM */
|
|
||||||
protected static $GPM;
|
|
||||||
|
|
||||||
public static function GPM()
|
|
||||||
{
|
|
||||||
if (!static::$GPM) {
|
|
||||||
static::$GPM = new GravGPM();
|
|
||||||
}
|
|
||||||
|
|
||||||
return static::$GPM;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default options for the install
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected static $options = [
|
|
||||||
'destination' => GRAV_ROOT,
|
|
||||||
'overwrite' => true,
|
|
||||||
'ignore_symlinks' => true,
|
|
||||||
'skip_invalid' => true,
|
|
||||||
'install_deps' => true,
|
|
||||||
'theme' => false
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Package[]|string[]|string $packages
|
|
||||||
* @param array $options
|
|
||||||
*
|
|
||||||
* @return string|bool
|
|
||||||
*/
|
|
||||||
public static function install($packages, array $options)
|
|
||||||
{
|
|
||||||
$options = array_merge(self::$options, $options);
|
|
||||||
|
|
||||||
if (!Installer::isGravInstance($options['destination']) || !Installer::isValidDestination($options['destination'],
|
|
||||||
[Installer::EXISTS, Installer::IS_LINK])
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$packages = is_array($packages) ? $packages : [$packages];
|
|
||||||
$count = count($packages);
|
|
||||||
|
|
||||||
$packages = array_filter(array_map(function ($p) {
|
|
||||||
return !is_string($p) ? $p instanceof Package ? $p : false : self::GPM()->findPackage($p);
|
|
||||||
}, $packages));
|
|
||||||
|
|
||||||
if (!$options['skip_invalid'] && $count !== count($packages)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$messages = '';
|
|
||||||
|
|
||||||
foreach ($packages as $package) {
|
|
||||||
if (isset($package->dependencies) && $options['install_deps']) {
|
|
||||||
$result = static::install($package->dependencies, $options);
|
|
||||||
|
|
||||||
if (!$result) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check destination
|
|
||||||
Installer::isValidDestination($options['destination'] . DS . $package->install_path);
|
|
||||||
|
|
||||||
if (!$options['overwrite'] && Installer::lastErrorCode() === Installer::EXISTS) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$options['ignore_symlinks'] && Installer::lastErrorCode() === Installer::IS_LINK) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$license = Licenses::get($package->slug);
|
|
||||||
$local = static::download($package, $license);
|
|
||||||
|
|
||||||
Installer::install($local, $options['destination'],
|
|
||||||
['install_path' => $package->install_path, 'theme' => $options['theme']]);
|
|
||||||
Folder::delete(dirname($local));
|
|
||||||
|
|
||||||
$errorCode = Installer::lastErrorCode();
|
|
||||||
if ($errorCode) {
|
|
||||||
$msg = Installer::lastErrorMsg();
|
|
||||||
throw new \RuntimeException($msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count($packages) === 1) {
|
|
||||||
$message = Installer::getMessage();
|
|
||||||
if ($message) {
|
|
||||||
return $message;
|
|
||||||
}
|
|
||||||
|
|
||||||
$messages .= $message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Cache::clearCache();
|
|
||||||
|
|
||||||
return $messages ?: true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Package[]|string[]|string $packages
|
|
||||||
* @param array $options
|
|
||||||
*
|
|
||||||
* @return string|bool
|
|
||||||
*/
|
|
||||||
public static function update($packages, array $options)
|
|
||||||
{
|
|
||||||
$options['overwrite'] = true;
|
|
||||||
|
|
||||||
return static::install($packages, $options);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Package[]|string[]|string $packages
|
|
||||||
* @param array $options
|
|
||||||
*
|
|
||||||
* @return string|bool
|
|
||||||
*/
|
|
||||||
public static function uninstall($packages, array $options)
|
|
||||||
{
|
|
||||||
$options = array_merge(self::$options, $options);
|
|
||||||
|
|
||||||
$packages = (array)$packages;
|
|
||||||
$count = count($packages);
|
|
||||||
|
|
||||||
$packages = array_filter(array_map(function ($p) {
|
|
||||||
|
|
||||||
if (is_string($p)) {
|
|
||||||
$p = strtolower($p);
|
|
||||||
$plugin = static::GPM()->getInstalledPlugin($p);
|
|
||||||
$p = $plugin ?: static::GPM()->getInstalledTheme($p);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $p instanceof Package ? $p : false;
|
|
||||||
|
|
||||||
}, $packages));
|
|
||||||
|
|
||||||
if (!$options['skip_invalid'] && $count !== count($packages)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($packages as $package) {
|
|
||||||
|
|
||||||
$location = Grav::instance()['locator']->findResource($package->package_type . '://' . $package->slug);
|
|
||||||
|
|
||||||
// Check destination
|
|
||||||
Installer::isValidDestination($location);
|
|
||||||
|
|
||||||
if (!$options['ignore_symlinks'] && Installer::lastErrorCode() === Installer::IS_LINK) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Installer::uninstall($location);
|
|
||||||
|
|
||||||
$errorCode = Installer::lastErrorCode();
|
|
||||||
if ($errorCode && $errorCode !== Installer::IS_LINK && $errorCode !== Installer::EXISTS) {
|
|
||||||
$msg = Installer::lastErrorMsg();
|
|
||||||
throw new \RuntimeException($msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (count($packages) === 1) {
|
|
||||||
$message = Installer::getMessage();
|
|
||||||
if ($message) {
|
|
||||||
return $message;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Cache::clearCache();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Direct install a file
|
|
||||||
*
|
|
||||||
* @param string $package_file
|
|
||||||
*
|
|
||||||
* @return string|bool
|
|
||||||
*/
|
|
||||||
public static function directInstall($package_file)
|
|
||||||
{
|
|
||||||
if (!$package_file) {
|
|
||||||
return Admin::translate('PLUGIN_ADMIN.NO_PACKAGE_NAME');
|
|
||||||
}
|
|
||||||
|
|
||||||
$tmp_dir = Grav::instance()['locator']->findResource('tmp://', true, true);
|
|
||||||
$tmp_zip = $tmp_dir . '/Grav-' . uniqid('', false);
|
|
||||||
|
|
||||||
if (Response::isRemote($package_file)) {
|
|
||||||
$zip = GravGPM::downloadPackage($package_file, $tmp_zip);
|
|
||||||
} else {
|
|
||||||
$zip = GravGPM::copyPackage($package_file, $tmp_zip);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (file_exists($zip)) {
|
|
||||||
$tmp_source = $tmp_dir . '/Grav-' . uniqid('', false);
|
|
||||||
$extracted = Installer::unZip($zip, $tmp_source);
|
|
||||||
|
|
||||||
if (!$extracted) {
|
|
||||||
Folder::delete($tmp_source);
|
|
||||||
Folder::delete($tmp_zip);
|
|
||||||
return Admin::translate('PLUGIN_ADMIN.PACKAGE_EXTRACTION_FAILED');
|
|
||||||
}
|
|
||||||
|
|
||||||
$type = GravGPM::getPackageType($extracted);
|
|
||||||
|
|
||||||
if (!$type) {
|
|
||||||
Folder::delete($tmp_source);
|
|
||||||
Folder::delete($tmp_zip);
|
|
||||||
return Admin::translate('PLUGIN_ADMIN.NOT_VALID_GRAV_PACKAGE');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($type === 'grav') {
|
|
||||||
Installer::isValidDestination(GRAV_ROOT . '/system');
|
|
||||||
if (Installer::IS_LINK === Installer::lastErrorCode()) {
|
|
||||||
Folder::delete($tmp_source);
|
|
||||||
Folder::delete($tmp_zip);
|
|
||||||
return Admin::translate('PLUGIN_ADMIN.CANNOT_OVERWRITE_SYMLINKS');
|
|
||||||
}
|
|
||||||
|
|
||||||
static::upgradeGrav($zip, $extracted);
|
|
||||||
} else {
|
|
||||||
$name = GravGPM::getPackageName($extracted);
|
|
||||||
|
|
||||||
if (!$name) {
|
|
||||||
Folder::delete($tmp_source);
|
|
||||||
Folder::delete($tmp_zip);
|
|
||||||
return Admin::translate('PLUGIN_ADMIN.NAME_COULD_NOT_BE_DETERMINED');
|
|
||||||
}
|
|
||||||
|
|
||||||
$install_path = GravGPM::getInstallPath($type, $name);
|
|
||||||
$is_update = file_exists($install_path);
|
|
||||||
|
|
||||||
Installer::isValidDestination(GRAV_ROOT . DS . $install_path);
|
|
||||||
if (Installer::lastErrorCode() === Installer::IS_LINK) {
|
|
||||||
Folder::delete($tmp_source);
|
|
||||||
Folder::delete($tmp_zip);
|
|
||||||
return Admin::translate('PLUGIN_ADMIN.CANNOT_OVERWRITE_SYMLINKS');
|
|
||||||
}
|
|
||||||
|
|
||||||
Installer::install($zip, GRAV_ROOT,
|
|
||||||
['install_path' => $install_path, 'theme' => $type === 'theme', 'is_update' => $is_update],
|
|
||||||
$extracted);
|
|
||||||
}
|
|
||||||
|
|
||||||
Folder::delete($tmp_source);
|
|
||||||
|
|
||||||
if (Installer::lastErrorCode()) {
|
|
||||||
return Installer::lastErrorMsg();
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
return Admin::translate('PLUGIN_ADMIN.ZIP_PACKAGE_NOT_FOUND');
|
|
||||||
}
|
|
||||||
|
|
||||||
Folder::delete($tmp_zip);
|
|
||||||
Cache::clearCache();
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Package $package
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private static function download(Package $package, $license = null)
|
|
||||||
{
|
|
||||||
$query = '';
|
|
||||||
|
|
||||||
if ($package->premium) {
|
|
||||||
$query = \json_encode(array_merge($package->premium, [
|
|
||||||
'slug' => $package->slug,
|
|
||||||
'license_key' => $license,
|
|
||||||
'sid' => md5(GRAV_ROOT)
|
|
||||||
]));
|
|
||||||
|
|
||||||
$query = '?d=' . base64_encode($query);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$contents = Response::get($package->zipball_url . $query, []);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
throw new \RuntimeException($e->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
$tmp_dir = Admin::getTempDir() . '/Grav-' . uniqid('', false);
|
|
||||||
Folder::mkdir($tmp_dir);
|
|
||||||
|
|
||||||
$bad_chars = array_merge(array_map('chr', range(0, 31)), ['<', '>', ':', '"', '/', '\\', '|', '?', '*']);
|
|
||||||
|
|
||||||
$filename = $package->slug . str_replace($bad_chars, '', \Grav\Common\Utils::basename($package->zipball_url));
|
|
||||||
$filename = preg_replace('/[\\\\\/:"*?&<>|]+/m', '-', $filename);
|
|
||||||
|
|
||||||
file_put_contents($tmp_dir . DS . $filename . '.zip', $contents);
|
|
||||||
|
|
||||||
return $tmp_dir . DS . $filename . '.zip';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array $package
|
|
||||||
* @param string $tmp
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
private static function _downloadSelfupgrade(array $package, $tmp)
|
|
||||||
{
|
|
||||||
$output = Response::get($package['download'], []);
|
|
||||||
Folder::mkdir($tmp);
|
|
||||||
file_put_contents($tmp . DS . $package['name'], $output);
|
|
||||||
|
|
||||||
return $tmp . DS . $package['name'];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public static function selfupgrade()
|
|
||||||
{
|
|
||||||
$upgrader = new Upgrader();
|
|
||||||
|
|
||||||
if (!Installer::isGravInstance(GRAV_ROOT)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_link(GRAV_ROOT . DS . 'index.php')) {
|
|
||||||
Installer::setError(Installer::IS_LINK);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (method_exists($upgrader, 'meetsRequirements') &&
|
|
||||||
method_exists($upgrader, 'minPHPVersion') &&
|
|
||||||
!$upgrader->meetsRequirements()) {
|
|
||||||
$error = [];
|
|
||||||
$error[] = '<p>Grav has increased the minimum PHP requirement.<br />';
|
|
||||||
$error[] = 'You are currently running PHP <strong>' . phpversion() . '</strong>';
|
|
||||||
$error[] = ', but PHP <strong>' . $upgrader->minPHPVersion() . '</strong> is required.</p>';
|
|
||||||
$error[] = '<p><a href="https://getgrav.org/blog/changing-php-requirements-to-5.5" class="button button-small secondary">Additional information</a></p>';
|
|
||||||
|
|
||||||
Installer::setError(implode("\n", $error));
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$update = $upgrader->getAssets()['grav-update'];
|
|
||||||
$tmp = Admin::getTempDir() . '/Grav-' . uniqid('', false);
|
|
||||||
if ($tmp) {
|
|
||||||
$file = self::_downloadSelfupgrade($update, $tmp);
|
|
||||||
$folder = Installer::unZip($file, $tmp . '/zip');
|
|
||||||
$keepFolder = false;
|
|
||||||
} else {
|
|
||||||
// If you make $tmp empty, you can install your local copy of Grav (for testing purposes only).
|
|
||||||
$file = 'grav.zip';
|
|
||||||
$folder = '~/phpstorm/grav-clones/grav';
|
|
||||||
//$folder = '/home/matias/phpstorm/rockettheme/grav-devtools/grav-clones/grav';
|
|
||||||
$keepFolder = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static::upgradeGrav($file, $folder, $keepFolder);
|
|
||||||
|
|
||||||
$errorCode = Installer::lastErrorCode();
|
|
||||||
|
|
||||||
if ($tmp) {
|
|
||||||
Folder::delete($tmp);
|
|
||||||
}
|
|
||||||
|
|
||||||
return !(is_string($errorCode) || ($errorCode & (Installer::ZIP_OPEN_ERROR | Installer::ZIP_EXTRACT_ERROR)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private static function upgradeGrav($zip, $folder, $keepFolder = false)
|
|
||||||
{
|
|
||||||
static $ignores = [
|
|
||||||
'backup',
|
|
||||||
'cache',
|
|
||||||
'images',
|
|
||||||
'logs',
|
|
||||||
'tmp',
|
|
||||||
'user',
|
|
||||||
'.htaccess',
|
|
||||||
'robots.txt'
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!is_dir($folder)) {
|
|
||||||
Installer::setError('Invalid source folder');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$script = $folder . '/system/install.php';
|
|
||||||
/** Install $installer */
|
|
||||||
if ((file_exists($script) && $install = include $script) && is_callable($install)) {
|
|
||||||
// Run preflight from the NEW package's installer if available
|
|
||||||
if (is_object($install) && method_exists($install, 'generatePreflightReport')) {
|
|
||||||
$report = $install->generatePreflightReport();
|
|
||||||
$blocking = $report['blocking'] ?? [];
|
|
||||||
$incompatible = $report['incompatible_packages'] ?? [];
|
|
||||||
|
|
||||||
if (!empty($blocking) || !empty($incompatible['blocking'])) {
|
|
||||||
$errors = [];
|
|
||||||
foreach ($blocking as $reason) {
|
|
||||||
$errors[] = $reason;
|
|
||||||
}
|
|
||||||
foreach ($incompatible['blocking'] ?? [] as $slug => $info) {
|
|
||||||
$errors[] = sprintf('<strong>%s</strong> (%s v%s) — not compatible with Grav %s. Disable it to proceed.',
|
|
||||||
$slug, $info['type'] ?? 'plugin', $info['version'] ?? '?', $incompatible['target'] ?? '?');
|
|
||||||
}
|
|
||||||
Installer::setError(implode('<br>', $errors));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$install($zip);
|
|
||||||
} else {
|
|
||||||
Installer::install(
|
|
||||||
$zip,
|
|
||||||
GRAV_ROOT,
|
|
||||||
['sophisticated' => true, 'overwrite' => true, 'ignore_symlinks' => true, 'ignores' => $ignores],
|
|
||||||
$folder,
|
|
||||||
$keepFolder
|
|
||||||
);
|
|
||||||
|
|
||||||
Cache::clearCache();
|
|
||||||
}
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
Installer::setError($e->getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,310 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package Grav\Plugin\Admin
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
|
||||||
* @license MIT License; see LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Grav\Plugin\Admin;
|
|
||||||
|
|
||||||
use Grav\Common\Config\Config;
|
|
||||||
use Grav\Common\Grav;
|
|
||||||
use Grav\Common\Page\Interfaces\PageInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class Popularity
|
|
||||||
* @package Grav\Plugin
|
|
||||||
*/
|
|
||||||
class Popularity
|
|
||||||
{
|
|
||||||
/** @var Config */
|
|
||||||
protected $config;
|
|
||||||
protected $data_path;
|
|
||||||
|
|
||||||
protected $daily_file;
|
|
||||||
protected $monthly_file;
|
|
||||||
protected $totals_file;
|
|
||||||
protected $visitors_file;
|
|
||||||
|
|
||||||
protected $daily_data;
|
|
||||||
protected $monthly_data;
|
|
||||||
protected $totals_data;
|
|
||||||
protected $visitors_data;
|
|
||||||
|
|
||||||
const DAILY_FORMAT = 'd-m-Y';
|
|
||||||
const MONTHLY_FORMAT = 'm-Y';
|
|
||||||
const DAILY_FILE = 'daily.json';
|
|
||||||
const MONTHLY_FILE = 'monthly.json';
|
|
||||||
const TOTALS_FILE = 'totals.json';
|
|
||||||
const VISITORS_FILE = 'visitors.json';
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->config = Grav::instance()['config'];
|
|
||||||
|
|
||||||
$this->data_path = Grav::instance()['locator']->findResource('log://popularity', true, true);
|
|
||||||
$this->daily_file = $this->data_path . '/' . self::DAILY_FILE;
|
|
||||||
$this->monthly_file = $this->data_path . '/' . self::MONTHLY_FILE;
|
|
||||||
$this->totals_file = $this->data_path . '/' . self::TOTALS_FILE;
|
|
||||||
$this->visitors_file = $this->data_path . '/' . self::VISITORS_FILE;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public function trackHit()
|
|
||||||
{
|
|
||||||
// Don't track bot or crawler requests
|
|
||||||
if (!Grav::instance()['browser']->isHuman()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Respect visitors "do not track" setting
|
|
||||||
if (!Grav::instance()['browser']->isTrackable()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var PageInterface $page */
|
|
||||||
$page = Grav::instance()['page'];
|
|
||||||
$relative_url = str_replace(Grav::instance()['base_url_relative'], '', $page->url());
|
|
||||||
|
|
||||||
// Don't track error pages or pages that have no route
|
|
||||||
if ($page->template() === 'error' || !$page->route()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure no 'widcard-style' ignore matches this url
|
|
||||||
foreach ((array)$this->config->get('plugins.admin.popularity.ignore') as $ignore) {
|
|
||||||
if (fnmatch($ignore, $relative_url)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// initial creation if it doesn't exist
|
|
||||||
if (!file_exists($this->data_path)) {
|
|
||||||
mkdir($this->data_path);
|
|
||||||
$this->flushPopularity();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the data we want to track
|
|
||||||
$this->updateDaily();
|
|
||||||
$this->updateMonthly();
|
|
||||||
$this->updateTotals($page->route());
|
|
||||||
$this->updateVisitors(Grav::instance()['uri']->ip());
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function updateDaily()
|
|
||||||
{
|
|
||||||
|
|
||||||
if (!$this->daily_data) {
|
|
||||||
$this->daily_data = $this->getData($this->daily_file);
|
|
||||||
}
|
|
||||||
|
|
||||||
$day_month_year = date(self::DAILY_FORMAT);
|
|
||||||
|
|
||||||
// get the daily access count
|
|
||||||
if (array_key_exists($day_month_year, $this->daily_data)) {
|
|
||||||
$this->daily_data[$day_month_year] = (int)$this->daily_data[$day_month_year] + 1;
|
|
||||||
} else {
|
|
||||||
$this->daily_data[$day_month_year] = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// keep correct number as set by history
|
|
||||||
$count = (int)$this->config->get('plugins.admin.popularity.history.daily', 30);
|
|
||||||
$total = count($this->daily_data);
|
|
||||||
|
|
||||||
if ($total > $count) {
|
|
||||||
$this->daily_data = array_slice($this->daily_data, -$count, $count, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
file_put_contents($this->daily_file, json_encode($this->daily_data));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function getDailyChartData()
|
|
||||||
{
|
|
||||||
if (!$this->daily_data) {
|
|
||||||
$this->daily_data = $this->getData($this->daily_file);
|
|
||||||
}
|
|
||||||
|
|
||||||
$limit = (int)$this->config->get('plugins.admin.popularity.dashboard.days_of_stats', 7);
|
|
||||||
$chart_data = array_slice($this->daily_data, -$limit, $limit);
|
|
||||||
|
|
||||||
$labels = [];
|
|
||||||
$data = [];
|
|
||||||
|
|
||||||
/** @var Admin $admin */
|
|
||||||
$admin = Grav::instance()['admin'];
|
|
||||||
foreach ($chart_data as $date => $count) {
|
|
||||||
$labels[] = $admin::translate([
|
|
||||||
'PLUGIN_ADMIN.' . strtoupper(date('D', strtotime($date)))]) .
|
|
||||||
'<br>' . date('M d', strtotime($date));
|
|
||||||
$data[] = $count;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['labels' => $labels, 'data' => $data];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getDailyTotal()
|
|
||||||
{
|
|
||||||
if (!$this->daily_data) {
|
|
||||||
$this->daily_data = $this->getData($this->daily_file);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($this->daily_data[date(self::DAILY_FORMAT)])) {
|
|
||||||
return $this->daily_data[date(self::DAILY_FORMAT)];
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getWeeklyTotal()
|
|
||||||
{
|
|
||||||
if (!$this->daily_data) {
|
|
||||||
$this->daily_data = $this->getData($this->daily_file);
|
|
||||||
}
|
|
||||||
|
|
||||||
$day = 0;
|
|
||||||
$total = 0;
|
|
||||||
foreach (array_reverse($this->daily_data) as $daily) {
|
|
||||||
$total += $daily;
|
|
||||||
$day++;
|
|
||||||
if ($day === 7) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $total;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return int
|
|
||||||
*/
|
|
||||||
public function getMonthlyTotal()
|
|
||||||
{
|
|
||||||
if (!$this->monthly_data) {
|
|
||||||
$this->monthly_data = $this->getData($this->monthly_file);
|
|
||||||
}
|
|
||||||
if (isset($this->monthly_data[date(self::MONTHLY_FORMAT)])) {
|
|
||||||
return $this->monthly_data[date(self::MONTHLY_FORMAT)];
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function updateMonthly()
|
|
||||||
{
|
|
||||||
|
|
||||||
if (!$this->monthly_data) {
|
|
||||||
$this->monthly_data = $this->getData($this->monthly_file);
|
|
||||||
}
|
|
||||||
|
|
||||||
$month_year = date(self::MONTHLY_FORMAT);
|
|
||||||
|
|
||||||
// get the monthly access count
|
|
||||||
if (array_key_exists($month_year, $this->monthly_data)) {
|
|
||||||
$this->monthly_data[$month_year] = (int)$this->monthly_data[$month_year] + 1;
|
|
||||||
} else {
|
|
||||||
$this->monthly_data[$month_year] = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// keep correct number as set by history
|
|
||||||
$count = (int)$this->config->get('plugins.admin.popularity.history.monthly', 12);
|
|
||||||
$total = count($this->monthly_data);
|
|
||||||
$this->monthly_data = array_slice($this->monthly_data, $total - $count, $count);
|
|
||||||
|
|
||||||
|
|
||||||
file_put_contents($this->monthly_file, json_encode($this->monthly_data));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
protected function getMonthyChartData()
|
|
||||||
{
|
|
||||||
if (!$this->monthly_data) {
|
|
||||||
$this->monthly_data = $this->getData($this->monthly_file);
|
|
||||||
}
|
|
||||||
|
|
||||||
$labels = [];
|
|
||||||
$data = [];
|
|
||||||
|
|
||||||
foreach ($this->monthly_data as $date => $count) {
|
|
||||||
$labels[] = date('M', strtotime($date));
|
|
||||||
$data[] = $count;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ['labels' => $labels, 'data' => $data];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $url
|
|
||||||
*/
|
|
||||||
protected function updateTotals($url)
|
|
||||||
{
|
|
||||||
if (!$this->totals_data) {
|
|
||||||
$this->totals_data = $this->getData($this->totals_file);
|
|
||||||
}
|
|
||||||
|
|
||||||
// get the totals for this url
|
|
||||||
if (array_key_exists($url, $this->totals_data)) {
|
|
||||||
$this->totals_data[$url] = (int)$this->totals_data[$url] + 1;
|
|
||||||
} else {
|
|
||||||
$this->totals_data[$url] = 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
file_put_contents($this->totals_file, json_encode($this->totals_data));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $ip
|
|
||||||
*/
|
|
||||||
protected function updateVisitors($ip)
|
|
||||||
{
|
|
||||||
if (!$this->visitors_data) {
|
|
||||||
$this->visitors_data = $this->getData($this->visitors_file);
|
|
||||||
}
|
|
||||||
|
|
||||||
// update with current timestamp
|
|
||||||
$this->visitors_data[hash('sha1', $ip)] = time();
|
|
||||||
$visitors = $this->visitors_data;
|
|
||||||
arsort($visitors);
|
|
||||||
|
|
||||||
$count = (int)$this->config->get('plugins.admin.popularity.history.visitors', 20);
|
|
||||||
$this->visitors_data = array_slice($visitors, 0, $count, true);
|
|
||||||
|
|
||||||
file_put_contents($this->visitors_file, json_encode($this->visitors_data));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $path
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
protected function getData($path)
|
|
||||||
{
|
|
||||||
if (file_exists($path)) {
|
|
||||||
return (array)json_decode(file_get_contents($path), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public function flushPopularity()
|
|
||||||
{
|
|
||||||
file_put_contents($this->daily_file, []);
|
|
||||||
file_put_contents($this->monthly_file, []);
|
|
||||||
file_put_contents($this->totals_file, []);
|
|
||||||
file_put_contents($this->visitors_file, []);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package Grav\Plugin\Admin
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
|
||||||
* @license MIT License; see LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Grav\Plugin\Admin;
|
|
||||||
|
|
||||||
use Grav\Common\Grav;
|
|
||||||
use Grav\Common\Processors\ProcessorBase;
|
|
||||||
use Grav\Framework\Route\Route;
|
|
||||||
use Grav\Plugin\Admin\Routers\LoginRouter;
|
|
||||||
use Psr\Http\Message\ResponseInterface;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
|
||||||
|
|
||||||
class Router extends ProcessorBase
|
|
||||||
{
|
|
||||||
public $id = 'admin_router';
|
|
||||||
public $title = 'Admin Panel';
|
|
||||||
|
|
||||||
/** @var Admin */
|
|
||||||
protected $admin;
|
|
||||||
|
|
||||||
public function __construct(Grav $container, Admin $admin)
|
|
||||||
{
|
|
||||||
parent::__construct($container);
|
|
||||||
|
|
||||||
$this->admin = $admin;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle routing to the dashboard, group and build objects.
|
|
||||||
*
|
|
||||||
* @param ServerRequestInterface $request
|
|
||||||
* @param RequestHandlerInterface $handler
|
|
||||||
* @return ResponseInterface
|
|
||||||
*/
|
|
||||||
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler) : ResponseInterface
|
|
||||||
{
|
|
||||||
$this->startTimer();
|
|
||||||
|
|
||||||
$context = $request->getAttributes();
|
|
||||||
$query = $request->getQueryParams();
|
|
||||||
|
|
||||||
/** @var Route $route */
|
|
||||||
$route = $context['route'];
|
|
||||||
$normalized = mb_strtolower(trim($route->getRoute(), '/'));
|
|
||||||
$parts = explode('/', $normalized);
|
|
||||||
array_shift($parts); // Admin path
|
|
||||||
$routeStr = implode('/', $parts);
|
|
||||||
$view = array_shift($parts);
|
|
||||||
$path = implode('/', $parts);
|
|
||||||
$task = $this->container['task'] ?? $query['task'] ?? null;
|
|
||||||
$action = $this->container['action'] ?? $query['action'] ?? null;
|
|
||||||
|
|
||||||
$params = ['view' => $view, 'route' => $routeStr, 'path' => $path, 'parts' => $parts, 'task' => $task, 'action' => $action];
|
|
||||||
$request = $request->withAttribute('admin', $params);
|
|
||||||
|
|
||||||
// Run login controller if user isn't fully logged in or asks to logout.
|
|
||||||
$user = $this->admin->user;
|
|
||||||
if (!$user->authorized || !$user->authorize('admin.login')) {
|
|
||||||
$params = (new LoginRouter())->matchServerRequest($request);
|
|
||||||
$request = $request->withAttribute('admin', $params + $request->getAttribute('admin'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->admin->request = $request;
|
|
||||||
|
|
||||||
$response = $handler->handle($request);
|
|
||||||
|
|
||||||
$this->stopTimer();
|
|
||||||
|
|
||||||
// Never allow admin pages to be rendered in <frame>, <iframe>, <embed> or <object> for improved security.
|
|
||||||
return $response->withHeader('X-Frame-Options', 'DENY');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package Grav\Plugin\Admin
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
|
||||||
* @license MIT License; see LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Grav\Plugin\Admin\Routers;
|
|
||||||
|
|
||||||
use Grav\Plugin\Admin\Admin;
|
|
||||||
use Grav\Plugin\Admin\Controllers\Login\LoginController;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
|
||||||
|
|
||||||
class LoginRouter
|
|
||||||
{
|
|
||||||
/** @var string[] */
|
|
||||||
private $taskTemplates = [
|
|
||||||
'logout' => 'login',
|
|
||||||
'twofa' => 'login',
|
|
||||||
'forgot' => 'forgot',
|
|
||||||
'reset' => 'reset'
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param ServerRequestInterface $request
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function matchServerRequest(ServerRequestInterface $request): array
|
|
||||||
{
|
|
||||||
$adminInfo = $request->getAttribute('admin');
|
|
||||||
$task = $adminInfo['task'];
|
|
||||||
$class = LoginController::class;
|
|
||||||
|
|
||||||
// Special controller for the new sites.
|
|
||||||
if (!Admin::doAnyUsersExist()) {
|
|
||||||
$method = $task === 'register' ? 'taskRegister' : 'displayRegister';
|
|
||||||
|
|
||||||
return [
|
|
||||||
'controller' => [
|
|
||||||
'class' => $class,
|
|
||||||
'method' => $method,
|
|
||||||
'params' => []
|
|
||||||
],
|
|
||||||
'template' => 'register',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$httpMethod = $request->getMethod();
|
|
||||||
$template = $this->taskTemplates[$task] ?? $adminInfo['view'];
|
|
||||||
$params = [];
|
|
||||||
|
|
||||||
switch ($template) {
|
|
||||||
case 'forgot':
|
|
||||||
break;
|
|
||||||
case 'reset':
|
|
||||||
$path = $adminInfo['path'];
|
|
||||||
if (str_starts_with($path, 'u/')) {
|
|
||||||
// Path is 'u/username/token'
|
|
||||||
$parts = explode('/', $path, 4);
|
|
||||||
$user = $parts[1] ?? null;
|
|
||||||
$token = $parts[2] ?? null;
|
|
||||||
} else {
|
|
||||||
// Old path used to be 'task:reset/user:username/token:token'
|
|
||||||
if ($httpMethod === 'GET' || $httpMethod === 'HEAD') {
|
|
||||||
$task = null;
|
|
||||||
}
|
|
||||||
$route = $request->getAttribute('route');
|
|
||||||
$user = $route->getGravParam('user');
|
|
||||||
$token = $route->getGravParam('token');
|
|
||||||
}
|
|
||||||
$params = [$user, $token];
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
$template = 'login';
|
|
||||||
}
|
|
||||||
|
|
||||||
$method = ($task ? 'task' : 'display') . ucfirst($task ?? $template);
|
|
||||||
if (!method_exists($class, $method)) {
|
|
||||||
$method = 'displayUnauthorized';
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'controller' => [
|
|
||||||
'class' => $class,
|
|
||||||
'method' => $method,
|
|
||||||
'params' => $params
|
|
||||||
],
|
|
||||||
'template' => $template,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package Grav\Plugin\Admin
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
|
||||||
* @license MIT License; see LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Grav\Plugin\Admin;
|
|
||||||
|
|
||||||
use ScssPhp\ScssPhp\Compiler;
|
|
||||||
use ScssPhp\ScssPhp\ValueConverter;
|
|
||||||
|
|
||||||
class ScssCompiler
|
|
||||||
{
|
|
||||||
protected $compiler;
|
|
||||||
|
|
||||||
public function compiler()
|
|
||||||
{
|
|
||||||
if ($this->compiler === null) {
|
|
||||||
$this->reset();
|
|
||||||
}
|
|
||||||
return $this->compiler;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function reset()
|
|
||||||
{
|
|
||||||
$this->compiler = new Compiler();
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setVariables(array $variables)
|
|
||||||
{
|
|
||||||
// $parsed = ValueConverter::fromPhp($variables);
|
|
||||||
$parsed = [];
|
|
||||||
foreach ($variables as $key => $value) {
|
|
||||||
$parsed[$key] = ValueConverter::parseValue($value);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->compiler()->addVariables($parsed);
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function setImportPaths(array $paths)
|
|
||||||
{
|
|
||||||
$this->compiler()->setImportPaths($paths);
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function compile(string $input_file, string $output_file)
|
|
||||||
{
|
|
||||||
$input = file_get_contents($input_file);
|
|
||||||
$output = $this->compiler()->compile($input);
|
|
||||||
file_put_contents($output_file, $output);
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function compileAll(array $input_paths, string $output_file)
|
|
||||||
{
|
|
||||||
$input = '';
|
|
||||||
foreach ($input_paths as $input_file) {
|
|
||||||
$input .= trim(file_get_contents($input_file)) . "\n\n";
|
|
||||||
}
|
|
||||||
$output = $this->compiler()->compileString($input)->getCss();
|
|
||||||
file_put_contents($output_file, $output);
|
|
||||||
return $this;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package Grav\Plugin\Admin
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
|
||||||
* @license MIT License; see LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Grav\Plugin\Admin;
|
|
||||||
|
|
||||||
class ScssList
|
|
||||||
{
|
|
||||||
/** @var string[] */
|
|
||||||
protected $list = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ScssList constructor.
|
|
||||||
* @param string|null $item
|
|
||||||
*/
|
|
||||||
public function __construct($item = null)
|
|
||||||
{
|
|
||||||
if ($item) {
|
|
||||||
$this->add($item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function all(): array
|
|
||||||
{
|
|
||||||
return $this->list;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $item
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function add($item): void
|
|
||||||
{
|
|
||||||
if ($item) {
|
|
||||||
$this->list[] = $item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $item
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function remove($item): void
|
|
||||||
{
|
|
||||||
$pos = array_search($item, $this->list, true);
|
|
||||||
if ($pos) {
|
|
||||||
unset($this->list[$pos]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package Grav\Plugin\Admin
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
|
||||||
* @license MIT License; see LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Grav\Plugin\Admin;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Admin theme object
|
|
||||||
*
|
|
||||||
* @author RocketTheme
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
class Themes extends \Grav\Common\Themes
|
|
||||||
{
|
|
||||||
public function init()
|
|
||||||
{
|
|
||||||
/** @var Themes $themes */
|
|
||||||
$themes = $this->grav['themes'];
|
|
||||||
$themes->configure();
|
|
||||||
$themes->initTheme();
|
|
||||||
|
|
||||||
$this->grav->fireEvent('onAdminThemeInitialized');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package Grav\Plugin\Admin
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
|
||||||
* @license MIT License; see LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Grav\Plugin\Admin\Twig;
|
|
||||||
|
|
||||||
use Grav\Common\Data\Data;
|
|
||||||
use Grav\Common\Grav;
|
|
||||||
use Grav\Common\Page\Interfaces\PageInterface;
|
|
||||||
use Grav\Common\Utils;
|
|
||||||
use Grav\Common\Yaml;
|
|
||||||
use Grav\Common\Language\Language;
|
|
||||||
use Twig\Extension\AbstractExtension;
|
|
||||||
use Twig\TwigFilter;
|
|
||||||
use Twig\TwigFunction;
|
|
||||||
use Grav\Plugin\Admin\Admin;
|
|
||||||
|
|
||||||
class AdminTwigExtension extends AbstractExtension
|
|
||||||
{
|
|
||||||
/** @var Grav */
|
|
||||||
protected $grav;
|
|
||||||
|
|
||||||
/** @var Language $lang */
|
|
||||||
protected $lang;
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->grav = Grav::instance();
|
|
||||||
$this->lang = $this->grav['user']->language;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getFilters(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
new TwigFilter('tu', [$this, 'tuFilter']),
|
|
||||||
new TwigFilter('toYaml', [$this, 'toYamlFilter']),
|
|
||||||
new TwigFilter('fromYaml', [$this, 'fromYamlFilter']),
|
|
||||||
new TwigFilter('adminNicetime', [$this, 'adminNicetimeFilter']),
|
|
||||||
new TwigFilter('nested', [$this, 'nestedFilter']),
|
|
||||||
new TwigFilter('flatten', [$this, 'flattenFilter']),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getFunctions(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
new TwigFunction('admin_route', [$this, 'adminRouteFunc']),
|
|
||||||
new TwigFunction('getPageUrl', [$this, 'getPageUrl']),
|
|
||||||
new TwigFunction('clone', [$this, 'cloneFunc']),
|
|
||||||
new TwigFunction('data', [$this, 'dataFunc']),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
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 flattenFilter($array)
|
|
||||||
{
|
|
||||||
return Utils::arrayFlattenDotNotation($array);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function cloneFunc($obj)
|
|
||||||
{
|
|
||||||
return clone $obj;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function adminRouteFunc(string $route = '', ?string $languageCode = null)
|
|
||||||
{
|
|
||||||
/** @var Admin $admin */
|
|
||||||
$admin = Grav::instance()['admin'];
|
|
||||||
|
|
||||||
return $admin->getAdminRoute($route, $languageCode)->toString(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPageUrl(PageInterface $page)
|
|
||||||
{
|
|
||||||
/** @var Admin $admin */
|
|
||||||
$admin = Grav::instance()['admin'];
|
|
||||||
|
|
||||||
return $admin->getAdminRoute('/pages' . $page->rawRoute(), $page->language())->toString(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function tuFilter()
|
|
||||||
{
|
|
||||||
$args = func_get_args();
|
|
||||||
$numargs = count($args);
|
|
||||||
$lang = null;
|
|
||||||
|
|
||||||
if (($numargs === 3 && is_array($args[1])) || ($numargs === 2 && !is_array($args[1]))) {
|
|
||||||
$lang = array_pop($args);
|
|
||||||
} elseif ($numargs === 2 && is_array($args[1])) {
|
|
||||||
$subs = array_pop($args);
|
|
||||||
$args = array_merge($args, $subs);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Grav::instance()['admin']->translate($args, $lang);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function toYamlFilter($value, $inline = null)
|
|
||||||
{
|
|
||||||
return Yaml::dump($value, $inline);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public function fromYamlFilter($value)
|
|
||||||
{
|
|
||||||
return Yaml::parse($value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function adminNicetimeFilter($date, $long_strings = true)
|
|
||||||
{
|
|
||||||
return Grav::instance()['admin']->adminNiceTime($date, $long_strings);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function dataFunc(array $data, $blueprints = null)
|
|
||||||
{
|
|
||||||
return new Data($data, $blueprints);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package Grav\Plugin\Admin
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
|
||||||
* @license MIT License; see LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Grav\Plugin\Admin;
|
|
||||||
|
|
||||||
use Grav\Common\Grav;
|
|
||||||
use Grav\Common\User\Interfaces\UserCollectionInterface;
|
|
||||||
use Grav\Common\User\Interfaces\UserInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Admin utils class
|
|
||||||
*
|
|
||||||
* @license MIT
|
|
||||||
*/
|
|
||||||
class Utils
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Matches an email to a user
|
|
||||||
*
|
|
||||||
* @param string $email
|
|
||||||
*
|
|
||||||
* @return UserInterface
|
|
||||||
*/
|
|
||||||
public static function findUserByEmail(string $email)
|
|
||||||
{
|
|
||||||
$grav = Grav::instance();
|
|
||||||
|
|
||||||
/** @var UserCollectionInterface $users */
|
|
||||||
$users = $grav['accounts'];
|
|
||||||
|
|
||||||
return $users->find($email, ['email']);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates a slug of the given string
|
|
||||||
*
|
|
||||||
* @param string $str
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public static function slug(string $str)
|
|
||||||
{
|
|
||||||
if (function_exists('transliterator_transliterate')) {
|
|
||||||
$str = transliterator_transliterate('Any-Latin; NFD; [:Nonspacing Mark:] Remove; NFC; [:Punctuation:] Remove;', $str);
|
|
||||||
} else {
|
|
||||||
$str = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $str);
|
|
||||||
}
|
|
||||||
|
|
||||||
$str = strtolower($str);
|
|
||||||
$str = preg_replace('/[-\s]+/', '-', $str);
|
|
||||||
$str = preg_replace('/[^a-z0-9-]/i', '', $str);
|
|
||||||
$str = trim($str, '-');
|
|
||||||
|
|
||||||
return $str;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
<?php
|
|
||||||
namespace Grav\Plugin\Admin;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @package Grav\Plugin\Admin
|
|
||||||
*
|
|
||||||
* @copyright Copyright (c) 2015 - 2024 Trilby Media, LLC. All rights reserved.
|
|
||||||
* @license MIT License; see LICENSE file for details.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use Grav\Common\Filesystem\Folder;
|
|
||||||
use Grav\Common\Grav;
|
|
||||||
use Grav\Framework\File\File;
|
|
||||||
use RocketTheme\Toolbox\Event\Event;
|
|
||||||
use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
|
|
||||||
use Symfony\Component\Yaml\Yaml;
|
|
||||||
|
|
||||||
class WhiteLabel
|
|
||||||
{
|
|
||||||
protected $grav;
|
|
||||||
protected $scss;
|
|
||||||
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
$this->grav = Grav::instance();
|
|
||||||
// ScssCompiler is now lazy-loaded to avoid loading scssphp classes until actually needed
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the ScssCompiler instance (lazy-loaded)
|
|
||||||
*
|
|
||||||
* @return ScssCompiler
|
|
||||||
*/
|
|
||||||
protected function getScss(): ScssCompiler
|
|
||||||
{
|
|
||||||
if ($this->scss === null) {
|
|
||||||
$this->scss = new ScssCompiler();
|
|
||||||
}
|
|
||||||
return $this->scss;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function compilePresetScss($config, $options = [
|
|
||||||
'input' => 'plugin://admin/themes/grav/scss/preset.scss',
|
|
||||||
'output' => 'asset://admin-preset.css'
|
|
||||||
])
|
|
||||||
{
|
|
||||||
if (is_array($config)) {
|
|
||||||
$color_scheme = $config['color_scheme'];
|
|
||||||
} else {
|
|
||||||
$color_scheme = $config->get('whitelabel.color_scheme');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($color_scheme) {
|
|
||||||
/** @var UniformResourceLocator $locator */
|
|
||||||
$locator = $this->grav['locator'];
|
|
||||||
|
|
||||||
// Use ScssList object to make it easier ot handle in event
|
|
||||||
$scss_list = new ScssList($locator->findResource($options['input']));
|
|
||||||
$output_css = $locator->findResource(($options['output']), true, true);
|
|
||||||
|
|
||||||
Folder::create(dirname($output_css));
|
|
||||||
|
|
||||||
Grav::instance()->fireEvent('onAdminCompilePresetSCSS', new Event(['scss' => $scss_list]));
|
|
||||||
|
|
||||||
// Convert bak to regular array now we have run the event
|
|
||||||
$input_scss = $scss_list->all();
|
|
||||||
|
|
||||||
$imports = [$locator->findResource('plugin://admin/themes/grav/scss')];
|
|
||||||
foreach ($input_scss as $scss) {
|
|
||||||
$input_path = dirname($scss);
|
|
||||||
if (!in_array($input_path, $imports)) {
|
|
||||||
$imports[] = $input_path;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$compiler = $this->getScss()->reset();
|
|
||||||
|
|
||||||
$compiler->setVariables($color_scheme['colors'] + $color_scheme['accents']);
|
|
||||||
$compiler->setImportPaths($imports);
|
|
||||||
$compiler->compileAll($input_scss, $output_css);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
return [false, $e->getMessage()];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return [true, 'Recompiled successfully'];
|
|
||||||
|
|
||||||
}
|
|
||||||
return [false, ' Could not be recompiled, missing color scheme...'];
|
|
||||||
}
|
|
||||||
|
|
||||||
public function exportPresetScsss($config, $location = 'asset://admin-theme-export.yaml')
|
|
||||||
{
|
|
||||||
|
|
||||||
if (isset($config['color_scheme'])) {
|
|
||||||
|
|
||||||
$color_scheme = $config['color_scheme'];
|
|
||||||
|
|
||||||
$body = Yaml::dump($color_scheme);
|
|
||||||
|
|
||||||
$file = new File($location);
|
|
||||||
$file->save($body);
|
|
||||||
// todo: handle errors/exceptions?
|
|
||||||
|
|
||||||
return [true, 'File created successfully'];
|
|
||||||
|
|
||||||
} else {
|
|
||||||
return [false, ' Could not export, missing color scheme...'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
actor: Tester
|
|
||||||
paths:
|
|
||||||
tests: tests
|
|
||||||
log: tests/_output
|
|
||||||
data: tests/_data
|
|
||||||
support: tests/_support
|
|
||||||
envs: tests/_envs
|
|
||||||
settings:
|
|
||||||
bootstrap: _bootstrap.php
|
|
||||||
colors: true
|
|
||||||
memory_limit: 1024M
|
|
||||||
extensions:
|
|
||||||
enabled:
|
|
||||||
- Codeception\Extension\RunFailed
|
|
||||||
# - Codeception\Extension\Recorder
|
|
||||||
|
|
||||||
modules:
|
|
||||||
config:
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "getgrav/grav-plugin-admin",
|
|
||||||
"type": "grav-plugin",
|
|
||||||
"description": "Admin plugin for Grav CMS",
|
|
||||||
"keywords": ["admin", "plugin", "manager", "panel"],
|
|
||||||
"homepage": "https://github.com/getgrav/grav-plugin-admin",
|
|
||||||
"license": "MIT",
|
|
||||||
"authors": [
|
|
||||||
{
|
|
||||||
"name": "Team Grav",
|
|
||||||
"email": "devs@getgrav.org",
|
|
||||||
"homepage": "https://getgrav.org",
|
|
||||||
"role": "Developer"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"support": {
|
|
||||||
"issues": "https://github.com/getgrav/grav-plugin-admin/issues",
|
|
||||||
"irc": "https://chat.getgrav.org",
|
|
||||||
"forum": "https://discourse.getgrav.org",
|
|
||||||
"docs": "https://github.com/getgrav/grav-plugin-admin/blob/master/README.md"
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": "^7.3.6 || ^8.0",
|
|
||||||
"ext-json": "*",
|
|
||||||
"scssphp/scssphp": "^1.13",
|
|
||||||
"p3k/picofeed": "^1.0"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"codeception/codeception": "^2.5",
|
|
||||||
"fzaninotto/faker": "^1.9",
|
|
||||||
"symfony/yaml": "~4.4",
|
|
||||||
"symfony/console": "~4.4",
|
|
||||||
"symfony/finder": "~4.4",
|
|
||||||
"symfony/event-dispatcher": "~4.4"
|
|
||||||
},
|
|
||||||
"replace": {
|
|
||||||
"symfony/polyfill-php73": "*"
|
|
||||||
},
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"Grav\\Plugin\\Admin\\": "classes/plugin"
|
|
||||||
},
|
|
||||||
"classmap": [
|
|
||||||
"admin.php"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"platform": {
|
|
||||||
"php": "7.3.6"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"test": "vendor/bin/codecept run unit",
|
|
||||||
"test-windows": "vendor\\bin\\codecept run unit"
|
|
||||||
},
|
|
||||||
"repositories": [
|
|
||||||
{
|
|
||||||
"type": "vcs",
|
|
||||||
"url": "https://github.com/rhukster/picoFeed"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user