UserObject.php 30 KB

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