mirror of
https://github.com/getgrav/grav-plugin-admin.git
synced 2026-07-12 05:52:11 +02:00
Merge branch 'develop' of github.com:getgrav/grav-plugin-admin into develop
This commit is contained in:
11
CHANGELOG.md
11
CHANGELOG.md
@@ -1,11 +1,22 @@
|
||||
# v1.10.9
|
||||
## mm/dd/2021
|
||||
|
||||
1. [](#new)
|
||||
* Requires **Grav 1.7.10**
|
||||
1. [](#improved)
|
||||
* Better isolate admin to prevent session related vulnerabilities
|
||||
* Removed support for custom login redirects for improved security
|
||||
* Shorten forgot password link lifetime from 7 days to 1 hour
|
||||
1. [](#bugfix)
|
||||
* Fixed issue where Adding a new page and canceling from within Editing would alter the Parent location of the edited page [#2067](https://github.com/getgrav/grav-plugin-admin/issues/2067)
|
||||
* Fixed and enhanced Range field to be Lists compatible [#2062](https://github.com/getgrav/grav-plugin-admin/issues/2062)
|
||||
* Fixed ERR_TOO_MANY_REDIRECTS with HTTPS = 'On' [#2100](https://github.com/getgrav/grav-plugin-admin/issues/2100)
|
||||
* Prevent expert editing mode from anyone else than super users [#2094](https://github.com/getgrav/grav-plugin-admin/issues/2094)
|
||||
* Fixed login related pages being accessible from admin when user has logged in
|
||||
* Fixed admin user creation and password reset allowing unsafe passwords
|
||||
* Fixed missing validation when registering the first admin user
|
||||
* Fixed reset password email not to have session specific token in it
|
||||
* Fixed admin controller running before setting `$grav['page']`
|
||||
|
||||
# v1.10.8
|
||||
## 03/19/2021
|
||||
|
||||
@@ -15,7 +15,7 @@ docs: https://github.com/getgrav/grav-plugin-admin/blob/develop/README.md
|
||||
license: MIT
|
||||
|
||||
dependencies:
|
||||
- { name: grav, version: '>=1.7.4' }
|
||||
- { name: grav, version: '>=1.7.10' }
|
||||
- { name: form, version: '>=4.1.0' }
|
||||
- { name: login, version: '>=3.3.5' }
|
||||
- { name: email, version: '>=3.0.9' }
|
||||
|
||||
@@ -26,7 +26,6 @@ use Grav\Common\Themes;
|
||||
use Grav\Common\Uri;
|
||||
use Grav\Common\User\Interfaces\UserCollectionInterface;
|
||||
use Grav\Common\User\Interfaces\UserInterface;
|
||||
use Grav\Common\User\User;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Framework\Acl\Action;
|
||||
use Grav\Framework\Acl\Permissions;
|
||||
@@ -40,6 +39,7 @@ use Grav\Plugin\AdminPlugin;
|
||||
use Grav\Plugin\Login\Login;
|
||||
use Grav\Plugin\Login\TwoFactorAuth\TwoFactorAuth;
|
||||
use PicoFeed\Parser\MalformedXmlException;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use RocketTheme\Toolbox\Event\Event;
|
||||
use RocketTheme\Toolbox\File\File;
|
||||
use RocketTheme\Toolbox\File\JsonFile;
|
||||
@@ -52,72 +52,63 @@ use PicoFeed\Reader\Reader;
|
||||
|
||||
define('LOGIN_REDIRECT_COOKIE', 'grav-login-redirect');
|
||||
|
||||
/**
|
||||
* Class Admin
|
||||
* @package Grav\Plugin\Admin
|
||||
*/
|
||||
class Admin
|
||||
{
|
||||
/** @var int */
|
||||
public const DEBUG = 1;
|
||||
/** @var int */
|
||||
public const MEDIA_PAGINATION_INTERVAL = 20;
|
||||
/** @var string */
|
||||
public const TMP_COOKIE_NAME = 'tmp-admin-message';
|
||||
|
||||
/** @var Grav */
|
||||
public $grav;
|
||||
|
||||
/** @var ServerRequestInterface|null */
|
||||
public $request;
|
||||
/** @var AdminForm */
|
||||
public $form;
|
||||
/** @var string */
|
||||
public $base;
|
||||
|
||||
/** @var string */
|
||||
public $location;
|
||||
|
||||
/** @var string */
|
||||
public $route;
|
||||
|
||||
/** @var User */
|
||||
/** @var UserInterface */
|
||||
public $user;
|
||||
|
||||
/** @var array */
|
||||
public $forgot;
|
||||
|
||||
/** @var string */
|
||||
public $task;
|
||||
|
||||
/** @var array */
|
||||
public $json_response;
|
||||
|
||||
/** @var Collection */
|
||||
public $collection;
|
||||
|
||||
/** @var bool */
|
||||
public $multilang;
|
||||
|
||||
/** @var string */
|
||||
public $language;
|
||||
|
||||
/** @var array */
|
||||
public $languages_enabled = [];
|
||||
|
||||
/** @var Uri $uri */
|
||||
protected $uri;
|
||||
|
||||
/** @var array */
|
||||
protected $pages = [];
|
||||
|
||||
/** @var Session */
|
||||
protected $session;
|
||||
|
||||
/** @var Data\Blueprints */
|
||||
protected $blueprints;
|
||||
|
||||
/** @var GPM */
|
||||
protected $gpm;
|
||||
|
||||
/** @var int */
|
||||
protected $pages_count;
|
||||
|
||||
/** @var bool */
|
||||
protected $load_additional_files_in_background = false;
|
||||
|
||||
/** @var bool */
|
||||
protected $loading_additional_files_in_background = false;
|
||||
|
||||
/** @var array */
|
||||
protected $temp_messages = [];
|
||||
|
||||
@@ -127,7 +118,7 @@ class Admin
|
||||
* @param Grav $grav
|
||||
* @param string $base
|
||||
* @param string $location
|
||||
* @param string $route
|
||||
* @param string|null $route
|
||||
*/
|
||||
public function __construct(Grav $grav, $base, $location, $route)
|
||||
{
|
||||
@@ -137,7 +128,7 @@ class Admin
|
||||
$this->grav = $grav;
|
||||
$this->base = $base;
|
||||
$this->location = $location;
|
||||
$this->route = $route;
|
||||
$this->route = $route ?? '';
|
||||
$this->uri = $grav['uri'];
|
||||
$this->session = $grav['session'];
|
||||
|
||||
@@ -176,7 +167,7 @@ class Admin
|
||||
$this->languages_enabled = (array)$this->grav['config']->get('system.languages.supported', []);
|
||||
|
||||
//Set the currently active language for the admin
|
||||
$languageCode = $this->grav['uri']->param('lang');
|
||||
$languageCode = $this->uri->param('lang');
|
||||
if (null === $languageCode && !$this->session->admin_lang) {
|
||||
$this->session->admin_lang = $language->getActive() ?? '';
|
||||
}
|
||||
@@ -190,7 +181,8 @@ class Admin
|
||||
|
||||
/**
|
||||
* @param string $message
|
||||
* @param array $data
|
||||
* @param array|object $data
|
||||
* @return void
|
||||
*/
|
||||
public static function addDebugMessage(string $message, $data = [])
|
||||
{
|
||||
@@ -199,6 +191,9 @@ class Admin
|
||||
$debugger->addMessage($message, 'debug', $data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public static function contentEditor()
|
||||
{
|
||||
$options = [
|
||||
@@ -238,6 +233,9 @@ class Admin
|
||||
return $languages;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getLanguage(): string
|
||||
{
|
||||
return $this->language ?: $this->grav['language']->getLanguage() ?: 'en';
|
||||
@@ -317,6 +315,9 @@ class Admin
|
||||
return $tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function toolsPermissions()
|
||||
{
|
||||
$tools = static::tools();
|
||||
@@ -349,12 +350,11 @@ class Admin
|
||||
/**
|
||||
* Static helper method to return the admin form nonce
|
||||
*
|
||||
* @param string $action
|
||||
* @return string
|
||||
*/
|
||||
public static function getNonce()
|
||||
public static function getNonce(string $action = 'admin-form')
|
||||
{
|
||||
$action = 'admin-form';
|
||||
|
||||
return Utils::getNonce($action);
|
||||
}
|
||||
|
||||
@@ -388,11 +388,16 @@ class Admin
|
||||
return $admin->getCurrentRoute();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $path
|
||||
* @param string|null $languageCode
|
||||
* @return Route
|
||||
*/
|
||||
public function getAdminRoute(string $path = '', $languageCode = null): Route
|
||||
{
|
||||
/** @var Language $language */
|
||||
$language = $this->grav['language'];
|
||||
$languageCode = $languageCode ?? $language->getActive();
|
||||
$languageCode = $languageCode ?? ($language->getActive() ?: null);
|
||||
$languagePrefix = $languageCode ? '/' . $languageCode : '';
|
||||
|
||||
$root = $this->grav['uri']->rootUrl();
|
||||
@@ -415,6 +420,11 @@ class Admin
|
||||
return RouteFactory::createFromParts($parts);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $route
|
||||
* @param string|null $languageCode
|
||||
* @return string
|
||||
*/
|
||||
public function adminUrl(string $route = '', $languageCode = null)
|
||||
{
|
||||
return $this->getAdminRoute($route, $languageCode)->toString(true);
|
||||
@@ -435,6 +445,9 @@ class Admin
|
||||
return $admin->getCurrentRoute();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
*/
|
||||
public function getCurrentRoute()
|
||||
{
|
||||
$pages = static::enablePages();
|
||||
@@ -459,7 +472,8 @@ class Admin
|
||||
* Route may or may not be prefixed by /en or /admin or /en/admin.
|
||||
*
|
||||
* @param string $redirect
|
||||
* @param int$redirectCode
|
||||
* @param int $redirectCode
|
||||
* @return void
|
||||
*/
|
||||
public function redirect($redirect, $redirectCode = 303)
|
||||
{
|
||||
@@ -520,6 +534,9 @@ class Admin
|
||||
return count($this->grav['config']->get('system.languages.supported', [])) > 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public static function getTempDir()
|
||||
{
|
||||
try {
|
||||
@@ -531,6 +548,9 @@ class Admin
|
||||
return $tmp_dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public static function getPageMedia()
|
||||
{
|
||||
$files = [];
|
||||
@@ -541,7 +561,7 @@ class Admin
|
||||
$route = '/' . ltrim($grav['admin']->route, '/');
|
||||
|
||||
/** @var PageInterface $page */
|
||||
$page = $pages->find($route);
|
||||
$page = $pages->find($route);
|
||||
$parent_route = null;
|
||||
if ($page) {
|
||||
$media = $page->media()->all();
|
||||
@@ -564,8 +584,7 @@ class Admin
|
||||
/**
|
||||
* Fetch and delete messages from the session queue.
|
||||
*
|
||||
* @param string $type
|
||||
*
|
||||
* @param string|null $type
|
||||
* @return array
|
||||
*/
|
||||
public function messages($type = null)
|
||||
@@ -579,7 +598,9 @@ class Admin
|
||||
/**
|
||||
* Authenticate user.
|
||||
*
|
||||
* @param array $credentials User credentials.
|
||||
* @param array $credentials User credentials.
|
||||
* @param array $post
|
||||
* @return never-return
|
||||
*/
|
||||
public function authenticate($credentials, $post)
|
||||
{
|
||||
@@ -658,6 +679,10 @@ class Admin
|
||||
|
||||
/**
|
||||
* Check Two-Factor Authentication.
|
||||
*
|
||||
* @param array $data
|
||||
* @param array $post
|
||||
* @return never-return
|
||||
*/
|
||||
public function twoFa($data, $post)
|
||||
{
|
||||
@@ -695,6 +720,10 @@ class Admin
|
||||
|
||||
/**
|
||||
* Logout from admin.
|
||||
*
|
||||
* @param array $data
|
||||
* @param array $post
|
||||
* @return never-return
|
||||
*/
|
||||
public function logout($data, $post)
|
||||
{
|
||||
@@ -718,15 +747,8 @@ class Admin
|
||||
public static function doAnyUsersExist()
|
||||
{
|
||||
$accounts = Grav::instance()['accounts'] ?? null;
|
||||
if ($accounts instanceof \Countable) {
|
||||
return $accounts->count() > 0;
|
||||
}
|
||||
|
||||
// TODO: remove old way to check for existence of a user account (Grav < v1.6.9)
|
||||
$account_dir = $file_path = Grav::instance()['locator']->findResource('account://');
|
||||
$user_check = glob($account_dir . '/*.yaml');
|
||||
|
||||
return $user_check;
|
||||
return $accounts && $accounts->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -734,6 +756,7 @@ class Admin
|
||||
*
|
||||
* @param string $msg
|
||||
* @param string $type
|
||||
* @return void
|
||||
*/
|
||||
public function setMessage($msg, $type = 'info')
|
||||
{
|
||||
@@ -742,11 +765,19 @@ class Admin
|
||||
$messages->add($msg, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $msg
|
||||
* @param string $type
|
||||
* @return void
|
||||
*/
|
||||
public function addTempMessage($msg, $type)
|
||||
{
|
||||
$this->temp_messages[] = ['message' => $msg, 'scope' => $type];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getTempMessages()
|
||||
{
|
||||
return $this->temp_messages;
|
||||
@@ -755,11 +786,9 @@ class Admin
|
||||
/**
|
||||
* Translate a string to the user-defined language
|
||||
*
|
||||
* @param array|mixed $args
|
||||
*
|
||||
* @param mixed $languages
|
||||
*
|
||||
* @return string
|
||||
* @param array|string $args
|
||||
* @param array|null $languages
|
||||
* @return string|string[]|null
|
||||
*/
|
||||
public static function translate($args, $languages = null)
|
||||
{
|
||||
@@ -812,7 +841,6 @@ class Admin
|
||||
* Checks user authorisation to the action.
|
||||
*
|
||||
* @param string|string[] $action
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize($action = 'admin.login')
|
||||
@@ -839,7 +867,6 @@ class Admin
|
||||
*
|
||||
* @param string $type
|
||||
* @param array $post
|
||||
*
|
||||
* @return mixed
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
@@ -958,7 +985,6 @@ class Admin
|
||||
*
|
||||
* @param string $type
|
||||
* @param array|null $post
|
||||
*
|
||||
* @return object
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
@@ -990,7 +1016,7 @@ class Admin
|
||||
if (preg_match('|plugins/|', $type)) {
|
||||
$obj = Plugins::get(preg_replace('|plugins/|', '', $type));
|
||||
if (null === $obj) {
|
||||
return [];
|
||||
return new \stdClass();
|
||||
}
|
||||
|
||||
if ($post) {
|
||||
@@ -1005,7 +1031,7 @@ class Admin
|
||||
$themes = $this->grav['themes'];
|
||||
$obj = $themes->get(preg_replace('|themes/|', '', $type));
|
||||
if (null === $obj) {
|
||||
return [];
|
||||
return new \stdClass();
|
||||
}
|
||||
|
||||
if ($post) {
|
||||
@@ -1070,6 +1096,11 @@ class Admin
|
||||
return $data[$type];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Data\Data $object
|
||||
* @param array $post
|
||||
* @return Data\Data
|
||||
*/
|
||||
protected function mergePost(Data\Data $object, array $post)
|
||||
{
|
||||
$object->merge($post);
|
||||
@@ -1105,6 +1136,9 @@ class Admin
|
||||
return $post;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
protected function hasErrorMessage()
|
||||
{
|
||||
$msgs = $this->grav['messages']->all();
|
||||
@@ -1120,7 +1154,6 @@ class Admin
|
||||
* Returns blueprints for the given type.
|
||||
*
|
||||
* @param string $type
|
||||
*
|
||||
* @return Data\Blueprint
|
||||
*/
|
||||
public function blueprints($type)
|
||||
@@ -1136,7 +1169,6 @@ class Admin
|
||||
* Converts dot notation to array notation.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function field($name)
|
||||
@@ -1150,7 +1182,6 @@ class Admin
|
||||
* Get all routes.
|
||||
*
|
||||
* @param bool $unique
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function routes($unique = false)
|
||||
@@ -1231,6 +1262,10 @@ class Admin
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|null $package_slug
|
||||
* @return string[]|string
|
||||
*/
|
||||
public function license($package_slug)
|
||||
{
|
||||
return Licenses::get($package_slug);
|
||||
@@ -1241,7 +1276,6 @@ class Admin
|
||||
* packages that can be removed when removing a package.
|
||||
*
|
||||
* @param string $slug The package slug
|
||||
*
|
||||
* @return array|bool
|
||||
*/
|
||||
public function dependenciesThatCanBeRemovedWhenRemoving($slug)
|
||||
@@ -1255,21 +1289,17 @@ class Admin
|
||||
|
||||
$package = $this->getPackageFromGPM($slug);
|
||||
|
||||
if ($package) {
|
||||
if ($package->dependencies) {
|
||||
foreach ($package->dependencies as $dependency) {
|
||||
// if (count($gpm->getPackagesThatDependOnPackage($dependency)) > 1) {
|
||||
// continue;
|
||||
// }
|
||||
if (isset($dependency['name'])) {
|
||||
$dependency = $dependency['name'];
|
||||
}
|
||||
if ($package && $package->dependencies) {
|
||||
foreach ($package->dependencies as $dependency) {
|
||||
// if (count($gpm->getPackagesThatDependOnPackage($dependency)) > 1) {
|
||||
// continue;
|
||||
// }
|
||||
if (isset($dependency['name'])) {
|
||||
$dependency = $dependency['name'];
|
||||
}
|
||||
|
||||
if (!in_array($dependency, $dependencies, true)) {
|
||||
if (!in_array($dependency, ['admin', 'form', 'login', 'email', 'php'])) {
|
||||
$dependencies[] = $dependency;
|
||||
}
|
||||
}
|
||||
if (!in_array($dependency, $dependencies, true) && !in_array($dependency, ['admin', 'form', 'login', 'email', 'php'])) {
|
||||
$dependencies[] = $dependency;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1295,6 +1325,10 @@ class Admin
|
||||
return $this->gpm;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $package_slug
|
||||
* @return mixed
|
||||
*/
|
||||
public function getPackageFromGPM($package_slug)
|
||||
{
|
||||
$package = $this->plugins(true)[$package_slug];
|
||||
@@ -1309,7 +1343,6 @@ class Admin
|
||||
* Get all plugins.
|
||||
*
|
||||
* @param bool $local
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function plugins($local = true)
|
||||
@@ -1338,7 +1371,6 @@ class Admin
|
||||
* Get all themes.
|
||||
*
|
||||
* @param bool $local
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function themes($local = true)
|
||||
@@ -1384,9 +1416,8 @@ class Admin
|
||||
* Check the passed packages list can be updated
|
||||
*
|
||||
* @param array $packages
|
||||
*
|
||||
* @throws \Exception
|
||||
* @return bool
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function checkPackagesCanBeInstalled($packages)
|
||||
{
|
||||
@@ -1405,7 +1436,6 @@ class Admin
|
||||
* to be installed.
|
||||
*
|
||||
* @param array $packages The packages slugs
|
||||
*
|
||||
* @return array|bool
|
||||
*/
|
||||
public function getDependenciesNeededToInstall($packages)
|
||||
@@ -1422,8 +1452,7 @@ class Admin
|
||||
* Used by the Dashboard in the admin to display the X latest pages
|
||||
* that have been modified
|
||||
*
|
||||
* @param integer $count number of pages to pull back
|
||||
*
|
||||
* @param int $count number of pages to pull back
|
||||
* @return array|null
|
||||
*/
|
||||
public function latestPages($count = 10)
|
||||
@@ -1517,7 +1546,6 @@ class Admin
|
||||
* Determine if the plugin or theme info passed is from Team Grav
|
||||
*
|
||||
* @param object $info Plugin or Theme info object
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isTeamGrav($info)
|
||||
@@ -1529,7 +1557,6 @@ class Admin
|
||||
* Determine if the plugin or theme info passed is premium
|
||||
*
|
||||
* @param object $info Plugin or Theme info object
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isPremiumProduct($info)
|
||||
@@ -1542,13 +1569,12 @@ class Admin
|
||||
*
|
||||
* @return string The phpinfo() output
|
||||
*/
|
||||
function phpinfo()
|
||||
public function phpinfo()
|
||||
{
|
||||
if (function_exists('phpinfo')) {
|
||||
ob_start();
|
||||
phpinfo();
|
||||
$pinfo = ob_get_clean();
|
||||
|
||||
$pinfo = preg_replace('%^.*<body>(.*)</body>.*$%ms', '$1', $pinfo);
|
||||
|
||||
return $pinfo;
|
||||
@@ -1560,8 +1586,7 @@ class Admin
|
||||
/**
|
||||
* Guest date format based on euro/US
|
||||
*
|
||||
* @param string $date
|
||||
*
|
||||
* @param string|null $date
|
||||
* @return string
|
||||
*/
|
||||
public function guessDateFormat($date)
|
||||
@@ -1584,6 +1609,7 @@ class Admin
|
||||
'g:ia'
|
||||
];
|
||||
|
||||
$date = (string)$date;
|
||||
if (!isset($guess[$date])) {
|
||||
$guess[$date] = 'd-m-Y H:i';
|
||||
foreach ($date_formats as $date_format) {
|
||||
@@ -1605,6 +1631,11 @@ class Admin
|
||||
return $guess[$date];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $date
|
||||
* @param string $format
|
||||
* @return bool
|
||||
*/
|
||||
public function validateDate($date, $format)
|
||||
{
|
||||
$d = DateTime::createFromFormat($format, $date);
|
||||
@@ -1614,7 +1645,6 @@ class Admin
|
||||
|
||||
/**
|
||||
* @param string $php_format
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function dateformatToMomentJS($php_format)
|
||||
|
||||
@@ -24,7 +24,7 @@ use Grav\Common\Page\Pages;
|
||||
use Grav\Common\Page\Collection;
|
||||
use Grav\Common\Security;
|
||||
use Grav\Common\User\Interfaces\UserCollectionInterface;
|
||||
use Grav\Common\User\User;
|
||||
use Grav\Common\User\Interfaces\UserInterface;
|
||||
use Grav\Common\Utils;
|
||||
use Grav\Framework\Psr7\Response;
|
||||
use Grav\Framework\RequestHandler\Exception\RequestException;
|
||||
@@ -195,29 +195,7 @@ class AdminController extends AdminBaseController
|
||||
return true;
|
||||
}
|
||||
|
||||
// LOGIN & USER TASKS
|
||||
|
||||
/**
|
||||
* Handle login.
|
||||
*
|
||||
* @return bool True if the action was performed.
|
||||
*/
|
||||
protected function taskLogin()
|
||||
{
|
||||
$this->admin->authenticate($this->data, $this->post);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool True if the action was performed.
|
||||
*/
|
||||
protected function taskTwofa()
|
||||
{
|
||||
$this->admin->twoFa($this->data, $this->post);
|
||||
|
||||
return true;
|
||||
}
|
||||
// USER TASKS
|
||||
|
||||
/**
|
||||
* Handle logout.
|
||||
@@ -226,6 +204,10 @@ class AdminController extends AdminBaseController
|
||||
*/
|
||||
protected function taskLogout()
|
||||
{
|
||||
if (!$this->authorizeTask('logout', ['admin.login'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->admin->logout($this->data, $this->post);
|
||||
|
||||
return true;
|
||||
@@ -241,7 +223,7 @@ class AdminController extends AdminBaseController
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var User $user */
|
||||
/** @var UserInterface $user */
|
||||
$user = $this->grav['user'];
|
||||
|
||||
/** @var TwoFactorAuth $twoFa */
|
||||
@@ -278,172 +260,6 @@ class AdminController extends AdminBaseController
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the reset password action.
|
||||
*
|
||||
* @return bool True if the action was performed.
|
||||
*/
|
||||
public function taskReset()
|
||||
{
|
||||
$data = $this->data;
|
||||
|
||||
if (isset($data['password'])) {
|
||||
/** @var UserCollectionInterface $users */
|
||||
$users = $this->grav['accounts'];
|
||||
|
||||
$username = isset($data['username']) ? strip_tags(strtolower($data['username'])) : null;
|
||||
$user = $username ? $users->load($username) : null;
|
||||
$password = $data['password'] ?? null;
|
||||
$token = $data['token'] ?? null;
|
||||
|
||||
if ($user && $user->exists() && !empty($user->get('reset'))) {
|
||||
list($good_token, $expire) = explode('::', $user->get('reset'));
|
||||
|
||||
if ($good_token === $token) {
|
||||
if (time() > $expire) {
|
||||
$this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.RESET_LINK_EXPIRED'), 'error');
|
||||
$this->setRedirect('/forgot');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$user->undef('hashed_password');
|
||||
$user->undef('reset');
|
||||
$user->set('password', $password);
|
||||
$user->save();
|
||||
|
||||
$this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.RESET_PASSWORD_RESET'), 'info');
|
||||
$this->setRedirect('/');
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.RESET_INVALID_LINK'), 'error');
|
||||
$this->setRedirect('/forgot');
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
$user = $this->grav['uri']->param('user');
|
||||
$token = $this->grav['uri']->param('token');
|
||||
|
||||
if (empty($user) || empty($token)) {
|
||||
$this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.RESET_INVALID_LINK'), 'error');
|
||||
$this->setRedirect('/forgot');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.RESET_NEW_PASSWORD'), 'info');
|
||||
|
||||
$this->admin->forgot = ['username' => $user, 'token' => $token];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the email password recovery procedure.
|
||||
*
|
||||
* @return bool True if the action was performed.
|
||||
*/
|
||||
protected function taskForgot()
|
||||
{
|
||||
$param_sep = $this->grav['config']->get('system.param_sep', ':');
|
||||
$post = $this->post;
|
||||
$data = $this->data;
|
||||
$login = $this->grav['login'];
|
||||
|
||||
/** @var UserCollectionInterface $users */
|
||||
$users = $this->grav['accounts'];
|
||||
|
||||
$username = isset($data['username']) ? strip_tags(strtolower($data['username'])) : '';
|
||||
$user = !empty($username) ? $users->load($username) : null;
|
||||
|
||||
if (!isset($this->grav['Email'])) {
|
||||
$this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.FORGOT_EMAIL_NOT_CONFIGURED'), 'error');
|
||||
$this->setRedirect($post['redirect']);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!$user || !$user->exists()) {
|
||||
$this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL'),
|
||||
'info');
|
||||
$this->setRedirect($post['redirect']);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (empty($user->email)) {
|
||||
$this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL'),
|
||||
'info');
|
||||
$this->setRedirect($post['redirect']);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$count = $this->grav['config']->get('plugins.login.max_pw_resets_count', 0);
|
||||
$interval =$this->grav['config']->get('plugins.login.max_pw_resets_interval', 2);
|
||||
|
||||
if ($login->isUserRateLimited($user, 'pw_resets', $count, $interval)) {
|
||||
$this->admin->setMessage($this->admin::translate(['PLUGIN_LOGIN.FORGOT_CANNOT_RESET_IT_IS_BLOCKED', $user->email, $interval]), 'error');
|
||||
$this->setRedirect($post['redirect']);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$token = md5(uniqid(mt_rand(), true));
|
||||
$expire = time() + 604800; // next week
|
||||
|
||||
$user->set('reset', $token . '::' . $expire);
|
||||
$user->save();
|
||||
|
||||
$author = $this->grav['config']->get('site.author.name', '');
|
||||
$fullname = $user->fullname ?: $username;
|
||||
$reset_link = rtrim($this->grav['uri']->rootUrl(true), '/') . '/' . trim($this->admin->base,
|
||||
'/') . '/reset/task' . $param_sep . 'reset/user' . $param_sep . $username . '/token' . $param_sep . $token . '/admin-nonce' . $param_sep . Utils::getNonce('admin-form');
|
||||
|
||||
$sitename = $this->grav['config']->get('site.title', 'Website');
|
||||
$from = $this->grav['config']->get('plugins.email.from');
|
||||
|
||||
if (empty($from)) {
|
||||
$this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.FORGOT_EMAIL_NOT_CONFIGURED'), 'error');
|
||||
$this->setRedirect($post['redirect']);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$to = $user->email;
|
||||
|
||||
$subject = $this->admin::translate(['PLUGIN_ADMIN.FORGOT_EMAIL_SUBJECT', $sitename]);
|
||||
$content = $this->admin::translate([
|
||||
'PLUGIN_ADMIN.FORGOT_EMAIL_BODY',
|
||||
$fullname,
|
||||
$reset_link,
|
||||
$author,
|
||||
$sitename
|
||||
]);
|
||||
|
||||
$body = $this->grav['twig']->processTemplate('email/base.html.twig', ['content' => $content]);
|
||||
|
||||
$message = $this->grav['Email']->message($subject, $body, 'text/html')->setFrom($from)->setTo($to);
|
||||
|
||||
$sent = $this->grav['Email']->send($message);
|
||||
|
||||
if ($sent < 1) {
|
||||
$this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.FORGOT_FAILED_TO_EMAIL'), 'error');
|
||||
} else {
|
||||
$this->admin->setMessage($this->admin::translate('PLUGIN_ADMIN.FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL'),
|
||||
'info');
|
||||
}
|
||||
|
||||
$this->setRedirect('/');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save user account.
|
||||
*
|
||||
|
||||
182
classes/plugin/AdminForm.php
Normal file
182
classes/plugin/AdminForm.php
Normal file
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Plugin\Admin
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
44
classes/plugin/AdminFormFactory.php
Normal file
44
classes/plugin/AdminFormFactory.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
359
classes/plugin/Controllers/AdminController.php
Normal file
359
classes/plugin/Controllers/AdminController.php
Normal file
@@ -0,0 +1,359 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Plugin\Admin
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 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 . trim($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 . trim($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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
630
classes/plugin/Controllers/Login/LoginController.php
Normal file
630
classes/plugin/Controllers/Login/LoginController.php
Normal file
@@ -0,0 +1,630 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Plugin\Admin
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 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\Page\Pages;
|
||||
use Grav\Common\Uri;
|
||||
use Grav\Common\User\Interfaces\UserCollectionInterface;
|
||||
use Grav\Common\User\Interfaces\UserInterface;
|
||||
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 = 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 = $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.
|
||||
*
|
||||
* @return ResponseInterface
|
||||
*/
|
||||
public function taskLogout(): ResponseInterface
|
||||
{
|
||||
try {
|
||||
$this->checkNonce();
|
||||
} catch (PageExpiredException $e) {
|
||||
$this->setMessage($this->translate('PLUGIN_ADMIN.INVALID_SECURITY_TOKEN'), 'error');
|
||||
|
||||
return $this->createDisplayResponse();
|
||||
}
|
||||
|
||||
$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('/');
|
||||
}
|
||||
|
||||
$this->page = $this->createPage('login');
|
||||
$this->form = $this->getForm('admin-login-twofa');
|
||||
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();
|
||||
try {
|
||||
$twoFa = $login->twoFactorAuth();
|
||||
} catch (TwoFactorAuthException $e) {
|
||||
/** @var Debugger $debugger */
|
||||
$debugger = $this->grav['debugger'];
|
||||
$debugger->addException($e);
|
||||
|
||||
$twoFa = null;
|
||||
}
|
||||
|
||||
$code = $data['2fa_code'] ?? null;
|
||||
$secret = $user->twofa_secret ?? null;
|
||||
$redirect = (string)$this->getRequest()->getUri();
|
||||
|
||||
if (null === $twoFa || !$user->authenticated || !$code || !$secret || !$twoFa->verifyCode($secret, $code)) {
|
||||
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.
|
||||
$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_link = $this->getAbsoluteAdminUrl("/reset/u/{$username}/{$token}");
|
||||
|
||||
// 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');
|
||||
}
|
||||
|
||||
// For testing only!
|
||||
//Admin::DEBUG && Admin::addDebugMessage(sprintf('Email sent to %s', $to), $body);
|
||||
|
||||
$this->setMessage($this->translate('PLUGIN_ADMIN.FORGOT_INSTRUCTIONS_SENT_VIA_EMAIL'));
|
||||
} catch (\RuntimeException|\Swift_SwiftException $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'];
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,10 @@
|
||||
|
||||
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;
|
||||
@@ -13,6 +15,16 @@ 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.
|
||||
*
|
||||
@@ -25,31 +37,32 @@ class Router extends ProcessorBase
|
||||
$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);
|
||||
$key = array_shift($parts);
|
||||
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;
|
||||
|
||||
$request = $request->withAttribute('admin', ['path' => $path, 'parts' => $parts]);
|
||||
$params = ['view' => $view, 'route' => $routeStr, 'path' => $path, 'parts' => $parts, 'task' => $task, 'action' => $action];
|
||||
$request = $request->withAttribute('admin', $params);
|
||||
|
||||
$response = null;
|
||||
/*
|
||||
if ($key === '__TODO__') {
|
||||
|
||||
$controller = new TodoController();
|
||||
|
||||
$response = $controller->handle($request);
|
||||
// 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'));
|
||||
}
|
||||
*/
|
||||
|
||||
if (!$response) {
|
||||
// Fallback to the old admin behavior.
|
||||
$response = $handler->handle($request);
|
||||
}
|
||||
$this->admin->request = $request;
|
||||
|
||||
$response = $handler->handle($request);
|
||||
|
||||
$this->stopTimer();
|
||||
|
||||
|
||||
93
classes/plugin/Routers/LoginRouter.php
Normal file
93
classes/plugin/Routers/LoginRouter.php
Normal file
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @package Grav\Plugin\Admin
|
||||
*
|
||||
* @copyright Copyright (c) 2015 - 2021 Trilby Media, LLC. All rights reserved.
|
||||
* @license MIT License; see LICENSE file for details.
|
||||
*/
|
||||
|
||||
namespace Grav\Plugin\Admin\Routers;
|
||||
|
||||
use Grav\Plugin\Admin\Admin;
|
||||
use Grav\Plugin\Admin\Controllers\Login\LoginController;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
|
||||
class LoginRouter
|
||||
{
|
||||
/** @var string[] */
|
||||
private $taskTemplates = [
|
||||
'logout' => 'login',
|
||||
'twofa' => 'login',
|
||||
'forgot' => 'forgot',
|
||||
'reset' => 'reset'
|
||||
];
|
||||
|
||||
/**
|
||||
* @param ServerRequestInterface $request
|
||||
* @return array
|
||||
*/
|
||||
public function matchServerRequest(ServerRequestInterface $request): array
|
||||
{
|
||||
$adminInfo = $request->getAttribute('admin');
|
||||
$task = $adminInfo['task'];
|
||||
$class = LoginController::class;
|
||||
|
||||
// Special controller for the new sites.
|
||||
if (!Admin::doAnyUsersExist()) {
|
||||
$method = $task === 'register' ? 'taskRegister' : 'displayRegister';
|
||||
|
||||
return [
|
||||
'controller' => [
|
||||
'class' => $class,
|
||||
'method' => $method,
|
||||
'params' => []
|
||||
],
|
||||
'template' => 'register',
|
||||
];
|
||||
}
|
||||
|
||||
$httpMethod = $request->getMethod();
|
||||
$template = $this->taskTemplates[$task] ?? $adminInfo['view'];
|
||||
$params = [];
|
||||
|
||||
switch ($template) {
|
||||
case 'forgot':
|
||||
break;
|
||||
case 'reset':
|
||||
$path = $adminInfo['path'];
|
||||
if (str_starts_with($path, 'u/')) {
|
||||
// Path is 'u/username/token'
|
||||
$parts = explode('/', $path, 4);
|
||||
$user = $parts[1] ?? null;
|
||||
$token = $parts[2] ?? null;
|
||||
} else {
|
||||
// Old path used to be 'task:reset/user:username/token:token'
|
||||
if ($httpMethod === 'GET' || $httpMethod === 'HEAD') {
|
||||
$task = null;
|
||||
}
|
||||
$route = $request->getAttribute('route');
|
||||
$user = $route->getGravParam('user');
|
||||
$token = $route->getGravParam('token');
|
||||
}
|
||||
$params = [$user, $token];
|
||||
break;
|
||||
default:
|
||||
$template = 'login';
|
||||
}
|
||||
|
||||
$method = ($task ? 'task' : 'display') . ucfirst($task ?? $template);
|
||||
if (!method_exists($class, $method)) {
|
||||
$method = 'displayUnauthorized';
|
||||
}
|
||||
|
||||
return [
|
||||
'controller' => [
|
||||
'class' => $class,
|
||||
'method' => $method,
|
||||
'params' => $params
|
||||
],
|
||||
'template' => $template,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
---
|
||||
title: Forgot password
|
||||
expires: 0
|
||||
access:
|
||||
admin.login: false
|
||||
|
||||
forms:
|
||||
admin-login-forgot:
|
||||
type: admin
|
||||
method: post
|
||||
|
||||
form:
|
||||
fields:
|
||||
- name: username
|
||||
type: text
|
||||
placeholder: PLUGIN_ADMIN.USERNAME
|
||||
autofocus: true
|
||||
validate:
|
||||
required: true
|
||||
username:
|
||||
type: text
|
||||
placeholder: PLUGIN_ADMIN.USERNAME
|
||||
autofocus: true
|
||||
validate:
|
||||
required: true
|
||||
---
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
---
|
||||
title: Admin Login
|
||||
expires: 0
|
||||
access:
|
||||
admin.login: false
|
||||
|
||||
forms:
|
||||
login:
|
||||
action:
|
||||
type: admin
|
||||
method: post
|
||||
|
||||
fields:
|
||||
@@ -22,7 +24,7 @@ forms:
|
||||
required: true
|
||||
|
||||
login-twofa:
|
||||
action:
|
||||
type: admin
|
||||
method: post
|
||||
|
||||
fields:
|
||||
|
||||
@@ -1,58 +1,63 @@
|
||||
---
|
||||
title: Register Admin User
|
||||
expires: 0
|
||||
access:
|
||||
admin.login: false
|
||||
|
||||
form:
|
||||
fields:
|
||||
- name: username
|
||||
type: text
|
||||
label: PLUGIN_ADMIN.USERNAME
|
||||
autofocus: true
|
||||
placeholder: PLUGIN_ADMIN.USERNAME_PLACEHOLDER
|
||||
validate:
|
||||
required: true
|
||||
message: PLUGIN_LOGIN.USERNAME_NOT_VALID
|
||||
config-pattern@: system.username_regex
|
||||
forms:
|
||||
admin-login-register:
|
||||
type: admin
|
||||
method: post
|
||||
|
||||
- name: email
|
||||
type: email
|
||||
label: PLUGIN_ADMIN.EMAIL
|
||||
placeholder: "valid email address"
|
||||
validate:
|
||||
fields:
|
||||
username:
|
||||
type: text
|
||||
label: PLUGIN_ADMIN.USERNAME
|
||||
autofocus: true
|
||||
placeholder: PLUGIN_ADMIN.USERNAME_PLACEHOLDER
|
||||
validate:
|
||||
required: true
|
||||
message: PLUGIN_LOGIN.USERNAME_NOT_VALID
|
||||
config-pattern@: system.username_regex
|
||||
|
||||
email:
|
||||
type: email
|
||||
message: PLUGIN_ADMIN.EMAIL_VALIDATION_MESSAGE
|
||||
required: true
|
||||
label: PLUGIN_ADMIN.EMAIL
|
||||
placeholder: "valid email address"
|
||||
validate:
|
||||
type: email
|
||||
message: PLUGIN_ADMIN.EMAIL_VALIDATION_MESSAGE
|
||||
required: true
|
||||
|
||||
- name: password1
|
||||
type: password
|
||||
label: PLUGIN_ADMIN.PASSWORD
|
||||
placeholder: PLUGIN_ADMIN.PWD_PLACEHOLDER
|
||||
validate:
|
||||
required: true
|
||||
message: PLUGIN_ADMIN.PASSWORD_VALIDATION_MESSAGE
|
||||
config-pattern@: system.pwd_regex
|
||||
password1:
|
||||
type: password
|
||||
label: PLUGIN_ADMIN.PASSWORD
|
||||
placeholder: PLUGIN_ADMIN.PWD_PLACEHOLDER
|
||||
validate:
|
||||
required: true
|
||||
message: PLUGIN_ADMIN.PASSWORD_VALIDATION_MESSAGE
|
||||
config-pattern@: system.pwd_regex
|
||||
|
||||
- name: password2
|
||||
type: password
|
||||
label: PLUGIN_ADMIN.PASSWORD_CONFIRM
|
||||
placeholder: PLUGIN_ADMIN.PWD_PLACEHOLDER
|
||||
validate:
|
||||
required: true
|
||||
message: PLUGIN_ADMIN.PASSWORD_VALIDATION_MESSAGE
|
||||
config-pattern@: system.pwd_regex
|
||||
password2:
|
||||
type: password
|
||||
label: PLUGIN_ADMIN.PASSWORD_CONFIRM
|
||||
placeholder: PLUGIN_ADMIN.PWD_PLACEHOLDER
|
||||
validate:
|
||||
required: true
|
||||
message: PLUGIN_ADMIN.PASSWORD_VALIDATION_MESSAGE
|
||||
config-pattern@: system.pwd_regex
|
||||
|
||||
- name: fullname
|
||||
type: text
|
||||
placeholder: "e.g. 'Joe Schmoe'"
|
||||
label: PLUGIN_ADMIN.FULL_NAME
|
||||
|
||||
- name: title
|
||||
type: text
|
||||
placeholder: "e.g. 'Administrator'"
|
||||
label: PLUGIN_ADMIN.TITLE
|
||||
|
||||
process:
|
||||
register_admin_user: true
|
||||
fullname:
|
||||
type: text
|
||||
placeholder: "e.g. 'Joe Schmoe'"
|
||||
label: PLUGIN_ADMIN.FULL_NAME
|
||||
validate:
|
||||
required: true
|
||||
|
||||
title:
|
||||
type: text
|
||||
placeholder: "e.g. 'Administrator'"
|
||||
label: PLUGIN_ADMIN.TITLE
|
||||
---
|
||||
|
||||
The Admin plugin has been installed, but no **admin accounts** could be found. Please create an admin account to ensure your Grav install is secure...
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
---
|
||||
title: Reset password
|
||||
expires: 0
|
||||
access:
|
||||
admin.login: false
|
||||
|
||||
|
||||
forms:
|
||||
admin-login-reset:
|
||||
type: admin
|
||||
method: post
|
||||
|
||||
form:
|
||||
fields:
|
||||
- name: username
|
||||
type: text
|
||||
placeholder: PLUGIN_ADMIN.USERNAME
|
||||
readonly: true
|
||||
- name: password
|
||||
type: password
|
||||
placeholder: PLUGIN_ADMIN.PASSWORD
|
||||
autofocus: true
|
||||
- name: token
|
||||
type: hidden
|
||||
username:
|
||||
type: text
|
||||
placeholder: PLUGIN_ADMIN.USERNAME
|
||||
readonly: true
|
||||
password:
|
||||
type: password
|
||||
placeholder: PLUGIN_ADMIN.PASSWORD
|
||||
autofocus: true
|
||||
token:
|
||||
type: hidden
|
||||
---
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{% embed 'partials/login.html.twig' with {title:'Grav Forgot Password'} %}
|
||||
|
||||
{% block form %}
|
||||
{% for field in form.fields %}
|
||||
{% for field_name,field in form.fields %}
|
||||
{% if field.type %}
|
||||
{% set field = field|merge({ name: field.name ?? field_name }) %}
|
||||
{% set value = form.value(field.name) %}
|
||||
<div>
|
||||
{% include ["forms/fields/#{field.type}/#{field.type}.html.twig", 'forms/fields/text/text.html.twig'] %}
|
||||
</div>
|
||||
|
||||
@@ -12,9 +12,10 @@
|
||||
{% block form %}
|
||||
{% set form = forms['login'] %}
|
||||
|
||||
{% for field in form.fields %}
|
||||
{% set value = field.name == 'username' ? username : '' %}
|
||||
{% for field_name,field in form.fields %}
|
||||
{% if field.type %}
|
||||
{% set field = field|merge({ name: field.name ?? field_name }) %}
|
||||
{% set value = form.value(field.name) %}
|
||||
<div>
|
||||
{% include ["forms/fields/#{field.type}/#{field.type}.html.twig", 'forms/fields/text/text.html.twig'] %}
|
||||
</div>
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
|
||||
{% set form = forms['login-twofa'] %}
|
||||
|
||||
{% for field in form.fields %}
|
||||
{% for field_name, field in form.fields %}
|
||||
{% if field.type %}
|
||||
{% set field = field|merge({ name: field.name ?? field_name }) %}
|
||||
{% set value = form.value(field.name) %}
|
||||
<div>
|
||||
{% include ["forms/fields/#{field.type}/#{field.type}.html.twig", 'forms/fields/text/text.html.twig'] %}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{% extends 'partials/base.html.twig' %}
|
||||
{% set scope = scope ?: 'data.' %}
|
||||
{% set scope = form.scope %}
|
||||
|
||||
{% block messages %}{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
@@ -14,13 +15,10 @@
|
||||
|
||||
{% block integration %}{% endblock %}
|
||||
|
||||
{% set redirect = redirect ?: uri.path ~ uri.params ~ (uri.query ? '?' ~ uri.query : '') %}
|
||||
|
||||
<form method="post" action="{{ admin_route('/')|trim('/', 'right') }}">
|
||||
<form method="post" action="">
|
||||
<div class="padding">
|
||||
{% block form %}{% endblock %}
|
||||
<input type="hidden" name="redirect" value="{{ redirect }}" />
|
||||
{{ nonce_field('admin-form', 'admin-nonce')|raw }}
|
||||
{{ nonce_field(form.getNonceAction(), form.getNonceName())|raw }}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{% extends 'partials/base.html.twig' %}
|
||||
{% set scope = scope ?: 'data.' %}
|
||||
{% set scope = form.scope %}
|
||||
|
||||
{% block body %}
|
||||
<body id="admin-login-wrapper">
|
||||
@@ -10,11 +10,12 @@
|
||||
|
||||
{% block instructions %}{% endblock %}
|
||||
|
||||
<form method="post" action="{{ admin_route('/') }}">
|
||||
<form method="post" action="">
|
||||
<div class="padding">
|
||||
{% block form %}{% endblock %}
|
||||
|
||||
{{ nonce_field('form', 'form-nonce')|raw }}
|
||||
{% block form %}{% endblock %}
|
||||
{% include "forms/fields/formname/formname.html.twig" %}
|
||||
{% include 'forms/fields/uniqueid/uniqueid.html.twig' %}
|
||||
{{ nonce_field(form.getNonceAction() ?? 'form', form.getNonceName() ?? 'form-nonce')|raw }}
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
|
||||
@@ -7,8 +7,9 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block form %}
|
||||
{% for field in form.fields %}
|
||||
{% for field_name,field in form.fields %}
|
||||
{% if field.type %}
|
||||
{% set field = field|merge({ name: field.name ?? field_name }) %}
|
||||
{% set value = form.value(field.name) %}
|
||||
<div class="wrapper-{{ field.name }}">
|
||||
{% include ["forms/fields/#{field.type}/#{field.type}.html.twig", 'forms/fields/text/text.html.twig'] %}
|
||||
@@ -19,7 +20,7 @@
|
||||
<div class="form-actions primary-accent">
|
||||
|
||||
<button type="reset" class="button secondary"><i class="fa fa-exclamation-circle"></i> {{ 'PLUGIN_ADMIN.LOGIN_BTN_CLEAR'|tu }}</button>
|
||||
<button type="submit" class="button primary"><i class="fa fa-sign-in"></i> {{ 'PLUGIN_ADMIN.LOGIN_BTN_CREATE_USER'|tu }}</button>
|
||||
<button type="submit" class="button primary" name="task" value="register"><i class="fa fa-sign-in"></i> {{ 'PLUGIN_ADMIN.LOGIN_BTN_CREATE_USER'|tu }}</button>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
{% embed 'partials/login.html.twig' with {title:'Grav Reset Password'} %}
|
||||
|
||||
{% block form %}
|
||||
{% for field in form.fields %}
|
||||
{% set value = attribute(admin.forgot, field.name) is defined ? attribute(admin.forgot, field.name) : null %}
|
||||
|
||||
{% for field_name,field in form.fields %}
|
||||
{% if field.type %}
|
||||
{% set field = field|merge({ name: field.name ?? field_name }) %}
|
||||
{% set value = form.value(field.name) %}
|
||||
<div>
|
||||
{% include ["forms/fields/#{field.type}/#{field.type}.html.twig", 'forms/fields/text/text.html.twig'] %}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user