Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
|
||||
#[ORM\Entity]
|
||||
class IdEntry
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $entityId;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected $expiryTimestamp;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('saml_id_entry');
|
||||
|
||||
$builder->createField('id', 'string')
|
||||
->makePrimaryKey()
|
||||
->generatedValue('NONE')
|
||||
->build();
|
||||
|
||||
$builder->createField('entityId', 'string')
|
||||
->columnName('entity_id')
|
||||
->makePrimaryKey()
|
||||
->generatedValue('NONE')
|
||||
->build();
|
||||
|
||||
$builder->createField('expiryTimestamp', 'integer')
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getEntityId()
|
||||
{
|
||||
return $this->entityId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $entityId
|
||||
*
|
||||
* @return IdEntry
|
||||
*/
|
||||
public function setEntityId($entityId)
|
||||
{
|
||||
$this->entityId = $entityId;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getExpiryTime(): \DateTime
|
||||
{
|
||||
$dt = new \DateTime();
|
||||
$dt->setTimestamp($this->expiryTimestamp);
|
||||
|
||||
return $dt;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return IdEntry
|
||||
*/
|
||||
public function setExpiryTime(\DateTime $expiryTime)
|
||||
{
|
||||
$this->expiryTimestamp = $expiryTime->getTimestamp();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $id
|
||||
*
|
||||
* @return IdEntry
|
||||
*/
|
||||
public function setId($id)
|
||||
{
|
||||
$this->id = $id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\CacheInvalidateInterface;
|
||||
use Mautic\CoreBundle\Entity\UuidInterface;
|
||||
use Mautic\CoreBundle\Entity\UuidTrait;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(security: "is_granted('user:roles:viewown')"),
|
||||
new Post(security: "is_granted('user:roles:create')"),
|
||||
new Get(security: "is_granted('user:roles:viewown')"),
|
||||
new Put(security: "is_granted('user:roles:editown')"),
|
||||
new Patch(security: "is_granted('user:roles:editother')"),
|
||||
new Delete(security: "is_granted('user:roles:deleteown')"),
|
||||
],
|
||||
normalizationContext: [
|
||||
'groups' => ['permission:read'],
|
||||
'swagger_definition_name' => 'Read',
|
||||
],
|
||||
denormalizationContext: [
|
||||
'groups' => ['permission:write'],
|
||||
'swagger_definition_name' => 'Write',
|
||||
]
|
||||
)]
|
||||
class Permission implements CacheInvalidateInterface, UuidInterface
|
||||
{
|
||||
use UuidTrait;
|
||||
|
||||
public const CACHE_NAMESPACE = 'Permission';
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['permission:read', 'role:read'])]
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['permission:read', 'permission:write', 'role:read'])]
|
||||
protected $bundle;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['permission:read', 'permission:write', 'role:read'])]
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* @var Role
|
||||
*/
|
||||
#[Groups(['permission:read', 'permission:write', 'role:read'])]
|
||||
protected $role;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['permission:read', 'permission:write', 'role:read'])]
|
||||
protected $bitwise;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('permissions')
|
||||
->setCustomRepositoryClass(PermissionRepository::class)
|
||||
->addUniqueConstraint(['bundle', 'name', 'role_id'], 'unique_perm');
|
||||
|
||||
$builder->addId();
|
||||
|
||||
$builder->createField('bundle', 'string')
|
||||
->length(50)
|
||||
->build();
|
||||
|
||||
$builder->createField('name', 'string')
|
||||
->length(50)
|
||||
->build();
|
||||
|
||||
$builder->createManyToOne('role', 'Role')
|
||||
->inversedBy('permissions')
|
||||
->addJoinColumn('role_id', 'id', false, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
$builder->addField('bitwise', 'integer');
|
||||
|
||||
static::addUuidField($builder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set bundle.
|
||||
*
|
||||
* @param string $bundle
|
||||
*
|
||||
* @return Permission
|
||||
*/
|
||||
public function setBundle($bundle)
|
||||
{
|
||||
$this->bundle = $bundle;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bundle.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getBundle()
|
||||
{
|
||||
return $this->bundle;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set bitwise.
|
||||
*
|
||||
* @param int $bitwise
|
||||
*
|
||||
* @return Permission
|
||||
*/
|
||||
public function setBitwise($bitwise)
|
||||
{
|
||||
$this->bitwise = $bitwise;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bitwise.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getBitwise()
|
||||
{
|
||||
return $this->bitwise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set role.
|
||||
*
|
||||
* @return Permission
|
||||
*/
|
||||
public function setRole(?Role $role = null)
|
||||
{
|
||||
$this->role = $role;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get role.
|
||||
*
|
||||
* @return Role
|
||||
*/
|
||||
public function getRole()
|
||||
{
|
||||
return $this->role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set name.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return Permission
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getCacheNamespacesToDelete(): array
|
||||
{
|
||||
return [self::CACHE_NAMESPACE];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Query;
|
||||
use Mautic\CoreBundle\Cache\ResultCacheHelper;
|
||||
use Mautic\CoreBundle\Cache\ResultCacheOptions;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Permission>
|
||||
*/
|
||||
class PermissionRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* Delete all permissions for a specific role.
|
||||
*/
|
||||
public function purgeRolePermissions(Role $role): void
|
||||
{
|
||||
$query = $this
|
||||
->createQueryBuilder('p')
|
||||
->delete(Permission::class, 'p')
|
||||
->where('p.role = :role')
|
||||
->setParameter('role', $role)
|
||||
->getQuery();
|
||||
$query->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves array of permissions for a set role. If $forForm, then the array will contain.
|
||||
*
|
||||
* @param bool $forForm
|
||||
*/
|
||||
public function getPermissionsByRole(Role $role, $forForm = false): array
|
||||
{
|
||||
$query = $this
|
||||
->createQueryBuilder('p')
|
||||
->where('p.role = :role')
|
||||
->orderBy('p.bundle')
|
||||
->setParameter(':role', $role)
|
||||
->getQuery();
|
||||
ResultCacheHelper::enableOrmQueryCache($query, new ResultCacheOptions(Permission::CACHE_NAMESPACE));
|
||||
$results = $query->getResult(Query::HYDRATE_ARRAY);
|
||||
|
||||
// rearrange the array to meet needs
|
||||
$permissions = [];
|
||||
foreach ($results as $r) {
|
||||
if ($forForm) {
|
||||
$permissions[$r['bundle']][$r['id']] = [
|
||||
'name' => $r['name'],
|
||||
'bitwise' => $r['bitwise'],
|
||||
];
|
||||
} else {
|
||||
$permissions[$r['bundle']][$r['name']] = $r['bitwise'];
|
||||
}
|
||||
}
|
||||
|
||||
return $permissions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\CacheInvalidateInterface;
|
||||
use Mautic\CoreBundle\Entity\FormEntity;
|
||||
use Mautic\CoreBundle\Entity\UuidInterface;
|
||||
use Mautic\CoreBundle\Entity\UuidTrait;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(security: "is_granted('user:roles:viewown')"),
|
||||
new Post(security: "is_granted('user:roles:create')"),
|
||||
new Get(security: "is_granted('user:roles:viewown')"),
|
||||
new Put(security: "is_granted('user:roles:editown')"),
|
||||
new Patch(security: "is_granted('user:roles:editother')"),
|
||||
new Delete(security: "is_granted('user:roles:deleteown')"),
|
||||
],
|
||||
normalizationContext: [
|
||||
'groups' => ['role:read'],
|
||||
'swagger_definition_name' => 'Read',
|
||||
'api_included' => ['permissions'],
|
||||
],
|
||||
denormalizationContext: [
|
||||
'groups' => ['role:write'],
|
||||
'swagger_definition_name' => 'Write',
|
||||
]
|
||||
)]
|
||||
class Role extends FormEntity implements CacheInvalidateInterface, UuidInterface
|
||||
{
|
||||
use UuidTrait;
|
||||
public const CACHE_NAMESPACE = 'Role';
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
#[Groups(['role:read'])]
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['role:read', 'role:write'])]
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['role:read', 'role:write'])]
|
||||
private $description;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
#[Groups(['role:read', 'role:write'])]
|
||||
private $isAdmin = false;
|
||||
|
||||
/**
|
||||
* @var ArrayCollection<int, Permission>
|
||||
*/
|
||||
#[Groups(['role:read', 'role:write'])]
|
||||
private $permissions;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
#[Groups(['role:read', 'role:write'])]
|
||||
private $rawPermissions;
|
||||
|
||||
/**
|
||||
* @var ArrayCollection<int, User>
|
||||
*/
|
||||
private $users;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->permissions = new ArrayCollection();
|
||||
$this->users = new ArrayCollection();
|
||||
}
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('roles')
|
||||
->setCustomRepositoryClass(RoleRepository::class);
|
||||
|
||||
$builder->addIdColumns();
|
||||
|
||||
$builder->createField('isAdmin', 'boolean')
|
||||
->columnName('is_admin')
|
||||
->build();
|
||||
|
||||
$builder->createOneToMany('permissions', 'Permission')
|
||||
->orphanRemoval()
|
||||
->mappedBy('role')
|
||||
->cascadePersist()
|
||||
->cascadeRemove()
|
||||
->fetchExtraLazy()
|
||||
->build();
|
||||
|
||||
$builder->createField('rawPermissions', 'array')
|
||||
->columnName('readable_permissions')
|
||||
->build();
|
||||
|
||||
$builder->createOneToMany('users', 'User')
|
||||
->mappedBy('role')
|
||||
->fetchExtraLazy()
|
||||
->build();
|
||||
|
||||
static::addUuidField($builder);
|
||||
}
|
||||
|
||||
public static function loadValidatorMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->addPropertyConstraint('name', new Assert\NotBlank(
|
||||
['message' => 'mautic.core.name.required']
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('role')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'name',
|
||||
'description',
|
||||
'isAdmin',
|
||||
'rawPermissions',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get id.
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set name.
|
||||
*
|
||||
* @param string $name
|
||||
*
|
||||
* @return Role
|
||||
*/
|
||||
public function setName($name)
|
||||
{
|
||||
$this->isChanged('name', $name);
|
||||
$this->name = $name;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getName()
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add permissions.
|
||||
*
|
||||
* @return Role
|
||||
*/
|
||||
public function addPermission(Permission $permissions)
|
||||
{
|
||||
$permissions->setRole($this);
|
||||
|
||||
$this->permissions[] = $permissions;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove permissions.
|
||||
*/
|
||||
public function removePermission(Permission $permissions): void
|
||||
{
|
||||
$this->permissions->removeElement($permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get permissions.
|
||||
*
|
||||
* @return \Doctrine\Common\Collections\Collection
|
||||
*/
|
||||
public function getPermissions()
|
||||
{
|
||||
return $this->permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set description.
|
||||
*
|
||||
* @param string $description
|
||||
*
|
||||
* @return Role
|
||||
*/
|
||||
public function setDescription($description)
|
||||
{
|
||||
$this->isChanged('description', $description);
|
||||
$this->description = $description;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get description.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getDescription()
|
||||
{
|
||||
return $this->description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set isAdmin.
|
||||
*
|
||||
* @param bool $isAdmin
|
||||
*
|
||||
* @return Role
|
||||
*/
|
||||
public function setIsAdmin($isAdmin)
|
||||
{
|
||||
$this->isChanged('isAdmin', $isAdmin);
|
||||
$this->isAdmin = $isAdmin;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get isAdmin.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function getIsAdmin()
|
||||
{
|
||||
return $this->isAdmin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get isAdmin.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isAdmin()
|
||||
{
|
||||
return $this->getIsAdmin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Simply used to store a readable format of permissions for the changelog.
|
||||
*/
|
||||
public function setRawPermissions(array $permissions): void
|
||||
{
|
||||
$this->isChanged('rawPermissions', $permissions);
|
||||
$this->rawPermissions = $permissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rawPermissions.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getRawPermissions()
|
||||
{
|
||||
return $this->rawPermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add users.
|
||||
*
|
||||
* @return Role
|
||||
*/
|
||||
public function addUser(User $users)
|
||||
{
|
||||
$this->users[] = $users;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove users.
|
||||
*/
|
||||
public function removeUser(User $users): void
|
||||
{
|
||||
$this->users->removeElement($users);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get users.
|
||||
*
|
||||
* @return \Doctrine\Common\Collections\Collection
|
||||
*/
|
||||
public function getUsers()
|
||||
{
|
||||
return $this->users;
|
||||
}
|
||||
|
||||
public function getCacheNamespacesToDelete(): array
|
||||
{
|
||||
return [
|
||||
self::CACHE_NAMESPACE,
|
||||
User::CACHE_NAMESPACE,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<Role>
|
||||
*/
|
||||
class RoleRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* Get a list of roles.
|
||||
*
|
||||
* @return Paginator
|
||||
*/
|
||||
public function getEntities(array $args = [])
|
||||
{
|
||||
$q = $this->createQueryBuilder('r');
|
||||
|
||||
$args['qb'] = $q;
|
||||
|
||||
return parent::getEntities($args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of roles.
|
||||
*
|
||||
* @param string $search
|
||||
* @param int $limit
|
||||
* @param int $start
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getRoleList($search = '', $limit = 10, $start = 0)
|
||||
{
|
||||
$q = $this->_em->createQueryBuilder();
|
||||
|
||||
$q->select('partial r.{id, name}')
|
||||
->from(Role::class, 'r');
|
||||
|
||||
if (!empty($search)) {
|
||||
$q->where('r.name LIKE :search')
|
||||
->setParameter('search', "{$search}%");
|
||||
}
|
||||
|
||||
$q->orderBy('r.name');
|
||||
|
||||
if (!empty($limit)) {
|
||||
$q->setFirstResult($start)
|
||||
->setMaxResults($limit);
|
||||
}
|
||||
|
||||
return $q->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
protected function addCatchAllWhereClause($q, $filter): array
|
||||
{
|
||||
return $this->addStandardCatchAllWhereClause(
|
||||
$q,
|
||||
$filter,
|
||||
[
|
||||
'r.name',
|
||||
'r.description',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
protected function addSearchCommandWhereClause($q, $filter): array
|
||||
{
|
||||
$command = $filter->command;
|
||||
$unique = $this->generateRandomParameterName();
|
||||
$returnParameter = false; // returning a parameter that is not used will lead to a Doctrine error
|
||||
[$expr, $parameters] = parent::addSearchCommandWhereClause($q, $filter);
|
||||
|
||||
switch ($command) {
|
||||
case $this->translator->trans('mautic.user.user.searchcommand.isadmin'):
|
||||
case $this->translator->trans('mautic.user.user.searchcommand.isadmin', [], null, 'en_US'):
|
||||
$expr = $q->expr()->eq('r.isAdmin', 1);
|
||||
break;
|
||||
case $this->translator->trans('mautic.core.searchcommand.name'):
|
||||
case $this->translator->trans('mautic.core.searchcommand.name', [], null, 'en_US'):
|
||||
$expr = $q->expr()->like('r.name', ':'.$unique);
|
||||
$returnParameter = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ($filter->not) {
|
||||
$expr = $q->expr()->not($expr);
|
||||
}
|
||||
|
||||
if ($returnParameter) {
|
||||
$string = ($filter->strict) ? $filter->string : "%{$filter->string}%";
|
||||
$parameters = ["$unique" => $string];
|
||||
}
|
||||
|
||||
return [
|
||||
$expr,
|
||||
$parameters,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a count of users that belong to the role.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getUserCount($roleIds)
|
||||
{
|
||||
$q = $this->_em->getConnection()->createQueryBuilder();
|
||||
|
||||
$q->select('count(u.id) as thecount, u.role_id')
|
||||
->from(MAUTIC_TABLE_PREFIX.'users', 'u');
|
||||
|
||||
$returnArray = is_array($roleIds);
|
||||
|
||||
if (!$returnArray) {
|
||||
$roleIds = [$roleIds];
|
||||
}
|
||||
|
||||
$q->where(
|
||||
$q->expr()->in('u.role_id', $roleIds)
|
||||
)
|
||||
->groupBy('u.role_id');
|
||||
|
||||
$result = $q->executeQuery()->fetchAllAssociative();
|
||||
|
||||
$return = [];
|
||||
foreach ($result as $r) {
|
||||
$return[$r['role_id']] = $r['thecount'];
|
||||
}
|
||||
|
||||
// Ensure lists without leads have a value
|
||||
foreach ($roleIds as $r) {
|
||||
if (!isset($return[$r])) {
|
||||
$return[$r] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return ($returnArray) ? $return : $return[$roleIds[0]];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSearchCommands(): array
|
||||
{
|
||||
$commands = [
|
||||
'mautic.user.user.searchcommand.isadmin',
|
||||
'mautic.core.searchcommand.name',
|
||||
];
|
||||
|
||||
return array_merge($commands, parent::getSearchCommands());
|
||||
}
|
||||
|
||||
protected function getDefaultOrder(): array
|
||||
{
|
||||
return [
|
||||
['r.name', 'ASC'],
|
||||
];
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'r';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,841 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\ApiBundle\Serializer\Driver\ApiMetadataDriver;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
use Mautic\CoreBundle\Entity\CacheInvalidateInterface;
|
||||
use Mautic\CoreBundle\Entity\FormEntity;
|
||||
use Mautic\UserBundle\Form\Validator\Constraints\NotWeak;
|
||||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Form\Form;
|
||||
use Symfony\Component\Security\Core\User\EquatableInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'User',
|
||||
operations: [
|
||||
new GetCollection(uriTemplate: '/users', security: "is_granted('user:users:viewown')"),
|
||||
new Post(uriTemplate: '/users', security: "is_granted('user:users:create')", processor: \Mautic\UserBundle\ApiPlatform\UserProcessor::class),
|
||||
new Get(uriTemplate: '/users/{id}', security: "is_granted('user:users:viewown')"),
|
||||
new Put(uriTemplate: '/users/{id}', security: "is_granted('user:users:editown')", processor: \Mautic\UserBundle\ApiPlatform\UserProcessor::class),
|
||||
new Patch(uriTemplate: '/users/{id}', security: "is_granted('user:users:editother')", processor: \Mautic\UserBundle\ApiPlatform\UserProcessor::class),
|
||||
new Delete(uriTemplate: '/users/{id}', security: "is_granted('user:users:deleteown')"),
|
||||
],
|
||||
normalizationContext: [
|
||||
'groups' => ['user:read'],
|
||||
'swagger_definition_name' => 'Read',
|
||||
],
|
||||
denormalizationContext: [
|
||||
'groups' => ['user:write'],
|
||||
'swagger_definition_name' => 'Write',
|
||||
]
|
||||
)]
|
||||
class User extends FormEntity implements UserInterface, EquatableInterface, PasswordAuthenticatedUserInterface, CacheInvalidateInterface
|
||||
{
|
||||
public const CACHE_NAMESPACE = 'User';
|
||||
|
||||
/**
|
||||
* @var ?int
|
||||
*/
|
||||
#[Groups(['user:read'])]
|
||||
protected $id;
|
||||
|
||||
#[Groups(['user:read', 'user:write'])]
|
||||
protected ?string $username = null;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $password;
|
||||
|
||||
/**
|
||||
* Used for when updating the password.
|
||||
*
|
||||
* @var ?string
|
||||
*/
|
||||
#[Groups(['user:write'])]
|
||||
private $plainPassword;
|
||||
|
||||
/**
|
||||
* Used for updating account.
|
||||
*
|
||||
* @var ?string
|
||||
*/
|
||||
private $currentPassword;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['user:read', 'user:write'])]
|
||||
private $firstName;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['user:read', 'user:write'])]
|
||||
private $lastName;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
#[Groups(['user:read', 'user:write'])]
|
||||
private $email;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['user:read', 'user:write'])]
|
||||
private $position;
|
||||
|
||||
/**
|
||||
* @var Role
|
||||
*/
|
||||
#[Groups(['user:read', 'user:write'])]
|
||||
private $role;
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['user:read', 'user:write'])]
|
||||
private $timezone = '';
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['user:read', 'user:write'])]
|
||||
private $locale = '';
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
#[Groups(['user:read'])]
|
||||
private $lastLogin;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface
|
||||
*/
|
||||
#[Groups(['user:read'])]
|
||||
private $lastActive;
|
||||
|
||||
/**
|
||||
* Stores active role permissions.
|
||||
*/
|
||||
private $activePermissions;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
#[Groups(['user:read', 'user:write'])]
|
||||
private $preferences = [];
|
||||
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
#[Groups(['user:read', 'user:write'])]
|
||||
private $signature;
|
||||
|
||||
/**
|
||||
* @param bool $guest
|
||||
*/
|
||||
public function __construct(
|
||||
private $guest = false,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('users')
|
||||
->setCustomRepositoryClass(UserRepository::class);
|
||||
|
||||
$builder->addId();
|
||||
|
||||
$builder->createField('username', 'string')
|
||||
->length(191)
|
||||
->unique()
|
||||
->build();
|
||||
|
||||
$builder->createField('password', 'string')
|
||||
->length(64)
|
||||
->build();
|
||||
|
||||
$builder->createField('firstName', 'string')
|
||||
->columnName('first_name')
|
||||
->length(191)
|
||||
->build();
|
||||
|
||||
$builder->createField('lastName', 'string')
|
||||
->columnName('last_name')
|
||||
->length(191)
|
||||
->build();
|
||||
|
||||
$builder->createField('email', 'string')
|
||||
->length(191)
|
||||
->unique()
|
||||
->build();
|
||||
|
||||
$builder->createField('position', 'string')
|
||||
->length(191)
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createManyToOne('role', 'Role')
|
||||
->inversedBy('users')
|
||||
->cascadeMerge()
|
||||
->addJoinColumn('role_id', 'id', false)
|
||||
->build();
|
||||
|
||||
$builder->createField('timezone', 'string')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('locale', 'string')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('lastLogin', 'datetime')
|
||||
->columnName('last_login')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('lastActive', 'datetime')
|
||||
->columnName('last_active')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('preferences', 'array')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('signature', 'text')
|
||||
->nullable()
|
||||
->build();
|
||||
}
|
||||
|
||||
public static function loadValidatorMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->addPropertyConstraint('username', new Assert\NotBlank(
|
||||
['message' => 'mautic.user.user.username.notblank']
|
||||
));
|
||||
|
||||
$metadata->addConstraint(new UniqueEntity(
|
||||
[
|
||||
'fields' => ['username'],
|
||||
'message' => 'mautic.user.user.username.unique',
|
||||
'repositoryMethod' => 'checkUniqueUsernameEmail',
|
||||
]
|
||||
));
|
||||
|
||||
$metadata->addPropertyConstraint('firstName', new Assert\NotBlank(
|
||||
['message' => 'mautic.user.user.firstname.notblank']
|
||||
));
|
||||
|
||||
$metadata->addPropertyConstraint('lastName', new Assert\NotBlank(
|
||||
['message' => 'mautic.user.user.lastname.notblank']
|
||||
));
|
||||
|
||||
$metadata->addPropertyConstraint('email', new Assert\NotBlank(
|
||||
['message' => 'mautic.user.user.email.valid']
|
||||
));
|
||||
|
||||
$metadata->addPropertyConstraint('email', new Assert\Email(
|
||||
[
|
||||
'message' => 'mautic.user.user.email.valid',
|
||||
'groups' => ['SecondPass'],
|
||||
]
|
||||
));
|
||||
|
||||
$metadata->addConstraint(new UniqueEntity(
|
||||
[
|
||||
'fields' => ['email'],
|
||||
'message' => 'mautic.user.user.email.unique',
|
||||
'repositoryMethod' => 'checkUniqueUsernameEmail',
|
||||
]
|
||||
));
|
||||
|
||||
$metadata->addPropertyConstraint('role', new Assert\NotBlank(
|
||||
['message' => 'mautic.user.user.role.notblank']
|
||||
));
|
||||
|
||||
$metadata->addPropertyConstraint('plainPassword', new Assert\NotBlank(
|
||||
[
|
||||
'message' => 'mautic.user.user.password.notblank',
|
||||
'groups' => ['CheckPasswordNotBlank'],
|
||||
]
|
||||
));
|
||||
|
||||
$metadata->addPropertyConstraint('plainPassword', new Assert\Length(
|
||||
[
|
||||
'min' => 6,
|
||||
'minMessage' => 'mautic.user.user.password.minlength',
|
||||
'groups' => ['CheckPassword'],
|
||||
]
|
||||
));
|
||||
|
||||
$metadata->addPropertyConstraint('plainPassword', new NotWeak(
|
||||
[
|
||||
'message' => 'mautic.user.user.password.weak',
|
||||
'groups' => ['CheckPassword'],
|
||||
]
|
||||
));
|
||||
|
||||
$metadata->setGroupSequence(['User', 'SecondPass', 'CheckPassword']);
|
||||
}
|
||||
|
||||
public static function determineValidationGroups(Form $form): array
|
||||
{
|
||||
$data = $form->getData();
|
||||
$groups = ['User', 'SecondPass'];
|
||||
if ($data instanceof User) {
|
||||
$isNewUser = !$data->getId();
|
||||
$hasPlainPassword = !empty($data->getPlainPassword());
|
||||
|
||||
if ($isNewUser) {
|
||||
$groups[] = $hasPlainPassword ? 'CheckPassword' : 'CheckPasswordNotBlank';
|
||||
} elseif ($hasPlainPassword) {
|
||||
$groups[] = 'CheckPassword';
|
||||
}
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the metadata for API usage.
|
||||
*/
|
||||
public static function loadApiMetadata(ApiMetadataDriver $metadata): void
|
||||
{
|
||||
$metadata->setGroupPrefix('user')
|
||||
->addListProperties(
|
||||
[
|
||||
'id',
|
||||
'username',
|
||||
'firstName',
|
||||
'lastName',
|
||||
]
|
||||
)
|
||||
->addProperties(
|
||||
[
|
||||
'email',
|
||||
'position',
|
||||
'role',
|
||||
'timezone',
|
||||
'locale',
|
||||
'lastLogin',
|
||||
'lastActive',
|
||||
'signature',
|
||||
]
|
||||
)
|
||||
->build();
|
||||
}
|
||||
|
||||
protected function isChanged($prop, $val)
|
||||
{
|
||||
$getter = 'get'.ucfirst($prop);
|
||||
$current = $this->$getter();
|
||||
if ('role' == $prop) {
|
||||
if ($current && !$val) {
|
||||
$this->changes['role'] = [$current->getName().' ('.$current->getId().')', $val];
|
||||
} elseif (!$this->role && $val) {
|
||||
$this->changes['role'] = [$current, $val->getName().' ('.$val->getId().')'];
|
||||
} elseif ($current && $val && $current->getId() != $val->getId()) {
|
||||
$this->changes['role'] = [
|
||||
$current->getName().'('.$current->getId().')',
|
||||
$val->getName().'('.$val->getId().')',
|
||||
];
|
||||
}
|
||||
} else {
|
||||
parent::isChanged($prop, $val);
|
||||
}
|
||||
}
|
||||
|
||||
public function getUsername(): ?string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
public function getUserIdentifier(): string
|
||||
{
|
||||
return $this->username ?? '';
|
||||
}
|
||||
|
||||
public function getSalt(): ?string
|
||||
{
|
||||
// bcrypt generates its own salt
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get plain password.
|
||||
*
|
||||
* @return ?string
|
||||
*/
|
||||
public function getPlainPassword()
|
||||
{
|
||||
return $this->plainPassword;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current password (that a user has typed into a form).
|
||||
*
|
||||
* @return ?string
|
||||
*/
|
||||
public function getCurrentPassword()
|
||||
{
|
||||
return $this->currentPassword;
|
||||
}
|
||||
|
||||
public function getRoles(): array
|
||||
{
|
||||
$roles = [];
|
||||
|
||||
if ($this->username) {
|
||||
$roles = [
|
||||
($this->isAdmin()) ? 'ROLE_ADMIN' : 'ROLE_USER',
|
||||
];
|
||||
|
||||
if (defined('MAUTIC_API_REQUEST') && MAUTIC_API_REQUEST) {
|
||||
$roles[] = 'ROLE_API';
|
||||
}
|
||||
}
|
||||
|
||||
return $roles;
|
||||
}
|
||||
|
||||
#[\Deprecated]
|
||||
public function eraseCredentials(): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
public function __serialize(): array
|
||||
{
|
||||
$this->plainPassword = null;
|
||||
$this->currentPassword = null;
|
||||
|
||||
return [
|
||||
$this->id,
|
||||
$this->username,
|
||||
$this->password,
|
||||
$this->isPublished(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $data
|
||||
*/
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
[
|
||||
$this->id,
|
||||
$this->username,
|
||||
$this->password,
|
||||
$published,
|
||||
] = $data;
|
||||
$this->setIsPublished($published);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?int
|
||||
*/
|
||||
public function getId()
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set username.
|
||||
*
|
||||
* @param string $username
|
||||
*
|
||||
* @return User
|
||||
*/
|
||||
public function setUsername($username)
|
||||
{
|
||||
$this->isChanged('username', $username);
|
||||
$this->username = $username;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set password.
|
||||
*
|
||||
* @param string $password
|
||||
*
|
||||
* @return User
|
||||
*/
|
||||
public function setPassword($password)
|
||||
{
|
||||
$this->password = $password;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set plain password.
|
||||
*
|
||||
* @return User
|
||||
*/
|
||||
public function setPlainPassword($plainPassword)
|
||||
{
|
||||
$this->plainPassword = $plainPassword;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set current password.
|
||||
*
|
||||
* @return User
|
||||
*/
|
||||
public function setCurrentPassword($currentPassword)
|
||||
{
|
||||
$this->currentPassword = $currentPassword;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set firstName.
|
||||
*
|
||||
* @param string $firstName
|
||||
*
|
||||
* @return User
|
||||
*/
|
||||
public function setFirstName($firstName)
|
||||
{
|
||||
$this->isChanged('firstName', $firstName);
|
||||
$this->firstName = $firstName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get firstName.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFirstName()
|
||||
{
|
||||
return $this->firstName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set lastName.
|
||||
*
|
||||
* @param string $lastName
|
||||
*
|
||||
* @return User
|
||||
*/
|
||||
public function setLastName($lastName)
|
||||
{
|
||||
$this->isChanged('lastName', $lastName);
|
||||
$this->lastName = $lastName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get lastName.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLastName()
|
||||
{
|
||||
return $this->lastName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get full name.
|
||||
*
|
||||
* @param bool $lastFirst
|
||||
*/
|
||||
public function getName($lastFirst = false): string
|
||||
{
|
||||
return ($lastFirst) ? $this->lastName.', '.$this->firstName : $this->firstName.' '.$this->lastName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set email.
|
||||
*
|
||||
* @param string $email
|
||||
*
|
||||
* @return User
|
||||
*/
|
||||
public function setEmail($email)
|
||||
{
|
||||
$this->isChanged('email', $email);
|
||||
$this->email = $email;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get email.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getEmail()
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set role.
|
||||
*
|
||||
* @return User
|
||||
*/
|
||||
public function setRole(?Role $role = null)
|
||||
{
|
||||
$this->isChanged('role', $role);
|
||||
$this->role = $role;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get role.
|
||||
*
|
||||
* @return Role
|
||||
*/
|
||||
public function getRole()
|
||||
{
|
||||
return $this->role;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set active permissions.
|
||||
*
|
||||
* @return User
|
||||
*/
|
||||
public function setActivePermissions(array $permissions)
|
||||
{
|
||||
$this->activePermissions = $permissions;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active permissions.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function getActivePermissions()
|
||||
{
|
||||
return $this->activePermissions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set position.
|
||||
*
|
||||
* @param string $position
|
||||
*
|
||||
* @return User
|
||||
*/
|
||||
public function setPosition($position)
|
||||
{
|
||||
$this->isChanged('position', $position);
|
||||
$this->position = $position;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get position.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPosition()
|
||||
{
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set timezone.
|
||||
*
|
||||
* @param string $timezone
|
||||
*
|
||||
* @return User
|
||||
*/
|
||||
public function setTimezone($timezone)
|
||||
{
|
||||
$this->isChanged('timezone', $timezone);
|
||||
$this->timezone = $timezone;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get timezone.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTimezone()
|
||||
{
|
||||
return $this->timezone;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User
|
||||
*/
|
||||
public function setLocale(?string $locale)
|
||||
{
|
||||
$this->isChanged('locale', $locale);
|
||||
$this->locale = $locale;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get locale.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getLocale()
|
||||
{
|
||||
return $this->locale;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if user is admin.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isAdmin()
|
||||
{
|
||||
if (null !== $this->role) {
|
||||
return $this->role->isAdmin();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLastLogin()
|
||||
{
|
||||
return $this->lastLogin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $lastLogin
|
||||
*/
|
||||
public function setLastLogin($lastLogin = null): void
|
||||
{
|
||||
if (empty($lastLogin)) {
|
||||
$lastLogin = new \DateTime();
|
||||
}
|
||||
$this->lastLogin = $lastLogin;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getLastActive()
|
||||
{
|
||||
return $this->lastActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $lastActive
|
||||
*/
|
||||
public function setLastActive($lastActive = null): void
|
||||
{
|
||||
if (empty($lastActive)) {
|
||||
$lastActive = new \DateTime();
|
||||
}
|
||||
$this->lastActive = $lastActive;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
public function getPreferences()
|
||||
{
|
||||
return $this->preferences;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $preferences
|
||||
*/
|
||||
public function setPreferences(array $preferences): void
|
||||
{
|
||||
$this->preferences = $preferences;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set signature.
|
||||
*
|
||||
* @param string $signature
|
||||
*
|
||||
* @return User
|
||||
*/
|
||||
public function setSignature($signature)
|
||||
{
|
||||
$this->isChanged('signature', $signature);
|
||||
$this->signature = $signature;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get signature.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getSignature()
|
||||
{
|
||||
return $this->signature;
|
||||
}
|
||||
|
||||
/**
|
||||
* Needed for SAML to work correctly.
|
||||
*/
|
||||
public function isEqualTo(UserInterface $user): bool
|
||||
{
|
||||
if (!$user instanceof self) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$thisUser = $this->getId().$this->getUserIdentifier().$this->getPassword();
|
||||
$thatUser = $user->getId().$user->getUserIdentifier().$user->getPassword();
|
||||
|
||||
return $thisUser === $thatUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isGuest()
|
||||
{
|
||||
return $this->guest;
|
||||
}
|
||||
|
||||
public function getCacheNamespacesToDelete(): array
|
||||
{
|
||||
return [self::CACHE_NAMESPACE];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Entity;
|
||||
|
||||
use Doctrine\DBAL\Query\QueryBuilder;
|
||||
use Doctrine\ORM\Tools\Pagination\Paginator;
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
use Mautic\CoreBundle\Helper\DateTimeHelper;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<User>
|
||||
*/
|
||||
class UserRepository extends CommonRepository
|
||||
{
|
||||
/**
|
||||
* Find user by username or email.
|
||||
*/
|
||||
public function findByIdentifier(string $identifier): ?User
|
||||
{
|
||||
$q = $this->createQueryBuilder('u')
|
||||
->where('u.username = :identifier OR u.email = :identifier')
|
||||
->setParameter('identifier', $identifier);
|
||||
|
||||
$result = $q->getQuery()->getResult();
|
||||
|
||||
return (!empty($result)) ? $result[0] : null;
|
||||
}
|
||||
|
||||
public function setLastLogin($user): void
|
||||
{
|
||||
$now = new DateTimeHelper();
|
||||
$datetime = $now->toUtcString();
|
||||
$conn = $this->_em->getConnection();
|
||||
$conn->update(MAUTIC_TABLE_PREFIX.'users', [
|
||||
'last_login' => $datetime,
|
||||
'last_active' => $datetime,
|
||||
], ['id' => (int) $user->getId()]);
|
||||
}
|
||||
|
||||
public function setLastActive($user): void
|
||||
{
|
||||
$now = new DateTimeHelper();
|
||||
$conn = $this->_em->getConnection();
|
||||
$conn->update(MAUTIC_TABLE_PREFIX.'users', ['last_active' => $now->toUtcString()], ['id' => (int) $user->getId()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to ensure that a username and/or email is unique.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function checkUniqueUsernameEmail($params)
|
||||
{
|
||||
$q = $this->createQueryBuilder('u');
|
||||
|
||||
if (isset($params['email'])) {
|
||||
$q->where('u.username = :email OR u.email = :email')
|
||||
->setParameter('email', $params['email']);
|
||||
}
|
||||
|
||||
if (isset($params['username'])) {
|
||||
$q->orWhere('u.username = :username OR u.email = :username')
|
||||
->setParameter('username', $params['username']);
|
||||
}
|
||||
|
||||
return $q->getQuery()->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of users.
|
||||
*
|
||||
* @return Paginator
|
||||
*/
|
||||
public function getEntities(array $args = [])
|
||||
{
|
||||
$q = $this
|
||||
->createQueryBuilder('u')
|
||||
->select('u, r')
|
||||
->leftJoin('u.role', 'r');
|
||||
|
||||
$args['qb'] = $q;
|
||||
|
||||
return parent::getEntities($args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of users for an autocomplete input.
|
||||
*
|
||||
* @param string $search
|
||||
* @param int $limit
|
||||
* @param int $start
|
||||
* @param array $permissionLimiter
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getUserList($search = '', $limit = 10, $start = 0, $permissionLimiter = [])
|
||||
{
|
||||
$q = $this->_em->createQueryBuilder();
|
||||
|
||||
$q->select('DISTINCT partial u.{id, firstName, lastName, email}')
|
||||
->from(User::class, 'u')
|
||||
->leftJoin('u.role', 'r')
|
||||
->leftJoin('r.permissions', 'p');
|
||||
|
||||
if (!empty($search)) {
|
||||
$q->where(
|
||||
$q->expr()->orX(
|
||||
$q->expr()->like('u.firstName', ':search'),
|
||||
$q->expr()->like('u.lastName', ':search'),
|
||||
$q->expr()->like(
|
||||
$q->expr()->concat('u.firstName',
|
||||
$q->expr()->concat(
|
||||
$q->expr()->literal(' '),
|
||||
'u.lastName'
|
||||
)
|
||||
),
|
||||
':search'
|
||||
)
|
||||
)
|
||||
)
|
||||
->setParameter('search', "{$search}%");
|
||||
}
|
||||
|
||||
if (!empty($permissionLimiter)) {
|
||||
// only get users with a role that has some sort of access to set permissions
|
||||
$expr = $q->expr()->andX();
|
||||
foreach ($permissionLimiter as $bundle => $level) {
|
||||
$expr->add(
|
||||
$q->expr()->andX(
|
||||
$q->expr()->eq('p.bundle', $q->expr()->literal($bundle)),
|
||||
$q->expr()->eq('p.name', $q->expr()->literal($level))
|
||||
)
|
||||
);
|
||||
}
|
||||
$expr = $q->expr()->orX(
|
||||
$q->expr()->eq('r.isAdmin', ':true'),
|
||||
$expr
|
||||
);
|
||||
$q->andWhere($expr);
|
||||
}
|
||||
|
||||
$q->andWhere('u.isPublished = :true')
|
||||
->setParameter('true', true, 'boolean')
|
||||
->orderBy('u.firstName, u.lastName');
|
||||
|
||||
if (!empty($limit)) {
|
||||
$q->setFirstResult($start)
|
||||
->setMaxResults($limit);
|
||||
}
|
||||
|
||||
return $q->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of Users for formType Choice.
|
||||
*/
|
||||
public function getOwnerListChoices(): array
|
||||
{
|
||||
$q = $this->createQueryBuilder('u');
|
||||
|
||||
$q->select('partial u.{id, firstName, lastName}');
|
||||
|
||||
$q->andWhere('u.isPublished = true')
|
||||
->orderBy('u.firstName, u.lastName');
|
||||
|
||||
$users = $q->getQuery()->getResult();
|
||||
|
||||
$result = [];
|
||||
/** @var User $user */
|
||||
foreach ($users as $user) {
|
||||
$result[$user->getName(true)] = $user->getId();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $search
|
||||
* @param int $limit
|
||||
* @param int $start
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPositionList($search = '', $limit = 10, $start = 0)
|
||||
{
|
||||
$q = $this->_em->createQueryBuilder()
|
||||
->select('u.position')
|
||||
->distinct()
|
||||
->from(User::class, 'u')
|
||||
->where("u.position != ''")
|
||||
->andWhere('u.position IS NOT NULL');
|
||||
if (!empty($search)) {
|
||||
$q->andWhere('u.position LIKE :search')
|
||||
->setParameter('search', "{$search}%");
|
||||
}
|
||||
|
||||
$q->orderBy('u.position');
|
||||
|
||||
if (!empty($limit)) {
|
||||
$q->setFirstResult($start)
|
||||
->setMaxResults($limit);
|
||||
}
|
||||
|
||||
return $q->getQuery()->getArrayResult();
|
||||
}
|
||||
|
||||
protected function addCatchAllWhereClause($q, $filter): array
|
||||
{
|
||||
return $this->addStandardCatchAllWhereClause(
|
||||
$q,
|
||||
$filter,
|
||||
[
|
||||
'u.username',
|
||||
'u.email',
|
||||
'u.firstName',
|
||||
'u.lastName',
|
||||
'u.position',
|
||||
'r.name',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
protected function addSearchCommandWhereClause($q, $filter): array
|
||||
{
|
||||
$command = $filter->command;
|
||||
$unique = $this->generateRandomParameterName();
|
||||
$returnParameter = false; // returning a parameter that is not used will lead to a Doctrine error
|
||||
[$expr, $parameters] = parent::addSearchCommandWhereClause($q, $filter);
|
||||
|
||||
switch ($command) {
|
||||
case $this->translator->trans('mautic.core.searchcommand.ispublished'):
|
||||
case $this->translator->trans('mautic.core.searchcommand.ispublished', [], null, 'en_US'):
|
||||
$expr = $q->expr()->eq('u.isPublished', ":$unique");
|
||||
$forceParameters = [$unique => true];
|
||||
|
||||
break;
|
||||
case $this->translator->trans('mautic.core.searchcommand.isunpublished'):
|
||||
case $this->translator->trans('mautic.core.searchcommand.isunpublished', [], null, 'en_US'):
|
||||
$expr = $q->expr()->eq('u.isPublished', ":$unique");
|
||||
$forceParameters = [$unique => false];
|
||||
|
||||
break;
|
||||
case $this->translator->trans('mautic.user.user.searchcommand.isadmin'):
|
||||
case $this->translator->trans('mautic.user.user.searchcommand.isadmin', [], null, 'en_US'):
|
||||
$expr = $q->expr()->eq('r.isAdmin', ":$unique");
|
||||
$forceParameters = [$unique => true];
|
||||
break;
|
||||
case $this->translator->trans('mautic.core.searchcommand.email'):
|
||||
case $this->translator->trans('mautic.core.searchcommand.email', [], null, 'en_US'):
|
||||
$expr = $q->expr()->like('u.email', ':'.$unique);
|
||||
$returnParameter = true;
|
||||
break;
|
||||
case $this->translator->trans('mautic.user.user.searchcommand.position'):
|
||||
case $this->translator->trans('mautic.user.user.searchcommand.position', [], null, 'en_US'):
|
||||
$expr = $q->expr()->like('u.position', ':'.$unique);
|
||||
$returnParameter = true;
|
||||
break;
|
||||
case $this->translator->trans('mautic.user.user.searchcommand.username'):
|
||||
case $this->translator->trans('mautic.user.user.searchcommand.username', [], null, 'en_US'):
|
||||
$expr = $q->expr()->like('u.username', ':'.$unique);
|
||||
$returnParameter = true;
|
||||
break;
|
||||
case $this->translator->trans('mautic.user.user.searchcommand.role'):
|
||||
case $this->translator->trans('mautic.user.user.searchcommand.role', [], null, 'en_US'):
|
||||
$expr = $q->expr()->like('r.name', ':'.$unique);
|
||||
$returnParameter = true;
|
||||
break;
|
||||
case $this->translator->trans('mautic.core.searchcommand.name'):
|
||||
case $this->translator->trans('mautic.core.searchcommand.name', [], null, 'en_US'):
|
||||
// This if/else can be removed once we upgrade to Dotrine 2.11 as both builders have the or() method there.
|
||||
if ($q instanceof QueryBuilder) {
|
||||
$expr = $q->expr()->or(
|
||||
$q->expr()->like('u.firstName', ':'.$unique),
|
||||
$q->expr()->like('u.lastName', ':'.$unique)
|
||||
);
|
||||
} else {
|
||||
$expr = $q->expr()->orX(
|
||||
$q->expr()->like('u.firstName', ':'.$unique),
|
||||
$q->expr()->like('u.lastName', ':'.$unique)
|
||||
);
|
||||
}
|
||||
$returnParameter = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!empty($forceParameters)) {
|
||||
$parameters = $forceParameters;
|
||||
} elseif ($returnParameter) {
|
||||
$string = ($filter->strict) ? $filter->string : "%{$filter->string}%";
|
||||
$parameters = ["$unique" => $string];
|
||||
}
|
||||
|
||||
return [$expr, $parameters];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getSearchCommands(): array
|
||||
{
|
||||
$commands = [
|
||||
'mautic.core.searchcommand.email',
|
||||
'mautic.core.searchcommand.ispublished',
|
||||
'mautic.core.searchcommand.isunpublished',
|
||||
'mautic.user.user.searchcommand.isadmin',
|
||||
'mautic.core.searchcommand.name',
|
||||
'mautic.user.user.searchcommand.position',
|
||||
'mautic.user.user.searchcommand.role',
|
||||
'mautic.user.user.searchcommand.username',
|
||||
];
|
||||
|
||||
return array_merge($commands, parent::getSearchCommands());
|
||||
}
|
||||
|
||||
protected function getDefaultOrder(): array
|
||||
{
|
||||
return [
|
||||
['u.lastName', 'ASC'],
|
||||
['u.firstName', 'ASC'],
|
||||
['u.username', 'ASC'],
|
||||
];
|
||||
}
|
||||
|
||||
public function getTableAlias(): string
|
||||
{
|
||||
return 'u';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User[]
|
||||
*/
|
||||
public function getAllAdminUsers(): array
|
||||
{
|
||||
return $this->createQueryBuilder('u')
|
||||
->join('u.role', 'r')
|
||||
->where('r.isAdmin = 1')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Mautic\CoreBundle\Doctrine\Mapping\ClassMetadataBuilder;
|
||||
|
||||
class UserToken
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var User
|
||||
*/
|
||||
private $user;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $authorizator;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $secret;
|
||||
|
||||
/**
|
||||
* @var \DateTimeInterface|null
|
||||
*/
|
||||
private $expiration;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $oneTimeOnly = true;
|
||||
|
||||
public static function loadMetadata(ORM\ClassMetadata $metadata): void
|
||||
{
|
||||
$builder = new ClassMetadataBuilder($metadata);
|
||||
|
||||
$builder->setTable('user_tokens')
|
||||
->setCustomRepositoryClass(UserTokenRepository::class);
|
||||
|
||||
$builder->addId();
|
||||
|
||||
$builder->createManyToOne('user', User::class)
|
||||
->addJoinColumn('user_id', 'id', false, false, 'CASCADE')
|
||||
->build();
|
||||
|
||||
$builder->createField('authorizator', 'string')
|
||||
->length(32)
|
||||
->build();
|
||||
|
||||
$builder->createField('secret', 'string')
|
||||
->length(120)
|
||||
->unique()
|
||||
->build();
|
||||
|
||||
$builder->createField('expiration', 'datetime')
|
||||
->nullable()
|
||||
->build();
|
||||
|
||||
$builder->createField('oneTimeOnly', 'boolean')
|
||||
->columnName('one_time_only')
|
||||
->build();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User $user
|
||||
*
|
||||
* @return UserToken
|
||||
*/
|
||||
public function setUser($user)
|
||||
{
|
||||
$this->user = $user;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getAuthorizator()
|
||||
{
|
||||
return $this->authorizator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $authorizator
|
||||
*
|
||||
* @return UserToken
|
||||
*/
|
||||
public function setAuthorizator($authorizator)
|
||||
{
|
||||
$this->authorizator = $authorizator;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getSecret()
|
||||
{
|
||||
return $this->secret;
|
||||
}
|
||||
|
||||
/**
|
||||
* Use \Mautic\UserBundle\Entity\UserTokenRepositoryInterface::generateSecret to get valid secret.
|
||||
*
|
||||
* @param string $secret
|
||||
*
|
||||
* @return UserToken
|
||||
*/
|
||||
public function setSecret($secret)
|
||||
{
|
||||
$this->secret = $secret;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \DateTimeInterface|null
|
||||
*/
|
||||
public function getExpiration()
|
||||
{
|
||||
return $this->expiration;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param \DateTime|null $expiration
|
||||
*
|
||||
* @return UserToken
|
||||
*/
|
||||
public function setExpiration($expiration = null)
|
||||
{
|
||||
$this->expiration = $expiration;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isOneTimeOnly()
|
||||
{
|
||||
return $this->oneTimeOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bool $oneTimeOnly
|
||||
*
|
||||
* @return UserToken
|
||||
*/
|
||||
public function setOneTimeOnly($oneTimeOnly = true)
|
||||
{
|
||||
$this->oneTimeOnly = $oneTimeOnly;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Entity;
|
||||
|
||||
use Mautic\CoreBundle\Entity\CommonRepository;
|
||||
|
||||
/**
|
||||
* @extends CommonRepository<UserToken>
|
||||
*/
|
||||
final class UserTokenRepository extends CommonRepository implements UserTokenRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* @param string $secret
|
||||
*/
|
||||
public function isSecretUnique($secret): bool
|
||||
{
|
||||
$tokens = $this->createQueryBuilder('ut')
|
||||
->where('ut.secret = :secret')
|
||||
->setParameter('secret', $secret)
|
||||
->setMaxResults(1)
|
||||
->getQuery()->execute();
|
||||
|
||||
return 0 === count($tokens);
|
||||
}
|
||||
|
||||
public function verify(UserToken $token): bool
|
||||
{
|
||||
/** @var UserToken[] $userTokens */
|
||||
$userTokens = $this->createQueryBuilder('ut')
|
||||
->where('ut.user = :user AND ut.authorizator = :authorizator AND ut.secret = :secret AND (ut.expiration IS NULL OR ut.expiration >= :now)')
|
||||
->setParameter('user', $token->getUser())
|
||||
->setParameter('authorizator', $token->getAuthorizator())
|
||||
->setParameter('secret', $token->getSecret())
|
||||
->setParameter('now', new \DateTime())
|
||||
->setMaxResults(1)
|
||||
->getQuery()->execute();
|
||||
$verified = (0 !== count($userTokens));
|
||||
if (false === $verified) {
|
||||
return false;
|
||||
}
|
||||
$userToken = reset($userTokens);
|
||||
if ($userToken->isOneTimeOnly()) {
|
||||
$this->deleteEntity($userToken);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function deleteExpired($isDryRun = false): int
|
||||
{
|
||||
$qb = $this->createQueryBuilder('ut');
|
||||
|
||||
if ($isDryRun) {
|
||||
$qb->select('count(ut.id) as records');
|
||||
} else {
|
||||
$qb->delete(UserToken::class, 'ut');
|
||||
}
|
||||
|
||||
return (int) $qb
|
||||
->where('ut.expiration <= :current_datetime')
|
||||
->setParameter('current_datetime', new \DateTime())
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Entity;
|
||||
|
||||
/**
|
||||
* Interface UserTokenRepositoryInterface.
|
||||
*/
|
||||
interface UserTokenRepositoryInterface
|
||||
{
|
||||
/**
|
||||
* @param string $secret
|
||||
*
|
||||
* @return UserToken
|
||||
*/
|
||||
public function isSecretUnique($secret);
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function verify(UserToken $token);
|
||||
|
||||
/**
|
||||
* Delete expired user tokens.
|
||||
*
|
||||
* @param bool $isDryRun
|
||||
*
|
||||
* @return int Number of selected or deleted rows
|
||||
*/
|
||||
public function deleteExpired($isDryRun = false);
|
||||
}
|
||||
Reference in New Issue
Block a user