app/resto/core/RestoUser.php
<?php
/*
* Copyright 2018 Jérôme Gasperi
*
* Licensed under the Apache License, version 2.0 (the "License");
* You may not use this file except in compliance with the License.
* You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
*/
/**
* @OA\Tag(
* name="User",
* description="Everything about user - profile, access rights, history, etc."
* )
*/
class RestoUser
{
const CREATE_COLLECTION = 'createCollection';
const DELETE_COLLECTION = 'deleteCollection';
const UPDATE_COLLECTION = 'updateCollection';
const DELETE_ANY_COLLECTION = 'deleteAnyCollection';
const UPDATE_ANY_COLLECTION = 'updateAnyCollection';
const CREATE_CATALOG = 'createCatalog';
const DELETE_CATALOG = 'deleteCatalog';
const UPDATE_CATALOG = 'updateCatalog';
const DELETE_ANY_CATALOG = 'deleteAnyCatalog';
const UPDATE_ANY_CATALOG = 'updateAnyCatalog';
const CREATE_FEATURE = 'createFeature';
const DELETE_FEATURE = 'deleteFeature';
const UPDATE_FEATURE = 'updateFeature';
const CREATE_ANY_FEATURE = 'createAnyFeature';
const DELETE_ANY_FEATURE = 'deleteAnyFeature';
const UPDATE_ANY_FEATURE = 'updateAnyFeature';
const DOWNLOAD_FEATURE = 'downloadFeature';
/**
* User profile
*
* @OA\Schema(
* schema="UserDisplayProfile",
* required={"id", "picture", "groups", "name", "followers", "followings"},
* @OA\Property(
* property="id",
* type="string",
* description="Unique user identifier. Identifier is related to user's registration date i.e. the greatest the identifier value, the most recently registered the user is"
* ),
* @OA\Property(
* property="picture",
* type="string",
* description="An http(s) url to the user's avatar picture"
* ),
* @OA\Property(
* property="groups",
* type="array",
* @OA\Items(
* type="string"
* ),
* description="Array of group identifiers"
* ),
* @OA\Property(
* property="name",
* type="string",
* description="User display name"
* ),
* @OA\Property(
* property="followers",
* type="integer",
* description="Number of user's followers"
* ),
* @OA\Property(
* property="followings",
* type="integer",
* description="Number of user's followings"
* ),
* @OA\Property(
* property="firstname",
* type="string",
* description="User firstname"
* ),
* @OA\Property(
* property="lastname",
* type="string",
* description="User lastname"
* ),
* @OA\Property(
* property="bio",
* type="string",
* description="User biography"
* ),
* @OA\Property(
* property="registrationdate",
* type="string",
* description="User registration date"
* ),
* @OA\Property(
* property="topics",
* type="string",
* description="Comma separated list of user's topics of interest"
* ),
* @OA\Property(
* property="followed",
* type="boolean",
* description="True if user is followed by requesting user"
* ),
* @OA\Property(
* property="followme",
* type="string",
* description="True if user follows requesting user"
* ),
* example={
* "id": "1356771884787565573",
* "picture": "https://robohash.org/d0e907f8b6f4ee74cd4c38a515e2a4de?gravatar=hashed&bgset=any&size=400x400",
* "groups": {
* "1"
* },
* "name": "jrom",
* "followers": 185,
* "followings": 144,
* "firstname": "Jérôme",
* "lastname": "Gasperi",
* "bio": "Working on new features for the next major release of SnapPlanet",
* "registrationdate": "2016-10-08T22:50:34.187217Z",
* "topics":"earth,fires,geology,glaciology,volcanism",
* "followed": false,
* "followme": false
* }
* )
*/
public $profile;
/*
* Current JWT token
*/
public $token = null;
/*
* Context
*/
private $context;
/*
* Reference to rights object
*/
private $rights;
/*
* Reference to groups object
*/
private $groups;
/*
* Unregistered profile
*/
private $unregistered = array(
'id' => null,
'email' => 'unregistered',
'activated' => 0
);
/*
* Set to true if profile is complete i.e. loaded from database
*/
private $isComplete = false;
/**
* Constructor
*
* @param array $profile : Profile parameters
* @param RestoContext $context
* @param boolean autoload
*/
public function __construct($profile, $context, $autoload = false)
{
$this->context = $context;
/*
* Impossible
*/
if (!isset($profile) || (!isset($profile['id']) && (!isset($profile['email']) || $profile['email'] == 'unregistered'))) {
$this->profile = $this->unregistered;
}
/*
* Load profile from database is autoload is set to true
* or if no id is provided
*/
elseif ($autoload || !isset($profile['id'])) {
if (isset($profile['id'])) {
$this->profile = (new UsersFunctions($this->context->dbDriver))->getUserProfile('id', $profile['id']);
} else {
$this->profile = (new UsersFunctions($this->context->dbDriver))->getUserProfile('email', $profile['email'], array(
'password' => $profile['password'] ?? null
));
}
if (!$this->profile) {
$this->profile = $this->unregistered;
}
$this->isComplete = true;
} else {
$this->profile = $profile;
}
}
/**
* Return true if user is validated by admin - false otherwise
*
* @return boolean
*/
public function isValidated()
{
$this->loadProfile();
if (isset($this->profile['id']) && isset($this->profile['validatedby'])) {
return true;
}
return false;
}
/**
* User rights are :
*
* - createCollection : create a collection
* - deleteCollection : delete a collection owned by user
* - updateCollection : update a collection owned by user
*
* - deleteAnyCollection : delete any collection i.e. including not owned by user
* - updateAnyCollection : update any collection i.e. including not owned by user
*
* - createCatalog : create a catalog
* - deleteCatalog : delete a catalog owned by user
* - updateCatalog : update a catalog owned by user
*
* - deleteAnyCatalog : delete any catalog i.e. including not owned by user
* - updateAnyCatalog : update any catalog i.e. including not owned by user
*
* - createFeature : create a feature in a collection owned by user
* - deleteFeature : delete a feature owned by user
* - updateFeature : update a feature owned by user
*
* - createAnyFeature : create a feature in any collection
* - deleteAnyFeature : delete any feature i.e. including not owned by user
* - updateAnyFeature : update any feature i.e. including not owned by user
*
* - downloadFeature : download a feature [NOT USED]
*
* @param string $action
* @param array $params
* @return boolean
*/
public function hasRightsTo($action, $params = array())
{
$rights = $this->getRights();
/*
* 1) Handle actions that are not known
* and actions that do not need params to be set
*/
$withParams = array(
RestoUser::DELETE_COLLECTION,
RestoUser::UPDATE_COLLECTION,
RestoUser::DELETE_CATALOG,
RestoUser::UPDATE_CATALOG,
RestoUser::CREATE_FEATURE,
RestoUser::DELETE_FEATURE,
RestoUser::UPDATE_FEATURE,
);
if ( !in_array($action, $withParams) ) {
return $rights[$action] ?? false;
}
/*
* Split camel case action into parts
* The first token is the action (create, update, delete)
* The last token is the target (collection, feature)
*/
$splittedAction = preg_split('/(?<=[a-z])(?=[A-Z])/x', $action);
if ( count($splittedAction) === 2 ) {
// 2) Handle "Any" cases
$any = $splittedAction[0] . 'Any' . $splittedAction[1];
if ( isset($rights[$any]) && $rights[$any] ) {
return true;
}
}
// 3) Handle action with params
switch ($action) {
// Only owner of collection can do this
case RestoUser::CREATE_FEATURE:
case RestoUser::DELETE_COLLECTION:
case RestoUser::UPDATE_COLLECTION:
return $rights[$action] && isset($params['collection']) && $params['collection']->owner === $this->profile['id'];
// Only owner of catalog can do this
case RestoUser::DELETE_CATALOG:
case RestoUser::UPDATE_CATALOG:
return $rights[$action] && isset($params['catalog']) && $params['catalog']['owner'] === $this->profile['id'];
// Only owner of feature can do this
case RestoUser::DELETE_FEATURE:
case RestoUser::UPDATE_FEATURE:
if ( !isset($params['feature']) ) {
return false;
}
$featureArray = $params['feature']->toArray();
return $rights[$action] && isset($featureArray['properties']['owner']) && $featureArray['properties']['owner'] === $this->profile['id'];
default:
return $rights[$action] ?? false;
}
}
/**
* Activate user
*/
public function activate()
{
if ((new UsersFunctions($this->context->dbDriver))->activateUser(
$this->profile['id'],
$this->context->core['userAutoValidation']
)) {
$this->profile['activated'] = 1;
return true;
} else {
return false;
}
}
/**
* Return user orders
*
* [IMPORTANT] This function requires Cart add-on
*/
public function getOrders()
{
if (! isset($this->context->addons['Cart'])) {
RestoLogUtil::httpError(404, 'Cart add-on not installed');
}
return (new CartFunctions($this->context->dbDriver))->getOrders($this->profile['id']);
}
/**
* Returns rights
*/
public function getRights()
{
$this->loadProfile();
/*
* Compute rights if they are not already set
*/
if ( !isset($this->rights) ) {
$this->rights = (new RightsFunctions($this->context->dbDriver))->getRightsForUser($this);
}
return $this->rights;
}
/**
* Get followers
*
* [IMPORTANT] Requires Social add-on
*/
public function getFollowers()
{
if (! isset($this->context->addons['Social'])) {
RestoLogUtil::httpError(404, 'Social add-on not installed');
}
return (new SocialFunctions($this->context->dbDriver))->getFollowers(array(
'id' => $this->profile['id']
));
}
/**
* Get followings
*/
public function getFollowings()
{
if (! isset($this->context->addons['Social'])) {
RestoLogUtil::httpError(404, 'Social add-on not installed');
}
return (new SocialFunctions($this->context->dbDriver))->getFollowings(array(
'id' => $this->profile['id']
));
}
/**
* Return the list of user groups
*
* @throws Exception
*/
public function getGroups()
{
if ( !isset($this->groups) ) {
$this->groups = (new GroupsFunctions($this->context->dbDriver))->getGroups(array('userid' => $this->profile['id']));
}
return $this->groups;
}
/**
* Return the list of user group ids
*
* @throws Exception
*/
public function getGroupIds()
{
$groups = $this->getGroups();
// Everybody is in the default RestoConstants::GROUP_DEFAULT_ID;
$ids = [
RestoConstants::GROUP_DEFAULT_ID
];
for ($i = 0, $ii = count($groups); $i < $ii; $i++) {
if ( $groups[$i]['id'] !== RestoConstants::GROUP_DEFAULT_ID ) {
$ids[] = $groups[$i]['id'];
}
}
return $ids;
}
/**
* Return true if user is in $group
*
* @param string $group
* @throws Exception
*/
public function hasGroup($group)
{
return in_array($group, $this->getGroupIds());
}
/**
* Send reset password link to user email adress
*
*/
public function sendResetPasswordLink()
{
$this->loadProfile();
/*
* Only existing local user can change there password
*/
if ((new UsersFunctions($this->context->dbDriver))->userActivatedStatus(array('email' => $this->profile['email'])) !== 1) {
RestoLogUtil::httpError(404, 'Email not Found');
}
/*
* User authenticated externally (i.e. google, facebook) cannot change there password
*/
if ((new UsersFunctions($this->context->dbDriver))->getUserPassword($this->profile['email']) === str_repeat('*', 60)) {
RestoLogUtil::httpError(400, 'External user');
}
/*
* Send email with reset link
*/
$token = RestoUtil::encrypt(mt_rand(0, 100000) . microtime());
if (! (new UsersFunctions($this->context->dbDriver))->updateResetToken(
$this->profile['email'],
$token
)) {
RestoLogUtil::httpError(500);
}
if (!(new RestoNotifier($this->context->servicesInfos, $this->context->lang))->sendMailForResetPassword($this->profile['email'], $this->context->core['sendmail'], array(
'token' => $token
))) {
RestoLogUtil::httpError(500, 'Cannot send reset link');
}
return RestoLogUtil::success('Reset link sent to ' . $this->profile['email']);
}
/**
* Reload profile from database
*/
public function loadProfile()
{
if ( !$this->isComplete && isset($this->profile['id']) ) {
$this->profile = (new UsersFunctions($this->context->dbDriver))->getUserProfile('id', $this->profile['id']);
$this->isComplete = true;
}
}
}