UserObject.php 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @package Grav\Common\Flex
  5. *
  6. * @copyright Copyright (c) 2015 - 2022 Trilby Media, LLC. All rights reserved.
  7. * @license MIT License; see LICENSE file for details.
  8. */
  9. namespace Grav\Common\Flex\Types\Users;
  10. use Closure;
  11. use Countable;
  12. use Grav\Common\Config\Config;
  13. use Grav\Common\Data\Blueprint;
  14. use Grav\Common\Flex\FlexObject;
  15. use Grav\Common\Flex\Traits\FlexGravTrait;
  16. use Grav\Common\Flex\Traits\FlexObjectTrait;
  17. use Grav\Common\Flex\Types\Users\Traits\UserObjectLegacyTrait;
  18. use Grav\Common\Grav;
  19. use Grav\Common\Media\Interfaces\MediaCollectionInterface;
  20. use Grav\Common\Media\Interfaces\MediaUploadInterface;
  21. use Grav\Common\Page\Media;
  22. use Grav\Common\Page\Medium\MediumFactory;
  23. use Grav\Common\User\Access;
  24. use Grav\Common\User\Authentication;
  25. use Grav\Common\Flex\Types\UserGroups\UserGroupCollection;
  26. use Grav\Common\Flex\Types\UserGroups\UserGroupIndex;
  27. use Grav\Common\User\Interfaces\UserInterface;
  28. use Grav\Common\User\Traits\UserTrait;
  29. use Grav\Common\Utils;
  30. use Grav\Framework\File\Formatter\JsonFormatter;
  31. use Grav\Framework\File\Formatter\YamlFormatter;
  32. use Grav\Framework\Filesystem\Filesystem;
  33. use Grav\Framework\Flex\Flex;
  34. use Grav\Framework\Flex\FlexDirectory;
  35. use Grav\Framework\Flex\Storage\FileStorage;
  36. use Grav\Framework\Flex\Traits\FlexMediaTrait;
  37. use Grav\Framework\Form\FormFlashFile;
  38. use Psr\Http\Message\UploadedFileInterface;
  39. use RocketTheme\Toolbox\Event\Event;
  40. use RocketTheme\Toolbox\File\FileInterface;
  41. use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
  42. use RuntimeException;
  43. use function is_array;
  44. use function is_bool;
  45. use function is_object;
  46. /**
  47. * Flex User
  48. *
  49. * Flex User is mostly compatible with the older User class, except on few key areas:
  50. *
  51. * - Constructor parameters have been changed. Old code creating a new user does not work.
  52. * - Serializer has been changed -- existing sessions will be killed.
  53. *
  54. * @package Grav\Common\User
  55. *
  56. * @property string $username
  57. * @property string $email
  58. * @property string $fullname
  59. * @property string $state
  60. * @property array $groups
  61. * @property array $access
  62. * @property bool $authenticated
  63. * @property bool $authorized
  64. */
  65. class UserObject extends FlexObject implements UserInterface, Countable
  66. {
  67. use FlexGravTrait;
  68. use FlexObjectTrait;
  69. use FlexMediaTrait {
  70. getMedia as private getFlexMedia;
  71. getMediaFolder as private getFlexMediaFolder;
  72. }
  73. use UserTrait;
  74. use UserObjectLegacyTrait;
  75. /** @var Closure|null */
  76. static public $authorizeCallable;
  77. /** @var Closure|null */
  78. static public $isAuthorizedCallable;
  79. /** @var array|null */
  80. protected $_uploads_original;
  81. /** @var FileInterface|null */
  82. protected $_storage;
  83. /** @var UserGroupIndex */
  84. protected $_groups;
  85. /** @var Access */
  86. protected $_access;
  87. /** @var array|null */
  88. protected $access;
  89. /**
  90. * @return array
  91. */
  92. public static function getCachedMethods(): array
  93. {
  94. return [
  95. 'authorize' => 'session',
  96. 'load' => false,
  97. 'find' => false,
  98. 'remove' => false,
  99. 'get' => true,
  100. 'set' => false,
  101. 'undef' => false,
  102. 'def' => false,
  103. ] + parent::getCachedMethods();
  104. }
  105. /**
  106. * UserObject constructor.
  107. * @param array $elements
  108. * @param string $key
  109. * @param FlexDirectory $directory
  110. * @param bool $validate
  111. */
  112. public function __construct(array $elements, $key, FlexDirectory $directory, bool $validate = false)
  113. {
  114. // User can only be authenticated via login.
  115. unset($elements['authenticated'], $elements['authorized']);
  116. // Define username if it's not set.
  117. if (!isset($elements['username'])) {
  118. $storageKey = $elements['__META']['storage_key'] ?? null;
  119. $storage = $directory->getStorage();
  120. if (null !== $storageKey && method_exists($storage, 'normalizeKey') && $key === $storage->normalizeKey($storageKey)) {
  121. $elements['username'] = $storageKey;
  122. } else {
  123. $elements['username'] = $key;
  124. }
  125. }
  126. // Define state if it isn't set.
  127. if (!isset($elements['state'])) {
  128. $elements['state'] = 'enabled';
  129. }
  130. parent::__construct($elements, $key, $directory, $validate);
  131. }
  132. public function __clone()
  133. {
  134. $this->_access = null;
  135. $this->_groups = null;
  136. parent::__clone();
  137. }
  138. /**
  139. * @return void
  140. */
  141. public function onPrepareRegistration(): void
  142. {
  143. if (!$this->getProperty('access')) {
  144. /** @var Config $config */
  145. $config = Grav::instance()['config'];
  146. $groups = $config->get('plugins.login.user_registration.groups', '');
  147. $access = $config->get('plugins.login.user_registration.access', ['site' => ['login' => true]]);
  148. $this->setProperty('groups', $groups);
  149. $this->setProperty('access', $access);
  150. }
  151. }
  152. /**
  153. * Helper to get content editor will fall back if not set
  154. *
  155. * @return string
  156. */
  157. public function getContentEditor(): string
  158. {
  159. return $this->getProperty('content_editor', 'default');
  160. }
  161. /**
  162. * Get value by using dot notation for nested arrays/objects.
  163. *
  164. * @example $value = $this->get('this.is.my.nested.variable');
  165. *
  166. * @param string $name Dot separated path to the requested value.
  167. * @param mixed $default Default value (or null).
  168. * @param string|null $separator Separator, defaults to '.'
  169. * @return mixed Value.
  170. */
  171. public function get($name, $default = null, $separator = null)
  172. {
  173. return $this->getNestedProperty($name, $default, $separator);
  174. }
  175. /**
  176. * Set value by using dot notation for nested arrays/objects.
  177. *
  178. * @example $data->set('this.is.my.nested.variable', $value);
  179. *
  180. * @param string $name Dot separated path to the requested value.
  181. * @param mixed $value New value.
  182. * @param string|null $separator Separator, defaults to '.'
  183. * @return $this
  184. */
  185. public function set($name, $value, $separator = null)
  186. {
  187. $this->setNestedProperty($name, $value, $separator);
  188. return $this;
  189. }
  190. /**
  191. * Unset value by using dot notation for nested arrays/objects.
  192. *
  193. * @example $data->undef('this.is.my.nested.variable');
  194. *
  195. * @param string $name Dot separated path to the requested value.
  196. * @param string|null $separator Separator, defaults to '.'
  197. * @return $this
  198. */
  199. public function undef($name, $separator = null)
  200. {
  201. $this->unsetNestedProperty($name, $separator);
  202. return $this;
  203. }
  204. /**
  205. * Set default value by using dot notation for nested arrays/objects.
  206. *
  207. * @example $data->def('this.is.my.nested.variable', 'default');
  208. *
  209. * @param string $name Dot separated path to the requested value.
  210. * @param mixed $default Default value (or null).
  211. * @param string|null $separator Separator, defaults to '.'
  212. * @return $this
  213. */
  214. public function def($name, $default = null, $separator = null)
  215. {
  216. $this->defNestedProperty($name, $default, $separator);
  217. return $this;
  218. }
  219. /**
  220. * @param UserInterface|null $user
  221. * @return bool
  222. */
  223. public function isMyself(?UserInterface $user = null): bool
  224. {
  225. if (null === $user) {
  226. $user = $this->getActiveUser();
  227. if ($user && !$user->authenticated) {
  228. $user = null;
  229. }
  230. }
  231. return $user && $this->username === $user->username;
  232. }
  233. /**
  234. * Checks user authorization to the action.
  235. *
  236. * @param string $action
  237. * @param string|null $scope
  238. * @return bool|null
  239. */
  240. public function authorize(string $action, string $scope = null): ?bool
  241. {
  242. if ($scope === 'test') {
  243. // Special scope to test user permissions.
  244. $scope = null;
  245. } else {
  246. // User needs to be enabled.
  247. if ($this->getProperty('state') !== 'enabled') {
  248. return false;
  249. }
  250. // User needs to be logged in.
  251. if (!$this->getProperty('authenticated')) {
  252. return false;
  253. }
  254. if (strpos($action, 'login') === false && !$this->getProperty('authorized')) {
  255. // User needs to be authorized (2FA).
  256. return false;
  257. }
  258. // Workaround bug in Login::isUserAuthorizedForPage() <= Login v3.0.4
  259. if ((string)(int)$action === $action) {
  260. return false;
  261. }
  262. }
  263. // Check custom application access.
  264. $authorizeCallable = static::$authorizeCallable;
  265. if ($authorizeCallable instanceof Closure) {
  266. $callable = $authorizeCallable->bindTo($this, $this);
  267. $authorized = $callable($action, $scope);
  268. if (is_bool($authorized)) {
  269. return $authorized;
  270. }
  271. }
  272. // Check user access.
  273. $access = $this->getAccess();
  274. $authorized = $access->authorize($action, $scope);
  275. if (is_bool($authorized)) {
  276. return $authorized;
  277. }
  278. // Check group access.
  279. $authorized = $this->getGroups()->authorize($action, $scope);
  280. if (is_bool($authorized)) {
  281. return $authorized;
  282. }
  283. // If any specific rule isn't hit, check if user is a superuser.
  284. return $access->authorize('admin.super') === true;
  285. }
  286. /**
  287. * @param string $property
  288. * @param mixed $default
  289. * @return mixed
  290. */
  291. public function getProperty($property, $default = null)
  292. {
  293. $value = parent::getProperty($property, $default);
  294. if ($property === 'avatar') {
  295. $settings = $this->getMediaFieldSettings($property);
  296. $value = $this->parseFileProperty($value, $settings);
  297. }
  298. return $value;
  299. }
  300. /**
  301. * @return UserGroupIndex
  302. */
  303. public function getRoles(): UserGroupIndex
  304. {
  305. return $this->getGroups();
  306. }
  307. /**
  308. * Convert object into an array.
  309. *
  310. * @return array
  311. */
  312. public function toArray()
  313. {
  314. $array = $this->jsonSerialize();
  315. $settings = $this->getMediaFieldSettings('avatar');
  316. $array['avatar'] = $this->parseFileProperty($array['avatar'] ?? null, $settings);
  317. return $array;
  318. }
  319. /**
  320. * Convert object into YAML string.
  321. *
  322. * @param int $inline The level where you switch to inline YAML.
  323. * @param int $indent The amount of spaces to use for indentation of nested nodes.
  324. * @return string A YAML string representing the object.
  325. */
  326. public function toYaml($inline = 5, $indent = 2)
  327. {
  328. $yaml = new YamlFormatter(['inline' => $inline, 'indent' => $indent]);
  329. return $yaml->encode($this->toArray());
  330. }
  331. /**
  332. * Convert object into JSON string.
  333. *
  334. * @return string
  335. */
  336. public function toJson()
  337. {
  338. $json = new JsonFormatter();
  339. return $json->encode($this->toArray());
  340. }
  341. /**
  342. * Join nested values together by using blueprints.
  343. *
  344. * @param string $name Dot separated path to the requested value.
  345. * @param mixed $value Value to be joined.
  346. * @param string|null $separator Separator, defaults to '.'
  347. * @return $this
  348. * @throws RuntimeException
  349. */
  350. public function join($name, $value, $separator = null)
  351. {
  352. $separator = $separator ?? '.';
  353. $old = $this->get($name, null, $separator);
  354. if ($old !== null) {
  355. if (!is_array($old)) {
  356. throw new RuntimeException('Value ' . $old);
  357. }
  358. if (is_object($value)) {
  359. $value = (array) $value;
  360. } elseif (!is_array($value)) {
  361. throw new RuntimeException('Value ' . $value);
  362. }
  363. $value = $this->getBlueprint()->mergeData($old, $value, $name, $separator);
  364. }
  365. $this->set($name, $value, $separator);
  366. return $this;
  367. }
  368. /**
  369. * Get nested structure containing default values defined in the blueprints.
  370. *
  371. * Fields without default value are ignored in the list.
  372. * @return array
  373. */
  374. public function getDefaults()
  375. {
  376. return $this->getBlueprint()->getDefaults();
  377. }
  378. /**
  379. * Set default values by using blueprints.
  380. *
  381. * @param string $name Dot separated path to the requested value.
  382. * @param mixed $value Value to be joined.
  383. * @param string|null $separator Separator, defaults to '.'
  384. * @return $this
  385. */
  386. public function joinDefaults($name, $value, $separator = null)
  387. {
  388. if (is_object($value)) {
  389. $value = (array) $value;
  390. }
  391. $old = $this->get($name, null, $separator);
  392. if ($old !== null) {
  393. $value = $this->getBlueprint()->mergeData($value, $old, $name, $separator ?? '.');
  394. }
  395. $this->setNestedProperty($name, $value, $separator);
  396. return $this;
  397. }
  398. /**
  399. * Get value from the configuration and join it with given data.
  400. *
  401. * @param string $name Dot separated path to the requested value.
  402. * @param array|object $value Value to be joined.
  403. * @param string $separator Separator, defaults to '.'
  404. * @return array
  405. * @throws RuntimeException
  406. */
  407. public function getJoined($name, $value, $separator = null)
  408. {
  409. if (is_object($value)) {
  410. $value = (array) $value;
  411. } elseif (!is_array($value)) {
  412. throw new RuntimeException('Value ' . $value);
  413. }
  414. $old = $this->get($name, null, $separator);
  415. if ($old === null) {
  416. // No value set; no need to join data.
  417. return $value;
  418. }
  419. if (!is_array($old)) {
  420. throw new RuntimeException('Value ' . $old);
  421. }
  422. // Return joined data.
  423. return $this->getBlueprint()->mergeData($old, $value, $name, $separator ?? '.');
  424. }
  425. /**
  426. * Set default values to the configuration if variables were not set.
  427. *
  428. * @param array $data
  429. * @return $this
  430. */
  431. public function setDefaults(array $data)
  432. {
  433. $this->setElements($this->getBlueprint()->mergeData($data, $this->toArray()));
  434. return $this;
  435. }
  436. /**
  437. * Validate by blueprints.
  438. *
  439. * @return $this
  440. * @throws \Exception
  441. */
  442. public function validate()
  443. {
  444. $this->getBlueprint()->validate($this->toArray());
  445. return $this;
  446. }
  447. /**
  448. * Filter all items by using blueprints.
  449. * @return $this
  450. */
  451. public function filter()
  452. {
  453. $this->setElements($this->getBlueprint()->filter($this->toArray()));
  454. return $this;
  455. }
  456. /**
  457. * Get extra items which haven't been defined in blueprints.
  458. *
  459. * @return array
  460. */
  461. public function extra()
  462. {
  463. return $this->getBlueprint()->extra($this->toArray());
  464. }
  465. /**
  466. * Return unmodified data as raw string.
  467. *
  468. * NOTE: This function only returns data which has been saved to the storage.
  469. *
  470. * @return string
  471. */
  472. public function raw()
  473. {
  474. $file = $this->file();
  475. return $file ? $file->raw() : '';
  476. }
  477. /**
  478. * Set or get the data storage.
  479. *
  480. * @param FileInterface|null $storage Optionally enter a new storage.
  481. * @return FileInterface|null
  482. */
  483. public function file(FileInterface $storage = null)
  484. {
  485. if (null !== $storage) {
  486. $this->_storage = $storage;
  487. }
  488. return $this->_storage;
  489. }
  490. /**
  491. * @return bool
  492. */
  493. public function isValid(): bool
  494. {
  495. return $this->getProperty('state') !== null;
  496. }
  497. /**
  498. * Save user
  499. *
  500. * @return static
  501. */
  502. public function save()
  503. {
  504. // TODO: We may want to handle this in the storage layer in the future.
  505. $key = $this->getStorageKey();
  506. if (!$key || strpos($key, '@@')) {
  507. $storage = $this->getFlexDirectory()->getStorage();
  508. if ($storage instanceof FileStorage) {
  509. $this->setStorageKey($this->getKey());
  510. }
  511. }
  512. $password = $this->getProperty('password') ?? $this->getProperty('password1');
  513. if (null !== $password && '' !== $password) {
  514. $password2 = $this->getProperty('password2');
  515. if (!\is_string($password) || ($password2 && $password !== $password2)) {
  516. throw new \RuntimeException('Passwords did not match.');
  517. }
  518. $this->setProperty('hashed_password', Authentication::create($password));
  519. }
  520. $this->unsetProperty('password');
  521. $this->unsetProperty('password1');
  522. $this->unsetProperty('password2');
  523. // Backwards compatibility with older plugins.
  524. $fireEvents = $this->isAdminSite() && $this->getFlexDirectory()->getConfig('object.compat.events', true);
  525. $grav = $this->getContainer();
  526. if ($fireEvents) {
  527. $self = $this;
  528. $grav->fireEvent('onAdminSave', new Event(['type' => 'flex', 'directory' => $this->getFlexDirectory(), 'object' => &$self]));
  529. if ($self !== $this) {
  530. throw new RuntimeException('Switching Flex User object during onAdminSave event is not supported! Please update plugin.');
  531. }
  532. }
  533. $instance = parent::save();
  534. // Backwards compatibility with older plugins.
  535. if ($fireEvents) {
  536. $grav->fireEvent('onAdminAfterSave', new Event(['type' => 'flex', 'directory' => $this->getFlexDirectory(), 'object' => $this]));
  537. }
  538. return $instance;
  539. }
  540. /**
  541. * @return array
  542. */
  543. public function prepareStorage(): array
  544. {
  545. $elements = parent::prepareStorage();
  546. // Do not save authorization information.
  547. unset($elements['authenticated'], $elements['authorized']);
  548. return $elements;
  549. }
  550. /**
  551. * @return MediaCollectionInterface
  552. */
  553. public function getMedia()
  554. {
  555. /** @var Media $media */
  556. $media = $this->getFlexMedia();
  557. // Deal with shared avatar folder.
  558. $path = $this->getAvatarFile();
  559. if ($path && !$media[$path] && is_file($path)) {
  560. $medium = MediumFactory::fromFile($path);
  561. if ($medium) {
  562. $media->add($path, $medium);
  563. $name = Utils::basename($path);
  564. if ($name !== $path) {
  565. $media->add($name, $medium);
  566. }
  567. }
  568. }
  569. return $media;
  570. }
  571. /**
  572. * @return string|null
  573. */
  574. public function getMediaFolder(): ?string
  575. {
  576. $folder = $this->getFlexMediaFolder();
  577. // Check for shared media
  578. if (!$folder && !$this->getFlexDirectory()->getMediaFolder()) {
  579. $this->_loadMedia = false;
  580. $folder = $this->getBlueprint()->fields()['avatar']['destination'] ?? 'account://avatars';
  581. }
  582. return $folder;
  583. }
  584. /**
  585. * @param string $name
  586. * @return Blueprint
  587. */
  588. protected function doGetBlueprint(string $name = ''): Blueprint
  589. {
  590. $blueprint = $this->getFlexDirectory()->getBlueprint($name ? '.' . $name : $name);
  591. // HACK: With folder storage we need to ignore the avatar destination.
  592. if ($this->getFlexDirectory()->getMediaFolder()) {
  593. $field = $blueprint->get('form/fields/avatar');
  594. if ($field) {
  595. unset($field['destination']);
  596. $blueprint->set('form/fields/avatar', $field);
  597. }
  598. }
  599. return $blueprint;
  600. }
  601. /**
  602. * @param UserInterface $user
  603. * @param string $action
  604. * @param string $scope
  605. * @param bool $isMe
  606. * @return bool|null
  607. */
  608. protected function isAuthorizedOverride(UserInterface $user, string $action, string $scope, bool $isMe = false): ?bool
  609. {
  610. // Check custom application access.
  611. $isAuthorizedCallable = static::$isAuthorizedCallable;
  612. if ($isAuthorizedCallable instanceof Closure) {
  613. $callable = $isAuthorizedCallable->bindTo($this, $this);
  614. $authorized = $callable($user, $action, $scope, $isMe);
  615. if (is_bool($authorized)) {
  616. return $authorized;
  617. }
  618. }
  619. if ($user instanceof self && $user->getStorageKey() === $this->getStorageKey()) {
  620. // User cannot delete his own account, otherwise he has full access.
  621. return $action !== 'delete';
  622. }
  623. return parent::isAuthorizedOverride($user, $action, $scope, $isMe);
  624. }
  625. /**
  626. * @return string|null
  627. */
  628. protected function getAvatarFile(): ?string
  629. {
  630. $avatars = $this->getElement('avatar');
  631. if (is_array($avatars) && $avatars) {
  632. $avatar = array_shift($avatars);
  633. return $avatar['path'] ?? null;
  634. }
  635. return null;
  636. }
  637. /**
  638. * Gets the associated media collection (original images).
  639. *
  640. * @return MediaCollectionInterface Representation of associated media.
  641. */
  642. protected function getOriginalMedia()
  643. {
  644. $folder = $this->getMediaFolder();
  645. if ($folder) {
  646. $folder .= '/original';
  647. }
  648. return (new Media($folder ?? '', $this->getMediaOrder()))->setTimestamps();
  649. }
  650. /**
  651. * @param array $files
  652. * @return void
  653. */
  654. protected function setUpdatedMedia(array $files): void
  655. {
  656. /** @var UniformResourceLocator $locator */
  657. $locator = Grav::instance()['locator'];
  658. $media = $this->getMedia();
  659. if (!$media instanceof MediaUploadInterface) {
  660. return;
  661. }
  662. $filesystem = Filesystem::getInstance(false);
  663. $list = [];
  664. $list_original = [];
  665. foreach ($files as $field => $group) {
  666. // Ignore files without a field.
  667. if ($field === '') {
  668. continue;
  669. }
  670. $field = (string)$field;
  671. // Load settings for the field.
  672. $settings = $this->getMediaFieldSettings($field);
  673. foreach ($group as $filename => $file) {
  674. if ($file) {
  675. // File upload.
  676. $filename = $file->getClientFilename();
  677. /** @var FormFlashFile $file */
  678. $data = $file->jsonSerialize();
  679. unset($data['tmp_name'], $data['path']);
  680. } else {
  681. // File delete.
  682. $data = null;
  683. }
  684. if ($file) {
  685. // Check file upload against media limits (except for max size).
  686. $filename = $media->checkUploadedFile($file, $filename, ['filesize' => 0] + $settings);
  687. }
  688. $self = $settings['self'];
  689. if ($this->_loadMedia && $self) {
  690. $filepath = $filename;
  691. } else {
  692. $filepath = "{$settings['destination']}/{$filename}";
  693. // For backwards compatibility we are always using relative path from the installation root.
  694. if ($locator->isStream($filepath)) {
  695. $filepath = $locator->findResource($filepath, false, true);
  696. }
  697. }
  698. // Special handling for original images.
  699. if (strpos($field, '/original')) {
  700. if ($this->_loadMedia && $self) {
  701. $list_original[$filename] = [$file, $settings];
  702. }
  703. continue;
  704. }
  705. // Calculate path without the retina scaling factor.
  706. $realpath = $filesystem->pathname($filepath) . str_replace(['@3x', '@2x'], '', Utils::basename($filepath));
  707. $list[$filename] = [$file, $settings];
  708. $path = str_replace('.', "\n", $field);
  709. if (null !== $data) {
  710. $data['name'] = $filename;
  711. $data['path'] = $filepath;
  712. $this->setNestedProperty("{$path}\n{$realpath}", $data, "\n");
  713. } else {
  714. $this->unsetNestedProperty("{$path}\n{$realpath}", "\n");
  715. }
  716. }
  717. }
  718. $this->clearMediaCache();
  719. $this->_uploads = $list;
  720. $this->_uploads_original = $list_original;
  721. }
  722. protected function saveUpdatedMedia(): void
  723. {
  724. $media = $this->getMedia();
  725. if (!$media instanceof MediaUploadInterface) {
  726. throw new RuntimeException('Internal error UO101');
  727. }
  728. // Upload/delete original sized images.
  729. /**
  730. * @var string $filename
  731. * @var UploadedFileInterface|array|null $file
  732. */
  733. foreach ($this->_uploads_original ?? [] as $filename => $file) {
  734. $filename = 'original/' . $filename;
  735. if (is_array($file)) {
  736. [$file, $settings] = $file;
  737. } else {
  738. $settings = null;
  739. }
  740. if ($file instanceof UploadedFileInterface) {
  741. $media->copyUploadedFile($file, $filename, $settings);
  742. } else {
  743. $media->deleteFile($filename, $settings);
  744. }
  745. }
  746. // Upload/delete altered files.
  747. /**
  748. * @var string $filename
  749. * @var UploadedFileInterface|array|null $file
  750. */
  751. foreach ($this->getUpdatedMedia() as $filename => $file) {
  752. if (is_array($file)) {
  753. [$file, $settings] = $file;
  754. } else {
  755. $settings = null;
  756. }
  757. if ($file instanceof UploadedFileInterface) {
  758. $media->copyUploadedFile($file, $filename, $settings);
  759. } else {
  760. $media->deleteFile($filename, $settings);
  761. }
  762. }
  763. $this->setUpdatedMedia([]);
  764. $this->clearMediaCache();
  765. }
  766. /**
  767. * @return array
  768. */
  769. protected function doSerialize(): array
  770. {
  771. return [
  772. 'type' => $this->getFlexType(),
  773. 'key' => $this->getKey(),
  774. 'elements' => $this->jsonSerialize(),
  775. 'storage' => $this->getMetaData()
  776. ];
  777. }
  778. /**
  779. * @return UserGroupIndex
  780. */
  781. protected function getUserGroups()
  782. {
  783. $grav = Grav::instance();
  784. /** @var Flex $flex */
  785. $flex = $grav['flex'];
  786. /** @var UserGroupCollection|null $groups */
  787. $groups = $flex->getDirectory('user-groups');
  788. if ($groups) {
  789. /** @var UserGroupIndex $index */
  790. $index = $groups->getIndex();
  791. return $index;
  792. }
  793. return $grav['user_groups'];
  794. }
  795. /**
  796. * @return UserGroupIndex
  797. */
  798. protected function getGroups()
  799. {
  800. if (null === $this->_groups) {
  801. /** @var UserGroupIndex $groups */
  802. $groups = $this->getUserGroups()->select((array)$this->getProperty('groups'));
  803. $this->_groups = $groups;
  804. }
  805. return $this->_groups;
  806. }
  807. /**
  808. * @return Access
  809. */
  810. protected function getAccess(): Access
  811. {
  812. if (null === $this->_access) {
  813. $this->_access = new Access($this->getProperty('access'));
  814. }
  815. return $this->_access;
  816. }
  817. /**
  818. * @param mixed $value
  819. * @return array
  820. */
  821. protected function offsetLoad_access($value): array
  822. {
  823. if (!$value instanceof Access) {
  824. $value = new Access($value);
  825. }
  826. return $value->jsonSerialize();
  827. }
  828. /**
  829. * @param mixed $value
  830. * @return array
  831. */
  832. protected function offsetPrepare_access($value): array
  833. {
  834. return $this->offsetLoad_access($value);
  835. }
  836. /**
  837. * @param array|null $value
  838. * @return array|null
  839. */
  840. protected function offsetSerialize_access(?array $value): ?array
  841. {
  842. return $value;
  843. }
  844. }