"Fossies" - the Fresh Open Source Software Archive

Member "grav/system/src/Grav/Common/User/FlexUser/User.php" (1 Sep 2020, 20187 Bytes) of package /linux/www/grav-v1.6.27.zip:


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

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