(Grav GitSync) Automatic Commit from smokephil
This commit is contained in:
commit
4267db646d
2765 changed files with 462171 additions and 0 deletions
2507
plugins/admin/classes/plugin/Admin.php
Normal file
2507
plugins/admin/classes/plugin/Admin.php
Normal file
File diff suppressed because it is too large
Load diff
1174
plugins/admin/classes/plugin/AdminBaseController.php
Normal file
1174
plugins/admin/classes/plugin/AdminBaseController.php
Normal file
File diff suppressed because it is too large
Load diff
3073
plugins/admin/classes/plugin/AdminController.php
Normal file
3073
plugins/admin/classes/plugin/AdminController.php
Normal file
File diff suppressed because it is too large
Load diff
182
plugins/admin/classes/plugin/AdminForm.php
Normal file
182
plugins/admin/classes/plugin/AdminForm.php
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
51
plugins/admin/classes/plugin/AdminFormFactory.php
Normal file
51
plugins/admin/classes/plugin/AdminFormFactory.php
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
414
plugins/admin/classes/plugin/Controllers/AbstractController.php
Normal file
414
plugins/admin/classes/plugin/Controllers/AbstractController.php
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
359
plugins/admin/classes/plugin/Controllers/AdminController.php
Normal file
359
plugins/admin/classes/plugin/Controllers/AdminController.php
Normal file
|
|
@ -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,634 @@
|
|||
<?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 ?? '';
|
||||
$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_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');
|
||||
}
|
||||
|
||||
$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'];
|
||||
}
|
||||
}
|
||||
442
plugins/admin/classes/plugin/Gpm.php
Normal file
442
plugins/admin/classes/plugin/Gpm.php
Normal file
|
|
@ -0,0 +1,442 @@
|
|||
<?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)) {
|
||||
$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());
|
||||
}
|
||||
}
|
||||
}
|
||||
310
plugins/admin/classes/plugin/Popularity.php
Normal file
310
plugins/admin/classes/plugin/Popularity.php
Normal file
|
|
@ -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, []);
|
||||
}
|
||||
}
|
||||
79
plugins/admin/classes/plugin/Router.php
Normal file
79
plugins/admin/classes/plugin/Router.php
Normal 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');
|
||||
}
|
||||
}
|
||||
93
plugins/admin/classes/plugin/Routers/LoginRouter.php
Normal file
93
plugins/admin/classes/plugin/Routers/LoginRouter.php
Normal file
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
70
plugins/admin/classes/plugin/ScssCompiler.php
Normal file
70
plugins/admin/classes/plugin/ScssCompiler.php
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
59
plugins/admin/classes/plugin/ScssList.php
Normal file
59
plugins/admin/classes/plugin/ScssList.php
Normal file
|
|
@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
29
plugins/admin/classes/plugin/Themes.php
Normal file
29
plugins/admin/classes/plugin/Themes.php
Normal file
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
138
plugins/admin/classes/plugin/Twig/AdminTwigExtension.php
Normal file
138
plugins/admin/classes/plugin/Twig/AdminTwigExtension.php
Normal file
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
61
plugins/admin/classes/plugin/Utils.php
Normal file
61
plugins/admin/classes/plugin/Utils.php
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
100
plugins/admin/classes/plugin/WhiteLabel.php
Normal file
100
plugins/admin/classes/plugin/WhiteLabel.php
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<?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();
|
||||
$this->scss = new ScssCompiler();
|
||||
}
|
||||
|
||||
public function compilePresetScss($config, $options = [
|
||||
'input' => 'plugin://admin/themes/grav/scss/preset.scss',
|
||||
'output' => 'asset://admin-preset.css'
|
||||
])
|
||||
{
|
||||
if (is_array($config)) {
|
||||
$color_scheme = $config['color_scheme'];
|
||||
} else {
|
||||
$color_scheme = $config->get('whitelabel.color_scheme');
|
||||
}
|
||||
|
||||
if ($color_scheme) {
|
||||
/** @var UniformResourceLocator $locator */
|
||||
$locator = $this->grav['locator'];
|
||||
|
||||
// Use ScssList object to make it easier ot handle in event
|
||||
$scss_list = new ScssList($locator->findResource($options['input']));
|
||||
$output_css = $locator->findResource(($options['output']), true, true);
|
||||
|
||||
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->scss->reset();
|
||||
|
||||
$compiler->setVariables($color_scheme['colors'] + $color_scheme['accents']);
|
||||
$compiler->setImportPaths($imports);
|
||||
$compiler->compileAll($input_scss, $output_css);
|
||||
} catch (\Exception $e) {
|
||||
return [false, $e->getMessage()];
|
||||
}
|
||||
|
||||
|
||||
return [true, 'Recompiled successfully'];
|
||||
|
||||
}
|
||||
return [false, ' Could not be recompiled, missing color scheme...'];
|
||||
}
|
||||
|
||||
public function exportPresetScsss($config, $location = 'asset://admin-theme-export.yaml')
|
||||
{
|
||||
|
||||
if (isset($config['color_scheme'])) {
|
||||
|
||||
$color_scheme = $config['color_scheme'];
|
||||
|
||||
$body = Yaml::dump($color_scheme);
|
||||
|
||||
$file = new File($location);
|
||||
$file->save($body);
|
||||
// todo: handle errors/exceptions?
|
||||
|
||||
return [true, 'File created successfully'];
|
||||
|
||||
} else {
|
||||
return [false, ' Could not export, missing color scheme...'];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue