User.php 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710
  1. <?php
  2. /**
  3. * @package Grav\Common\User
  4. *
  5. * @copyright Copyright (C) 2015 - 2019 Trilby Media, LLC. All rights reserved.
  6. * @license MIT License; see LICENSE file for details.
  7. */
  8. namespace Grav\Common\User\FlexUser;
  9. use Grav\Common\Data\Blueprint;
  10. use Grav\Common\Grav;
  11. use Grav\Common\Media\Interfaces\MediaCollectionInterface;
  12. use Grav\Common\Page\Media;
  13. use Grav\Common\Page\Medium\ImageMedium;
  14. use Grav\Common\Page\Medium\Medium;
  15. use Grav\Common\User\Authentication;
  16. use Grav\Common\User\Interfaces\UserInterface;
  17. use Grav\Common\User\Traits\UserTrait;
  18. use Grav\Framework\File\Formatter\JsonFormatter;
  19. use Grav\Framework\File\Formatter\YamlFormatter;
  20. use Grav\Framework\Flex\FlexDirectory;
  21. use Grav\Framework\Flex\FlexObject;
  22. use Grav\Framework\Flex\Storage\FileStorage;
  23. use Grav\Framework\Flex\Traits\FlexAuthorizeTrait;
  24. use Grav\Framework\Flex\Traits\FlexMediaTrait;
  25. use Grav\Framework\Form\FormFlashFile;
  26. use Grav\Framework\Media\Interfaces\MediaManipulationInterface;
  27. use Psr\Http\Message\UploadedFileInterface;
  28. use RocketTheme\Toolbox\File\FileInterface;
  29. /**
  30. * Flex User
  31. *
  32. * Flex User is mostly compatible with the older User class, except on few key areas:
  33. *
  34. * - Constructor parameters have been changed. Old code creating a new user does not work.
  35. * - Serializer has been changed -- existing sessions will be killed.
  36. *
  37. * @package Grav\Common\User
  38. *
  39. * @property string $username
  40. * @property string $email
  41. * @property string $fullname
  42. * @property string $state
  43. * @property array $groups
  44. * @property array $access
  45. * @property bool $authenticated
  46. * @property bool $authorized
  47. */
  48. class User extends FlexObject implements UserInterface, MediaManipulationInterface, \Countable
  49. {
  50. use FlexMediaTrait;
  51. use FlexAuthorizeTrait;
  52. use UserTrait;
  53. protected $_uploads_original;
  54. /**
  55. * @var FileInterface|null
  56. */
  57. protected $_storage;
  58. /**
  59. * @return array
  60. */
  61. public static function getCachedMethods(): array
  62. {
  63. return [
  64. 'load' => false,
  65. 'find' => false,
  66. 'remove' => false,
  67. 'get' => true,
  68. 'set' => false,
  69. 'undef' => false,
  70. 'def' => false,
  71. ] + parent::getCachedMethods();
  72. }
  73. public function __construct(array $elements, $key, FlexDirectory $directory, bool $validate = false)
  74. {
  75. // User can only be authenticated via login.
  76. unset($elements['authenticated'], $elements['authorized']);
  77. parent::__construct($elements, $key, $directory, $validate);
  78. // Define username and state if they aren't set.
  79. $this->defProperty('username', $key);
  80. $this->defProperty('state', 'enabled');
  81. }
  82. /**
  83. * Get value by using dot notation for nested arrays/objects.
  84. *
  85. * @example $value = $this->get('this.is.my.nested.variable');
  86. *
  87. * @param string $name Dot separated path to the requested value.
  88. * @param mixed $default Default value (or null).
  89. * @param string $separator Separator, defaults to '.'
  90. * @return mixed Value.
  91. */
  92. public function get($name, $default = null, $separator = null)
  93. {
  94. return $this->getNestedProperty($name, $default, $separator);
  95. }
  96. /**
  97. * Set value by using dot notation for nested arrays/objects.
  98. *
  99. * @example $data->set('this.is.my.nested.variable', $value);
  100. *
  101. * @param string $name Dot separated path to the requested value.
  102. * @param mixed $value New value.
  103. * @param string $separator Separator, defaults to '.'
  104. * @return $this
  105. */
  106. public function set($name, $value, $separator = null)
  107. {
  108. $this->setNestedProperty($name, $value, $separator);
  109. return $this;
  110. }
  111. /**
  112. * Unset value by using dot notation for nested arrays/objects.
  113. *
  114. * @example $data->undef('this.is.my.nested.variable');
  115. *
  116. * @param string $name Dot separated path to the requested value.
  117. * @param string $separator Separator, defaults to '.'
  118. * @return $this
  119. */
  120. public function undef($name, $separator = null)
  121. {
  122. $this->unsetNestedProperty($name, $separator);
  123. return $this;
  124. }
  125. /**
  126. * Set default value by using dot notation for nested arrays/objects.
  127. *
  128. * @example $data->def('this.is.my.nested.variable', 'default');
  129. *
  130. * @param string $name Dot separated path to the requested value.
  131. * @param mixed $default Default value (or null).
  132. * @param string $separator Separator, defaults to '.'
  133. * @return $this
  134. */
  135. public function def($name, $default = null, $separator = null)
  136. {
  137. $this->defNestedProperty($name, $default, $separator);
  138. return $this;
  139. }
  140. /**
  141. * Get value from a page variable (used mostly for creating edit forms).
  142. *
  143. * @param string $name Variable name.
  144. * @param mixed $default
  145. * @param string|null $separator
  146. * @return mixed
  147. */
  148. public function getFormValue(string $name, $default = null, string $separator = null)
  149. {
  150. $value = parent::getFormValue($name, null, $separator);
  151. if ($name === 'avatar') {
  152. return $this->parseFileProperty($value);
  153. }
  154. if (null === $value) {
  155. if ($name === 'media_order') {
  156. return implode(',', $this->getMediaOrder());
  157. }
  158. }
  159. return $value ?? $default;
  160. }
  161. /**
  162. * @param string $property
  163. * @param mixed $default
  164. * @return mixed
  165. */
  166. public function getProperty($property, $default = null)
  167. {
  168. $value = parent::getProperty($property, $default);
  169. if ($property === 'avatar') {
  170. $value = $this->parseFileProperty($value);
  171. }
  172. return $value;
  173. }
  174. /**
  175. * Convert object into an array.
  176. *
  177. * @return array
  178. */
  179. public function toArray()
  180. {
  181. $array = $this->jsonSerialize();
  182. $array['avatar'] = $this->parseFileProperty($array['avatar'] ?? null);
  183. return $array;
  184. }
  185. /**
  186. * Convert object into YAML string.
  187. *
  188. * @param int $inline The level where you switch to inline YAML.
  189. * @param int $indent The amount of spaces to use for indentation of nested nodes.
  190. *
  191. * @return string A YAML string representing the object.
  192. */
  193. public function toYaml($inline = 5, $indent = 2)
  194. {
  195. $yaml = new YamlFormatter(['inline' => $inline, 'indent' => $indent]);
  196. return $yaml->encode($this->toArray());
  197. }
  198. /**
  199. * Convert object into JSON string.
  200. *
  201. * @return string
  202. */
  203. public function toJson()
  204. {
  205. $json = new JsonFormatter();
  206. return $json->encode($this->toArray());
  207. }
  208. /**
  209. * Join nested values together by using blueprints.
  210. *
  211. * @param string $name Dot separated path to the requested value.
  212. * @param mixed $value Value to be joined.
  213. * @param string $separator Separator, defaults to '.'
  214. * @return $this
  215. * @throws \RuntimeException
  216. */
  217. public function join($name, $value, $separator = null)
  218. {
  219. $separator = $separator ?? '.';
  220. $old = $this->get($name, null, $separator);
  221. if ($old !== null) {
  222. if (!\is_array($old)) {
  223. throw new \RuntimeException('Value ' . $old);
  224. }
  225. if (\is_object($value)) {
  226. $value = (array) $value;
  227. } elseif (!\is_array($value)) {
  228. throw new \RuntimeException('Value ' . $value);
  229. }
  230. $value = $this->getBlueprint()->mergeData($old, $value, $name, $separator);
  231. }
  232. $this->set($name, $value, $separator);
  233. return $this;
  234. }
  235. /**
  236. * Get nested structure containing default values defined in the blueprints.
  237. *
  238. * Fields without default value are ignored in the list.
  239. * @return array
  240. */
  241. public function getDefaults()
  242. {
  243. return $this->getBlueprint()->getDefaults();
  244. }
  245. /**
  246. * Set default values by using blueprints.
  247. *
  248. * @param string $name Dot separated path to the requested value.
  249. * @param mixed $value Value to be joined.
  250. * @param string $separator Separator, defaults to '.'
  251. * @return $this
  252. */
  253. public function joinDefaults($name, $value, $separator = null)
  254. {
  255. if (\is_object($value)) {
  256. $value = (array) $value;
  257. }
  258. $old = $this->get($name, null, $separator);
  259. if ($old !== null) {
  260. $value = $this->getBlueprint()->mergeData($value, $old, $name, $separator);
  261. }
  262. $this->setNestedProperty($name, $value, $separator);
  263. return $this;
  264. }
  265. /**
  266. * Get value from the configuration and join it with given data.
  267. *
  268. * @param string $name Dot separated path to the requested value.
  269. * @param array|object $value Value to be joined.
  270. * @param string $separator Separator, defaults to '.'
  271. * @return array
  272. * @throws \RuntimeException
  273. */
  274. public function getJoined($name, $value, $separator = null)
  275. {
  276. if (\is_object($value)) {
  277. $value = (array) $value;
  278. } elseif (!\is_array($value)) {
  279. throw new \RuntimeException('Value ' . $value);
  280. }
  281. $old = $this->get($name, null, $separator);
  282. if ($old === null) {
  283. // No value set; no need to join data.
  284. return $value;
  285. }
  286. if (!\is_array($old)) {
  287. throw new \RuntimeException('Value ' . $old);
  288. }
  289. // Return joined data.
  290. return $this->getBlueprint()->mergeData($old, $value, $name, $separator);
  291. }
  292. /**
  293. * Set default values to the configuration if variables were not set.
  294. *
  295. * @param array $data
  296. * @return $this
  297. */
  298. public function setDefaults(array $data)
  299. {
  300. $this->setElements($this->getBlueprint()->mergeData($data, $this->toArray()));
  301. return $this;
  302. }
  303. /**
  304. * Validate by blueprints.
  305. *
  306. * @return $this
  307. * @throws \Exception
  308. */
  309. public function validate()
  310. {
  311. $this->getBlueprint()->validate($this->toArray());
  312. return $this;
  313. }
  314. /**
  315. * Filter all items by using blueprints.
  316. * @return $this
  317. */
  318. public function filter()
  319. {
  320. $this->setElements($this->getBlueprint()->filter($this->toArray()));
  321. return $this;
  322. }
  323. /**
  324. * Get extra items which haven't been defined in blueprints.
  325. *
  326. * @return array
  327. */
  328. public function extra()
  329. {
  330. return $this->getBlueprint()->extra($this->toArray());
  331. }
  332. /**
  333. * @param string $name
  334. * @return Blueprint
  335. */
  336. public function getBlueprint(string $name = '')
  337. {
  338. $blueprint = clone parent::getBlueprint($name);
  339. $blueprint->addDynamicHandler('flex', function (array &$field, $property, array &$call) {
  340. $params = (array)$call['params'];
  341. $method = array_shift($params);
  342. if (method_exists($this, $method)) {
  343. $value = $this->{$method}(...$params);
  344. if (\is_array($value) && isset($field[$property]) && \is_array($field[$property])) {
  345. $field[$property] = array_merge_recursive($field[$property], $value);
  346. } else {
  347. $field[$property] = $value;
  348. }
  349. }
  350. });
  351. return $blueprint->init();
  352. }
  353. /**
  354. * Return unmodified data as raw string.
  355. *
  356. * NOTE: This function only returns data which has been saved to the storage.
  357. *
  358. * @return string
  359. */
  360. public function raw()
  361. {
  362. $file = $this->file();
  363. return $file ? $file->raw() : '';
  364. }
  365. /**
  366. * Set or get the data storage.
  367. *
  368. * @param FileInterface $storage Optionally enter a new storage.
  369. * @return FileInterface
  370. */
  371. public function file(FileInterface $storage = null)
  372. {
  373. if (null !== $storage) {
  374. $this->_storage = $storage;
  375. }
  376. return $this->_storage;
  377. }
  378. public function isValid(): bool
  379. {
  380. return $this->getProperty('state') !== null;
  381. }
  382. /**
  383. * Save user without the username
  384. */
  385. public function save()
  386. {
  387. // TODO: We may want to handle this in the storage layer in the future.
  388. $key = $this->getStorageKey();
  389. if (!$key || strpos($key, '@@')) {
  390. $storage = $this->getFlexDirectory()->getStorage();
  391. if ($storage instanceof FileStorage) {
  392. $this->setStorageKey($this->getKey());
  393. }
  394. }
  395. $password = $this->getProperty('password');
  396. if (null !== $password) {
  397. $this->unsetProperty('password');
  398. $this->unsetProperty('password1');
  399. $this->unsetProperty('password2');
  400. $this->setProperty('hashed_password', Authentication::create($password));
  401. }
  402. return parent::save();
  403. }
  404. public function isAuthorized(string $action, string $scope = null, UserInterface $user = null): bool
  405. {
  406. if (null === $user) {
  407. /** @var UserInterface $user */
  408. $user = Grav::instance()['user'] ?? null;
  409. }
  410. if ($user instanceof User && $user->getStorageKey() === $this->getStorageKey()) {
  411. return true;
  412. }
  413. return parent::isAuthorized($action, $scope, $user);
  414. }
  415. /**
  416. * @return array
  417. */
  418. public function prepareStorage(): array
  419. {
  420. $elements = parent::prepareStorage();
  421. // Do not save authorization information.
  422. unset($elements['authenticated'], $elements['authorized']);
  423. return $elements;
  424. }
  425. /**
  426. * Merge two configurations together.
  427. *
  428. * @param array $data
  429. * @return $this
  430. * @deprecated 1.6 Use `->update($data)` instead (same but with data validation & filtering, file upload support).
  431. */
  432. public function merge(array $data)
  433. {
  434. user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->update($data) method instead', E_USER_DEPRECATED);
  435. $this->setElements($this->getBlueprint()->mergeData($this->toArray(), $data));
  436. return $this;
  437. }
  438. /**
  439. * Return media object for the User's avatar.
  440. *
  441. * @return ImageMedium|null
  442. * @deprecated 1.6 Use ->getAvatarImage() method instead.
  443. */
  444. public function getAvatarMedia()
  445. {
  446. user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getAvatarImage() method instead', E_USER_DEPRECATED);
  447. return $this->getAvatarImage();
  448. }
  449. /**
  450. * Return the User's avatar URL
  451. *
  452. * @return string
  453. * @deprecated 1.6 Use ->getAvatarUrl() method instead.
  454. */
  455. public function avatarUrl()
  456. {
  457. user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6, use ->getAvatarUrl() method instead', E_USER_DEPRECATED);
  458. return $this->getAvatarUrl();
  459. }
  460. /**
  461. * Checks user authorization to the action.
  462. * Ensures backwards compatibility
  463. *
  464. * @param string $action
  465. * @return bool
  466. * @deprecated 1.5 Use ->authorize() method instead.
  467. */
  468. public function authorise($action)
  469. {
  470. user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.5, use ->authorize() method instead', E_USER_DEPRECATED);
  471. return $this->authorize($action);
  472. }
  473. /**
  474. * Implements Countable interface.
  475. *
  476. * @return int
  477. * @deprecated 1.6 Method makes no sense for user account.
  478. */
  479. public function count()
  480. {
  481. user_error(__CLASS__ . '::' . __FUNCTION__ . '() is deprecated since Grav 1.6', E_USER_DEPRECATED);
  482. return \count($this->jsonSerialize());
  483. }
  484. /**
  485. * Gets the associated media collection (original images).
  486. *
  487. * @return MediaCollectionInterface Representation of associated media.
  488. */
  489. protected function getOriginalMedia()
  490. {
  491. return (new Media($this->getMediaFolder() . '/original', $this->getMediaOrder()))->setTimestamps();
  492. }
  493. /**
  494. * @param array $files
  495. */
  496. protected function setUpdatedMedia(array $files): void
  497. {
  498. $list = [];
  499. $list_original = [];
  500. foreach ($files as $field => $group) {
  501. foreach ($group as $filename => $file) {
  502. if (strpos($field, '/original')) {
  503. // Special handling for original images.
  504. $list_original[$filename] = $file;
  505. continue;
  506. }
  507. $list[$filename] = $file;
  508. if ($file) {
  509. /** @var FormFlashFile $file */
  510. $data = $file->jsonSerialize();
  511. $path = $file->getClientFilename();
  512. unset($data['tmp_name'], $data['path']);
  513. $this->setNestedProperty("{$field}\n{$path}", $data, "\n");
  514. } else {
  515. $this->unsetNestedProperty("{$field}\n{$filename}", "\n");
  516. }
  517. }
  518. }
  519. $this->_uploads = $list;
  520. $this->_uploads_original = $list_original;
  521. }
  522. protected function saveUpdatedMedia(): void
  523. {
  524. // Upload/delete original sized images.
  525. /** @var FormFlashFile $file */
  526. foreach ($this->_uploads_original ?? [] as $name => $file) {
  527. $name = 'original/' . $name;
  528. if ($file) {
  529. $this->uploadMediaFile($file, $name);
  530. } else {
  531. $this->deleteMediaFile($name);
  532. }
  533. }
  534. /**
  535. * @var string $filename
  536. * @var UploadedFileInterface $file
  537. */
  538. foreach ($this->getUpdatedMedia() as $filename => $file) {
  539. if ($file) {
  540. $this->uploadMediaFile($file, $filename);
  541. } else {
  542. $this->deleteMediaFile($filename);
  543. }
  544. }
  545. $this->setUpdatedMedia([]);
  546. }
  547. /**
  548. * @param array $value
  549. * @return array
  550. */
  551. protected function parseFileProperty($value)
  552. {
  553. if (!\is_array($value)) {
  554. return $value;
  555. }
  556. $originalMedia = $this->getOriginalMedia();
  557. $resizedMedia = $this->getMedia();
  558. $list = [];
  559. foreach ($value as $filename => $info) {
  560. if (!\is_array($info)) {
  561. continue;
  562. }
  563. /** @var Medium $thumbFile */
  564. $thumbFile = $resizedMedia[$filename];
  565. /** @var Medium $imageFile */
  566. $imageFile = $originalMedia[$filename] ?? $thumbFile;
  567. if ($thumbFile) {
  568. $list[$filename] = [
  569. 'name' => $filename,
  570. 'type' => $info['type'],
  571. 'size' => $info['size'],
  572. 'image_url' => $imageFile->url(),
  573. 'thumb_url' => $thumbFile->url(),
  574. 'cropData' => (object)($imageFile->metadata()['upload']['crop'] ?? [])
  575. ];
  576. }
  577. }
  578. return $list;
  579. }
  580. /**
  581. * @return array
  582. */
  583. protected function doSerialize(): array
  584. {
  585. return [
  586. 'type' => 'accounts',
  587. 'key' => $this->getKey(),
  588. 'elements' => $this->jsonSerialize(),
  589. 'storage' => $this->getStorage()
  590. ];
  591. }
  592. /**
  593. * @param array $serialized
  594. */
  595. protected function doUnserialize(array $serialized): void
  596. {
  597. $grav = Grav::instance();
  598. /** @var UserCollection $accounts */
  599. $accounts = $grav['accounts'];
  600. $directory = $accounts->getFlexDirectory();
  601. if (!$directory) {
  602. throw new \InvalidArgumentException('Internal error');
  603. }
  604. $this->setFlexDirectory($directory);
  605. $this->setStorage($serialized['storage']);
  606. $this->setKey($serialized['key']);
  607. $this->setElements($serialized['elements']);
  608. }
  609. }