123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874 |
- <?php
- /**
- * @package Grav\Framework\Form
- *
- * @copyright Copyright (c) 2015 - 2022 Trilby Media, LLC. All rights reserved.
- * @license MIT License; see LICENSE file for details.
- */
- namespace Grav\Framework\Form\Traits;
- use ArrayAccess;
- use Exception;
- use FilesystemIterator;
- use Grav\Common\Data\Blueprint;
- use Grav\Common\Data\Data;
- use Grav\Common\Data\ValidationException;
- use Grav\Common\Debugger;
- use Grav\Common\Form\FormFlash;
- use Grav\Common\Grav;
- use Grav\Common\Twig\Twig;
- use Grav\Common\User\Interfaces\UserInterface;
- use Grav\Common\Utils;
- use Grav\Framework\Compat\Serializable;
- use Grav\Framework\ContentBlock\HtmlBlock;
- use Grav\Framework\Form\FormFlashFile;
- use Grav\Framework\Form\Interfaces\FormFlashInterface;
- use Grav\Framework\Form\Interfaces\FormInterface;
- use Grav\Framework\Session\SessionInterface;
- use Psr\Http\Message\ServerRequestInterface;
- use Psr\Http\Message\UploadedFileInterface;
- use RuntimeException;
- use SplFileInfo;
- use Twig\Error\LoaderError;
- use Twig\Error\SyntaxError;
- use Twig\Template;
- use Twig\TemplateWrapper;
- use function in_array;
- use function is_array;
- use function is_object;
- /**
- * Trait FormTrait
- * @package Grav\Framework\Form
- */
- trait FormTrait
- {
- use Serializable;
- /** @var string */
- public $status = 'success';
- /** @var string|null */
- public $message;
- /** @var string[] */
- public $messages = [];
- /** @var string */
- private $name;
- /** @var string */
- private $id;
- /** @var bool */
- private $enabled = true;
- /** @var string */
- private $uniqueid;
- /** @var string */
- private $sessionid;
- /** @var bool */
- private $submitted;
- /** @var ArrayAccess<string,mixed>|Data|null */
- private $data;
- /** @var UploadedFileInterface[] */
- private $files = [];
- /** @var FormFlashInterface|null */
- private $flash;
- /** @var string */
- private $flashFolder;
- /** @var Blueprint */
- private $blueprint;
- /**
- * @return string
- */
- public function getId(): string
- {
- return $this->id;
- }
- /**
- * @param string $id
- */
- public function setId(string $id): void
- {
- $this->id = $id;
- }
- /**
- * @return void
- */
- public function disable(): void
- {
- $this->enabled = false;
- }
- /**
- * @return void
- */
- public function enable(): void
- {
- $this->enabled = true;
- }
- /**
- * @return bool
- */
- public function isEnabled(): bool
- {
- return $this->enabled;
- }
- /**
- * @return string
- */
- public function getUniqueId(): string
- {
- return $this->uniqueid;
- }
- /**
- * @param string $uniqueId
- * @return void
- */
- public function setUniqueId(string $uniqueId): void
- {
- $this->uniqueid = $uniqueId;
- }
- /**
- * @return string
- */
- public function getName(): string
- {
- return $this->name;
- }
- /**
- * @return string
- */
- public function getFormName(): string
- {
- return $this->name;
- }
- /**
- * @return string
- */
- public function getNonceName(): string
- {
- return 'form-nonce';
- }
- /**
- * @return string
- */
- public function getNonceAction(): string
- {
- return 'form';
- }
- /**
- * @return string
- */
- public function getNonce(): string
- {
- return Utils::getNonce($this->getNonceAction());
- }
- /**
- * @return string
- */
- public function getAction(): string
- {
- return '';
- }
- /**
- * @return string
- */
- public function getTask(): string
- {
- return $this->getBlueprint()->get('form/task') ?? '';
- }
- /**
- * @param string|null $name
- * @return mixed
- */
- public function getData(string $name = null)
- {
- return null !== $name ? $this->data[$name] : $this->data;
- }
- /**
- * @return array|UploadedFileInterface[]
- */
- public function getFiles(): array
- {
- return $this->files;
- }
- /**
- * @param string $name
- * @return mixed|null
- */
- public function getValue(string $name)
- {
- return $this->data[$name] ?? null;
- }
- /**
- * @param string $name
- * @return mixed|null
- */
- public function getDefaultValue(string $name)
- {
- $path = explode('.', $name);
- $offset = array_shift($path);
- $current = $this->getDefaultValues();
- if (!isset($current[$offset])) {
- return null;
- }
- $current = $current[$offset];
- while ($path) {
- $offset = array_shift($path);
- if ((is_array($current) || $current instanceof ArrayAccess) && isset($current[$offset])) {
- $current = $current[$offset];
- } elseif (is_object($current) && isset($current->{$offset})) {
- $current = $current->{$offset};
- } else {
- return null;
- }
- };
- return $current;
- }
- /**
- * @return array
- */
- public function getDefaultValues(): array
- {
- return $this->getBlueprint()->getDefaults();
- }
- /**
- * @param ServerRequestInterface $request
- * @return FormInterface|$this
- */
- public function handleRequest(ServerRequestInterface $request): FormInterface
- {
- // Set current form to be active.
- $grav = Grav::instance();
- $forms = $grav['forms'] ?? null;
- if ($forms) {
- $forms->setActiveForm($this);
- /** @var Twig $twig */
- $twig = $grav['twig'];
- $twig->twig_vars['form'] = $this;
- }
- try {
- [$data, $files] = $this->parseRequest($request);
- $this->submit($data, $files);
- } catch (Exception $e) {
- /** @var Debugger $debugger */
- $debugger = $grav['debugger'];
- $debugger->addException($e);
- $this->setError($e->getMessage());
- }
- return $this;
- }
- /**
- * @param ServerRequestInterface $request
- * @return FormInterface|$this
- */
- public function setRequest(ServerRequestInterface $request): FormInterface
- {
- [$data, $files] = $this->parseRequest($request);
- $this->data = new Data($data, $this->getBlueprint());
- $this->files = $files;
- return $this;
- }
- /**
- * @return bool
- */
- public function isValid(): bool
- {
- return $this->status === 'success';
- }
- /**
- * @return string|null
- */
- public function getError(): ?string
- {
- return !$this->isValid() ? $this->message : null;
- }
- /**
- * @return array
- */
- public function getErrors(): array
- {
- return !$this->isValid() ? $this->messages : [];
- }
- /**
- * @return bool
- */
- public function isSubmitted(): bool
- {
- return $this->submitted;
- }
- /**
- * @return bool
- */
- public function validate(): bool
- {
- if (!$this->isValid()) {
- return false;
- }
- try {
- $this->validateData($this->data);
- $this->validateUploads($this->getFiles());
- } catch (ValidationException $e) {
- $this->setErrors($e->getMessages());
- } catch (Exception $e) {
- /** @var Debugger $debugger */
- $debugger = Grav::instance()['debugger'];
- $debugger->addException($e);
- $this->setError($e->getMessage());
- }
- $this->filterData($this->data);
- return $this->isValid();
- }
- /**
- * @param array $data
- * @param UploadedFileInterface[]|null $files
- * @return FormInterface|$this
- */
- public function submit(array $data, array $files = null): FormInterface
- {
- try {
- if ($this->isSubmitted()) {
- throw new RuntimeException('Form has already been submitted');
- }
- $this->data = new Data($data, $this->getBlueprint());
- $this->files = $files ?? [];
- if (!$this->validate()) {
- return $this;
- }
- $this->doSubmit($this->data->toArray(), $this->files);
- $this->submitted = true;
- } catch (Exception $e) {
- /** @var Debugger $debugger */
- $debugger = Grav::instance()['debugger'];
- $debugger->addException($e);
- $this->setError($e->getMessage());
- }
- return $this;
- }
- /**
- * @return void
- */
- public function reset(): void
- {
- // Make sure that the flash object gets deleted.
- $this->getFlash()->delete();
- $this->data = null;
- $this->files = [];
- $this->status = 'success';
- $this->message = null;
- $this->messages = [];
- $this->submitted = false;
- $this->flash = null;
- }
- /**
- * @return array
- */
- public function getFields(): array
- {
- return $this->getBlueprint()->fields();
- }
- /**
- * @return array
- */
- public function getButtons(): array
- {
- return $this->getBlueprint()->get('form/buttons') ?? [];
- }
- /**
- * @return array
- */
- public function getTasks(): array
- {
- return $this->getBlueprint()->get('form/tasks') ?? [];
- }
- /**
- * @return Blueprint
- */
- abstract public function getBlueprint(): Blueprint;
- /**
- * Get form flash object.
- *
- * @return FormFlashInterface
- */
- public function getFlash()
- {
- if (null === $this->flash) {
- $grav = Grav::instance();
- $config = [
- 'session_id' => $this->getSessionId(),
- 'unique_id' => $this->getUniqueId(),
- 'form_name' => $this->getName(),
- 'folder' => $this->getFlashFolder()
- ];
- $this->flash = new FormFlash($config);
- $this->flash->setUrl($grav['uri']->url)->setUser($grav['user'] ?? null);
- }
- return $this->flash;
- }
- /**
- * Get all available form flash objects for this form.
- *
- * @return FormFlashInterface[]
- */
- public function getAllFlashes(): array
- {
- $folder = $this->getFlashFolder();
- if (!$folder || !is_dir($folder)) {
- return [];
- }
- $name = $this->getName();
- $list = [];
- /** @var SplFileInfo $file */
- foreach (new FilesystemIterator($folder) as $file) {
- $uniqueId = $file->getFilename();
- $config = [
- 'session_id' => $this->getSessionId(),
- 'unique_id' => $uniqueId,
- 'form_name' => $name,
- 'folder' => $this->getFlashFolder()
- ];
- $flash = new FormFlash($config);
- if ($flash->exists() && $flash->getFormName() === $name) {
- $list[] = $flash;
- }
- }
- return $list;
- }
- /**
- * {@inheritdoc}
- * @see FormInterface::render()
- */
- public function render(string $layout = null, array $context = [])
- {
- if (null === $layout) {
- $layout = 'default';
- }
- $grav = Grav::instance();
- $block = HtmlBlock::create();
- $block->disableCache();
- $output = $this->getTemplate($layout)->render(
- ['grav' => $grav, 'config' => $grav['config'], 'block' => $block, 'form' => $this, 'layout' => $layout] + $context
- );
- $block->setContent($output);
- return $block;
- }
- /**
- * @return array
- */
- public function jsonSerialize(): array
- {
- return $this->doSerialize();
- }
- /**
- * @return array
- */
- final public function __serialize(): array
- {
- return $this->doSerialize();
- }
- /**
- * @param array $data
- * @return void
- */
- final public function __unserialize(array $data): void
- {
- $this->doUnserialize($data);
- }
- protected function getSessionId(): string
- {
- if (null === $this->sessionid) {
- /** @var Grav $grav */
- $grav = Grav::instance();
- /** @var SessionInterface|null $session */
- $session = $grav['session'] ?? null;
- $this->sessionid = $session ? ($session->getId() ?? '') : '';
- }
- return $this->sessionid;
- }
- /**
- * @param string $sessionId
- * @return void
- */
- protected function setSessionId(string $sessionId): void
- {
- $this->sessionid = $sessionId;
- }
- /**
- * @return void
- */
- protected function unsetFlash(): void
- {
- $this->flash = null;
- }
- /**
- * @return string|null
- */
- protected function getFlashFolder(): ?string
- {
- $grav = Grav::instance();
- /** @var UserInterface|null $user */
- $user = $grav['user'] ?? null;
- if (null !== $user && $user->exists()) {
- $username = $user->username;
- $mediaFolder = $user->getMediaFolder();
- } else {
- $username = null;
- $mediaFolder = null;
- }
- $session = $grav['session'] ?? null;
- $sessionId = $session ? $session->getId() : null;
- // Fill template token keys/value pairs.
- $dataMap = [
- '[FORM_NAME]' => $this->getName(),
- '[SESSIONID]' => $sessionId ?? '!!',
- '[USERNAME]' => $username ?? '!!',
- '[USERNAME_OR_SESSIONID]' => $username ?? $sessionId ?? '!!',
- '[ACCOUNT]' => $mediaFolder ?? '!!'
- ];
- $flashLookupFolder = $this->getFlashLookupFolder();
- $path = str_replace(array_keys($dataMap), array_values($dataMap), $flashLookupFolder);
- // Make sure we only return valid paths.
- return strpos($path, '!!') === false ? rtrim($path, '/') : null;
- }
- /**
- * @return string
- */
- protected function getFlashLookupFolder(): string
- {
- if (null === $this->flashFolder) {
- $this->flashFolder = $this->getBlueprint()->get('form/flash_folder') ?? 'tmp://forms/[SESSIONID]';
- }
- return $this->flashFolder;
- }
- /**
- * @param string $folder
- * @return void
- */
- protected function setFlashLookupFolder(string $folder): void
- {
- $this->flashFolder = $folder;
- }
- /**
- * Set a single error.
- *
- * @param string $error
- * @return void
- */
- protected function setError(string $error): void
- {
- $this->status = 'error';
- $this->message = $error;
- }
- /**
- * Set all errors.
- *
- * @param array $errors
- * @return void
- */
- protected function setErrors(array $errors): void
- {
- $this->status = 'error';
- $this->messages = $errors;
- }
- /**
- * @param string $layout
- * @return Template|TemplateWrapper
- * @throws LoaderError
- * @throws SyntaxError
- */
- protected function getTemplate($layout)
- {
- $grav = Grav::instance();
- /** @var Twig $twig */
- $twig = $grav['twig'];
- return $twig->twig()->resolveTemplate(
- [
- "forms/{$layout}/form.html.twig",
- 'forms/default/form.html.twig'
- ]
- );
- }
- /**
- * Parse PSR-7 ServerRequest into data and files.
- *
- * @param ServerRequestInterface $request
- * @return array
- */
- protected function parseRequest(ServerRequestInterface $request): array
- {
- $method = $request->getMethod();
- if (!in_array($method, ['PUT', 'POST', 'PATCH'])) {
- throw new RuntimeException(sprintf('FlexForm: Bad HTTP method %s', $method));
- }
- $body = (array)$request->getParsedBody();
- $data = isset($body['data']) ? $this->decodeData($body['data']) : null;
- $flash = $this->getFlash();
- /*
- if (null !== $data) {
- $flash->setData($data);
- $flash->save();
- }
- */
- $blueprint = $this->getBlueprint();
- $includeOriginal = (bool)($blueprint->form()['images']['original'] ?? null);
- $files = $flash->getFilesByFields($includeOriginal);
- $data = $blueprint->processForm($data ?? [], $body['toggleable_data'] ?? []);
- return [
- $data,
- $files
- ];
- }
- /**
- * Validate data and throw validation exceptions if validation fails.
- *
- * @param ArrayAccess|Data|null $data
- * @return void
- * @throws ValidationException
- * @phpstan-param ArrayAccess<string,mixed>|Data|null $data
- * @throws Exception
- */
- protected function validateData($data = null): void
- {
- if ($data instanceof Data) {
- $data->validate();
- }
- }
- /**
- * Filter validated data.
- *
- * @param ArrayAccess|Data|null $data
- * @return void
- * @phpstan-param ArrayAccess<string,mixed>|Data|null $data
- */
- protected function filterData($data = null): void
- {
- if ($data instanceof Data) {
- $data->filter();
- }
- }
- /**
- * Validate all uploaded files.
- *
- * @param array $files
- * @return void
- */
- protected function validateUploads(array $files): void
- {
- foreach ($files as $file) {
- if (null === $file) {
- continue;
- }
- if ($file instanceof UploadedFileInterface) {
- $this->validateUpload($file);
- } else {
- $this->validateUploads($file);
- }
- }
- }
- /**
- * Validate uploaded file.
- *
- * @param UploadedFileInterface $file
- * @return void
- */
- protected function validateUpload(UploadedFileInterface $file): void
- {
- // Handle bad filenames.
- $filename = $file->getClientFilename();
- if ($filename && !Utils::checkFilename($filename)) {
- $grav = Grav::instance();
- throw new RuntimeException(
- sprintf($grav['language']->translate('PLUGIN_FORM.FILEUPLOAD_UNABLE_TO_UPLOAD', null, true), $filename, 'Bad filename')
- );
- }
- if ($file instanceof FormFlashFile) {
- $file->checkXss();
- }
- }
- /**
- * Decode POST data
- *
- * @param array $data
- * @return array
- */
- protected function decodeData($data): array
- {
- if (!is_array($data)) {
- return [];
- }
- // Decode JSON encoded fields and merge them to data.
- if (isset($data['_json'])) {
- $data = array_replace_recursive($data, $this->jsonDecode($data['_json']));
- unset($data['_json']);
- }
- return $data;
- }
- /**
- * Recursively JSON decode POST data.
- *
- * @param array $data
- * @return array
- */
- protected function jsonDecode(array $data): array
- {
- foreach ($data as $key => &$value) {
- if (is_array($value)) {
- $value = $this->jsonDecode($value);
- } elseif (trim($value) === '') {
- unset($data[$key]);
- } else {
- $value = json_decode($value, true);
- if ($value === null && json_last_error() !== JSON_ERROR_NONE) {
- unset($data[$key]);
- $this->setError("Badly encoded JSON data (for {$key}) was sent to the form");
- }
- }
- }
- return $data;
- }
- /**
- * @return array
- */
- protected function doSerialize(): array
- {
- $data = $this->data instanceof Data ? $this->data->toArray() : null;
- return [
- 'name' => $this->name,
- 'id' => $this->id,
- 'uniqueid' => $this->uniqueid,
- 'submitted' => $this->submitted,
- 'status' => $this->status,
- 'message' => $this->message,
- 'messages' => $this->messages,
- 'data' => $data,
- 'files' => $this->files,
- ];
- }
- /**
- * @param array $data
- * @return void
- */
- protected function doUnserialize(array $data): void
- {
- $this->name = $data['name'];
- $this->id = $data['id'];
- $this->uniqueid = $data['uniqueid'];
- $this->submitted = $data['submitted'] ?? false;
- $this->status = $data['status'] ?? 'success';
- $this->message = $data['message'] ?? null;
- $this->messages = $data['messages'] ?? [];
- $this->data = isset($data['data']) ? new Data($data['data'], $this->getBlueprint()) : null;
- $this->files = $data['files'] ?? [];
- }
- }
|