app/resto/core/api/UsersAPI.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.
*/
/**
* Users API
*/
class UsersAPI
{
private $context;
private $user;
/**
* Constructor
*/
public function __construct($context, $user)
{
$this->context = $context;
$this->user = $user;
}
/**
* Return users
*
* @OA\Get(
* path="/users",
* summary="Get users",
* description="Return the list of user's profiles ordered by descending user identifier. A maximum of 50 profiles are returned per page. The *lt* parameter should be used for pagination",
* tags={"User"},
* @OA\Parameter(
* name="lt",
* in="query",
* style="form",
* description="Return user profiles with identifier lower than *lt* - used for pagination",
* @OA\Schema(
* type="integer"
* )
* ),
* @OA\Parameter(
* name="in",
* in="query",
* style="form",
* description="List of comma separated user identifiers",
* @OA\Schema(
* type="string"
* )
* ),
* @OA\Parameter(
* name="groupid",
* in="query",
* style="form",
* description="Return user profiles belonging to group identified by *groupid* ",
* @OA\Schema(
* type="string"
* )
* ),
* @OA\Parameter(
* name="q",
* in="query",
* style="form",
* description="Filter by name, firstname or lastname",
* @OA\Schema(
* type="string"
* )
* ),
* @OA\Response(
* response="200",
* description="List of users profiles",
* @OA\JsonContent(
* @OA\Property(
* property="totalResults",
* type="integer",
* description="Total number of user profiles"
* ),
* @OA\Property(
* property="exactCount",
* type="boolean",
* description="True if totalResults is an exact count. False if estimated."
* ),
* @OA\Property(
* property="profiles",
* type="array",
* @OA\Items(ref="#/components/schemas/UserDisplayProfile")
* ),
* example={
* "totalResults": 2,
* "exactCount": true,
* "profiles":{
* {
* "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
* },
* {
* "id": "1381434932013827205",
* "picture": "https://graph.facebook.com/410860042635946/picture?type=large",
* "groups": {
* "1"
* },
* "name": "Sergio",
* "followers": 16,
* "followings": 9,
* "registrationdate": "2016-10-08T22:50:34.187217Z",
* "followed": false,
* "followme": false
* }
* }
* }
* )
* ),
* @OA\Response(
* response="401",
* description="Unauthorized",
* @OA\JsonContent(ref="#/components/schemas/UnauthorizedError")
* ),
* security={
* {"basicAuth":{}, "bearerAuth":{}, "queryAuth":{}}
* }
* )
*
* @param array $params
*/
public function getUsersProfiles($params)
{
if (isset($params['lt']) && !ctype_digit($params['lt'])) {
return RestoLogUtil::httpError(400, 'Invalid lt - should be numeric');
}
if (isset($params['groupid']) && !ctype_digit($params['groupid'])) {
return RestoLogUtil::httpError(400, 'Invalid groupid');
}
if (isset($params['in'])) {
$exploded = explode(',', $params['in']);
for ($i = count($exploded);$i--;) {
if (!ctype_digit(trim($exploded[$i]))) {
return RestoLogUtil::httpError(400, 'Invalid in');
}
}
}
return (new UsersFunctions($this->context->dbDriver))->getUsersProfiles(
array(
'lt' => $params['lt'] ?? null,
'groupid' => $params['groupid'] ?? null,
'in' => $params['in'] ?? null,
'q' => $params['q'] ?? null
),
!$this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID) ? $this->user->profile['id'] : null
);
}
/**
* Get user profile
*
* @OA\Get(
* path="/users/{userid}",
* summary="Get user",
* tags={"User"},
* @OA\Parameter(
* name="userid",
* in="path",
* required=true,
* description="User's identifier",
* @OA\Schema(
* type="string"
* )
* ),
* @OA\Response(
* response="200",
* description="User profile",
* @OA\JsonContent(ref="#/components/schemas/UserDisplayProfile")
* ),
* @OA\Response(
* response="401",
* description="Unauthorized",
* @OA\JsonContent(ref="#/components/schemas/UnauthorizedError")
* ),
* @OA\Response(
* response="404",
* description="Resource not found",
* @OA\JsonContent(ref="#/components/schemas/NotFoundError")
* ),
* security={
* {"basicAuth":{}, "bearerAuth":{}, "queryAuth":{}}
* }
* )
*/
public function getUserProfile($params)
{
if ($this->user->profile['id'] === $params['userid']) {
$this->user->loadProfile();
return $this->user->profile;
}
return (new UsersFunctions($this->context->dbDriver))->getUserProfile(
'id',
$params['userid'],
array(
'from' => $this->user->profile['id'],
'partial' => true
)
);
}
/**
* Create user
*
* @OA\Post(
* path="/users",
* summary="Create user",
* tags={"User"},
* @OA\Response(
* response="200",
* description="User is created but not activated. An activation code is sent to user's email address.",
* @OA\JsonContent(
* @OA\Property(
* property="status",
* type="string",
* description="Status is *success*"
* ),
* @OA\Property(
* property="message",
* type="string",
* description="Message information"
* ),
* example={
* "status": "success",
* "message": "User john.doe@dev.null created"
* }
* )
* ),
* @OA\Response(
* response="400",
* description="Bad request",
* @OA\JsonContent(ref="#/components/schemas/BadRequestError")
* ),
* @OA\Response(
* response="409",
* description="User already exist",
* @OA\JsonContent(ref="#/components/schemas/ConflictError")
* ),
* @OA\Response(
* response="412",
* description="User already exist but is not activated",
* @OA\JsonContent(ref="#/components/schemas/ConflictError")
* ),
* @OA\RequestBody(
* description="User information to create user account",
* required=true,
* @OA\JsonContent(
* required={"email", "password"},
* @OA\Property(
* property="email",
* type="string",
* description="User email"
* ),
* @OA\Property(
* property="password",
* type="string",
* description="User password - don't worry it's encrypted server side"
* ),
* @OA\Property(
* property="picture",
* type="string",
* description="An http(s) url to the user's avatar picture"
* ),
* @OA\Property(
* property="name",
* type="string",
* description="User display name"
* ),
* @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="country",
* type="string",
* description="User country code (ISO 3166-1 alpha2 code)"
* ),
* @OA\Property(
* property="organization",
* type="string",
* description="Organization name"
* ),
* @OA\Property(
* property="flags",
* type="string",
* description="[Unused] Comma separated list of flags"
* ),
* @OA\Property(
* property="topics",
* type="string",
* description="Comma separated list of user's topics of interest"
* ),
* example={
* "email": "john.doe@dev.null",
* "password":"MySuperSecretPassword",
* "picture": "https://robohash.org/d0e907f8b6f4ee74cd4c38a515e2a4de?gravatar=hashed&bgset=any&size=400x400",
* "name": "jj",
* "firstname": "John",
* "lastname": "Doe",
* "bio": "Just a user",
* "country":"FR",
* "organization":"My nice company",
* "topics":"earth,fires,geology,glaciology,volcanism"
* }
* )
* )
* )
*
* @param array $params
* @param array $body
*/
public function createUser($params, $body)
{
foreach (array('email', 'password') as $required) {
if (!isset($body[$required])) {
RestoLogUtil::httpError(400, $required . ' is not set');
}
}
$profile = array(
'email' => $body['email'],
'password' => $body['password'] ?? null,
'name'=> $body['name'] ?? trim(join(' ', array(ucfirst($body['firstname'] ?? ''), ucfirst($body['lastname'] ?? '')))),
'firstname' => $body['firstname'] ?? null,
'lastname' => $body['lastname'] ?? null,
// [TODO] Check lang from request
'lang' => 'en',
'bio' => $body['bio'] ?? null,
'picture' => $body['picture'] ?? null,
'country' => $body['country'] ?? null,
'organization' => $body['organization'] ?? null,
'flags' => $body['flags'] ?? null,
'topics' => $body['topics'] ?? null,
// User created by admin is automatically activated
'activated' => ($this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID) || $this->context->core['userAutoActivation']) ? 1 : 0,
'followers' => 0,
'followings' => 0
);
return $this->storeProfile($profile, $this->context->core['storageInfo']);
}
/**
* Update user profile
*
* @OA\Put(
* path="/users/{userid}",
* summary="Update user",
* tags={"User"},
* @OA\Parameter(
* name="userid",
* in="path",
* required=true,
* description="User's identifier",
* @OA\Schema(
* type="string"
* )
* ),
* @OA\Response(
* response="200",
* description="User profile is updated",
* @OA\JsonContent(
* @OA\Property(
* property="status",
* type="string",
* description="Status is *success*"
* ),
* @OA\Property(
* property="message",
* type="string",
* description="Message information"
* ),
* example={
* "status": "success",
* "message": "Update profile for user john.doe@dev.null"
* }
* )
* ),
* @OA\Response(
* response="400",
* description="Bad request",
* @OA\JsonContent(ref="#/components/schemas/BadRequestError")
* ),
* @OA\RequestBody(
* description="User information to update",
* required=true,
* @OA\JsonContent(
* @OA\Property(
* property="password",
* type="string",
* description="User password - don't worry it's encrypted server side"
* ),
* @OA\Property(
* property="picture",
* type="string",
* description="An http(s) url to the user's avatar picture"
* ),
* @OA\Property(
* property="name",
* type="string",
* description="User display name"
* ),
* @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="topics",
* type="string",
* description="Comma separated list of user's topics of interest"
* ),
* example={
* "picture": "https://robohash.org/d0e907f8b6f4ee74cd4c38a515e2a4de?gravatar=hashed&bgset=any&size=400x400",
* "bio": "I just changed my picture, bio information and topics of interest list",
* "topics":"earth,fires"
* }
* )
* ),
* security={
* {"basicAuth":{}, "bearerAuth":{}, "queryAuth":{}}
* }
* )
*
* @param array $params
* @param array $body
*/
public function updateUserProfile($params, $body)
{
RestoUtil::checkUser($this->user, $params['userid']);
/*
* For normal user (i.e. non admin), some properties cannot be modified after validation
*/
if (! $this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID)) {
/*
* Already validated => avoid updating administrative properties
*/
if (isset($this->user->profile['validatedby'])) {
unset($body['activated'], $body['validatedby'], $body['validationdate'], $body['country'], $body['organization'], $body['organizationcountry'], $body['flags']);
}
}
/*
* Ensure that user can only update its profile
*/
$body['email'] = $this->user->profile['email'];
(new UsersFunctions($this->context->dbDriver))->updateUserProfile(
$body,
$this->context->core['storageInfo']
);
return RestoLogUtil::success('Update profile for user ' . $this->user->profile['email']);
}
/**
* Get user searches
*
* @OA\Get(
* path="/users/{userid}/history",
* summary="Get user's search history",
* description="Results are returned by pages with 50 results per page from most recent to oldest.",
* tags={"User"},
* @OA\Parameter(
* name="userid",
* in="path",
* required=true,
* description="User's identifier",
* @OA\Schema(
* type="string"
* )
* ),
* @OA\Parameter(
* name="querytime",
* in="query",
* style="form",
* description="Filter on query time. Interval of ISO8601 date (i.e. YYYY-MM-DDTHH:MM:SSZ)",
* @OA\Schema(
* type="string"
* )
* ),
* @OA\Parameter(
* name="lt",
* in="query",
* style="form",
* description="Return logs with gid lower than *lt* - used for pagination",
* @OA\Schema(
* type="integer"
* )
* ),
* @OA\Response(
* response="200",
* description="User's history",
* @OA\JsonContent(
* @OA\Property(
* property="id",
* type="string",
* description="User identifier"
* ),
* @OA\Property(
* property="logs",
* type="array",
* description="Array of user's log history",
* @OA\Items(
* type="object",
* @OA\Property(
* property="gid",
* type="integer",
* description="Log unique idendifier"
* ),
* @OA\Property(
* property="method",
* type="string",
* description="Method - one of GET, POST, PUT or DELETE"
* ),
* @OA\Property(
* property="path",
* type="string",
* description="Path relative to the root endpoint"
* ),
* @OA\Property(
* property="querytime",
* type="string",
* description="Date of query (in ISO 8601)"
* ),
* @OA\Property(
* property="query",
* type="string",
* description="Query string"
* ),
* @OA\Property(
* property="userid",
* type="string",
* description="User identifier (only display to admin user)"
* ),
* @OA\Property(
* property="ip",
* type="string",
* description="Calling IP address (only display to admin user)"
* )
* )
* ),
* example={
* "id": "1356771884787565573",
* "logs":{
* {
* "gid": 65,
* "method": "GET",
* "path": "/users/202707441557308418/logs",
* "querytime": "2019-01-05T07:10:38.785236Z"
* },
* {
* "gid": 39,
* "method": "GET",
* "path": "/users",
* "querytime": "2019-01-03T22:19:28.251167Z",
* "query": "&in=202707441557308418%60"
* }
* }
* }
* )
* ),
* @OA\Response(
* response="401",
* description="Unauthorized",
* @OA\JsonContent(ref="#/components/schemas/UnauthorizedError")
* ),
* @OA\Response(
* response="403",
* description="Forbidden",
* @OA\JsonContent(ref="#/components/schemas/ForbiddenError")
* ),
* security={
* {"basicAuth":{}, "bearerAuth":{}, "queryAuth":{}}
* }
* )
*/
public function getUserLogs($params)
{
/*
* [SECURITY] User is limited to its own history logs
*/
$isAdmin = $this->user->hasGroup(RestoConstants::GROUP_ADMIN_ID);
if (!$isAdmin) {
RestoUtil::checkUser($this->user, $params['userid']);
}
if (isset($params['lt']) && !ctype_digit($params['lt'])) {
return RestoLogUtil::httpError(400, 'Invalid lt - should be numeric');
}
return (new LogsFunctions($this->context->dbDriver))->getLogs(array(
'userid' => $params['userid'],
'lt' => $params['lt'] ?? null,
'querytime' => $params['querytime'] ?? null,
'fullDisplay' => $isAdmin
));
}
/**
* Store user profile
*
* @param array $profile
* @param array $storageInfo
*/
private function storeProfile($profile, $storageInfo)
{
$userInfo = (new UsersFunctions($this->context->dbDriver))->storeUserProfile(
$profile,
$storageInfo
);
if (isset($userInfo)) {
// Auto activation no email sent
if ($userInfo['activated'] === 1) {
return RestoLogUtil::success('User ' . $userInfo['email'] . ' created and activated', array('profile' => $userInfo));
}
if (!(new RestoNotifier($this->context->servicesInfos, $this->context->lang))->sendMailForUserActivation($userInfo['email'], $this->context->core['sendmail'], array(
'token' => $this->context->createRJWT($userInfo['id'], $this->context->core['tokenDuration'])
))) {
RestoLogUtil::httpError(500, 'Cannot send activation link');
}
} else {
RestoLogUtil::httpError(500, 'Database connection error');
}
}
}