Initial commit: CloudOps infrastructure platform

This commit is contained in:
root
2026-04-09 19:58:57 +02:00
commit 1166a52f26
7762 changed files with 839452 additions and 0 deletions

View File

@@ -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;
}
}

View File

@@ -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];
}
}

View File

@@ -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;
}
}

View File

@@ -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,
];
}
}

View File

@@ -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';
}
}

View File

@@ -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];
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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);
}