"Fossies" - the Fresh Open Source Software Archive

Member "grav/system/src/Grav/Framework/Form/Traits/FormTrait.php" (1 Sep 2020, 17369 Bytes) of package /linux/www/grav-v1.6.27.zip:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) PHP source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "FormTrait.php" see the Fossies "Dox" file reference documentation.

    1 <?php
    2 
    3 /**
    4  * @package    Grav\Framework\Form
    5  *
    6  * @copyright  Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
    7  * @license    MIT License; see LICENSE file for details.
    8  */
    9 
   10 namespace Grav\Framework\Form\Traits;
   11 
   12 use Grav\Common\Data\Blueprint;
   13 use Grav\Common\Data\Data;
   14 use Grav\Common\Data\ValidationException;
   15 use Grav\Common\Form\FormFlash;
   16 use Grav\Common\Grav;
   17 use Grav\Common\Twig\Twig;
   18 use Grav\Common\User\Interfaces\UserInterface;
   19 use Grav\Common\Utils;
   20 use Grav\Framework\ContentBlock\HtmlBlock;
   21 use Grav\Framework\Form\Interfaces\FormInterface;
   22 use Grav\Framework\Session\SessionInterface;
   23 use Psr\Http\Message\ServerRequestInterface;
   24 use Psr\Http\Message\UploadedFileInterface;
   25 use Twig\Error\LoaderError;
   26 use Twig\Error\SyntaxError;
   27 use Twig\TemplateWrapper;
   28 
   29 /**
   30  * Trait FormTrait
   31  * @package Grav\Framework\Form
   32  */
   33 trait FormTrait
   34 {
   35     /** @var string */
   36     public $status = 'success';
   37     /** @var string */
   38     public $message;
   39     /** @var string[] */
   40     public $messages = [];
   41 
   42     /** @var string */
   43     private $name;
   44     /** @var string */
   45     private $id;
   46     /** @var string */
   47     private $uniqueid;
   48     /** @var bool */
   49     private $submitted;
   50     /** @var Data|object|null */
   51     private $data;
   52     /** @var array|UploadedFileInterface[] */
   53     private $files;
   54     /** @var FormFlash|null */
   55     private $flash;
   56     /** @var Blueprint */
   57     private $blueprint;
   58 
   59     public function getId(): string
   60     {
   61         return $this->id;
   62     }
   63 
   64     public function setId(string $id): void
   65     {
   66         $this->id = $id;
   67     }
   68 
   69     public function getUniqueId(): string
   70     {
   71         return $this->uniqueid;
   72     }
   73 
   74     public function setUniqueId(string $uniqueId): void
   75     {
   76         $this->uniqueid = $uniqueId;
   77     }
   78 
   79     public function getName(): string
   80     {
   81         return $this->name;
   82     }
   83 
   84     public function getFormName(): string
   85     {
   86         return $this->name;
   87     }
   88 
   89     public function getNonceName(): string
   90     {
   91         return 'form-nonce';
   92     }
   93 
   94     public function getNonceAction(): string
   95     {
   96         return 'form';
   97     }
   98 
   99     public function getNonce(): string
  100     {
  101         return Utils::getNonce($this->getNonceAction());
  102     }
  103 
  104     public function getAction(): string
  105     {
  106         return '';
  107     }
  108 
  109     public function getTask(): string
  110     {
  111         return $this->getBlueprint()->get('form/task') ?? '';
  112     }
  113 
  114     public function getData(string $name = null)
  115     {
  116         return null !== $name ? $this->data[$name] : $this->data;
  117     }
  118 
  119     /**
  120      * @return array|UploadedFileInterface[]
  121      */
  122     public function getFiles(): array
  123     {
  124         return $this->files ?? [];
  125     }
  126 
  127     public function getValue(string $name)
  128     {
  129         return $this->data[$name] ?? null;
  130     }
  131 
  132     public function getDefaultValue(string $name)
  133     {
  134         $path = explode('.', $name) ?: [];
  135         $offset = array_shift($path) ?? '';
  136 
  137         $current = $this->getDefaultValues();
  138 
  139         if (!isset($current[$offset])) {
  140             return null;
  141         }
  142 
  143         $current = $current[$offset];
  144 
  145         while ($path) {
  146             $offset = array_shift($path);
  147 
  148             if ((\is_array($current) || $current instanceof \ArrayAccess) && isset($current[$offset])) {
  149                 $current = $current[$offset];
  150             } elseif (\is_object($current) && isset($current->{$offset})) {
  151                 $current = $current->{$offset};
  152             } else {
  153                 return null;
  154             }
  155         };
  156 
  157         return $current;
  158     }
  159 
  160     /**
  161      * @return array
  162      */
  163     public function getDefaultValues(): array
  164     {
  165         return $this->getBlueprint()->getDefaults();
  166     }
  167 
  168     /**
  169      * @param ServerRequestInterface $request
  170      * @return FormInterface|$this
  171      */
  172     public function handleRequest(ServerRequestInterface $request): FormInterface
  173     {
  174         // Set current form to be active.
  175         $grav = Grav::instance();
  176         $forms = $grav['forms'] ?? null;
  177         if ($forms) {
  178             $forms->setActiveForm($this);
  179 
  180             /** @var Twig $twig */
  181             $twig = $grav['twig'];
  182             $twig->twig_vars['form'] = $this;
  183 
  184         }
  185 
  186         try {
  187             [$data, $files] = $this->parseRequest($request);
  188 
  189             $this->submit($data, $files);
  190         } catch (\Exception $e) {
  191             $this->setError($e->getMessage());
  192         }
  193 
  194         return $this;
  195     }
  196 
  197     /**
  198      * @param ServerRequestInterface $request
  199      * @return FormInterface|$this
  200      */
  201     public function setRequest(ServerRequestInterface $request): FormInterface
  202     {
  203         [$data, $files] = $this->parseRequest($request);
  204 
  205         $this->data = new Data($data, $this->getBlueprint());
  206         $this->files = $files;
  207 
  208         return $this;
  209     }
  210 
  211     public function isValid(): bool
  212     {
  213         return $this->status === 'success';
  214     }
  215 
  216     public function getError(): ?string
  217     {
  218         return !$this->isValid() ? $this->message : null;
  219     }
  220 
  221     public function getErrors(): array
  222     {
  223         return !$this->isValid() ? $this->messages : [];
  224     }
  225 
  226     public function isSubmitted(): bool
  227     {
  228         return $this->submitted;
  229     }
  230 
  231     public function validate(): bool
  232     {
  233         if (!$this->isValid()) {
  234             return false;
  235         }
  236 
  237         try {
  238             $this->validateData($this->data);
  239             $this->validateUploads($this->getFiles());
  240         } catch (ValidationException $e) {
  241             $this->setErrors($e->getMessages());
  242         }  catch (\Exception $e) {
  243             $this->setError($e->getMessage());
  244         }
  245 
  246         $this->filterData($this->data);
  247 
  248         return $this->isValid();
  249     }
  250 
  251     /**
  252      * @param array $data
  253      * @param UploadedFileInterface[] $files
  254      * @return FormInterface|$this
  255      */
  256     public function submit(array $data, array $files = null): FormInterface
  257     {
  258         try {
  259             if ($this->isSubmitted()) {
  260                 throw new \RuntimeException('Form has already been submitted');
  261             }
  262 
  263             $this->data = new Data($data, $this->getBlueprint());
  264             $this->files = $files ?? [];
  265 
  266             if (!$this->validate()) {
  267                 return $this;
  268             }
  269 
  270             $this->doSubmit($this->data->toArray(), $this->files);
  271 
  272             $this->submitted = true;
  273         } catch (\Exception $e) {
  274             $this->setError($e->getMessage());
  275         }
  276 
  277         return $this;
  278     }
  279 
  280     public function reset(): void
  281     {
  282         // Make sure that the flash object gets deleted.
  283         $this->getFlash()->delete();
  284 
  285         $this->data = null;
  286         $this->files = [];
  287         $this->status = 'success';
  288         $this->message = null;
  289         $this->messages = [];
  290         $this->submitted = false;
  291         $this->flash = null;
  292     }
  293 
  294     public function getFields(): array
  295     {
  296         return $this->getBlueprint()->fields();
  297     }
  298 
  299     public function getButtons(): array
  300     {
  301         return $this->getBlueprint()->get('form/buttons') ?? [];
  302     }
  303 
  304     public function getTasks(): array
  305     {
  306         return $this->getBlueprint()->get('form/tasks') ?? [];
  307     }
  308 
  309     abstract public function getBlueprint(): Blueprint;
  310 
  311     /**
  312      * Implements \Serializable::serialize().
  313      *
  314      * @return string
  315      */
  316     public function serialize(): string
  317     {
  318         return serialize($this->doSerialize());
  319     }
  320 
  321     /**
  322      * Implements \Serializable::unserialize().
  323      *
  324      * @param string $serialized
  325      */
  326     public function unserialize($serialized): void
  327     {
  328         $data = unserialize($serialized, ['allowed_classes' => false]);
  329 
  330         $this->doUnserialize($data);
  331     }
  332 
  333     /**
  334      * Get form flash object.
  335      *
  336      * @return FormFlash
  337      */
  338     public function getFlash(): FormFlash
  339     {
  340         if (null === $this->flash) {
  341             $grav = Grav::instance();
  342             $config = [
  343                 'session_id' => $this->getSessionId(),
  344                 'unique_id' => $this->getUniqueId(),
  345                 'form_name' => $this->getName(),
  346                 'folder' => $this->getFlashFolder()
  347             ];
  348 
  349 
  350             $this->flash = new FormFlash($config);
  351             $this->flash->setUrl($grav['uri']->url)->setUser($grav['user'] ?? null);
  352         }
  353 
  354         return $this->flash;
  355     }
  356 
  357     /**
  358      * Get all available form flash objects for this form.
  359      *
  360      * @return FormFlash[]
  361      */
  362     public function getAllFlashes(): array
  363     {
  364         $folder = $this->getFlashFolder();
  365         if (!$folder || !is_dir($folder)) {
  366             return [];
  367         }
  368 
  369         $name = $this->getName();
  370 
  371         $list = [];
  372         /** @var \SplFileInfo $file */
  373         foreach (new \FilesystemIterator($folder) as $file) {
  374             $uniqueId = $file->getFilename();
  375             $config = [
  376                 'session_id' => $this->getSessionId(),
  377                 'unique_id' => $uniqueId,
  378                 'form_name' => $name,
  379                 'folder' => $this->getFlashFolder()
  380             ];
  381             $flash = new FormFlash($config);
  382             if ($flash->exists() && $flash->getFormName() === $name) {
  383                 $list[] = $flash;
  384             }
  385         }
  386 
  387         return $list;
  388 
  389     }
  390 
  391     /**
  392      * {@inheritdoc}
  393      * @see FormInterface::render()
  394      */
  395     public function render(string $layout = null, array $context = [])
  396     {
  397         if (null === $layout) {
  398             $layout = 'default';
  399         }
  400 
  401         $grav = Grav::instance();
  402 
  403         $block = HtmlBlock::create();
  404         $block->disableCache();
  405 
  406         $output = $this->getTemplate($layout)->render(
  407             ['grav' => $grav, 'config' => $grav['config'], 'block' => $block, 'form' => $this, 'layout' => $layout] + $context
  408         );
  409 
  410         $block->setContent($output);
  411 
  412         return $block;
  413     }
  414 
  415     protected function getSessionId(): string
  416     {
  417         /** @var Grav $grav */
  418         $grav = Grav::instance();
  419 
  420         /** @var SessionInterface $session */
  421         $session = $grav['session'] ?? null;
  422 
  423         return $session ? ($session->getId() ?? '') : '';
  424     }
  425 
  426     protected function unsetFlash(): void
  427     {
  428         $this->flash = null;
  429     }
  430 
  431     protected function getFlashFolder(): ?string
  432     {
  433         $grav = Grav::instance();
  434 
  435         /** @var UserInterface $user */
  436         $user = $grav['user'] ?? null;
  437         $userExists = $user && $user->exists();
  438         $username = $userExists ? $user->username : null;
  439         $mediaFolder = $userExists ? $user->getMediaFolder() : null;
  440         $session = $grav['session'] ?? null;
  441         $sessionId = $session ? $session->getId() : null;
  442 
  443         // Fill template token keys/value pairs.
  444         $dataMap = [
  445             '[FORM_NAME]' => $this->getName(),
  446             '[SESSIONID]' => $sessionId ?? '!!',
  447             '[USERNAME]' => $username ?? '!!',
  448             '[USERNAME_OR_SESSIONID]' => $username ?? $sessionId ?? '!!',
  449             '[ACCOUNT]' => $mediaFolder ?? '!!'
  450         ];
  451 
  452         $flashFolder = $this->getBlueprint()->get('form/flash_folder', 'tmp://forms/[SESSIONID]');
  453 
  454         $path = str_replace(array_keys($dataMap), array_values($dataMap), $flashFolder);
  455 
  456         // Make sure we only return valid paths.
  457         return strpos($path, '!!') === false ? rtrim($path, '/') : null;
  458     }
  459 
  460     /**
  461      * Set a single error.
  462      *
  463      * @param string $error
  464      */
  465     protected function setError(string $error): void
  466     {
  467         $this->status = 'error';
  468         $this->message = $error;
  469     }
  470 
  471     /**
  472      * Set all errors.
  473      *
  474      * @param array $errors
  475      */
  476     protected function setErrors(array $errors): void
  477     {
  478         $this->status = 'error';
  479         $this->messages = $errors;
  480     }
  481 
  482     /**
  483      * @param string $layout
  484      * @return TemplateWrapper
  485      * @throws LoaderError
  486      * @throws SyntaxError
  487      */
  488     protected function getTemplate($layout)
  489     {
  490         $grav = Grav::instance();
  491 
  492         /** @var Twig $twig */
  493         $twig = $grav['twig'];
  494 
  495         return $twig->twig()->resolveTemplate(
  496             [
  497                 "forms/{$layout}/form.html.twig",
  498                 'forms/default/form.html.twig'
  499             ]
  500         );
  501     }
  502 
  503     /**
  504      * Parse PSR-7 ServerRequest into data and files.
  505      *
  506      * @param ServerRequestInterface $request
  507      * @return array
  508      */
  509     protected function parseRequest(ServerRequestInterface $request): array
  510     {
  511         $method = $request->getMethod();
  512         if (!\in_array($method, ['PUT', 'POST', 'PATCH'])) {
  513             throw new \RuntimeException(sprintf('FlexForm: Bad HTTP method %s', $method));
  514         }
  515 
  516         $body = $request->getParsedBody();
  517         $data = isset($body['data']) ? $this->decodeData($body['data']) : null;
  518 
  519         $flash = $this->getFlash();
  520         /*
  521         if (null !== $data) {
  522             $flash->setData($data);
  523             $flash->save();
  524         }
  525         */
  526 
  527         $blueprint = $this->getBlueprint();
  528         $includeOriginal = (bool)($blueprint->form()['images']['original'] ?? null);
  529         $files = $flash->getFilesByFields($includeOriginal);
  530 
  531         $data = $blueprint->processForm($data ?? [], $body['toggleable_data'] ?? []);
  532 
  533         return [
  534             $data,
  535             $files ?? []
  536         ];
  537     }
  538 
  539     /**
  540      * Form submit logic goes here.
  541      *
  542      * @param array $data
  543      * @param array $files
  544      * @return mixed
  545      */
  546     abstract protected function doSubmit(array $data, array $files);
  547 
  548     /**
  549      * Validate data and throw validation exceptions if validation fails.
  550      *
  551      * @param \ArrayAccess $data
  552      * @throws ValidationException
  553      * @throws \Exception
  554      */
  555     protected function validateData(\ArrayAccess $data): void
  556     {
  557         if ($data instanceof Data) {
  558             $data->validate();
  559         }
  560     }
  561 
  562     /**
  563      * Filter validated data.
  564      *
  565      * @param \ArrayAccess $data
  566      */
  567     protected function filterData(\ArrayAccess $data): void
  568     {
  569         if ($data instanceof Data) {
  570             $data->filter();
  571         }
  572     }
  573 
  574     /**
  575      * Validate all uploaded files.
  576      *
  577      * @param array $files
  578      */
  579     protected function validateUploads(array $files): void
  580     {
  581         foreach ($files as $file) {
  582             if (null === $file) {
  583                 continue;
  584             }
  585             if ($file instanceof UploadedFileInterface) {
  586                 $this->validateUpload($file);
  587             } else {
  588                 $this->validateUploads($file);
  589             }
  590         }
  591     }
  592 
  593     /**
  594      * Validate uploaded file.
  595      *
  596      * @param UploadedFileInterface $file
  597      */
  598     protected function validateUpload(UploadedFileInterface $file): void
  599     {
  600         // Handle bad filenames.
  601         $filename = $file->getClientFilename();
  602 
  603         if (!Utils::checkFilename($filename)) {
  604             $grav = Grav::instance();
  605             throw new \RuntimeException(
  606                 sprintf($grav['language']->translate('PLUGIN_FORM.FILEUPLOAD_UNABLE_TO_UPLOAD', null, true), $filename, 'Bad filename')
  607             );
  608         }
  609     }
  610 
  611     /**
  612      * Decode POST data
  613      *
  614      * @param array $data
  615      * @return array
  616      */
  617     protected function decodeData($data): array
  618     {
  619         if (!\is_array($data)) {
  620             return [];
  621         }
  622 
  623         // Decode JSON encoded fields and merge them to data.
  624         if (isset($data['_json'])) {
  625             $data = array_replace_recursive($data, $this->jsonDecode($data['_json']));
  626             unset($data['_json']);
  627         }
  628 
  629         return $data;
  630     }
  631 
  632     /**
  633      * Recursively JSON decode POST data.
  634      *
  635      * @param  array $data
  636      * @return array
  637      */
  638     protected function jsonDecode(array $data): array
  639     {
  640         foreach ($data as $key => &$value) {
  641             if (\is_array($value)) {
  642                 $value = $this->jsonDecode($value);
  643             } elseif (trim($value) === '') {
  644                 unset($data[$key]);
  645             } else {
  646                 $value = json_decode($value, true);
  647                 if ($value === null && json_last_error() !== JSON_ERROR_NONE) {
  648                     unset($data[$key]);
  649                     $this->setError("Badly encoded JSON data (for {$key}) was sent to the form");
  650                 }
  651             }
  652         }
  653 
  654         return $data;
  655     }
  656 
  657     /**
  658      * @return array
  659      */
  660     protected function doSerialize(): array
  661     {
  662         $data = $this->data instanceof Data ? $this->data->toArray() : null;
  663 
  664         return [
  665             'name' => $this->name,
  666             'id' => $this->id,
  667             'uniqueid' => $this->uniqueid,
  668             'submitted' => $this->submitted,
  669             'status' => $this->status,
  670             'message' => $this->message,
  671             'messages' => $this->messages,
  672             'data' => $data,
  673             'files' => $this->files,
  674         ];
  675     }
  676 
  677     /**
  678      * @param array $data
  679      */
  680     protected function doUnserialize(array $data): void
  681     {
  682         $this->name = $data['name'];
  683         $this->id = $data['id'];
  684         $this->uniqueid = $data['uniqueid'];
  685         $this->submitted = $data['submitted'] ?? false;
  686         $this->status = $data['status'] ?? 'success';
  687         $this->message = $data['message'] ?? null;
  688         $this->messages = $data['messages'] ?? [];
  689         $this->data = isset($data['data']) ? new Data($data['data'], $this->getBlueprint()) : null;
  690         $this->files = $data['files'] ?? [];
  691     }
  692 }