Find this useful? Enter your email to receive occasional updates for securing PHP code.
Signing you up...
Thank you for signing up!
PHP Decode
<?php declare(strict_types=1); /** * Passbolt ~ Open source password manager for teams ..
Decoded Output download
<?php
declare(strict_types=1);
/**
* Passbolt ~ Open source password manager for teams
* Copyright (c) Passbolt SA (https://www.passbolt.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Passbolt SA (https://www.passbolt.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.passbolt.com Passbolt(tm)
* @since 2.0.0
*/
namespace App\Model\Traits\Users;
use App\Error\Exception\NoAdminInDbException;
use App\Model\Entity\Role;
use App\Model\Entity\User;
use App\Model\Event\TableFindIndexBefore;
use App\Model\Table\AvatarsTable;
use App\Model\Table\Dto\FindIndexOptions;
use App\Model\Validation\EmailValidationRule;
use App\Utility\UuidFactory;
use Cake\Collection\CollectionInterface;
use Cake\Database\Expression\IdentifierExpression;
use Cake\Database\Expression\QueryExpression;
use Cake\I18n\FrozenTime;
use Cake\ORM\Query;
use Cake\Utility\Hash;
use Cake\Validation\Validation;
use Exception;
use InvalidArgumentException;
/**
* @method \Cake\Event\EventManager getEventManager()
* @property \Passbolt\Log\Model\Table\ActionLogsTable $ActionLogs
*/
trait UsersFindersTrait
{
/**
* Filter a Groups query by groups users.
*
* @param \Cake\ORM\Query $query The query to augment.
* @param array<string> $groupsIds The users to filter the query on.
* @param bool $areManager (optional) Should the users be only managers ? Default false.
* @return \Cake\ORM\Query $query
*/
private function _filterQueryByGroupsUsers(Query $query, array $groupsIds, bool $areManager = false)
{
// If there is only one group use a left join
if (count($groupsIds) == 1) {
$query->leftJoinWith('GroupsUsers');
$query->where(['GroupsUsers.group_id' => $groupsIds[0]]);
if ($areManager) {
$query->where(['GroupsUsers.is_admin' => true]);
}
return $query;
}
// Otherwise use a subquery to find all the users that are members of all the listed groups
$having = $query->getConnection()->getDriver()->quoteIdentifier('COUNT(GroupsUsers.user_id)');
$subQuery = $this->GroupsUsers->find()
->select('GroupsUsers.user_id')
->where(['GroupsUsers.group_id IN' => $groupsIds])
->group('GroupsUsers.user_id')
->having([$having => count($groupsIds)]);
// Execute the sub query and extract the user ids.
$matchingUserIds = Hash::extract($subQuery->toArray(), '{n}.user_id');
// Filter the query.
if (empty($matchingUserIds)) {
// if no user match all groups it should return nobody
$query->where(['Users.id' => 'NOT_A_VALID_USER_ID']);
} else {
$query->where(['Users.id IN' => $matchingUserIds]);
}
return $query;
}
/**
* Filter a Users query by resource access.
* Only the users who have a permission (Read/Update/Owner) to access a resource should be returned by the query.
*
* By instance :
* $query = $Users->find()->where('Users.username LIKE' => '%@passbolt.com');
* _filterQueryByResourceAccess($query, 'RESOURCE_UUID');
*
* Should filter all the users with a passbolt username who have a permission to access the resource identified by
* RESOURCE_UUID.
*
* @param \Cake\ORM\Query $query The query to augment.
* @param string $resourceId The resource the users must have access.
* @return \Cake\ORM\Query $query
* @throws \InvalidArgumentException if the ressourceId is not a valid uuid
*/
public function filterQueryByResourceAccess(Query $query, string $resourceId): Query
{
if (!Validation::uuid($resourceId)) {
throw new InvalidArgumentException(__('The resource identifier should be a valid UUID.'));
}
return $this->filterQueryByResourcesAccess($query, [$resourceId]);
}
/**
* @param \Cake\ORM\Query $query Users query
* @param array|\Cake\ORM\Query $resourceIds Resource IDs the users should have access to
* @param array $permissionTypes array of permission type to filter along (OWNER, UPDATE or READ). If empty do not filter vy permission type
* @return \Cake\ORM\Query
*/
public function filterQueryByResourcesAccess(Query $query, $resourceIds, array $permissionTypes = []): Query
{
if (is_array($resourceIds) && empty($resourceIds)) {
return $query;
}
// The query requires a join with Permissions not constraint with the default condition added by the HasMany
// relationship : Users.id = Permissions.aro_foreign_key.
// The join will be used in relation to Groups as well, to find the users inherited permissions from Groups.
// To do so, add an extra join.
$conditions = ['PermissionsFilterAccess.aco_foreign_key IN' => $resourceIds];
if (!empty($permissionTypes)) {
$conditions['PermissionsFilterAccess.type IN'] = $permissionTypes;
}
$query->join([
'table' => $this->getAssociation('Permissions')->getTable(),
'alias' => 'PermissionsFilterAccess',
'type' => 'INNER',
'conditions' => $conditions,
]);
// Subquery to retrieve the groups the user is member of.
$groupIdsSubquery = $this->Groups->GroupsUsers
->find()
->select('group_id')
->where(['user_id' => new IdentifierExpression('Users.id')]);
// Use distinct to avoid duplicate as it can happen that a user is member of two groups which
// both have a permission for the same resource
return $query->distinct()
// Filter on the users who have a direct permissions.
// Or on users who are members of a group which have permissions.
->where(
['OR' => [
['PermissionsFilterAccess.aro_foreign_key' => new IdentifierExpression('Users.id')],
['PermissionsFilterAccess.aro_foreign_key IN' => $groupIdsSubquery],
]]
);
}
/**
* Filter a Users query by search.
* Search on the following fields :
* - Users.username
* - Users.Profile.first_name
* - Users.Profile.last_name
*
* By instance :
* $query = $Users->find();
* $Users->_filterQueryBySearch($query, 'ada');
*
* Should filter all the users with a username or a name containing ada.
*
* @param \Cake\ORM\Query $query The query to augment.
* @param string $search The string to search.
* @return \Cake\ORM\Query $query
*/
private function _filterQueryBySearch(Query $query, string $search)
{
$search = '%' . $search . '%';
return $query->where(['OR' => [
['Users.username LIKE' => $search],
['Profiles.first_name LIKE' => $search],
['Profiles.last_name LIKE' => $search],
]]);
}
/**
* Filter a Users query by users that don't have permission for a resource.
*
* By instance :
* $query = $Users->find();
* $Users->_filterQueryByHasNotPermission($query, 'ada');
*
* Should filter all the users that do not have a permission for apache.
*
* @param \Cake\ORM\Query $query The query to augment.
* @param string $resourceId The resource to search potential users for.
* @return \Cake\ORM\Query $query
* @throws \InvalidArgumentException if the resource id is not a valid uuid
*/
private function _filterQueryByHasNotPermission(Query $query, string $resourceId)
{
if (!Validation::uuid($resourceId)) {
throw new InvalidArgumentException('The resource identifier should be a valid UUID.');
}
$permissionQuery = $this->Permissions->find()
->select(['Permissions.aro_foreign_key'])
->where([
'Permissions.aro' => 'User',
'Permissions.aco_foreign_key' => $resourceId,
]);
// Filter on the users who do not have yet a permission.
return $query->where(['Users.id NOT IN' => $permissionQuery]);
}
/**
* Build the query that fetches data for user index
*
* @param string $role name
* @param array $options filters
* @return \Cake\ORM\Query
* @throws \InvalidArgumentException if no role is specified
*/
public function findIndex(string $role, ?array $options = [])
{
$query = $this->find();
$event = TableFindIndexBefore::create($query, FindIndexOptions::createFromArray($options), $this);
/** @var \App\Model\Event\TableFindIndexBefore $event */
$this->getEventManager()->dispatch($event);
$query = $event->getQuery();
// Options must contain a role
if (!$this->Roles->isValidRoleName($role)) {
throw new InvalidArgumentException('The role name is not valid.');
}
// Default associated data
$containDefault = [
'gpgkey' => true, 'profile' => true, 'groups_users' => true, 'role' => true,
];
$options['contain'] = $options['contain'] ?? [];
$options['contain'] = array_merge($containDefault, $options['contain']);
if (isset($options['contain']['role']) && $options['contain']['role']) {
$query->contain('Roles');
}
if (isset($options['contain']['gpgkey']) && $options['contain']['gpgkey']) {
$query->contain('Gpgkeys');
}
if (isset($options['contain']['profile']) && $options['contain']['profile']) {
$query->contain(['Profiles' => AvatarsTable::addContainAvatar()]);
}
if (isset($options['contain']['groups_users']) && $options['contain']['groups_users']) {
$query->contain('GroupsUsers');
}
if (isset($options['contain']['last_logged_in']) && $options['contain']['last_logged_in']) {
$query->find('lastLoggedIn');
}
// Filter out guests and deleted users
$query->where([
'Users.deleted' => false,
'Users.role_id <>' => $this->Roles->getIdByName(Role::GUEST),
]);
// If searching admins
if (isset($options['filter']['is-admin'])) {
$query->where([
'Users.role_id' => $this->Roles->getIdByName(Role::ADMIN),
]);
}
// If user is admin, we allow seeing inactive users via the 'is-active' filter
if ($role === Role::ADMIN) {
if (isset($options['filter']['is-active'])) {
$query->where(['Users.active' => $options['filter']['is-active']]);
}
} else {
// otherwise we only show active users
$query->where(['Users.active' => true]);
}
// If searching for a name or username
if (isset($options['filter']['search']) && count($options['filter']['search'])) {
$query = $this->_filterQueryBySearch($query, $options['filter']['search'][0]);
}
// If searching by group id
if (isset($options['filter']['has-groups']) && count($options['filter']['has-groups'])) {
$query = $this->_filterQueryByGroupsUsers($query, $options['filter']['has-groups']);
}
// If searching by resource access
if (isset($options['filter']['has-access']) && count($options['filter']['has-access'])) {
$query = $this->filterQueryByResourceAccess($query, $options['filter']['has-access'][0]);
}
// If searching by resource the user do not have a direct permission for
if (isset($options['filter']['has-not-permission']) && count($options['filter']['has-not-permission'])) {
$query = $this->_filterQueryByHasNotPermission($query, $options['filter']['has-not-permission'][0]);
}
// Ordering options
if (isset($options['order'])) {
$query->order($options['order']);
}
return $query;
}
/**
* Find view
*
* @param string $userId uuid
* @param string $roleName role name
* @return \Cake\ORM\Query
* @throws \Exception
* @throws \InvalidArgumentException if the role name or user id are not valid
*/
public function findView(string $userId, string $roleName)
{
if (!Validation::uuid($userId)) {
throw new InvalidArgumentException('The user identifier should be a valid UUID.');
}
if (!$this->Roles->isValidRoleName($roleName)) {
throw new InvalidArgumentException('The role name is not valid.');
}
// Same rule than index apply with a specific id requested
return $this->findIndex($roleName)->where(['Users.id' => $userId]);
}
/**
* Find delete
*
* @param string $userId uuid
* @param string $roleName role name
* @return \Cake\ORM\Query
* @throws \InvalidArgumentException if the role name or user id are not valid
*/
public function findDelete(string $userId, string $roleName)
{
if (!Validation::uuid($userId)) {
throw new InvalidArgumentException('The user identifier should be a valid UUID.');
}
if (!$this->Roles->isValidRoleName($roleName)) {
throw new InvalidArgumentException('The role name is not valid.');
}
return $this->findIndex($roleName)->where(['Users.id' => $userId]);
}
/**
* Build the query that fetches the user data during authentication
*
* @param \Cake\ORM\Query $query a query instance
* @param array $options options
* @return \Cake\ORM\Query
* @throws \Exception if fingerprint id is not set
*/
public function findAuth(Query $query, array $options)
{
// Options must contain an id
if (!isset($options['fingerprint'])) {
throw new Exception('User table findAuth should have a fingerprint id set in options.');
}
// auth query is always done as guest
// Use default index option (active:true, deleted:false) and contains
$query = $this->findIndex(Role::GUEST)
->where(['Gpgkeys.fingerprint' => $options['fingerprint']]);
return $query;
}
/**
* Build the query that fetches a user by username
* including role and profile
*
* @param string $username email of user to retrieve
* @param array $options options
* @return \Cake\ORM\Query
* @throws \InvalidArgumentException if the username is not an email
*/
public function findByUsername(string $username, ?array $options = [])
{
if (!EmailValidationRule::check($username)) {
throw new InvalidArgumentException('The username should be a valid email.');
}
// show active first and do not count deleted ones
return $this->findByUsernameCaseAware($username)
->where(['deleted' => false])
->contain([
'Roles',
'Profiles' => AvatarsTable::addContainAvatar(),
])
->order(['Users.active' => 'DESC']);
}
/**
* Search a user by username. If username are defined as case-sensitive,
* filter out the false matches
*
* @param string $username username to query
* @return \Cake\ORM\Query
* @throws \InvalidArgumentException if the username is not valid email
* @see UsersTable::isUsernameCaseSensitive()
*/
public function findByUsernameCaseAware(string $username): Query
{
$query = $this->find()->where([
'LOWER(Users.username)' => mb_strtolower($username),
]);
if ($this->isUsernameCaseSensitive()) {
$query->formatResults(function (CollectionInterface $results) use ($username): CollectionInterface {
return $results->filter(function (User $user) use ($username) {
return $user->username === $username;
})->compile(false);
});
}
return $query;
}
/**
* Lists ['user_id' => 'username'] not deleted and featured multiple times
*
* @return \Cake\ORM\Query
*/
public function listDuplicateUsernames(): Query
{
if ($this->isUsernameCaseSensitive()) {
return $this->listDuplicateUsernameCaseSensitive();
} else {
return $this->listDuplicateUsernamesCaseInsensitive();
}
}
/**
* Lists all duplicated lower-cased usernames
*
* @return \Cake\ORM\Query
*/
protected function listDuplicateUsernamesCaseInsensitive(): Query
{
$subQueryOfLowerCasedUsernameDuplicates = $this
->find()
// MAX() here is just to make MySQL happy without that query breaks in MySQL(especially in 5.7)
->select(['lower_username' => 'MAX(LOWER(Users.username))'])
->where(['deleted' => false])
->group('LOWER(Users.username)')
->having('count(*) > 1');
return $this->find('list', ['keyField' => 'id', 'valueField' => 'username'])
->disableHydration()
->select(['id', 'username'])
->where([
'LOWER(username) IN' => $subQueryOfLowerCasedUsernameDuplicates,
'deleted' => false,
])
->orderAsc('LOWER(username)');
}
/**
* @return \Cake\ORM\Query
*/
protected function listDuplicateUsernameCaseSensitive(): Query
{
// Let PHP remove the unique usernames, case sensitive
$filterUniqueCaseSensitive = function (CollectionInterface $results): CollectionInterface {
$duplicates = $results->toArray();
foreach (array_count_values($duplicates) as $val => $c) {
if ($c === 1) {
$results = $results->reject(function (string $value) use ($val) {
return $val === $value;
});
}
}
return $results->compile(false);
};
return $this
->listDuplicateUsernamesCaseInsensitive()
->formatResults($filterUniqueCaseSensitive);
}
/**
* Get a user info for an email notification context
*
* @param string $userId uuid
* @throws \InvalidArgumentException if the user id is not a valid uuid
* @return \App\Model\Entity\User|null
*/
public function findFirstForEmail(string $userId)
{
if (!Validation::uuid($userId)) {
throw new InvalidArgumentException('The user identifier should be a valid UUID.');
}
/** @var \App\Model\Entity\User $user */
$user = $this->find('locale')
->where(['Users.id' => $userId])
->contain([
'Profiles' => AvatarsTable::addContainAvatar(),
'Roles',
])
->first();
return $user;
}
/**
* Get a user info for an email notification context
*
* @return \App\Model\Entity\User|null
*/
public function findFirstAdmin(): ?User
{
$tableHasDisabledField = $this->getSchema()->hasColumn('disabled');
$query = $this->find();
// This check is required as this method is called in migrations
// anterior to the creation of the "disabled" field
if ($tableHasDisabledField) {
$query->find('notDisabled');
}
/** @var \App\Model\Entity\User $user */
$user = $query
->where([
'Users.deleted' => false,
'Users.active' => true,
'Roles.name' => Role::ADMIN,
])
->order(['Users.created' => 'ASC'])
->contain(['Roles'])
->first();
return $user;
}
/**
* @return \App\Model\Entity\User
* @throws \App\Error\Exception\NoAdminInDbException if no admin were found
*/
public function findFirstAdminOrThrowNoAdminInDbException(): User
{
$user = $this->findFirstAdmin();
if (is_null($user)) {
throw new NoAdminInDbException();
}
return $user;
}
/**
* Return a list of admin users (active, non soft-deleted) with their role attached
*
* @return \Cake\ORM\Query
*/
public function findAdmins(): Query
{
return $this->find()
->where(
[
'Users.deleted' => false,
'Users.active' => true,
'Roles.name' => Role::ADMIN,
]
)
->order(['Users.created' => 'ASC'])
->contain(['Roles']);
}
/**
* Get all active users.
*
* @return \Cake\ORM\Query
*/
public function findActive()
{
return $this->find()
->where([
'Users.deleted' => false,
'Users.active' => true,
])
->order(['Users.created' => 'ASC']);
}
/**
* Filter out disabled users.
*
* @param \Cake\ORM\Query $query query
* @return \Cake\ORM\Query
*/
public function findNotDisabled(Query $query): Query
{
return $query->where(function (QueryExpression $where) {
return $where->or(function (QueryExpression $or) {
return $or
->isNull($this->aliasField('disabled'))
->gt($this->aliasField('disabled'), FrozenTime::now());
});
});
}
/**
* Retrieve users' last logged in date.
*
* @param \Cake\ORM\Query $query query
* @return \Cake\ORM\Query
*/
public function findlastLoggedIn(Query $query)
{
// Retrieve the last logged in date for each user, based on the action_logs table.
$loginActionId = UuidFactory::uuid('AuthLogin.loginPost');
$subQuery = $this->ActionLogs->find();
$subQuery
->select(['last_logged_in' => $subQuery->func()->max(new IdentifierExpression('ActionLogs.created'))])
->where([
'ActionLogs.action_id' => $loginActionId,
'ActionLogs.status' => 1,
'ActionLogs.user_id' => new IdentifierExpression('Users.id'),
])
->limit(1);
$selectTypeMap = $query->getSelectTypeMap();
$selectTypeMap->addDefaults(['last_logged_in' => 'datetime']);
$query->selectAlso(['last_logged_in' => $subQuery]);
return $query;
}
/**
* Active and non deleted users.
*
* @param \Cake\ORM\Query $query Query to carve.
* @return \Cake\ORM\Query
*/
public function findActiveNotDeleted(Query $query): Query
{
return $query->where([
$this->aliasField('active') => true,
$this->aliasField('deleted') => false,
]);
}
/**
* Active and non deleted users only with role
*
* @param \Cake\ORM\Query $query Query to carve.
* @return \Cake\ORM\Query
*/
public function findActiveNotDeletedContainRole(Query $query): Query
{
return $query->find('activeNotDeleted')->contain('Roles');
}
}
?>
Did this file decode correctly?
Original Code
<?php
declare(strict_types=1);
/**
* Passbolt ~ Open source password manager for teams
* Copyright (c) Passbolt SA (https://www.passbolt.com)
*
* Licensed under GNU Affero General Public License version 3 of the or any later version.
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Passbolt SA (https://www.passbolt.com)
* @license https://opensource.org/licenses/AGPL-3.0 AGPL License
* @link https://www.passbolt.com Passbolt(tm)
* @since 2.0.0
*/
namespace App\Model\Traits\Users;
use App\Error\Exception\NoAdminInDbException;
use App\Model\Entity\Role;
use App\Model\Entity\User;
use App\Model\Event\TableFindIndexBefore;
use App\Model\Table\AvatarsTable;
use App\Model\Table\Dto\FindIndexOptions;
use App\Model\Validation\EmailValidationRule;
use App\Utility\UuidFactory;
use Cake\Collection\CollectionInterface;
use Cake\Database\Expression\IdentifierExpression;
use Cake\Database\Expression\QueryExpression;
use Cake\I18n\FrozenTime;
use Cake\ORM\Query;
use Cake\Utility\Hash;
use Cake\Validation\Validation;
use Exception;
use InvalidArgumentException;
/**
* @method \Cake\Event\EventManager getEventManager()
* @property \Passbolt\Log\Model\Table\ActionLogsTable $ActionLogs
*/
trait UsersFindersTrait
{
/**
* Filter a Groups query by groups users.
*
* @param \Cake\ORM\Query $query The query to augment.
* @param array<string> $groupsIds The users to filter the query on.
* @param bool $areManager (optional) Should the users be only managers ? Default false.
* @return \Cake\ORM\Query $query
*/
private function _filterQueryByGroupsUsers(Query $query, array $groupsIds, bool $areManager = false)
{
// If there is only one group use a left join
if (count($groupsIds) == 1) {
$query->leftJoinWith('GroupsUsers');
$query->where(['GroupsUsers.group_id' => $groupsIds[0]]);
if ($areManager) {
$query->where(['GroupsUsers.is_admin' => true]);
}
return $query;
}
// Otherwise use a subquery to find all the users that are members of all the listed groups
$having = $query->getConnection()->getDriver()->quoteIdentifier('COUNT(GroupsUsers.user_id)');
$subQuery = $this->GroupsUsers->find()
->select('GroupsUsers.user_id')
->where(['GroupsUsers.group_id IN' => $groupsIds])
->group('GroupsUsers.user_id')
->having([$having => count($groupsIds)]);
// Execute the sub query and extract the user ids.
$matchingUserIds = Hash::extract($subQuery->toArray(), '{n}.user_id');
// Filter the query.
if (empty($matchingUserIds)) {
// if no user match all groups it should return nobody
$query->where(['Users.id' => 'NOT_A_VALID_USER_ID']);
} else {
$query->where(['Users.id IN' => $matchingUserIds]);
}
return $query;
}
/**
* Filter a Users query by resource access.
* Only the users who have a permission (Read/Update/Owner) to access a resource should be returned by the query.
*
* By instance :
* $query = $Users->find()->where('Users.username LIKE' => '%@passbolt.com');
* _filterQueryByResourceAccess($query, 'RESOURCE_UUID');
*
* Should filter all the users with a passbolt username who have a permission to access the resource identified by
* RESOURCE_UUID.
*
* @param \Cake\ORM\Query $query The query to augment.
* @param string $resourceId The resource the users must have access.
* @return \Cake\ORM\Query $query
* @throws \InvalidArgumentException if the ressourceId is not a valid uuid
*/
public function filterQueryByResourceAccess(Query $query, string $resourceId): Query
{
if (!Validation::uuid($resourceId)) {
throw new InvalidArgumentException(__('The resource identifier should be a valid UUID.'));
}
return $this->filterQueryByResourcesAccess($query, [$resourceId]);
}
/**
* @param \Cake\ORM\Query $query Users query
* @param array|\Cake\ORM\Query $resourceIds Resource IDs the users should have access to
* @param array $permissionTypes array of permission type to filter along (OWNER, UPDATE or READ). If empty do not filter vy permission type
* @return \Cake\ORM\Query
*/
public function filterQueryByResourcesAccess(Query $query, $resourceIds, array $permissionTypes = []): Query
{
if (is_array($resourceIds) && empty($resourceIds)) {
return $query;
}
// The query requires a join with Permissions not constraint with the default condition added by the HasMany
// relationship : Users.id = Permissions.aro_foreign_key.
// The join will be used in relation to Groups as well, to find the users inherited permissions from Groups.
// To do so, add an extra join.
$conditions = ['PermissionsFilterAccess.aco_foreign_key IN' => $resourceIds];
if (!empty($permissionTypes)) {
$conditions['PermissionsFilterAccess.type IN'] = $permissionTypes;
}
$query->join([
'table' => $this->getAssociation('Permissions')->getTable(),
'alias' => 'PermissionsFilterAccess',
'type' => 'INNER',
'conditions' => $conditions,
]);
// Subquery to retrieve the groups the user is member of.
$groupIdsSubquery = $this->Groups->GroupsUsers
->find()
->select('group_id')
->where(['user_id' => new IdentifierExpression('Users.id')]);
// Use distinct to avoid duplicate as it can happen that a user is member of two groups which
// both have a permission for the same resource
return $query->distinct()
// Filter on the users who have a direct permissions.
// Or on users who are members of a group which have permissions.
->where(
['OR' => [
['PermissionsFilterAccess.aro_foreign_key' => new IdentifierExpression('Users.id')],
['PermissionsFilterAccess.aro_foreign_key IN' => $groupIdsSubquery],
]]
);
}
/**
* Filter a Users query by search.
* Search on the following fields :
* - Users.username
* - Users.Profile.first_name
* - Users.Profile.last_name
*
* By instance :
* $query = $Users->find();
* $Users->_filterQueryBySearch($query, 'ada');
*
* Should filter all the users with a username or a name containing ada.
*
* @param \Cake\ORM\Query $query The query to augment.
* @param string $search The string to search.
* @return \Cake\ORM\Query $query
*/
private function _filterQueryBySearch(Query $query, string $search)
{
$search = '%' . $search . '%';
return $query->where(['OR' => [
['Users.username LIKE' => $search],
['Profiles.first_name LIKE' => $search],
['Profiles.last_name LIKE' => $search],
]]);
}
/**
* Filter a Users query by users that don't have permission for a resource.
*
* By instance :
* $query = $Users->find();
* $Users->_filterQueryByHasNotPermission($query, 'ada');
*
* Should filter all the users that do not have a permission for apache.
*
* @param \Cake\ORM\Query $query The query to augment.
* @param string $resourceId The resource to search potential users for.
* @return \Cake\ORM\Query $query
* @throws \InvalidArgumentException if the resource id is not a valid uuid
*/
private function _filterQueryByHasNotPermission(Query $query, string $resourceId)
{
if (!Validation::uuid($resourceId)) {
throw new InvalidArgumentException('The resource identifier should be a valid UUID.');
}
$permissionQuery = $this->Permissions->find()
->select(['Permissions.aro_foreign_key'])
->where([
'Permissions.aro' => 'User',
'Permissions.aco_foreign_key' => $resourceId,
]);
// Filter on the users who do not have yet a permission.
return $query->where(['Users.id NOT IN' => $permissionQuery]);
}
/**
* Build the query that fetches data for user index
*
* @param string $role name
* @param array $options filters
* @return \Cake\ORM\Query
* @throws \InvalidArgumentException if no role is specified
*/
public function findIndex(string $role, ?array $options = [])
{
$query = $this->find();
$event = TableFindIndexBefore::create($query, FindIndexOptions::createFromArray($options), $this);
/** @var \App\Model\Event\TableFindIndexBefore $event */
$this->getEventManager()->dispatch($event);
$query = $event->getQuery();
// Options must contain a role
if (!$this->Roles->isValidRoleName($role)) {
throw new InvalidArgumentException('The role name is not valid.');
}
// Default associated data
$containDefault = [
'gpgkey' => true, 'profile' => true, 'groups_users' => true, 'role' => true,
];
$options['contain'] = $options['contain'] ?? [];
$options['contain'] = array_merge($containDefault, $options['contain']);
if (isset($options['contain']['role']) && $options['contain']['role']) {
$query->contain('Roles');
}
if (isset($options['contain']['gpgkey']) && $options['contain']['gpgkey']) {
$query->contain('Gpgkeys');
}
if (isset($options['contain']['profile']) && $options['contain']['profile']) {
$query->contain(['Profiles' => AvatarsTable::addContainAvatar()]);
}
if (isset($options['contain']['groups_users']) && $options['contain']['groups_users']) {
$query->contain('GroupsUsers');
}
if (isset($options['contain']['last_logged_in']) && $options['contain']['last_logged_in']) {
$query->find('lastLoggedIn');
}
// Filter out guests and deleted users
$query->where([
'Users.deleted' => false,
'Users.role_id <>' => $this->Roles->getIdByName(Role::GUEST),
]);
// If searching admins
if (isset($options['filter']['is-admin'])) {
$query->where([
'Users.role_id' => $this->Roles->getIdByName(Role::ADMIN),
]);
}
// If user is admin, we allow seeing inactive users via the 'is-active' filter
if ($role === Role::ADMIN) {
if (isset($options['filter']['is-active'])) {
$query->where(['Users.active' => $options['filter']['is-active']]);
}
} else {
// otherwise we only show active users
$query->where(['Users.active' => true]);
}
// If searching for a name or username
if (isset($options['filter']['search']) && count($options['filter']['search'])) {
$query = $this->_filterQueryBySearch($query, $options['filter']['search'][0]);
}
// If searching by group id
if (isset($options['filter']['has-groups']) && count($options['filter']['has-groups'])) {
$query = $this->_filterQueryByGroupsUsers($query, $options['filter']['has-groups']);
}
// If searching by resource access
if (isset($options['filter']['has-access']) && count($options['filter']['has-access'])) {
$query = $this->filterQueryByResourceAccess($query, $options['filter']['has-access'][0]);
}
// If searching by resource the user do not have a direct permission for
if (isset($options['filter']['has-not-permission']) && count($options['filter']['has-not-permission'])) {
$query = $this->_filterQueryByHasNotPermission($query, $options['filter']['has-not-permission'][0]);
}
// Ordering options
if (isset($options['order'])) {
$query->order($options['order']);
}
return $query;
}
/**
* Find view
*
* @param string $userId uuid
* @param string $roleName role name
* @return \Cake\ORM\Query
* @throws \Exception
* @throws \InvalidArgumentException if the role name or user id are not valid
*/
public function findView(string $userId, string $roleName)
{
if (!Validation::uuid($userId)) {
throw new InvalidArgumentException('The user identifier should be a valid UUID.');
}
if (!$this->Roles->isValidRoleName($roleName)) {
throw new InvalidArgumentException('The role name is not valid.');
}
// Same rule than index apply with a specific id requested
return $this->findIndex($roleName)->where(['Users.id' => $userId]);
}
/**
* Find delete
*
* @param string $userId uuid
* @param string $roleName role name
* @return \Cake\ORM\Query
* @throws \InvalidArgumentException if the role name or user id are not valid
*/
public function findDelete(string $userId, string $roleName)
{
if (!Validation::uuid($userId)) {
throw new InvalidArgumentException('The user identifier should be a valid UUID.');
}
if (!$this->Roles->isValidRoleName($roleName)) {
throw new InvalidArgumentException('The role name is not valid.');
}
return $this->findIndex($roleName)->where(['Users.id' => $userId]);
}
/**
* Build the query that fetches the user data during authentication
*
* @param \Cake\ORM\Query $query a query instance
* @param array $options options
* @return \Cake\ORM\Query
* @throws \Exception if fingerprint id is not set
*/
public function findAuth(Query $query, array $options)
{
// Options must contain an id
if (!isset($options['fingerprint'])) {
throw new Exception('User table findAuth should have a fingerprint id set in options.');
}
// auth query is always done as guest
// Use default index option (active:true, deleted:false) and contains
$query = $this->findIndex(Role::GUEST)
->where(['Gpgkeys.fingerprint' => $options['fingerprint']]);
return $query;
}
/**
* Build the query that fetches a user by username
* including role and profile
*
* @param string $username email of user to retrieve
* @param array $options options
* @return \Cake\ORM\Query
* @throws \InvalidArgumentException if the username is not an email
*/
public function findByUsername(string $username, ?array $options = [])
{
if (!EmailValidationRule::check($username)) {
throw new InvalidArgumentException('The username should be a valid email.');
}
// show active first and do not count deleted ones
return $this->findByUsernameCaseAware($username)
->where(['deleted' => false])
->contain([
'Roles',
'Profiles' => AvatarsTable::addContainAvatar(),
])
->order(['Users.active' => 'DESC']);
}
/**
* Search a user by username. If username are defined as case-sensitive,
* filter out the false matches
*
* @param string $username username to query
* @return \Cake\ORM\Query
* @throws \InvalidArgumentException if the username is not valid email
* @see UsersTable::isUsernameCaseSensitive()
*/
public function findByUsernameCaseAware(string $username): Query
{
$query = $this->find()->where([
'LOWER(Users.username)' => mb_strtolower($username),
]);
if ($this->isUsernameCaseSensitive()) {
$query->formatResults(function (CollectionInterface $results) use ($username): CollectionInterface {
return $results->filter(function (User $user) use ($username) {
return $user->username === $username;
})->compile(false);
});
}
return $query;
}
/**
* Lists ['user_id' => 'username'] not deleted and featured multiple times
*
* @return \Cake\ORM\Query
*/
public function listDuplicateUsernames(): Query
{
if ($this->isUsernameCaseSensitive()) {
return $this->listDuplicateUsernameCaseSensitive();
} else {
return $this->listDuplicateUsernamesCaseInsensitive();
}
}
/**
* Lists all duplicated lower-cased usernames
*
* @return \Cake\ORM\Query
*/
protected function listDuplicateUsernamesCaseInsensitive(): Query
{
$subQueryOfLowerCasedUsernameDuplicates = $this
->find()
// MAX() here is just to make MySQL happy without that query breaks in MySQL(especially in 5.7)
->select(['lower_username' => 'MAX(LOWER(Users.username))'])
->where(['deleted' => false])
->group('LOWER(Users.username)')
->having('count(*) > 1');
return $this->find('list', ['keyField' => 'id', 'valueField' => 'username'])
->disableHydration()
->select(['id', 'username'])
->where([
'LOWER(username) IN' => $subQueryOfLowerCasedUsernameDuplicates,
'deleted' => false,
])
->orderAsc('LOWER(username)');
}
/**
* @return \Cake\ORM\Query
*/
protected function listDuplicateUsernameCaseSensitive(): Query
{
// Let PHP remove the unique usernames, case sensitive
$filterUniqueCaseSensitive = function (CollectionInterface $results): CollectionInterface {
$duplicates = $results->toArray();
foreach (array_count_values($duplicates) as $val => $c) {
if ($c === 1) {
$results = $results->reject(function (string $value) use ($val) {
return $val === $value;
});
}
}
return $results->compile(false);
};
return $this
->listDuplicateUsernamesCaseInsensitive()
->formatResults($filterUniqueCaseSensitive);
}
/**
* Get a user info for an email notification context
*
* @param string $userId uuid
* @throws \InvalidArgumentException if the user id is not a valid uuid
* @return \App\Model\Entity\User|null
*/
public function findFirstForEmail(string $userId)
{
if (!Validation::uuid($userId)) {
throw new InvalidArgumentException('The user identifier should be a valid UUID.');
}
/** @var \App\Model\Entity\User $user */
$user = $this->find('locale')
->where(['Users.id' => $userId])
->contain([
'Profiles' => AvatarsTable::addContainAvatar(),
'Roles',
])
->first();
return $user;
}
/**
* Get a user info for an email notification context
*
* @return \App\Model\Entity\User|null
*/
public function findFirstAdmin(): ?User
{
$tableHasDisabledField = $this->getSchema()->hasColumn('disabled');
$query = $this->find();
// This check is required as this method is called in migrations
// anterior to the creation of the "disabled" field
if ($tableHasDisabledField) {
$query->find('notDisabled');
}
/** @var \App\Model\Entity\User $user */
$user = $query
->where([
'Users.deleted' => false,
'Users.active' => true,
'Roles.name' => Role::ADMIN,
])
->order(['Users.created' => 'ASC'])
->contain(['Roles'])
->first();
return $user;
}
/**
* @return \App\Model\Entity\User
* @throws \App\Error\Exception\NoAdminInDbException if no admin were found
*/
public function findFirstAdminOrThrowNoAdminInDbException(): User
{
$user = $this->findFirstAdmin();
if (is_null($user)) {
throw new NoAdminInDbException();
}
return $user;
}
/**
* Return a list of admin users (active, non soft-deleted) with their role attached
*
* @return \Cake\ORM\Query
*/
public function findAdmins(): Query
{
return $this->find()
->where(
[
'Users.deleted' => false,
'Users.active' => true,
'Roles.name' => Role::ADMIN,
]
)
->order(['Users.created' => 'ASC'])
->contain(['Roles']);
}
/**
* Get all active users.
*
* @return \Cake\ORM\Query
*/
public function findActive()
{
return $this->find()
->where([
'Users.deleted' => false,
'Users.active' => true,
])
->order(['Users.created' => 'ASC']);
}
/**
* Filter out disabled users.
*
* @param \Cake\ORM\Query $query query
* @return \Cake\ORM\Query
*/
public function findNotDisabled(Query $query): Query
{
return $query->where(function (QueryExpression $where) {
return $where->or(function (QueryExpression $or) {
return $or
->isNull($this->aliasField('disabled'))
->gt($this->aliasField('disabled'), FrozenTime::now());
});
});
}
/**
* Retrieve users' last logged in date.
*
* @param \Cake\ORM\Query $query query
* @return \Cake\ORM\Query
*/
public function findlastLoggedIn(Query $query)
{
// Retrieve the last logged in date for each user, based on the action_logs table.
$loginActionId = UuidFactory::uuid('AuthLogin.loginPost');
$subQuery = $this->ActionLogs->find();
$subQuery
->select(['last_logged_in' => $subQuery->func()->max(new IdentifierExpression('ActionLogs.created'))])
->where([
'ActionLogs.action_id' => $loginActionId,
'ActionLogs.status' => 1,
'ActionLogs.user_id' => new IdentifierExpression('Users.id'),
])
->limit(1);
$selectTypeMap = $query->getSelectTypeMap();
$selectTypeMap->addDefaults(['last_logged_in' => 'datetime']);
$query->selectAlso(['last_logged_in' => $subQuery]);
return $query;
}
/**
* Active and non deleted users.
*
* @param \Cake\ORM\Query $query Query to carve.
* @return \Cake\ORM\Query
*/
public function findActiveNotDeleted(Query $query): Query
{
return $query->where([
$this->aliasField('active') => true,
$this->aliasField('deleted') => false,
]);
}
/**
* Active and non deleted users only with role
*
* @param \Cake\ORM\Query $query Query to carve.
* @return \Cake\ORM\Query
*/
public function findActiveNotDeletedContainRole(Query $query): Query
{
return $query->find('activeNotDeleted')->contain('Roles');
}
}
Function Calls
None |
Stats
MD5 | 32a03df5049b4f8eb23a7b57f001fa0f |
Eval Count | 0 |
Decode Time | 92 ms |