FormTrait.php 21 KB

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