(Grav GitSync) Automatic Commit from GitSync
This commit is contained in:
@@ -0,0 +1,11 @@
|
|||||||
|
state: enabled
|
||||||
|
email: mischa@gorinskat.nl
|
||||||
|
fullname: Mischa
|
||||||
|
title: Administrator
|
||||||
|
access:
|
||||||
|
admin:
|
||||||
|
login: true
|
||||||
|
super: true
|
||||||
|
site:
|
||||||
|
login: true
|
||||||
|
hashed_password: $2y$12$e5KC3ryCqg7FRFSMOi3ttOAjIppO5XYpzYdyYB7XAfsvwWkNXyJV.
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
state: enabled
|
||||||
|
email: info@natascha-rieter.nl
|
||||||
|
fullname: Natascha
|
||||||
|
language: nl
|
||||||
|
content_editor: default
|
||||||
|
twofa_enabled: false
|
||||||
|
twofa_secret: PCLSZHITHBEC6EDZP2SDCCNBGFWGOQS2
|
||||||
|
avatar: { }
|
||||||
|
hashed_password: $2y$12$LPk5BBcgOW6LTNy3QSiHg.0hhzB7UVmGWB4Gnfrqv9V4NTnI8MVU2
|
||||||
|
access:
|
||||||
|
site:
|
||||||
|
login: true
|
||||||
|
admin:
|
||||||
|
login: true
|
||||||
|
super: false
|
||||||
|
cache: true
|
||||||
|
configuration:
|
||||||
|
site: true
|
||||||
|
media: false
|
||||||
|
pages: true
|
||||||
|
pages: true
|
||||||
|
statistics: true
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
enabled: true
|
||||||
|
folders:
|
||||||
|
- pages
|
||||||
|
- themes
|
||||||
|
- config
|
||||||
|
sync:
|
||||||
|
direction: both
|
||||||
|
on_save: true
|
||||||
|
on_delete: true
|
||||||
|
on_media: true
|
||||||
|
cron_enable: false
|
||||||
|
cron_at: '0 12,23 * * *'
|
||||||
|
SyncNotice: null
|
||||||
|
local_repository: null
|
||||||
|
repository: 'https://git.gorinskat.nl/m038/natascha-rieter.nl-user.git'
|
||||||
|
no_user: '0'
|
||||||
|
user: natascha-rieter-grav-sync
|
||||||
|
password: gitsync-def50200a9605e736c3e3398b6e6bcd2bee9ed83c1325a4930f6f70383f8991a516d6a5b3ff5b0686b43e31f6f26b72a5f2592ff7ec617173208a21e432f70a344aa0a136e8cdaacf7e7aafbe66973eb1409f1829ec3b6f865d1be90a893371e7940613f2a29eadbd32fa8db63c0fbffe525620803789e15312e49ce4b952af0e3b79c6be795
|
||||||
|
webhook: /_git-sync-ac3b85097275
|
||||||
|
webhook_enabled: '0'
|
||||||
|
webhook_secret: 71e7fdb32dc616c2de167119a1a572e3d5f25b293827aeec
|
||||||
|
branch: main
|
||||||
|
remote:
|
||||||
|
name: origin
|
||||||
|
branch: main
|
||||||
|
git:
|
||||||
|
author: gitsync
|
||||||
|
message: '(Grav GitSync) Automatic Commit'
|
||||||
|
name: GitSync
|
||||||
|
email: git-sync@trilby.media
|
||||||
|
bin: git
|
||||||
|
ignore: null
|
||||||
|
private_key: null
|
||||||
|
logging: true
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
version: '1.2'
|
||||||
|
timestamp: 1781395436
|
||||||
|
count: 2
|
||||||
|
index:
|
||||||
|
admin:
|
||||||
|
storage_key: admin
|
||||||
|
storage_timestamp: 1781395072
|
||||||
|
key: admin
|
||||||
|
email: mischa@gorinskat.nl
|
||||||
|
natascha:
|
||||||
|
storage_key: natascha
|
||||||
|
storage_timestamp: 1781395436
|
||||||
|
key: natascha
|
||||||
|
email: info@natascha-rieter.nl
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"version":"1.5","timestamp":1781395073,"count":7,"index":{"":{"key":"","storage_key":"","template":null,"storage_timestamp":1781393636,"children":{"01.home":1781393636,"02.cv":1781393636,"03.galerie":1781393636,"04.werk-in-detail":1781393636,"05.contact":1781393636},"checksum":"f12a5b2bcdd84ce357b09c6f0babd3b6"},"01.home":{"key":"home","storage_key":"01.home","template":"home","storage_timestamp":1781393636,"markdown":{"en":{"home":1781393636},"nl":{"home":1781393636}},"checksum":"4030f9f917204a89ba47a872904f6cd7"},"02.cv":{"key":"cv","storage_key":"02.cv","template":"cv","storage_timestamp":1781393636,"markdown":{"en":{"cv":1781393636},"nl":{"cv":1781393636}},"checksum":"8ccac2cf915f0bfa23cbe25edb73eca6"},"03.galerie":{"key":"galerie","storage_key":"03.galerie","template":"galerie","storage_timestamp":1781393636,"markdown":{"en":{"galerie":1781393636},"nl":{"galerie":1781393636}},"checksum":"c7cb60cde84b8363fe69f00e7b244c9c"},"04.werk-in-detail":{"key":"werk-in-detail","storage_key":"04.werk-in-detail","template":"werk-in-detail","storage_timestamp":1781393636,"markdown":{"en":{"werk-in-detail":1781393636},"nl":{"werk-in-detail":1781393636}},"children":{"grote-objecten":1781393636},"checksum":"5e48805367a46d4db68569024d1919ea"},"04.werk-in-detail\/grote-objecten":{"key":"werk-in-detail\/grote-objecten","storage_key":"04.werk-in-detail\/grote-objecten","template":"album","storage_timestamp":1781393636,"markdown":{"en":{"album":1781393636},"nl":{"album":1781393636}},"checksum":"63b3ea992f2b739207f3abac649c6646"},"05.contact":{"key":"contact","storage_key":"05.contact","template":"contact","storage_timestamp":1781393636,"markdown":{"en":{"contact":1781393636},"nl":{"contact":1781393636}},"checksum":"81cb1220bca2763890183126302c3a62"}}}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
last_checked: 1781395370
|
||||||
|
data:
|
||||||
|
feed:
|
||||||
|
-
|
||||||
|
id: 40
|
||||||
|
date: '2026-01-27 12:00'
|
||||||
|
message: '🔑 <b>New Licensing System!</b> Grav premium products now use our new in-house license manager. Update Admin to the latest version.'
|
||||||
|
link: 'https://getgrav.org/blog/new-licensing-system'
|
||||||
|
type: note
|
||||||
|
location:
|
||||||
|
- feed
|
||||||
|
-
|
||||||
|
id: 38
|
||||||
|
date: '2025-10-21 11:40'
|
||||||
|
message: '️🗺️ <b>AI Translate</b> Instant professional-grade translations for Grav Admin'
|
||||||
|
link: 'https://getgrav.org/premium/ai-translate'
|
||||||
|
type: info
|
||||||
|
location:
|
||||||
|
- feed
|
||||||
|
-
|
||||||
|
id: 37
|
||||||
|
date: '2025-10-20 11:40'
|
||||||
|
message: '🤖 <b>AI Pro</b> providing an Admin AI Assistant with OpenAI, Anthropic, Google Models and more.'
|
||||||
|
link: 'https://getgrav.org/premium/ai-pro'
|
||||||
|
type: info
|
||||||
|
location:
|
||||||
|
- feed
|
||||||
|
-
|
||||||
|
id: 36
|
||||||
|
date: '2025-08-28 11:40'
|
||||||
|
message: '✍️️ Edit smarter with <b>Editor Pro</b>: Modern WYSIWYM editing, live preview, and powerful content tools!'
|
||||||
|
link: 'https://getgrav.org/premium/editor-pro'
|
||||||
|
type: info
|
||||||
|
location:
|
||||||
|
- feed
|
||||||
|
-
|
||||||
|
id: 35
|
||||||
|
date: '2025-07-30 11:40'
|
||||||
|
message: '🕙️ Power up Grav with <b>Revisions Pro</b>, Track, Compare, Revert content and config!'
|
||||||
|
link: 'https://getgrav.org/premium/revisions-pro'
|
||||||
|
type: info
|
||||||
|
location:
|
||||||
|
- feed
|
||||||
|
-
|
||||||
|
id: 34
|
||||||
|
date: '2025-07-29 11:40'
|
||||||
|
message: '🗳️ Transform your Grav site into an interactive experience with <b>Polls.</b>'
|
||||||
|
link: 'https://getgrav.org/premium/polls'
|
||||||
|
type: info
|
||||||
|
location:
|
||||||
|
- feed
|
||||||
|
-
|
||||||
|
id: 33
|
||||||
|
date: '2025-07-28 11:40'
|
||||||
|
message: '💬 Build a community with <b>Comments Pro</b> for Grav, now available.'
|
||||||
|
link: 'https://getgrav.org/premium/comments-pro'
|
||||||
|
type: info
|
||||||
|
location:
|
||||||
|
- feed
|
||||||
|
-
|
||||||
|
id: 31
|
||||||
|
date: '2024-02-13 11:40'
|
||||||
|
message: '🗜️Optimize your Grav site with <b>Image Optimize</b>, now available.'
|
||||||
|
link: 'https://getgrav.org/premium/image-optimize'
|
||||||
|
type: info
|
||||||
|
location:
|
||||||
|
- feed
|
||||||
|
-
|
||||||
|
id: 12
|
||||||
|
date: '2024-01-17 15:15'
|
||||||
|
message: '☕️ Support Grav for the price of a <span class=''fa fa-coffee''></span> a month!'
|
||||||
|
link: 'https://opencollective.com/grav'
|
||||||
|
type: note
|
||||||
|
location:
|
||||||
|
- feed
|
||||||
|
-
|
||||||
|
id: 23
|
||||||
|
date: '2024-01-05 15:50'
|
||||||
|
message: '🙊 Grav community chat has moved from Slack to <span class=''fa fa-comments''></span> Discord'
|
||||||
|
link: 'https://chat.getgrav.org'
|
||||||
|
type: info
|
||||||
|
location:
|
||||||
|
- feed
|
||||||
|
-
|
||||||
|
id: 2
|
||||||
|
date: '2024-01-05 02:23'
|
||||||
|
message: '🎖 Don''t forget to star Grav on GitHub!'
|
||||||
|
link: 'https://github.com/getgrav/grav'
|
||||||
|
type: info
|
||||||
|
location:
|
||||||
|
- feed
|
||||||
|
top:
|
||||||
|
-
|
||||||
|
id: 39
|
||||||
|
date: '2026-01-27 12:00'
|
||||||
|
message: "<style>\n.top-notifications-container .alert.notice.position-top {\n background: none;\n border: none;\n padding: 0;\n margin: 0 0 1rem 0;\n position: relative;\n}\n.top-notifications-container .alert.notice.position-top:before {\n display: none;\n}\n.top-notifications-container .alert.notice.position-top > .hide-notification {\n top: 50%;\n transform: translateY(-50%);\n right: 12px;\n color: rgba(255,255,255,0.7);\n text-shadow: none;\n z-index: 1;\n}\n.top-notifications-container .alert.notice.position-top > .hide-notification:hover {\n color: #fff;\n}\n.licensing-banner {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 15px;\n background: linear-gradient(135deg, #323640 0%, #3d424e 100%);\n color: #fff;\n padding: 14px 40px 14px 20px;\n border-radius: 4px;\n font-size: 14px;\n line-height: 1.4;\n flex-wrap: wrap;\n}\n.licensing-banner .lb-icon {\n font-size: 20px;\n}\n.licensing-banner .lb-text {\n flex: 1;\n min-width: 200px;\n color: rgba(255,255,255,0.9);\n}\n.licensing-banner .lb-text b {\n color: #fff;\n}\n.licensing-banner .lb-action {\n display: inline-block;\n background: transparent;\n color: #fff;\n padding: 5px 14px;\n border: 1px solid rgba(255,255,255,0.5);\n border-radius: 3px;\n text-decoration: none;\n font-weight: bold;\n font-size: 13px;\n white-space: nowrap;\n}\n.licensing-banner .lb-action:hover {\n background: rgba(255,255,255,0.15);\n border-color: #fff;\n}\n</style>\n<div class=\"licensing-banner\">\n <span class=\"lb-icon\">🔑</span>\n <span class=\"lb-text\"><b>New Licensing & Purchase System!</b> Grav premium products now use our new in-house license manager. Please update the <b>Admin plugin</b> to the latest version for purchase links to work correctly.</span>\n <a class=\"lb-action\" href=\"https://getgrav.org/blog/new-licensing-system\" target=\"_blank\">Learn More</a>\n</div>\n"
|
||||||
|
type: notice
|
||||||
|
location:
|
||||||
|
- top
|
||||||
|
dependencies:
|
||||||
|
admin: '< 2.0.0'
|
||||||
|
dashboard:
|
||||||
|
-
|
||||||
|
id: 27
|
||||||
|
date: '2020-02-21 00:01'
|
||||||
|
message: "<style>\n.dashboard-notifications-container, .dashboard-notifications-container .alert, .themes-notifications-container, .themes-notifications-container .alert, .plugins-notifications-container, .plugins-notifications-container .alert {\n margin: 0;\n padding: 0;\n}\n.dashboard-notifications-container .alert.position-dashboard:before,\n.themes-notifications-container .alert.position-themes:before,\n.plugins-notifications-container .alert.position-plugins:before {\n display: none;\n}\n.dashboard-notifications-container .alert .hide-notification,\n.themes-notifications-container .alert .hide-notification,\n.plugins-notifications-container .alert .hide-notification {\n top: 50%;\n transform: translateY(-50%);\n right: 12px;\n color: rgba(255,255,255,0.7);\n text-shadow: none;\n}\n.dashboard-notifications-container .alert .hide-notification:hover,\n.themes-notifications-container .alert .hide-notification:hover,\n.plugins-notifications-container .alert .hide-notification:hover {\n color: #fff;\n}\n.gp-banner {\n display: flex;\n align-items: center;\n gap: 20px;\n background: linear-gradient(135deg, #322048 0%, #4a2068 50%, #2d1b4e 100%);\n color: #fff;\n padding: 12px 40px 12px 20px;\n border-radius: 4px;\n font-size: 14px;\n line-height: 1.4;\n flex-wrap: wrap;\n margin-bottom: 1rem;\n}\n.gp-banner .gp-image {\n flex-shrink: 0;\n}\n.gp-banner .gp-image img {\n height: 28px;\n display: block;\n}\n.gp-banner .gp-text {\n flex: 1;\n min-width: 200px;\n color: rgba(255,255,255,0.9);\n}\n.gp-banner .gp-text b {\n color: #fff;\n}\n.gp-banner .gp-learn-more {\n display: inline-block;\n background: transparent;\n color: #fff;\n padding: 5px 14px;\n border: 1px solid rgba(255,255,255,0.5);\n border-radius: 3px;\n text-decoration: none;\n font-weight: bold;\n font-size: 13px;\n white-space: nowrap;\n}\n.gp-banner .gp-learn-more:hover {\n background: rgba(255,255,255,0.15);\n border-color: #fff;\n}\n</style>\n<div class=\"gp-banner\">\n <div class=\"gp-image\">\n <img src=\"https://getgrav.org/user/themes/planetoid/images/grav-premium-logo-white.svg\" />\n </div>\n <span class=\"gp-text\">Turbo-charge your Grav site with <b>premium plugins & themes</b> from the creators of Grav</span>\n <a class=\"gp-learn-more\" href=\"https://getgrav.org/premium\" target=\"_blank\">Learn more</a>\n</div>\n"
|
||||||
|
type: notice
|
||||||
|
link: 'https://getgrav.org/premium'
|
||||||
|
location:
|
||||||
|
- dashboard
|
||||||
|
- plugins
|
||||||
|
- themes
|
||||||
|
plugins:
|
||||||
|
-
|
||||||
|
id: 27
|
||||||
|
date: '2020-02-21 00:01'
|
||||||
|
message: "<style>\n.dashboard-notifications-container, .dashboard-notifications-container .alert, .themes-notifications-container, .themes-notifications-container .alert, .plugins-notifications-container, .plugins-notifications-container .alert {\n margin: 0;\n padding: 0;\n}\n.dashboard-notifications-container .alert.position-dashboard:before,\n.themes-notifications-container .alert.position-themes:before,\n.plugins-notifications-container .alert.position-plugins:before {\n display: none;\n}\n.dashboard-notifications-container .alert .hide-notification,\n.themes-notifications-container .alert .hide-notification,\n.plugins-notifications-container .alert .hide-notification {\n top: 50%;\n transform: translateY(-50%);\n right: 12px;\n color: rgba(255,255,255,0.7);\n text-shadow: none;\n}\n.dashboard-notifications-container .alert .hide-notification:hover,\n.themes-notifications-container .alert .hide-notification:hover,\n.plugins-notifications-container .alert .hide-notification:hover {\n color: #fff;\n}\n.gp-banner {\n display: flex;\n align-items: center;\n gap: 20px;\n background: linear-gradient(135deg, #322048 0%, #4a2068 50%, #2d1b4e 100%);\n color: #fff;\n padding: 12px 40px 12px 20px;\n border-radius: 4px;\n font-size: 14px;\n line-height: 1.4;\n flex-wrap: wrap;\n margin-bottom: 1rem;\n}\n.gp-banner .gp-image {\n flex-shrink: 0;\n}\n.gp-banner .gp-image img {\n height: 28px;\n display: block;\n}\n.gp-banner .gp-text {\n flex: 1;\n min-width: 200px;\n color: rgba(255,255,255,0.9);\n}\n.gp-banner .gp-text b {\n color: #fff;\n}\n.gp-banner .gp-learn-more {\n display: inline-block;\n background: transparent;\n color: #fff;\n padding: 5px 14px;\n border: 1px solid rgba(255,255,255,0.5);\n border-radius: 3px;\n text-decoration: none;\n font-weight: bold;\n font-size: 13px;\n white-space: nowrap;\n}\n.gp-banner .gp-learn-more:hover {\n background: rgba(255,255,255,0.15);\n border-color: #fff;\n}\n</style>\n<div class=\"gp-banner\">\n <div class=\"gp-image\">\n <img src=\"https://getgrav.org/user/themes/planetoid/images/grav-premium-logo-white.svg\" />\n </div>\n <span class=\"gp-text\">Turbo-charge your Grav site with <b>premium plugins & themes</b> from the creators of Grav</span>\n <a class=\"gp-learn-more\" href=\"https://getgrav.org/premium\" target=\"_blank\">Learn more</a>\n</div>\n"
|
||||||
|
type: notice
|
||||||
|
link: 'https://getgrav.org/premium'
|
||||||
|
location:
|
||||||
|
- dashboard
|
||||||
|
- plugins
|
||||||
|
- themes
|
||||||
|
themes:
|
||||||
|
-
|
||||||
|
id: 27
|
||||||
|
date: '2020-02-21 00:01'
|
||||||
|
message: "<style>\n.dashboard-notifications-container, .dashboard-notifications-container .alert, .themes-notifications-container, .themes-notifications-container .alert, .plugins-notifications-container, .plugins-notifications-container .alert {\n margin: 0;\n padding: 0;\n}\n.dashboard-notifications-container .alert.position-dashboard:before,\n.themes-notifications-container .alert.position-themes:before,\n.plugins-notifications-container .alert.position-plugins:before {\n display: none;\n}\n.dashboard-notifications-container .alert .hide-notification,\n.themes-notifications-container .alert .hide-notification,\n.plugins-notifications-container .alert .hide-notification {\n top: 50%;\n transform: translateY(-50%);\n right: 12px;\n color: rgba(255,255,255,0.7);\n text-shadow: none;\n}\n.dashboard-notifications-container .alert .hide-notification:hover,\n.themes-notifications-container .alert .hide-notification:hover,\n.plugins-notifications-container .alert .hide-notification:hover {\n color: #fff;\n}\n.gp-banner {\n display: flex;\n align-items: center;\n gap: 20px;\n background: linear-gradient(135deg, #322048 0%, #4a2068 50%, #2d1b4e 100%);\n color: #fff;\n padding: 12px 40px 12px 20px;\n border-radius: 4px;\n font-size: 14px;\n line-height: 1.4;\n flex-wrap: wrap;\n margin-bottom: 1rem;\n}\n.gp-banner .gp-image {\n flex-shrink: 0;\n}\n.gp-banner .gp-image img {\n height: 28px;\n display: block;\n}\n.gp-banner .gp-text {\n flex: 1;\n min-width: 200px;\n color: rgba(255,255,255,0.9);\n}\n.gp-banner .gp-text b {\n color: #fff;\n}\n.gp-banner .gp-learn-more {\n display: inline-block;\n background: transparent;\n color: #fff;\n padding: 5px 14px;\n border: 1px solid rgba(255,255,255,0.5);\n border-radius: 3px;\n text-decoration: none;\n font-weight: bold;\n font-size: 13px;\n white-space: nowrap;\n}\n.gp-banner .gp-learn-more:hover {\n background: rgba(255,255,255,0.15);\n border-color: #fff;\n}\n</style>\n<div class=\"gp-banner\">\n <div class=\"gp-image\">\n <img src=\"https://getgrav.org/user/themes/planetoid/images/grav-premium-logo-white.svg\" />\n </div>\n <span class=\"gp-text\">Turbo-charge your Grav site with <b>premium plugins & themes</b> from the creators of Grav</span>\n <a class=\"gp-learn-more\" href=\"https://getgrav.org/premium\" target=\"_blank\">Learn more</a>\n</div>\n"
|
||||||
|
type: notice
|
||||||
|
link: 'https://getgrav.org/premium'
|
||||||
|
location:
|
||||||
|
- dashboard
|
||||||
|
- plugins
|
||||||
|
- themes
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
last_checked: 1781395073
|
||||||
|
data:
|
||||||
|
feed:
|
||||||
|
-
|
||||||
|
id: 40
|
||||||
|
date: '2026-01-27 12:00'
|
||||||
|
message: '🔑 <b>New Licensing System!</b> Grav premium products now use our new in-house license manager. Update Admin to the latest version.'
|
||||||
|
link: 'https://getgrav.org/blog/new-licensing-system'
|
||||||
|
type: note
|
||||||
|
location:
|
||||||
|
- feed
|
||||||
|
-
|
||||||
|
id: 38
|
||||||
|
date: '2025-10-21 11:40'
|
||||||
|
message: '️🗺️ <b>AI Translate</b> Instant professional-grade translations for Grav Admin'
|
||||||
|
link: 'https://getgrav.org/premium/ai-translate'
|
||||||
|
type: info
|
||||||
|
location:
|
||||||
|
- feed
|
||||||
|
-
|
||||||
|
id: 37
|
||||||
|
date: '2025-10-20 11:40'
|
||||||
|
message: '🤖 <b>AI Pro</b> providing an Admin AI Assistant with OpenAI, Anthropic, Google Models and more.'
|
||||||
|
link: 'https://getgrav.org/premium/ai-pro'
|
||||||
|
type: info
|
||||||
|
location:
|
||||||
|
- feed
|
||||||
|
-
|
||||||
|
id: 36
|
||||||
|
date: '2025-08-28 11:40'
|
||||||
|
message: '✍️️ Edit smarter with <b>Editor Pro</b>: Modern WYSIWYM editing, live preview, and powerful content tools!'
|
||||||
|
link: 'https://getgrav.org/premium/editor-pro'
|
||||||
|
type: info
|
||||||
|
location:
|
||||||
|
- feed
|
||||||
|
-
|
||||||
|
id: 35
|
||||||
|
date: '2025-07-30 11:40'
|
||||||
|
message: '🕙️ Power up Grav with <b>Revisions Pro</b>, Track, Compare, Revert content and config!'
|
||||||
|
link: 'https://getgrav.org/premium/revisions-pro'
|
||||||
|
type: info
|
||||||
|
location:
|
||||||
|
- feed
|
||||||
|
-
|
||||||
|
id: 34
|
||||||
|
date: '2025-07-29 11:40'
|
||||||
|
message: '🗳️ Transform your Grav site into an interactive experience with <b>Polls.</b>'
|
||||||
|
link: 'https://getgrav.org/premium/polls'
|
||||||
|
type: info
|
||||||
|
location:
|
||||||
|
- feed
|
||||||
|
-
|
||||||
|
id: 33
|
||||||
|
date: '2025-07-28 11:40'
|
||||||
|
message: '💬 Build a community with <b>Comments Pro</b> for Grav, now available.'
|
||||||
|
link: 'https://getgrav.org/premium/comments-pro'
|
||||||
|
type: info
|
||||||
|
location:
|
||||||
|
- feed
|
||||||
|
-
|
||||||
|
id: 31
|
||||||
|
date: '2024-02-13 11:40'
|
||||||
|
message: '🗜️Optimize your Grav site with <b>Image Optimize</b>, now available.'
|
||||||
|
link: 'https://getgrav.org/premium/image-optimize'
|
||||||
|
type: info
|
||||||
|
location:
|
||||||
|
- feed
|
||||||
|
-
|
||||||
|
id: 12
|
||||||
|
date: '2024-01-17 15:15'
|
||||||
|
message: '☕️ Support Grav for the price of a <span class=''fa fa-coffee''></span> a month!'
|
||||||
|
link: 'https://opencollective.com/grav'
|
||||||
|
type: note
|
||||||
|
location:
|
||||||
|
- feed
|
||||||
|
-
|
||||||
|
id: 23
|
||||||
|
date: '2024-01-05 15:50'
|
||||||
|
message: '🙊 Grav community chat has moved from Slack to <span class=''fa fa-comments''></span> Discord'
|
||||||
|
link: 'https://chat.getgrav.org'
|
||||||
|
type: info
|
||||||
|
location:
|
||||||
|
- feed
|
||||||
|
-
|
||||||
|
id: 2
|
||||||
|
date: '2024-01-05 02:23'
|
||||||
|
message: '🎖 Don''t forget to star Grav on GitHub!'
|
||||||
|
link: 'https://github.com/getgrav/grav'
|
||||||
|
type: info
|
||||||
|
location:
|
||||||
|
- feed
|
||||||
|
top:
|
||||||
|
-
|
||||||
|
id: 39
|
||||||
|
date: '2026-01-27 12:00'
|
||||||
|
message: "<style>\n.top-notifications-container .alert.notice.position-top {\n background: none;\n border: none;\n padding: 0;\n margin: 0 0 1rem 0;\n position: relative;\n}\n.top-notifications-container .alert.notice.position-top:before {\n display: none;\n}\n.top-notifications-container .alert.notice.position-top > .hide-notification {\n top: 50%;\n transform: translateY(-50%);\n right: 12px;\n color: rgba(255,255,255,0.7);\n text-shadow: none;\n z-index: 1;\n}\n.top-notifications-container .alert.notice.position-top > .hide-notification:hover {\n color: #fff;\n}\n.licensing-banner {\n display: flex;\n align-items: center;\n justify-content: center;\n gap: 15px;\n background: linear-gradient(135deg, #323640 0%, #3d424e 100%);\n color: #fff;\n padding: 14px 40px 14px 20px;\n border-radius: 4px;\n font-size: 14px;\n line-height: 1.4;\n flex-wrap: wrap;\n}\n.licensing-banner .lb-icon {\n font-size: 20px;\n}\n.licensing-banner .lb-text {\n flex: 1;\n min-width: 200px;\n color: rgba(255,255,255,0.9);\n}\n.licensing-banner .lb-text b {\n color: #fff;\n}\n.licensing-banner .lb-action {\n display: inline-block;\n background: transparent;\n color: #fff;\n padding: 5px 14px;\n border: 1px solid rgba(255,255,255,0.5);\n border-radius: 3px;\n text-decoration: none;\n font-weight: bold;\n font-size: 13px;\n white-space: nowrap;\n}\n.licensing-banner .lb-action:hover {\n background: rgba(255,255,255,0.15);\n border-color: #fff;\n}\n</style>\n<div class=\"licensing-banner\">\n <span class=\"lb-icon\">🔑</span>\n <span class=\"lb-text\"><b>New Licensing & Purchase System!</b> Grav premium products now use our new in-house license manager. Please update the <b>Admin plugin</b> to the latest version for purchase links to work correctly.</span>\n <a class=\"lb-action\" href=\"https://getgrav.org/blog/new-licensing-system\" target=\"_blank\">Learn More</a>\n</div>\n"
|
||||||
|
type: notice
|
||||||
|
location:
|
||||||
|
- top
|
||||||
|
dependencies:
|
||||||
|
admin: '< 2.0.0'
|
||||||
|
dashboard:
|
||||||
|
-
|
||||||
|
id: 27
|
||||||
|
date: '2020-02-21 00:01'
|
||||||
|
message: "<style>\n.dashboard-notifications-container, .dashboard-notifications-container .alert, .themes-notifications-container, .themes-notifications-container .alert, .plugins-notifications-container, .plugins-notifications-container .alert {\n margin: 0;\n padding: 0;\n}\n.dashboard-notifications-container .alert.position-dashboard:before,\n.themes-notifications-container .alert.position-themes:before,\n.plugins-notifications-container .alert.position-plugins:before {\n display: none;\n}\n.dashboard-notifications-container .alert .hide-notification,\n.themes-notifications-container .alert .hide-notification,\n.plugins-notifications-container .alert .hide-notification {\n top: 50%;\n transform: translateY(-50%);\n right: 12px;\n color: rgba(255,255,255,0.7);\n text-shadow: none;\n}\n.dashboard-notifications-container .alert .hide-notification:hover,\n.themes-notifications-container .alert .hide-notification:hover,\n.plugins-notifications-container .alert .hide-notification:hover {\n color: #fff;\n}\n.gp-banner {\n display: flex;\n align-items: center;\n gap: 20px;\n background: linear-gradient(135deg, #322048 0%, #4a2068 50%, #2d1b4e 100%);\n color: #fff;\n padding: 12px 40px 12px 20px;\n border-radius: 4px;\n font-size: 14px;\n line-height: 1.4;\n flex-wrap: wrap;\n margin-bottom: 1rem;\n}\n.gp-banner .gp-image {\n flex-shrink: 0;\n}\n.gp-banner .gp-image img {\n height: 28px;\n display: block;\n}\n.gp-banner .gp-text {\n flex: 1;\n min-width: 200px;\n color: rgba(255,255,255,0.9);\n}\n.gp-banner .gp-text b {\n color: #fff;\n}\n.gp-banner .gp-learn-more {\n display: inline-block;\n background: transparent;\n color: #fff;\n padding: 5px 14px;\n border: 1px solid rgba(255,255,255,0.5);\n border-radius: 3px;\n text-decoration: none;\n font-weight: bold;\n font-size: 13px;\n white-space: nowrap;\n}\n.gp-banner .gp-learn-more:hover {\n background: rgba(255,255,255,0.15);\n border-color: #fff;\n}\n</style>\n<div class=\"gp-banner\">\n <div class=\"gp-image\">\n <img src=\"https://getgrav.org/user/themes/planetoid/images/grav-premium-logo-white.svg\" />\n </div>\n <span class=\"gp-text\">Turbo-charge your Grav site with <b>premium plugins & themes</b> from the creators of Grav</span>\n <a class=\"gp-learn-more\" href=\"https://getgrav.org/premium\" target=\"_blank\">Learn more</a>\n</div>\n"
|
||||||
|
type: notice
|
||||||
|
link: 'https://getgrav.org/premium'
|
||||||
|
location:
|
||||||
|
- dashboard
|
||||||
|
- plugins
|
||||||
|
- themes
|
||||||
|
plugins:
|
||||||
|
-
|
||||||
|
id: 27
|
||||||
|
date: '2020-02-21 00:01'
|
||||||
|
message: "<style>\n.dashboard-notifications-container, .dashboard-notifications-container .alert, .themes-notifications-container, .themes-notifications-container .alert, .plugins-notifications-container, .plugins-notifications-container .alert {\n margin: 0;\n padding: 0;\n}\n.dashboard-notifications-container .alert.position-dashboard:before,\n.themes-notifications-container .alert.position-themes:before,\n.plugins-notifications-container .alert.position-plugins:before {\n display: none;\n}\n.dashboard-notifications-container .alert .hide-notification,\n.themes-notifications-container .alert .hide-notification,\n.plugins-notifications-container .alert .hide-notification {\n top: 50%;\n transform: translateY(-50%);\n right: 12px;\n color: rgba(255,255,255,0.7);\n text-shadow: none;\n}\n.dashboard-notifications-container .alert .hide-notification:hover,\n.themes-notifications-container .alert .hide-notification:hover,\n.plugins-notifications-container .alert .hide-notification:hover {\n color: #fff;\n}\n.gp-banner {\n display: flex;\n align-items: center;\n gap: 20px;\n background: linear-gradient(135deg, #322048 0%, #4a2068 50%, #2d1b4e 100%);\n color: #fff;\n padding: 12px 40px 12px 20px;\n border-radius: 4px;\n font-size: 14px;\n line-height: 1.4;\n flex-wrap: wrap;\n margin-bottom: 1rem;\n}\n.gp-banner .gp-image {\n flex-shrink: 0;\n}\n.gp-banner .gp-image img {\n height: 28px;\n display: block;\n}\n.gp-banner .gp-text {\n flex: 1;\n min-width: 200px;\n color: rgba(255,255,255,0.9);\n}\n.gp-banner .gp-text b {\n color: #fff;\n}\n.gp-banner .gp-learn-more {\n display: inline-block;\n background: transparent;\n color: #fff;\n padding: 5px 14px;\n border: 1px solid rgba(255,255,255,0.5);\n border-radius: 3px;\n text-decoration: none;\n font-weight: bold;\n font-size: 13px;\n white-space: nowrap;\n}\n.gp-banner .gp-learn-more:hover {\n background: rgba(255,255,255,0.15);\n border-color: #fff;\n}\n</style>\n<div class=\"gp-banner\">\n <div class=\"gp-image\">\n <img src=\"https://getgrav.org/user/themes/planetoid/images/grav-premium-logo-white.svg\" />\n </div>\n <span class=\"gp-text\">Turbo-charge your Grav site with <b>premium plugins & themes</b> from the creators of Grav</span>\n <a class=\"gp-learn-more\" href=\"https://getgrav.org/premium\" target=\"_blank\">Learn more</a>\n</div>\n"
|
||||||
|
type: notice
|
||||||
|
link: 'https://getgrav.org/premium'
|
||||||
|
location:
|
||||||
|
- dashboard
|
||||||
|
- plugins
|
||||||
|
- themes
|
||||||
|
themes:
|
||||||
|
-
|
||||||
|
id: 27
|
||||||
|
date: '2020-02-21 00:01'
|
||||||
|
message: "<style>\n.dashboard-notifications-container, .dashboard-notifications-container .alert, .themes-notifications-container, .themes-notifications-container .alert, .plugins-notifications-container, .plugins-notifications-container .alert {\n margin: 0;\n padding: 0;\n}\n.dashboard-notifications-container .alert.position-dashboard:before,\n.themes-notifications-container .alert.position-themes:before,\n.plugins-notifications-container .alert.position-plugins:before {\n display: none;\n}\n.dashboard-notifications-container .alert .hide-notification,\n.themes-notifications-container .alert .hide-notification,\n.plugins-notifications-container .alert .hide-notification {\n top: 50%;\n transform: translateY(-50%);\n right: 12px;\n color: rgba(255,255,255,0.7);\n text-shadow: none;\n}\n.dashboard-notifications-container .alert .hide-notification:hover,\n.themes-notifications-container .alert .hide-notification:hover,\n.plugins-notifications-container .alert .hide-notification:hover {\n color: #fff;\n}\n.gp-banner {\n display: flex;\n align-items: center;\n gap: 20px;\n background: linear-gradient(135deg, #322048 0%, #4a2068 50%, #2d1b4e 100%);\n color: #fff;\n padding: 12px 40px 12px 20px;\n border-radius: 4px;\n font-size: 14px;\n line-height: 1.4;\n flex-wrap: wrap;\n margin-bottom: 1rem;\n}\n.gp-banner .gp-image {\n flex-shrink: 0;\n}\n.gp-banner .gp-image img {\n height: 28px;\n display: block;\n}\n.gp-banner .gp-text {\n flex: 1;\n min-width: 200px;\n color: rgba(255,255,255,0.9);\n}\n.gp-banner .gp-text b {\n color: #fff;\n}\n.gp-banner .gp-learn-more {\n display: inline-block;\n background: transparent;\n color: #fff;\n padding: 5px 14px;\n border: 1px solid rgba(255,255,255,0.5);\n border-radius: 3px;\n text-decoration: none;\n font-weight: bold;\n font-size: 13px;\n white-space: nowrap;\n}\n.gp-banner .gp-learn-more:hover {\n background: rgba(255,255,255,0.15);\n border-color: #fff;\n}\n</style>\n<div class=\"gp-banner\">\n <div class=\"gp-image\">\n <img src=\"https://getgrav.org/user/themes/planetoid/images/grav-premium-logo-white.svg\" />\n </div>\n <span class=\"gp-text\">Turbo-charge your Grav site with <b>premium plugins & themes</b> from the creators of Grav</span>\n <a class=\"gp-learn-more\" href=\"https://getgrav.org/premium\" target=\"_blank\">Learn more</a>\n</div>\n"
|
||||||
|
type: notice
|
||||||
|
link: 'https://getgrav.org/premium'
|
||||||
|
location:
|
||||||
|
- dashboard
|
||||||
|
- plugins
|
||||||
|
- themes
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
39: 'Sat, 13 Jun 2026 23:58:37 +0000'
|
||||||
|
27: 'Sun, 14 Jun 2026 00:26:11 +0000'
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
39: 'Sun, 14 Jun 2026 00:02:57 +0000'
|
||||||
|
undefined: 'Sun, 14 Jun 2026 00:02:59 +0000'
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.gradle
|
||||||
|
build
|
||||||
|
out
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# 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)
|
||||||
|
*
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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.
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#
|
||||||
|
# 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
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* 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"}');
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
#
|
||||||
|
# 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
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<!--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>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.gradle
|
||||||
|
build
|
||||||
|
out
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# 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)
|
||||||
|
*
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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.
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#
|
||||||
|
# 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
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/*
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
#
|
||||||
|
# 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
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
.gradle
|
||||||
|
build
|
||||||
|
out
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
# 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)
|
||||||
|
*
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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.
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# 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)
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
#
|
||||||
|
# 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
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
/*
|
||||||
|
* 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();
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#
|
||||||
|
# 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
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
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>.
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?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);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{% extends "forms/field.html.twig" %}
|
||||||
|
|
||||||
|
{% block label %}
|
||||||
|
{{ label ?: "File" }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block input %}
|
||||||
|
<input name="{{ name }}" type="file" style="width: 100%"/>
|
||||||
|
{% endblock %}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<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>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# EditorConfig is awesome: https://EditorConfig.org
|
||||||
|
|
||||||
|
# top-most EditorConfig file
|
||||||
|
root = true
|
||||||
|
|
||||||
|
# Unix-style newlines with a newline ending every file
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = lf
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
|
||||||
|
# 4 space indentation
|
||||||
|
[*.php]
|
||||||
|
indent_size = 4
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Linguist Normalizer
|
||||||
|
*.yaml linguistic-language=PHP
|
||||||
|
*.twig linguistic-language=PHP
|
||||||
|
**/gulpfile.babel.js linguist-vendored
|
||||||
|
**/webpack.conf.js linguist-vendored
|
||||||
|
**/js/*.js linguist-vendored
|
||||||
|
**/js/*.json linguist-vendored
|
||||||
|
**/css-compiled/*.css linguist-vendored
|
||||||
Vendored
+8
@@ -0,0 +1,8 @@
|
|||||||
|
# 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
|
||||||
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
themes/grav/.sass-cache
|
||||||
|
.DS_Store
|
||||||
|
crowdin.yaml
|
||||||
|
|
||||||
|
# Node Modules
|
||||||
|
**/node_modules/**
|
||||||
|
themes/grav/js/admin.js
|
||||||
|
themes/grav/js/vendor.js
|
||||||
|
themes/grav/js/*.map
|
||||||
|
.idea
|
||||||
|
|
||||||
|
tests/_output/*
|
||||||
|
tests/_support/_generated/*
|
||||||
|
tests/cache/*
|
||||||
|
tests/error.log
|
||||||
|
/crowdin.yaml
|
||||||
|
.vscode
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
|||||||
|
Please read the <a href="https://github.com/getgrav/grav/blob/develop/CONTRIBUTING.md" target="_blank">Contributing Guidelines of the Grav Project</a>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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.
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
# 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.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# Upgrading to Admin 1.10
|
||||||
|
|
||||||
|
Twig:
|
||||||
|
|
||||||
|
* **Admin link**: When linking to another admin page, use `{{ admin_route('/config/site') }}` instead of any other method, such as `{{ base_url_relative }}/config/site` (fixes multi-language issues)
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,84 @@
|
|||||||
|
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: true
|
||||||
|
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.
|
After Width: | Height: | Size: 236 KiB |
@@ -0,0 +1,781 @@
|
|||||||
|
name: Admin Panel
|
||||||
|
slug: admin
|
||||||
|
type: plugin
|
||||||
|
version: 1.10.52
|
||||||
|
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
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
form:
|
||||||
|
validation: loose
|
||||||
|
fields:
|
||||||
|
route:
|
||||||
|
type: hidden
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
rules:
|
||||||
|
slug:
|
||||||
|
pattern: '[a-zA-Zа-яA-Я0-9_\-]+'
|
||||||
|
min: 1
|
||||||
|
max: 200
|
||||||
|
|
||||||
|
form:
|
||||||
|
validation: loose
|
||||||
|
fields:
|
||||||
|
|
||||||
|
tabs:
|
||||||
|
type: tabs
|
||||||
|
active: 1
|
||||||
|
|
||||||
|
fields:
|
||||||
|
content:
|
||||||
|
type: tab
|
||||||
|
title: PLUGIN_ADMIN.CONTENT
|
||||||
|
|
||||||
|
fields:
|
||||||
|
frontmatter:
|
||||||
|
classes: frontmatter
|
||||||
|
type: editor
|
||||||
|
label: PLUGIN_ADMIN.FRONTMATTER
|
||||||
|
autofocus: true
|
||||||
|
codemirror:
|
||||||
|
mode: 'yaml'
|
||||||
|
indentUnit: 4
|
||||||
|
autofocus: true
|
||||||
|
indentWithTabs: false
|
||||||
|
lineNumbers: true
|
||||||
|
styleActiveLine: true
|
||||||
|
gutters: ['CodeMirror-lint-markers']
|
||||||
|
lint: true
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
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
@@ -0,0 +1,182 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,414 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,359 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,644 @@
|
|||||||
|
<?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'];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,461 @@
|
|||||||
|
<?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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,310 @@
|
|||||||
|
<?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, []);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
<?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,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
<?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]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
<?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...'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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:
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
{
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Generated
+3962
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"project":"grav-plugin-admin",
|
||||||
|
"platforms":{
|
||||||
|
"grav":{
|
||||||
|
"nodes":{
|
||||||
|
"plugin":[
|
||||||
|
{
|
||||||
|
"source":"/",
|
||||||
|
"destination":"/user/plugins/admin"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,351 @@
|
|||||||
|
---
|
||||||
|
PLUGIN_ADMIN:
|
||||||
|
ADMIN_NOSCRIPT_MSG: "يرجى تفعيل جافا سكريبت في المتصفح الخاص بك."
|
||||||
|
ADMIN_BETA_MSG: "هذا إصدار بيتا! استخدم هذا في الإنتاج على مسؤوليتك الخاصة..."
|
||||||
|
ADMIN_REPORT_ISSUE: "وجدت مشكلة؟ الرجاء الإبلاغ عن GitHub."
|
||||||
|
EMAIL_FOOTER: "<a href=\"https://getgrav.org\">الموقع مدعوم من Grav</a> - نظام إدارة الملفات الحديثة"
|
||||||
|
LOGIN_BTN: "تسجل الدخول"
|
||||||
|
LOGIN_BTN_FORGOT: "نسيت"
|
||||||
|
LOGIN_BTN_RESET: "إعادة تعيين كلمة المرور"
|
||||||
|
LOGIN_BTN_SEND_INSTRUCTIONS: "إرسال إرشادات إعادة تعيين"
|
||||||
|
LOGIN_BTN_CLEAR: "مسح النموذج"
|
||||||
|
LOGIN_BTN_CREATE_USER: "أنشاء مستخدم جديد"
|
||||||
|
LOGIN_LOGGED_IN: "لقد تم تسجيل بنجاح"
|
||||||
|
LOGIN_FAILED: "فشل تسجيل الخول"
|
||||||
|
LOGGED_OUT: "لقد قمت بتسجيل الخروج"
|
||||||
|
RESET_NEW_PASSWORD: "إدخال كلمة سر جديدة رجاءً …"
|
||||||
|
RESET_LINK_EXPIRED: "انتهت مدة صلاحية إعادة الارتباط، الرجاء المحاولة مرة أخرى"
|
||||||
|
RESET_PASSWORD_RESET: "لقد تم إعادة تعيين كلمة المرور"
|
||||||
|
RESET_INVALID_LINK: "اللينك خاطئ ، الرجاء المحاولة مرة أخرى"
|
||||||
|
FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL: "تم إرسال إرشادات إعادة تعيين كلمة المرور الخاصة بك عبر البريد الإلكتروني إلى %s"
|
||||||
|
FORGOT_FAILED_TO_EMAIL: "فشل في تعليمات البريد الإلكتروني، الرجاء المحاولة مرة أخرى لاحقاً"
|
||||||
|
FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL: "لا يمكن إعادة تعيين كلمة المرور ل %s، لم يتم تعيين عنوان البريد الإلكتروني"
|
||||||
|
FORGOT_USERNAME_DOES_NOT_EXIST: "لا يوجد المستخدم مع اسم المستخدم <b>%s</b>"
|
||||||
|
FORGOT_EMAIL_NOT_CONFIGURED: "لا يمكن إعادة تعيين كلمة المرور. لم يتم تكوين هذا الموقع لإرسال رسائل البريد الإلكتروني"
|
||||||
|
FORGOT_EMAIL_SUBJECT: "طلب إعادة تعيين كلمة المرور %s"
|
||||||
|
FORGOT_EMAIL_BODY: "<h1>\"إعادة تعيين كلمة المرور\"</h1><p>عزيزي %1$s،</p><p>طلبا قدم في <b>%4$s</b> لإعادة تعيين كلمة المرور الخاصة بك.</p><p><br /><a href=\"%2$s\" class=\"btn-primary\">انقر فوق هذا الخيار لإعادة تعيين الخاص بك كلمة مرور</a><br /><br /></p><p>بدلاً من ذلك، نسخ عنوان URL التالي في شريط العناوين في المستعرض الخاص بك:</p> <p>%2$s</p><p><br />أطيب التحيات،<br /><br />%3$s</p>"
|
||||||
|
MANAGE_PAGES: "إدارة الصفحات"
|
||||||
|
PAGES: "الصفحات"
|
||||||
|
PLUGINS: "الملحقات"
|
||||||
|
PLUGIN: "البرنامج الإضافي"
|
||||||
|
THEMES: "المواضيع"
|
||||||
|
LOGOUT: "تسجيل الخروج"
|
||||||
|
BACK: "الرجوع"
|
||||||
|
NEXT: "التالي"
|
||||||
|
PREVIOUS: "السابق"
|
||||||
|
ADD_PAGE: "إضافة صفحة"
|
||||||
|
MOVE: "انقل"
|
||||||
|
DELETE: "حذف"
|
||||||
|
UNSET: "تراجع عن التعيين"
|
||||||
|
VIEW: "عرض"
|
||||||
|
SAVE: "حفظ"
|
||||||
|
NORMAL: "عادي"
|
||||||
|
EXPERT: "خبير"
|
||||||
|
EXPAND_ALL: "عرض الكل"
|
||||||
|
COLLAPSE_ALL: "طي الكل"
|
||||||
|
ERROR: "خطأ"
|
||||||
|
CLOSE: "أغلق"
|
||||||
|
CANCEL: "إلغاء"
|
||||||
|
CONTINUE: "المتابعة"
|
||||||
|
MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_TITLE: "التأكيد مطلوب"
|
||||||
|
MODAL_CHANGED_DETECTED_TITLE: "تم الكشف عن التغييرات"
|
||||||
|
MODAL_CHANGED_DETECTED_DESC: "وقد تغييرات غير محفوظة. هل أنت متأكد من أنك تريد ترك دون الحفظ؟"
|
||||||
|
MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_TITLE: "التأكيد مطلوب"
|
||||||
|
MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_DESC: "هل أنت متأكد من حذف هذا الملف؟ لا يمكن التراجع عن هذا الإجراء."
|
||||||
|
ADD_FILTERS: "إضافة عامل تصفية"
|
||||||
|
SEARCH_PAGES: "صفحات البحث"
|
||||||
|
VERSION: "النسخة"
|
||||||
|
WAS_MADE_WITH: "تم عمله مع"
|
||||||
|
BY: "بواسطة"
|
||||||
|
UPDATE_THEME: "تحديث الموضوع"
|
||||||
|
UPDATE_PLUGIN: "تحديث البرنامج الإضافي"
|
||||||
|
OF_THIS_THEME_IS_NOW_AVAILABLE: "من هذه السمة الآن متوفرة"
|
||||||
|
OF_THIS_PLUGIN_IS_NOW_AVAILABLE: "من هذه الإضافة متوفرة الآن"
|
||||||
|
AUTHOR: "المؤلّف"
|
||||||
|
HOMEPAGE: "الصفحة الرئيسية"
|
||||||
|
DEMO: "عرض تجريبي"
|
||||||
|
BUG_TRACKER: "متتبع الأخطاء"
|
||||||
|
KEYWORDS: "الكلمات الرئيسية"
|
||||||
|
LICENSE: "الرخصة"
|
||||||
|
DESCRIPTION: "الوصف"
|
||||||
|
README: "الملف التمهيدي"
|
||||||
|
REMOVE_THEME: "إزالة الموضوع"
|
||||||
|
INSTALL_THEME: "تثبيت الموضوع"
|
||||||
|
THEME: "الموضوع"
|
||||||
|
BACK_TO_THEMES: "العودة إلى المواضيع"
|
||||||
|
BACK_TO_PLUGINS: "العودة إلى البرامج الإضافية"
|
||||||
|
CHECK_FOR_UPDATES: "التحقق من وجود تحديثات"
|
||||||
|
ADD: "أَضِف"
|
||||||
|
CLEAR_CACHE: "مسح ذاكرة التخزين المؤقتة"
|
||||||
|
CLEAR_CACHE_ALL_CACHE: "كافة المحفوظات"
|
||||||
|
CLEAR_CACHE_ASSETS_ONLY: "الاصول فقط"
|
||||||
|
CLEAR_CACHE_IMAGES_ONLY: "الصور فقط"
|
||||||
|
CLEAR_CACHE_CACHE_ONLY: "المحفوظات فقط"
|
||||||
|
CLEAR_CACHE_TMP_ONLY: "المؤقتة فقط"
|
||||||
|
UPDATES_AVAILABLE: "تحديثات متوفّرة"
|
||||||
|
DAYS: "أيام"
|
||||||
|
UPDATE: "تحديث"
|
||||||
|
BACKUP: "نسخة احتياطية"
|
||||||
|
BACKUPS: "النسخ الاحتياطية"
|
||||||
|
BACKUP_NOW: "قم بحفض نسخة احتياطية"
|
||||||
|
BACKUPS_STATS: "قم بحفض نسخة احتياطية لاحصائيات"
|
||||||
|
BACKUPS_HISTORY: "قم بحفض نسخة احتياطية للسجل"
|
||||||
|
BACKUPS_PROFILES: "قم بحفض نسخة احتياطي للحسابات"
|
||||||
|
BACKUPS_COUNT: "عدد النسخ الاحتياطية"
|
||||||
|
BACKUPS_PROFILES_COUNT: "عدد الحسابات"
|
||||||
|
BACKUPS_TOTAL_SIZE: "المساحة المستعملة"
|
||||||
|
BACKUPS_NEWEST: "النسخ الاحتياطية الحديثة"
|
||||||
|
BACKUPS_OLDEST: "النسخ الاحتياطية القديمة"
|
||||||
|
BACKUPS_PURGE: "تطهير"
|
||||||
|
BACKUPS_NOT_GENERATED: "لم يتم إنشاء نسخ احتياطية بعد..."
|
||||||
|
BACKUPS_PURGE_NUMBER: "استخدام %s من %s من فتحات النسخ الاحتياطي"
|
||||||
|
BACKUPS_PURGE_TIME: "%s ايام من النسخ الاحتياطية بقية"
|
||||||
|
BACKUPS_PURGE_SPACE: "استعمال %s من %s"
|
||||||
|
BACKUP_DELETED: "تم حذف النسخة الاحتياطية بنجاح"
|
||||||
|
BACKUP_NOT_FOUND: "لم يتم العثور على نسخة احتياطية"
|
||||||
|
BACKUP_DATE: "تاريخ النسخ الاحتياطي"
|
||||||
|
STATISTICS: "إحصائيات"
|
||||||
|
TODAY: "اليوم"
|
||||||
|
WEEK: "اسبوع"
|
||||||
|
MONTH: "شهر"
|
||||||
|
LATEST_PAGE_UPDATES: "آخر التحديثات"
|
||||||
|
MAINTENANCE: "الصيانه"
|
||||||
|
UPDATED: "تم تحديثه"
|
||||||
|
MON: "الإثنين"
|
||||||
|
TUE: "الثلاثاء"
|
||||||
|
WED: "الإربعاء"
|
||||||
|
THU: "الخميس"
|
||||||
|
FRI: "الجمعة"
|
||||||
|
SAT: "السبت"
|
||||||
|
SUN: "الأحد"
|
||||||
|
COPY: "نسخ"
|
||||||
|
EDIT: "تحرير"
|
||||||
|
CREATE: "انشاء"
|
||||||
|
GRAV_ADMIN: "مدير جريف"
|
||||||
|
GRAV_OFFICIAL_PLUGIN: "اضاف رسمية ل جريف"
|
||||||
|
GRAV_OFFICIAL_THEME: "سمة جريف رسمية"
|
||||||
|
PLUGIN_SYMBOLICALLY_LINKED: "هذه الاضافه مرتبطة بمرجع. لن يام اكتشاف التحديثات."
|
||||||
|
THEME_SYMBOLICALLY_LINKED: "هذه السمة مرتبطة بمرجع. لم يتم اكتشاف التحديثات"
|
||||||
|
REMOVE_PLUGIN: "حذف الإضافة"
|
||||||
|
INSTALL_PLUGIN: "تثبيت الإضافة"
|
||||||
|
AVAILABLE: "متوفر"
|
||||||
|
INSTALLED: "مثبت"
|
||||||
|
INSTALL: "تثبيت"
|
||||||
|
ACTIVE_THEME: "سمة نشطة"
|
||||||
|
SWITCHING_TO: "التبديل إلى"
|
||||||
|
SWITCHING_TO_DESCRIPTION: "بالتبديل إلى سمة مختلفة, لايوجد أي ضمانات بأن صفحات الواجهة مدعومة, من المحتمل أن تحدث أخطاء عند محاولة تحميل الصفحات المذكورة."
|
||||||
|
SWITCHING_TO_CONFIRMATION: "هل تريد التبديل الى السمة والمتابعة"
|
||||||
|
CREATE_NEW_USER: "إنشاء مستخدم جديد"
|
||||||
|
REMOVE_USER: "حذف المستخدم"
|
||||||
|
ACCESS_DENIED: "الدخول ممنوع"
|
||||||
|
ACCOUNT_NOT_ADMIN: "لا يملك حسابك أي صلاحيات للإدارة"
|
||||||
|
PHP_INFO: "معلومات بي إش بي"
|
||||||
|
INSTALLER: "المثبت"
|
||||||
|
AVAILABLE_THEMES: "سمات متوفرة"
|
||||||
|
AVAILABLE_PLUGINS: "إضافات متوفرة"
|
||||||
|
INSTALLED_THEMES: "السمات المثبتة"
|
||||||
|
INSTALLED_PLUGINS: "الإضافات المثبتة"
|
||||||
|
BROWSE_ERROR_LOGS: "سجل أخطاء التصفح"
|
||||||
|
SITE: "موقع"
|
||||||
|
INFO: "معلومات"
|
||||||
|
SYSTEM: "النظام"
|
||||||
|
USER: "المستخدم"
|
||||||
|
ADD_ACCOUNT: "إضافة حساب"
|
||||||
|
SWITCH_LANGUAGE: "تبديل اللغة"
|
||||||
|
SUCCESSFULLY_ENABLED_PLUGIN: "تم تفعيل الإضافة بنجاح"
|
||||||
|
SUCCESSFULLY_DISABLED_PLUGIN: "تم إيقاف الإضافة بنجاح"
|
||||||
|
SUCCESSFULLY_CHANGED_THEME: "تم تبديل السمة الإفتراضية بنجاح"
|
||||||
|
INSTALLATION_FAILED: "فشل التثبيت"
|
||||||
|
INSTALLATION_SUCCESSFUL: "تم التثبيت بنجاح"
|
||||||
|
UNINSTALL_FAILED: "فشل الغاء التثبيت"
|
||||||
|
UNINSTALL_SUCCESSFUL: "تم إلغاء التثبيت بنجاح"
|
||||||
|
SUCCESSFULLY_SAVED: "حفظ بنجاح"
|
||||||
|
SUCCESSFULLY_COPIED: "نسخ بنجاح"
|
||||||
|
REORDERING_WAS_SUCCESSFUL: "إعادة ترتيب تم بنجاح"
|
||||||
|
SUCCESSFULLY_DELETED: "تم الحذف بنجاح"
|
||||||
|
SUCCESSFULLY_SWITCHED_LANGUAGE: "تم تغيير اللغة بنجاح"
|
||||||
|
INSUFFICIENT_PERMISSIONS_FOR_TASK: "لديك أذونات غير كافية للقيام بالمهمة"
|
||||||
|
CACHE_CLEARED: "تم مسح الذاكرة المؤقتة"
|
||||||
|
METHOD: "الأسلوب"
|
||||||
|
ERROR_CLEARING_CACHE: "خطأ مسح ذاكرة التخزين المؤقت"
|
||||||
|
AN_ERROR_OCCURRED: "حدث خطأ ما"
|
||||||
|
YOUR_BACKUP_IS_READY_FOR_DOWNLOAD: "النسخة الاحتياطية جاهزة للتحميل"
|
||||||
|
DOWNLOAD_BACKUP: "تنزيل النسخة الاحتياطية"
|
||||||
|
PAGES_FILTERED: "الصفحات التي تمت تصفيتها"
|
||||||
|
NO_PAGE_FOUND: "لم يتم العثور على أي صفحة"
|
||||||
|
INVALID_PARAMETERS: "متغيرات غير صالحة"
|
||||||
|
NO_FILES_SENT: "لا توجد ملفات أرسلت حتى الآن"
|
||||||
|
EXCEEDED_FILESIZE_LIMIT: "تجاوز الحد الأقصى لحجم ملف التكوين بي إتش بي"
|
||||||
|
UNKNOWN_ERRORS: "خطأ غير معروف"
|
||||||
|
EXCEEDED_GRAV_FILESIZE_LIMIT: "تجاوز الحد الأقصى لحجم ملف التكوين بي إتش بي"
|
||||||
|
UNSUPPORTED_FILE_TYPE: "نوع ملف غير معتمد"
|
||||||
|
FAILED_TO_MOVE_UPLOADED_FILE: "فشل في تحميل الملف"
|
||||||
|
FILE_UPLOADED_SUCCESSFULLY: "تم رفع الملف بنجاح"
|
||||||
|
FILE_DELETED: "تم حذف الملف"
|
||||||
|
FILE_COULD_NOT_BE_DELETED: "لا يمكن حذف الملف"
|
||||||
|
FILE_NOT_FOUND: "لم يتم العثور على الملف"
|
||||||
|
NO_FILE_FOUND: "لا يوجد ملف"
|
||||||
|
GRAV_WAS_SUCCESSFULLY_UPDATED_TO: "تم تحديث النظام بنجاح الى"
|
||||||
|
GRAV_UPDATE_FAILED: "فشل تحديث نظام جراف"
|
||||||
|
EVERYTHING_UPDATED: "تم تحديث كل شيء"
|
||||||
|
UPDATES_FAILED: "فشل التحديث"
|
||||||
|
AVATAR_BY: "الصورة الرمزية التي"
|
||||||
|
AVATAR_UPLOAD_OWN: "أو قم بتحميل الخاصة بك..."
|
||||||
|
LAST_BACKUP: "آخر نسخ احتياطي"
|
||||||
|
FULL_NAME: "الإسم الكامل"
|
||||||
|
USERNAME: "إسم المستخدم"
|
||||||
|
EMAIL: "البريد الإلكتروني"
|
||||||
|
USERNAME_EMAIL: "إسم المستخدم أو البريد الإلكتروني"
|
||||||
|
PASSWORD: "كلمة السر"
|
||||||
|
PASSWORD_CONFIRM: "تأكيد كلمة السر"
|
||||||
|
TITLE: "العنوان"
|
||||||
|
ACCOUNT: "الحساب"
|
||||||
|
EMAIL_VALIDATION_MESSAGE: "يجب أن يكون البريد الإلكتروني صحيحاً"
|
||||||
|
PASSWORD_VALIDATION_MESSAGE: "يجب أن تحتوي كلمة المرور على الأقل على رقم وعلى حرف كبير وعلى حرف صغير، و أن تكون مكونة على الأقل من 8 أحرف أو أكثر"
|
||||||
|
LANGUAGE: "اللّغة"
|
||||||
|
LANGUAGE_HELP: "تعيين اللغة المفضلة"
|
||||||
|
MEDIA: "وسائط"
|
||||||
|
DEFAULTS: "الإعدادات الافتراضية"
|
||||||
|
SITE_TITLE: "عنوان الموقع"
|
||||||
|
SITE_TITLE_PLACEHOLDER: "عنوان الموقع العريض"
|
||||||
|
SITE_TITLE_HELP: "العنوان الإفتراضي لموقعك, غالبا يستخدم في السمات"
|
||||||
|
SITE_DEFAULT_LANG: "اللغة الإفتراضية"
|
||||||
|
SITE_DEFAULT_LANG_PLACEHOLDER: "اللغة الإفتراضية المستخدمة في السمات وسم <HTML>"
|
||||||
|
SITE_DEFAULT_LANG_HELP: "اللغة الإفتراضية المستخدمة في السمات وسم <HTML>"
|
||||||
|
DEFAULT_AUTHOR: "المؤلف الافتراضي"
|
||||||
|
DEFAULT_AUTHOR_HELP: "إسم المؤلف الإفتراضي, يستخدم عادة في السمات او محتوى الصفحة"
|
||||||
|
DEFAULT_EMAIL: "البريد الإلكتروني الإفتراضي"
|
||||||
|
DEFAULT_EMAIL_HELP: "البريد الإلكتروني للإشارة في المواضيع أو صفحات"
|
||||||
|
TAXONOMY_TYPES: "أنواع التصنيف"
|
||||||
|
TAXONOMY_TYPES_HELP: "يجب أن يتم تعريف أنواع التصنيف هنا إذا كنت ترغب في استخدامها في صفحات"
|
||||||
|
PAGE_SUMMARY: "ملخص الصفحة"
|
||||||
|
ENABLED: "فعال"
|
||||||
|
ENABLED_HELP: "تمكين صفحة الموجز (الملخص ترجع نفس محتوى الصفحة)"
|
||||||
|
'YES': "نعم"
|
||||||
|
'NO': "لا"
|
||||||
|
SUMMARY_SIZE: "حجم الملخص"
|
||||||
|
SUMMARY_SIZE_HELP: "مقدار أحرف صفحة لاستخدامها كمحتوى موجز"
|
||||||
|
FORMAT: "الشكل"
|
||||||
|
FORMAT_HELP: "اختصار = الاستخدام الأولى لوقوع محدد أو الحجم؛ = فترة طويلة سيتم تجاهل موجز محدد"
|
||||||
|
SHORT: "قصير"
|
||||||
|
LONG: "طويل"
|
||||||
|
DELIMITER: "الفاصل"
|
||||||
|
DELIMITER_HELP: "موجز محدد (الافتراضي '= = =')"
|
||||||
|
METADATA: "البيانات الوصفية"
|
||||||
|
METADATA_HELP: "قيم بيانات التعريف الافتراضية التي سيتم عرضها في كل صفحة ما لم يتم تجاوز الصفحة"
|
||||||
|
NAME: "الإسم"
|
||||||
|
CONTENT: "المحتوى"
|
||||||
|
SIZE: "حجم"
|
||||||
|
ACTION: "اجراء"
|
||||||
|
REDIRECTS_AND_ROUTES: "اعادة التوجيه و المسارات"
|
||||||
|
CUSTOM_REDIRECTS: "تخصيص اعادة التوجيه"
|
||||||
|
CUSTOM_REDIRECTS_HELP: "مسارات لإعادة التوجيه إلى صفحات أخرى. الاستبدال الاساسي ل Regex صحيح"
|
||||||
|
CUSTOM_REDIRECTS_PLACEHOLDER_KEY: "/ الخاص / الاسم المستعار"
|
||||||
|
CUSTOM_REDIRECTS_PLACEHOLDER_VALUE: "/ الخاص /إعادة التوجيه"
|
||||||
|
CUSTOM_ROUTES: "تخصيص المسارات"
|
||||||
|
CUSTOM_ROUTES_HELP: "مسارات لاسماء المستعارة إلى صفحات أخرى. الاستبدال الاساسي ل Regex صحيح"
|
||||||
|
CUSTOM_ROUTES_PLACEHOLDER_KEY: "/ الخاص / الاسم المستعار"
|
||||||
|
CUSTOM_ROUTES_PLACEHOLDER_VALUE: "/ الخاص / المسار"
|
||||||
|
FILE_STREAMS: "تيارات الملف"
|
||||||
|
DEFAULT: "الإعدادات العامة"
|
||||||
|
PAGE_MEDIA: "صور و فيديوهات الصفحة"
|
||||||
|
OPTIONS: "الخيارات"
|
||||||
|
PUBLISHED: "نُشِرَ"
|
||||||
|
PUBLISHED_HELP: "بشكل افتراضي، يتم نشر صفحة إلا إذا قمت بتعيين المنشور: false أو عن طريق publish_date يجري في المستقبل، أو unpublish_date في الماضي"
|
||||||
|
DATE: "التاريخ"
|
||||||
|
PUBLISHED_DATE: "تاريخ النشر"
|
||||||
|
ROBOTS: "الروبوتات"
|
||||||
|
ADVANCED: "خيارات متقدمة"
|
||||||
|
SETTINGS: "الإعدادات"
|
||||||
|
FOLDER_NAME: "إسم المجلد"
|
||||||
|
MENU: "القائمة"
|
||||||
|
USE_GLOBAL: "الاستخدام العام"
|
||||||
|
ROUTABLE: "قابل للتوجيه"
|
||||||
|
ROUTABLE_HELP: "إذا أمكن ولوج هذه الصفحة عبر عنوان URL"
|
||||||
|
VISIBLE: "مرئي"
|
||||||
|
ASCENDING: "تصاعدي"
|
||||||
|
DESCENDING: "تنازلي"
|
||||||
|
PAGE_TITLE: "موضوع عنوان الصفحة"
|
||||||
|
PAGE_TITLE_HELP: "عنوان الصفحة"
|
||||||
|
PAGE: "صفحة"
|
||||||
|
FILENAME: "اسم الملف"
|
||||||
|
PARENT_PAGE: "الصفحة الأصل"
|
||||||
|
HOME_PAGE: "الصفحة الرئيسية"
|
||||||
|
TIMEZONE: "المنطقة الزمنية"
|
||||||
|
LANGUAGES: "اللغات"
|
||||||
|
EXPIRES: "انتهاء الصلاحية"
|
||||||
|
LAST_MODIFIED: "آخر تعديل"
|
||||||
|
SESSION: "الجلسة"
|
||||||
|
CURRENT: "الحالي"
|
||||||
|
SAVE_AS: "حفظ كـ"
|
||||||
|
AND: "و"
|
||||||
|
FULLY_UPDATED: "تم تحديث النظام بالكامل"
|
||||||
|
IGNORE_FILES: "تجاهل الملفات"
|
||||||
|
IGNORE_FOLDERS: "تجاهل المجلدات"
|
||||||
|
GROUPS: "الفِرَق"
|
||||||
|
ADMIN_ACCESS: "النفاذ إلى الإدارة"
|
||||||
|
SITE_ACCESS: "الوصول إلى الموقع"
|
||||||
|
INVALID_SECURITY_TOKEN: "رمز الأمان غير صالح"
|
||||||
|
ACTIVATE: "تفعيل"
|
||||||
|
HIDE_HOME_IN_URLS: "إخفاء المسار الرئيسي في عناوين URL"
|
||||||
|
SESSION_SECURE: "آمن"
|
||||||
|
ADD_FOLDER: "إضافة مجلد"
|
||||||
|
ADD_ITEM: "إضافة عنصر"
|
||||||
|
LOADING: "جار التحميل …"
|
||||||
|
PACKAGES_SUCCESSFULLY_UPDATED: "تم تحديث حزمة أو حزمات بنجاح."
|
||||||
|
INSERT: "إدراج"
|
||||||
|
UNDO: "تراجع"
|
||||||
|
REDO: "إعادة"
|
||||||
|
HEADERS: "العناوين الرأسية"
|
||||||
|
ITALIC: "مائل"
|
||||||
|
LINK: "رابط"
|
||||||
|
IMAGE: "صورة"
|
||||||
|
EDITOR: "المحرر"
|
||||||
|
PREVIEW: "معاينة"
|
||||||
|
FULLSCREEN: "ملء الشاشة"
|
||||||
|
PUBLISHING: "النشر"
|
||||||
|
IMAGE_OPTIONS: "خيارات الصورة"
|
||||||
|
ALL: "الكل"
|
||||||
|
FROM: "من"
|
||||||
|
TO: "إلى"
|
||||||
|
RELEASE_DATE: "تاريخ الإصدار"
|
||||||
|
FORCE_SSL: "فرض SSL"
|
||||||
|
DROPZONE_REMOVE_FILE: "إزالة الملف"
|
||||||
|
TOOLS: "الأدوات"
|
||||||
|
2FA_CODE_INPUT: "000000"
|
||||||
|
2FA_REGENERATE: "إعادة التوليد"
|
||||||
|
VIEW_SITE_TIP: "رؤية الموقع"
|
||||||
|
TOOLS_DIRECT_INSTALL_UPLOAD_BUTTON: "تحميل وتثبيت"
|
||||||
|
SCHEDULER_INSTALL_INSTRUCTIONS: "إرشادات التثبيت"
|
||||||
|
SCHEDULER_INSTALLED_READY: "مثبت وجاهز"
|
||||||
|
EXPERIMENTAL: "تجريبي"
|
||||||
|
FILE: "ملف"
|
||||||
|
ACCOUNTS: "الحسابات"
|
||||||
|
USER_ACCOUNTS: "حسابات المستخدمين"
|
||||||
|
USER_GROUPS: "مجموعات المستخدمين"
|
||||||
|
GROUP_NAME: "اسم الفريق"
|
||||||
|
DISPLAY_NAME: "الاسم المعروض"
|
||||||
|
ICON: "أيقونة"
|
||||||
|
ACCESS: "الوصول"
|
||||||
|
NO_ACCESS: "لا يوجد وصول"
|
||||||
|
SUPER_USER: "مستخدم خارق"
|
||||||
|
ALLOWED: "مسموح"
|
||||||
|
DENIED: "مرفوض"
|
||||||
|
ACCESS_ADMIN_SETTINGS: "الإعدادات"
|
||||||
|
ACCESS_ADMIN_PAGES: "إدارة الصفحات"
|
||||||
|
ACCESS_ADMIN_MAINTENANCE: "صيانة الموقع"
|
||||||
|
ACCESS_ADMIN_STATISTICS: "إحصائيات الموقع"
|
||||||
|
ACCESS_ADMIN_PLUGINS: "إدارة الإضافات"
|
||||||
|
ACCESS_ADMIN_THEMES: "إدارة السمات"
|
||||||
|
ACCESS_ADMIN_TOOLS: "الوصول إلى الأدوات"
|
||||||
|
ACCESS_ADMIN_USERS: "إدارة المستخدمين"
|
||||||
|
USERS: "المستخدمون"
|
||||||
|
ACL: "إدارة صلاحيات الوصول"
|
||||||
|
LANGUAGE_TRANSLATIONS: "الترجمات"
|
||||||
|
LOGOS: "الشعارات"
|
||||||
|
PRESETS: "الإعدادات المسبقة"
|
||||||
|
COLOR_SCHEME_LABEL: "تشكيلات الألوان"
|
||||||
|
CONFIGURATION: "الإعدادات"
|
||||||
|
DASHBOARD: "لوحة المعلومات"
|
||||||
|
MONTHLY_HISTORY: "السجل الشهري"
|
||||||
|
ENABLE: تمكين
|
||||||
@@ -0,0 +1,362 @@
|
|||||||
|
---
|
||||||
|
PLUGIN_ADMIN:
|
||||||
|
ADMIN_NOSCRIPT_MSG: "Моля активирайте JavaScript във вашия браузър."
|
||||||
|
ADMIN_BETA_MSG: "Това е Бета версия! Използвате на ваша отговорност..."
|
||||||
|
ADMIN_REPORT_ISSUE: "Открили сте проблем? Моля, съобщете за него в GitHub."
|
||||||
|
EMAIL_FOOTER: ""
|
||||||
|
LOGIN_BTN: "Вход"
|
||||||
|
LOGIN_BTN_FORGOT: "Забравена парола"
|
||||||
|
LOGIN_BTN_RESET: "Промяна на паролата"
|
||||||
|
LOGIN_BTN_SEND_INSTRUCTIONS: "Изпращане на инструкциите за възстановяването"
|
||||||
|
LOGIN_BTN_CLEAR: "Изтриване на формуляра"
|
||||||
|
LOGIN_BTN_CREATE_USER: "Създаване на потребител"
|
||||||
|
LOGIN_LOGGED_IN: "Влязохте успешно"
|
||||||
|
LOGIN_FAILED: "Влизането не е успешно"
|
||||||
|
LOGGED_OUT: "Излязохте от системата"
|
||||||
|
RESET_NEW_PASSWORD: "Въведете нова парола …"
|
||||||
|
RESET_LINK_EXPIRED: "Връзката за нулиране е изтекла, опитайте отново"
|
||||||
|
RESET_PASSWORD_RESET: "Паролата е променена"
|
||||||
|
RESET_INVALID_LINK: "Използвана невалидна връзка за нулиране, моля опитайте отново"
|
||||||
|
FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL: "На Вашия имейл, бяха изпратени инструкции за възстановяване на паролата"
|
||||||
|
FORGOT_FAILED_TO_EMAIL: "Неуспешно изпращане на имейл с инструкции, опитайте по-късно"
|
||||||
|
FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL: "Паролата на %s не може да бъде обновена, няма въведен имейл адрес"
|
||||||
|
FORGOT_USERNAME_DOES_NOT_EXIST: "Потребител с име <b>%s</b> не съществува"
|
||||||
|
FORGOT_EMAIL_NOT_CONFIGURED: "Не може да обнови паролата. Този сайт не е конфигуриран да изпраща имейли"
|
||||||
|
FORGOT_EMAIL_SUBJECT: "%s - искане за смяна на парола"
|
||||||
|
FORGOT_EMAIL_BODY: "<h1>Смяна на парола</h1><p>Уважаеми %1$s</p><p>На <b>%4$s</b> бе направено искане за смяна на парола.</p><p><br /><a href=\"%2$s\" class=\"btn-primary\">Натиснете тук за да обновите паролата си</a><br /><br /></p><p>Като алтернатива, можете да копирате линка в адресната лента на браузъра си:</p><p>%2$s</p><p><br />С уважение,<br /><br />%3$s</p>"
|
||||||
|
MANAGE_PAGES: "Управление на страниците"
|
||||||
|
PAGES: "Страници"
|
||||||
|
PLUGINS: "Разширения"
|
||||||
|
PLUGIN: "Разширение"
|
||||||
|
THEMES: "Теми"
|
||||||
|
LOGOUT: "Изход"
|
||||||
|
BACK: "Назад"
|
||||||
|
NEXT: "Напред"
|
||||||
|
PREVIOUS: "Назад"
|
||||||
|
ADD_PAGE: "Добавяне на страница"
|
||||||
|
MOVE: "Преместване"
|
||||||
|
DELETE: "Изтриване"
|
||||||
|
UNSET: "Незададен"
|
||||||
|
VIEW: "Виж"
|
||||||
|
SAVE: "Запазване"
|
||||||
|
NORMAL: "Обикновен"
|
||||||
|
EXPERT: "Експертен"
|
||||||
|
EXPAND_ALL: "Разгъване на всички"
|
||||||
|
COLLAPSE_ALL: "Свиване на всички"
|
||||||
|
ERROR: "Грешка"
|
||||||
|
CLOSE: "Затваряне"
|
||||||
|
CANCEL: "Отказ"
|
||||||
|
CONTINUE: "Продължаване"
|
||||||
|
CONFIRM: "Потвърди"
|
||||||
|
MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_TITLE: "Изисква се потвърждение"
|
||||||
|
MODAL_CHANGED_DETECTED_TITLE: "Засечени са промени"
|
||||||
|
MODAL_CHANGED_DETECTED_DESC: "Имате незапазени промени. Наистина ли искате да излезете без да сте ги запазили?"
|
||||||
|
MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_TITLE: "Изисква се потвърждение"
|
||||||
|
MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_DESC: "Наистина ли искате да изтриете този файл? Това действие не може да бъде отменено."
|
||||||
|
MODAL_UPDATE_GRAV_CONFIRMATION_REQUIRED_DESC: "Предстои да обновите Grav до последна версия. Искате ли да продължите?"
|
||||||
|
ADD_FILTERS: "Добавяне на филтри"
|
||||||
|
SEARCH_PAGES: "Търсене"
|
||||||
|
VERSION: "Версия"
|
||||||
|
WAS_MADE_WITH: "е създаден с"
|
||||||
|
BY: "от"
|
||||||
|
UPDATE_THEME: "Актуализация на тема"
|
||||||
|
UPDATE_PLUGIN: "Актуализация на разширение"
|
||||||
|
OF_THIS_THEME_IS_NOW_AVAILABLE: "на тази тема е наличен"
|
||||||
|
OF_THIS_PLUGIN_IS_NOW_AVAILABLE: "на този плъгин е вече наличен"
|
||||||
|
AUTHOR: "Автор"
|
||||||
|
HOMEPAGE: "Страница"
|
||||||
|
DEMO: "Демо"
|
||||||
|
BUG_TRACKER: "Докладване за грешки"
|
||||||
|
KEYWORDS: "Ключови думи"
|
||||||
|
LICENSE: "Лиценз"
|
||||||
|
DESCRIPTION: "Описание"
|
||||||
|
README: "Документация"
|
||||||
|
DOCS: "Документи"
|
||||||
|
REMOVE_THEME: "Премахване на тема"
|
||||||
|
INSTALL_THEME: "Инсталиране на тема"
|
||||||
|
THEME: "Тема"
|
||||||
|
BACK_TO_THEMES: "Обратно към темите"
|
||||||
|
BACK_TO_PLUGINS: "Обратно към разширенията"
|
||||||
|
CHECK_FOR_UPDATES: "Проверка за актуализации"
|
||||||
|
ADD: "Добавяне"
|
||||||
|
CLEAR_CACHE: "Изтриване на временните файлове"
|
||||||
|
CLEAR_CACHE_ALL_CACHE: "Всички"
|
||||||
|
CLEAR_CACHE_ASSETS_ONLY: "Само Assets"
|
||||||
|
CLEAR_CACHE_IMAGES_ONLY: "Само изображенията"
|
||||||
|
CLEAR_CACHE_CACHE_ONLY: "Само временните файлове"
|
||||||
|
CLEAR_CACHE_TMP_ONLY: "само временен"
|
||||||
|
UPDATES_AVAILABLE: "Налични актуализации"
|
||||||
|
DAYS: "Дни"
|
||||||
|
UPDATE: "Актуализация"
|
||||||
|
BACKUP: "Резервно копие"
|
||||||
|
BACKUPS: "Резервно копие"
|
||||||
|
BACKUP_NOW: "Създай резервно копие"
|
||||||
|
BACKUPS_STATS: "Статистика резервни копия"
|
||||||
|
BACKUPS_HISTORY: "История резервни копия"
|
||||||
|
BACKUPS_PURGE_CONFIG: "Настройка изтриване на резервно копие"
|
||||||
|
BACKUPS_PROFILES: "Профили на резервни копия"
|
||||||
|
BACKUPS_COUNT: "Брой резервни копия"
|
||||||
|
BACKUPS_PROFILES_COUNT: "Брой профили"
|
||||||
|
BACKUPS_TOTAL_SIZE: "Използвано пространство"
|
||||||
|
BACKUPS_NEWEST: "Най-ново резервно копие"
|
||||||
|
BACKUPS_OLDEST: "Най-старо резервно копие"
|
||||||
|
BACKUPS_PURGE: "Изтриване"
|
||||||
|
BACKUPS_NOT_GENERATED: "Все още не са генериране резервни копия..."
|
||||||
|
BACKUPS_PURGE_NUMBER: "Използва %s от %s слота за резервни копия"
|
||||||
|
BACKUPS_PURGE_TIME: "Остават %s дни за резервно копие"
|
||||||
|
BACKUPS_PURGE_SPACE: "Използва %s от %s"
|
||||||
|
BACKUP_DELETED: "Успешно изтрито резервно копие"
|
||||||
|
BACKUP_NOT_FOUND: "Не е намерено резервно копие"
|
||||||
|
BACKUP_DATE: "Данни за резервно копие"
|
||||||
|
STATISTICS: "Статистика"
|
||||||
|
VIEWS_STATISTICS: "Статистика на страницата"
|
||||||
|
TODAY: "Днес"
|
||||||
|
WEEK: "Седмица"
|
||||||
|
MONTH: "Месец"
|
||||||
|
LATEST_PAGE_UPDATES: "Скоро актуализирани страници"
|
||||||
|
MAINTENANCE: "Техническа поддръжка"
|
||||||
|
UPDATED: "Актуализиран"
|
||||||
|
MON: "пон"
|
||||||
|
TUE: "вт"
|
||||||
|
WED: "ср"
|
||||||
|
THU: "чт"
|
||||||
|
FRI: "пт"
|
||||||
|
SAT: "сб"
|
||||||
|
SUN: "нд"
|
||||||
|
COPY: "Копиране"
|
||||||
|
EDIT: "Редактиране"
|
||||||
|
CREATE: "Създаване"
|
||||||
|
GRAV_ADMIN: "Grav Admin"
|
||||||
|
GRAV_OFFICIAL_PLUGIN: "Официално разширение на Grav"
|
||||||
|
GRAV_OFFICIAL_THEME: "Официална тема на Grav"
|
||||||
|
PLUGIN_SYMBOLICALLY_LINKED: "Този плъгин е символично свързан. Актуализации няма да бъдат отразени."
|
||||||
|
THEME_SYMBOLICALLY_LINKED: "Тази тема е символично свързана. Актуализации няма да бъдат отразени."
|
||||||
|
REMOVE_PLUGIN: "Премахване на разширение"
|
||||||
|
INSTALL_PLUGIN: "Инсталиране на разширение"
|
||||||
|
AVAILABLE: "Налични"
|
||||||
|
INSTALLED: "Инсталирани"
|
||||||
|
INSTALL: "Инсталиране"
|
||||||
|
ACTIVE_THEME: "Активна тема"
|
||||||
|
SWITCHING_TO: "Превключване към"
|
||||||
|
SWITCHING_TO_DESCRIPTION: "При превключването към различна тема няма гаранция, че всички страници са поддържани, което може да доведе до потенциални грешки при опит за зареждане на тези страници."
|
||||||
|
SWITCHING_TO_CONFIRMATION: "Искате ли да продължите и да превключите към темата"
|
||||||
|
CREATE_NEW_USER: "Създаване на нов потребител"
|
||||||
|
REMOVE_USER: "Премахване на потребител"
|
||||||
|
ACCESS_DENIED: "Нямате достъп"
|
||||||
|
ACCOUNT_NOT_ADMIN: "вашият профил няма администраторски права"
|
||||||
|
PHP_INFO: "Информация за PHP"
|
||||||
|
INSTALLER: "Инсталатор"
|
||||||
|
AVAILABLE_THEMES: "Налични теми"
|
||||||
|
AVAILABLE_PLUGINS: "Налични разширения"
|
||||||
|
INSTALLED_THEMES: "Инсталирани теми"
|
||||||
|
INSTALLED_PLUGINS: "Инсталирани разширения"
|
||||||
|
BROWSE_ERROR_LOGS: "Преглед на дневниците за грешки"
|
||||||
|
SITE: "Сайт"
|
||||||
|
INFO: "Информация"
|
||||||
|
SYSTEM: "Система"
|
||||||
|
USER: "Потребител"
|
||||||
|
ADD_ACCOUNT: "Добавяне на профил"
|
||||||
|
SWITCH_LANGUAGE: "Превключване на езика"
|
||||||
|
SUCCESSFULLY_ENABLED_PLUGIN: "Разширението е активирано успешно"
|
||||||
|
SUCCESSFULLY_DISABLED_PLUGIN: "Разширението е спряно"
|
||||||
|
SUCCESSFULLY_CHANGED_THEME: "Промяната на подразбиращата се тема е успешно"
|
||||||
|
INSTALLATION_FAILED: "Неуспешна инсталация"
|
||||||
|
INSTALLATION_SUCCESSFUL: "Инсталацията е успешна"
|
||||||
|
UNINSTALL_FAILED: "Неуспешно деинсталиране"
|
||||||
|
UNINSTALL_SUCCESSFUL: "Деинсталирането е успешно"
|
||||||
|
SUCCESSFULLY_SAVED: "Успешно запазено"
|
||||||
|
SUCCESSFULLY_COPIED: "Успешно копирано"
|
||||||
|
REORDERING_WAS_SUCCESSFUL: "Записът бе успешен"
|
||||||
|
SUCCESSFULLY_DELETED: "Успешно изтрити"
|
||||||
|
SUCCESSFULLY_SWITCHED_LANGUAGE: "Езикът е променен успешно"
|
||||||
|
INSUFFICIENT_PERMISSIONS_FOR_TASK: "Нямате достатъчно права за тази задача"
|
||||||
|
CACHE_CLEARED: "Временните файлове са изчистени"
|
||||||
|
METHOD: "Метод"
|
||||||
|
ERROR_CLEARING_CACHE: "Грешка при изтриването на временните файлове"
|
||||||
|
AN_ERROR_OCCURRED: "Възникна грешка"
|
||||||
|
YOUR_BACKUP_IS_READY_FOR_DOWNLOAD: "Резервното копие е готово за изтегляне"
|
||||||
|
DOWNLOAD_BACKUP: "Изтегляне на резервното копие"
|
||||||
|
PAGES_FILTERED: "Филтрирани страници"
|
||||||
|
NO_PAGE_FOUND: "Няма намерени страници"
|
||||||
|
INVALID_PARAMETERS: "Невалидни параметри"
|
||||||
|
NO_FILES_SENT: "Няма изпратени файлове"
|
||||||
|
EXCEEDED_FILESIZE_LIMIT: "Надхвърлен лимит за размер на PHP конфигурационен файл"
|
||||||
|
EXCEEDED_POSTMAX_LIMIT: "Превишена PHP конфигурация post_max_size"
|
||||||
|
UNKNOWN_ERRORS: "Неизвестни грешки"
|
||||||
|
EXCEEDED_GRAV_FILESIZE_LIMIT: "Превишен лимит за размера на конфигурационен GRAV файл"
|
||||||
|
UNSUPPORTED_FILE_TYPE: "Този файлов формат не се поддържа"
|
||||||
|
FAILED_TO_MOVE_UPLOADED_FILE: "Преместването на качения файл не е успешно."
|
||||||
|
FILE_UPLOADED_SUCCESSFULLY: "Файлът е качен успешно"
|
||||||
|
FILE_DELETED: "Файлът е изтрит"
|
||||||
|
FILE_COULD_NOT_BE_DELETED: "Файлът не може да бъде изтрит"
|
||||||
|
FILE_NOT_FOUND: "Файлът не е намерен"
|
||||||
|
NO_FILE_FOUND: "Няма намерени файлове"
|
||||||
|
GRAV_WAS_SUCCESSFULLY_UPDATED_TO: "Grav беше успешно актуализиран до"
|
||||||
|
GRAV_UPDATE_FAILED: "Актуализацията на Grav е неуспешна"
|
||||||
|
EVERYTHING_UPDATED: "Всичко е актуализирано"
|
||||||
|
UPDATES_FAILED: "Актуализациите не бяха успшено"
|
||||||
|
AVATAR_BY: "Аватар от"
|
||||||
|
AVATAR_UPLOAD_OWN: "Или качи собствени..."
|
||||||
|
LAST_BACKUP: "Последно резервно копие"
|
||||||
|
FULL_NAME: "Пълно име"
|
||||||
|
USERNAME: "Потребителско име"
|
||||||
|
EMAIL: "Ел. поща"
|
||||||
|
USERNAME_EMAIL: "Потребителско име или инейл"
|
||||||
|
PASSWORD: "Парола"
|
||||||
|
PASSWORD_CONFIRM: "Потвърждение на паролата"
|
||||||
|
TITLE: "Титла"
|
||||||
|
ACCOUNT: "Профил"
|
||||||
|
EMAIL_VALIDATION_MESSAGE: "Ел. поща трябва да бъде валидна"
|
||||||
|
PASSWORD_VALIDATION_MESSAGE: "Паролата трябва да съдържа поне един номер, една главна буква, една малка буква и да съдържа поне 8 или повече знака"
|
||||||
|
LANGUAGE: "Език"
|
||||||
|
LANGUAGE_HELP: "Задаване на любим език"
|
||||||
|
MEDIA: "Медиа"
|
||||||
|
DEFAULTS: "По подразбиране"
|
||||||
|
SITE_TITLE: "Заглавие на сайта"
|
||||||
|
SITE_TITLE_PLACEHOLDER: "Заглавие за всички страници"
|
||||||
|
SITE_TITLE_HELP: "Подразбиращо се заглавие за вашият сайт, често се използва от темите"
|
||||||
|
SITE_DEFAULT_LANG: "Език по подразбиране"
|
||||||
|
SITE_DEFAULT_LANG_PLACEHOLDER: "Език по подразбиране, използван от <HTML> тага на темите"
|
||||||
|
SITE_DEFAULT_LANG_HELP: "Език по подразбиране, използван от <HTML> тага на темите"
|
||||||
|
DEFAULT_AUTHOR: "Подразбиращ се автор"
|
||||||
|
DEFAULT_AUTHOR_HELP: "Име на автор по подразбиране, често използвано в теми или страници"
|
||||||
|
DEFAULT_EMAIL: "Имейл по подразбиране"
|
||||||
|
DEFAULT_EMAIL_HELP: "Имейл по подразбиране, използван в теми или страници"
|
||||||
|
TAXONOMY_TYPES: "Видове таксономии"
|
||||||
|
TAXONOMY_TYPES_HELP: "Типовете таксономия трябва да бъдат дефинирани тук, ако искате да ги използвате в страници"
|
||||||
|
PAGE_SUMMARY: "Резюме на страницата"
|
||||||
|
ENABLED: "Включен"
|
||||||
|
ENABLED_HELP: "Разреши извлечение (извлечението връща същото съдържание, като в страницата)"
|
||||||
|
'YES': "Да"
|
||||||
|
'NO': "Не"
|
||||||
|
SUMMARY_SIZE: "Размер на резюмето"
|
||||||
|
SUMMARY_SIZE_HELP: "Брой знаци, които да бъдат използвани при създаването на резюме за страницата"
|
||||||
|
FORMAT: "Формат"
|
||||||
|
SHORT: "Къс"
|
||||||
|
LONG: "Дълъг"
|
||||||
|
DELIMITER: "Делител"
|
||||||
|
DELIMITER_HELP: "Разделител на извлечението (по подразбиране '===')"
|
||||||
|
METADATA: "Мета-данни"
|
||||||
|
METADATA_HELP: "Ще бъдат показани метадата стойностите по подразбиране на всяка страница, освен ако не са отхвърлени от страницата"
|
||||||
|
NAME: "Име"
|
||||||
|
CONTENT: "Съдържание"
|
||||||
|
SIZE: "Размер"
|
||||||
|
ACTION: "Действие"
|
||||||
|
REDIRECTS_AND_ROUTES: "Пренасочвания и пътища"
|
||||||
|
CUSTOM_REDIRECTS: "Потребителски пренасочвания"
|
||||||
|
CUSTOM_REDIRECTS_HELP: "маршрути за пренасочване към сдруга страница. Подмяна със стандартен Regex е валидна"
|
||||||
|
CUSTOM_REDIRECTS_PLACEHOLDER_KEY: "/your/alias"
|
||||||
|
CUSTOM_REDIRECTS_PLACEHOLDER_VALUE: "/your/redirect"
|
||||||
|
CUSTOM_ROUTES: "Потребителски пренасочвания"
|
||||||
|
CUSTOM_ROUTES_PLACEHOLDER_KEY: "/your/alias"
|
||||||
|
CUSTOM_ROUTES_PLACEHOLDER_VALUE: "/your/route"
|
||||||
|
FILE_STREAMS: "Потоци файлове"
|
||||||
|
DEFAULT: "По подразбиране"
|
||||||
|
PAGE_MEDIA: "Страница с медия"
|
||||||
|
OPTIONS: "Опции"
|
||||||
|
PUBLISHED: "Публикувано"
|
||||||
|
PUBLISHED_HELP: "По подразбиране страницата се публикува, освен ако не е зададео published: false или publish_date е в бъдеще или unpublish_date е в миналото"
|
||||||
|
DATE: "Дата"
|
||||||
|
DATE_HELP: "Променливата за дата позволява да се зададе дата асоциирана със страницата."
|
||||||
|
PUBLISHED_DATE: "Дата на публикуване"
|
||||||
|
PUBLISHED_DATE_HELP: "Дата, на която автоматично ще се публикува."
|
||||||
|
UNPUBLISHED_DATE: "Дата на непубликуване"
|
||||||
|
UNPUBLISHED_DATE_HELP: "Може да зададе дата за автоматично непубликуване."
|
||||||
|
ROBOTS: "Роботи"
|
||||||
|
TAXONOMIES: "Таксономии"
|
||||||
|
TAXONOMY: "Таксономия"
|
||||||
|
ADVANCED: "Разширено"
|
||||||
|
SETTINGS: "Настройки"
|
||||||
|
FOLDER_NUMERIC_PREFIX: "Цифров префикс на папка"
|
||||||
|
FOLDER_NUMERIC_PREFIX_HELP: "Цифров префикс осигуряващ ръчно подреждане и по-добра видимост"
|
||||||
|
FOLDER_NAME: "Име на папка"
|
||||||
|
FOLDER_NAME_HELP: "Името на папката, която ще се съхрани във файлова система за тази страница"
|
||||||
|
PARENT: "Родител"
|
||||||
|
DEFAULT_OPTION_ROOT: "- Коренова папка -"
|
||||||
|
DEFAULT_OPTION_SELECT: "- Избор -"
|
||||||
|
DISPLAY_TEMPLATE: "Показване на шаблон"
|
||||||
|
ORDERING: "Подреждане"
|
||||||
|
PAGE_ORDER: "Подредба на страниците"
|
||||||
|
OVERRIDES: "Замени"
|
||||||
|
MENU: "Меню"
|
||||||
|
MENU_HELP: "Стрингът, който ще се използва в меню. Ако не се зададе, ще се използва Title."
|
||||||
|
SLUG: "Слъг"
|
||||||
|
SLUG_HELP: "Слъг променливата Ви позволява да зададете конкретна порция от URL на страницата"
|
||||||
|
SLUG_VALIDATE_MESSAGE: "Слъговете могат да съдържат само малки букви, цифри и тирета"
|
||||||
|
PROCESS: "Обработване"
|
||||||
|
PROCESS_HELP: "Контролирайте обработката на страниците. Може да бъде за отделна страница или глобално"
|
||||||
|
DEFAULT_CHILD_TYPE: "Тип по подразбиране"
|
||||||
|
USE_GLOBAL: "Използвай глобални"
|
||||||
|
ROUTABLE: "Маршрутизируем"
|
||||||
|
ROUTABLE_HELP: "Ако страницата е достъпна през URL"
|
||||||
|
CACHING: "Създаване на временни файлове"
|
||||||
|
VISIBLE: "Видим"
|
||||||
|
VISIBLE_HELP: "Определя дали една страница е видима в навигацията."
|
||||||
|
DISABLED: "Изключено"
|
||||||
|
ITEMS: "Елементи"
|
||||||
|
ORDER_BY: "Подреждане по"
|
||||||
|
ORDER: "Подреждане"
|
||||||
|
FOLDER: "Папка"
|
||||||
|
ASCENDING: "Възходящо"
|
||||||
|
DESCENDING: "Низходящо"
|
||||||
|
PAGE_TITLE: "Заглавие на страницата"
|
||||||
|
PAGE_TITLE_HELP: "Заглавието на страницата"
|
||||||
|
PAGE: "Страница"
|
||||||
|
FRONTMATTER: "Встъпление"
|
||||||
|
FILENAME: "Име на файла"
|
||||||
|
PARENT_PAGE: "Родителска страница"
|
||||||
|
HOME_PAGE: "Начална страница"
|
||||||
|
HOME_PAGE_HELP: "Страницата, която Grav ще използва по подразбиране за начална страница"
|
||||||
|
DEFAULT_THEME: "Тема по подразбиране"
|
||||||
|
DEFAULT_THEME_HELP: "Задаване на темата по подразбиране, която Grav ще използва (по подразбиране това е Antimatter)"
|
||||||
|
TIMEZONE: "Часова зона"
|
||||||
|
TIMEZONE_HELP: "Презаписване на времевата зона на сървъра"
|
||||||
|
SHORT_DATE_FORMAT: "Кратък формат дата"
|
||||||
|
SHORT_DATE_FORMAT_HELP: "Задай кратък формат дата, който може да се използва от темите"
|
||||||
|
LONG_DATE_FORMAT: "Пълен формат дата"
|
||||||
|
LONG_DATE_FORMAT_HELP: "Задай пълен формат дата, който може да се използва от темите"
|
||||||
|
DEFAULT_ORDERING: "По подразбиране"
|
||||||
|
DEFAULT_ORDERING_DEFAULT: "По подразбиране - според име на папка"
|
||||||
|
DEFAULT_ORDERING_FOLDER: "Папка - според името на папката без префикс"
|
||||||
|
DEFAULT_ORDERING_TITLE: "Заглавие - според заглавно поле в главата"
|
||||||
|
DEFAULT_ORDERING_DATE: "Дата - според поле за дата в главата"
|
||||||
|
DEFAULT_ORDER_DIRECTION: "Подреждане по подразбиране"
|
||||||
|
DEFAULT_ORDER_DIRECTION_HELP: "Посоката на страниците в списък"
|
||||||
|
DEFAULT_PAGE_COUNT: "Брой страници по подразбиране"
|
||||||
|
DEFAULT_PAGE_COUNT_HELP: "Максимален брой страници в списък по подразбиране"
|
||||||
|
DATE_BASED_PUBLISHING: "Публикуване според датата"
|
||||||
|
DATE_BASED_PUBLISHING_HELP: "Автоматично (не)публикувай постове според датата"
|
||||||
|
EVENTS: "Събития"
|
||||||
|
EVENTS_HELP: "Пускане или спиране на специфични събития. Спирането на някои събития може да счупи определени приставки"
|
||||||
|
REDIRECT_DEFAULT_ROUTE: "Пренасочване на пътя по подразбиране"
|
||||||
|
REDIRECT_DEFAULT_ROUTE_HELP: "Автоматично пренасочване към пътя по подразбиране на страницата"
|
||||||
|
LANGUAGES: "Езици"
|
||||||
|
SUPPORTED: "Поддържани"
|
||||||
|
SUPPORTED_HELP: "Списък от двубуквени езикови кодове, отделени със запетая (пример 'bg,en,de')"
|
||||||
|
HTTP_HEADERS: "HTTP заглавки"
|
||||||
|
EXPIRES: "Изтича на"
|
||||||
|
CACHE_CONTROL: "HTTP кеш-контрол"
|
||||||
|
LAST_MODIFIED: "Последна промяна"
|
||||||
|
CACHE_CHECK_METHOD: "Мотод проверка на кеша"
|
||||||
|
CACHE_DRIVER: "Кеш драйвър"
|
||||||
|
CACHE_PREFIX: "Кеш представка"
|
||||||
|
CACHE_PURGE: "Изтриване на стр кеш"
|
||||||
|
LIFETIME: "Продължителност на живот"
|
||||||
|
GZIP_COMPRESSION: "Gzip компресия"
|
||||||
|
GZIP_COMPRESSION_HELP: "Разреши GZip компресия на Grav страницата, за оптимизация."
|
||||||
|
CSS_PIPELINE: "CSS pipeline"
|
||||||
|
JAVASCRIPT_PIPELINE: "JavaScript pipeline"
|
||||||
|
LOG_HANDLER: "Обработка на лога"
|
||||||
|
DEBUGGER: "Дибъгър"
|
||||||
|
SESSION: "Сесия"
|
||||||
|
CURRENT: "Текущ"
|
||||||
|
SAVE_AS: "Запази като"
|
||||||
|
AND: "и"
|
||||||
|
UPDATE_AVAILABLE: "Налична актуализация"
|
||||||
|
FULLY_UPDATED: "Напълно обновен"
|
||||||
|
SAVE_LOCATION: "Местоположение за запис"
|
||||||
|
IGNORE_HIDDEN_HELP: "Игнорирай всички файлове и папки, които започват с точка"
|
||||||
|
WRAPPED_SITE: "Опаковани сайт"
|
||||||
|
CONFIGURATION: "Настройки"
|
||||||
|
TIMEOUT: "Таймаут"
|
||||||
|
DASHBOARD: "Контролен панел"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
PLUGIN_ADMIN:
|
||||||
|
LOGIN_BTN: "লগ ইন"
|
||||||
|
LOGIN_BTN_FORGOT: "ভুলে গেছি"
|
||||||
@@ -0,0 +1,591 @@
|
|||||||
|
---
|
||||||
|
PLUGIN_ADMIN:
|
||||||
|
ADMIN_BETA_MSG: "Un ermaeziadenn beta an hini eo! Arverit en endro produadur gant evezh..."
|
||||||
|
ADMIN_REPORT_ISSUE: "Kavet hoc'h eus ur gudenn? Danevellit anezhi war Github."
|
||||||
|
LOGIN_BTN: "Anv arveriad"
|
||||||
|
LOGIN_BTN_FORGOT: "Ankouaet"
|
||||||
|
LOGIN_BTN_RESET: "Adderaouekaat ar ger-tremen"
|
||||||
|
LOGIN_BTN_SEND_INSTRUCTIONS: "Kas an ditouroù adderaouekaat"
|
||||||
|
LOGIN_BTN_CLEAR: "Skarzhañ ar furmskrid"
|
||||||
|
LOGIN_BTN_CREATE_USER: "Krouiñ an arveriad"
|
||||||
|
LOGIN_LOGGED_IN: "Kennasket oc'h gant berzh"
|
||||||
|
LOGIN_FAILED: "C'hwitadenn war ar c'hennask"
|
||||||
|
LOGGED_OUT: "Digennasket oc'h"
|
||||||
|
RESET_NEW_PASSWORD: "Enankit ur ger-tremen nevez …"
|
||||||
|
RESET_LINK_EXPIRED: "Diamzeret eo an ere adderaouekaat, klaskit en-dro"
|
||||||
|
RESET_PASSWORD_RESET: "Adderaouekaet eo bet ar ger-tremen"
|
||||||
|
RESET_INVALID_LINK: "Ere adderaouekaat didalvoudek, klaskit en-dro"
|
||||||
|
FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL: "Kaset eo bet an ditouroù da adderaouekaat ho ker-tremen d'ho chmolec'h postel"
|
||||||
|
FORGOT_FAILED_TO_EMAIL: "C'hwitadenn en ur gas an ditouroù, klaskit en-dro diwezhatoc'h"
|
||||||
|
FORGOT_CANNOT_RESET_EMAIL_NO_EMAIL: "N'haller ket adderaouekaat ar ger-tremen evit %s, chomlec'h postel ebet arventennet"
|
||||||
|
FORGOT_USERNAME_DOES_NOT_EXIST: "N'eus ket eus an arveriad gant an anv <b>%s</b>"
|
||||||
|
FORGOT_EMAIL_NOT_CONFIGURED: "N'haller ket adderaouekaat ar ger-tremen. N'eo ket kefluniet al lec'hienn evit kas posteloù"
|
||||||
|
FORGOT_EMAIL_SUBJECT: "%s Goulenn adderaouekaat ar ger-tremen"
|
||||||
|
FORGOT_EMAIL_BODY: "<h1>Adderaouekaat ar ger-tremen/h1><p>%1$s,</p><p>Graet eo bet un azgoulenn war <b>%4$s</b> evit adderaouekaat ho ker-tremen.</p><p><br /><a href=\"%2$s\" class=\"btn-primary\">Klikit amañ da adderaouekaat ho ker-tremen</a><br /><br /></p><p>Gallout a rit ivez eilañ an URL da heul er varrenn chomlec'h en ho merdeer:</p> <p>%2$s</p><p><br />A galon,<br /><br />%3$s</p>"
|
||||||
|
MANAGE_PAGES: "Ardeiñ ar pajennoù"
|
||||||
|
PAGES: "Pajennoù"
|
||||||
|
PLUGINS: "Enlugelladoù"
|
||||||
|
PLUGIN: "Enlugellad"
|
||||||
|
THEMES: "Neuzioù"
|
||||||
|
LOGOUT: "Digennaskañ"
|
||||||
|
BACK: "Distreiñ"
|
||||||
|
NEXT: "War-lerc'h"
|
||||||
|
PREVIOUS: "Diaraog"
|
||||||
|
ADD_PAGE: "Ouzhpennañ ur bajenn"
|
||||||
|
MOVE: "Dilec'hiañ"
|
||||||
|
DELETE: "Dilemel"
|
||||||
|
VIEW: "Gwel"
|
||||||
|
SAVE: "Enrollañ"
|
||||||
|
NORMAL: "Reoliek"
|
||||||
|
EXPERT: "Kemplezhoc'h"
|
||||||
|
EXPAND_ALL: "Astenn an holl"
|
||||||
|
COLLAPSE_ALL: "Bihanaat an holl"
|
||||||
|
ERROR: "Fazi"
|
||||||
|
CLOSE: "Serriñ"
|
||||||
|
CANCEL: "Nullañ"
|
||||||
|
CONTINUE: "Kenderc'hel"
|
||||||
|
MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_TITLE: "Kadarnadur azgoulennet"
|
||||||
|
MODAL_CHANGED_DETECTED_TITLE: "Kemmoù dinoet"
|
||||||
|
MODAL_CHANGED_DETECTED_DESC: "Kemmoù dienrollet a zo. Sur oc'h e fell deoc'h kuitaat hep enrollañ?"
|
||||||
|
MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_TITLE: "Kadarnadur azgoulennet"
|
||||||
|
MODAL_DELETE_FILE_CONFIRMATION_REQUIRED_DESC: "Sur oc'h e fell deoc'h dilemel ar restr-mañ? N'haller ket dizober ar gwered-mañ."
|
||||||
|
ADD_FILTERS: "Ouzhpennañ siloù"
|
||||||
|
SEARCH_PAGES: "Klask pajennoù"
|
||||||
|
VERSION: "Handelv"
|
||||||
|
WAS_MADE_WITH: "Savet eo bet gant"
|
||||||
|
BY: "Gant"
|
||||||
|
UPDATE_THEME: "Hizivaat an neuz"
|
||||||
|
UPDATE_PLUGIN: "Hizivaat an enlugellad"
|
||||||
|
OF_THIS_THEME_IS_NOW_AVAILABLE: "an neuz-mañ a zo hegerz"
|
||||||
|
OF_THIS_PLUGIN_IS_NOW_AVAILABLE: "an enlugellad-mañ a zo hegerz"
|
||||||
|
AUTHOR: "Aozer"
|
||||||
|
HOMEPAGE: "Pennbajenn"
|
||||||
|
DEMO: "Tañva"
|
||||||
|
BUG_TRACKER: "Heulier beugoù"
|
||||||
|
KEYWORDS: "Gerioù-alc'hwez"
|
||||||
|
LICENSE: "Lañvaz"
|
||||||
|
DESCRIPTION: "Deskrivadur"
|
||||||
|
README: "Skoazell"
|
||||||
|
REMOVE_THEME: "Dilemel an neuz"
|
||||||
|
INSTALL_THEME: "Staliañ an neuz"
|
||||||
|
THEME: "Neuz"
|
||||||
|
BACK_TO_THEMES: "Distreiñ d'an neuzioù"
|
||||||
|
BACK_TO_PLUGINS: "Distreiñ d'an enlugelladoù"
|
||||||
|
CHECK_FOR_UPDATES: "Klask hizivadennoù"
|
||||||
|
ADD: "Ouzhpenañ"
|
||||||
|
CLEAR_CACHE: "Skarzhañ ar c'hrubuilh"
|
||||||
|
CLEAR_CACHE_ALL_CACHE: "Ar c'hrubuilh a-bezh"
|
||||||
|
CLEAR_CACHE_ASSETS_ONLY: "Loazioù nemetken"
|
||||||
|
CLEAR_CACHE_IMAGES_ONLY: "Skeudennoù nemetken"
|
||||||
|
CLEAR_CACHE_CACHE_ONLY: "Krubuilh nemetken"
|
||||||
|
CLEAR_CACHE_TMP_ONLY: "Padennek hepken"
|
||||||
|
UPDATES_AVAILABLE: "Hizivadennoù hegerz"
|
||||||
|
DAYS: "Devezhioù"
|
||||||
|
UPDATE: "Hizivadenn"
|
||||||
|
BACKUP: "Gwared"
|
||||||
|
STATISTICS: "Stadegoù"
|
||||||
|
TODAY: "Hiziv"
|
||||||
|
WEEK: "Sizhun"
|
||||||
|
MONTH: "Miz"
|
||||||
|
LATEST_PAGE_UPDATES: "Hizivadennoù diwezhañ ar bajenn"
|
||||||
|
MAINTENANCE: "Trezalc'h"
|
||||||
|
UPDATED: "Hizivaet"
|
||||||
|
MON: "Lun"
|
||||||
|
TUE: "Meu"
|
||||||
|
WED: "Mer"
|
||||||
|
THU: "Yao"
|
||||||
|
FRI: "Gwe"
|
||||||
|
SAT: "Sad"
|
||||||
|
SUN: "Sul"
|
||||||
|
COPY: "Eilañ"
|
||||||
|
EDIT: "Kemmañ"
|
||||||
|
CREATE: "Krouiñ"
|
||||||
|
GRAV_ADMIN: "Merour Grav"
|
||||||
|
GRAV_OFFICIAL_PLUGIN: "Enlugellad Kefridiel Grav"
|
||||||
|
GRAV_OFFICIAL_THEME: "Neuz Kefridiel Grav"
|
||||||
|
PLUGIN_SYMBOLICALLY_LINKED: "Gant un ere arouezus eo lakaet an enlugellad. Ne vo ket dinoet an hizivadennoù."
|
||||||
|
THEME_SYMBOLICALLY_LINKED: "Gant un ere arouezus eo lakaet an neuz. Ne vo ket dinoet an hizivadennoù"
|
||||||
|
REMOVE_PLUGIN: "Dilemel an enlugellad"
|
||||||
|
INSTALL_PLUGIN: "Staliañ an enlugellad"
|
||||||
|
AVAILABLE: "Hegerz"
|
||||||
|
INSTALLED: "Staliet"
|
||||||
|
INSTALL: "Staliañ"
|
||||||
|
ACTIVE_THEME: "Neuz oberiant"
|
||||||
|
SWITCHING_TO: "Kemmañ da"
|
||||||
|
SWITCHING_TO_DESCRIPTION: "En ur gemmañ d'un neuz disheñvel n'eus gwarant ebet e vo skoret an holl frammoù pajenn, ar pezh a zegasfe fazioù en ur gargañ ar pajennoù-mañ."
|
||||||
|
SWITCHING_TO_CONFIRMATION: "Fellout a ra deoc'h kenderc'hel ha kemmañ an neuz"
|
||||||
|
CREATE_NEW_USER: "Krouiñ un arveriad nevez"
|
||||||
|
REMOVE_USER: "Dilemel an arveriad"
|
||||||
|
ACCESS_DENIED: "Haeziñ nac'het"
|
||||||
|
ACCOUNT_NOT_ADMIN: "n'hoc'h eus ket an aotreoù a-zere"
|
||||||
|
PHP_INFO: "Titouroù PHP"
|
||||||
|
INSTALLER: "Stalier"
|
||||||
|
AVAILABLE_THEMES: "Neuzioù hegerz"
|
||||||
|
AVAILABLE_PLUGINS: "Enlugelladoù hegerz"
|
||||||
|
INSTALLED_THEMES: "Neuzioù staliet"
|
||||||
|
INSTALLED_PLUGINS: "Enlugelladoù staliet"
|
||||||
|
BROWSE_ERROR_LOGS: "Furchal er c'herzhlevr fazioù"
|
||||||
|
SITE: "Lec'hienn"
|
||||||
|
INFO: "Titouroù"
|
||||||
|
SYSTEM: "Reizhiad"
|
||||||
|
USER: "Arveriad"
|
||||||
|
ADD_ACCOUNT: "Ouzhpennañ ur gont"
|
||||||
|
SWITCH_LANGUAGE: "Kemmañ ar yezh"
|
||||||
|
SUCCESSFULLY_ENABLED_PLUGIN: "Gweredekaet an enlugellad gant berzh"
|
||||||
|
SUCCESSFULLY_DISABLED_PLUGIN: "Diweredekaet an enlugellad gant berzh"
|
||||||
|
SUCCESSFULLY_CHANGED_THEME: "Kemmet an neuz dre ziouer gant berzh"
|
||||||
|
INSTALLATION_FAILED: "C'hwitadenn war ar staliadur"
|
||||||
|
INSTALLATION_SUCCESSFUL: "Berzh war ar staliadur"
|
||||||
|
UNINSTALL_FAILED: "C'hwitadenn war an distaliadur"
|
||||||
|
UNINSTALL_SUCCESSFUL: "Berzh war an distaliadur"
|
||||||
|
SUCCESSFULLY_SAVED: "Enrollet gant berzh"
|
||||||
|
SUCCESSFULLY_COPIED: "Eilet gant berzh"
|
||||||
|
REORDERING_WAS_SUCCESSFUL: "Adurzhiet gant berzh"
|
||||||
|
SUCCESSFULLY_DELETED: "Dilamet gant berzh"
|
||||||
|
SUCCESSFULLY_SWITCHED_LANGUAGE: "Kemmet ar yezh gant berzh"
|
||||||
|
INSUFFICIENT_PERMISSIONS_FOR_TASK: "N'ho peus ket trawalc'h a aotreoù evit ar gwered"
|
||||||
|
CACHE_CLEARED: "Skarzhet ar c'hrubuilh"
|
||||||
|
METHOD: "Hentenn"
|
||||||
|
ERROR_CLEARING_CACHE: "Fazi en ur skarzhañ ar c'hrubuilh"
|
||||||
|
AN_ERROR_OCCURRED: "Degouezhet ez eus bet ur fazi"
|
||||||
|
YOUR_BACKUP_IS_READY_FOR_DOWNLOAD: "Prest eo ho kwared da vezañ pellgarget"
|
||||||
|
DOWNLOAD_BACKUP: "Pellgargañ ar gwared"
|
||||||
|
PAGES_FILTERED: "Pajennoù silet"
|
||||||
|
NO_PAGE_FOUND: "Pajenn ebet kavet"
|
||||||
|
INVALID_PARAMETERS: "Arventennoù didalvoudek"
|
||||||
|
NO_FILES_SENT: "Restr ebet kaset"
|
||||||
|
UNKNOWN_ERRORS: "Fazioù dianav"
|
||||||
|
UNSUPPORTED_FILE_TYPE: "Doare restr amskor"
|
||||||
|
FAILED_TO_MOVE_UPLOADED_FILE: "C'hwitadenn en ur zilec'hiañ ar restr pellgaset."
|
||||||
|
FILE_UPLOADED_SUCCESSFULLY: "Restr pellgaset gant berzh"
|
||||||
|
FILE_DELETED: "Restr dilamet"
|
||||||
|
FILE_COULD_NOT_BE_DELETED: "N'haller ket dilemel ar restr"
|
||||||
|
FILE_NOT_FOUND: "N'eus ket bet kavet ar restr"
|
||||||
|
NO_FILE_FOUND: "Restr ebet kavet"
|
||||||
|
GRAV_WAS_SUCCESSFULLY_UPDATED_TO: "Hizivaet eo bet Grav da"
|
||||||
|
GRAV_UPDATE_FAILED: "C'hwitadenn war hizivadenn Grav"
|
||||||
|
EVERYTHING_UPDATED: "Hizivaet pep tra"
|
||||||
|
UPDATES_FAILED: "C'hwitadenn war a hizivadennoù"
|
||||||
|
AVATAR_BY: "Avatar gant"
|
||||||
|
LAST_BACKUP: "Gwared diwezhañ"
|
||||||
|
FULL_NAME: "Anv klok"
|
||||||
|
USERNAME: "Anv arveriad"
|
||||||
|
EMAIL: "Chomlec'h postel"
|
||||||
|
PASSWORD: "Ger-tremen"
|
||||||
|
PASSWORD_CONFIRM: "Kadarnat ar ger-tremen"
|
||||||
|
TITLE: "Titl"
|
||||||
|
ACCOUNT: "Kont"
|
||||||
|
EMAIL_VALIDATION_MESSAGE: "Ret eo reiñ ur chomlec'h postel talvoudek"
|
||||||
|
PASSWORD_VALIDATION_MESSAGE: "Ret eo d'ar ger-tremen enderc'hel ur niverenn, ul lizherenn vras hag ul lizherenn vihan hag 8 arouezenn d'an nebeutañ"
|
||||||
|
LANGUAGE: "Yezh"
|
||||||
|
LANGUAGE_HELP: "Dibabit ar yezh"
|
||||||
|
MEDIA: "Media"
|
||||||
|
DEFAULTS: "Dre ziouer"
|
||||||
|
SITE_TITLE: "Titl al lec'hienn"
|
||||||
|
SITE_TITLE_PLACEHOLDER: "Titl ledan al lec'hienn"
|
||||||
|
SITE_TITLE_HELP: "Titl dre ziouer ho lec'hienn, arveret en neuzioù"
|
||||||
|
SITE_DEFAULT_LANG: "Yezh defot"
|
||||||
|
DEFAULT_AUTHOR: "Aozer dre ziouer"
|
||||||
|
DEFAULT_AUTHOR_HELP: "Un anv aozer dre ziouer, arveret en neuzioù pe er pajennoù"
|
||||||
|
DEFAULT_EMAIL: "Chomlec'h postel dre ziouer"
|
||||||
|
DEFAULT_EMAIL_HELP: "Ur chomlec'h postel dre ziouer, arveret en neuze pe er pajennoù"
|
||||||
|
TAXONOMY_TYPES: "Doareoù rummadoù"
|
||||||
|
TAXONOMY_TYPES_HELP: "An doareoù rummadoù a rank bezañ erspizet amañ ma fell deoc'h arverañ anezho er pajennoù"
|
||||||
|
PAGE_SUMMARY: "Berradenn ar bajenn"
|
||||||
|
ENABLED: "Gweredekaet"
|
||||||
|
ENABLED_HELP: "Gweredekaat berradenn ar bajenn (ar verradenn a zistro an hevelep tra hag endalc'had ar bajenn)"
|
||||||
|
'YES': "Ya"
|
||||||
|
'NO': "Ket"
|
||||||
|
SUMMARY_SIZE: "Ment ar verradenn"
|
||||||
|
SUMMARY_SIZE_HELP: "An niverenn a arouezenn da arverañ evel berradenn ur bajenn"
|
||||||
|
FORMAT: "Mentrezh"
|
||||||
|
FORMAT_HELP: "berr = arverañ degouezh kentañ an disranner pe ment; hir = laosket e vo an disranner berradenn a-gostez"
|
||||||
|
SHORT: "Berr"
|
||||||
|
LONG: "Hir"
|
||||||
|
DELIMITER: "Disranner"
|
||||||
|
DELIMITER_HELP: "Disranner ar verradenn (diouer '===')"
|
||||||
|
METADATA: "Metaroadennoù"
|
||||||
|
METADATA_HELP: "Skrammet e vo ar gwerzhioù metaroadennoù dre ziouer war an holl bajennoù war-bouez m'eo flastret gant ar bajenn"
|
||||||
|
NAME: "Anv"
|
||||||
|
CONTENT: "Endalc'had"
|
||||||
|
REDIRECTS_AND_ROUTES: "Adheñchañ ha treugoù"
|
||||||
|
CUSTOM_REDIRECTS: "Adheñchañ personelaet"
|
||||||
|
CUSTOM_REDIRECTS_HELP: "treugoù da adheñchañ davet pajennoù all. Talvoudek eo an amsaviñ regex"
|
||||||
|
CUSTOM_REDIRECTS_PLACEHOLDER_KEY: "/un/anv"
|
||||||
|
CUSTOM_REDIRECTS_PLACEHOLDER_VALUE: "/un/adeñchañ"
|
||||||
|
CUSTOM_ROUTES: "Treugoù personelaet"
|
||||||
|
CUSTOM_ROUTES_HELP: "treugoù da adheñchañ davet pajennoù all. Talvoudek eo an amsaviñ Regex"
|
||||||
|
CUSTOM_ROUTES_PLACEHOLDER_KEY: "/ho/anv"
|
||||||
|
CUSTOM_ROUTES_PLACEHOLDER_VALUE: "/ho/treug"
|
||||||
|
FILE_STREAMS: "Lanvioù restroù"
|
||||||
|
DEFAULT: "Dre ziouer"
|
||||||
|
PAGE_MEDIA: "Media ar bajenn"
|
||||||
|
OPTIONS: "Dibarzhioù"
|
||||||
|
PUBLISHED: "Embannet"
|
||||||
|
PUBLISHED_HELP: "Dre ziouer eo embannet ur bajenn war-bouez m'eo lakaet da \"Embannet: ket\" pe dre un deiziad embann en dazont, pe un deiziad diembannañ tremenet"
|
||||||
|
DATE: "Deiziad"
|
||||||
|
DATE_HELP: "Ar vaezienn deiziad a laosk ac'hanoc'h da arventennañ un deiziad liammet gant ar bajenn."
|
||||||
|
PUBLISHED_DATE: "Deiziad embann"
|
||||||
|
PUBLISHED_DATE_HELP: "Gallout a rit reiñ un deiziad da embann ent emgefreek."
|
||||||
|
UNPUBLISHED_DATE: "Deiziad diembannañ"
|
||||||
|
UNPUBLISHED_DATE_HELP: "Gallout a rit reiñ un deiziad evit diembannañ ent emgefreek."
|
||||||
|
ROBOTS: "Robotoù"
|
||||||
|
TAXONOMIES: "Rummadoù"
|
||||||
|
TAXONOMY: "Rummad"
|
||||||
|
ADVANCED: "Kempleshoc'h"
|
||||||
|
SETTINGS: "Arventennoù"
|
||||||
|
FOLDER_NUMERIC_PREFIX: "Rakger niverel an teuliad"
|
||||||
|
FOLDER_NUMERIC_PREFIX_HELP: "Rakgerioù niverel evit urzhiañ gant an dorn ha emplegañ ar gwelusted"
|
||||||
|
FOLDER_NAME: "Anv an teuliad"
|
||||||
|
FOLDER_NAME_HELP: "Anv an teuliad a vo kadavet er reizhiad restroù evit ar bajenn"
|
||||||
|
PARENT: "Kar"
|
||||||
|
DEFAULT_OPTION_ROOT: "- Gwrizienn -"
|
||||||
|
DEFAULT_OPTION_SELECT: "- Diuzañ -"
|
||||||
|
DISPLAY_TEMPLATE: "Skrammañ ar patrom"
|
||||||
|
DISPLAY_TEMPLATE_HELP: "An doare pajenn a ziviz peseurt patrom twig a zeznaouo ar bajenn"
|
||||||
|
ORDERING: "Urzh"
|
||||||
|
PAGE_ORDER: "Urzh ar pajennoù"
|
||||||
|
OVERRIDES: "Flastrañ"
|
||||||
|
MENU: "Lañser"
|
||||||
|
MENU_HELP: "Ar chadennoù da arverañ el lañser. Ma n'eo ket arventennet, Titl a vo arveret."
|
||||||
|
SLUG: "Slug"
|
||||||
|
SLUG_HELP: "An argemenn slug a aotren ac'hanoc'h da arventennañ URL lodenn ar bajenn"
|
||||||
|
SLUG_VALIDATE_MESSAGE: "Lizherennoù bihan, sifroù ha tiredoù a c'hall bezañ er slug hepken"
|
||||||
|
PROCESS: "Keweriañ"
|
||||||
|
PROCESS_HELP: "Reoliañ penaos eo keweriet ar pajennoù. Gallout a ra bezañ lakaet dre bajenn kentoc'h eget en un doare hollek"
|
||||||
|
DEFAULT_CHILD_TYPE: "Doare bugel dre ziouer"
|
||||||
|
USE_GLOBAL: "Arverañ Hollek"
|
||||||
|
ROUTABLE: "Treugus"
|
||||||
|
ROUTABLE_HELP: "M'eo haezadus ar bajenn dre un URL"
|
||||||
|
CACHING: "Krubuilhiñ"
|
||||||
|
VISIBLE: "Gwelus"
|
||||||
|
VISIBLE_HELP: "Despizañ a ra gwelusted ur bajenn er merdeiñ."
|
||||||
|
DISABLED: "Diweredekaet"
|
||||||
|
ITEMS: "Ergorennoù"
|
||||||
|
ORDER_BY: "Urzhiañ dre"
|
||||||
|
ORDER: "Urzh"
|
||||||
|
FOLDER: "Teuliad"
|
||||||
|
ASCENDING: "War-gresk"
|
||||||
|
DESCENDING: "War-zigresk"
|
||||||
|
PAGE_TITLE: "Titl ar bajenn"
|
||||||
|
PAGE_TITLE_HELP: "Titl ar bajenn"
|
||||||
|
PAGE: "Pajenn"
|
||||||
|
FRONTMATTER: "Frontmatter"
|
||||||
|
FILENAME: "Anv ar restr"
|
||||||
|
PARENT_PAGE: "Pajenn gar"
|
||||||
|
HOME_PAGE: "Pennbajenn"
|
||||||
|
HOME_PAGE_HELP: "Pajenn arveret gant Grav evel pajenn degemer dre ziouer"
|
||||||
|
DEFAULT_THEME: "Neuz dre ziouer"
|
||||||
|
DEFAULT_THEME_HELP: "Arventennañ an neuz arveret gant Grav dre ziouer (Antimatter dre ziouer)"
|
||||||
|
TIMEZONE: "Gwerzhid-eur"
|
||||||
|
TIMEZONE_HELP: "Flastrañ gwerzhid-eur dre ziouer an dafariad"
|
||||||
|
SHORT_DATE_FORMAT: "Mentrezh skrammañ an deiziad berr"
|
||||||
|
SHORT_DATE_FORMAT_HELP: "Arventennan ar mentrezh deiziad berr da arverañ gant an neuzioù"
|
||||||
|
LONG_DATE_FORMAT: "Mentrezh deiziad hir"
|
||||||
|
LONG_DATE_FORMAT_HELP: "Arventennañ ar mentrezh deiziad hir a vo arveret en neuzioù"
|
||||||
|
DEFAULT_ORDERING: "Urzh dre ziouer"
|
||||||
|
DEFAULT_ORDERING_HELP: "Pajennoù er roll a vo skrammet en urzh-mañ war-bouez m'eo flastret"
|
||||||
|
DEFAULT_ORDERING_DEFAULT: "Dre ziouer - diazezet war anv an teuliad"
|
||||||
|
DEFAULT_ORDERING_FOLDER: "Teuliad - diazezet war anv an teuliad hep rakger"
|
||||||
|
DEFAULT_ORDERING_TITLE: "Titl - diazezet war vaezienn ditl an talbenn"
|
||||||
|
DEFAULT_ORDERING_DATE: "Deiziad - diazezet war vaezienn deiziad an talbenn"
|
||||||
|
DEFAULT_ORDER_DIRECTION: "Tu an urzh dre ziouer"
|
||||||
|
DEFAULT_ORDER_DIRECTION_HELP: "Tu ar pajennoù er roll"
|
||||||
|
DEFAULT_PAGE_COUNT: "Niver a bajennoù dre ziouer"
|
||||||
|
DEFAULT_PAGE_COUNT_HELP: "Niver a bajennoù en ur roll d'ar muiañ"
|
||||||
|
DATE_BASED_PUBLISHING: "Embannadenn diazezet war un deiziad"
|
||||||
|
DATE_BASED_PUBLISHING_HELP: "(Di)embann pennadoù ent emgefreek hervez o deiziad"
|
||||||
|
EVENTS: "Darvoudoù"
|
||||||
|
EVENTS_HELP: "(Di)weredekaat darvoudoù resis. Diweredekaat anezho a c'hall terriñ enlugelladoù"
|
||||||
|
REDIRECT_DEFAULT_ROUTE: "Adheñchañ an treug dre ziouer"
|
||||||
|
REDIRECT_DEFAULT_ROUTE_HELP: "Adheñchañ ent emgefreek d'un treug pajenn dre ziouer"
|
||||||
|
LANGUAGES: "Yezhoù"
|
||||||
|
SUPPORTED: "Skoret"
|
||||||
|
SUPPORTED_HELP: "Roll bonegoù yezh 2 lizherenn ennañ disrannet gant skejoù (skouer: 'br, cy, en')"
|
||||||
|
TRANSLATIONS_FALLBACK: "Troidigezh dre ziouer"
|
||||||
|
TRANSLATIONS_FALLBACK_HELP: "Arverañ un droidigezh all ma n'eus ket eus ar tezh oberiant"
|
||||||
|
ACTIVE_LANGUAGE_IN_SESSION: "Yezhoù oberiant en estez"
|
||||||
|
ACTIVE_LANGUAGE_IN_SESSION_HELP: "Kadaviñ ar yezh oberiant en estez"
|
||||||
|
HTTP_HEADERS: "Talbennoù HTTP"
|
||||||
|
EXPIRES: "Diamzer"
|
||||||
|
EXPIRES_HELP: "Arventennañ an talbenn diamzeriñ e eilennoù."
|
||||||
|
LAST_MODIFIED: "Kemmet da ziwezhañ"
|
||||||
|
LAST_MODIFIED_HELP: "Arventennañ an talbenn kemmet da ziwezhañ a c'hall skoazell da wellaat ar proksi ha krubuilh ar merdeer"
|
||||||
|
ETAG: "ETag"
|
||||||
|
ETAG_HELP: "Arventennañ an talbenn etag evit skoazell da c'houzout peur eo bet kemmet ur bajenn"
|
||||||
|
VARY_ACCEPT_ENCODING: "Vary accept encoding"
|
||||||
|
VARY_ACCEPT_ENCODING_HELP: "Arventennañ a ra an talbenn `Vary: Accept Encoding` evit skoazell gant ar proksi hag ar c'hrubuilh CDN"
|
||||||
|
MARKDOWN_EXTRA_HELP: "Gweredekaat ar skor dre ziouer evit Markdown Ectra - https://michelf.ca/projects/php-markdown/extra/"
|
||||||
|
AUTO_LINE_BREAKS: "Tremen d'al linenn ent emgefreek"
|
||||||
|
AUTO_LINE_BREAKS_HELP: "Gweredekaat skor tremen al linenn ent emgefreek e Markdown"
|
||||||
|
AUTO_URL_LINKS: "Ereoù URL emgefreek"
|
||||||
|
AUTO_URL_LINKS_HELP: "Gweredekaat amdroadur emgefreek an URLoù da ereoù HTML"
|
||||||
|
ESCAPE_MARKUP: "Gwareziñ an HTML"
|
||||||
|
ESCAPE_MARKUP_HELP: "Gwareziñ ar c'hlavioù e elfennoù HTML"
|
||||||
|
CACHING_HELP: "Trec'haoler hollek evit (di)weredekaat krubuilh Grav"
|
||||||
|
CACHE_CHECK_METHOD: "Hentenn gwiriekaat ar c'hrubuilh"
|
||||||
|
CACHE_CHECK_METHOD_HELP: "Dibab an hentenn arveret gant Grav evit gwiriekaat m'eo bet kemmer ar restoù pajenn."
|
||||||
|
CACHE_DRIVER: "Sturier Krubuilh"
|
||||||
|
CACHE_DRIVER_HELP: "Dibab pe sturier krubuilh a zo arveret Grav. 'Dinoiñ emgefreek' a glask kavout pe zoare a zo an hini gwellañ"
|
||||||
|
CACHE_PREFIX: "Rakger ar c'hrubuilh"
|
||||||
|
CACHE_PREFIX_HELP: "Lodenn naoudi an alc'hwez Grav. Na gemmit anezhi ma n'ouzit ket petra rit."
|
||||||
|
CACHE_PREFIX_PLACEHOLDER: "Deveret eus an URL diazez (flastret en un enkañ ur chadenn dargouezhek)"
|
||||||
|
LIFETIME: "Padelezh buhez"
|
||||||
|
LIFETIME_HELP: "Arventennañ padelezh ar c'hrubuilh e eilennoù. 0 = anvevenn"
|
||||||
|
GZIP_COMPRESSION: "Koazhadur Gzip"
|
||||||
|
GZIP_COMPRESSION_HELP: "Gweredekaat koazhadur Gzip ar bajenn Grav evit kreskiñ an digonusted."
|
||||||
|
TWIG_TEMPLATING: "Patromiñ Twig"
|
||||||
|
TWIG_CACHING: "Krubuilh Twig"
|
||||||
|
TWIG_CACHING_HELP: "Reoliañ wikefre krubuilh Twig. Laoskit gweredekaet evit an digonusted gwellañ."
|
||||||
|
TWIG_DEBUG: "Diveugañ Twig"
|
||||||
|
TWIG_DEBUG_HELP: "Aotren an dibarzh evit chom hep kargañ an askouezh diveugañ Twig"
|
||||||
|
DETECT_CHANGES: "Dinoiñ ar c'hemmoù"
|
||||||
|
DETECT_CHANGES_HELP: "Adkempunet e vo krubuilh Twig ent emgefreek ma vez dinoet kemmoù er patromoù Twig"
|
||||||
|
AUTOESCAPE_VARIABLES: "Gwareziñ an argemennoù ent emgefreek"
|
||||||
|
AUTOESCAPE_VARIABLES_HELP: "Gwareziñ an holl argemennoù ent emgefreek. Moarvat e torro ho lec'hienn"
|
||||||
|
ASSETS: "Madoù"
|
||||||
|
CSS_PIPELINE: "Arrevellañ CSS"
|
||||||
|
CSS_PIPELINE_HELP: "Arrevellañ ar CSS a zo unvanadur meur a loaz CSS en ur restr hepken"
|
||||||
|
CSS_PIPELINE_INCLUDE_EXTERNALS: "Ebarzhiñ restroù estren en arrevellañ CSS"
|
||||||
|
CSS_PIPELINE_INCLUDE_EXTERNALS_HELP: "URLoù diavaez a zo gant daveoù restroù daveel a-wechoù ha ne rankont ket bezañ arrevellet"
|
||||||
|
CSS_PIPELINE_BEFORE_EXCLUDES: "Deoueziñ an arrevellañ CSS da gentañ"
|
||||||
|
CSS_PIPELINE_BEFORE_EXCLUDES_HELP: "Deoueziñ an arrevellañ CSS a-raok kement dave CSS all ha n'int ket enkorfet"
|
||||||
|
CSS_MINIFY: "Bihanadur CSS"
|
||||||
|
CSS_MINIFY_HELP: "Bihanaat ar CSS e-pad an arrevellañ"
|
||||||
|
CSS_MINIFY_WINDOWS_OVERRIDE: "Amsaviñ bihanadur ar CSS Windows"
|
||||||
|
CSS_MINIFY_WINDOWS_OVERRIDE_HELP: "Amsaviñ ar bihanadur evit savennoù Windows. Faos dre ziouer abalamour da ThreadStackSize"
|
||||||
|
CSS_REWRITE: "Adskrivañ CSS"
|
||||||
|
CSS_REWRITE_HELP: "Adskrivañ kement URL daveel CSS e-pad an arrevellañ"
|
||||||
|
JAVASCRIPT_PIPELINE: "Arrevellañ Javascript"
|
||||||
|
JAVASCRIPT_PIPELINE_HELP: "An arrevellañ JS a zo unvanadur meur a restr JS en ur restr hepken"
|
||||||
|
JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS: "Enkorfañ ar JS diavaez evit an arrevellañ"
|
||||||
|
JAVASCRIPT_PIPELINE_INCLUDE_EXTERNALS_HELP: "Urloù diavaez o deus daveoù restroù daveel a-wechoù ha ne rankont ket bezañ arrevellet"
|
||||||
|
JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES: "Arrevellañ JS da gentañ"
|
||||||
|
JAVASCRIPT_PIPELINE_BEFORE_EXCLUDES_HELP: "Deoueziñ an arrevellañ JS a-raok kement dave JS all ha n'int ket enkorfet"
|
||||||
|
JAVASCRIPT_MINIFY: "Bihanat ar javascript"
|
||||||
|
JAVASCRIPT_MINIFY_HELP: "Bihanaat ar JS e-pad an arrevellañ"
|
||||||
|
ENABLED_TIMESTAMPS_ON_ASSETS: "Gweredekaat ar boneg-amzer war al loazioù"
|
||||||
|
ENABLED_TIMESTAMPS_ON_ASSETS_HELP: "Gweredekaat bonegoù-amzer al loazioù"
|
||||||
|
COLLECTIONS: "Dastumadegoù"
|
||||||
|
ERROR_HANDLER: "Dornataour fazioù"
|
||||||
|
DISPLAY_ERRORS: "Skrammañ ar fazioù"
|
||||||
|
DISPLAY_ERRORS_HELP: "Skrammañ ur bajenn fazi gant munudoù"
|
||||||
|
LOG_ERRORS: "Kerzhlevr ar fazioù"
|
||||||
|
LOG_ERRORS_HELP: "Lakaat kerzhlevr ar fazioù en teuliad /logs"
|
||||||
|
DEBUGGER: "Diveuger"
|
||||||
|
DEBUGGER_HELP: "Gweredekaat diveuger Grav hag an arventennoù da heul"
|
||||||
|
DEBUG_TWIG: "Diveugañ Twig"
|
||||||
|
DEBUG_TWIG_HELP: "Gweredekaat diveugañ ar patromoù Twig"
|
||||||
|
SHUTDOWN_CLOSE_CONNECTION: "Shutdown a serr ar c'hennask"
|
||||||
|
SHUTDOWN_CLOSE_CONNECTION_HELP: "Serriñ ar c'hennask a-raok gervel onShutdown(). 'false' evit diveugañ"
|
||||||
|
DEFAULT_IMAGE_QUALITY: "Perzhded skeudenn dre ziouer"
|
||||||
|
DEFAULT_IMAGE_QUALITY_HELP: "Perzhded skeudenn dre ziouer da arverañ e-pad adstandilhonañ ar skeudennoù (85%)"
|
||||||
|
CACHE_ALL: "Lakaat an holl skeudennoù er c'hrubuilh"
|
||||||
|
CACHE_ALL_HELP: "Lakaat an holl skeudennoù da dremen dre reizhiad krubuilh Grav zoken ma n'o deus dornatadur media ebet"
|
||||||
|
IMAGES_DEBUG: "Rouedigell diveugañ ar skeudenn"
|
||||||
|
IMAGES_DEBUG_HELP: "Diskouez un diflugell a-us d'ar skeudennoù a ziskouez an donder piksel pa labourer war Retina da skouer"
|
||||||
|
UPLOAD_LIMIT: "Bevenn ment ar restroù da bellgas"
|
||||||
|
UPLOAD_LIMIT_HELP: "Lakaat ar ment restroù uhelañ e eizhbitoù (0 a zo anvevenn)"
|
||||||
|
ENABLE_MEDIA_TIMESTAMP: "Gweredekaat ar boneg-amzer war ar media"
|
||||||
|
ENABLE_MEDIA_TIMESTAMP_HELP: "Ouzhpennañ ur boneg-amzer diazezet war an deiziad kemmadur evit pep elfenn media"
|
||||||
|
SESSION: "Estez"
|
||||||
|
SESSION_ENABLED_HELP: "Gweredekaat skor an estez evit Grav"
|
||||||
|
SESSION_NAME_HELP: "Un naoudi arveret da stummañ anv toupin an estez"
|
||||||
|
ABSOLUTE_URLS: "URL dizave"
|
||||||
|
ABSOLUTE_URLS_HELP: "URLoù dizave pe daveel evit 'base_url'"
|
||||||
|
PARAMETER_SEPARATOR: "Disranner arventenn"
|
||||||
|
PARAMETER_SEPARATOR_HELP: "An disranner evit an arventennoù tremenet a c'hall bezañ kemmet evit Apache war Windows"
|
||||||
|
TASK_COMPLETED: "Trevell echuet"
|
||||||
|
EVERYTHING_UP_TO_DATE: "Pep tra a zo hizivaet"
|
||||||
|
UPDATES_ARE_AVAILABLE: "hizivadennoù hegerz"
|
||||||
|
IS_AVAILABLE_FOR_UPDATE: "a zo gant un hizivadenn hegerz"
|
||||||
|
IS_NOW_AVAILABLE: "a zo hegerz"
|
||||||
|
CURRENT: "Bremanel"
|
||||||
|
UPDATE_GRAV_NOW: "Hizivaat Grav bremañ"
|
||||||
|
GRAV_SYMBOLICALLY_LINKED: "Gant un ere arouezel eo staliet Grav. Dihegerz eo an hizivadenn"
|
||||||
|
UPDATING_PLEASE_WAIT: "Oc'h hizivaat... gortozit, emañ o pellgargañ"
|
||||||
|
OF_THIS: "eus an"
|
||||||
|
OF_YOUR: "eus ho"
|
||||||
|
HAVE_AN_UPDATE_AVAILABLE: "en deus un hizivadenn hegerz"
|
||||||
|
SAVE_AS: "Enrollañ evel"
|
||||||
|
MODAL_DELETE_PAGE_CONFIRMATION_REQUIRED_DESC: "Sur oc'h e fell deoc'h dilemel ar bajenn-mañ hag holl he bugale? M'eo troet ar bajenn en ur yezh all e vo miret an troidigezhioù a rankout a reot o dilemel en un doare distag. E mod all e vo dilamet teuliad ar bajenn gant an is-pajennoù. N'haller ket dizober ar gwered-mañ."
|
||||||
|
AND: "ha"
|
||||||
|
UPDATE_AVAILABLE: "Hizivadenn hegerz"
|
||||||
|
METADATA_KEY: "Alc'hwez (sk. 'Gerioù-alc'hwez')"
|
||||||
|
METADATA_VALUE: "Gwerzh (sk. 'Blog, Grav')"
|
||||||
|
USERNAME_HELP: "Etre 3 ha 16 arouezenn e rank an anv arveriad bezañ o kontañ al lizherennoù bihan, an niverennoù, an islinennoù hag ar barrennigoù. N'eo ket aotreet al lizherennoù bras, an esaouennoù hag an arouezennoù arbennik"
|
||||||
|
FULLY_UPDATED: "Hizivaet"
|
||||||
|
SAVE_LOCATION: "Lec'hiadur enrollañ"
|
||||||
|
PAGE_FILE: "Patrom pajenn"
|
||||||
|
PAGE_FILE_HELP: "Anv restr patrom ar bajenn, ha patrom skrammañ ar bajenn dre ziouer"
|
||||||
|
NO_USER_ACCOUNTS: "Kont arveriad ebet kavet, krouit unan da gentañ..."
|
||||||
|
REDIRECT_TRAILING_SLASH: "Adheñchañ ar veskell dibenn"
|
||||||
|
REDIRECT_TRAILING_SLASH_HELP: "Ober un adheñchañ 301 e-lerc'h merañ an beskell dibenn an URI en un doare treuzwelus."
|
||||||
|
DEFAULT_DATE_FORMAT: "Mentrezh deiziad ar bajenn"
|
||||||
|
DEFAULT_DATE_FORMAT_HELP: "Mentrezh deiziad ar bajenn arveret gant Grav. Dre ziouer, Grav a glask divinout mentrezh an deiziad met gallout a rit erspizañ unan gant kevreadur deiziad PHP (sk.: Y-m-d H:i)"
|
||||||
|
DEFAULT_DATE_FORMAT_PLACEHOLDER: "Divinout en emgefreek"
|
||||||
|
IGNORE_FILES: "Leuskel restroù a-gostez"
|
||||||
|
IGNORE_FILES_HELP: "Restroù da leuskel a-gostez e-pad keweriañ ar pajennoù"
|
||||||
|
IGNORE_FOLDERS: "Leuskel teuliadoù a-gostez"
|
||||||
|
IGNORE_FOLDERS_HELP: "Teuliadoù resis da leuskel a-gostez e-pad keweriañ ar pajennoù"
|
||||||
|
HTTP_ACCEPT_LANGUAGE: "Lakaat yezh ar merdeer"
|
||||||
|
HTTP_ACCEPT_LANGUAGE_HELP: "Gallout a rit klask arventennañ ar yezh gant hini ar talbenn `http_accept_language` ar merdeer"
|
||||||
|
OVERRIDE_LOCALE: "Flastrañ ar yezh"
|
||||||
|
OVERRIDE_LOCALE_HELP: "Flastrañ arventenn yezh PHP diazezet war ar yezh vremanel"
|
||||||
|
REDIRECT: "Adheñchañ ar bajenn"
|
||||||
|
REDIRECT_HELP: "Enankit hent ur bajenn pe un URL diavaez da adheñchañ ar bajenn. Sk. '/un/hent' pe 'http://ulload.bzh'"
|
||||||
|
PLUGIN_STATUS: "Stad an elugellad"
|
||||||
|
INCLUDE_DEFAULT_LANG: "Enkorfañ ar yezh dre ziouer"
|
||||||
|
INCLUDE_DEFAULT_LANG_HELP: "Ouzhpennañ a raio ar yezh dre ziouer en holl URLoù er yezh dre ziouer. Sk. '/br/blog/post'"
|
||||||
|
ALLOW_URL_TAXONOMY_FILTERS: "URL siloù rummad"
|
||||||
|
ALLOW_URL_TAXONOMY_FILTERS_HELP: "Dastumadegoù pajennoù a aotren ac'hanoc'h da silañ dre '/rummad:gwerzh'."
|
||||||
|
REDIRECT_DEFAULT_CODE: "Boneg adheñchan dre ziouer"
|
||||||
|
REDIRECT_DEFAULT_CODE_HELP: "Boneg stad HTTP da arverañ evit adheñchañ"
|
||||||
|
IGNORE_HIDDEN: "Leuskel ar re kuzhet a-gostez"
|
||||||
|
IGNORE_HIDDEN_HELP: "Leuskel an holl restroù ha teuliadoù a grog gant ur POENT"
|
||||||
|
WRAPPED_SITE: "Lec'hienn enkorfet"
|
||||||
|
WRAPPED_SITE_HELP: "Evit ma ouife an neuzioù/enlugelladoù m'eo enkorfet Grav en ur savenn all"
|
||||||
|
FALLBACK_TYPES: "Aotren doareoù fallback"
|
||||||
|
FALLBACK_TYPES_HELP: "Doareoù restr aotreet a c'hall bezañ kavet m'int haezet dre hent ar bajenn. An holl zoareoù media skoret dre ziouer."
|
||||||
|
INLINE_TYPES: "Doareoù fallback enkorfet"
|
||||||
|
INLINE_TYPES_HELP: "Ur roll doareoù restroù a rank bezañ skrammet en un doare enkorfet kentoc'h eget pellgarget"
|
||||||
|
APPEND_URL_EXT: "Ouzhpennañ an astenn d'an URL"
|
||||||
|
APPEND_URL_EXT_HELP: "Ouzhpennañ a raio un astenn personelaet da URL ar bajenn. Talvezout a ra e glasko Grav ur patrom anvet `<patrom>.<astenn>.twig`"
|
||||||
|
PAGE_MODES: "Modoù pajenn"
|
||||||
|
PAGE_TYPES: "Doareoù pajenn"
|
||||||
|
ACCESS_LEVELS: "Liveoù haeziñ"
|
||||||
|
GROUPS: "Strolladoù"
|
||||||
|
GROUPS_HELP: "Roll ar strolladoù gant an arveriad enno"
|
||||||
|
ADMIN_ACCESS: "Haeziñ ardoer"
|
||||||
|
SITE_ACCESS: "Haeziñ d'al lec'hienn"
|
||||||
|
INVALID_SECURITY_TOKEN: "Reveziadenn diogelroez didalvoudek"
|
||||||
|
ACTIVATE: "Gweredekaat"
|
||||||
|
TWIG_UMASK_FIX: "Ratreadur Umask"
|
||||||
|
TWIG_UMASK_FIX_HELP: "Twig a grou ar restroù krubuilh gant 0755 dre ziouer, ar ratreañ a lak anezho da 0755"
|
||||||
|
CACHE_PERMS: "Aotreoù ar c'hrubuilh"
|
||||||
|
CACHE_PERMS_HELP: "Aotreoù dre ziouer teuliad ar c'hrubuilh. 0755 pe 0775 peurvuiañ, hervez ar c'hefluniadur"
|
||||||
|
REMOVE_SUCCESSFUL: "Dilamet gant berzh"
|
||||||
|
REMOVE_FAILED: "C'hwitadenn war an dilemel"
|
||||||
|
HIDE_HOME_IN_URLS: "Kuzhat hent ar pennbajenn en URL"
|
||||||
|
HIDE_HOME_IN_URLS_HELP: "Gwiriekaat a raio n'eo ket daveet hent skoueriek an degemer gant hentoù dre ziouer ar pajennoù dindan an degemer"
|
||||||
|
TWIG_FIRST: "Keweriañ an Twig da gentañ"
|
||||||
|
TWIG_FIRST_HELP: "M'ho peus gweredekaat keweriañ ar bajenn Twig e c'hallit kefluniañ Twig evit e geweriañ a-raok pe goude ar Markdown"
|
||||||
|
SESSION_SECURE: "Diogel"
|
||||||
|
SESSION_SECURE_HELP: "M'eo gwir, diskouez a ra eo ret d'ar c'hehentiñ evit an toupin-mañ bezañ graet war un treuzkas diogel. DIWALLIT: Gweredekait an dra-se war lec'hiennoù e HTTPS nemetken"
|
||||||
|
SESSION_HTTPONLY: "HTTP nemetken"
|
||||||
|
SESSION_HTTPONLY_HELP: "M'eo gwir, diskouez a ra eo ret d'ar c'hehentiñ evit an toupin-mañ bezañ graet war un treuzkas HTTP ha n'eo ket aotreet kemmañ ar Javascript"
|
||||||
|
REVERSE_PROXY: "Proksi en tu-gin"
|
||||||
|
REVERSE_PROXY_HELP: "Gweredekait an dra-se m'hoc'h a-dreñv ur proksi en tu-gin hag ho peus diaesterioù gant an URLoù oc'h enderc'hel ur porzh didalvoudek"
|
||||||
|
INVALID_FRONTMATTER_COULD_NOT_SAVE: "Frontmatter didalvoudek, n'haller ket enrollan"
|
||||||
|
ADD_FOLDER: "Ouzhpennañ un teuliad"
|
||||||
|
PROXY_URL: "URL ar proksi"
|
||||||
|
PROXY_URL_HELP: "Enankit HERBERC'HIER pe IP ar proksi hag ar PORZH"
|
||||||
|
NOTHING_TO_SAVE: "Netra da enrollañ"
|
||||||
|
FILE_ERROR_ADD: "Degouezhet ez eus bet ur fazi en ur glask enrollañ ar restr"
|
||||||
|
FILE_ERROR_UPLOAD: "Degouezhet ez eus bet ur fazi en ur glask pellgas ar restr"
|
||||||
|
FILE_UNSUPPORTED: "Doare restr anskor"
|
||||||
|
ADD_ITEM: "Ouzhpennañ un elfenn"
|
||||||
|
FILE_TOO_LARGE: "Re leden eo ar restr evit bezañ pellgaset. %s eo an uhelañ aotreet hervez <br> hoc'h arventennoù PHP. Kreskit an arventenn PHP`post_max_size`"
|
||||||
|
INSTALLING: "O staliañ"
|
||||||
|
LOADING: "O kargañ.."
|
||||||
|
DEPENDENCIES_NOT_MET_MESSAGE: "Ret eo deoc'h staliañ an amzalc'hoù da-heul a-raok:"
|
||||||
|
ERROR_INSTALLING_PACKAGES: "Fazi en ur staliañ ar pakad(où)"
|
||||||
|
INSTALLING_DEPENDENCIES: "O staliañ an amzalc'hoù..."
|
||||||
|
INSTALLING_PACKAGES: "O staliañ ar pakad(où).."
|
||||||
|
PACKAGES_SUCCESSFULLY_INSTALLED: "Pakad(où) staliet gant berzh."
|
||||||
|
READY_TO_INSTALL_PACKAGES: "Prest da staliañ ar pakad(où)"
|
||||||
|
PACKAGES_NOT_INSTALLED: "N'eo ket stalied ar pakadoù"
|
||||||
|
PACKAGES_NEED_UPDATE: "Staliet eo ar pakadoù endeo, met re gozh eo"
|
||||||
|
PACKAGES_SUGGESTED_UPDATE: "Staliet eo ar pakadoù endeo, dereat eo an handelv, met hizivaet e vint evit ma vefec'h en handelv diwezhañ"
|
||||||
|
REMOVE_THE: "Dilemel an %s"
|
||||||
|
CONFIRM_REMOVAL: "Sur oc'h e fell deoc'h dilemel %s?"
|
||||||
|
REMOVED_SUCCESSFULLY: "%s dilamet gant berzh"
|
||||||
|
ERROR_REMOVING_THE: "Fazi en ur zilemel %s"
|
||||||
|
ADDITIONAL_DEPENDENCIES_CAN_BE_REMOVED: "An amzalc'hoù da heul a zo azgoulennet gant %s, met n'eo ket azgoulennet gant ur pakad all. Ma ne arverit ket anezho e c'hallit o dilemel adalek amañ."
|
||||||
|
READY_TO_UPDATE_PACKAGES: "Prest da hizivaat ar pakad(où)"
|
||||||
|
ERROR_UPDATING_PACKAGES: "Fazi en ur hizivaat ar pakad(où)"
|
||||||
|
UPDATING_PACKAGES: "Oc'h hizivaat ar pakad(où).."
|
||||||
|
PACKAGES_SUCCESSFULLY_UPDATED: "Pakad(où) hizivaet gant berzh."
|
||||||
|
UPDATING: "Hizivaet"
|
||||||
|
GPM_RELEASES: "Ermaeziadennoù GPM"
|
||||||
|
GPM_RELEASES_HELP: "Dibabit 'Amprouiñ' evit staliañ an handelv beta pe amprouiñ"
|
||||||
|
AUTO: "Oto"
|
||||||
|
FOPEN: "fopen"
|
||||||
|
CURL: "cURL"
|
||||||
|
STABLE: "Stabil"
|
||||||
|
TESTING: "Amprouiñ"
|
||||||
|
FRONTMATTER_PROCESS_TWIG: "Keweriañ frontmatter Twig"
|
||||||
|
FRONTMATTER_PROCESS_TWIG_HELP: "P'eo oberiant e c'hallit arverañ argemennoù kefluniañ Twig e frontmatter ar bajenn"
|
||||||
|
FRONTMATTER_IGNORE_FIELDS: "Leuskel maeziennoù Frontmatter a-gostez"
|
||||||
|
FRONTMATTER_IGNORE_FIELDS_HELP: "Maeziennoù Frontmatter a c'hall enderc'hel Twig met ne rankont ket bezañ keweriet, evel 'forms'"
|
||||||
|
PACKAGE_X_INSTALLED_SUCCESSFULLY: "Pakad %s staliet gant berzh"
|
||||||
|
ORDERING_DISABLED_BECAUSE_PARENT_SETTING_ORDER: "Urzh ar c'har, diweredekaet eo an urzhiañ"
|
||||||
|
ORDERING_DISABLED_BECAUSE_PAGE_NOT_VISIBLE: "Diwelus eo ar bajenn, diweredekaet eo an urzhiañ"
|
||||||
|
ORDERING_DISABLED_BECAUSE_TOO_MANY_SIBLINGS: "N'eo ket skoret an urzhiañ dre an ardeiñ dre ma zo ouzhpenn 200 c'hoar"
|
||||||
|
CANNOT_ADD_MEDIA_FILES_PAGE_NOT_SAVED: "EVEZHIADENN: n'hallit ket ouzhpennañ restroù media evit enrollañ ar bajenn. Klikit war 'Enrollañ' a-us"
|
||||||
|
CANNOT_ADD_FILES_PAGE_NOT_SAVED: "EVEZHIADENN: ret eo enrollañ ar bajenn a-raok pellgas restroù dezhi."
|
||||||
|
DROP_FILES_HERE_TO_UPLOAD: "Lakait ho restroù amañ pe <strong>klikit amañ</strong>"
|
||||||
|
INSERT: "Enlakaat"
|
||||||
|
UNDO: "Dizober"
|
||||||
|
REDO: "Adober"
|
||||||
|
HEADERS: "Talbennoù"
|
||||||
|
BOLD: "Tev"
|
||||||
|
ITALIC: "Stouet"
|
||||||
|
STRIKETHROUGH: "Barrennet"
|
||||||
|
SUMMARY_DELIMITER: "Bonner diverrad"
|
||||||
|
LINK: "Liamm"
|
||||||
|
IMAGE: "Skeudenn"
|
||||||
|
BLOCKQUOTE: "Meneg"
|
||||||
|
UNORDERED_LIST: "Roll dizurzh"
|
||||||
|
ORDERED_LIST: "Roll urzhiet"
|
||||||
|
EDITOR: "Embanner"
|
||||||
|
PREVIEW: "Alberz"
|
||||||
|
FULLSCREEN: "Skramm a-bezh"
|
||||||
|
NON_ROUTABLE: "Dihentus"
|
||||||
|
NON_VISIBLE: "Diwelus"
|
||||||
|
NON_PUBLISHED: "Diembannet"
|
||||||
|
CHARACTERS: "arouezioù"
|
||||||
|
PUBLISHING: "Embann"
|
||||||
|
MEDIA_TYPES: "Doareoù media"
|
||||||
|
IMAGE_OPTIONS: "Opsionoù skeudenn"
|
||||||
|
MIME_TYPE: "Doare Mime"
|
||||||
|
THUMB: "Miniaturenn"
|
||||||
|
TYPE: "Doare"
|
||||||
|
FILE_EXTENSION: "Astenn ar fichenn"
|
||||||
|
LEGEND: "Alc'hwez ar bajenn"
|
||||||
|
MEMCACHE_SERVER: "Servijer Memcached"
|
||||||
|
MEMCACHE_SERVER_HELP: "Chomlec'h ar servijer Memcached"
|
||||||
|
MEMCACHE_PORT: "Porzh Memcached"
|
||||||
|
MEMCACHE_PORT_HELP: "Porzh ar servijer Memcached"
|
||||||
|
MEMCACHED_SERVER: "Servijer Memcached"
|
||||||
|
MEMCACHED_SERVER_HELP: "Chomlec'h ar servijer Memcached"
|
||||||
|
MEMCACHED_PORT: "Porzh Memcached"
|
||||||
|
MEMCACHED_PORT_HELP: "Porzh ar servijer Memcached"
|
||||||
|
REDIS_SERVER: "Servijer Redis"
|
||||||
|
REDIS_SERVER_HELP: "Chomlec'h ar servijer Redis"
|
||||||
|
REDIS_PORT: "Porzh Redis"
|
||||||
|
REDIS_PORT_HELP: "Porzh ar servijer Redis"
|
||||||
|
ALL: "Tout"
|
||||||
|
FROM: "eus"
|
||||||
|
TO: "da"
|
||||||
|
RESOURCE_FILTER: "Sil..."
|
||||||
|
FORCE_SSL: "Forsañ SSL"
|
||||||
|
DROPZONE_CANCEL_UPLOAD: 'Nullañ ar gargamant'
|
||||||
|
DROPZONE_REMOVE_FILE: "Dilemel ar fichenn"
|
||||||
|
PREMIUM_PRODUCT: "Premium"
|
||||||
|
ERROR_SIMPLE: "Fazi simpl"
|
||||||
|
ERROR_SYSTEM: "Fazi Sistem"
|
||||||
|
NOT_SET: "Pas termenet"
|
||||||
|
PERMISSIONS: "Permisionoù"
|
||||||
|
REINSTALL_PLUGIN: "Adstaliañ ar Plugin"
|
||||||
|
REINSTALL_THEME: "Adstaliañ an Tem"
|
||||||
|
REINSTALL_THE: "Adstaliañ ar %s"
|
||||||
|
REINSTALLATION_FAILED: "Adstaliañ c'hwitet"
|
||||||
|
TOOLS: "Ostilhoù"
|
||||||
|
DIRECT_INSTALL: "Staliañ war-eeun"
|
||||||
|
2FA_CODE_INPUT: "000000"
|
||||||
|
CONFIGURATION: "Kefluniadur"
|
||||||
|
TIMEOUT: "Diamzeriñ"
|
||||||
|
TIMEOUT_HELP: "Lakaat an amzer diamzeriñ e eilennoù"
|
||||||
|
DASHBOARD: "Taolenn labour"
|
||||||
|
NOTIFICATIONS: "Notifiadenn"
|
||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user