Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\ApiPlatform;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
|
||||
/**
|
||||
* @implements ProcessorInterface<User, User>
|
||||
*/
|
||||
final class UserProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ProcessorInterface $persistProcessor,
|
||||
private UserPasswordHasherInterface $passwordHasher,
|
||||
) {
|
||||
}
|
||||
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): mixed
|
||||
{
|
||||
if ($data instanceof User && null !== $data->getPlainPassword()) {
|
||||
$hashedPassword = $this->passwordHasher->hashPassword($data, $data->getPlainPassword());
|
||||
$data->setPassword($hashedPassword);
|
||||
$data->setPlainPassword(null);
|
||||
}
|
||||
|
||||
return $this->persistProcessor->process($data, $operation, $uriVariables, $context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
/* UserBundle */
|
||||
|
||||
.profile-details {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.col-user-id, .col-role-id {
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.col-user-avatar {
|
||||
width: 75px;
|
||||
}
|
||||
|
||||
.mautic-logo {
|
||||
width: 150px;
|
||||
margin: 0 auto;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.mautic-logo > svg.mautic-logo-figure .circle {
|
||||
fill: #4e5e9e;
|
||||
}
|
||||
|
||||
.mautic-logo > svg.mautic-logo-figure .m,
|
||||
.mautic-logo > svg.mautic-logo-figure .m-arrow {
|
||||
fill: #fdb933;
|
||||
}
|
||||
|
||||
input:-webkit-autofill {
|
||||
-webkit-box-shadow: 0 0 0px 1000px white inset;
|
||||
}
|
||||
|
||||
span.input-group-addon i.fa {
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.inline-login .login-form {
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.accent-color {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.accent-color,
|
||||
.accent-dropdown {
|
||||
outline: 2px var(--outline-style) transparent;
|
||||
outline-offset: 2px;
|
||||
transition: var(--transition-all-productive);
|
||||
}
|
||||
|
||||
input:checked + .accent-color,
|
||||
.dropdown.open .accent-dropdown {
|
||||
outline: 2px var(--outline-style) var(--focus);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
//UserBundle
|
||||
Mautic.userOnLoad = function (container) {
|
||||
if (mQuery(container + ' form[name="user"]').length) {
|
||||
if (mQuery('#user_position').length) {
|
||||
Mautic.activateTypeahead('#user_position', { displayKey: 'position' });
|
||||
}
|
||||
} else {
|
||||
if (mQuery(container + ' #list-search').length) {
|
||||
Mautic.activateSearchAutocomplete('list-search', 'user.user');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes radio button states for UI settings and updates hidden inputs
|
||||
* when settings are changed.
|
||||
*/
|
||||
// Initialize radio buttons based on hidden input values
|
||||
document.querySelectorAll('input[type="radio"][data-attribute-toggle]').forEach(radio => {
|
||||
const attributeName = radio.dataset.attributeToggle;
|
||||
const hiddenInput = document.getElementById(`user_preferences_${attributeName.replace('-', '_')}`);
|
||||
|
||||
if (hiddenInput && hiddenInput.value) {
|
||||
// If hidden input has a value, set the corresponding radio
|
||||
const correspondingRadio = document.querySelector(
|
||||
`input[name="${attributeName}"][data-attribute-value="${hiddenInput.value}"]`
|
||||
);
|
||||
if (correspondingRadio) correspondingRadio.checked = true;
|
||||
} else if (radio.checked) {
|
||||
// Use the checked state from the HTML as the default
|
||||
if (hiddenInput) {
|
||||
hiddenInput.value = radio.dataset.attributeValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Handle radio button changes - update hidden inputs and HTML attributes
|
||||
document.querySelectorAll('input[type="radio"][data-attribute-toggle]').forEach(radio => {
|
||||
radio.addEventListener('change', function() {
|
||||
if (this.checked) {
|
||||
const attributeName = this.dataset.attributeToggle;
|
||||
const hiddenInput = document.getElementById(`user_preferences_${attributeName.replace('-', '_')}`);
|
||||
|
||||
// Update hidden input value
|
||||
if (hiddenInput) {
|
||||
hiddenInput.value = this.dataset.attributeValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelector('[id^="user_buttons_save_toolbar"]').addEventListener('click', function() {
|
||||
// Re-apply all current preferences after clicking save
|
||||
document.querySelectorAll('input[type="radio"][data-attribute-toggle]:checked').forEach(radio => {
|
||||
const attributeToggle = radio.dataset.attributeToggle;
|
||||
const attributeValue = radio.dataset.attributeValue;
|
||||
document.documentElement.setAttribute(attributeToggle, attributeValue);
|
||||
});
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
Mautic.roleOnLoad = function (container, response) {
|
||||
if (mQuery(container + ' #list-search').length) {
|
||||
Mautic.activateSearchAutocomplete('list-search', 'user.role');
|
||||
}
|
||||
|
||||
if (response && response.permissionList) {
|
||||
MauticVars.permissionList = response.permissionList;
|
||||
}
|
||||
Mautic.togglePermissionVisibility();
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggles permission panel visibility for roles
|
||||
*/
|
||||
Mautic.togglePermissionVisibility = function () {
|
||||
//add a very slight delay in order for the clicked on checkbox to be selected since the onclick action
|
||||
//is set to the parent div
|
||||
setTimeout(function () {
|
||||
if (mQuery('#role_isAdmin_0').prop('checked')) {
|
||||
mQuery('#rolePermissions').removeClass('hide');
|
||||
mQuery('#isAdminMessage').addClass('hide');
|
||||
mQuery('#permissions-tab').removeClass('disabled');
|
||||
} else {
|
||||
mQuery('#rolePermissions').addClass('hide');
|
||||
mQuery('#isAdminMessage').removeClass('hide');
|
||||
mQuery('#permissions-tab').addClass('disabled');
|
||||
}
|
||||
}, 10);
|
||||
};
|
||||
|
||||
/**
|
||||
* Toggle permissions, update ratio, etc
|
||||
*
|
||||
* @param changedPermission
|
||||
* @param bundle
|
||||
*/
|
||||
Mautic.onPermissionChange = function (changedPermission, bundle) {
|
||||
var granted = 0;
|
||||
|
||||
if (mQuery(changedPermission).prop('checked')) {
|
||||
if (mQuery(changedPermission).val() == 'full') {
|
||||
//uncheck all of the others
|
||||
mQuery(changedPermission).closest('.choice-wrapper').find("label input:checkbox:checked").map(function () {
|
||||
if (mQuery(this).val() != 'full') {
|
||||
mQuery(this).prop('checked', false);
|
||||
mQuery(this).parent().toggleClass('active');
|
||||
}
|
||||
})
|
||||
} else {
|
||||
//uncheck full
|
||||
mQuery(changedPermission).closest('.choice-wrapper').find("label input:checkbox:checked").map(function () {
|
||||
if (mQuery(this).val() == 'full') {
|
||||
granted = granted - 1;
|
||||
mQuery(this).prop('checked', false);
|
||||
mQuery(this).parent().toggleClass('active');
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//update granted numbers
|
||||
if (mQuery('.' + bundle + '_granted').length) {
|
||||
var granted = 0;
|
||||
var levelPerms = MauticVars.permissionList[bundle];
|
||||
mQuery.each(levelPerms, function (level, perms) {
|
||||
mQuery.each(perms, function (index, perm) {
|
||||
var isChecked = mQuery('input[data-permission="' + bundle + ':' + level + ':' + perm + '"]').prop('checked');
|
||||
if (perm == 'full') {
|
||||
if (isChecked) {
|
||||
if (perms.length === 1) {
|
||||
granted++;
|
||||
} else {
|
||||
granted += perms.length - 1;
|
||||
}
|
||||
}
|
||||
} else if (isChecked) {
|
||||
granted++;
|
||||
}
|
||||
});
|
||||
});
|
||||
mQuery('.' + bundle + '_granted').html(granted);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,273 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'menu' => [
|
||||
'admin' => [
|
||||
'mautic.user_management' => [
|
||||
'id' => 'mautic_user_management_root',
|
||||
'priority' => 17,
|
||||
'access' => ['user:users:view', 'user:roles:view'],
|
||||
],
|
||||
'mautic.user.users' => [
|
||||
'access' => 'user:users:view',
|
||||
'route' => 'mautic_user_index',
|
||||
'parent' => 'mautic.user_management',
|
||||
'iconClass' => 'ri-user-settings-line',
|
||||
],
|
||||
'mautic.user.roles' => [
|
||||
'access' => 'user:roles:view',
|
||||
'route' => 'mautic_role_index',
|
||||
'parent' => 'mautic.user_management',
|
||||
'iconClass' => 'ri-shield-user-line',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'routes' => [
|
||||
'main' => [
|
||||
'login' => [
|
||||
'path' => '/login',
|
||||
'controller' => 'Mautic\UserBundle\Controller\SecurityController::loginAction',
|
||||
],
|
||||
'mautic_user_logincheck' => [
|
||||
'path' => '/login_check',
|
||||
'controller' => 'Mautic\UserBundle\Controller\SecurityController::loginCheckAction',
|
||||
],
|
||||
'mautic_user_logout' => [
|
||||
'path' => '/logout',
|
||||
],
|
||||
'mautic_sso_login' => [
|
||||
'path' => '/sso_login/{integration}',
|
||||
'controller' => 'Mautic\UserBundle\Controller\SecurityController::ssoLoginAction',
|
||||
],
|
||||
'mautic_sso_login_check' => [
|
||||
'path' => '/sso_login_check/{integration}',
|
||||
'controller' => 'Mautic\UserBundle\Controller\SecurityController::ssoLoginCheckAction',
|
||||
],
|
||||
'lightsaml_sp.login' => [
|
||||
'path' => '/saml/login',
|
||||
'controller' => 'LightSaml\SpBundle\Controller\DefaultController::loginAction',
|
||||
],
|
||||
'lightsaml_sp.login_check' => [
|
||||
'path' => '/saml/login_check',
|
||||
],
|
||||
'mautic_user_index' => [
|
||||
'path' => '/users/{page}',
|
||||
'controller' => 'Mautic\UserBundle\Controller\UserController::indexAction',
|
||||
],
|
||||
'mautic_user_action' => [
|
||||
'path' => '/users/{objectAction}/{objectId}',
|
||||
'controller' => 'Mautic\UserBundle\Controller\UserController::executeAction',
|
||||
],
|
||||
'mautic_role_index' => [
|
||||
'path' => '/roles/{page}',
|
||||
'controller' => 'Mautic\UserBundle\Controller\RoleController::indexAction',
|
||||
],
|
||||
'mautic_role_action' => [
|
||||
'path' => '/roles/{objectAction}/{objectId}',
|
||||
'controller' => 'Mautic\UserBundle\Controller\RoleController::executeAction',
|
||||
],
|
||||
'mautic_user_account' => [
|
||||
'path' => '/account',
|
||||
'controller' => 'Mautic\UserBundle\Controller\ProfileController::indexAction',
|
||||
],
|
||||
],
|
||||
|
||||
'api' => [
|
||||
'mautic_api_usersstandard' => [
|
||||
'standard_entity' => true,
|
||||
'name' => 'users',
|
||||
'path' => '/users',
|
||||
'controller' => Mautic\UserBundle\Controller\Api\UserApiController::class,
|
||||
],
|
||||
'mautic_api_getself' => [
|
||||
'path' => '/users/self',
|
||||
'controller' => 'Mautic\UserBundle\Controller\Api\UserApiController::getSelfAction',
|
||||
],
|
||||
'mautic_api_checkpermission' => [
|
||||
'path' => '/users/{id}/permissioncheck',
|
||||
'controller' => 'Mautic\UserBundle\Controller\Api\UserApiController::isGrantedAction',
|
||||
'method' => 'POST',
|
||||
],
|
||||
'mautic_api_getuserroles' => [
|
||||
'path' => '/users/list/roles',
|
||||
'controller' => 'Mautic\UserBundle\Controller\Api\UserApiController::getRolesAction',
|
||||
],
|
||||
'mautic_api_rolesstandard' => [
|
||||
'standard_entity' => true,
|
||||
'name' => 'roles',
|
||||
'path' => '/roles',
|
||||
'controller' => Mautic\UserBundle\Controller\Api\RoleApiController::class,
|
||||
],
|
||||
],
|
||||
'public' => [
|
||||
'mautic_user_passwordreset' => [
|
||||
'path' => '/passwordreset',
|
||||
'controller' => 'Mautic\UserBundle\Controller\PublicController::passwordResetAction',
|
||||
],
|
||||
'mautic_user_passwordresetconfirm' => [
|
||||
'path' => '/passwordresetconfirm',
|
||||
'controller' => 'Mautic\UserBundle\Controller\PublicController::passwordResetConfirmAction',
|
||||
],
|
||||
'lightsaml_sp.metadata' => [
|
||||
'path' => '/saml/metadata.xml',
|
||||
'controller' => 'LightSaml\SpBundle\Controller\DefaultController::metadataAction',
|
||||
],
|
||||
'lightsaml_sp.discovery' => [
|
||||
'path' => '/saml/discovery',
|
||||
'controller' => 'LightSaml\SpBundle\Controller\DefaultController::discoveryAction',
|
||||
],
|
||||
'mautic_saml_login_retry' => [
|
||||
'path' => '/saml/login_retry',
|
||||
'controller' => 'Mautic\UserBundle\Controller\SecurityController::samlLoginRetryAction',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'services' => [
|
||||
'other' => [
|
||||
// Authentication
|
||||
'mautic.user.manager' => [
|
||||
'class' => Doctrine\ORM\EntityManager::class,
|
||||
'arguments' => Mautic\UserBundle\Entity\User::class,
|
||||
'factory' => ['@doctrine', 'getManagerForClass'],
|
||||
],
|
||||
'mautic.permission.manager' => [
|
||||
'class' => Doctrine\ORM\EntityManager::class,
|
||||
'arguments' => Mautic\UserBundle\Entity\Permission::class,
|
||||
'factory' => ['@doctrine', 'getManagerForClass'],
|
||||
],
|
||||
'mautic.user.provider' => [
|
||||
'class' => Mautic\UserBundle\Security\Provider\UserProvider::class,
|
||||
'arguments' => [
|
||||
'mautic.user.repository',
|
||||
'mautic.permission.repository',
|
||||
'event_dispatcher',
|
||||
'security.password_hasher',
|
||||
],
|
||||
],
|
||||
'mautic.security.authentication_handler' => [
|
||||
'class' => Mautic\UserBundle\Security\Authentication\AuthenticationHandler::class,
|
||||
'arguments' => [
|
||||
'router',
|
||||
],
|
||||
],
|
||||
'mautic.security.logout_handler' => [
|
||||
'class' => Mautic\UserBundle\EventListener\LogoutListener::class,
|
||||
'tagArguments' => [
|
||||
'event' => Symfony\Component\Security\Http\Event\LogoutEvent::class,
|
||||
],
|
||||
'tag' => 'kernel.event_listener',
|
||||
'arguments' => [
|
||||
'mautic.user.model.user',
|
||||
'event_dispatcher',
|
||||
'mautic.helper.user',
|
||||
],
|
||||
],
|
||||
|
||||
// SAML
|
||||
'mautic.security.saml.credential_store' => [
|
||||
'class' => Mautic\UserBundle\Security\SAML\Store\CredentialsStore::class,
|
||||
'arguments' => [
|
||||
'mautic.helper.core_parameters',
|
||||
'%mautic.saml_idp_entity_id%',
|
||||
],
|
||||
'tag' => 'lightsaml.own_credential_store',
|
||||
],
|
||||
|
||||
'mautic.security.saml.trust_store' => [
|
||||
'class' => Mautic\UserBundle\Security\SAML\Store\TrustOptionsStore::class,
|
||||
'arguments' => [
|
||||
'mautic.helper.core_parameters',
|
||||
'%mautic.saml_idp_entity_id%',
|
||||
],
|
||||
'tag' => 'lightsaml.trust_options_store',
|
||||
],
|
||||
|
||||
'mautic.security.saml.entity_descriptor_provider' => [
|
||||
'class' => LightSaml\Builder\EntityDescriptor\SimpleEntityDescriptorBuilder::class,
|
||||
'factory' => [Mautic\UserBundle\Security\SAML\EntityDescriptorProviderFactory::class, 'build'],
|
||||
'arguments' => [
|
||||
'%lightsaml.own.entity_id%',
|
||||
'router',
|
||||
'%lightsaml.route.login_check%',
|
||||
'lightsaml.own.credential_store',
|
||||
],
|
||||
],
|
||||
|
||||
'mautic.security.saml.entity_descriptor_store' => [
|
||||
'class' => Mautic\UserBundle\Security\SAML\Store\EntityDescriptorStore::class,
|
||||
'arguments' => [
|
||||
'mautic.helper.core_parameters',
|
||||
],
|
||||
'tag' => 'lightsaml.idp_entity_store',
|
||||
],
|
||||
|
||||
'mautic.security.saml.id_store' => [
|
||||
'class' => Mautic\UserBundle\Security\SAML\Store\IdStore::class,
|
||||
'arguments' => [
|
||||
'doctrine.orm.entity_manager',
|
||||
'lightsaml.system.time_provider',
|
||||
],
|
||||
],
|
||||
|
||||
'mautic.security.saml.username_mapper' => [
|
||||
'class' => Mautic\UserBundle\Security\SAML\User\UserMapper::class,
|
||||
'arguments' => [
|
||||
[
|
||||
'email' => '%mautic.saml_idp_email_attribute%',
|
||||
'username' => '%mautic.saml_idp_username_attribute%',
|
||||
'firstname' => '%mautic.saml_idp_firstname_attribute%',
|
||||
'lastname' => '%mautic.saml_idp_lastname_attribute%',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'mautic.security.saml.user_creator' => [
|
||||
'class' => Mautic\UserBundle\Security\SAML\User\UserCreator::class,
|
||||
'arguments' => [
|
||||
'doctrine.orm.entity_manager',
|
||||
'mautic.security.saml.username_mapper',
|
||||
'mautic.user.model.user',
|
||||
'security.password_hasher',
|
||||
'%mautic.saml_idp_default_role%',
|
||||
],
|
||||
],
|
||||
'mautic.security.user_token_setter' => [
|
||||
'class' => Mautic\UserBundle\Security\UserTokenSetter::class,
|
||||
'arguments' => ['mautic.user.model.user', 'security.token_storage'],
|
||||
],
|
||||
'mautic.user.model.user_token_service' => [
|
||||
'class' => Mautic\UserBundle\Model\UserToken\UserTokenService::class,
|
||||
'arguments' => [
|
||||
'mautic.helper.random',
|
||||
'mautic.user.repository.user_token',
|
||||
],
|
||||
],
|
||||
],
|
||||
'fixtures' => [
|
||||
'mautic.user.fixture.role' => [
|
||||
'class' => Mautic\UserBundle\DataFixtures\ORM\LoadRoleData::class,
|
||||
'tag' => Doctrine\Bundle\FixturesBundle\DependencyInjection\CompilerPass\FixturesCompilerPass::FIXTURE_TAG,
|
||||
'arguments' => ['mautic.user.model.role'],
|
||||
],
|
||||
'mautic.user.fixture.user' => [
|
||||
'class' => Mautic\UserBundle\DataFixtures\ORM\LoadUserData::class,
|
||||
'tag' => Doctrine\Bundle\FixturesBundle\DependencyInjection\CompilerPass\FixturesCompilerPass::FIXTURE_TAG,
|
||||
'arguments' => ['security.password_hasher'],
|
||||
],
|
||||
],
|
||||
],
|
||||
'parameters' => [
|
||||
'saml_idp_metadata' => '',
|
||||
'saml_idp_entity_id' => '',
|
||||
'saml_idp_own_certificate' => '',
|
||||
'saml_idp_own_private_key' => '',
|
||||
'saml_idp_own_password' => '',
|
||||
'saml_idp_email_attribute' => '',
|
||||
'saml_idp_username_attribute' => '',
|
||||
'saml_idp_firstname_attribute' => '',
|
||||
'saml_idp_lastname_attribute' => '',
|
||||
'saml_idp_default_role' => '',
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Mautic\CoreBundle\DependencyInjection\MauticCoreExtension;
|
||||
use Mautic\UserBundle\EventListener\ApiUserSubscriber;
|
||||
use Mautic\UserBundle\Security\Authentication\Token\Permissions\TokenPermissions;
|
||||
use Mautic\UserBundle\Security\Authenticator\PluginAuthenticator;
|
||||
use Mautic\UserBundle\Security\Authenticator\SsoAuthenticator;
|
||||
use Mautic\UserBundle\Security\EntryPoint\MainEntryPoint;
|
||||
|
||||
use function Symfony\Component\DependencyInjection\Loader\Configurator\abstract_arg;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
|
||||
use function Symfony\Component\DependencyInjection\Loader\Configurator\service;
|
||||
|
||||
return function (ContainerConfigurator $configurator): void {
|
||||
$services = $configurator->services()
|
||||
->defaults()
|
||||
->autowire()
|
||||
->autoconfigure()
|
||||
->public();
|
||||
|
||||
$excludes = [
|
||||
];
|
||||
|
||||
$services->load('Mautic\\UserBundle\\', '../')
|
||||
->exclude('../{'.implode(',', array_merge(MauticCoreExtension::DEFAULT_EXCLUDES, $excludes)).'}');
|
||||
|
||||
$services->load('Mautic\\UserBundle\\Entity\\', '../Entity/*Repository.php')
|
||||
->tag(Doctrine\Bundle\DoctrineBundle\DependencyInjection\Compiler\ServiceRepositoryCompilerPass::REPOSITORY_SERVICE_TAG);
|
||||
|
||||
$services->set(Mautic\UserBundle\ApiPlatform\UserProcessor::class)
|
||||
->args([
|
||||
service('api_platform.doctrine.orm.state.persist_processor'),
|
||||
service('security.user_password_hasher'),
|
||||
])
|
||||
->tag('api_platform.state_processor');
|
||||
|
||||
$services->set('security.authenticator.mautic_sso', SsoAuthenticator::class)
|
||||
->abstract()
|
||||
->args([
|
||||
'$httpUtils' => service('security.http_utils'),
|
||||
'$userProvider' => abstract_arg('user provider'),
|
||||
'$successHandler' => abstract_arg('authentication success handler'),
|
||||
'$failureHandler' => abstract_arg('authentication failure handler'),
|
||||
'$options' => abstract_arg('options'),
|
||||
]);
|
||||
|
||||
$services->set('security.authenticator.mautic_api', PluginAuthenticator::class)
|
||||
->abstract()
|
||||
->args([
|
||||
'$oAuth2' => service('fos_oauth_server.server'),
|
||||
]);
|
||||
|
||||
$services->set(Mautic\UserBundle\Security\SAML\Helper::class);
|
||||
$services->set('security.token.permissions', TokenPermissions::class);
|
||||
|
||||
$services->load('Mautic\\UserBundle\\Security\\EntryPoint\\', '../Security/EntryPoint/*.php');
|
||||
$services->load('Mautic\\UserBundle\\Security\\Authentication\\Token\\Permissions\\', '../Security/Authentication/Token/Permissions/*.php');
|
||||
|
||||
$services->alias(Mautic\UserBundle\Entity\UserTokenRepositoryInterface::class, Mautic\UserBundle\Entity\UserTokenRepository::class);
|
||||
|
||||
$services->alias('mautic.user.model.role', Mautic\UserBundle\Model\RoleModel::class);
|
||||
$services->alias('mautic.user.model.user', Mautic\UserBundle\Model\UserModel::class);
|
||||
$services->alias('mautic.user.repository.user_token', Mautic\UserBundle\Entity\UserTokenRepository::class);
|
||||
$services->alias('mautic.user.repository', Mautic\UserBundle\Entity\UserRepository::class);
|
||||
$services->alias('mautic.permission.repository', Mautic\UserBundle\Entity\PermissionRepository::class);
|
||||
$services->alias('mautic.user.model.password_strength_estimator', Mautic\UserBundle\Model\PasswordStrengthEstimatorModel::class);
|
||||
$services->get(Mautic\UserBundle\Form\Validator\Constraints\NotWeakValidator::class)->tag('validator.constraint_validator');
|
||||
|
||||
$services->load('Mautic\\UserBundle\\Security\\SAML\Store\\Request\\', '../Security/SAML/Store/Request/*.php');
|
||||
$services->get(Mautic\UserBundle\Security\SAML\Store\Request\RequestStateStore::class)
|
||||
->arg('$prefix', '%lightsaml.store.request_session_prefix%')
|
||||
->arg('$suffix', '%lightsaml.store.request_session_sufix%');
|
||||
$services->get(MainEntryPoint::class)->arg('$samlEnabled', '%env(MAUTIC_SAML_ENABLED)%');
|
||||
$services->get(ApiUserSubscriber::class)->arg('$userProvider', service('security.user_providers'));
|
||||
|
||||
// Below are fixes for autowiring of SAML SpBundle.
|
||||
$services->alias(LightSaml\SymfonyBridgeBundle\Bridge\Container\BuildContainer::class, 'lightsaml.container.build');
|
||||
$services->load('LightSaml\\SpBundle\\Controller\\', '%kernel.project_dir%/vendor/javer/sp-bundle/src/LightSaml/SpBundle/Controller/*.php')
|
||||
->tag('controller.service_arguments');
|
||||
// Decorate the form_login class to ensure no user enumeration can
|
||||
// happen via timing attacks.
|
||||
$services->set('mautic.security.authenticator.form_login.decorator', Mautic\UserBundle\Security\TimingSafeFormLoginAuthenticator::class)
|
||||
->decorate('security.authenticator.form_login.main')
|
||||
->args([
|
||||
service('.inner'),
|
||||
service('mautic.user.provider'),
|
||||
service('security.password_hasher_factory'),
|
||||
[], // This will be replaced by the compiler pass
|
||||
]);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\AppVersion;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\UserBundle\Entity\Role;
|
||||
use Mautic\UserBundle\Model\RoleModel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<Role>
|
||||
*/
|
||||
class RoleApiController extends CommonApiController
|
||||
{
|
||||
/**
|
||||
* @var RoleModel|null
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
public function __construct(CorePermissions $security, Translator $translator, EntityResultHelper $entityResultHelper, RouterInterface $router, FormFactoryInterface $formFactory, AppVersion $appVersion, RequestStack $requestStack, ManagerRegistry $doctrine, ModelFactory $modelFactory, EventDispatcherInterface $dispatcher, CoreParametersHelper $coreParametersHelper)
|
||||
{
|
||||
$roleModel = $modelFactory->getModel('user.role');
|
||||
\assert($roleModel instanceof RoleModel);
|
||||
|
||||
$this->model = $roleModel;
|
||||
$this->entityClass = Role::class;
|
||||
$this->entityNameOne = 'role';
|
||||
$this->entityNameMulti = 'roles';
|
||||
$this->serializerGroups = ['roleDetails', 'publishDetails'];
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Role &$entity
|
||||
* @param string $action
|
||||
*/
|
||||
protected function preSaveEntity(&$entity, $form, $parameters, $action = 'edit')
|
||||
{
|
||||
if (isset($parameters['rawPermissions'])) {
|
||||
$this->model->setRolePermissions($entity, $parameters['rawPermissions']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Controller\Api;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ApiBundle\Controller\CommonApiController;
|
||||
use Mautic\ApiBundle\Helper\EntityResultHelper;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\AppVersion;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Mautic\UserBundle\Model\UserModel;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
|
||||
/**
|
||||
* @extends CommonApiController<User>
|
||||
*/
|
||||
class UserApiController extends CommonApiController
|
||||
{
|
||||
/**
|
||||
* @var UserModel|null
|
||||
*/
|
||||
protected $model;
|
||||
|
||||
public function __construct(
|
||||
CorePermissions $security,
|
||||
Translator $translator,
|
||||
EntityResultHelper $entityResultHelper,
|
||||
RouterInterface $router,
|
||||
FormFactoryInterface $formFactory,
|
||||
AppVersion $appVersion,
|
||||
private UserPasswordHasherInterface $hasher,
|
||||
RequestStack $requestStack,
|
||||
ManagerRegistry $doctrine,
|
||||
ModelFactory $modelFactory,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
$userModel = $modelFactory->getModel('user.user');
|
||||
\assert($userModel instanceof UserModel);
|
||||
|
||||
$this->model = $userModel;
|
||||
$this->entityClass = User::class;
|
||||
$this->entityNameOne = 'user';
|
||||
$this->entityNameMulti = 'users';
|
||||
$this->serializerGroups = ['userDetails', 'roleList', 'publishDetails'];
|
||||
$this->dataInputMasks = ['signature' => 'html'];
|
||||
|
||||
parent::__construct($security, $translator, $entityResultHelper, $router, $formFactory, $appVersion, $requestStack, $doctrine, $modelFactory, $dispatcher, $coreParametersHelper);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains the logged in user's data.
|
||||
*
|
||||
* @return Response
|
||||
*
|
||||
* @throws NotFoundHttpException
|
||||
*/
|
||||
public function getSelfAction(TokenStorageInterface $tokenStorage)
|
||||
{
|
||||
$currentUser = $tokenStorage->getToken()->getUser();
|
||||
$view = $this->view($currentUser, Response::HTTP_OK);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new user.
|
||||
*/
|
||||
public function newEntityAction(Request $request)
|
||||
{
|
||||
$entity = $this->model->getEntity();
|
||||
|
||||
if (!$this->security->isGranted('user:users:create')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$parameters = $request->request->all();
|
||||
|
||||
if (isset($parameters['plainPassword']['password'])) {
|
||||
$submittedPassword = $parameters['plainPassword']['password'];
|
||||
$entity->setPassword($this->model->checkNewPassword($entity, $this->hasher, $submittedPassword));
|
||||
}
|
||||
|
||||
return $this->processForm($request, $entity, $parameters, 'POST');
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits an existing user or creates a new one on PUT if not found.
|
||||
*
|
||||
* @param int $id User ID
|
||||
*
|
||||
* @return Response
|
||||
*
|
||||
* @throws NotFoundHttpException
|
||||
*/
|
||||
public function editEntityAction(Request $request, $id)
|
||||
{
|
||||
$entity = $this->model->getEntity($id);
|
||||
$parameters = $request->request->all();
|
||||
$method = $request->getMethod();
|
||||
|
||||
if (!$this->security->isGranted('user:users:edit')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
if (null === $entity) {
|
||||
if ('PATCH' === $method
|
||||
|| ('PUT' === $method && !$this->security->isGranted('user:users:create'))
|
||||
) {
|
||||
// PATCH requires that an entity exists or must have create access for PUT
|
||||
return $this->notFound();
|
||||
} else {
|
||||
$entity = $this->model->getEntity();
|
||||
if (isset($parameters['plainPassword']['password'])) {
|
||||
$submittedPassword = $parameters['plainPassword']['password'];
|
||||
$entity->setPassword($this->model->checkNewPassword($entity, $this->hasher, $submittedPassword));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Changing passwords via API is forbidden
|
||||
if (!empty($parameters['plainPassword'])) {
|
||||
unset($parameters['plainPassword']);
|
||||
}
|
||||
if ('PATCH' == $method) {
|
||||
// PATCH will accept a diff so just remove the entities
|
||||
|
||||
// Changing username via API is forbidden
|
||||
if (!empty($parameters['username'])) {
|
||||
unset($parameters['username']);
|
||||
}
|
||||
} else {
|
||||
// PUT requires the entire entity so overwrite the username with the original
|
||||
$parameters['username'] = $entity->getUsername();
|
||||
$parameters['role'] = $entity->getRole()->getId();
|
||||
}
|
||||
}
|
||||
|
||||
return $this->processForm($request, $entity, $parameters, $method);
|
||||
}
|
||||
|
||||
protected function preSaveEntity(&$entity, $form, $parameters, $action = 'edit')
|
||||
{
|
||||
switch ($action) {
|
||||
case 'new':
|
||||
$submittedPassword = null;
|
||||
if (isset($parameters['plainPassword'])) {
|
||||
if (is_array($parameters['plainPassword']) && isset($parameters['plainPassword']['password'])) {
|
||||
$submittedPassword = $parameters['plainPassword']['password'];
|
||||
} else {
|
||||
$submittedPassword = $parameters['plainPassword'];
|
||||
}
|
||||
}
|
||||
|
||||
$entity->setPassword($this->model->checkNewPassword($entity, $this->hasher, $submittedPassword, true));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies if a user has permission(s) to a action.
|
||||
*
|
||||
* @param int $id User ID
|
||||
*
|
||||
* @return Response
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
|
||||
* @throws NotFoundHttpException
|
||||
*/
|
||||
public function isGrantedAction(Request $request, $id)
|
||||
{
|
||||
$entity = $this->model->getEntity($id);
|
||||
if (!$entity instanceof $this->entityClass) {
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
$permissions = $request->request->all()['permissions'] ?? [];
|
||||
|
||||
if (empty($permissions)) {
|
||||
return $this->badRequest('mautic.api.call.permissionempty');
|
||||
} elseif (!is_array($permissions)) {
|
||||
$permissions = [$permissions];
|
||||
}
|
||||
|
||||
$return = $this->security->isGranted($permissions, 'RETURN_ARRAY', $entity);
|
||||
$view = $this->view($return, Response::HTTP_OK);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains a list of roles for user edits.
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function getRolesAction(Request $request)
|
||||
{
|
||||
if (!$this->security->isGranted(
|
||||
['user:users:create', 'user:users:edit'],
|
||||
'MATCH_ONE'
|
||||
)
|
||||
) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$filter = $request->query->get('filter', null);
|
||||
$limit = (int) $request->query->get('limit', null);
|
||||
$roles = $this->model->getLookupResults('role', $filter, $limit);
|
||||
|
||||
$view = $this->view($roles, Response::HTTP_OK);
|
||||
$context = $view->getContext()->setGroups(['roleList']);
|
||||
$view->setContext($context);
|
||||
|
||||
return $this->handleView($view);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\FormController;
|
||||
use Mautic\CoreBundle\Helper\LanguageHelper;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Mautic\UserBundle\Model\UserModel;
|
||||
use Mautic\UserBundle\Security\SAML\Helper as SAMLHelper;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
|
||||
class ProfileController extends FormController
|
||||
{
|
||||
/**
|
||||
* Generate's account profile.
|
||||
*/
|
||||
public function indexAction(Request $request, LanguageHelper $languageHelper, UserPasswordHasherInterface $hasher,
|
||||
TokenStorageInterface $tokenStorage, SAMLHelper $samlHelper): Response|RedirectResponse
|
||||
{
|
||||
// get current user
|
||||
$me = $tokenStorage->getToken()->getUser();
|
||||
\assert($me instanceof User);
|
||||
/** @var UserModel $model */
|
||||
$model = $this->getModel('user');
|
||||
|
||||
// set some permissions
|
||||
$permissions = [
|
||||
'apiAccess' => ($this->coreParametersHelper->get('api_enabled')) ?
|
||||
$this->security->isGranted('api:access:full')
|
||||
: 0,
|
||||
'editName' => $this->security->isGranted('user:profile:editname'),
|
||||
'editUsername' => $this->security->isGranted('user:profile:editusername'),
|
||||
'editPosition' => $this->security->isGranted('user:profile:editposition'),
|
||||
'editEmail' => $this->security->isGranted('user:profile:editemail'),
|
||||
];
|
||||
|
||||
$action = $this->generateUrl('mautic_user_account');
|
||||
$form = $model->createForm($me, $this->formFactory, $action, ['in_profile' => true]);
|
||||
|
||||
$overrides = [];
|
||||
|
||||
// make sure this user has access to edit privileged fields
|
||||
foreach ($permissions as $permName => $hasAccess) {
|
||||
if ('apiAccess' == $permName) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$hasAccess) {
|
||||
// set the value to its original
|
||||
switch ($permName) {
|
||||
case 'editName':
|
||||
$overrides['firstName'] = $me->getFirstName();
|
||||
$overrides['lastName'] = $me->getLastName();
|
||||
$form->remove('firstName');
|
||||
$form->add(
|
||||
'firstName_unbound',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.core.firstname',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control'],
|
||||
'mapped' => false,
|
||||
'disabled' => true,
|
||||
'data' => $me->getFirstName(),
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$form->remove('lastName');
|
||||
$form->add(
|
||||
'lastName_unbound',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.core.lastname',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control'],
|
||||
'mapped' => false,
|
||||
'disabled' => true,
|
||||
'data' => $me->getLastName(),
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
break;
|
||||
|
||||
case 'editUsername':
|
||||
$overrides['username'] = $me->getUserIdentifier();
|
||||
$form->remove('username');
|
||||
$form->add(
|
||||
'username_unbound',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.core.username',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control'],
|
||||
'mapped' => false,
|
||||
'disabled' => true,
|
||||
'data' => $me->getUserIdentifier(),
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
break;
|
||||
case 'editPosition':
|
||||
$overrides['position'] = $me->getPosition();
|
||||
$form->remove('position');
|
||||
$form->add(
|
||||
'position_unbound',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.core.position',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control'],
|
||||
'mapped' => false,
|
||||
'disabled' => true,
|
||||
'data' => $me->getPosition(),
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
break;
|
||||
case 'editEmail':
|
||||
$overrides['email'] = $me->getEmail();
|
||||
$form->remove('email');
|
||||
$form->add(
|
||||
'email_unbound',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.core.type.email',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control'],
|
||||
'mapped' => false,
|
||||
'disabled' => true,
|
||||
'data' => $me->getEmail(),
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for a submitted form and process it
|
||||
$submitted = $request->getSession()->get('formProcessed', 0);
|
||||
if ('POST' === $request->getMethod() && !$submitted) {
|
||||
$request->getSession()->set('formProcessed', 1);
|
||||
|
||||
// check to see if the password needs to be rehashed
|
||||
$formUser = $request->request->all()['user'] ?? [];
|
||||
$submittedPassword = $formUser['plainPassword']['password'] ?? null;
|
||||
$overrides['password'] = $model->checkNewPassword($me, $hasher, $submittedPassword);
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($this->isFormValid($form)) {
|
||||
foreach ($overrides as $k => $v) {
|
||||
$func = 'set'.ucfirst($k);
|
||||
$me->$func($v);
|
||||
}
|
||||
|
||||
// form is valid so process the data
|
||||
$model->saveEntity($me);
|
||||
|
||||
// check if the user's locale has been downloaded already, fetch it if not
|
||||
$installedLanguages = $languageHelper->getSupportedLanguages();
|
||||
|
||||
if ($me->getLocale() && !array_key_exists($me->getLocale(), $installedLanguages)) {
|
||||
$fetchLanguage = $languageHelper->extractLanguagePackage($me->getLocale());
|
||||
|
||||
// If there is an error, we need to reset the user's locale to the default
|
||||
if ($fetchLanguage['error']) {
|
||||
$me->setLocale(null);
|
||||
$model->saveEntity($me);
|
||||
$message = 'mautic.core.could.not.set.language';
|
||||
$messageVars = [];
|
||||
|
||||
if (isset($fetchLanguage['message'])) {
|
||||
$message = $fetchLanguage['message'];
|
||||
}
|
||||
|
||||
if (isset($fetchLanguage['vars'])) {
|
||||
$messageVars = $fetchLanguage['vars'];
|
||||
}
|
||||
|
||||
$this->addFlashMessage($message, $messageVars);
|
||||
}
|
||||
}
|
||||
|
||||
// Update timezone and locale
|
||||
$tz = $me->getTimezone();
|
||||
if (empty($tz)) {
|
||||
$tz = $this->coreParametersHelper->getDefaultTimezone();
|
||||
}
|
||||
$request->getSession()->set('_timezone', $tz);
|
||||
|
||||
$locale = $me->getLocale();
|
||||
if (empty($locale)) {
|
||||
$locale = $this->coreParametersHelper->get('locale');
|
||||
}
|
||||
$request->getSession()->set('_locale', $locale);
|
||||
|
||||
$returnUrl = $this->generateUrl('mautic_user_account');
|
||||
|
||||
return $this->postActionRedirect(
|
||||
[
|
||||
'returnUrl' => $returnUrl,
|
||||
'contentTemplate' => 'Mautic\UserBundle\Controller\ProfileController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'mauticContent' => 'user',
|
||||
],
|
||||
'flashes' => [ // success
|
||||
[
|
||||
'type' => 'notice',
|
||||
'msg' => 'mautic.user.account.notice.updated',
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return $this->redirectToRoute('mautic_dashboard_index');
|
||||
}
|
||||
}
|
||||
$request->getSession()->set('formProcessed', 0);
|
||||
|
||||
$isSamlUser = $samlHelper->isSamlSession();
|
||||
if ($isSamlUser) {
|
||||
$form->remove('plainPassword');
|
||||
}
|
||||
|
||||
$parameters = [
|
||||
'permissions' => $permissions,
|
||||
'me' => $me,
|
||||
'userForm' => $form->createView(),
|
||||
'isSamlUser' => $isSamlUser,
|
||||
'authorizedClients' => $this->forward('Mautic\ApiBundle\Controller\ClientController::authorizedClientsAction')->getContent(),
|
||||
];
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => $parameters,
|
||||
'contentTemplate' => '@MauticUser/Profile/index.html.twig',
|
||||
'passthroughVars' => [
|
||||
'route' => $this->generateUrl('mautic_user_account'),
|
||||
'mauticContent' => 'user',
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\FormController;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Mautic\UserBundle\Form\Type\PasswordResetConfirmType;
|
||||
use Mautic\UserBundle\Form\Type\PasswordResetType;
|
||||
use Mautic\UserBundle\Model\UserModel;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
|
||||
class PublicController extends FormController
|
||||
{
|
||||
/**
|
||||
* Generates a new password for the user and emails it to them.
|
||||
*/
|
||||
public function passwordResetAction(Request $request): \Symfony\Component\HttpFoundation\RedirectResponse|\Symfony\Component\HttpFoundation\Response
|
||||
{
|
||||
/** @var UserModel $model */
|
||||
$model = $this->getModel('user');
|
||||
|
||||
$data = ['identifier' => ''];
|
||||
$action = $this->generateUrl('mautic_user_passwordreset');
|
||||
$form = $this->formFactory->create(PasswordResetType::class, $data, ['action' => $action]);
|
||||
|
||||
// /Check for a submitted form and process it
|
||||
if ('POST' === $request->getMethod()) {
|
||||
if ($isValid = $this->isFormValid($form)) {
|
||||
// find the user
|
||||
$data = $form->getData();
|
||||
$user = $model->getRepository()->findByIdentifier($data['identifier']);
|
||||
|
||||
/**
|
||||
* Calculation of time to standardize fix response for vulnerability
|
||||
* Users enumeration - forgot password. Constant response time is 1s.
|
||||
*/
|
||||
$desiredTime = 1.0;
|
||||
$startTime = microtime(true);
|
||||
|
||||
try {
|
||||
if (null !== $user) {
|
||||
$model->sendResetEmail($user);
|
||||
}
|
||||
$this->addFlashMessage('mautic.user.user.notice.passwordreset');
|
||||
} catch (\Exception) {
|
||||
$this->addFlashMessage('mautic.user.user.notice.passwordreset.error', [], 'error');
|
||||
}
|
||||
|
||||
$endTime = microtime(true);
|
||||
$executionTime = $endTime - $startTime;
|
||||
|
||||
if ($executionTime < $desiredTime) {
|
||||
usleep((int) (($desiredTime - $executionTime) * 1000000));
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('login');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
],
|
||||
'contentTemplate' => '@MauticUser/Security/reset.html.twig',
|
||||
'passthroughVars' => [
|
||||
'route' => $action,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function passwordResetConfirmAction(Request $request, UserPasswordHasherInterface $hasher): mixed
|
||||
{
|
||||
/** @var UserModel $model */
|
||||
$model = $this->getModel('user');
|
||||
|
||||
$data = ['identifier' => '', 'password' => '', 'password_confirm' => ''];
|
||||
$action = $this->generateUrl('mautic_user_passwordresetconfirm');
|
||||
$form = $this->formFactory->create(PasswordResetConfirmType::class, [], ['action' => $action]);
|
||||
$token = $request->query->get('token');
|
||||
|
||||
if ($token) {
|
||||
$request->getSession()->set('resetToken', $token);
|
||||
}
|
||||
|
||||
// /Check for a submitted form and process it
|
||||
if ('POST' === $request->getMethod()) {
|
||||
if ($isValid = $this->isFormValid($form)) {
|
||||
// find the user
|
||||
$data = $form->getData();
|
||||
/** @var User $user */
|
||||
$user = $model->getRepository()->findByIdentifier($data['identifier']);
|
||||
|
||||
if (null == $user) {
|
||||
$this->addFlashMessage('mautic.user.user.notice.passwordreset.success');
|
||||
|
||||
return $this->redirectToRoute('login');
|
||||
} else {
|
||||
if ($request->getSession()->has('resetToken')) {
|
||||
$resetToken = $request->getSession()->get('resetToken');
|
||||
|
||||
if ($model->confirmResetToken($user, $resetToken)) {
|
||||
$encodedPassword = $model->checkNewPassword($user, $hasher, $data['plainPassword']);
|
||||
$user->setPassword($encodedPassword);
|
||||
$model->saveEntity($user);
|
||||
|
||||
$this->addFlashMessage('mautic.user.user.notice.passwordreset.success');
|
||||
|
||||
$request->getSession()->remove('resetToken');
|
||||
|
||||
return $this->redirectToRoute('login');
|
||||
}
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
],
|
||||
'contentTemplate' => '@MauticUser/Security/resetconfirm.html.twig',
|
||||
'passthroughVars' => [
|
||||
'route' => $action,
|
||||
],
|
||||
]);
|
||||
} else {
|
||||
$this->addFlashMessage('mautic.user.user.notice.passwordreset.missingtoken');
|
||||
|
||||
return $this->redirectToRoute('mautic_user_passwordresetconfirm');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
],
|
||||
'contentTemplate' => '@MauticUser/Security/resetconfirm.html.twig',
|
||||
'passthroughVars' => [
|
||||
'route' => $action,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,485 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Controller\FormController;
|
||||
use Mautic\CoreBundle\Factory\PageHelperFactoryInterface;
|
||||
use Mautic\UserBundle\Entity;
|
||||
use Mautic\UserBundle\Entity\PermissionRepository;
|
||||
use Mautic\UserBundle\Entity\UserRepository;
|
||||
use Mautic\UserBundle\Model\RoleModel;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\PreconditionRequiredHttpException;
|
||||
|
||||
class RoleController extends FormController
|
||||
{
|
||||
/**
|
||||
* Generate's default role list view.
|
||||
*
|
||||
* @param int $page
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function indexAction(Request $request, PageHelperFactoryInterface $pageHelperFactory, $page = 1)
|
||||
{
|
||||
if (!$this->security->isGranted('user:roles:view')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$this->setListFilters();
|
||||
|
||||
$pageHelper = $pageHelperFactory->make('mautic.role', $page);
|
||||
|
||||
$limit = $pageHelper->getLimit();
|
||||
$start = $pageHelper->getStart();
|
||||
$orderBy = $request->getSession()->get('mautic.role.orderby', 'r.name');
|
||||
$orderByDir = $request->getSession()->get('mautic.role.orderbydir', 'ASC');
|
||||
$filter = $request->get('search', $request->getSession()->get('mautic.role.filter', ''));
|
||||
$tmpl = $request->isXmlHttpRequest() ? $request->get('tmpl', 'index') : 'index';
|
||||
$model = $this->getModel('user.role');
|
||||
\assert($model instanceof RoleModel);
|
||||
$items = $model->getEntities(
|
||||
[
|
||||
'start' => $start,
|
||||
'limit' => $limit,
|
||||
'filter' => $filter,
|
||||
'orderBy' => $orderBy,
|
||||
'orderByDir' => $orderByDir,
|
||||
]
|
||||
);
|
||||
|
||||
$request->getSession()->set('mautic.role.filter', $filter);
|
||||
|
||||
$count = count($items);
|
||||
if ($count && $count < ($start + 1)) {
|
||||
$lastPage = $pageHelper->countPage($count);
|
||||
$returnUrl = $this->generateUrl('mautic_role_index', ['page' => $lastPage]);
|
||||
$pageHelper->rememberPage($lastPage);
|
||||
|
||||
return $this->postActionRedirect([
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => [
|
||||
'page' => $lastPage,
|
||||
'tmpl' => $tmpl,
|
||||
],
|
||||
'contentTemplate' => 'Mautic\UserBundle\Controller\RoleController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_role_index',
|
||||
'mauticContent' => 'role',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$roleIds = [];
|
||||
|
||||
foreach ($items as $role) {
|
||||
$roleIds[] = $role->getId();
|
||||
}
|
||||
|
||||
$pageHelper->rememberPage($page);
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'items' => $items,
|
||||
'userCounts' => (!empty($roleIds)) ? $model->getRepository()->getUserCount($roleIds) : [],
|
||||
'searchValue' => $filter,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
'tmpl' => $tmpl,
|
||||
'permissions' => [
|
||||
'create' => $this->security->isGranted('user:roles:create'),
|
||||
'edit' => $this->security->isGranted('user:roles:edit'),
|
||||
'delete' => $this->security->isGranted('user:roles:delete'),
|
||||
],
|
||||
],
|
||||
'contentTemplate' => '@MauticUser/Role/list.html.twig',
|
||||
'passthroughVars' => [
|
||||
'route' => $this->generateUrl('mautic_role_index', ['page' => $page]),
|
||||
'mauticContent' => 'role',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate's new role form and processes post data.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
*/
|
||||
public function newAction(Request $request)
|
||||
{
|
||||
if (!$this->security->isGranted('user:roles:create')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
// retrieve the entity
|
||||
$entity = new Entity\Role();
|
||||
$model = $this->getModel('user.role');
|
||||
\assert($model instanceof RoleModel);
|
||||
|
||||
// set the return URL for post actions
|
||||
$returnUrl = $this->generateUrl('mautic_role_index');
|
||||
|
||||
// set the page we came from
|
||||
$page = $request->getSession()->get('mautic.role.page', 1);
|
||||
$action = $this->generateUrl('mautic_role_action', ['objectAction' => 'new']);
|
||||
|
||||
// get the user form factory
|
||||
$permissionsConfig = $this->getPermissionsConfig($entity);
|
||||
$form = $model->createForm($entity, $this->formFactory, $action, ['permissionsConfig' => $permissionsConfig['config']]);
|
||||
|
||||
// /Check for a submitted form and process it
|
||||
if ('POST' === $request->getMethod()) {
|
||||
$valid = false;
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
// set the permissions
|
||||
$role = $request->request->all()['role'] ?? [];
|
||||
$permissions = $role['permissions'] ?? null;
|
||||
$model->setRolePermissions($entity, $permissions);
|
||||
|
||||
// form is valid so process the data
|
||||
$model->saveEntity($entity);
|
||||
|
||||
$this->addFlashMessage('mautic.core.notice.created', [
|
||||
'%name%' => $entity->getName(),
|
||||
'%menu_link%' => 'mautic_role_index',
|
||||
'%url%' => $this->generateUrl('mautic_role_action', [
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => $entity->getId(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($cancelled || ($valid && $this->getFormButton($form, ['buttons', 'save'])->isClicked())) {
|
||||
return $this->postActionRedirect([
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\UserBundle\Controller\RoleController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_role_index',
|
||||
'mauticContent' => 'role',
|
||||
],
|
||||
]);
|
||||
} elseif ($valid) {
|
||||
return $this->editAction($request, $entity->getId(), true);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
'permissionsConfig' => $permissionsConfig,
|
||||
],
|
||||
'contentTemplate' => '@MauticUser/Role/form.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_role_new',
|
||||
'route' => $this->generateUrl('mautic_role_action', ['objectAction' => 'new']),
|
||||
'mauticContent' => 'role',
|
||||
'permissionList' => $permissionsConfig['list'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate's role edit form and processes post data.
|
||||
*
|
||||
* @param int $objectId
|
||||
* @param bool $ignorePost
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\JsonResponse|Response
|
||||
*/
|
||||
public function editAction(Request $request, $objectId, $ignorePost = false)
|
||||
{
|
||||
if (!$this->security->isGranted('user:roles:edit')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
/** @var RoleModel $model */
|
||||
$model = $this->getModel('user.role');
|
||||
$entity = $model->getEntity($objectId);
|
||||
|
||||
// set the page we came from
|
||||
$page = $request->getSession()->get('mautic.role.page', 1);
|
||||
|
||||
// set the return URL
|
||||
$returnUrl = $this->generateUrl('mautic_role_index', ['page' => $page]);
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\UserBundle\Controller\RoleController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_role_index',
|
||||
'mauticContent' => 'role',
|
||||
],
|
||||
];
|
||||
|
||||
// user not found
|
||||
if (null === $entity) {
|
||||
return $this->postActionRedirect(
|
||||
array_merge($postActionVars, [
|
||||
'flashes' => [
|
||||
[
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.user.role.error.notfound',
|
||||
'msgVars' => ['%id' => $objectId],
|
||||
],
|
||||
],
|
||||
])
|
||||
);
|
||||
} elseif ($model->isLocked($entity)) {
|
||||
// deny access if the entity is locked
|
||||
return $this->isLocked($postActionVars, $entity, 'user.role');
|
||||
}
|
||||
|
||||
$permissionsConfig = $this->getPermissionsConfig($entity);
|
||||
$action = $this->generateUrl('mautic_role_action', ['objectAction' => 'edit', 'objectId' => $objectId]);
|
||||
$form = $model->createForm($entity, $this->formFactory, $action, ['permissionsConfig' => $permissionsConfig['config']]);
|
||||
|
||||
// /Check for a submitted form and process it
|
||||
if (!$ignorePost && 'POST' === $request->getMethod()) {
|
||||
$valid = false;
|
||||
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
// set the permissions
|
||||
$role = $request->request->all()['role'] ?? [];
|
||||
$permissions = $role['permissions'] ?? null;
|
||||
$model->setRolePermissions($entity, $permissions);
|
||||
|
||||
// form is valid so process the data
|
||||
$model->saveEntity($entity, $this->getFormButton($form, ['buttons', 'save'])->isClicked());
|
||||
|
||||
$this->addFlashMessage('mautic.core.notice.updated', [
|
||||
'%name%' => $entity->getName(),
|
||||
'%menu_link%' => 'mautic_role_index',
|
||||
'%url%' => $this->generateUrl('mautic_role_action', [
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => $entity->getId(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
// unlock the entity
|
||||
$model->unlockEntity($entity);
|
||||
}
|
||||
|
||||
if ($cancelled || ($valid && $this->getFormButton($form, ['buttons', 'save'])->isClicked())) {
|
||||
return $this->postActionRedirect($postActionVars);
|
||||
} else {
|
||||
// the form has to be rebuilt because the permissions were updated
|
||||
$permissionsConfig = $this->getPermissionsConfig($entity);
|
||||
$form = $model->createForm($entity, $this->formFactory, $action, ['permissionsConfig' => $permissionsConfig['config']]);
|
||||
}
|
||||
} else {
|
||||
// lock the entity
|
||||
$model->lockEntity($entity);
|
||||
}
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
'permissionsConfig' => $permissionsConfig,
|
||||
],
|
||||
'contentTemplate' => '@MauticUser/Role/form.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_role_index',
|
||||
'route' => $action,
|
||||
'mauticContent' => 'role',
|
||||
'permissionList' => $permissionsConfig['list'],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function getPermissionsConfig(Entity\Role $role): array
|
||||
{
|
||||
$permissionObjects = $this->security->getPermissionObjects();
|
||||
$translator = $this->translator;
|
||||
$permissionRepo = $this->doctrine->getRepository(Entity\Permission::class);
|
||||
\assert($permissionRepo instanceof PermissionRepository);
|
||||
|
||||
$permissionsArray = ($role->getId()) ? $permissionRepo->getPermissionsByRole($role, true) : [];
|
||||
|
||||
$permissions = [];
|
||||
$permissionsList = [];
|
||||
/** @var \Mautic\CoreBundle\Security\Permissions\AbstractPermissions $object */
|
||||
foreach ($permissionObjects as $object) {
|
||||
if (!is_object($object)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($object->isEnabled()) {
|
||||
$bundle = $object->getName();
|
||||
$label = $translator->trans("mautic.{$bundle}.permissions.header");
|
||||
|
||||
// convert the permission bits from the db into readable names
|
||||
$data = $object->convertBitsToPermissionNames($permissionsArray);
|
||||
|
||||
// get the ratio of granted/total
|
||||
[$granted, $total] = $object->getPermissionRatio($data);
|
||||
|
||||
$permissions[$bundle] = [
|
||||
'label' => $label,
|
||||
'permissionObject' => $object,
|
||||
'ratio' => [$granted, $total],
|
||||
'data' => $data,
|
||||
];
|
||||
|
||||
$perms = $object->getPermissions();
|
||||
foreach ($perms as $level => $perm) {
|
||||
$levelPerms = array_keys($perm);
|
||||
$object->parseForJavascript($levelPerms);
|
||||
$permissionsList[$bundle][$level] = $levelPerms;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// order permissions by label
|
||||
uasort($permissions, fn ($a, $b): int => strnatcmp($a['label'], $b['label']));
|
||||
|
||||
return ['config' => $permissions, 'list' => $permissionsList];
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete's a role.
|
||||
*
|
||||
* @param int $objectId
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function deleteAction(Request $request, $objectId)
|
||||
{
|
||||
if (!$this->security->isGranted('user:roles:delete')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$page = $request->getSession()->get('mautic.role.page', 1);
|
||||
$returnUrl = $this->generateUrl('mautic_role_index', ['page' => $page]);
|
||||
$success = 0;
|
||||
$flashes = [];
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\UserBundle\Controller\RoleController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_role_index',
|
||||
'success' => $success,
|
||||
'mauticContent' => 'role',
|
||||
],
|
||||
];
|
||||
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
try {
|
||||
$model = $this->getModel('user.role');
|
||||
\assert($model instanceof RoleModel);
|
||||
$entity = $model->getEntity($objectId);
|
||||
|
||||
if (null === $entity) {
|
||||
$flashes[] = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.user.role.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
];
|
||||
} elseif ($model->isLocked($entity)) {
|
||||
return $this->isLocked($postActionVars, $entity, 'user.role');
|
||||
} else {
|
||||
$model->deleteEntity($entity);
|
||||
$name = $entity->getName();
|
||||
$flashes[] = [
|
||||
'type' => 'notice',
|
||||
'msg' => 'mautic.core.notice.deleted',
|
||||
'msgVars' => [
|
||||
'%name%' => $name,
|
||||
'%id%' => $objectId,
|
||||
],
|
||||
];
|
||||
}
|
||||
} catch (PreconditionRequiredHttpException $e) {
|
||||
$flashes[] = [
|
||||
'type' => 'error',
|
||||
'msg' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
} // else don't do anything
|
||||
|
||||
return $this->postActionRedirect(
|
||||
array_merge($postActionVars, [
|
||||
'flashes' => $flashes,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a group of entities.
|
||||
*/
|
||||
public function batchDeleteAction(Request $request, RoleModel $model): Response
|
||||
{
|
||||
$page = $request->getSession()->get('mautic.role.page', 1);
|
||||
$returnUrl = $this->generateUrl('mautic_role_index', ['page' => $page]);
|
||||
$flashes = [];
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\UserBundle\Controller\RoleController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_role_index',
|
||||
'mauticContent' => 'role',
|
||||
],
|
||||
];
|
||||
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
$ids = json_decode($request->query->get('ids', ''));
|
||||
$deleteIds = [];
|
||||
$userRepo = $this->doctrine->getRepository(Entity\User::class);
|
||||
\assert($userRepo instanceof UserRepository);
|
||||
|
||||
// Loop over the IDs to perform access checks pre-delete
|
||||
foreach ($ids as $objectId) {
|
||||
$entity = $model->getEntity($objectId);
|
||||
$users = $userRepo->findByRole($entity);
|
||||
|
||||
if (null === $entity) {
|
||||
$flashes[] = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.user.role.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
];
|
||||
} elseif (count($users)) {
|
||||
$flashes[] = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.user.role.error.deletenotallowed',
|
||||
'msgVars' => ['%name%' => $entity->getName()],
|
||||
];
|
||||
} elseif (!$this->security->isGranted('user:roles:delete')) {
|
||||
$flashes[] = $this->accessDenied(true);
|
||||
} elseif ($model->isLocked($entity)) {
|
||||
$flashes[] = $this->isLocked($postActionVars, $entity, 'user.role', true);
|
||||
} else {
|
||||
$deleteIds[] = $objectId;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete everything we are able to
|
||||
if (!empty($deleteIds)) {
|
||||
$entities = $model->deleteEntities($deleteIds);
|
||||
|
||||
$flashes[] = [
|
||||
'type' => 'notice',
|
||||
'msg' => 'mautic.user.role.notice.batch_deleted',
|
||||
'msgVars' => [
|
||||
'%count%' => count($entities),
|
||||
],
|
||||
];
|
||||
}
|
||||
} // else don't do anything
|
||||
|
||||
return $this->postActionRedirect(
|
||||
array_merge($postActionVars, [
|
||||
'flashes' => $flashes,
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Controller;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\CoreBundle\Controller\CommonController;
|
||||
use Mautic\CoreBundle\Factory\ModelFactory;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Service\FlashBag;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\PluginBundle\Helper\IntegrationHelper;
|
||||
use Mautic\UserBundle\Exception\WeakPasswordException;
|
||||
use Mautic\UserBundle\Security\SAML\Helper as SAMLHelper;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
use Symfony\Component\Security\Core\Exception;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class SecurityController extends CommonController implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
ManagerRegistry $doctrine,
|
||||
ModelFactory $modelFactory,
|
||||
UserHelper $userHelper,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
Translator $translator,
|
||||
FlashBag $flashBag,
|
||||
?RequestStack $requestStack,
|
||||
?CorePermissions $security,
|
||||
private AuthorizationCheckerInterface $authorizationChecker,
|
||||
) {
|
||||
parent::__construct($doctrine, $modelFactory, $userHelper, $coreParametersHelper, $dispatcher, $translator, $flashBag, $requestStack, $security);
|
||||
}
|
||||
|
||||
public function onRequest(RequestEvent $event): void
|
||||
{
|
||||
$controller = $event->getRequest()->attributes->get('_controller');
|
||||
\assert(is_string($controller));
|
||||
|
||||
if (!str_contains($controller, self::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// redirect user if they are already authenticated
|
||||
if ($this->authorizationChecker->isGranted('IS_AUTHENTICATED_FULLY')
|
||||
|| $this->authorizationChecker->isGranted('IS_AUTHENTICATED_REMEMBERED')
|
||||
) {
|
||||
$redirectUrl = $this->generateUrl('mautic_dashboard_index');
|
||||
$event->setResponse(new RedirectResponse($redirectUrl));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates login form and processes login.
|
||||
*/
|
||||
public function loginAction(Request $request, AuthenticationUtils $authenticationUtils, IntegrationHelper $integrationHelper, TranslatorInterface $translator): Response
|
||||
{
|
||||
$error = $authenticationUtils->getLastAuthenticationError();
|
||||
|
||||
if (null !== $error) {
|
||||
if ($error instanceof WeakPasswordException) {
|
||||
$this->addFlash(FlashBag::LEVEL_ERROR, $translator->trans('mautic.user.auth.error.weakpassword', [], 'flashes'));
|
||||
|
||||
return $this->forward('Mautic\UserBundle\Controller\PublicController::passwordResetAction');
|
||||
} elseif ($error instanceof Exception\BadCredentialsException) {
|
||||
$msg = 'mautic.user.auth.error.invalidlogin';
|
||||
} elseif ($error instanceof Exception\DisabledException) {
|
||||
$msg = 'mautic.user.auth.error.disabledaccount';
|
||||
} else {
|
||||
$msg = $error->getMessage();
|
||||
}
|
||||
|
||||
$this->addFlashMessage($msg, [], FlashBag::LEVEL_ERROR, null, false);
|
||||
}
|
||||
$request->query->set('tmpl', 'login');
|
||||
|
||||
// Get a list of SSO integrations
|
||||
$integrations = $integrationHelper->getIntegrationObjects(null, ['sso_service'], true, null, true);
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'last_username' => $authenticationUtils->getLastUsername(),
|
||||
'integrations' => $integrations,
|
||||
],
|
||||
'contentTemplate' => '@MauticUser/Security/login.html.twig',
|
||||
'passthroughVars' => [
|
||||
'route' => $this->generateUrl('login'),
|
||||
'mauticContent' => 'user',
|
||||
'sessionExpired' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Do nothing.
|
||||
*/
|
||||
public function loginCheckAction(): void
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* The plugin should be handling this in it's listener.
|
||||
*/
|
||||
public function ssoLoginAction($integration): RedirectResponse
|
||||
{
|
||||
return new RedirectResponse($this->generateUrl('login'));
|
||||
}
|
||||
|
||||
/**
|
||||
* The plugin should be handling this in it's listener.
|
||||
*/
|
||||
public function ssoLoginCheckAction($integration): RedirectResponse
|
||||
{
|
||||
// The plugin should be handling this in it's listener
|
||||
|
||||
return new RedirectResponse($this->generateUrl('login'));
|
||||
}
|
||||
|
||||
public function samlLoginRetryAction(Request $request, SAMLHelper $samlHelper, SessionInterface $session): Response
|
||||
{
|
||||
if (!$samlHelper->isSamlEnabled()) {
|
||||
return new RedirectResponse($this->generateUrl('login'));
|
||||
}
|
||||
|
||||
$session->invalidate();
|
||||
|
||||
$this->addFlashMessage('mautic.user.security.saml.clearsession', [], FlashBag::LEVEL_ERROR);
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'loginRoute' => $this->generateUrl('lightsaml_sp.discovery'),
|
||||
],
|
||||
'contentTemplate' => '@MauticUser/Security/saml_login_retry.html.twig',
|
||||
'passthroughVars' => [
|
||||
'route' => $this->generateUrl('mautic_base_index'),
|
||||
'mauticContent' => 'user',
|
||||
'sessionExpired' => true,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
KernelEvents::REQUEST => 'onRequest',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,628 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Controller;
|
||||
|
||||
use JMS\Serializer\SerializerInterface;
|
||||
use Mautic\CoreBundle\Controller\FormController;
|
||||
use Mautic\CoreBundle\Factory\PageHelperFactoryInterface;
|
||||
use Mautic\CoreBundle\Helper\InputHelper;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Helper\LanguageHelper;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use Mautic\CoreBundle\Model\FormModel;
|
||||
use Mautic\EmailBundle\Helper\MailHelper;
|
||||
use Mautic\UserBundle\Form\Type\ContactType;
|
||||
use Mautic\UserBundle\Model\RoleModel;
|
||||
use Mautic\UserBundle\Model\UserModel;
|
||||
use Mautic\UserBundle\Security\SAML\Helper as SAMLHelper;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
|
||||
class UserController extends FormController
|
||||
{
|
||||
/**
|
||||
* Generate's default user list.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\JsonResponse|Response
|
||||
*/
|
||||
public function indexAction(Request $request, PageHelperFactoryInterface $pageHelperFactory, int $page = 1)
|
||||
{
|
||||
if (!$this->security->isGranted('user:users:view')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
$pageHelper = $pageHelperFactory->make('mautic.user', $page);
|
||||
|
||||
$this->setListFilters();
|
||||
|
||||
$currentUserId = $this->user->getId();
|
||||
$limit = $pageHelper->getLimit();
|
||||
$start = $pageHelper->getStart();
|
||||
$orderBy = $request->getSession()->get('mautic.user.orderby', 'u.lastName, u.firstName, u.username');
|
||||
$orderByDir = $request->getSession()->get('mautic.user.orderbydir', 'ASC');
|
||||
$search = $request->get('search', $request->getSession()->get('mautic.user.filter', ''));
|
||||
$search = html_entity_decode($search);
|
||||
$request->getSession()->set('mautic.user.filter', $search);
|
||||
|
||||
// do some default filtering
|
||||
$filter = ['string' => $search, 'force' => ''];
|
||||
$tmpl = $request->isXmlHttpRequest() ? $request->get('tmpl', 'index') : 'index';
|
||||
$users = $this->getModel('user.user')->getEntities(
|
||||
[
|
||||
'start' => $start,
|
||||
'limit' => $limit,
|
||||
'filter' => $filter,
|
||||
'orderBy' => $orderBy,
|
||||
'orderByDir' => $orderByDir,
|
||||
]);
|
||||
|
||||
// Check to see if the number of pages match the number of users
|
||||
$count = count($users);
|
||||
if ($count && $count < ($start + 1)) {
|
||||
// the number of entities are now less then the current page so redirect to the last page
|
||||
$lastPage = $pageHelper->countPage($count);
|
||||
$pageHelper->rememberPage($lastPage);
|
||||
$returnUrl = $this->generateUrl('mautic_user_index', ['page' => $lastPage]);
|
||||
|
||||
return $this->postActionRedirect([
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => [
|
||||
'page' => $lastPage,
|
||||
'tmpl' => $tmpl,
|
||||
],
|
||||
'contentTemplate' => 'Mautic\UserBundle\Controller\UserController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_user_index',
|
||||
'mauticContent' => 'user',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$pageHelper->rememberPage($page);
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'items' => $users,
|
||||
'searchValue' => $search,
|
||||
'page' => $page,
|
||||
'limit' => $limit,
|
||||
'tmpl' => $tmpl,
|
||||
'currentUserId' => $currentUserId,
|
||||
'permissions' => [
|
||||
'create' => $this->security->isGranted('user:users:create'),
|
||||
'edit' => $this->security->isGranted('user:users:editother'),
|
||||
'delete' => $this->security->isGranted('user:users:deleteother'),
|
||||
],
|
||||
],
|
||||
'contentTemplate' => '@MauticUser/User/list.html.twig',
|
||||
'passthroughVars' => [
|
||||
'route' => $this->generateUrl('mautic_user_index', ['page' => $page]),
|
||||
'mauticContent' => 'user',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate's form and processes new post data.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\JsonResponse|Response
|
||||
*/
|
||||
public function newAction(Request $request, LanguageHelper $languageHelper, UserPasswordHasherInterface $hasher, SAMLHelper $samlHelper)
|
||||
{
|
||||
if (!$this->security->isGranted('user:users:create')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
/** @var UserModel $model */
|
||||
$model = $this->getModel('user.user');
|
||||
|
||||
// retrieve the user entity
|
||||
$user = $model->getEntity();
|
||||
|
||||
// set the return URL for post actions
|
||||
$returnUrl = $this->generateUrl('mautic_user_index');
|
||||
|
||||
// set the page we came from
|
||||
$page = $request->getSession()->get('mautic.user.page', 1);
|
||||
|
||||
// get the user form factory
|
||||
$action = $this->generateUrl('mautic_user_action', ['objectAction' => 'new']);
|
||||
$form = $model->createForm($user, $this->formFactory, $action);
|
||||
|
||||
// Check for a submitted form and process it
|
||||
if ('POST' === $request->getMethod()) {
|
||||
$valid = false;
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
// check to see if the password needs to be rehashed
|
||||
$formUser = $request->request->all()['user'] ?? [];
|
||||
$submittedPassword = $formUser['plainPassword']['password'] ?? null;
|
||||
$password = $model->checkNewPassword($user, $hasher, $submittedPassword);
|
||||
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
// form is valid so process the data
|
||||
$user->setPassword($password);
|
||||
$model->saveEntity($user);
|
||||
|
||||
// check if the user's locale has been downloaded already, fetch it if not
|
||||
$installedLanguages = $languageHelper->getSupportedLanguages();
|
||||
|
||||
if ($user->getLocale() && !array_key_exists($user->getLocale(), $installedLanguages)) {
|
||||
$fetchLanguage = $languageHelper->extractLanguagePackage($user->getLocale());
|
||||
|
||||
// If there is an error, we need to reset the user's locale to the default
|
||||
if ($fetchLanguage['error']) {
|
||||
$user->setLocale(null);
|
||||
$model->saveEntity($user);
|
||||
$message = 'mautic.core.could.not.set.language';
|
||||
$messageVars = [];
|
||||
|
||||
if (isset($fetchLanguage['message'])) {
|
||||
$message = $fetchLanguage['message'];
|
||||
}
|
||||
|
||||
if (isset($fetchLanguage['vars'])) {
|
||||
$messageVars = $fetchLanguage['vars'];
|
||||
}
|
||||
|
||||
$this->addFlashMessage($message, $messageVars);
|
||||
}
|
||||
}
|
||||
|
||||
$this->addFlashMessage('mautic.core.notice.created', [
|
||||
'%name%' => $user->getName(),
|
||||
'%menu_link%' => 'mautic_user_index',
|
||||
'%url%' => $this->generateUrl('mautic_user_action', [
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => $user->getId(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if ($cancelled || ($valid && $this->getFormButton($form, ['buttons', 'save'])->isClicked())) {
|
||||
return $this->postActionRedirect([
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page, 'isSamlUser' => false],
|
||||
'contentTemplate' => 'Mautic\UserBundle\Controller\UserController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_user_index',
|
||||
'mauticContent' => 'user',
|
||||
],
|
||||
]);
|
||||
} elseif ($valid && !$cancelled) {
|
||||
return $this->editAction($request, $languageHelper, $hasher, $samlHelper, $user->getId(), true);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => ['form' => $form->createView(), 'isSamlUser' => false],
|
||||
'contentTemplate' => '@MauticUser/User/form.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_user_new',
|
||||
'route' => $action,
|
||||
'mauticContent' => 'user',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates edit form and processes post data.
|
||||
*
|
||||
* @param int $objectId
|
||||
* @param bool $ignorePost
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\JsonResponse|Response
|
||||
*/
|
||||
public function editAction(Request $request, LanguageHelper $languageHelper, UserPasswordHasherInterface $hasher, SAMLHelper $samlHelper, $objectId, $ignorePost = false)
|
||||
{
|
||||
if (!$this->security->isGranted('user:users:edit')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
$model = $this->getModel('user.user');
|
||||
\assert($model instanceof UserModel);
|
||||
$user = $model->getEntity($objectId);
|
||||
if (null === $user) {
|
||||
return $this->postActionRedirect([
|
||||
'returnUrl' => $this->generateUrl('mautic_user_index'),
|
||||
'flashes' => [
|
||||
[
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.user.user.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
$oldEmail = $user->getEmail();
|
||||
|
||||
/** @var AuditLogModel $auditLogModel */
|
||||
$auditLogModel = $this->getModel('core.auditlog');
|
||||
$auditLogRepository = $auditLogModel->getRepository();
|
||||
$userActivity = $auditLogRepository->getLogsForUser($user);
|
||||
$users = $model->getEntities();
|
||||
|
||||
$roleModel = $this->getModel('user.role');
|
||||
\assert($roleModel instanceof RoleModel);
|
||||
$roleRepository = $roleModel->getRepository();
|
||||
$roles = $roleRepository->getEntities();
|
||||
|
||||
// set the page we came from
|
||||
$page = $request->getSession()->get('mautic.user.page', 1);
|
||||
|
||||
// set the return URL
|
||||
$returnUrl = $this->generateUrl('mautic_user_index', ['page' => $page]);
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\UserBundle\Controller\UserController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_user_index',
|
||||
'mauticContent' => 'user',
|
||||
],
|
||||
];
|
||||
|
||||
if ($model->isLocked($user)) {
|
||||
// deny access if the entity is locked
|
||||
return $this->isLocked($postActionVars, $user, 'user.user');
|
||||
}
|
||||
|
||||
$action = $this->generateUrl('mautic_user_action', ['objectAction' => 'edit', 'objectId' => $objectId]);
|
||||
$form = $model->createForm($user, $this->formFactory, $action);
|
||||
|
||||
$isSamlUser = $samlHelper->isSamlSession();
|
||||
if ($isSamlUser) {
|
||||
$form->remove('plainPassword');
|
||||
}
|
||||
|
||||
// /Check for a submitted form and process it
|
||||
if (!$ignorePost && 'POST' === $request->getMethod()) {
|
||||
$valid = false;
|
||||
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
// check to see if the password needs to be rehashed
|
||||
$formUser = $request->request->all()['user'] ?? [];
|
||||
$submittedPassword = $formUser['plainPassword']['password'] ?? null;
|
||||
$password = $model->checkNewPassword($user, $hasher, $submittedPassword);
|
||||
$newEmail = $formUser['email'] ?? null;
|
||||
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
// form is valid so process the data
|
||||
$user->setPassword($password);
|
||||
$model->saveEntity($user, $this->getFormButton($form, ['buttons', 'save'])->isClicked());
|
||||
if (!empty($submittedPassword)) {
|
||||
$model->sendChangePasswordInfo($user);
|
||||
}
|
||||
|
||||
if ($newEmail !== $oldEmail) {
|
||||
$model->sendChangeEmailInfo($oldEmail, $user);
|
||||
}
|
||||
|
||||
// check if the user's locale has been downloaded already, fetch it if not
|
||||
$installedLanguages = $languageHelper->getSupportedLanguages();
|
||||
|
||||
if ($user->getLocale() && !array_key_exists($user->getLocale(), $installedLanguages)) {
|
||||
$fetchLanguage = $languageHelper->extractLanguagePackage($user->getLocale());
|
||||
|
||||
// If there is an error, we need to reset the user's locale to the default
|
||||
if ($fetchLanguage['error']) {
|
||||
$user->setLocale(null);
|
||||
$model->saveEntity($user);
|
||||
$message = 'mautic.core.could.not.set.language';
|
||||
$messageVars = [];
|
||||
|
||||
if (isset($fetchLanguage['message'])) {
|
||||
$message = $fetchLanguage['message'];
|
||||
}
|
||||
|
||||
if (isset($fetchLanguage['vars'])) {
|
||||
$messageVars = $fetchLanguage['vars'];
|
||||
}
|
||||
|
||||
$this->addFlashMessage($message, $messageVars);
|
||||
}
|
||||
}
|
||||
|
||||
$this->addFlashMessage('mautic.core.notice.updated', [
|
||||
'%name%' => $user->getName(),
|
||||
'%menu_link%' => 'mautic_user_index',
|
||||
'%url%' => $this->generateUrl('mautic_user_action', [
|
||||
'objectAction' => 'edit',
|
||||
'objectId' => $user->getId(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
} else {
|
||||
// unlock the entity
|
||||
$model->unlockEntity($user);
|
||||
}
|
||||
|
||||
if ($cancelled || ($valid && $this->getFormButton($form, ['buttons', 'save'])->isClicked())) {
|
||||
return $this->postActionRedirect($postActionVars);
|
||||
}
|
||||
} else {
|
||||
// lock the entity
|
||||
$model->lockEntity($user);
|
||||
}
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
'logs' => $userActivity,
|
||||
'users' => $users,
|
||||
'roles' => $roles,
|
||||
'editAction' => true,
|
||||
'isSamlUser' => $isSamlUser,
|
||||
],
|
||||
'contentTemplate' => '@MauticUser/User/form.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_user_index',
|
||||
'route' => $action,
|
||||
'mauticContent' => 'user',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a user object.
|
||||
*
|
||||
* @param int $objectId
|
||||
*
|
||||
* @return Response
|
||||
*/
|
||||
public function deleteAction(Request $request, $objectId)
|
||||
{
|
||||
if (!$this->security->isGranted('user:users:delete')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$currentUser = $this->user;
|
||||
$page = $request->getSession()->get('mautic.user.page', 1);
|
||||
$returnUrl = $this->generateUrl('mautic_user_index', ['page' => $page]);
|
||||
$success = 0;
|
||||
$flashes = [];
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\UserBundle\Controller\UserController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_user_index',
|
||||
'route' => $returnUrl,
|
||||
'success' => $success,
|
||||
'mauticContent' => 'user',
|
||||
],
|
||||
];
|
||||
if ('POST' === $request->getMethod()) {
|
||||
// ensure the user logged in is not getting deleted
|
||||
if ((int) $currentUser->getId() !== (int) $objectId) {
|
||||
$model = $this->getModel('user.user');
|
||||
\assert($model instanceof UserModel);
|
||||
$entity = $model->getEntity($objectId);
|
||||
|
||||
if (null === $entity) {
|
||||
$flashes[] = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.user.user.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
];
|
||||
} elseif ($model->isLocked($entity)) {
|
||||
return $this->isLocked($postActionVars, $entity, 'user.user');
|
||||
} else {
|
||||
$model->deleteEntity($entity);
|
||||
$name = $entity->getName();
|
||||
$flashes[] = [
|
||||
'type' => 'notice',
|
||||
'msg' => 'mautic.core.notice.deleted',
|
||||
'msgVars' => [
|
||||
'%name%' => $name,
|
||||
'%id%' => $objectId,
|
||||
],
|
||||
];
|
||||
}
|
||||
} else {
|
||||
$flashes[] = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.user.user.error.cannotdeleteself',
|
||||
];
|
||||
}
|
||||
} // else don't do anything
|
||||
|
||||
return $this->postActionRedirect(
|
||||
array_merge($postActionVars, [
|
||||
'flashes' => $flashes,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Contacts a user.
|
||||
*
|
||||
* @param int $objectId
|
||||
*/
|
||||
public function contactAction(Request $request, SerializerInterface $serializer, MailHelper $mailer, IpLookupHelper $ipLookupHelper, $objectId): Response|\Symfony\Component\HttpFoundation\RedirectResponse
|
||||
{
|
||||
$model = $this->getModel('user.user');
|
||||
$user = $model->getEntity($objectId);
|
||||
|
||||
// user not found
|
||||
if (null === $user) {
|
||||
return $this->postActionRedirect([
|
||||
'returnUrl' => $this->generateUrl('mautic_dashboard_index'),
|
||||
'contentTemplate' => 'Mautic\UserBundle\Controller\UserController::contactAction',
|
||||
'flashes' => [
|
||||
[
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.user.user.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
$action = $this->generateUrl('mautic_user_action', ['objectAction' => 'contact', 'objectId' => $objectId]);
|
||||
$form = $this->createForm(ContactType::class, [], ['action' => $action]);
|
||||
|
||||
$currentUser = $this->user;
|
||||
|
||||
if ('POST' === $request->getMethod()) {
|
||||
$contact = $request->request->all()['contact'] ?? [];
|
||||
$formUrl = $contact['returnUrl'] ?? '';
|
||||
$returnUrl = $formUrl ? urldecode($formUrl) : $this->generateUrl('mautic_dashboard_index');
|
||||
$valid = false;
|
||||
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
if ($valid = $this->isFormValid($form)) {
|
||||
$subject = InputHelper::clean($form->get('msg_subject')->getData());
|
||||
$body = InputHelper::clean($form->get('msg_body')->getData());
|
||||
|
||||
$mailer->setFrom($currentUser->getEmail(), $currentUser->getName());
|
||||
$mailer->setSubject($subject);
|
||||
$mailer->setTo($user->getEmail(), $user->getName());
|
||||
$mailer->setBody($body);
|
||||
$mailer->send();
|
||||
|
||||
$reEntity = $form->get('entity')->getData();
|
||||
if (empty($reEntity)) {
|
||||
$bundle = $object = 'user';
|
||||
$entityId = $user->getId();
|
||||
} else {
|
||||
$bundle = $object = $reEntity;
|
||||
if (strpos($reEntity, ':')) {
|
||||
[$bundle, $object] = explode(':', $reEntity);
|
||||
}
|
||||
$entityId = $form->get('id')->getData();
|
||||
}
|
||||
|
||||
$details = $serializer->serialize([
|
||||
'from' => $currentUser->getName(),
|
||||
'to' => $user->getName(),
|
||||
'subject' => $subject,
|
||||
'message' => $body,
|
||||
], 'json');
|
||||
|
||||
$log = [
|
||||
'bundle' => $bundle,
|
||||
'object' => $object,
|
||||
'objectId' => $entityId,
|
||||
'action' => 'communication',
|
||||
'details' => $details,
|
||||
'ipAddress' => $ipLookupHelper->getIpAddressFromRequest(),
|
||||
];
|
||||
$auditLogModel = $this->getModel('core.auditlog');
|
||||
\assert($auditLogModel instanceof AuditLogModel);
|
||||
$auditLogModel->writeToLog($log);
|
||||
|
||||
$this->addFlashMessage('mautic.user.user.notice.messagesent', ['%name%' => $user->getName()]);
|
||||
}
|
||||
}
|
||||
if ($cancelled || $valid) {
|
||||
return $this->redirect($returnUrl);
|
||||
}
|
||||
} else {
|
||||
$reEntityId = (int) $request->get('id');
|
||||
$reSubject = InputHelper::clean($request->get('subject'));
|
||||
$returnUrl = InputHelper::clean($request->get('returnUrl', $this->generateUrl('mautic_dashboard_index')));
|
||||
$reEntity = InputHelper::clean($request->get('entity'));
|
||||
|
||||
$form->get('entity')->setData($reEntity);
|
||||
$form->get('id')->setData($reEntityId);
|
||||
$form->get('returnUrl')->setData($returnUrl);
|
||||
|
||||
if (!empty($reEntity) && !empty($reEntityId)) {
|
||||
/** @var FormModel<object> $model */
|
||||
$model = $this->getModel($reEntity);
|
||||
$entity = $model->getEntity($reEntityId);
|
||||
|
||||
if (null !== $entity) {
|
||||
$subject = $model->getUserContactSubject($reSubject, $entity);
|
||||
$form->get('msg_subject')->setData($subject);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'form' => $form->createView(),
|
||||
'user' => $user,
|
||||
],
|
||||
'contentTemplate' => '@MauticUser/User/contact.html.twig',
|
||||
'passthroughVars' => [
|
||||
'route' => $action,
|
||||
'mauticContent' => 'user',
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a group of entities.
|
||||
*/
|
||||
public function batchDeleteAction(Request $request): Response
|
||||
{
|
||||
$page = $request->getSession()->get('mautic.user.page', 1);
|
||||
$returnUrl = $this->generateUrl('mautic_user_index', ['page' => $page]);
|
||||
$flashes = [];
|
||||
|
||||
$postActionVars = [
|
||||
'returnUrl' => $returnUrl,
|
||||
'viewParameters' => ['page' => $page],
|
||||
'contentTemplate' => 'Mautic\UserBundle\Controller\UserController::indexAction',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_user_index',
|
||||
'mauticContent' => 'user',
|
||||
],
|
||||
];
|
||||
|
||||
if (Request::METHOD_POST === $request->getMethod()) {
|
||||
$model = $this->getModel('user');
|
||||
\assert($model instanceof UserModel);
|
||||
$ids = json_decode($request->query->get('ids', ''));
|
||||
$deleteIds = [];
|
||||
$currentUser = $this->user;
|
||||
|
||||
// Loop over the IDs to perform access checks pre-delete
|
||||
foreach ($ids as $objectId) {
|
||||
$entity = $model->getEntity($objectId);
|
||||
|
||||
if ((int) $currentUser->getId() === (int) $objectId) {
|
||||
$flashes[] = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.user.user.error.cannotdeleteself',
|
||||
];
|
||||
} elseif (null === $entity) {
|
||||
$flashes[] = [
|
||||
'type' => 'error',
|
||||
'msg' => 'mautic.user.user.error.notfound',
|
||||
'msgVars' => ['%id%' => $objectId],
|
||||
];
|
||||
} elseif (!$this->security->isGranted('user:users:delete')) {
|
||||
$flashes[] = $this->accessDenied(true);
|
||||
} elseif ($model->isLocked($entity)) {
|
||||
$flashes[] = $this->isLocked($postActionVars, $entity, 'user', true);
|
||||
} else {
|
||||
$deleteIds[] = $objectId;
|
||||
}
|
||||
}
|
||||
|
||||
// Delete everything we are able to
|
||||
if (!empty($deleteIds)) {
|
||||
$entities = $model->deleteEntities($deleteIds);
|
||||
|
||||
$flashes[] = [
|
||||
'type' => 'notice',
|
||||
'msg' => 'mautic.user.user.notice.batch_deleted',
|
||||
'msgVars' => [
|
||||
'%count%' => count($entities),
|
||||
],
|
||||
];
|
||||
}
|
||||
} // else don't do anything
|
||||
|
||||
return $this->postActionRedirect(
|
||||
array_merge($postActionVars, [
|
||||
'flashes' => $flashes,
|
||||
])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\DataFixtures\ORM;
|
||||
|
||||
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Mautic\UserBundle\Entity\Role;
|
||||
use Mautic\UserBundle\Model\RoleModel;
|
||||
|
||||
class LoadRoleData extends AbstractFixture implements OrderedFixtureInterface, FixtureGroupInterface
|
||||
{
|
||||
public static function getGroups(): array
|
||||
{
|
||||
return ['group_mautic_install_data'];
|
||||
}
|
||||
|
||||
public function __construct(
|
||||
private RoleModel $roleModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
if (!$this->hasReference('admin-role')) {
|
||||
$role = new Role();
|
||||
$role->setName('Administrators');
|
||||
$role->setDescription('Has access to everything.');
|
||||
$role->setIsAdmin(1);
|
||||
$manager->persist($role);
|
||||
$manager->flush();
|
||||
|
||||
$this->addReference('admin-role', $role);
|
||||
}
|
||||
|
||||
$role = new Role();
|
||||
$role->setName('Sales Team');
|
||||
$role->setDescription('Has access to sales');
|
||||
$role->setIsAdmin(0);
|
||||
|
||||
$permissions = [
|
||||
'user:profile' => ['editname'],
|
||||
'lead:leads' => ['full'],
|
||||
];
|
||||
$this->roleModel->setRolePermissions($role, $permissions);
|
||||
|
||||
$manager->persist($role);
|
||||
$manager->flush();
|
||||
|
||||
$this->addReference('sales-role', $role);
|
||||
}
|
||||
|
||||
public function getOrder()
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\DataFixtures\ORM;
|
||||
|
||||
use Doctrine\Bundle\FixturesBundle\FixtureGroupInterface;
|
||||
use Doctrine\Common\DataFixtures\AbstractFixture;
|
||||
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
|
||||
use Doctrine\Persistence\ObjectManager;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher;
|
||||
|
||||
class LoadUserData extends AbstractFixture implements OrderedFixtureInterface, FixtureGroupInterface
|
||||
{
|
||||
public static function getGroups(): array
|
||||
{
|
||||
return ['group_mautic_install_data'];
|
||||
}
|
||||
|
||||
public function __construct(
|
||||
private UserPasswordHasher $hasher,
|
||||
) {
|
||||
}
|
||||
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
$user = new User();
|
||||
$user->setFirstName('Admin');
|
||||
$user->setLastName('User');
|
||||
$user->setUsername('admin');
|
||||
$user->setEmail('admin@yoursite.com');
|
||||
$user->setPassword($this->hasher->hashPassword($user, 'Maut1cR0cks!'));
|
||||
$user->setRole($this->getReference('admin-role'));
|
||||
$manager->persist($user);
|
||||
$manager->flush();
|
||||
|
||||
$this->addReference('admin-user', $user);
|
||||
|
||||
$user = new User();
|
||||
$user->setFirstName('Sales');
|
||||
$user->setLastName('User');
|
||||
$user->setUsername('sales');
|
||||
$user->setEmail('sales@yoursite.com');
|
||||
$user->setPassword($this->hasher->hashPassword($user, 'Maut1cR0cks!'));
|
||||
$user->setRole($this->getReference('sales-role'));
|
||||
$manager->persist($user);
|
||||
$manager->flush();
|
||||
|
||||
$this->addReference('sales-user', $user);
|
||||
}
|
||||
|
||||
public function getOrder()
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\DependencyInjection\Compiler;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
||||
class FormLoginAuthenticatorOptionsPass implements CompilerPassInterface
|
||||
{
|
||||
public function process(ContainerBuilder $container): void
|
||||
{
|
||||
if (!$container->has('mautic.security.authenticator.form_login.decorator')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$decoratedServiceId = 'mautic.security.authenticator.form_login.decorator.inner';
|
||||
if (!$container->has($decoratedServiceId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$decoratedService = $container->getDefinition($decoratedServiceId);
|
||||
// Grab the options from the original definition
|
||||
$options = $decoratedService->getArgument(4);
|
||||
|
||||
$decorator = $container->getDefinition('mautic.security.authenticator.form_login.decorator');
|
||||
// Set the options for our decorated service
|
||||
$decorator->replaceArgument(3, $options);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\DependencyInjection\Compiler;
|
||||
|
||||
use Mautic\UserBundle\Security\Authenticator\Oauth2Authenticator;
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
|
||||
class OAuthReplacePass implements CompilerPassInterface
|
||||
{
|
||||
public function process(ContainerBuilder $container): void
|
||||
{
|
||||
if (!$container->hasDefinition('fos_oauth_server.security.authenticator.manager')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$oAuthAuthenticatorDefinition = $container->getDefinition('fos_oauth_server.security.authenticator.manager');
|
||||
$oAuthAuthenticatorDefinition->setClass(Oauth2Authenticator::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\DependencyInjection\Compiler;
|
||||
|
||||
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException;
|
||||
|
||||
/**
|
||||
* This will replace $options in the
|
||||
* \Mautic\UserBundle\DependencyInjection\Firewall\Factory\MauticSsoFactory::createAuthenticator.
|
||||
*/
|
||||
class SsoAuthenticatorPass implements CompilerPassInterface
|
||||
{
|
||||
public function process(ContainerBuilder $container): void
|
||||
{
|
||||
$ssoAuthenticatorId = 'security.authenticator.mautic_sso.main';
|
||||
if (!$container->hasDefinition($ssoAuthenticatorId)) {
|
||||
throw new ServiceNotFoundException($ssoAuthenticatorId);
|
||||
}
|
||||
|
||||
$formLoginAuthenticatorId = 'security.authenticator.form_login.main';
|
||||
if (!$container->hasDefinition($formLoginAuthenticatorId)) {
|
||||
throw new ServiceNotFoundException($formLoginAuthenticatorId);
|
||||
}
|
||||
|
||||
$loginFormAuthenticator = $container->getDefinition($formLoginAuthenticatorId);
|
||||
$formLoginOptions = $loginFormAuthenticator->getArgument(4);
|
||||
|
||||
if (!is_array($formLoginOptions)) {
|
||||
throw new InvalidArgumentException('The $options parameter must be an array. Maybe Symfony moved the parameter for the "form_login"?');
|
||||
}
|
||||
|
||||
$container->getDefinition($ssoAuthenticatorId)
|
||||
->replaceArgument('$options', $formLoginOptions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\DependencyInjection\Firewall\Factory;
|
||||
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AbstractFactory;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface;
|
||||
use Symfony\Component\DependencyInjection\ChildDefinition;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
final class MauticSsoFactory extends AbstractFactory implements AuthenticatorFactoryInterface
|
||||
{
|
||||
/**
|
||||
* Before form_login, otherwise the \Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator::supports
|
||||
* of form_login will return true, and the authentication will be finished with an error.
|
||||
* SSO Authenticator will be executed only if it has same options as form_login + request
|
||||
* will have 'integration_parameter' in the Request.
|
||||
*/
|
||||
public const PRIORITY = -25;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->addOption('username_parameter', '_username');
|
||||
$this->addOption('password_parameter', '_password');
|
||||
$this->addOption('integration_parameter', 'integration');
|
||||
$this->addOption('csrf_parameter', '_csrf_token');
|
||||
$this->addOption('csrf_token_id', 'authenticate');
|
||||
$this->addOption('enable_csrf', true);
|
||||
$this->addOption('post_only', true);
|
||||
$this->addOption('form_only', false);
|
||||
$this->addOption('login_path', '/s/login');
|
||||
$this->addOption('check_path', '/s/login_check');
|
||||
}
|
||||
|
||||
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string
|
||||
{
|
||||
$authenticatorId = 'security.authenticator.mautic_sso.'.$firewallName;
|
||||
|
||||
// use same auth handlers as in login_form
|
||||
$authenticationSuccessHandlerId = $this->getSuccessHandlerId($firewallName);
|
||||
$formLoginSuccessHandlerId = str_replace('mautic_sso', 'form_login', $authenticationSuccessHandlerId);
|
||||
$authenticationFailureHandlerId = $this->getFailureHandlerId($firewallName);
|
||||
$formLoginFailureHandlerId = str_replace('mautic_sso', 'form_login', $authenticationFailureHandlerId);
|
||||
|
||||
$authenticator = $container
|
||||
->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.mautic_sso'))
|
||||
->replaceArgument('$options', []) // Options will be replaced in \Mautic\UserBundle\DependencyInjection\Compiler\SsoAuthenticatorPass
|
||||
->replaceArgument('$userProvider', new Reference($userProviderId))
|
||||
->replaceArgument('$successHandler', new Reference($formLoginSuccessHandlerId))
|
||||
->replaceArgument('$failureHandler', new Reference($formLoginFailureHandlerId));
|
||||
|
||||
$container->setDefinition($authenticatorId, $authenticator);
|
||||
|
||||
return $authenticatorId;
|
||||
}
|
||||
|
||||
public function getKey(): string
|
||||
{
|
||||
return 'mautic-sso';
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return self::PRIORITY;
|
||||
}
|
||||
|
||||
public function getPosition(): string
|
||||
{
|
||||
return 'form';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $config
|
||||
*/
|
||||
protected function createAuthProvider(ContainerBuilder $container, string $id, array $config, string $userProviderId): string
|
||||
{
|
||||
throw new \Exception('The old authentication system is not supported with mautic-sso.');
|
||||
}
|
||||
|
||||
protected function getListenerId(): string
|
||||
{
|
||||
throw new \Exception('The old authentication system is not supported with mautic-sso.');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\DependencyInjection\Firewall\Factory;
|
||||
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AuthenticatorFactoryInterface;
|
||||
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
|
||||
use Symfony\Component\DependencyInjection\ChildDefinition;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Reference;
|
||||
|
||||
class PluginFactory implements AuthenticatorFactoryInterface
|
||||
{
|
||||
public const PRIORITY = -30;
|
||||
|
||||
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string
|
||||
{
|
||||
$authenticatorId = 'security.authentication.provider.mautic.'.$firewallName;
|
||||
|
||||
$authenticator = $container
|
||||
->setDefinition($authenticatorId, new ChildDefinition('security.authenticator.mautic_api'))
|
||||
->replaceArgument('$firewallName', $firewallName)
|
||||
->replaceArgument('$userProvider', new Reference($userProviderId));
|
||||
|
||||
$container->setDefinition($authenticatorId, $authenticator);
|
||||
|
||||
return $authenticatorId;
|
||||
}
|
||||
|
||||
public function getPriority(): int
|
||||
{
|
||||
return self::PRIORITY;
|
||||
}
|
||||
|
||||
public function getKey(): string
|
||||
{
|
||||
return 'mautic_plugin_auth';
|
||||
}
|
||||
|
||||
public function addConfiguration(NodeDefinition $node): void
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\DependencyInjection;
|
||||
|
||||
use Mautic\UserBundle\EventListener\SAMLSubscriber;
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Extension\Extension;
|
||||
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
|
||||
|
||||
class MauticUserExtension extends Extension
|
||||
{
|
||||
/**
|
||||
* @param mixed[] $configs
|
||||
*/
|
||||
public function load(array $configs, ContainerBuilder $container): void
|
||||
{
|
||||
$loader = new PhpFileLoader($container, new FileLocator(__DIR__.'/../Config'));
|
||||
$loader->load('services.php');
|
||||
|
||||
$samlEnabled = $container->getParameter('mautic.saml_enabled');
|
||||
if (true !== $samlEnabled) {
|
||||
$container->removeDefinition(SAMLSubscriber::class);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Enum;
|
||||
|
||||
final class UserTokenAuthorizator
|
||||
{
|
||||
public const RESET_PASSWORD_AUTHORIZATOR = 'reset-password';
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Event;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
class AuthenticationContentEvent extends Event
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
protected $content = [];
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $postLogout = false;
|
||||
|
||||
public function __construct(
|
||||
protected Request $request,
|
||||
) {
|
||||
$this->postLogout = $request->getSession()->get('post_logout', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Request
|
||||
*/
|
||||
public function getRequest()
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isLogout()
|
||||
{
|
||||
return $this->postLogout;
|
||||
}
|
||||
|
||||
public function addContent($content): void
|
||||
{
|
||||
$this->content[] = $content;
|
||||
}
|
||||
|
||||
public function getContent(): string
|
||||
{
|
||||
return implode("\n\n", $this->content);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Event;
|
||||
|
||||
use Mautic\PluginBundle\Integration\AbstractIntegration;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Mautic\UserBundle\Security\Authentication\Token\PluginToken;
|
||||
use Mautic\UserBundle\Security\Provider\UserProvider;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||
use Symfony\Component\Security\Core\User\ChainUserProvider;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
class AuthenticationEvent extends Event
|
||||
{
|
||||
/**
|
||||
* @var Response
|
||||
*/
|
||||
protected $response;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $isAuthenticated = false;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
protected $forceFailedAuthentication = false;
|
||||
|
||||
/**
|
||||
* @var UserProvider
|
||||
*/
|
||||
protected UserProviderInterface $userProvider;
|
||||
|
||||
protected bool $isFormLogin;
|
||||
|
||||
/**
|
||||
* Message to display to user if there is a failed authentication.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $failedAuthMessage;
|
||||
|
||||
/**
|
||||
* @param string|User|null $user
|
||||
* @param bool $isLoginCheck Event executed from the mautic_sso_login_check route typically used as the SSO callback
|
||||
* @param string $authenticatingService Service Service requesting authentication
|
||||
* @param array<AbstractIntegration>|null $integrations
|
||||
*/
|
||||
public function __construct(
|
||||
protected $user,
|
||||
protected TokenInterface $token,
|
||||
UserProviderInterface $userProvider,
|
||||
protected Request $request,
|
||||
protected $isLoginCheck = false,
|
||||
protected $authenticatingService = null,
|
||||
protected $integrations = null,
|
||||
) {
|
||||
$this->isFormLogin = $token instanceof UsernamePasswordToken;
|
||||
|
||||
if ($userProvider instanceof ChainUserProvider) {
|
||||
// Chain of user providers so let's find Mautic's
|
||||
$providers = $userProvider->getProviders();
|
||||
foreach ($providers as $provider) {
|
||||
if ($provider instanceof UserProvider) {
|
||||
$userProvider = $provider;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$this->userProvider = $userProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user returned by username search.
|
||||
*
|
||||
* @return string|User|null
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the user to be used after authentication.
|
||||
*
|
||||
* @param bool|true $saveUser
|
||||
* @param bool|true $createIfNotExists If true, the user will be created if it does not exist
|
||||
*/
|
||||
public function setUser(User $user, $saveUser = true, $createIfNotExists = true): void
|
||||
{
|
||||
if ($saveUser) {
|
||||
$user = $this->userProvider->saveUser($user, $createIfNotExists);
|
||||
}
|
||||
|
||||
$this->user = $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the token that has credentials, etc used to login.
|
||||
*
|
||||
* @return PluginToken
|
||||
*/
|
||||
public function getToken()
|
||||
{
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
public function setToken($service, TokenInterface $token): void
|
||||
{
|
||||
$this->token = $token;
|
||||
$this->authenticatingService = $service;
|
||||
$this->isAuthenticated = null !== $token->getUser();
|
||||
|
||||
$this->stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the username used.
|
||||
*/
|
||||
public function getUsername(): string
|
||||
{
|
||||
return $this->token->getUserIdentifier();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user provider to find and/or create new users.
|
||||
*
|
||||
* @return UserProvider
|
||||
*/
|
||||
public function getUserProvider()
|
||||
{
|
||||
return $this->userProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set if this user is successfully authenticated.
|
||||
*
|
||||
* @param string $service Service that authenticated the user; if using a Integration, it should match that of AbstractIntegration::getName();
|
||||
* @param bool|true $createIfNotExists
|
||||
*/
|
||||
public function setIsAuthenticated($service, ?User $user = null, $createIfNotExists = true): void
|
||||
{
|
||||
$this->authenticatingService = $service;
|
||||
|
||||
if (null !== $user) {
|
||||
$this->isAuthenticated = true;
|
||||
$this->setUser($user, $createIfNotExists);
|
||||
}
|
||||
|
||||
// Authenticated so stop propagation
|
||||
$this->stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the user has been authenticated.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isAuthenticated()
|
||||
{
|
||||
return $this->isAuthenticated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent any other authentication method from authorizing the user.
|
||||
* Mainly used to prevent a form login from trying to auth with the given password for a local user (think two-factor requirements).
|
||||
*/
|
||||
public function setIsFailedAuthentication(): void
|
||||
{
|
||||
$this->forceFailedAuthentication = true;
|
||||
|
||||
// Authenticated so stop propagation
|
||||
$this->stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the message to display to the user for failing auth.
|
||||
*/
|
||||
public function setFailedAuthenticationMessage($message): void
|
||||
{
|
||||
$this->failedAuthMessage = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns message to display to user for failing auth.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getFailedAuthenticationMessage()
|
||||
{
|
||||
return $this->failedAuthMessage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a plugin has forcefully failed authentication.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isFailed()
|
||||
{
|
||||
return $this->forceFailedAuthentication;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the service that authenticated the user.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getAuthenticatingService()
|
||||
{
|
||||
return $this->authenticatingService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a response such as a redirect.
|
||||
*/
|
||||
public function setResponse(Response $response): void
|
||||
{
|
||||
$this->response = $response;
|
||||
|
||||
// A response has been requested so stop propagation
|
||||
$this->stopPropagation();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the response if set by the listener.
|
||||
*
|
||||
* @return Response|null
|
||||
*/
|
||||
public function getResponse()
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the request.
|
||||
*
|
||||
* @return Request
|
||||
*/
|
||||
public function getRequest()
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a form login authentication request or pre-auth.
|
||||
*/
|
||||
public function isFormLogin(): bool
|
||||
{
|
||||
return $this->isFormLogin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the event is executed as the result of accessing mautic_sso_login_check.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isLoginCheck()
|
||||
{
|
||||
return $this->isLoginCheck;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return AbstractIntegration|bool
|
||||
*/
|
||||
public function getIntegration($integrationName)
|
||||
{
|
||||
return $this->integrations[$integrationName] ?? false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Event;
|
||||
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
class LoginEvent extends Event
|
||||
{
|
||||
public function __construct(
|
||||
private User $user,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User|null
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Event;
|
||||
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
class LogoutEvent extends Event
|
||||
{
|
||||
private array $session = [];
|
||||
|
||||
public function __construct(
|
||||
private User $user,
|
||||
private Request $request,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add value to session after it's been cleared.
|
||||
*/
|
||||
public function setPostSessionItem($key, $value): void
|
||||
{
|
||||
$this->session[$key] = $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get session items to be added after session has been cleared.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPostSessionItems()
|
||||
{
|
||||
return $this->session;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Request
|
||||
*/
|
||||
public function getRequest()
|
||||
{
|
||||
return $this->request;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Event;
|
||||
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
final class PasswordStrengthValidateEvent extends Event
|
||||
{
|
||||
public function __construct(
|
||||
public bool $isValid,
|
||||
public string $password,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Event;
|
||||
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
use Mautic\UserBundle\Entity\Role;
|
||||
|
||||
class RoleEvent extends CommonEvent
|
||||
{
|
||||
/**
|
||||
* @param bool $isNew
|
||||
*/
|
||||
public function __construct(Role &$role, $isNew = false)
|
||||
{
|
||||
$this->entity = &$role;
|
||||
$this->isNew = $isNew;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Role entity.
|
||||
*
|
||||
* @return Role
|
||||
*/
|
||||
public function getRole()
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the Role entity.
|
||||
*/
|
||||
public function setRole(Role $role): void
|
||||
{
|
||||
$this->entity = $role;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Event;
|
||||
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
|
||||
class UserEvent extends CommonEvent
|
||||
{
|
||||
/**
|
||||
* @param bool $isNew
|
||||
*/
|
||||
public function __construct(User &$user, $isNew = false)
|
||||
{
|
||||
$this->entity = &$user;
|
||||
$this->isNew = $isNew;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the User entity.
|
||||
*
|
||||
* @return User
|
||||
*/
|
||||
public function getUser()
|
||||
{
|
||||
return $this->entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the User entity.
|
||||
*/
|
||||
public function setUser(User $user): void
|
||||
{
|
||||
$this->entity = $user;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\EventListener;
|
||||
|
||||
use FOS\OAuthServerBundle\Security\Authenticator\Passport\Badge\AccessTokenBadge;
|
||||
use FOS\OAuthServerBundle\Security\Authenticator\Token\OAuthToken;
|
||||
use Mautic\UserBundle\Security\Authentication\Token\Permissions\TokenPermissions;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||
use Symfony\Component\Security\Http\Event\AuthenticationTokenCreatedEvent;
|
||||
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
|
||||
|
||||
class ApiUserSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(private UserProviderInterface $userProvider, private TokenPermissions $tokenPermissions)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the authentication if authentication is oAuth and has no UserLoader set.
|
||||
* Sets permissions, and if user is not yet fetched - gets the user from TokenStorage, or creates one.
|
||||
*/
|
||||
public function onCheckPassport(CheckPassportEvent $event): void
|
||||
{
|
||||
$passport = $event->getPassport();
|
||||
if (!$passport->hasBadge(UserBadge::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$badge = $passport->getBadge(UserBadge::class);
|
||||
\assert($badge instanceof UserBadge);
|
||||
if (null !== $badge->getUserLoader()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$passport->hasBadge(AccessTokenBadge::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$accessTokenBadge = $passport->getBadge(AccessTokenBadge::class);
|
||||
\assert($accessTokenBadge instanceof AccessTokenBadge);
|
||||
|
||||
$badge->setUserLoader(function (string $userIdentifier) use ($passport, $accessTokenBadge): ?UserInterface {
|
||||
$user = null;
|
||||
|
||||
try {
|
||||
$user = $this->userProvider->loadUserByIdentifier($userIdentifier);
|
||||
} catch (UserNotFoundException) {
|
||||
// ignore and get the user from oAuth2 token.
|
||||
}
|
||||
|
||||
$accessToken = $accessTokenBadge->getAccessToken();
|
||||
if (null === $user) {
|
||||
$user = $this->tokenPermissions->setActivePermissionsOnAuthToken($accessToken);
|
||||
}
|
||||
|
||||
if (null === $user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$passport->addBadge(new AccessTokenBadge($accessToken, $user->getRoles()));
|
||||
|
||||
return $user;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Transfers User instance from \Mautic\UserBundle\Security\Authentication\Token\Permissions\TokenPermissions::setActivePermissionsOnAuthToken
|
||||
* to the token, to be authorized later.
|
||||
*/
|
||||
public function onTokenCreated(AuthenticationTokenCreatedEvent $event): void
|
||||
{
|
||||
$passport = $event->getPassport();
|
||||
|
||||
if (!$passport->hasBadge(AccessTokenBadge::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$accessTokenBadge = $passport->getBadge(AccessTokenBadge::class);
|
||||
\assert($accessTokenBadge instanceof AccessTokenBadge);
|
||||
|
||||
$authenticatedToken = $event->getAuthenticatedToken();
|
||||
\assert($authenticatedToken instanceof OAuthToken);
|
||||
|
||||
if (null !== $authenticatedToken->getUser()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$authenticatedToken->setUser($accessTokenBadge->getAccessToken()->getUser());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|array{0: string, 1: int}|list<array{0: string, 1?: int}>>
|
||||
*/
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CheckPassportEvent::class => ['onCheckPassport', 2048],
|
||||
AuthenticationTokenCreatedEvent::class => 'onTokenCreated',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\EventListener;
|
||||
|
||||
use Mautic\ConfigBundle\ConfigEvents;
|
||||
use Mautic\ConfigBundle\Event\ConfigBuilderEvent;
|
||||
use Mautic\ConfigBundle\Event\ConfigEvent;
|
||||
use Mautic\UserBundle\Form\Type\ConfigType;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class ConfigSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private array $fileFields = [
|
||||
'saml_idp_metadata',
|
||||
'saml_idp_own_certificate',
|
||||
'saml_idp_own_private_key',
|
||||
];
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
ConfigEvents::CONFIG_ON_GENERATE => ['onConfigGenerate', 0],
|
||||
ConfigEvents::CONFIG_PRE_SAVE => ['onConfigSave', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onConfigGenerate(ConfigBuilderEvent $event): void
|
||||
{
|
||||
$event->addFileFields($this->fileFields)
|
||||
->addForm(
|
||||
[
|
||||
'bundle' => 'UserBundle',
|
||||
'formAlias' => 'userconfig',
|
||||
'formType' => ConfigType::class,
|
||||
'formTheme' => '@MauticUser/FormTheme/Config/_config_userconfig_widget.html.twig',
|
||||
'parameters' => $event->getParametersFromConfig('MauticUserBundle'),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function onConfigSave(ConfigEvent $event): void
|
||||
{
|
||||
// Preserve existing value
|
||||
$event->unsetIfEmpty('saml_idp_own_password');
|
||||
|
||||
$data = $event->getConfig('userconfig');
|
||||
|
||||
foreach ($this->fileFields as $field) {
|
||||
if (!isset($data[$field]) || !$data[$field] instanceof UploadedFile) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$data[$field] = $event->getFileContent($data[$field]);
|
||||
|
||||
switch ($field) {
|
||||
case 'saml_idp_metadata':
|
||||
if (!$this->validateXml($data[$field])) {
|
||||
$event->setError('mautic.user.saml.metadata.invalid', [], 'userconfig', $field);
|
||||
}
|
||||
break;
|
||||
case 'saml_idp_own_certificate':
|
||||
if (!str_starts_with($data[$field], '-----BEGIN CERTIFICATE-----')) {
|
||||
$event->setError('mautic.user.saml.certificate.invalid', [], 'userconfig', $field);
|
||||
}
|
||||
break;
|
||||
case 'saml_idp_own_private_key':
|
||||
$encryptedKey = str_starts_with($data[$field], '-----BEGIN ENCRYPTED PRIVATE KEY-----');
|
||||
$decryptedKey = str_starts_with($data[$field], '-----BEGIN RSA PRIVATE KEY-----');
|
||||
|
||||
if (!$encryptedKey && !$decryptedKey) {
|
||||
$event->setError('mautic.user.saml.private_key.invalid', [], 'userconfig', $field);
|
||||
}
|
||||
|
||||
if ($encryptedKey && empty($data['saml_idp_own_password'])) {
|
||||
$event->setError('mautic.user.saml.private_key.password_needed', [], 'userconfig', 'saml_idp_own_password');
|
||||
}
|
||||
|
||||
if ($encryptedKey && !empty($data['saml_idp_own_password']) && !openssl_get_privatekey($data[$field], $data['saml_idp_own_password'])) {
|
||||
$event->setError('mautic.user.saml.private_key.password_invalid', [], 'userconfig', 'saml_idp_own_password');
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$data[$field] = $event->encodeFileContents($data[$field]);
|
||||
}
|
||||
|
||||
$event->setConfig($data, 'userconfig');
|
||||
}
|
||||
|
||||
private function validateXml($content): bool
|
||||
{
|
||||
$valid = true;
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = simplexml_load_string($content);
|
||||
if (false === $doc) {
|
||||
$valid = false;
|
||||
libxml_clear_errors();
|
||||
}
|
||||
|
||||
return $valid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\UserBundle\Event\LogoutEvent;
|
||||
use Mautic\UserBundle\Model\UserModel;
|
||||
use Mautic\UserBundle\UserEvents;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
class LogoutListener implements \Symfony\Component\EventDispatcher\EventSubscriberInterface
|
||||
{
|
||||
protected ?\Mautic\UserBundle\Entity\User $user;
|
||||
|
||||
public function __construct(
|
||||
protected UserModel $userModel,
|
||||
protected EventDispatcherInterface $dispatcher,
|
||||
UserHelper $userHelper,
|
||||
) {
|
||||
$this->user = $userHelper->getUser();
|
||||
}
|
||||
|
||||
public function onLogout(\Symfony\Component\Security\Http\Event\LogoutEvent $logoutEvent): void
|
||||
{
|
||||
$request = $logoutEvent->getRequest();
|
||||
$session = $request->getSession();
|
||||
if ($this->dispatcher->hasListeners(UserEvents::USER_LOGOUT)) {
|
||||
$mauticEvent = new LogoutEvent($this->user, $request);
|
||||
$this->dispatcher->dispatch($mauticEvent, UserEvents::USER_LOGOUT);
|
||||
$sessionItems = $mauticEvent->getPostSessionItems();
|
||||
foreach ($sessionItems as $key => $value) {
|
||||
$session->set($key, $value);
|
||||
}
|
||||
}
|
||||
// Clear session
|
||||
$session->clear();
|
||||
|
||||
// Note that a logout occurred
|
||||
$session->set('post_logout', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [\Symfony\Component\Security\Http\Event\LogoutEvent::class => 'onLogout'];
|
||||
}
|
||||
|
||||
public function onSymfonyComponentSecurityHttpEventLogoutEvent(\Symfony\Component\Security\Http\Event\LogoutEvent $logoutEvent): void
|
||||
{
|
||||
$this->onLogout($logoutEvent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\EventListener;
|
||||
|
||||
use Mautic\UserBundle\Security\Authenticator\Passport\Badge\PasswordStrengthBadge;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
|
||||
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
|
||||
|
||||
/**
|
||||
* This must record a PW before CheckCredentialsListener will take an effect.
|
||||
* Subscriber will add a password check badge.
|
||||
*/
|
||||
final class PasswordStrengthSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CheckPassportEvent::class => ['checkPassport', 100],
|
||||
];
|
||||
}
|
||||
|
||||
public function checkPassport(CheckPassportEvent $event): void
|
||||
{
|
||||
$passport = $event->getPassport();
|
||||
if (!$passport->hasBadge(PasswordCredentials::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$badge = $passport->getBadge(PasswordCredentials::class);
|
||||
\assert($badge instanceof PasswordCredentials);
|
||||
$presentedPassword = $badge->getPassword();
|
||||
|
||||
$passport->addBadge(new PasswordStrengthBadge($presentedPassword));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\EventListener;
|
||||
|
||||
use Mautic\UserBundle\Exception\WeakPasswordException;
|
||||
use Mautic\UserBundle\Model\PasswordStrengthEstimatorModel;
|
||||
use Mautic\UserBundle\Security\Authenticator\Passport\Badge\PasswordStrengthBadge;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
|
||||
|
||||
final class PasswordSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(private PasswordStrengthEstimatorModel $passwordStrengthEstimatorModel)
|
||||
{
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CheckPassportEvent::class => ['checkPassport', -100], // After default password checker
|
||||
];
|
||||
}
|
||||
|
||||
public function checkPassport(CheckPassportEvent $event): void
|
||||
{
|
||||
$passport = $event->getPassport();
|
||||
if (!$passport->hasBadge(PasswordStrengthBadge::class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$badge = $passport->getBadge(PasswordStrengthBadge::class);
|
||||
\assert($badge instanceof PasswordStrengthBadge);
|
||||
$presentedPassword = $badge->getPresentedPassword();
|
||||
if ('' === $presentedPassword) {
|
||||
throw new BadCredentialsException('The presented password cannot be empty.');
|
||||
}
|
||||
|
||||
if (!$this->passwordStrengthEstimatorModel->validate($presentedPassword)) {
|
||||
throw new WeakPasswordException();
|
||||
}
|
||||
|
||||
$badge->setResolved();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\EventListener;
|
||||
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
|
||||
class SAMLSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private RouterInterface $router,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
KernelEvents::REQUEST => ['onKernelRequest', 256],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Block access to SAML URLs if SAML is disabled.
|
||||
* This listener is removed from Kernel if SAML is not enabled. See mautic.saml_enabled parameter.
|
||||
*/
|
||||
public function onKernelRequest(RequestEvent $event): void
|
||||
{
|
||||
if (!$event->isMainRequest()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$request = $event->getRequest();
|
||||
$route = (string) $request->attributes->get('_route');
|
||||
$url = (string) $request->getRequestUri();
|
||||
if (!str_contains($route, 'lightsaml') && !str_contains($url, '/saml/')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Redirect to standard login page if SAML is disabled
|
||||
$event->setResponse(
|
||||
new RedirectResponse($this->router->generate('login'))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\CoreEvents;
|
||||
use Mautic\CoreBundle\DTO\GlobalSearchFilterDTO;
|
||||
use Mautic\CoreBundle\Event as MauticEvents;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Service\GlobalSearch;
|
||||
use Mautic\UserBundle\Model\RoleModel;
|
||||
use Mautic\UserBundle\Model\UserModel;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class SearchSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private UserModel $userModel,
|
||||
private RoleModel $userRoleModel,
|
||||
private CorePermissions $security,
|
||||
private GlobalSearch $globalSearch,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
CoreEvents::GLOBAL_SEARCH => [
|
||||
['onGlobalSearchUser', 0],
|
||||
['onGlobalSearchRoles', 0],
|
||||
],
|
||||
CoreEvents::BUILD_COMMAND_LIST => ['onBuildCommandList', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onGlobalSearchUser(MauticEvents\GlobalSearchEvent $event): void
|
||||
{
|
||||
$filterDTO = new GlobalSearchFilterDTO($event->getSearchString());
|
||||
$results = $this->globalSearch->performSearch(
|
||||
$filterDTO,
|
||||
$this->userModel,
|
||||
'@MauticUser/SubscribedEvents/Search/global_user.html.twig'
|
||||
);
|
||||
|
||||
if (!empty($results)) {
|
||||
$event->addResults('mautic.user.users', $results);
|
||||
}
|
||||
}
|
||||
|
||||
public function onGlobalSearchRoles(MauticEvents\GlobalSearchEvent $event): void
|
||||
{
|
||||
$filterDTO = new GlobalSearchFilterDTO($event->getSearchString());
|
||||
$results = $this->globalSearch->performSearch(
|
||||
$filterDTO,
|
||||
$this->userRoleModel,
|
||||
'@MauticUser/SubscribedEvents/Search/global_role.html.twig'
|
||||
);
|
||||
|
||||
if (!empty($results)) {
|
||||
$event->addResults('mautic.user.roles', $results);
|
||||
}
|
||||
}
|
||||
|
||||
public function onBuildCommandList(MauticEvents\CommandListEvent $event): void
|
||||
{
|
||||
if ($this->security->isGranted('user:users:view')) {
|
||||
$event->addCommands(
|
||||
'mautic.user.users',
|
||||
$this->userModel->getCommandList()
|
||||
);
|
||||
}
|
||||
if ($this->security->isGranted('user:roles:view')) {
|
||||
$event->addCommands(
|
||||
'mautic.user.roles',
|
||||
$this->userRoleModel->getCommandList()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use Mautic\UserBundle\Event\LoginEvent;
|
||||
use Mautic\UserBundle\UserEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class SecuritySubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private IpLookupHelper $ipLookupHelper,
|
||||
private AuditLogModel $auditLogModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
UserEvents::USER_LOGIN => ['onSecurityInteractiveLogin', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onSecurityInteractiveLogin(LoginEvent $event): void
|
||||
{
|
||||
$userId = (int) $event->getUser()->getId();
|
||||
$useName = $event->getUser()->getUserIdentifier();
|
||||
|
||||
$log = [
|
||||
'bundle' => 'user',
|
||||
'object' => 'security',
|
||||
'objectId' => $userId,
|
||||
'action' => 'login',
|
||||
'details' => ['username' => $useName],
|
||||
'ipAddress' => $this->ipLookupHelper->getIpAddressFromRequest(),
|
||||
];
|
||||
|
||||
$this->auditLogModel->writeToLog($log);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\EventListener;
|
||||
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use Mautic\UserBundle\Event as Events;
|
||||
use Mautic\UserBundle\UserEvents;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class UserSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private IpLookupHelper $ipLookupHelper,
|
||||
private AuditLogModel $auditLogModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
UserEvents::USER_POST_SAVE => ['onUserPostSave', 0],
|
||||
UserEvents::USER_POST_DELETE => ['onUserDelete', 0],
|
||||
UserEvents::ROLE_POST_SAVE => ['onRolePostSave', 0],
|
||||
UserEvents::ROLE_POST_DELETE => ['onRoleDelete', 0],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a user entry to the audit log.
|
||||
*/
|
||||
public function onUserPostSave(Events\UserEvent $event): void
|
||||
{
|
||||
$user = $event->getUser();
|
||||
|
||||
if ($details = $event->getChanges()) {
|
||||
$log = [
|
||||
'bundle' => 'user',
|
||||
'object' => 'user',
|
||||
'objectId' => $user->getId(),
|
||||
'action' => ($event->isNew()) ? 'create' : 'update',
|
||||
'details' => $details,
|
||||
'ipAddress' => $this->ipLookupHelper->getIpAddressFromRequest(),
|
||||
];
|
||||
$this->auditLogModel->writeToLog($log);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a user delete entry to the audit log.
|
||||
*/
|
||||
public function onUserDelete(Events\UserEvent $event): void
|
||||
{
|
||||
$user = $event->getUser();
|
||||
$log = [
|
||||
'bundle' => 'user',
|
||||
'object' => 'user',
|
||||
'objectId' => $user->deletedId,
|
||||
'action' => 'delete',
|
||||
'details' => ['name' => $user->getName()],
|
||||
'ipAddress' => $this->ipLookupHelper->getIpAddressFromRequest(),
|
||||
];
|
||||
$this->auditLogModel->writeToLog($log);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a role entry to the audit log.
|
||||
*/
|
||||
public function onRolePostSave(Events\RoleEvent $event): void
|
||||
{
|
||||
$role = $event->getRole();
|
||||
if ($details = $event->getChanges()) {
|
||||
$log = [
|
||||
'bundle' => 'user',
|
||||
'object' => 'role',
|
||||
'objectId' => $role->getId(),
|
||||
'action' => ($event->isNew()) ? 'create' : 'update',
|
||||
'details' => $details,
|
||||
'ipAddress' => $this->ipLookupHelper->getIpAddressFromRequest(),
|
||||
];
|
||||
$this->auditLogModel->writeToLog($log);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a role delete entry to the audit log.
|
||||
*/
|
||||
public function onRoleDelete(Events\RoleEvent $event): void
|
||||
{
|
||||
$role = $event->getRole();
|
||||
$log = [
|
||||
'bundle' => 'user',
|
||||
'object' => 'role',
|
||||
'objectId' => $role->deletedId,
|
||||
'action' => 'delete',
|
||||
'details' => ['name' => $role->getName()],
|
||||
'ipAddress' => $this->ipLookupHelper->getIpAddressFromRequest(),
|
||||
];
|
||||
$this->auditLogModel->writeToLog($log);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Exception;
|
||||
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
|
||||
class WeakPasswordException extends AuthenticationException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Form\Type;
|
||||
|
||||
use Mautic\ConfigBundle\Form\Type\ConfigFileType;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\Validator\Constraints\File;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class ConfigType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
protected CoreParametersHelper $parameters,
|
||||
protected TranslatorInterface $translator,
|
||||
) {
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$samlEntityIdChoices = ['', rtrim($this->parameters->get('mautic.site_url'), '/')];
|
||||
if (!empty($this->parameters->get('mautic.subdomain_url'))) {
|
||||
$samlEntityIdChoices[] = rtrim($this->parameters->get('mautic.subdomain_url'), '/');
|
||||
}
|
||||
$builder->add('saml_idp_entity_id', ChoiceType::class,
|
||||
[
|
||||
'choices' => array_combine($samlEntityIdChoices, $samlEntityIdChoices),
|
||||
'label' => 'mautic.user.config.form.saml.idp_entity_id_label',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'required' => true,
|
||||
'multiple' => false,
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
],
|
||||
]);
|
||||
|
||||
$builder->add(
|
||||
'saml_idp_metadata',
|
||||
ConfigFileType::class,
|
||||
[
|
||||
'label' => 'mautic.user.config.form.saml.idp.metadata',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.user.config.form.saml.idp.metadata.tooltip',
|
||||
'rows' => 10,
|
||||
],
|
||||
'required' => false,
|
||||
'constraints' => [
|
||||
new File(
|
||||
[
|
||||
'mimeTypes' => ['text/plain', 'text/xml', 'application/xml'],
|
||||
'mimeTypesMessage' => 'mautic.core.invalid_file_type',
|
||||
]
|
||||
),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'saml_idp_own_certificate',
|
||||
ConfigFileType::class,
|
||||
[
|
||||
'label' => 'mautic.user.config.form.saml.idp.own_certificate',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.user.config.form.saml.idp.own_certificate.tooltip',
|
||||
],
|
||||
'required' => false,
|
||||
'constraints' => [
|
||||
new File(
|
||||
[
|
||||
'mimeTypes' => ['text/plain'],
|
||||
'mimeTypesMessage' => 'mautic.core.invalid_file_type',
|
||||
]
|
||||
),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'saml_idp_own_private_key',
|
||||
ConfigFileType::class,
|
||||
[
|
||||
'label' => 'mautic.user.config.form.saml.idp.own_private_key',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.user.config.form.saml.idp.own_private_key.tooltip',
|
||||
],
|
||||
'required' => false,
|
||||
'constraints' => [
|
||||
new File(
|
||||
[
|
||||
'mimeTypes' => ['text/plain'],
|
||||
'mimeTypesMessage' => 'mautic.core.invalid_file_type',
|
||||
]
|
||||
),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'saml_idp_own_password',
|
||||
PasswordType::class,
|
||||
[
|
||||
'label' => 'mautic.user.config.form.saml.idp.own_password',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.user.config.form.saml.idp.own_password.tooltip',
|
||||
],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'saml_idp_email_attribute',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.user.config.form.saml.idp.attribute_email',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
],
|
||||
'empty_data' => 'EmailAddress',
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'saml_idp_username_attribute',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.user.config.form.saml.idp.attribute_username',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'saml_idp_firstname_attribute',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.user.config.form.saml.idp.attribute_firstname',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
],
|
||||
'empty_data' => 'FirstName',
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'saml_idp_lastname_attribute',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.user.config.form.saml.idp.attribute_lastname',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
],
|
||||
'empty_data' => 'LastName',
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'saml_idp_default_role',
|
||||
RoleListType::class,
|
||||
[
|
||||
'label' => 'mautic.user.config.form.saml.idp.default_role',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'data-placeholder' => $this->translator->trans('mautic.user.config.form.saml.idp.disable_creation'),
|
||||
'tooltip' => 'mautic.user.config.form.saml.idp.default_role.tooltip',
|
||||
],
|
||||
'required' => false,
|
||||
'placeholder' => '',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
$view->vars['entityId'] = $this->parameters->get('mautic.saml_idp_entity_id');
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'userconfig';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Form\Type;
|
||||
|
||||
use Mautic\CoreBundle\Form\Type\FormButtonsType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Validator\Constraints\Length;
|
||||
use Symfony\Component\Validator\Constraints\NotBlank;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class ContactType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @param FormBuilderInterface<array<mixed>|null> $builder
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add(
|
||||
'msg_subject',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.email.subject',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control'],
|
||||
'constraints' => [
|
||||
new NotBlank(['message' => 'Subject should not be blank.']),
|
||||
new Length(['min' => 3]),
|
||||
],
|
||||
]
|
||||
)
|
||||
->add(
|
||||
'msg_body',
|
||||
TextareaType::class,
|
||||
[
|
||||
'label' => 'mautic.user.user.contact.message',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'rows' => 10,
|
||||
],
|
||||
'constraints' => [
|
||||
new NotBlank(['message' => 'Message should not be blank.']),
|
||||
new Length(['min' => 5]),
|
||||
],
|
||||
]
|
||||
)
|
||||
->add(
|
||||
'entity',
|
||||
HiddenType::class,
|
||||
[
|
||||
'attr' => [
|
||||
'autocomplete' => 'off',
|
||||
],
|
||||
]
|
||||
)
|
||||
->add(
|
||||
'id',
|
||||
HiddenType::class,
|
||||
[
|
||||
'attr' => [
|
||||
'autocomplete' => 'off',
|
||||
],
|
||||
]
|
||||
)
|
||||
->add(
|
||||
'returnUrl',
|
||||
HiddenType::class,
|
||||
[
|
||||
'attr' => [
|
||||
'autocomplete' => 'off',
|
||||
],
|
||||
]
|
||||
)
|
||||
->add('buttons', FormButtonsType::class, [
|
||||
'save_text' => 'mautic.user.user.contact.send',
|
||||
'save_icon' => 'ri-send-plane-line',
|
||||
'apply_text' => false,
|
||||
]);
|
||||
|
||||
if (!empty($options['action'])) {
|
||||
$builder->setAction($options['action']);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Form\Type;
|
||||
|
||||
use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber;
|
||||
use Mautic\UserBundle\Form\Validator\Constraints\NotWeak;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class PasswordResetConfirmType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->addEventSubscriber(new CleanFormSubscriber([]));
|
||||
|
||||
$builder->add(
|
||||
'identifier',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.user.auth.form.loginusername',
|
||||
'label_attr' => ['class' => 'sr-only'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'preaddon' => 'ri-user-6-fill',
|
||||
'placeholder' => 'mautic.user.auth.form.loginusername',
|
||||
],
|
||||
'required' => true,
|
||||
'constraints' => [
|
||||
new Assert\NotBlank(['message' => 'mautic.user.user.passwordreset.notblank']),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'plainPassword',
|
||||
RepeatedType::class,
|
||||
[
|
||||
'first_name' => 'password',
|
||||
'first_options' => [
|
||||
'label' => 'mautic.core.password',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'placeholder' => 'mautic.user.user.passwordreset.password.placeholder',
|
||||
'tooltip' => 'mautic.user.user.form.help.passwordrequirements',
|
||||
'preaddon' => 'ri-lock-fill',
|
||||
'autocomplete' => 'off',
|
||||
],
|
||||
'required' => true,
|
||||
'error_bubbling' => false,
|
||||
'constraints' => [
|
||||
new Assert\NotBlank(['message' => 'mautic.user.user.passwordreset.notblank']),
|
||||
new Assert\Length([
|
||||
'min' => 6,
|
||||
'minMessage' => 'mautic.user.user.password.minlength',
|
||||
]),
|
||||
new NotWeak([
|
||||
'message' => 'mautic.user.user.password.weak',
|
||||
]),
|
||||
],
|
||||
],
|
||||
'second_name' => 'confirm',
|
||||
'second_options' => [
|
||||
'label' => 'mautic.user.user.form.passwordconfirm',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'placeholder' => 'mautic.user.user.passwordreset.confirm.placeholder',
|
||||
'tooltip' => 'mautic.user.user.form.help.passwordrequirements',
|
||||
'preaddon' => 'ri-lock-fill',
|
||||
'autocomplete' => 'off',
|
||||
],
|
||||
'required' => true,
|
||||
'error_bubbling' => false,
|
||||
'constraints' => [
|
||||
new Assert\NotBlank(['message' => 'mautic.user.user.passwordreset.notblank']),
|
||||
],
|
||||
],
|
||||
'type' => PasswordType::class,
|
||||
'invalid_message' => 'mautic.user.user.password.mismatch',
|
||||
'required' => true,
|
||||
'error_bubbling' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'submit',
|
||||
SubmitType::class,
|
||||
[
|
||||
'attr' => [
|
||||
'class' => 'btn btn-lg btn-primary btn-block',
|
||||
],
|
||||
'label' => 'mautic.user.user.passwordreset.reset',
|
||||
]
|
||||
);
|
||||
|
||||
if (!empty($options['action'])) {
|
||||
$builder->setAction($options['action']);
|
||||
}
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'passwordresetconfirm';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Form\Type;
|
||||
|
||||
use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class PasswordResetType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->addEventSubscriber(new CleanFormSubscriber([]));
|
||||
|
||||
$builder->add(
|
||||
'identifier',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.user.auth.form.loginusername',
|
||||
'label_attr' => ['class' => 'sr-only'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'preaddon' => 'ri-user-6-fill',
|
||||
'placeholder' => 'mautic.user.auth.form.loginusername',
|
||||
],
|
||||
'constraints' => [
|
||||
new Assert\NotBlank(['message' => 'mautic.user.user.passwordreset.notblank']),
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'submit',
|
||||
SubmitType::class,
|
||||
[
|
||||
'attr' => [
|
||||
'class' => 'btn btn-lg btn-primary btn-block',
|
||||
],
|
||||
'label' => 'mautic.user.user.passwordreset.reset',
|
||||
]
|
||||
);
|
||||
|
||||
if (!empty($options['action'])) {
|
||||
$builder->setAction($options['action']);
|
||||
}
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'passwordreset';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class PermissionListType extends AbstractType
|
||||
{
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setRequired(['bundle', 'level']);
|
||||
|
||||
$resolver->setDefaults([
|
||||
'multiple' => true,
|
||||
'expanded' => true,
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => fn (Options $options): array => [
|
||||
'data-permission' => $options['bundle'].':'.$options['level'],
|
||||
'onchange' => 'Mautic.onPermissionChange(this, \''.$options['bundle'].'\')',
|
||||
],
|
||||
'choices_as_values' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getParent(): ?string
|
||||
{
|
||||
return ChoiceType::class;
|
||||
}
|
||||
|
||||
public function getBlockPrefix(): string
|
||||
{
|
||||
return 'permissionlist';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\Valid;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class PermissionsType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
foreach ($options['permissionsConfig'] as $bundle => $config) {
|
||||
$builder->add(
|
||||
$bundle,
|
||||
HiddenType::class,
|
||||
[
|
||||
'data' => 'newbundle',
|
||||
'label' => false,
|
||||
'mapped' => false,
|
||||
]
|
||||
);
|
||||
$config['permissionObject']->buildForm($builder, $options, $config['data']);
|
||||
}
|
||||
|
||||
if (!empty($options['action'])) {
|
||||
$builder->setAction($options['action']);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'permissionsConfig' => [],
|
||||
'constraints' => [new Valid()],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Form\Type;
|
||||
|
||||
use Mautic\UserBundle\Model\RoleModel;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class RoleListType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
private RoleModel $roleModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults(
|
||||
[
|
||||
'choices' => $this->getRoleChoices(),
|
||||
'expanded' => false,
|
||||
'multiple' => false,
|
||||
'required' => false,
|
||||
'placeholder' => 'mautic.core.form.chooseone',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function getParent(): ?string
|
||||
{
|
||||
return ChoiceType::class;
|
||||
}
|
||||
|
||||
private function getRoleChoices(): array
|
||||
{
|
||||
$choices = [];
|
||||
$roles = $this->roleModel->getRepository()->getEntities(
|
||||
[
|
||||
'filter' => [
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'r.isPublished',
|
||||
'expr' => 'eq',
|
||||
'value' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
foreach ($roles as $role) {
|
||||
$choices[$role->getName(true)] = $role->getId();
|
||||
}
|
||||
|
||||
// sort by name
|
||||
ksort($choices);
|
||||
|
||||
return $choices;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Form\Type;
|
||||
|
||||
use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber;
|
||||
use Mautic\CoreBundle\Form\EventListener\FormExitSubscriber;
|
||||
use Mautic\CoreBundle\Form\Type\FormButtonsType;
|
||||
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
|
||||
use Mautic\UserBundle\Entity\Role;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints\Valid;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<RoleType>
|
||||
*/
|
||||
class RoleType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->addEventSubscriber(new CleanFormSubscriber(['description' => 'html']));
|
||||
$builder->addEventSubscriber(new FormExitSubscriber('user.role', $options));
|
||||
|
||||
$builder->add(
|
||||
'name',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.core.name',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control'],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'description',
|
||||
TextareaType::class,
|
||||
[
|
||||
'label' => 'mautic.core.description',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control editor'],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add('isAdmin', YesNoButtonGroupType::class, [
|
||||
'label' => 'mautic.user.role.form.isadmin',
|
||||
'attr' => [
|
||||
'onchange' => 'Mautic.togglePermissionVisibility();',
|
||||
'tooltip' => 'mautic.user.role.form.isadmin.tooltip',
|
||||
],
|
||||
]);
|
||||
|
||||
// add a normal text field, but add your transformer to it
|
||||
$hidden = ($options['data']->isAdmin()) ? ' hide' : '';
|
||||
|
||||
$builder->add(
|
||||
'permissions',
|
||||
PermissionsType::class,
|
||||
[
|
||||
'label' => 'mautic.user.role.permissions',
|
||||
'mapped' => false, // we'll have to manually build the permissions for persisting
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'class' => $hidden,
|
||||
],
|
||||
'permissionsConfig' => $options['permissionsConfig'],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add('buttons', FormButtonsType::class);
|
||||
|
||||
if (!empty($options['action'])) {
|
||||
$builder->setAction($options['action']);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Role::class,
|
||||
'constraints' => [new Valid()],
|
||||
'permissionsConfig' => [],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Form\Type;
|
||||
|
||||
use Mautic\UserBundle\Model\UserModel;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array<mixed>>
|
||||
*/
|
||||
class UserListType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* @var array<string,int>
|
||||
*/
|
||||
private array $choices = [];
|
||||
|
||||
public function __construct(
|
||||
private UserModel $userModel,
|
||||
) {
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults(
|
||||
[
|
||||
'choices' => $this->getUserChoices(),
|
||||
'expanded' => false,
|
||||
'multiple' => true,
|
||||
'required' => false,
|
||||
'placeholder' => 'mautic.core.form.chooseone',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
public function getParent(): ?string
|
||||
{
|
||||
return ChoiceType::class;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,int>
|
||||
*/
|
||||
private function getUserChoices(): array
|
||||
{
|
||||
if ($this->choices) {
|
||||
return $this->choices;
|
||||
}
|
||||
|
||||
$users = $this->userModel->getRepository()->getEntities(
|
||||
[
|
||||
'filter' => [
|
||||
'force' => [
|
||||
[
|
||||
'column' => 'u.isPublished',
|
||||
'expr' => 'eq',
|
||||
'value' => true,
|
||||
],
|
||||
],
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
foreach ($users as $user) {
|
||||
$this->choices[$user->getName(true).' ('.$user->getId().')'] = $user->getId();
|
||||
}
|
||||
|
||||
// sort by user name
|
||||
ksort($this->choices);
|
||||
|
||||
return $this->choices;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<mixed>
|
||||
*/
|
||||
class UserPreferencesType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
// Theme
|
||||
$builder->add(
|
||||
'theme',
|
||||
HiddenType::class,
|
||||
[
|
||||
'label' => 'mautic.user.preferences.theme',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
// Reduce Transparency
|
||||
$builder->add(
|
||||
'reduce_transparency',
|
||||
HiddenType::class,
|
||||
[
|
||||
'label' => 'mautic.user.preferences.reduce_transparency',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
// Reduce Motion
|
||||
$builder->add(
|
||||
'reduce_motion',
|
||||
HiddenType::class,
|
||||
[
|
||||
'label' => 'mautic.user.preferences.reduce_motion',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
// Contrast Borders
|
||||
$builder->add(
|
||||
'contrast_borders',
|
||||
HiddenType::class,
|
||||
[
|
||||
'label' => 'mautic.user.preferences.contrast_borders',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
// Enable Underlines
|
||||
$builder->add(
|
||||
'enable_underlines',
|
||||
HiddenType::class,
|
||||
[
|
||||
'label' => 'mautic.user.preferences.enable_underlines',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Form\Type;
|
||||
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Doctrine\ORM\EntityRepository;
|
||||
use Mautic\CoreBundle\Form\EventListener\CleanFormSubscriber;
|
||||
use Mautic\CoreBundle\Form\EventListener\FormExitSubscriber;
|
||||
use Mautic\CoreBundle\Form\Type\FormButtonsType;
|
||||
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
|
||||
use Mautic\CoreBundle\Helper\LanguageHelper;
|
||||
use Mautic\UserBundle\Entity\Role;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Mautic\UserBundle\Model\UserModel;
|
||||
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\RepeatedType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TimezoneType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<User>
|
||||
*/
|
||||
class UserType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
private TranslatorInterface $translator,
|
||||
private UserModel $model,
|
||||
private LanguageHelper $languageHelper,
|
||||
) {
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->addEventSubscriber(new CleanFormSubscriber(['signature' => 'html', 'email' => 'email']));
|
||||
$builder->addEventSubscriber(new FormExitSubscriber('user.user', $options));
|
||||
|
||||
$builder->add(
|
||||
'username',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.core.username',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'preaddon' => 'ri-user-6-fill',
|
||||
'autocomplete' => 'off',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'firstName',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.core.firstname',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control'],
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'lastName',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.core.lastname',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => ['class' => 'form-control'],
|
||||
]
|
||||
);
|
||||
|
||||
$positions = $this->model->getLookupResults('position', null, 0);
|
||||
$builder->add(
|
||||
'position',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.core.position',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'data-options' => json_encode($positions),
|
||||
],
|
||||
'required' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'email',
|
||||
EmailType::class,
|
||||
[
|
||||
'label' => 'mautic.core.type.email',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'preaddon' => 'ri-mail-line',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
$existing = (!empty($options['data']) && $options['data']->getId());
|
||||
$placeholder = ($existing) ?
|
||||
$this->translator->trans('mautic.user.user.form.passwordplaceholder') : '';
|
||||
$required = ($existing) ? false : true;
|
||||
$builder->add(
|
||||
'plainPassword',
|
||||
RepeatedType::class,
|
||||
[
|
||||
'first_name' => 'password',
|
||||
'first_options' => [
|
||||
'label' => 'mautic.core.password',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'placeholder' => $placeholder,
|
||||
'tooltip' => 'mautic.user.user.form.help.passwordrequirements',
|
||||
'preaddon' => 'ri-lock-fill',
|
||||
'autocomplete' => 'off',
|
||||
],
|
||||
'required' => $required,
|
||||
'error_bubbling' => false,
|
||||
],
|
||||
'second_name' => 'confirm',
|
||||
'second_options' => [
|
||||
'label' => 'mautic.user.user.form.passwordconfirm',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
'placeholder' => $placeholder,
|
||||
'tooltip' => 'mautic.user.user.form.help.passwordrequirements',
|
||||
'preaddon' => 'ri-lock-fill',
|
||||
'autocomplete' => 'off',
|
||||
],
|
||||
'required' => $required,
|
||||
'error_bubbling' => false,
|
||||
],
|
||||
'type' => PasswordType::class,
|
||||
'invalid_message' => 'mautic.user.user.password.mismatch',
|
||||
'required' => $required,
|
||||
'error_bubbling' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'timezone',
|
||||
TimezoneType::class,
|
||||
[
|
||||
'label' => 'mautic.core.timezone',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
],
|
||||
'multiple' => false,
|
||||
'placeholder' => 'mautic.user.user.form.defaulttimezone',
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'locale',
|
||||
ChoiceType::class,
|
||||
[
|
||||
'choices' => $this->getSupportedLanguageChoices(),
|
||||
'label' => 'mautic.core.language',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
],
|
||||
'multiple' => false,
|
||||
'placeholder' => 'mautic.user.user.form.defaultlocale',
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'preferences',
|
||||
UserPreferencesType::class,
|
||||
[
|
||||
'label' => false,
|
||||
]
|
||||
);
|
||||
|
||||
$defaultSignature = '';
|
||||
if (isset($options['data']) && null === $options['data']->getSignature()) {
|
||||
$defaultSignature = $this->translator->trans('mautic.email.default.signature', ['%from_name%' => '|FROM_NAME|']);
|
||||
} elseif (isset($options['data'])) {
|
||||
$defaultSignature = $options['data']->getSignature();
|
||||
}
|
||||
|
||||
$builder->add(
|
||||
'signature',
|
||||
TextareaType::class,
|
||||
[
|
||||
'label' => 'mautic.email.token.signature',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
],
|
||||
'data' => $defaultSignature,
|
||||
'help' => 'mautic.user.config.signature.helper',
|
||||
]
|
||||
);
|
||||
|
||||
if (empty($options['in_profile'])) {
|
||||
$builder->add(
|
||||
$builder->create(
|
||||
'role',
|
||||
EntityType::class,
|
||||
[
|
||||
'label' => 'mautic.user.role',
|
||||
'label_attr' => ['class' => 'control-label'],
|
||||
'attr' => [
|
||||
'class' => 'form-control',
|
||||
],
|
||||
'class' => Role::class,
|
||||
'choice_label' => 'name',
|
||||
'query_builder' => fn (EntityRepository $er) => $er->createQueryBuilder('r')
|
||||
->where('r.isPublished = true')
|
||||
->orderBy('r.name', Order::Ascending->value),
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
$builder->add('isPublished', YesNoButtonGroupType::class);
|
||||
|
||||
$builder->add('buttons', FormButtonsType::class);
|
||||
} else {
|
||||
$builder->add(
|
||||
'buttons',
|
||||
FormButtonsType::class,
|
||||
[
|
||||
'save_text' => 'mautic.core.form.apply',
|
||||
'apply_text' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
if (!empty($options['action'])) {
|
||||
$builder->setAction($options['action']);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults(
|
||||
[
|
||||
'data_class' => User::class,
|
||||
'validation_groups' => [
|
||||
User::class,
|
||||
'determineValidationGroups',
|
||||
],
|
||||
'ignore_formexit' => false,
|
||||
'in_profile' => false,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function getSupportedLanguageChoices(): array
|
||||
{
|
||||
// Get the list of available languages
|
||||
$languages = $this->languageHelper->fetchLanguages(false, false);
|
||||
$choices = [];
|
||||
|
||||
foreach ($languages as $code => $langData) {
|
||||
$choices[$langData['name']] = $code;
|
||||
}
|
||||
$choices = array_merge($choices, array_flip($this->languageHelper->getSupportedLanguages()));
|
||||
|
||||
// Alpha sort the languages by name
|
||||
ksort($choices);
|
||||
|
||||
return $choices;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Form\Validator\Constraints;
|
||||
|
||||
use Mautic\UserBundle\Model\PasswordStrengthEstimatorModel;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
final class NotWeak extends Constraint
|
||||
{
|
||||
public const TOO_WEAK = 'f61e730a-284e-11eb-adc1-0242ac120002';
|
||||
|
||||
protected const ERROR_NAMES = [
|
||||
self::TOO_WEAK => 'PASSWORD_TOO_WEAK_ERROR',
|
||||
];
|
||||
|
||||
public string $message = 'This password is too weak. Consider using a stronger password.';
|
||||
|
||||
public int $score = PasswordStrengthEstimatorModel::MINIMUM_PASSWORD_STRENGTH_ALLOWED;
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Form\Validator\Constraints;
|
||||
|
||||
use Mautic\UserBundle\Model\PasswordStrengthEstimatorModel;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
use Symfony\Component\Validator\Exception\UnexpectedTypeException;
|
||||
|
||||
final class NotWeakValidator extends ConstraintValidator
|
||||
{
|
||||
public function __construct(private PasswordStrengthEstimatorModel $passwordStrengthEstimatorModel)
|
||||
{
|
||||
}
|
||||
|
||||
public function validate(mixed $value, Constraint $constraint): void
|
||||
{
|
||||
if (!$constraint instanceof NotWeak) {
|
||||
throw new UnexpectedTypeException($constraint, NotWeak::class);
|
||||
}
|
||||
|
||||
if ($this->passwordStrengthEstimatorModel->validate($value, $constraint->score)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->context->buildViolation($constraint->message)
|
||||
->setCode(NotWeak::TOO_WEAK)
|
||||
->addViolation();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Hash;
|
||||
|
||||
class UserHash
|
||||
{
|
||||
public const FAKE_USER_HASH = 'xxxxxxxxxxxxxx';
|
||||
|
||||
/**
|
||||
* Return fake user hash for emails etc. Users does not have hash, only Contacts.
|
||||
*/
|
||||
public static function getFakeUserHash(): string
|
||||
{
|
||||
return self::FAKE_USER_HASH;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle;
|
||||
|
||||
use Mautic\UserBundle\DependencyInjection\Compiler\FormLoginAuthenticatorOptionsPass;
|
||||
use Mautic\UserBundle\DependencyInjection\Compiler\OAuthReplacePass;
|
||||
use Mautic\UserBundle\DependencyInjection\Compiler\SsoAuthenticatorPass;
|
||||
use Mautic\UserBundle\DependencyInjection\Firewall\Factory\MauticSsoFactory;
|
||||
use Mautic\UserBundle\DependencyInjection\Firewall\Factory\PluginFactory;
|
||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension;
|
||||
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
class MauticUserBundle extends Bundle
|
||||
{
|
||||
public function build(ContainerBuilder $container): void
|
||||
{
|
||||
parent::build($container);
|
||||
|
||||
$extension = $container->getExtension('security');
|
||||
\assert($extension instanceof SecurityExtension);
|
||||
$extension->addAuthenticatorFactory(new PluginFactory());
|
||||
$extension->addAuthenticatorFactory(new MauticSsoFactory());
|
||||
|
||||
$container->addCompilerPass(new OAuthReplacePass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||
$container->addCompilerPass(new SsoAuthenticatorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||
$container->addCompilerPass(new FormLoginAuthenticatorOptionsPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Model;
|
||||
|
||||
use Mautic\UserBundle\Event\PasswordStrengthValidateEvent;
|
||||
use Mautic\UserBundle\UserEvents;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use ZxcvbnPhp\Zxcvbn as PasswordStrengthEstimator;
|
||||
|
||||
class PasswordStrengthEstimatorModel
|
||||
{
|
||||
public const MINIMUM_PASSWORD_STRENGTH_ALLOWED = 3;
|
||||
|
||||
private const DICTIONARY = [
|
||||
'mautic',
|
||||
'user',
|
||||
'lead',
|
||||
'bundle',
|
||||
'campaign',
|
||||
'company',
|
||||
];
|
||||
|
||||
private PasswordStrengthEstimator $passwordStrengthEstimator;
|
||||
|
||||
public function __construct(private EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->passwordStrengthEstimator = new PasswordStrengthEstimator();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $dictionary
|
||||
*/
|
||||
public function validate(?string $password, int $score = self::MINIMUM_PASSWORD_STRENGTH_ALLOWED, array $dictionary = self::DICTIONARY): bool
|
||||
{
|
||||
$isValid = $score <= $this->passwordStrengthEstimator->passwordStrength($password, $this->sanitizeDictionary($dictionary))['score'];
|
||||
|
||||
$passwordStrengthValidateEvent = new PasswordStrengthValidateEvent($isValid, $password);
|
||||
$this->dispatcher->dispatch($passwordStrengthValidateEvent, UserEvents::USER_PASSWORD_STRENGTH_VALIDATION);
|
||||
|
||||
return $passwordStrengthValidateEvent->isValid;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $dictionary
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
private function sanitizeDictionary(array $dictionary): array
|
||||
{
|
||||
return array_unique(array_filter($dictionary));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Model;
|
||||
|
||||
use Mautic\CoreBundle\Model\FormModel;
|
||||
use Mautic\CoreBundle\Model\GlobalSearchInterface;
|
||||
use Mautic\UserBundle\Entity\Role;
|
||||
use Mautic\UserBundle\Entity\RoleRepository;
|
||||
use Mautic\UserBundle\Event\RoleEvent;
|
||||
use Mautic\UserBundle\Form\Type\RoleType;
|
||||
use Mautic\UserBundle\UserEvents;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\PreconditionRequiredHttpException;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* @extends FormModel<Role>
|
||||
*/
|
||||
class RoleModel extends FormModel implements GlobalSearchInterface
|
||||
{
|
||||
public function getRepository(): RoleRepository
|
||||
{
|
||||
return $this->em->getRepository(Role::class);
|
||||
}
|
||||
|
||||
public function getPermissionBase(): string
|
||||
{
|
||||
return 'user:roles';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
public function saveEntity($entity, $unlock = true): void
|
||||
{
|
||||
if (!$entity instanceof Role) {
|
||||
throw new MethodNotAllowedHttpException(['Role'], 'Entity must be of class Role()');
|
||||
}
|
||||
|
||||
$isNew = ($entity->getId()) ? 0 : 1;
|
||||
|
||||
if (!$isNew) {
|
||||
// delete all existing
|
||||
$this->em->getRepository(\Mautic\UserBundle\Entity\Permission::class)->purgeRolePermissions($entity);
|
||||
}
|
||||
|
||||
parent::saveEntity($entity, $unlock);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate the role's permissions.
|
||||
*
|
||||
* @param array $rawPermissions (i.e. from request)
|
||||
*/
|
||||
public function setRolePermissions(Role &$entity, $rawPermissions): void
|
||||
{
|
||||
if (!is_array($rawPermissions)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// set permissions if applicable and if the user is not an admin
|
||||
$permissions = (!$entity->isAdmin() && !empty($rawPermissions)) ?
|
||||
$this->security->generatePermissions($rawPermissions) :
|
||||
[];
|
||||
|
||||
foreach ($permissions as $permissionEntity) {
|
||||
$entity->addPermission($permissionEntity);
|
||||
}
|
||||
|
||||
$entity->setRawPermissions($rawPermissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws PreconditionRequiredHttpException
|
||||
*/
|
||||
public function deleteEntity($entity): void
|
||||
{
|
||||
if (!$entity instanceof Role) {
|
||||
throw new MethodNotAllowedHttpException(['Role'], 'Entity must be of class Role()');
|
||||
}
|
||||
|
||||
$users = $this->em->getRepository(\Mautic\UserBundle\Entity\User::class)->findByRole($entity);
|
||||
if (count($users)) {
|
||||
throw new PreconditionRequiredHttpException($this->translator->trans('mautic.user.role.error.deletenotallowed', ['%name%' => $entity->getName()], 'flashes'));
|
||||
}
|
||||
|
||||
parent::deleteEntity($entity);
|
||||
}
|
||||
|
||||
public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): \Symfony\Component\Form\FormInterface
|
||||
{
|
||||
if (!$entity instanceof Role) {
|
||||
throw new MethodNotAllowedHttpException(['Role']);
|
||||
}
|
||||
|
||||
if (!empty($action)) {
|
||||
$options['action'] = $action;
|
||||
}
|
||||
|
||||
return $formFactory->create(RoleType::class, $entity, $options);
|
||||
}
|
||||
|
||||
public function getEntity($id = null): ?Role
|
||||
{
|
||||
if (null === $id) {
|
||||
return new Role();
|
||||
}
|
||||
|
||||
return parent::getEntity($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
protected function dispatchEvent($action, &$entity, $isNew = false, ?Event $event = null): ?Event
|
||||
{
|
||||
if (!$entity instanceof Role) {
|
||||
throw new MethodNotAllowedHttpException(['Role'], 'Entity must be of class Role()');
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'pre_save':
|
||||
$name = UserEvents::ROLE_PRE_SAVE;
|
||||
break;
|
||||
case 'post_save':
|
||||
$name = UserEvents::ROLE_POST_SAVE;
|
||||
break;
|
||||
case 'pre_delete':
|
||||
$name = UserEvents::ROLE_PRE_DELETE;
|
||||
break;
|
||||
case 'post_delete':
|
||||
$name = UserEvents::ROLE_POST_DELETE;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->dispatcher->hasListeners($name)) {
|
||||
if (empty($event)) {
|
||||
$event = new RoleEvent($entity, $isNew);
|
||||
$event->setEntityManager($this->em);
|
||||
}
|
||||
$this->dispatcher->dispatch($event, $name);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,394 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Model;
|
||||
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\UserHelper;
|
||||
use Mautic\CoreBundle\Model\FormModel;
|
||||
use Mautic\CoreBundle\Model\GlobalSearchInterface;
|
||||
use Mautic\CoreBundle\Security\Permissions\CorePermissions;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\EmailBundle\Helper\MailHelper;
|
||||
use Mautic\UserBundle\Entity\Role;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Mautic\UserBundle\Entity\UserRepository;
|
||||
use Mautic\UserBundle\Entity\UserToken;
|
||||
use Mautic\UserBundle\Enum\UserTokenAuthorizator;
|
||||
use Mautic\UserBundle\Event\UserEvent;
|
||||
use Mautic\UserBundle\Form\Type\UserType;
|
||||
use Mautic\UserBundle\Model\UserToken\UserTokenServiceInterface;
|
||||
use Mautic\UserBundle\UserEvents;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* @extends FormModel<User>
|
||||
*/
|
||||
class UserModel extends FormModel implements GlobalSearchInterface
|
||||
{
|
||||
public function __construct(
|
||||
protected MailHelper $mailHelper,
|
||||
private UserTokenServiceInterface $userTokenService,
|
||||
EntityManager $em,
|
||||
CorePermissions $security,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
UrlGeneratorInterface $router,
|
||||
Translator $translator,
|
||||
UserHelper $userHelper,
|
||||
LoggerInterface $mauticLogger,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
parent::__construct($em, $security, $dispatcher, $router, $translator, $userHelper, $mauticLogger, $coreParametersHelper);
|
||||
}
|
||||
|
||||
public function getRepository(): UserRepository
|
||||
{
|
||||
return $this->em->getRepository(User::class);
|
||||
}
|
||||
|
||||
public function getPermissionBase(): string
|
||||
{
|
||||
return 'user:users';
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
public function saveEntity($entity, $unlock = true): void
|
||||
{
|
||||
if (!$entity instanceof User) {
|
||||
throw new MethodNotAllowedHttpException(['User'], 'Entity must be of class User()');
|
||||
}
|
||||
|
||||
parent::saveEntity($entity, $unlock);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = [])
|
||||
{
|
||||
return $this->getRepository()->getUserList($search, $limit, $start, $permissionLimiter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for a new password and rehashes if necessary.
|
||||
*
|
||||
* @param string $submittedPassword
|
||||
* @param bool|false $validate
|
||||
*/
|
||||
public function checkNewPassword(User $entity, UserPasswordHasherInterface $hasher, $submittedPassword, $validate = false): ?string
|
||||
{
|
||||
if ($validate) {
|
||||
if (strlen($submittedPassword) < 6) {
|
||||
throw new \InvalidArgumentException($this->translator->trans('mautic.user.user.password.minlength', [], 'validators'));
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($submittedPassword)) {
|
||||
// hash the clear password submitted via the form
|
||||
return $hasher->hashPassword($entity, $submittedPassword);
|
||||
}
|
||||
|
||||
return $entity->getPassword();
|
||||
}
|
||||
|
||||
public function createForm($entity, FormFactoryInterface $formFactory, $action = null, $options = []): \Symfony\Component\Form\FormInterface
|
||||
{
|
||||
if (!$entity instanceof User) {
|
||||
throw new MethodNotAllowedHttpException(['User'], 'Entity must be of class User()');
|
||||
}
|
||||
if (!empty($action)) {
|
||||
$options['action'] = $action;
|
||||
}
|
||||
|
||||
return $formFactory->create(UserType::class, $entity, $options);
|
||||
}
|
||||
|
||||
public function getEntity($id = null): ?User
|
||||
{
|
||||
if (null === $id) {
|
||||
return new User();
|
||||
}
|
||||
|
||||
$entity = parent::getEntity($id);
|
||||
|
||||
if ($entity) {
|
||||
// add user's permissions
|
||||
$entity->setActivePermissions(
|
||||
$this->em->getRepository(\Mautic\UserBundle\Entity\Permission::class)->getPermissionsByRole($entity->getRole())
|
||||
);
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User|null
|
||||
*/
|
||||
public function getSystemAdministrator()
|
||||
{
|
||||
$adminRole = $this->em->getRepository(Role::class)->findOneBy(['isAdmin' => true]);
|
||||
|
||||
return $this->getRepository()->findOneBy(
|
||||
[
|
||||
'role' => $adminRole,
|
||||
'isPublished' => true,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws MethodNotAllowedHttpException
|
||||
*/
|
||||
protected function dispatchEvent($action, &$entity, $isNew = false, ?Event $event = null): ?Event
|
||||
{
|
||||
if (!$entity instanceof User) {
|
||||
throw new MethodNotAllowedHttpException(['User'], 'Entity must be of class User()');
|
||||
}
|
||||
|
||||
switch ($action) {
|
||||
case 'pre_save':
|
||||
$name = UserEvents::USER_PRE_SAVE;
|
||||
break;
|
||||
case 'post_save':
|
||||
$name = UserEvents::USER_POST_SAVE;
|
||||
break;
|
||||
case 'pre_delete':
|
||||
$name = UserEvents::USER_PRE_DELETE;
|
||||
break;
|
||||
case 'post_delete':
|
||||
$name = UserEvents::USER_POST_DELETE;
|
||||
break;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->dispatcher->hasListeners($name)) {
|
||||
if (empty($event)) {
|
||||
$event = new UserEvent($entity, $isNew);
|
||||
$event->setEntityManager($this->em);
|
||||
}
|
||||
$this->dispatcher->dispatch($event, $name);
|
||||
|
||||
return $event;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of entities for autopopulate fields.
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $filter
|
||||
* @param int $limit
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLookupResults($type, $filter = '', $limit = 10)
|
||||
{
|
||||
$results = [];
|
||||
|
||||
return match ($type) {
|
||||
'role' => $this->em->getRepository(Role::class)->getRoleList($filter, $limit),
|
||||
'user' => $this->em->getRepository(User::class)->getUserList($filter, $limit),
|
||||
'position' => $this->em->getRepository(User::class)->getPositionList($filter, $limit),
|
||||
default => $results,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets the user password and emails it.
|
||||
*
|
||||
* @param string $newPassword
|
||||
*/
|
||||
public function resetPassword(User $user, UserPasswordHasher $hasher, $newPassword): void
|
||||
{
|
||||
$hashedPassword = $this->checkNewPassword($user, $hasher, $newPassword);
|
||||
|
||||
$user->setPassword($hashedPassword);
|
||||
$this->saveEntity($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return UserToken
|
||||
*/
|
||||
protected function getResetToken(User $user)
|
||||
{
|
||||
$userToken = new UserToken();
|
||||
$userToken->setUser($user)
|
||||
->setAuthorizator(UserTokenAuthorizator::RESET_PASSWORD_AUTHORIZATOR)
|
||||
->setExpiration((new \DateTime())->add(new \DateInterval('PT24H')))
|
||||
->setOneTimeOnly();
|
||||
|
||||
return $this->userTokenService->generateSecret($userToken, 64);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $token
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function confirmResetToken(User $user, $token)
|
||||
{
|
||||
$userToken = new UserToken();
|
||||
$userToken->setUser($user)
|
||||
->setAuthorizator(UserTokenAuthorizator::RESET_PASSWORD_AUTHORIZATOR)
|
||||
->setSecret($token);
|
||||
|
||||
return $this->userTokenService->verify($userToken);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function sendResetEmail(User $user): void
|
||||
{
|
||||
$mailer = $this->mailHelper->getMailer();
|
||||
|
||||
$resetToken = $this->getResetToken($user);
|
||||
$this->em->persist($resetToken);
|
||||
try {
|
||||
$this->em->flush();
|
||||
} catch (\Exception $exception) {
|
||||
$this->logger->error($exception->getMessage());
|
||||
throw new \RuntimeException();
|
||||
}
|
||||
$resetLink = $this->router->generate('mautic_user_passwordresetconfirm', ['token' => $resetToken->getSecret()], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
|
||||
$mailer->setTo([$user->getEmail() => $user->getName()]);
|
||||
$mailer->setSubject($this->translator->trans('mautic.user.user.passwordreset.subject'));
|
||||
$text = $this->translator->trans(
|
||||
'mautic.user.user.passwordreset.email.body',
|
||||
['%name%' => $user->getFirstName(), '%resetlink%' => '<a href="'.$resetLink.'">'.$resetLink.'</a>']
|
||||
);
|
||||
$text = str_replace('\\n', "\n", $text);
|
||||
$html = nl2br($text);
|
||||
|
||||
$this->emailUser(
|
||||
$user,
|
||||
$this->translator->trans('mautic.user.user.passwordreset.subject'),
|
||||
$html
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function sendChangePasswordInfo(User $user): void
|
||||
{
|
||||
$text = $this->translator->trans(
|
||||
'mautic.user.user.passwordchange.email.body',
|
||||
['%name%' => $user->getFirstName()]
|
||||
);
|
||||
$text = str_replace('\\n', "\n", $text);
|
||||
$html = nl2br($text);
|
||||
|
||||
$this->emailUser(
|
||||
$user,
|
||||
$this->translator->trans('mautic.user.user.passwordchange.subject'),
|
||||
$html
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function sendChangeEmailInfo(string $oldEmail, User $user): void
|
||||
{
|
||||
$mailer = $this->mailHelper->getMailer();
|
||||
$text = $this->translator->trans(
|
||||
'mautic.user.user.emailchange.email.body',
|
||||
['%name%' => $user->getFirstName()]
|
||||
);
|
||||
$text = str_replace('\\n', "\n", $text);
|
||||
$html = nl2br($text);
|
||||
|
||||
$mailer->setTo([$oldEmail => $user->getName()]);
|
||||
$mailer->setBody($html);
|
||||
$mailer->setSubject($this->translator->trans('mautic.user.user.emailchange.subject'));
|
||||
$mailer->send();
|
||||
}
|
||||
|
||||
public function emailUser(User $user, string $subject, string $content): void
|
||||
{
|
||||
$mailer = $this->prepareEMail($subject, $content);
|
||||
$mailer->setTo([$user->getEmail() => $user->getName()]);
|
||||
$mailer->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $emailAddresses
|
||||
*/
|
||||
public function sendMailToEmailAddresses(array $emailAddresses, string $subject, string $content): void
|
||||
{
|
||||
$mailer = $this->prepareEMail($subject, $content);
|
||||
$mailer->setTo($emailAddresses);
|
||||
$mailer->send();
|
||||
}
|
||||
|
||||
private function prepareEMail(string $subject, string $content): MailHelper
|
||||
{
|
||||
$mailer = $this->mailHelper->getMailer();
|
||||
$content = str_replace('\\n', "\n", $content);
|
||||
$html = nl2br($content);
|
||||
$mailer->setSubject($subject);
|
||||
$mailer->setBody($html);
|
||||
$mailer->setPlainText(strip_tags($content));
|
||||
|
||||
return $mailer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set user preference.
|
||||
*/
|
||||
public function setPreference($key, $value = null, ?User $user = null): void
|
||||
{
|
||||
if (null == $user) {
|
||||
$user = $this->userHelper->getUser();
|
||||
}
|
||||
|
||||
$preferences = $user->getPreferences();
|
||||
$preferences[$key] = $value;
|
||||
|
||||
$user->setPreferences($preferences);
|
||||
|
||||
$this->getRepository()->saveEntity($user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user preference.
|
||||
*/
|
||||
public function getPreference($key, $default = null, ?User $user = null)
|
||||
{
|
||||
if (null == $user) {
|
||||
$user = $this->userHelper->getUser();
|
||||
}
|
||||
$preferences = $user->getPreferences();
|
||||
|
||||
return $preferences[$key] ?? $default;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of Users for formType Choice.
|
||||
*/
|
||||
public function getOwnerListChoices(): array
|
||||
{
|
||||
return $this->getRepository()->getOwnerListChoices();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Model\UserToken;
|
||||
|
||||
use Mautic\CoreBundle\Helper\RandomHelper\RandomHelperInterface;
|
||||
use Mautic\UserBundle\Entity\UserToken;
|
||||
use Mautic\UserBundle\Entity\UserTokenRepositoryInterface;
|
||||
|
||||
final class UserTokenService implements UserTokenServiceInterface
|
||||
{
|
||||
public function __construct(
|
||||
private RandomHelperInterface $randomHelper,
|
||||
private UserTokenRepositoryInterface $userTokenRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $secretLength
|
||||
*
|
||||
* @return UserToken
|
||||
*/
|
||||
public function generateSecret(UserToken $token, $secretLength = 32)
|
||||
{
|
||||
do {
|
||||
$randomSecret = $this->randomHelper->generate($secretLength);
|
||||
$isSecretUnique = $this->userTokenRepository->isSecretUnique($randomSecret);
|
||||
} while (false === $isSecretUnique);
|
||||
|
||||
return $token->setSecret($randomSecret);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function verify(UserToken $token)
|
||||
{
|
||||
return $this->userTokenRepository->verify($token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Model\UserToken;
|
||||
|
||||
use Mautic\UserBundle\Entity\UserToken;
|
||||
|
||||
/**
|
||||
* Interface UserTokenServiceInterface.
|
||||
*/
|
||||
interface UserTokenServiceInterface
|
||||
{
|
||||
/**
|
||||
* @param int $secretLength
|
||||
*
|
||||
* @return UserToken
|
||||
*/
|
||||
public function generateSecret(UserToken $token, $secretLength = 32);
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function verify(UserToken $token);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{% block _config_userconfig_widget %}
|
||||
{% set fields = form.children %}
|
||||
{% set fieldKeys = fields|keys %}
|
||||
|
||||
{% if fieldKeys|filter(v => v in ['saml_idp_metadata', 'saml_idp_own_certificate', 'saml_idp_own_private_key', 'saml_idp_own_password'])|length > 0 %}
|
||||
<h4 class="fw-sb mt-48 mb-xs">{{ 'mautic.user.config.header.saml'|trans }}</h4>
|
||||
<div class="text-muted small pb-md">{{ 'mautic.core.config.header.saml.description'|trans }}</div>
|
||||
<div class="row">
|
||||
<div class="panel panel-default mb-md">
|
||||
<div class="panel-body">
|
||||
<div class="row">
|
||||
{% if fields.saml_idp_entity_id is defined %}
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(fields.saml_idp_entity_id) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(fields.saml_idp_metadata, {'fieldValue': formConfig.parameters.saml_idp_metadata}) }}
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(fields.saml_idp_default_role) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<div class="alert alert-info">{{ 'mautic.user.config.form.saml.idp_attributes'|trans }}</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(fields.saml_idp_email_attribute) }}
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(fields.saml_idp_username_attribute) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(fields.saml_idp_firstname_attribute) }}
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(fields.saml_idp_lastname_attribute) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
<div class="alert alert-info">{{ 'mautic.user.config.form.saml.idp.own_certificate.description'|trans }}</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(fields.saml_idp_own_certificate, {'fieldValue': formConfig.parameters.saml_idp_own_certificate}) }}
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(fields.saml_idp_own_private_key, {'fieldValue': formConfig.parameters.saml_idp_own_private_key}) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(fields.saml_idp_own_password) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,20 @@
|
||||
{% for theme in themes %}
|
||||
<div class="radio-option col-xs-12 col-md-4 mb-lg">
|
||||
<label class="d-flex fd-column" for="theme-{{ theme.id }}">
|
||||
{% if theme.preview ends with '.twig' %}
|
||||
{{ include(theme.preview) }}
|
||||
{% else %}
|
||||
<img src="{{ asset(theme.preview) }}" alt="{{ theme.name|trans }} preview" aria-hidden="true" class="mb-sm">
|
||||
{% endif %}
|
||||
<div class="d-flex fd-row gap-3">
|
||||
<input type="radio" id="theme-{{ theme.id }}" name="theme" value="{{ theme.id }}"
|
||||
data-attribute-toggle="theme" data-attribute-value="{{ theme.id }}"
|
||||
{% if theme.checked|default(false) %}checked{% endif %}>
|
||||
<div class="text-content">
|
||||
<strong>{{ theme.name|trans }}</strong>
|
||||
<div class="text-help">{{ theme.description|trans }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
@@ -0,0 +1,29 @@
|
||||
<svg class="mb-sm" viewBox="0 0 228 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_795_75)">
|
||||
<path d="M0 0H228V120H0V0Z" fill="#161616"/>
|
||||
<path d="M75 39H47C45.8954 39 45 39.8954 45 41V73C45 74.1046 45.8954 75 47 75H75C76.1046 75 77 74.1046 77 73V41C77 39.8954 76.1046 39 75 39Z" fill="#262626"/>
|
||||
<path d="M0 0H40V120H0V0Z" fill="#262626"/>
|
||||
<path d="M90 20H48C46.3431 20 45 21.3431 45 23C45 24.6569 46.3431 26 48 26H90C91.6569 26 93 24.6569 93 23C93 21.3431 91.6569 20 90 20Z" fill="#393939"/>
|
||||
<path d="M214.153 39H84.8472C83.827 39 83 40.132 83 41.5283V103.472C83 104.868 83.827 106 84.8472 106H214.153C215.173 106 216 104.868 216 103.472V41.5283C216 40.132 215.173 39 214.153 39Z" fill="#262626"/>
|
||||
<path opacity="0.3" d="M216 44H83V56H216V44Z" fill="var(--primary-60)"/>
|
||||
<path d="M143.958 47H90.389C88.8587 47 87.6182 48.3431 87.6182 50C87.6182 51.6569 88.8587 53 90.389 53H143.958C145.489 53 146.729 51.6569 146.729 50C146.729 48.3431 145.489 47 143.958 47Z" fill="var(--primary-60)"/>
|
||||
<g clip-path="url(#clip1_795_75)">
|
||||
<path d="M213 20H209C208.448 20 208 20.4477 208 21V25C208 25.5523 208.448 26 209 26H213C213.552 26 214 25.5523 214 25V21C214 20.4477 213.552 20 213 20Z" fill="white"/>
|
||||
</g>
|
||||
<g clip-path="url(#clip2_795_75)">
|
||||
<path d="M205 20H201C200.448 20 200 20.4477 200 21V25C200 25.5523 200.448 26 201 26H205C205.552 26 206 25.5523 206 25V21C206 20.4477 205.552 20 205 20Z" fill="var(--primary-60)"/>
|
||||
</g>
|
||||
<path d="M213 9H187C185.343 9 184 10.3431 184 12C184 13.6569 185.343 15 187 15H213C214.657 15 216 13.6569 216 12C216 10.3431 214.657 9 213 9Z" fill="#393939"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_795_75">
|
||||
<rect width="228" height="120" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_795_75">
|
||||
<rect x="208" y="20" width="6" height="6" rx="3" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip2_795_75">
|
||||
<rect x="200" y="20" width="6" height="6" rx="3" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,29 @@
|
||||
<svg width="228" height="120" viewBox="0 0 228 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_795_117)">
|
||||
<path d="M0 0H228V120H0V0Z" fill="#1C1C2E"/>
|
||||
<path d="M75 39H47C45.8954 39 45 39.8954 45 41V73C45 74.1046 45.8954 75 47 75H75C76.1046 75 77 74.1046 77 73V41C77 39.8954 76.1046 39 75 39Z" fill="#232336"/>
|
||||
<path d="M0 0H40V120H0V0Z" fill="#232336"/>
|
||||
<path d="M90 20H48C46.3431 20 45 21.3431 45 23C45 24.6569 46.3431 26 48 26H90C91.6569 26 93 24.6569 93 23C93 21.3431 91.6569 20 90 20Z" fill="#323248"/>
|
||||
<path d="M214.153 39H84.8472C83.827 39 83 40.132 83 41.5283V103.472C83 104.868 83.827 106 84.8472 106H214.153C215.173 106 216 104.868 216 103.472V41.5283C216 40.132 215.173 39 214.153 39Z" fill="#232336"/>
|
||||
<path opacity="0.3" d="M216 44H83V56H216V44Z" fill="#F7B84B"/>
|
||||
<path d="M143.958 47H90.389C88.8587 47 87.6182 48.3431 87.6182 50C87.6182 51.6569 88.8587 53 90.389 53H143.958C145.489 53 146.729 51.6569 146.729 50C146.729 48.3431 145.489 47 143.958 47Z" fill="#F7B84B"/>
|
||||
<g clip-path="url(#clip1_795_117)">
|
||||
<path d="M213 20H209C208.448 20 208 20.4477 208 21V25C208 25.5523 208.448 26 209 26H213C213.552 26 214 25.5523 214 25V21C214 20.4477 213.552 20 213 20Z" fill="#8C98C6"/>
|
||||
</g>
|
||||
<g clip-path="url(#clip2_795_117)">
|
||||
<path d="M205 20H201C200.448 20 200 20.4477 200 21V25C200 25.5523 200.448 26 201 26H205C205.552 26 206 25.5523 206 25V21C206 20.4477 205.552 20 205 20Z" fill="#F7B84B"/>
|
||||
</g>
|
||||
<path d="M213 9H187C185.343 9 184 10.3431 184 12C184 13.6569 185.343 15 187 15H213C214.657 15 216 13.6569 216 12C216 10.3431 214.657 9 213 9Z" fill="#323248"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_795_117">
|
||||
<rect width="228" height="120" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_795_117">
|
||||
<rect x="208" y="20" width="6" height="6" rx="3" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip2_795_117">
|
||||
<rect x="200" y="20" width="6" height="6" rx="3" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,29 @@
|
||||
<svg class="mb-sm" viewBox="0 0 228 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_794_43)">
|
||||
<path d="M0 0H228V120H0V0Z" fill="white"/>
|
||||
<path d="M75 39H47C45.8954 39 45 39.8954 45 41V73C45 74.1046 45.8954 75 47 75H75C76.1046 75 77 74.1046 77 73V41C77 39.8954 76.1046 39 75 39Z" fill="#F4F4F4"/>
|
||||
<path d="M0 0H40V120H0V0Z" fill="#F4F4F4"/>
|
||||
<path d="M90 20H48C46.3431 20 45 21.3431 45 23C45 24.6569 46.3431 26 48 26H90C91.6569 26 93 24.6569 93 23C93 21.3431 91.6569 20 90 20Z" fill="#DADADA"/>
|
||||
<path d="M214.153 39H84.8472C83.827 39 83 40.132 83 41.5283V103.472C83 104.868 83.827 106 84.8472 106H214.153C215.173 106 216 104.868 216 103.472V41.5283C216 40.132 215.173 39 214.153 39Z" fill="#F4F4F4"/>
|
||||
<path opacity="0.3" d="M216 44H83V56H216V44Z" fill="var(--primary-60)"/>
|
||||
<path d="M143.958 47H90.389C88.8587 47 87.6182 48.3431 87.6182 50C87.6182 51.6569 88.8587 53 90.389 53H143.958C145.489 53 146.729 51.6569 146.729 50C146.729 48.3431 145.489 47 143.958 47Z" fill="var(--primary-60)"/>
|
||||
<g clip-path="url(#clip1_794_43)">
|
||||
<path d="M213 20H209C208.448 20 208 20.4477 208 21V25C208 25.5523 208.448 26 209 26H213C213.552 26 214 25.5523 214 25V21C214 20.4477 213.552 20 213 20Z" fill="#161616"/>
|
||||
</g>
|
||||
<g clip-path="url(#clip2_794_43)">
|
||||
<path d="M205 20H201C200.448 20 200 20.4477 200 21V25C200 25.5523 200.448 26 201 26H205C205.552 26 206 25.5523 206 25V21C206 20.4477 205.552 20 205 20Z" fill="var(--primary-60)"/>
|
||||
</g>
|
||||
<path d="M213 9H187C185.343 9 184 10.3431 184 12C184 13.6569 185.343 15 187 15H213C214.657 15 216 13.6569 216 12C216 10.3431 214.657 9 213 9Z" fill="#DADADA"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_794_43">
|
||||
<rect width="228" height="120" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_794_43">
|
||||
<rect x="208" y="20" width="6" height="6" rx="3" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip2_794_43">
|
||||
<rect x="200" y="20" width="6" height="6" rx="3" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,29 @@
|
||||
<svg width="228" height="120" viewBox="0 0 228 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_795_103)">
|
||||
<path d="M0 0H228V120H0V0Z" fill="#002B36"/>
|
||||
<path d="M75 39H47C45.8954 39 45 39.8954 45 41V73C45 74.1046 45.8954 75 47 75H75C76.1046 75 77 74.1046 77 73V41C77 39.8954 76.1046 39 75 39Z" fill="#073642"/>
|
||||
<path d="M0 0H40V120H0V0Z" fill="#073642"/>
|
||||
<path d="M90 20H48C46.3431 20 45 21.3431 45 23C45 24.6569 46.3431 26 48 26H90C91.6569 26 93 24.6569 93 23C93 21.3431 91.6569 20 90 20Z" fill="#586E75"/>
|
||||
<path d="M214.153 39H84.8472C83.827 39 83 40.132 83 41.5283V103.472C83 104.868 83.827 106 84.8472 106H214.153C215.173 106 216 104.868 216 103.472V41.5283C216 40.132 215.173 39 214.153 39Z" fill="#073642"/>
|
||||
<path opacity="0.3" d="M216 44H83V56H216V44Z" fill="#268BD2"/>
|
||||
<path d="M143.958 47H90.389C88.8587 47 87.6182 48.3431 87.6182 50C87.6182 51.6569 88.8587 53 90.389 53H143.958C145.489 53 146.729 51.6569 146.729 50C146.729 48.3431 145.489 47 143.958 47Z" fill="#268BD2"/>
|
||||
<g clip-path="url(#clip1_795_103)">
|
||||
<path d="M213 20H209C208.448 20 208 20.4477 208 21V25C208 25.5523 208.448 26 209 26H213C213.552 26 214 25.5523 214 25V21C214 20.4477 213.552 20 213 20Z" fill="#839496"/>
|
||||
</g>
|
||||
<g clip-path="url(#clip2_795_103)">
|
||||
<path d="M205 20H201C200.448 20 200 20.4477 200 21V25C200 25.5523 200.448 26 201 26H205C205.552 26 206 25.5523 206 25V21C206 20.4477 205.552 20 205 20Z" fill="#268BD2"/>
|
||||
</g>
|
||||
<path d="M213 9H187C185.343 9 184 10.3431 184 12C184 13.6569 185.343 15 187 15H213C214.657 15 216 13.6569 216 12C216 10.3431 214.657 9 213 9Z" fill="#586E75"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_795_103">
|
||||
<rect width="228" height="120" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_795_103">
|
||||
<rect x="208" y="20" width="6" height="6" rx="3" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip2_795_103">
|
||||
<rect x="200" y="20" width="6" height="6" rx="3" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,29 @@
|
||||
<svg width="228" height="120" viewBox="0 0 228 120" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_795_89)">
|
||||
<path d="M0 0H228V120H0V0Z" fill="#FDF6E3"/>
|
||||
<path d="M75 39H47C45.8954 39 45 39.8954 45 41V73C45 74.1046 45.8954 75 47 75H75C76.1046 75 77 74.1046 77 73V41C77 39.8954 76.1046 39 75 39Z" fill="#EEE8D5"/>
|
||||
<path d="M0 0H40V120H0V0Z" fill="#EEE8D5"/>
|
||||
<path d="M90 20H48C46.3431 20 45 21.3431 45 23C45 24.6569 46.3431 26 48 26H90C91.6569 26 93 24.6569 93 23C93 21.3431 91.6569 20 90 20Z" fill="#DCD4C0"/>
|
||||
<path d="M214.153 39H84.8472C83.827 39 83 40.132 83 41.5283V103.472C83 104.868 83.827 106 84.8472 106H214.153C215.173 106 216 104.868 216 103.472V41.5283C216 40.132 215.173 39 214.153 39Z" fill="#EEE8D5"/>
|
||||
<path opacity="0.3" d="M216 44H83V56H216V44Z" fill="#268BD2"/>
|
||||
<path d="M143.958 47H90.389C88.8587 47 87.6182 48.3431 87.6182 50C87.6182 51.6569 88.8587 53 90.389 53H143.958C145.489 53 146.729 51.6569 146.729 50C146.729 48.3431 145.489 47 143.958 47Z" fill="#268BD2"/>
|
||||
<g clip-path="url(#clip1_795_89)">
|
||||
<path d="M213 20H209C208.448 20 208 20.4477 208 21V25C208 25.5523 208.448 26 209 26H213C213.552 26 214 25.5523 214 25V21C214 20.4477 213.552 20 213 20Z" fill="#586E75"/>
|
||||
</g>
|
||||
<g clip-path="url(#clip2_795_89)">
|
||||
<path d="M205 20H201C200.448 20 200 20.4477 200 21V25C200 25.5523 200.448 26 201 26H205C205.552 26 206 25.5523 206 25V21C206 20.4477 205.552 20 205 20Z" fill="#268BD2"/>
|
||||
</g>
|
||||
<path d="M213 9H187C185.343 9 184 10.3431 184 12C184 13.6569 185.343 15 187 15H213C214.657 15 216 13.6569 216 12C216 10.3431 214.657 9 213 9Z" fill="#DCD4C0"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_795_89">
|
||||
<rect width="228" height="120" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_795_89">
|
||||
<rect x="208" y="20" width="6" height="6" rx="3" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip2_795_89">
|
||||
<rect x="200" y="20" width="6" height="6" rx="3" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,285 @@
|
||||
{% extends '@MauticCore/Default/content.html.twig' %}
|
||||
|
||||
{% block headerTitle %}{{ 'mautic.user.account.settings'|trans }}{% endblock %}
|
||||
{% block mauticContent %}user{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- start: box layout -->
|
||||
<div class="container">
|
||||
<!-- step container -->
|
||||
<div class="row">
|
||||
<div class="col-md-3 height-auto">
|
||||
|
||||
{% if me.getId() %}
|
||||
<div class="media">
|
||||
<div class="pull-left">
|
||||
<img class="img-circle mt-md mb-xs img-bordered media-object" src="{{ gravatarGetImage(me.getEmail()) }}"
|
||||
alt="" width="65px">
|
||||
</div>
|
||||
<div class="media-body">
|
||||
<h3 class="fw-sb">{{ me.getName() }}</h3>
|
||||
<span class="text-secondary">{{ me.getPosition() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
{% endif %}
|
||||
|
||||
<ul class="list-group list-group-tabs">
|
||||
<li class="list-group-item active">
|
||||
<a href="#profile" class="list-group-item-heading steps" data-toggle="tab">
|
||||
{{ 'mautic.user.account.header.details'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
|
||||
{% if permissions['apiAccess'] %}
|
||||
<li class="list-group-item">
|
||||
<a href="#clients" class="list-group-item-heading steps" data-toggle="tab">
|
||||
{{ 'mautic.user.account.header.authorizedclients'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
|
||||
<li class="list-group-item">
|
||||
<a href="#appearance" class="list-group-item-heading steps" data-toggle="tab">{{
|
||||
'mautic.user.account.appearance'|trans }}</a>
|
||||
</li>
|
||||
|
||||
<li class="list-group-item">
|
||||
<a href="#accessibility" class="list-group-item-heading steps" data-toggle="tab">{{
|
||||
'mautic.user.account.accessibility'|trans }}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
<!--/ step container -->
|
||||
|
||||
<!-- container -->
|
||||
<div class="col-md-9 height-auto">
|
||||
<div class="tab-content">
|
||||
<div class="tab-pane fade in active bdr-rds-0 bdr-w-0" id="profile">
|
||||
{{ form_start(userForm) }}
|
||||
<div class="row pa-md bdr-b">
|
||||
<h4 class="fw-sb">{{ 'mautic.user.account.header.details'|trans }}</h4>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h3 id="personal-info" class="pt-20 mb-xs">{{ 'mautic.user.config.title.get_to_know_you'|trans }}</h3>
|
||||
<div class="text-secondary small pb-20">{{ 'mautic.user.config.personalize.interface.helper'|trans }}</div>
|
||||
{{ permissions.editName ? form_row(userForm.firstName) : form_row(userForm.firstName_unbound) }}
|
||||
{{ permissions.editName ? form_row(userForm.lastName) : form_row(userForm.lastName_unbound) }}
|
||||
{{ permissions.editPosition ? form_row(userForm.position) : form_row(userForm.position_unbound) }}
|
||||
{{ form_row(userForm.signature) }}
|
||||
|
||||
<h3 id="locale" class="pt-20 mb-xs">{{ 'mautic.user.config.title.locale'|trans }}</h3>
|
||||
<div class="text-secondary small pb-20">{{ 'mautic.user.config.region.preferences.helper'|trans }}</div>
|
||||
{{ form_row(userForm.timezone) }}
|
||||
{{ form_row(userForm.locale) }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h3 id="security" class="pt-20 mb-xs">{{ 'mautic.user.config.title.account_data_security'|trans }}</h3>
|
||||
<div class="text-secondary small pb-20">{{ 'mautic.user.config.account.security.helper'|trans }}</div>
|
||||
{{ permissions.editUsername ? form_row(userForm.username) : form_row(userForm.username_unbound) }}
|
||||
{{ permissions.editEmail ? form_row(userForm.email) : form_row(userForm.email_unbound) }}
|
||||
{% if not isSamlUser %}
|
||||
{{ form_row(userForm.plainPassword.password) }}
|
||||
{{ form_row(userForm.plainPassword.confirm) }}
|
||||
{% endif %}
|
||||
<h3 id="learning" class="pt-20 mb-xs">{{ 'mautic.user.config.title.experience_and_learning'|trans }}</h3>
|
||||
<div class="text-secondary small pb-20">{{ 'mautic.user.config.title.experience_and_learning.description'|trans }}</div>
|
||||
{% include '@MauticCore/Helper/button.html.twig' with {
|
||||
buttons: [
|
||||
{
|
||||
label: 'mautic.user.config.title.experience_and_learning.reset_dismissed',
|
||||
variant: 'tertiary',
|
||||
size: 'sm',
|
||||
icon: 'ri-refresh-line',
|
||||
danger: true,
|
||||
onclick: 'Mautic.resetDismissedElements()',
|
||||
}
|
||||
]
|
||||
} %}
|
||||
</div>
|
||||
</div>
|
||||
{{ form_row(userForm.preferences) }}
|
||||
{{ form_end(userForm) }}
|
||||
</div>
|
||||
|
||||
{% if permissions.apiAccess is defined and permissions.apiAccess %}
|
||||
<div class="tab-pane fade bdr-rds-0 bdr-w-0" id="clients">
|
||||
<div class="row pa-md bdr-b">
|
||||
<h4 class="fw-sb">{{ 'mautic.user.account.header.authorizedclients'|trans }}</h4>
|
||||
</div>
|
||||
<div class="row">
|
||||
{{ authorizedClients|raw }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="tab-pane fade bdr-rds-0 bdr-w-0" id="appearance">
|
||||
<div class="row pa-md bdr-b">
|
||||
<h4 class="fw-sb">{{ 'mautic.user.account.appearance'|trans }}</h4>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 mb-lg">
|
||||
|
||||
<h3 class="pt-20">
|
||||
{{ 'mautic.user.account.appearance.theme_preferences'|trans }}
|
||||
{% include '@MauticCore/Components/toggletip.html.twig' with {
|
||||
title: 'mautic.user.account.appearance.theme_preferences.popover.title',
|
||||
content: 'mautic.user.account.appearance.theme_preferences.popover.content',
|
||||
} %}
|
||||
</h3>
|
||||
<div class="help-block mb-md">{{ 'mautic.user.account.appearance.theme_help'|trans }}</div>
|
||||
<div class="radio-group radio-cards row">
|
||||
{{ include('@MauticUser/Helper/theme.html.twig', {
|
||||
themes: [
|
||||
{
|
||||
id: 'light',
|
||||
name: 'mautic.user.account.appearance.theme_light',
|
||||
description: 'mautic.user.account.appearance.theme_light_description',
|
||||
preview: '@MauticUser/Profile/images/_light_preview.html.twig',
|
||||
checked: true
|
||||
},
|
||||
{
|
||||
id: 'dark',
|
||||
name: 'mautic.user.account.appearance.theme_dark',
|
||||
description: 'mautic.user.account.appearance.theme_dark_description',
|
||||
preview: '@MauticUser/Profile/images/_dark_preview.html.twig'
|
||||
},
|
||||
{
|
||||
id: 'solarized-light',
|
||||
name: 'mautic.user.account.appearance.theme_solarized_light',
|
||||
description: 'mautic.user.account.appearance.theme_solarized_light_description',
|
||||
preview: 'app/bundles/UserBundle/Resources/views/Profile/images/_solarized_light_preview.svg'
|
||||
},
|
||||
{
|
||||
id: 'solarized-dark',
|
||||
name: 'mautic.user.account.appearance.theme_solarized_dark',
|
||||
description: 'mautic.user.account.appearance.theme_solarized_dark_description',
|
||||
preview: 'app/bundles/UserBundle/Resources/views/Profile/images/_solarized_dark_preview.svg'
|
||||
},
|
||||
{
|
||||
id: 'dark-freire',
|
||||
name: 'mautic.user.account.appearance.theme_freire',
|
||||
description: 'mautic.user.account.appearance.theme_freire_description',
|
||||
preview: 'app/bundles/UserBundle/Resources/views/Profile/images/_freire_preview.svg'
|
||||
}
|
||||
]
|
||||
}) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade bdr-rds-0 bdr-w-0" id="accessibility">
|
||||
<div class="row pa-md bdr-b">
|
||||
<h4 class="fw-sb">{{ 'mautic.user.account.accessibility'|trans }}</h4>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 mb-lg">
|
||||
<h3 class="pt-20 mb-md">{{ 'mautic.user.account.accessibility.appearance'|trans }}</h3>
|
||||
<h4>{{ 'mautic.user.account.accessibility.smooth_color_transitions'|trans }}</h4>
|
||||
<div class="help-block">{{ 'mautic.user.account.accessibility.smooth_color_transitions_help'|trans }}</div>
|
||||
<div class="radio-group">
|
||||
<div class="radio-option">
|
||||
<input type="radio" id="transitions-enabled" name="reduce-motion" value="false"
|
||||
data-attribute-toggle="reduce-motion" data-attribute-value="false" checked>
|
||||
<label for="transitions-enabled">
|
||||
<strong>{{ 'mautic.user.account.accessibility.enabled'|trans }}</strong>
|
||||
<div class="text-help">{{ 'mautic.user.account.accessibility.transitions_enabled_description'|trans }}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio-option">
|
||||
<input type="radio" id="transitions-disabled" name="reduce-motion" value="true"
|
||||
data-attribute-toggle="reduce-motion" data-attribute-value="true">
|
||||
<label for="transitions-disabled">
|
||||
<strong>{{ 'mautic.user.account.accessibility.disabled'|trans }}</strong>
|
||||
<div class="text-help">{{ 'mautic.user.account.accessibility.transitions_disabled_description'|trans }}</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<h4>{{ 'mautic.user.account.accessibility.borders_outline_style'|trans }}</h4>
|
||||
<div class="help-block">{{ 'mautic.user.account.accessibility.borders_outline_style_help'|trans }}</div>
|
||||
<div class="radio-group">
|
||||
<div class="radio-option">
|
||||
<input type="radio" id="borders-disabled" name="contrast-borders" value="false"
|
||||
data-attribute-toggle="contrast-borders" data-attribute-value="false" checked>
|
||||
<label for="borders-disabled">
|
||||
<strong>{{ 'mautic.user.account.accessibility.disabled'|trans }}</strong>
|
||||
<div class="text-help">{{ 'mautic.user.account.accessibility.borders_disabled_description'|trans }}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio-option">
|
||||
<input type="radio" id="borders-enabled" name="contrast-borders" value="true"
|
||||
data-attribute-toggle="contrast-borders" data-attribute-value="true">
|
||||
<label for="borders-enabled">
|
||||
<strong>{{ 'mautic.user.account.accessibility.enabled'|trans }}</strong>
|
||||
<div class="text-help">{{ 'mautic.user.account.accessibility.borders_enabled_description'|trans }}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<h4>{{ 'mautic.user.account.accessibility.reduce_transparency_blur'|trans }}</h4>
|
||||
<div class="help-block">{{ 'mautic.user.account.accessibility.reduce_transparency_blur_help'|trans }}</div>
|
||||
<div class="radio-group">
|
||||
<div class="radio-option">
|
||||
<input type="radio" id="transparency-normal" name="reduce-transparency"
|
||||
value="false" data-attribute-toggle="reduce-transparency"
|
||||
data-attribute-value="false" checked>
|
||||
<label for="transparency-normal">
|
||||
<strong>{{ 'mautic.user.account.accessibility.normal'|trans }}</strong>
|
||||
<div class="text-help">{{ 'mautic.user.account.accessibility.transparency_normal_description'|trans }}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio-option">
|
||||
<input type="radio" id="transparency-reduced" name="reduce-transparency"
|
||||
value="true" data-attribute-toggle="reduce-transparency"
|
||||
data-attribute-value="true">
|
||||
<label for="transparency-reduced">
|
||||
<strong>{{ 'mautic.user.account.accessibility.reduced'|trans }}</strong>
|
||||
<div class="text-help">{{ 'mautic.user.account.accessibility.transparency_reduced_description'|trans }}</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<h3 class="pt-20 mb-md">{{ 'mautic.user.account.accessibility.content'|trans }}</h3>
|
||||
<h4>{{ 'mautic.user.account.accessibility.link_underlines'|trans }}</h4>
|
||||
<div class="help-block">{{ 'mautic.user.account.accessibility.link_underlines_help'|trans }}
|
||||
</div>
|
||||
<div class="radio-group">
|
||||
<div class="radio-option">
|
||||
<input type="radio" id="underlines-disabled" name="enable-underlines" value="false"
|
||||
data-attribute-toggle="enable-underlines" data-attribute-value="false" checked>
|
||||
<label for="underlines-disabled">
|
||||
<strong>{{ 'mautic.user.account.accessibility.disabled'|trans }}</strong>
|
||||
<div class="text-help">{{ 'mautic.user.account.accessibility.underlines_disabled_description'|trans }}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio-option">
|
||||
<input type="radio" id="underlines-enabled" name="enable-underlines" value="true"
|
||||
data-attribute-toggle="enable-underlines" data-attribute-value="true">
|
||||
<label for="underlines-enabled">
|
||||
<strong>{{ 'mautic.user.account.accessibility.enabled'|trans }}</strong>
|
||||
<div class="text-help">{{ 'mautic.user.account.accessibility.underlines_enabled_description'|trans }}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--/ end: container -->
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,105 @@
|
||||
{% extends '@MauticCore/Default/content.html.twig' %}
|
||||
{% block mauticContent %}role{% endblock %}
|
||||
{% do assetAddScriptDeclaration('MauticVars.permissionList = ' ~ (permissionsConfig['list']|json_encode), 'bodyClose') %}
|
||||
|
||||
{% set objectId = form.vars.data.getId() %}
|
||||
{% if objectId is not empty %}
|
||||
{% set name = form.vars.data.getName() %}
|
||||
{% set header = 'mautic.user.role.header.edit'|trans({'%name%' : name}) %}
|
||||
{% else %}
|
||||
{% set header = 'mautic.user.role.header.new'|trans %}
|
||||
{% endif %}
|
||||
|
||||
{% block headerTitle %}{{ header }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{{ form_start(form) }}
|
||||
<div class="box-layout">
|
||||
<div class="col-xs-12 height-auto">
|
||||
<!-- tabs controls -->
|
||||
<ul class="nav nav-tabs nav-tabs-contained">
|
||||
<li class="active"><a href="#details-container" role="tab" data-toggle="tab">{% trans %}mautic.core.details{% endtrans %}</a></li>
|
||||
<li class="" id="permissions-tab"><a href="#permissions-container" role="tab" data-toggle="tab">{% trans %}mautic.user.role.permissions{% endtrans %}</a></li>
|
||||
</ul>
|
||||
<!--/ tabs controls -->
|
||||
|
||||
<div class="tab-content pa-md">
|
||||
<div class="tab-pane fade in active bdr-w-0 height-auto" id="details-container">
|
||||
<div class="row">
|
||||
<div class="pa-md">
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.name) }}
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
{{ form_row(form.isAdmin) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="pa-md">
|
||||
{{ form_row(form.description) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% set hidePerms = form.isAdmin.vars.data %}
|
||||
<div class="tab-pane fade bdr-w-0" id="permissions-container">
|
||||
<div id="rolePermissions"{% if hidePerms %}class="hide"{% endif %}>
|
||||
<!-- start: box layout -->
|
||||
<div class="box-layout">
|
||||
<!-- step container -->
|
||||
<div class="col-md-5 height-auto">
|
||||
<div class="pr-lg pl-lg pt-md pb-md">
|
||||
|
||||
<!-- Nav tabs -->
|
||||
<ul class="list-group list-group-tabs" role="tablist">
|
||||
{% for bundle, config in permissionsConfig['config'] %}
|
||||
<li role="presentation" class="list-group-item {{ loop.index0 is same as(0) ? 'in active' : '' }}">
|
||||
<a href="#{{ bundle }}PermissionTab" aria-controls="{{ bundle }}PermissionTab" role="tab" data-toggle="tab" class="list-group-item-heading steps">
|
||||
<span>{{ config['label'] }}</span>
|
||||
<span class="permission-ratio"> (<span class="{{ bundle }}_granted">{{ config['ratio'][0] }}</span> / <span class="{{ bundle }}_total">{{ config['ratio'][1] }}</span>)</span>
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- container -->
|
||||
<div class="col-md-7 height-auto bdr-l">
|
||||
<div class="tab-content">
|
||||
{% set permissions = form.permissions.children %}
|
||||
{% for child in permissions %}
|
||||
{% if 'newbundle' == child.vars.value %}
|
||||
{% if loop.index0 > 0 %}
|
||||
{# // Close tab panel #}
|
||||
</div>{{ "\n" }}</div>{{ "\n" }}
|
||||
{% endif %}
|
||||
<div role="tabpanel" class="tab-pane fade{{ 0 is same as(loop.index0) ? ' in active' : '' }} bdr-w-0" id="{{ child.vars.name }}PermissionTab">{{ "\n" }}
|
||||
<div class="pt-md pr-md pl-md pb-md"> {{ "\n" }}
|
||||
{% do child.setRendered() %}
|
||||
{% else %}
|
||||
{{ form_row(child) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{# //close last tab #}
|
||||
</div>{{ "\n" }}
|
||||
{% do form.permissions.setRendered() %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="isAdminMessage"{% if not hidePerms %} class="hide"{% endif %}>
|
||||
<div class="alert alert-warning">
|
||||
<h4>{% trans %}mautic.user.role.permission.isadmin.header{% endtrans %}</h4>
|
||||
<p>{% trans %}mautic.user.role.permission.isadmin.message{% endtrans %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,150 @@
|
||||
{% set isIndex = tmpl == 'index' ? true : false %}
|
||||
{% set tmpl = 'list' %}
|
||||
|
||||
{% extends isIndex ? '@MauticCore/Default/content.html.twig' : '@MauticCore/Default/raw_output.html.twig' %}
|
||||
{% block mauticContent %}role{% endblock %}
|
||||
{% block headerTitle %}{% trans %}mautic.user.roles{% endtrans %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if isIndex %}
|
||||
<div id="page-list-wrapper" class="panel panel-default">
|
||||
{{- include('@MauticCore/Helper/list_toolbar.html.twig', {
|
||||
'searchValue': searchValue,
|
||||
'action': currentRoute,
|
||||
'page_actions': {
|
||||
'templateButtons': {
|
||||
'new': permissions['create'],
|
||||
},
|
||||
'routeBase': 'role',
|
||||
'langVar': 'user.role',
|
||||
},
|
||||
'bulk_actions': {
|
||||
'routeBase': 'role',
|
||||
'langVar': 'user.role',
|
||||
'templateButtons': {
|
||||
'delete': permissions['delete']
|
||||
}
|
||||
},
|
||||
'quickFilters': [
|
||||
{
|
||||
'search': 'mautic.user.user.searchcommand.isadmin',
|
||||
'label': 'mautic.user.role.form.isadmin',
|
||||
'tooltip': 'mautic.core.search.quickfilter.is_admin',
|
||||
'icon': 'ri-admin-line'
|
||||
}
|
||||
]
|
||||
}) -}}
|
||||
<div class="page-list">
|
||||
{{ block('listResults') }}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ block('listResults') }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block listResults %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover role-list" id="roleTable">
|
||||
<thead>
|
||||
<tr>
|
||||
{{- include('@MauticCore/Helper/tableheader.html.twig',
|
||||
{
|
||||
'checkall' : 'true',
|
||||
'target' : '#roleTable'
|
||||
}
|
||||
) -}}
|
||||
|
||||
{{- include(
|
||||
'@MauticCore/Helper/tableheader.html.twig',
|
||||
{
|
||||
'sessionVar' : 'role',
|
||||
'orderBy' : 'r.name',
|
||||
'text' : 'mautic.core.name',
|
||||
'class' : 'col-role-name',
|
||||
'default' : true,
|
||||
}
|
||||
) -}}
|
||||
|
||||
{{- include(
|
||||
'@MauticCore/Helper/tableheader.html.twig',
|
||||
{
|
||||
'sessionVar' : 'role',
|
||||
'orderBy' : 'r.description',
|
||||
'text' : 'mautic.core.description',
|
||||
'class' : 'visible-md visible-lg col-role-desc',
|
||||
}
|
||||
) -}}
|
||||
<th class="visible-md visible-lg col-rolelist-usercount">
|
||||
{% trans %}mautic.user.role.list.thead.usercount{% endtrans %}
|
||||
</th>
|
||||
{{- include('@MauticCore/Helper/tableheader.html.twig',
|
||||
{
|
||||
'sessionVar' : 'role',
|
||||
'orderBy' : 'r.id',
|
||||
'text' : 'mautic.core.id',
|
||||
'class' : 'visible-md visible-lg col-role-id',
|
||||
}
|
||||
) -}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
{% set mauticTemplateVars = _context|merge({'item' : item}) %}
|
||||
<tr>
|
||||
<td>
|
||||
{{- include('@MauticCore/Helper/list_actions.html.twig',
|
||||
{
|
||||
'item' : item,
|
||||
'templateButtons' : {
|
||||
'edit' : permissions['edit'],
|
||||
'delete' : permissions['delete'],
|
||||
},
|
||||
'routeBase' : 'role',
|
||||
'langVar' : 'user.role',
|
||||
'pull' : 'left',
|
||||
}) -}}
|
||||
</td>
|
||||
<td>
|
||||
{% if permissions['edit'] %}
|
||||
<a href="{{ path('mautic_role_action',
|
||||
{'objectAction' : 'edit', 'objectId' : item.getId()}) }}" data-toggle="ajax">
|
||||
{{ item.getName() }}
|
||||
{{ customContent('role.name', mauticTemplateVars) }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ item.getName() }}
|
||||
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="visible-md visible-lg">
|
||||
{{ item.getDescription()|purify }}
|
||||
</td>
|
||||
<td class="visible-md visible-lg">
|
||||
<a size="sm" class="label label-gray" href="{{ path('mautic_user_index',
|
||||
{'search' : ('mautic.user.user.searchcommand.role'|trans) ~ ':"' ~ item.getName() ~ '"'}) }}" data-toggle="ajax"{{ userCounts[item.getId()] == 0 ? 'disabled=disabled' : '' }}>
|
||||
{{ 'mautic.user.role.list.viewusers_count'|trans(
|
||||
{'%count%' : userCounts[item.getId()]}
|
||||
) }}
|
||||
</a>
|
||||
</td>
|
||||
<td class="visible-md visible-lg">
|
||||
{{ item.getId() }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
{{- include('@MauticCore/Helper/pagination.html.twig',
|
||||
{
|
||||
'totalItems' : items|length,
|
||||
'page' : page,
|
||||
'limit' : limit,
|
||||
'baseUrl' : path('mautic_role_index'),
|
||||
'sessionVar' : 'role',
|
||||
}
|
||||
) -}}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,42 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>{% block pageTitle %}Mautic{% endblock %}</title>
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="{{ getOverridableUrl('images/favicon.ico') }}" />
|
||||
<link rel="apple-touch-icon" href="{{ getOverridableUrl('images/apple-touch-icon.png') }}" />
|
||||
{{ outputSystemStylesheets() }}
|
||||
{{- include('@MauticCore/Default/script.html.twig') -}}
|
||||
{{ outputHeadDeclarations() }}
|
||||
</head>
|
||||
<body>
|
||||
<section id="main" role="main">
|
||||
<div class="container ml-a mr-a" style="margin-top:100px;">
|
||||
<div class="row">
|
||||
<div class="col-lg-4 col-lg-offset-4">
|
||||
<div class="panel" name="form-login">
|
||||
<div class="panel-body">
|
||||
<div class="mautic-logo img-circle mb-md text-center">
|
||||
{{ source('@MauticCore/Assets/images/logo--minimized.svg') }}
|
||||
</div>
|
||||
<div id="main-panel-flash-msgs">
|
||||
{{- include('@MauticCore/Notification/flashes.html.twig') -}}
|
||||
</div>
|
||||
{% block content %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-lg-4 col-lg-offset-4 text-center text-secondary">
|
||||
{{ 'mautic.core.copyright'|trans({'%date%' : date('Y').format('Y') }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{{ securityGetAuthenticationContext() }}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,69 @@
|
||||
{% extends app.request.xmlHttpRequest ? '@MauticCore/Default/content.html.twig' : '@MauticUser/Security/base.html.twig' %}
|
||||
{% block header %}{% trans %}mautic.user.auth.header{% endtrans %}{% endblock %}
|
||||
{% block headerTitle %}{% trans %}mautic.user.auth.expired.header{% endtrans %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if app.request.xmlHttpRequest %}
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-sm-8 col-md-6 inline-login">
|
||||
{{ block('mainContent') }}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ block('mainContent') }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block mainContent %}
|
||||
<form class="form-group login-form" name="login" data-toggle="ajax" role="form" action="{{ path('mautic_user_logincheck') }}" method="post">
|
||||
<div class="input-group mb-md">
|
||||
|
||||
<span class="input-group-addon">
|
||||
<i class="ri-user-6-fill"></i>
|
||||
</span>
|
||||
<label for="username" class="sr-only">{% trans %}mautic.user.auth.form.loginusername{% endtrans %}</label>
|
||||
<input type="text" id="username" name="_username" class="form-control input-lg" value="{{ last_username|escape }}" required autofocus placeholder="{% trans %}mautic.user.auth.form.loginusername{% endtrans %}"/>
|
||||
</div>
|
||||
<div class="input-group mb-md">
|
||||
<span class="input-group-addon">
|
||||
<i class="ri-key-2-line"></i>
|
||||
</span>
|
||||
<label for="password" class="sr-only">{% trans %}mautic.core.password{% endtrans %}:</label>
|
||||
<input type="password" id="password" name="_password" class="form-control input-lg" required placeholder="{% trans %}mautic.core.password{% endtrans %}"/>
|
||||
</div>
|
||||
|
||||
<div class="checkbox-inline custom-primary pull-left mb-md">
|
||||
<label for="remember_me">
|
||||
<input type="checkbox" id="remember_me" name="_remember_me"/>
|
||||
<span></span>
|
||||
{% trans %}mautic.user.auth.form.rememberme{% endtrans %}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token('authenticate')|escape }}"/>
|
||||
{% include '@MauticCore/Helper/button.html.twig' with {
|
||||
buttons: [
|
||||
{
|
||||
label: 'mautic.user.auth.form.loginbtn',
|
||||
variant: 'primary',
|
||||
size: 'lg',
|
||||
wide: true
|
||||
}
|
||||
]
|
||||
} %}
|
||||
|
||||
<div class="mt-sm text-right">
|
||||
<a href="{{ path('mautic_user_passwordreset') }}">{% trans %}mautic.user.user.passwordreset.link{% endtrans %}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% if integrations is not empty %}
|
||||
<ul class="list-group">
|
||||
{% for sso in integrations %}
|
||||
<a href="{{ path('mautic_sso_login', {'integration' : sso.getName()}) }}" class="list-group-item">
|
||||
<img class="pull-left mr-xs" style="height: 16px;" src="{{ getAssetUrl(sso.getIcon()) }}">
|
||||
<p class="list-group-item-heading">{{ ('mautic.integration.sso.' ~ sso.getName())|trans }}</p>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,12 @@
|
||||
{% extends app.request.xmlHttpRequest ? '@MauticCore/Default/content.html.twig' : '@MauticUser/Security/base.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="alert alert-warning">{% trans %}mautic.user.user.passwordreset.info{% endtrans %}</div>
|
||||
{{ form_start(form) }}
|
||||
{{ form_row(form.identifier) }}
|
||||
{{ form_widget(form.submit) }}
|
||||
{{ form_end(form) }}
|
||||
<div class="mt-sm">
|
||||
<a href="{{ path('login') }}">{% trans %}mautic.user.user.passwordreset.back{% endtrans %}</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,15 @@
|
||||
{% extends app.request.xmlHttpRequest ? '@MauticCore/Default/content.html.twig' : '@MauticUser/Security/base.html.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="alert alert-warning">{% trans %}mautic.user.user.passwordresetconfirm.info{% endtrans %}</div>
|
||||
{{ form_start(form) }}
|
||||
{{ form_row(form.identifier) }}
|
||||
{{ form_row(form.plainPassword.password) }}
|
||||
{{ form_row(form.plainPassword.confirm) }}
|
||||
{{ form_widget(form.submit) }}
|
||||
{{ form_end(form) }}
|
||||
|
||||
<div class="mt-sm">
|
||||
<a href="{{ path('login') }}">{% trans %}mautic.user.user.passwordreset.back{% endtrans %}</a>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,6 @@
|
||||
{% extends app.request.xmlHttpRequest ? '@MauticCore/Default/content.html.twig' : '@MauticUser/Security/base.html.twig' %}
|
||||
{% block content %}
|
||||
<a class="btn btn-lg btn-primary btn-block" href="{{ loginRoute }}">
|
||||
{{ 'mautic.user.auth.form.loginbtn'|trans }}
|
||||
</a>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,10 @@
|
||||
{% if showMore is defined and showMore is not empty %}
|
||||
<a href="{{ url('mautic_role_index', {'filter-user' : searchString}) }}" data-toggle="ajax">
|
||||
<span>{{ 'mautic.core.search.more'|trans({'%count%' : remaining}) }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{{ url('mautic_role_action', {'objectAction' : 'edit', 'objectId' : item.getId()}) }}">
|
||||
<span class="fw-sb">{{ item.name }}</span>
|
||||
<span class="ml-4 mr-sm">#{{ item.id }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,25 @@
|
||||
{% if showMore is defined and showMore is not empty %}
|
||||
<a href="{{ url('mautic_user_index', {'filter-user' : searchString}) }}" data-toggle="ajax">
|
||||
<span>{{ 'mautic.core.search.more'|trans({'%count%' : remaining}) }}</span>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="d-flex ai-center">
|
||||
<span class="pull-left mr-sm pt-xs" style="width:36px">
|
||||
<span class="img-wrapper img-rounded"><img src="{{ gravatarGetImage(item.getEmail(), '100') }}" /></span>
|
||||
</span>
|
||||
<div class="d-flex fd-column">
|
||||
<div>
|
||||
<a class="fw-sb mr-4" href="{{ url('mautic_user_action', {'objectAction' : 'edit', 'objectId' : item.getId()}) }}">
|
||||
{{ item.getName() }}
|
||||
</a>
|
||||
{{- include('@MauticCore/Helper/publishstatus_badge.html.twig', {
|
||||
'entity': item,
|
||||
'status': 'active',
|
||||
'simplified': 'true'
|
||||
}) -}}
|
||||
</div>
|
||||
<div class="small text-muted">{{ item.getPosition() }}</div>
|
||||
</div>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends '@MauticCore/Default/content.html.twig' %}
|
||||
{% block mauticContent %}user{% endblock %}
|
||||
{% block headerTitle %}{{ 'mautic.user.user.header.contact'|trans({'%name%': user.getName()}) }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="panel">
|
||||
<div class="panel-body pa-md">
|
||||
{{ form(form) }}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,94 @@
|
||||
{% extends '@MauticCore/Default/content.html.twig' %}
|
||||
{% block mauticContent %}user
|
||||
{% endblock %}
|
||||
{% set userId = form.vars.data.getId() %}
|
||||
{% if userId is not empty %}
|
||||
{% set user = form.vars.data.getName() %}
|
||||
{% set header = 'mautic.user.user.header.edit'|trans({'%name%': user}) %}
|
||||
{% else %}
|
||||
{% set header = 'mautic.user.user.header.new'|trans %}
|
||||
{% endif %}
|
||||
|
||||
{% block headerTitle %}
|
||||
{{ header }}
|
||||
{% endblock %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
|
||||
<!-- start: box layout -->
|
||||
<div
|
||||
class="box-layout">
|
||||
<!-- container -->
|
||||
{{ form_start(form) }}
|
||||
<div class="col-md-9 height-auto bdr-r">
|
||||
<div class="pa-md">
|
||||
<div class="form-group mb-0">
|
||||
<div class="row">
|
||||
<div class="col-sm-6{{ form.firstName.vars.errors|length ? ' has-error' : '' }}">
|
||||
<h3 class="mb-lg mt-lg">{{ 'mautic.user.config.title.personal_information'|trans }}</h3>
|
||||
<div class="form-group">
|
||||
<label class="control-label mb-xs">{{ form_label(form.firstName) }}</label>
|
||||
{{ form_widget(form.firstName, {'attr' : {'placeholder' : form.firstName.vars.label }}) }}
|
||||
{{ form_errors(form.firstName) }}
|
||||
</div>
|
||||
<div class="form-group{{ form.lastName.vars.errors|length ? ' has-error' : '' }}">
|
||||
<label class="control-label mb-xs">{{ form_label(form.lastName) }}</label>
|
||||
{{ form_widget(form.lastName, {'attr' : {'placeholder' : form.lastName.vars.label }}) }}
|
||||
{{ form_errors(form.lastName) }}
|
||||
</div>
|
||||
<div class="form-group{{ form.role.vars.errors|length ? ' has-error' : '' }}">
|
||||
<label class="control-label mb-xs">{{ form_label(form.role) }}</label>
|
||||
{{ form_widget(form.role, {'attr' : {'placeholder' : form.role.vars.label }}) }}
|
||||
{{ form_errors(form.role) }}
|
||||
</div>
|
||||
<div class="form-group pb-lg{{ form.position.vars.errors|length ? ' has-error' : '' }}">
|
||||
<label class="control-label mb-xs">{{ form_label(form.position) }}</label>
|
||||
{{ form_widget(form.position, {'attr' : {'placeholder' : form.position.vars.label }}) }}
|
||||
{{ form_errors(form.position) }}
|
||||
</div>
|
||||
<div class="form-group pt-md{{ form.signature.vars.errors|length ? ' has-error' : '' }}">
|
||||
<label class="control-label mb-xs">{{ form_label(form.signature) }}</label>
|
||||
{{ form_widget(form.signature, {'attr' : {'placeholder' : form.signature.vars.label }}) }}
|
||||
{{ form_errors(form.signature) }}
|
||||
<p class="help-text">{{ 'mautic.user.config.signature.admin.helper'|trans }} </p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
<h3 class="mb-lg mt-lg">{{ 'mautic.user.config.title.account_data_security'|trans }}</h3>
|
||||
<div class="form-group{{ form.username.vars.errors|length ? ' has-error' : '' }}">
|
||||
{{ form_label(form.username) }}
|
||||
{{ form_widget(form.username, {'attr' : {'placeholder' : form.username.vars.label }}) }}
|
||||
{{ form_errors(form.username) }}
|
||||
</div>
|
||||
<div class="form-group{{ form.email.vars.errors|length ? ' has-error' : '' }}">
|
||||
{{ form_label(form.email) }}
|
||||
{{ form_widget(form.email, {'attr' : {'placeholder' : form.email.vars.label }}) }}
|
||||
{{ form_errors(form.email) }}
|
||||
</div>
|
||||
{% if isSamlUser is empty %}
|
||||
{{ form_widget(form.plainPassword, {'attr' : {'placeholder' : form.plainPassword.vars.label }}) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 height-auto">
|
||||
<div class="pr-lg pl-lg pt-md pb-md">
|
||||
<h3 class="mb-lg mt-lg">{{ 'mautic.user.config.title.locale'|trans }}</h3>
|
||||
{{ form_rest(form) }}
|
||||
</div>
|
||||
</div>
|
||||
{% if editAction is defined and editAction == true %}
|
||||
{{- include('@MauticUser/User/recent_activity.html.twig', {
|
||||
'logs' : logs,
|
||||
'users' : users,
|
||||
'roles' : roles,
|
||||
}) -}}
|
||||
{% endif %}
|
||||
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,174 @@
|
||||
{% set isIndex = tmpl == 'index' ? true : false %}
|
||||
{% set tmpl = 'list' %}
|
||||
|
||||
{% extends isIndex ? '@MauticCore/Default/content.html.twig' : '@MauticCore/Default/raw_output.html.twig' %}
|
||||
{% block mauticContent %}user
|
||||
{% endblock %}
|
||||
{% block headerTitle %}
|
||||
{% trans %}mautic.user.users{% endtrans %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% if isIndex %}
|
||||
<div id="page-list-wrapper" class="panel panel-default">
|
||||
{{- include('@MauticCore/Helper/list_toolbar.html.twig', {
|
||||
'searchValue': searchValue,
|
||||
'action': currentRoute,
|
||||
'page_actions': {
|
||||
'templateButtons': {
|
||||
'new': permissions['create'],
|
||||
},
|
||||
'routeBase': 'user',
|
||||
'langVar': 'user.user',
|
||||
},
|
||||
'bulk_actions': {
|
||||
'routeBase': 'user',
|
||||
'langVar': 'user.user',
|
||||
'templateButtons': {
|
||||
'delete': permissions['delete']
|
||||
}
|
||||
},
|
||||
'quickFilters': [
|
||||
{
|
||||
'search': 'mautic.user.user.searchcommand.isadmin',
|
||||
'label': 'mautic.user.role.form.isadmin',
|
||||
'tooltip': 'mautic.core.search.quickfilter.is_admin',
|
||||
'icon': 'ri-admin-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.ispublished',
|
||||
'label': 'mautic.core.form.active',
|
||||
'tooltip': 'mautic.core.search.quickfilter.is_published',
|
||||
'icon': 'ri-check-line'
|
||||
},
|
||||
{
|
||||
'search': 'mautic.core.searchcommand.isunpublished',
|
||||
'label': 'mautic.core.form.inactive',
|
||||
'tooltip': 'mautic.core.search.quickfilter.is_unpublished',
|
||||
'icon': 'ri-close-line'
|
||||
}
|
||||
]
|
||||
}) -}}
|
||||
<div class="page-list">
|
||||
{{ block('listResults') }}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{{ block('listResults') }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block listResults %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover user-list" id="userTable">
|
||||
<thead>
|
||||
<tr>
|
||||
{{- include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'checkall' : 'true',
|
||||
'target' : '#userTable'
|
||||
}) -}}
|
||||
|
||||
<th class="visible-md visible-lg col-user-avatar"></th>
|
||||
{{- include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar' : 'user',
|
||||
'orderBy' : 'u.lastName, u.firstName, u.username',
|
||||
'text' : 'mautic.core.name',
|
||||
'class' : 'col-user-name',
|
||||
'default' : true,
|
||||
}) -}}
|
||||
{{- include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar' : 'user',
|
||||
'orderBy' : 'u.username',
|
||||
'text' : 'mautic.core.username',
|
||||
'class' : 'col-user-username',
|
||||
}) -}}
|
||||
{{- include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar' : 'user',
|
||||
'orderBy' : 'u.email',
|
||||
'text' : 'mautic.core.type.email',
|
||||
'class' : 'visible-md visible-lg col-user-email',
|
||||
}) -}}
|
||||
|
||||
{{- include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar' : 'user',
|
||||
'orderBy' : 'r.name',
|
||||
'text' : 'mautic.user.role',
|
||||
'class' : 'visible-md visible-lg col-user-role',
|
||||
}) -}}
|
||||
|
||||
{{- include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar' : 'user',
|
||||
'orderBy' : 'u.lastLogin',
|
||||
'text' : 'mautic.user.lastlogin',
|
||||
'class' : 'visible-md visible-lg col-user-lastlogin',
|
||||
}) -}}
|
||||
|
||||
{{- include('@MauticCore/Helper/tableheader.html.twig', {
|
||||
'sessionVar' : 'user',
|
||||
'orderBy' : 'u.id',
|
||||
'text' : 'mautic.core.id',
|
||||
'class' : 'visible-md visible-lg col-user-id',
|
||||
}) -}}
|
||||
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td>
|
||||
{{- include('@MauticCore/Helper/list_actions.html.twig', {
|
||||
'item' : item,
|
||||
'templateButtons' : {
|
||||
'edit' : permissions['edit'],
|
||||
'delete' : permissions['delete'],
|
||||
},
|
||||
'routeBase' : 'user',
|
||||
'langVar' : 'user.user',
|
||||
'pull' : 'left',
|
||||
}) -}}
|
||||
</td>
|
||||
<td class="visible-md visible-lg">
|
||||
<img class="img img-responsive img-thumbnail w-44" src="{{ gravatarGetImage(item.getEmail(), '50') }}"/>
|
||||
</td>
|
||||
<td>
|
||||
<div>
|
||||
{% if item.getId() != currentUserId %}
|
||||
{{- include('@MauticCore/Helper/publishstatus_icon.html.twig', {
|
||||
'item' : item,
|
||||
'model' : 'user',
|
||||
}) -}}
|
||||
{% endif %}
|
||||
{% if permissions['edit'] %}
|
||||
<a href="{{ path( 'mautic_user_action', {'objectAction' : 'edit', 'objectId' : item.getId()} ) }}" data-toggle="ajax">
|
||||
{{ item.getName(true) }}
|
||||
</a>
|
||||
{% else %}
|
||||
{{ item.getName(true) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="small">
|
||||
<em>{{ item.getPosition() }}</em>
|
||||
</div>
|
||||
</td>
|
||||
<td>{{ item.getUsername() }}</td>
|
||||
<td class="visible-md visible-lg">
|
||||
<a href="mailto: {{ item.getEmail() }}">{{ item.getEmail() }}</a>
|
||||
</td>
|
||||
<td class="visible-md visible-lg">{{ item.getRole().getName() }}</td>
|
||||
<td class="visible-md visible-lg">{{ dateToText(item.getLastLogin(), 'local', 'Y-m-d H:i:s', true) }}</td>
|
||||
<td class="visible-md visible-lg">{{ item.getId() }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-footer">
|
||||
{{- include('@MauticCore/Helper/pagination.html.twig', {
|
||||
'totalItems' : items|length,
|
||||
'page' : page,
|
||||
'limit' : limit,
|
||||
'baseUrl' : path('mautic_user_index'),
|
||||
'sessionVar' : 'user',
|
||||
}) -}}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,195 @@
|
||||
<!-- Recent activity block(audit_log table) -->
|
||||
<div class="col-md-3 height-auto">
|
||||
{{ customContent('right.section.top', _context) }}
|
||||
<div class="panel shd-none bdr-rds-0 bdr-w-0 mb-0">
|
||||
<div class="panel-heading">
|
||||
<div class="panel-title">{% trans %}mautic.core.recent.activity{% endtrans %}</div>
|
||||
</div>
|
||||
<div class="panel-body pt-xs">
|
||||
{% if logs is defined and logs is not empty %}
|
||||
<ul class="media-list media-list-feed">
|
||||
{% for log in logs %}
|
||||
|
||||
{% if log.action == 'create' and log.object == 'user' %}
|
||||
|
||||
{% set userPath = [] %}
|
||||
{% set user_id = '1' %}
|
||||
{% set usersEmail = log.details['email'][1] %}
|
||||
{% set emailIsMatch = false %}
|
||||
{% set usernameIsMatch = false %}
|
||||
|
||||
{% for user in users %}
|
||||
{% if usersEmail is defined and usersEmail is not empty and user.email == usersEmail %}
|
||||
{% set emailIsMatch = true %}
|
||||
{% set user_id = user.id %}
|
||||
{% else %}
|
||||
{% set usernameIsMatch = true %}
|
||||
{% set user_id = user.id %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if log.details['username'][1] is defined and log.details['username'][1] is not empty %}
|
||||
{% if usernameIsMatch == true or emailIsMatch == true %}
|
||||
{% set userPath = userPath|merge([user_id, log.details['username'][1]]) %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% elseif log.action == 'create' and log.object == 'role' %}
|
||||
|
||||
{% set role_id = '1' %}
|
||||
{% set roleName = log.details['name'][1] %}
|
||||
{% set roleIsMatch = false %}
|
||||
|
||||
{% for role in roles %}
|
||||
{% if role.name == roleName %}
|
||||
{% set roleIsMatch = true %}
|
||||
{% set role_id = role.id %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if roleIsMatch == true %}
|
||||
{% set rolePath = []|merge([role_id, roleName]) %}
|
||||
{% endif %}
|
||||
|
||||
{% elseif log.action == 'update' and log.object == 'user' %}
|
||||
|
||||
{% set userPath = [] %}
|
||||
{% set usersUsername, usersEmail, usersFirstName, usersLastName, usersRole, usersPosition, usersSignature = '', '', '', '', '', '', '' %}
|
||||
{% set user_id = '1' %}
|
||||
{% set user_username = 'admin' %}
|
||||
{% set isMatch = false %}
|
||||
{% for detail in log.details %}
|
||||
{% if detail == 'username' %}
|
||||
{% set usersUsername = detail[1] %}
|
||||
|
||||
{% elseif detail == 'email' %}
|
||||
{% set usersEmail = detail[1] %}
|
||||
|
||||
{% elseif detail == 'firstname' %}
|
||||
{% set usersFirstName = detail[1] %}
|
||||
|
||||
{% elseif detail == 'lastName' %}
|
||||
{% set usersLastName = detail[1] %}
|
||||
|
||||
{% elseif detail == 'role' %}
|
||||
{% set usersRole = detail[1] %}
|
||||
|
||||
{% elseif detail == 'position' %}
|
||||
{% set usersPosition = detail[1] %}
|
||||
|
||||
{% elseif detail == 'signature' %}
|
||||
{% set usersSignature = detail[1] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% for user in users %}
|
||||
{% if user.email == usersEmail or user.userName == usersUsername or user.firstName == usersFirstName
|
||||
or user.lastName == usersLastName or user.role == usersRole
|
||||
or user.position == usersPosition or user.signature == usersSignature %}
|
||||
{% set isMatch = true %}
|
||||
{% set user_id = user.id %}
|
||||
{% set user_username = user.userName %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if isMatch == true %}
|
||||
{% set userPath = userPath|merge([user_id, user_username]) %}
|
||||
{% endif %}
|
||||
|
||||
{% elseif log.action == 'update' and log.object == 'role' %}
|
||||
{% set name, description, rowPermissions = '', '', '' %}
|
||||
{% set role_id = '1' %}
|
||||
{% set roleName = 'Admininstrator' %}
|
||||
{% set isMatch = false %}
|
||||
{% for detail in log.details %}
|
||||
{% if detail == 'name' %}
|
||||
{% set name = detail[1] %}
|
||||
|
||||
{% elseif detail == 'description' %}
|
||||
{% set description = detail[1] %}
|
||||
|
||||
{% elseif detail == 'rawPermissions' %}
|
||||
{% set rowPermissions = detail[1] %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
{% for role in roles %}
|
||||
{% if role.name == name or role.description == description or
|
||||
role.rawPermissions == rowPermissions %}
|
||||
{% set isMatch = true %}
|
||||
{% set role_id = role.id %}
|
||||
{% set roleName = role.name %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if isMatch == true %}
|
||||
{% set rolePath = []|merge([role_id, roleName]) %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<li class="media">
|
||||
<div class="media-object pull-left">
|
||||
{% if log.action == 'login' %}
|
||||
<span class="figure featured">
|
||||
<span class="ri-login-circle-line"></span>
|
||||
</span>
|
||||
{% elseif log.action == 'update' %}
|
||||
<span class="figure"></span>
|
||||
{% else %}
|
||||
<span class="figure"></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="media-body">
|
||||
{% if log.object == 'user' %}
|
||||
{% if log.action == 'update' %}
|
||||
{{ 'mautic.user.user.form.user'|trans }}
|
||||
<a href="{{ path('mautic_user_action', {objectAction: 'edit', objectId: userPath[0]}) }}" data-toggle="ajax">
|
||||
{{ userPath[1] }}</a>
|
||||
{{ 'mautic.user.role.form.updated_by'|trans }}
|
||||
|
||||
{% elseif log.action == 'create' %}
|
||||
<a href="{{ path('mautic_user_action', {objectAction: 'edit', objectId: userPath[0]}) }}" data-toggle="ajax">
|
||||
{{ userPath[1] }}</a>
|
||||
{{ 'mautic.user.user.form.created_by'|trans }}
|
||||
{% endif %}
|
||||
|
||||
{% elseif log.object == 'role' %}
|
||||
{% if log.action == 'create' %}
|
||||
{{ 'mautic.role.role'|trans }}
|
||||
<a href="{{ path('mautic_role_action', {objectAction: 'edit', objectId: rolePath[0]}) }}" data-toggle="ajax">
|
||||
{{ rolePath[1] }}</a>
|
||||
{{ 'mautic.user.user.form.created_by'|trans }}
|
||||
|
||||
{% elseif log.action == 'update' %}
|
||||
{{ 'mautic.role.role'|trans }}
|
||||
<a href="{{ path('mautic_role_action', {objectAction: 'edit', objectId: rolePath[0]}) }}" data-toggle="ajax">
|
||||
{{ rolePath[1] }}</a>
|
||||
{{ 'mautic.user.role.form.updated_by'|trans }}
|
||||
{% endif %}
|
||||
|
||||
{% elseif log.object == 'security' %}
|
||||
{% if log.action == 'login' %}
|
||||
{{ 'mautic.user.user.form.user'|trans }}
|
||||
|
||||
{% elseif log.action == 'update' %}
|
||||
{{ 'mautic.user.user.form.updated_by'|trans }}
|
||||
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if log.userId is defined and log.userId is not empty %}
|
||||
<a href="{{ path('mautic_user_action', {objectAction: 'edit', objectId: log.userId}) }}" data-toggle="ajax">{{ log.userName }}</a>
|
||||
{% if log.action == 'login' %}{{ 'mautic.user.user.form.login_by'|trans }}{% endif %}
|
||||
|
||||
{% else %}
|
||||
{{ log.userName }}
|
||||
{% endif %}
|
||||
<p class="fs-12 dark-sm">
|
||||
<small>
|
||||
{{ dateToFull(log.dateAdded) }}</small>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{{ customContent('right.section.bottom', _context) }}
|
||||
</div>
|
||||
<!-- Recent activity block(audit_log table) -->
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Security\Authentication;
|
||||
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\RouterInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
|
||||
use Symfony\Component\Security\Http\SecurityRequestAttributes;
|
||||
|
||||
class AuthenticationHandler implements AuthenticationSuccessHandlerInterface, AuthenticationFailureHandlerInterface
|
||||
{
|
||||
public function __construct(
|
||||
private RouterInterface $router,
|
||||
) {
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token): ?Response
|
||||
{
|
||||
// Remove post_logout if set
|
||||
$request->getSession()->remove('post_logout');
|
||||
|
||||
$format = $request->request->get('format');
|
||||
|
||||
if ('json' == $format) {
|
||||
$array = ['success' => true];
|
||||
$response = new Response(json_encode($array));
|
||||
$response->headers->set('Content-Type', 'application/json');
|
||||
|
||||
return $response;
|
||||
} else {
|
||||
$redirectUrl = $request->getSession()->get('_security.main.target_path', $this->router->generate('mautic_dashboard_index'));
|
||||
|
||||
return new RedirectResponse($redirectUrl);
|
||||
}
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
|
||||
{
|
||||
// Remove post_logout if set
|
||||
$request->getSession()->remove('post_logout');
|
||||
|
||||
$format = $request->request->get('format');
|
||||
|
||||
if ('json' == $format) {
|
||||
$array = ['success' => false, 'message' => $exception->getMessage()];
|
||||
$response = new Response(json_encode($array));
|
||||
$response->headers->set('Content-Type', 'application/json');
|
||||
|
||||
return $response;
|
||||
} else {
|
||||
$request->getSession()->set(SecurityRequestAttributes::AUTHENTICATION_ERROR, $exception);
|
||||
|
||||
return new RedirectResponse($this->router->generate('login'));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Security\Authentication\Token\Permissions;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use FOS\OAuthServerBundle\Model\TokenInterface as OAuthTokenInterface;
|
||||
use FOS\OAuthServerBundle\Security\Authenticator\Token\OAuthToken;
|
||||
use Mautic\ApiBundle\Entity\oAuth2\AccessToken;
|
||||
use Mautic\ApiBundle\Entity\oAuth2\Client;
|
||||
use Mautic\UserBundle\Entity\PermissionRepository;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
class TokenPermissions
|
||||
{
|
||||
public function __construct(private TokenStorageInterface $tokenStorage, private PermissionRepository $permissionRepository, private EntityManagerInterface $entityManager)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the active permissions on the current user.
|
||||
*/
|
||||
public function setActivePermissionsOnAuthToken(TokenInterface|OAuthTokenInterface|null $token = null): ?UserInterface
|
||||
{
|
||||
if (null === $token) {
|
||||
$token = $this->tokenStorage->getToken();
|
||||
}
|
||||
|
||||
if (null === $token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$user = $token->getUser();
|
||||
\assert(null === $user || $user instanceof User);
|
||||
|
||||
// If no user is associated with a token, it's a client credentials grant type. Handle accordingly.
|
||||
if (null === $user && ($token instanceof OAuthToken || $token instanceof OAuthTokenInterface)) {
|
||||
$user = $this->assignRoleFromToken($token->getToken());
|
||||
}
|
||||
|
||||
if (null !== $user) {
|
||||
$this->setPermissionsOnUser($user);
|
||||
}
|
||||
|
||||
if (null === $user) {
|
||||
throw new \RuntimeException('The user should be either already set in the token, or come from assignRoleFromToken.');
|
||||
}
|
||||
|
||||
$token->setUser($user);
|
||||
|
||||
if ($token instanceof TokenInterface) {
|
||||
$this->tokenStorage->setToken($token);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle permission for Client Credential grant type.
|
||||
*/
|
||||
private function assignRoleFromToken(string $tokenIdentifier): User
|
||||
{
|
||||
/** @var AccessToken|null $accessToken assert ill yield phpstan error. */
|
||||
$accessToken = $this->entityManager->getRepository(AccessToken::class)->findOneBy(['token' => $tokenIdentifier]);
|
||||
|
||||
if (null === $accessToken) {
|
||||
throw new UserNotFoundException('API access token not found.');
|
||||
}
|
||||
|
||||
$client = $accessToken->getClient();
|
||||
if (!$client instanceof Client) {
|
||||
// There are no tests for this part, so an exception will reveal any inconsistencies earlier.
|
||||
throw new \RuntimeException('The client is not a valid API client.');
|
||||
}
|
||||
|
||||
$role = $client->getRole();
|
||||
|
||||
// Create a pseudo user and assign the role
|
||||
$user = new User();
|
||||
$user->setRole($role);
|
||||
|
||||
// Set for the audit log and the entity's "created by user" metadata which takes the first and last name
|
||||
$user->setFirstName($client->getName());
|
||||
$user->setLastName(sprintf('[%s]', $client->getId()));
|
||||
$user->setUsername($user->getName());
|
||||
defined('MAUTIC_AUDITLOG_USER') || define('MAUTIC_AUDITLOG_USER', $user->getName());
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function setPermissionsOnUser(User $user): void
|
||||
{
|
||||
if (!$user->isAdmin() && (null === $user->getActivePermissions() || [] === $user->getActivePermissions())) {
|
||||
$activePermissions = $this->permissionRepository->getPermissionsByRole($user->getRole());
|
||||
|
||||
$user->setActivePermissions($activePermissions);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Security\Authentication\Token;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
class PluginToken extends AbstractToken
|
||||
{
|
||||
private ?string $providerKey;
|
||||
|
||||
/**
|
||||
* @param UserInterface|string|null $user
|
||||
* @param array<string> $roles
|
||||
*/
|
||||
public function __construct(
|
||||
?string $providerKey,
|
||||
private ?string $authenticatingService = null,
|
||||
$user = null,
|
||||
private string $credentials = '',
|
||||
array $roles = [],
|
||||
private ?Response $response = null,
|
||||
) {
|
||||
parent::__construct($roles);
|
||||
|
||||
if ('' === $providerKey) {
|
||||
throw new \InvalidArgumentException('$providerKey must not be empty.');
|
||||
}
|
||||
|
||||
if (is_string($user)) {
|
||||
$user = null;
|
||||
}
|
||||
|
||||
if (null !== $user) {
|
||||
$this->setUser($user);
|
||||
}
|
||||
|
||||
$this->providerKey = $providerKey;
|
||||
}
|
||||
|
||||
public function getCredentials(): string
|
||||
{
|
||||
return $this->credentials;
|
||||
}
|
||||
|
||||
public function getProviderKey(): ?string
|
||||
{
|
||||
return $this->providerKey;
|
||||
}
|
||||
|
||||
public function getAuthenticatingService(): ?string
|
||||
{
|
||||
return $this->authenticatingService;
|
||||
}
|
||||
|
||||
public function getResponse(): ?Response
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, mixed>
|
||||
*/
|
||||
public function __serialize(): array
|
||||
{
|
||||
return array_merge([$this->authenticatingService, $this->credentials, $this->providerKey, parent::__serialize()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<int, mixed> $data
|
||||
*/
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
[$this->authenticatingService, $this->credentials, $this->providerKey, $parentArray] = $data;
|
||||
parent::__unserialize($parentArray);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Security\Authenticator;
|
||||
|
||||
use OAuth2\OAuth2ServerException;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
|
||||
class Oauth2Authenticator extends \FOS\OAuthServerBundle\Security\Authenticator\Oauth2Authenticator
|
||||
{
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
// needed until the oAuth2 library will not be updated to 4.0.5
|
||||
return null !== $this->serverService->getBearerToken($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* A BC compatible response.
|
||||
*/
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||
{
|
||||
$previous = $exception->getPrevious();
|
||||
if ($previous instanceof OAuth2ServerException) {
|
||||
return $previous->getHttpResponse();
|
||||
}
|
||||
|
||||
return parent::onAuthenticationFailure($request, $exception);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Security\Authenticator\Passport\Badge;
|
||||
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
|
||||
|
||||
class PasswordStrengthBadge implements BadgeInterface
|
||||
{
|
||||
private bool $resolved = false;
|
||||
|
||||
public function __construct(private ?string $presentedPassword)
|
||||
{
|
||||
}
|
||||
|
||||
public function getPresentedPassword(): ?string
|
||||
{
|
||||
return $this->presentedPassword;
|
||||
}
|
||||
|
||||
public function setResolved(): void
|
||||
{
|
||||
$this->resolved = true;
|
||||
}
|
||||
|
||||
public function isResolved(): bool
|
||||
{
|
||||
return $this->resolved;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Security\Authenticator\Passport\Badge;
|
||||
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
|
||||
|
||||
class PluginBadge implements BadgeInterface
|
||||
{
|
||||
public function __construct(private ?TokenInterface $preAuthenticatedToken, private ?Response $pluginResponse, private ?string $authenticatingService)
|
||||
{
|
||||
}
|
||||
|
||||
public function getPreAuthenticatedToken(): ?TokenInterface
|
||||
{
|
||||
return $this->preAuthenticatedToken;
|
||||
}
|
||||
|
||||
public function getPluginResponse(): ?Response
|
||||
{
|
||||
return $this->pluginResponse;
|
||||
}
|
||||
|
||||
public function getAuthenticatingService(): ?string
|
||||
{
|
||||
return $this->authenticatingService;
|
||||
}
|
||||
|
||||
public function isResolved(): bool
|
||||
{
|
||||
return null !== $this->preAuthenticatedToken || null !== $this->pluginResponse;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Security\Authenticator;
|
||||
|
||||
use Mautic\PluginBundle\Helper\IntegrationHelper;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Mautic\UserBundle\Event\AuthenticationEvent;
|
||||
use Mautic\UserBundle\Security\Authentication\AuthenticationHandler;
|
||||
use Mautic\UserBundle\Security\Authentication\Token\Permissions\TokenPermissions;
|
||||
use Mautic\UserBundle\Security\Authentication\Token\PluginToken;
|
||||
use Mautic\UserBundle\Security\Authenticator\Passport\Badge\PluginBadge;
|
||||
use Mautic\UserBundle\UserEvents;
|
||||
use OAuth2\OAuth2;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\LazyResponseException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
|
||||
use Symfony\Component\Security\Http\SecurityEvents;
|
||||
use Symfony\Component\Security\Http\SecurityRequestAttributes;
|
||||
|
||||
final class PluginAuthenticator extends AbstractAuthenticator
|
||||
{
|
||||
public function __construct(private TokenPermissions $tokenPermissions, private EventDispatcherInterface $dispatcher, private IntegrationHelper $integrationHelper, private UserProviderInterface $userProvider, private AuthenticationHandler $authenticationHandler, private OAuth2 $oAuth2, private LoggerInterface $logger, private string $firewallName)
|
||||
{
|
||||
}
|
||||
|
||||
public function supports(Request $request): ?bool
|
||||
{
|
||||
// Pass to oAuth2 if the token is present, but try to execute in case the Request does not look like oAuth2.
|
||||
return null === $this->oAuth2->getBearerToken($request) ? null : false;
|
||||
}
|
||||
|
||||
public function authenticate(Request $request): Passport
|
||||
{
|
||||
$authenticatingService = $request->get('integration');
|
||||
\assert(null === $authenticatingService || is_string($authenticatingService));
|
||||
$token = new PluginToken($this->firewallName, $authenticatingService);
|
||||
|
||||
$user = null;
|
||||
$response = null;
|
||||
$authenticated = false;
|
||||
$authenticatedToken = null;
|
||||
|
||||
// Try authenticating with a plugin
|
||||
if ($this->dispatcher->hasListeners(UserEvents::USER_PRE_AUTHENTICATION)) {
|
||||
$integrations = $this->integrationHelper->getIntegrationObjects($authenticatingService, ['sso_service'], false, null, true);
|
||||
|
||||
$loginCheck = 'mautic_sso_login_check' === $request->attributes->get('_route');
|
||||
$authEvent = new AuthenticationEvent(
|
||||
null,
|
||||
$token,
|
||||
$this->userProvider,
|
||||
$request,
|
||||
$loginCheck,
|
||||
$authenticatingService,
|
||||
$integrations
|
||||
);
|
||||
$authEvent = $this->dispatcher->dispatch($authEvent, UserEvents::USER_PRE_AUTHENTICATION);
|
||||
\assert($authEvent instanceof AuthenticationEvent);
|
||||
|
||||
if ($authenticated = $authEvent->isAuthenticated()) {
|
||||
$eventToken = $authEvent->getToken();
|
||||
$authenticatingService = $authEvent->getAuthenticatingService();
|
||||
|
||||
// Return passport with the token set in the event, if the event set a different token.
|
||||
if ($eventToken !== $token) {
|
||||
return new SelfValidatingPassport(
|
||||
new UserBadge($eventToken->getUserIdentifier(), function () use ($eventToken): UserInterface {
|
||||
return $eventToken->getUser();
|
||||
}),
|
||||
[new PluginBadge($eventToken, null, $authenticatingService)]
|
||||
);
|
||||
}
|
||||
|
||||
// Set the user in the token.
|
||||
$user = $authEvent->getUser();
|
||||
|
||||
if (null === $user) {
|
||||
throw new \RuntimeException('User must be set in the authenticated token.');
|
||||
}
|
||||
|
||||
$authenticatedToken = $eventToken;
|
||||
$authenticatedToken->setUser($user);
|
||||
}
|
||||
|
||||
$response = $authEvent->getResponse();
|
||||
|
||||
if (!$authenticated && $loginCheck && null === $response) {
|
||||
// Set an empty JSON response
|
||||
$response = new JsonResponse([]);
|
||||
}
|
||||
}
|
||||
|
||||
// The check is intended to catch: Plugin authenticator must be authenticated and have $user. oAuth should have a response.
|
||||
if (!$user instanceof User && !$authenticated && null === $response) {
|
||||
throw new AuthenticationException('mautic.user.auth.error.invalidlogin');
|
||||
}
|
||||
|
||||
// Otherwise if the plugin authenticator has a response, then pass it to the Symfony.
|
||||
if (null === $user && !$authenticated && null !== $response) {
|
||||
throw new LazyResponseException($response);
|
||||
}
|
||||
|
||||
return new SelfValidatingPassport(
|
||||
new UserBadge(
|
||||
$user instanceof User ? $user->getUserIdentifier() : $user,
|
||||
function (string $userIdentifier) use ($user): UserInterface {
|
||||
if ($user instanceof User) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
return $this->userProvider->loadUserByIdentifier($userIdentifier);
|
||||
}
|
||||
),
|
||||
[new PluginBadge($authenticatedToken, $response, $authenticatingService)]
|
||||
);
|
||||
}
|
||||
|
||||
public function createToken(Passport $passport, string $firewallName): TokenInterface
|
||||
{
|
||||
$pluginBadge = $passport->getBadge(PluginBadge::class);
|
||||
\assert($pluginBadge instanceof PluginBadge);
|
||||
$userBadge = $passport->getBadge(UserBadge::class);
|
||||
\assert($userBadge instanceof UserBadge);
|
||||
|
||||
// A custom token has not been set by the plugin, so create a new one. Mainly used for oAuth.
|
||||
if (null === $token = $pluginBadge->getPreAuthenticatedToken()) {
|
||||
$user = $userBadge->getUser();
|
||||
$token = new PluginToken(
|
||||
$this->firewallName,
|
||||
$pluginBadge->getAuthenticatingService(),
|
||||
$user,
|
||||
($user instanceof User) ? $user->getPassword() : '',
|
||||
($user instanceof User) ? $user->getRoles() : [],
|
||||
$pluginBadge->getPluginResponse()
|
||||
);
|
||||
}
|
||||
|
||||
$this->tokenPermissions->setActivePermissionsOnAuthToken($token);
|
||||
|
||||
return $token;
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||
{
|
||||
if (!$token instanceof PluginToken) {
|
||||
// Maybe this need to be replaced with assert, but as no tests cover this, an exception will be noticed earlier.
|
||||
throw new \RuntimeException('Token is not a PluginToken');
|
||||
}
|
||||
|
||||
if ('api' === $this->firewallName) {
|
||||
return $token->getResponse();
|
||||
}
|
||||
|
||||
$this->logger->info(sprintf('User "%s" has been authenticated successfully', $token->getUserIdentifier()));
|
||||
|
||||
$session = $request->getSession();
|
||||
$session->remove(SecurityRequestAttributes::AUTHENTICATION_ERROR);
|
||||
|
||||
$loginEvent = new InteractiveLoginEvent($request, $token);
|
||||
$this->dispatcher->dispatch($loginEvent, SecurityEvents::INTERACTIVE_LOGIN);
|
||||
|
||||
$response = null;
|
||||
if (null === $token->getResponse()) {
|
||||
$response = $this->authenticationHandler->onAuthenticationSuccess($request, $token);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||
{
|
||||
$this->logger->info(sprintf('Authentication request failed: %s', $exception->getMessage()));
|
||||
|
||||
// Gets app/bundles/UserBundle/Security/Firewall/AuthenticationListener.php:74 and till the end of the method referenced.
|
||||
if (in_array($this->firewallName, ['api', 'v2api'])) {
|
||||
// Continue with another authentication.
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->authenticationHandler->onAuthenticationFailure($request, $exception);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Security\Authenticator;
|
||||
|
||||
use Mautic\PluginBundle\Helper\IntegrationHelper;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Mautic\UserBundle\Event\AuthenticationEvent;
|
||||
use Mautic\UserBundle\Security\Authentication\Token\PluginToken;
|
||||
use Mautic\UserBundle\UserEvents;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
|
||||
use Symfony\Component\Security\Http\Authenticator\InteractiveAuthenticatorInterface;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\CsrfTokenBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
|
||||
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||
use Symfony\Component\Security\Http\HttpUtils;
|
||||
use Symfony\Component\Security\Http\SecurityRequestAttributes;
|
||||
|
||||
/**
|
||||
* This is a modified copy of the \Symfony\Component\Security\Http\Authenticator\FormLoginAuthenticator
|
||||
* Replaces \Mautic\UserBundle\Security\Authenticator\FormAuthenticator.
|
||||
*/
|
||||
final class SsoAuthenticator extends AbstractAuthenticator implements InteractiveAuthenticatorInterface
|
||||
{
|
||||
/**
|
||||
* @var array<mixed>
|
||||
*/
|
||||
private array $options;
|
||||
|
||||
/**
|
||||
* @param array<mixed> $options
|
||||
*/
|
||||
public function __construct(array $options, private HttpUtils $httpUtils, private UserProviderInterface $userProvider, private AuthenticationSuccessHandlerInterface $successHandler, private AuthenticationFailureHandlerInterface $failureHandler, private IntegrationHelper $integrationHelper, private EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
if ([] === $options) {
|
||||
throw new \RuntimeException('$options parameter is empty. Did you forgot to configure?');
|
||||
}
|
||||
|
||||
$this->options = array_merge([
|
||||
'username_parameter' => '_username',
|
||||
'password_parameter' => '_password',
|
||||
'integration_parameter' => 'integration',
|
||||
'post_only' => true,
|
||||
'enable_csrf' => true,
|
||||
'csrf_parameter' => '_csrf_token',
|
||||
'csrf_token_id' => 'authenticate',
|
||||
], $options);
|
||||
}
|
||||
|
||||
public function supports(Request $request): bool
|
||||
{
|
||||
if (true === $this->options['post_only'] && !$request->isMethod(Request::METHOD_POST)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$this->httpUtils->checkRequestPath($request, $this->options['check_path'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (true === $this->options['form_only'] && 'form' !== $request->getContentTypeFormat()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (true === $this->options['post_only']) {
|
||||
return $request->request->has($this->options['integration_parameter']);
|
||||
}
|
||||
|
||||
return $request->query->has($this->options['integration_parameter'])
|
||||
|| $request->request->has($this->options['integration_parameter']);
|
||||
}
|
||||
|
||||
public function authenticate(Request $request): Passport
|
||||
{
|
||||
$credentials = $this->getCredentials($request);
|
||||
$authenticatingService = $credentials['integration'] ?? null;
|
||||
|
||||
$passport = new Passport(
|
||||
new UserBadge($credentials['username'], function (string $userIdentifier) use ($request, $authenticatingService, $credentials): ?User {
|
||||
/** @var User|null $user */
|
||||
$user = null;
|
||||
|
||||
try {
|
||||
$user = $this->userProvider->loadUserByIdentifier($userIdentifier);
|
||||
} catch (UserNotFoundException) {
|
||||
// Do nothing. Will try to authenticate by username.
|
||||
}
|
||||
|
||||
// Try authenticating with a plugin
|
||||
$integrations = $this->integrationHelper->getIntegrationObjects($authenticatingService, ['sso_form'], false, null, true);
|
||||
|
||||
$token = new PluginToken(
|
||||
null,
|
||||
$authenticatingService,
|
||||
$userIdentifier,
|
||||
null !== $user ? ($credentials['password'] ?? null) : '',
|
||||
null !== $user ? $user->getRoles() : [],
|
||||
);
|
||||
|
||||
$authEvent = new AuthenticationEvent(
|
||||
$user ?? $userIdentifier,
|
||||
$token,
|
||||
$this->userProvider,
|
||||
$request,
|
||||
false,
|
||||
$authenticatingService,
|
||||
$integrations
|
||||
);
|
||||
|
||||
if ($this->dispatcher->hasListeners(UserEvents::USER_FORM_AUTHENTICATION)) {
|
||||
$authEvent = $this->dispatcher->dispatch($authEvent, UserEvents::USER_FORM_AUTHENTICATION);
|
||||
}
|
||||
|
||||
if ($authEvent->isAuthenticated()) {
|
||||
$user = $authEvent->getUser();
|
||||
|
||||
// This line is most likely will never happen. Keep it until this is thoroughly tested manually.
|
||||
if (null !== $user && !$user instanceof User) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
if ($authEvent->isFailed()) {
|
||||
throw new AuthenticationException($authEvent->getFailedAuthenticationMessage());
|
||||
}
|
||||
|
||||
if (!$user instanceof User) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $user;
|
||||
}),
|
||||
new PasswordCredentials($credentials['password'] ?? null),
|
||||
[new RememberMeBadge()]
|
||||
);
|
||||
|
||||
if ($this->options['enable_csrf']) {
|
||||
$passport->addBadge(new CsrfTokenBadge($this->options['csrf_token_id'], $credentials['csrf_token']));
|
||||
}
|
||||
|
||||
return $passport;
|
||||
}
|
||||
|
||||
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||
{
|
||||
return $this->successHandler->onAuthenticationSuccess($request, $token);
|
||||
}
|
||||
|
||||
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response
|
||||
{
|
||||
return $this->failureHandler->onAuthenticationFailure($request, $exception);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function getCredentials(Request $request): array
|
||||
{
|
||||
$credentials = [];
|
||||
$credentials['csrf_token'] = $request->get($this->options['csrf_parameter']);
|
||||
|
||||
if ($this->options['post_only']) {
|
||||
$credentials['username'] = $request->request->get($this->options['username_parameter']);
|
||||
$credentials['password'] = $request->request->get($this->options['password_parameter']) ?? '';
|
||||
$credentials['integration'] = $request->request->get($this->options['integration_parameter']);
|
||||
} else {
|
||||
$credentials['username'] = $request->get($this->options['username_parameter']);
|
||||
$credentials['password'] = $request->get($this->options['password_parameter']) ?? '';
|
||||
$credentials['integration'] = $request->get($this->options['integration_parameter']);
|
||||
}
|
||||
|
||||
if (!\is_string($credentials['username'])) {
|
||||
throw new BadRequestHttpException(sprintf('The key "%s" must be a string, "%s" given.', $this->options['username_parameter'], \gettype($credentials['username'])));
|
||||
}
|
||||
|
||||
$credentials['username'] = trim($credentials['username']);
|
||||
|
||||
if (\strlen($credentials['username']) > UserBadge::MAX_USERNAME_LENGTH) {
|
||||
throw new BadCredentialsException('Invalid username.');
|
||||
}
|
||||
|
||||
if (null !== $credentials['integration'] && !\is_string($credentials['integration'])) {
|
||||
throw new BadRequestHttpException(sprintf('The key "%s" must be a string or null, "%s" given.', $this->options['integration_parameter'], \gettype($credentials['integration'])));
|
||||
}
|
||||
|
||||
$request->getSession()->set(SecurityRequestAttributes::LAST_USERNAME, $credentials['username']);
|
||||
|
||||
if (!\is_string($credentials['password'])) {
|
||||
throw new BadRequestHttpException(sprintf('The key "%s" must be a string, "%s" given.', $this->options['password_parameter'], \gettype($credentials['password'])));
|
||||
}
|
||||
|
||||
return $credentials;
|
||||
}
|
||||
|
||||
public function isInteractive(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Security;
|
||||
|
||||
use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
|
||||
|
||||
final class DummyToken extends AbstractToken
|
||||
{
|
||||
public function getCredentials(): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\UserBundle\Security\EntryPoint;
|
||||
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
|
||||
|
||||
class MainEntryPoint implements AuthenticationEntryPointInterface
|
||||
{
|
||||
public function __construct(private UrlGeneratorInterface $urlGenerator, private bool $samlEnabled)
|
||||
{
|
||||
}
|
||||
|
||||
public function start(Request $request, ?AuthenticationException $authException = null): Response
|
||||
{
|
||||
// as per https://docs.mautic.org/en/5.x/authentication/authentication.html#logging-in
|
||||
// log in always as SAML for all requests.
|
||||
// todo: task for testers: enable saml, and check if regular login page is available
|
||||
$route = (string) $request->attributes->get('_route');
|
||||
if ($this->samlEnabled && 'login' !== $route && 'mautic_user_logincheck' !== $route) {
|
||||
// As the system doesn't know the IDP of the service, we can spare one redirect,
|
||||
// and redirect the user straight to discovery.
|
||||
return new RedirectResponse($this->urlGenerator->generate('lightsaml_sp.discovery'));
|
||||
}
|
||||
|
||||
return new RedirectResponse($this->urlGenerator->generate('login'));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\UserBundle\Security\Permissions;
|
||||
|
||||
use Mautic\CoreBundle\Security\Permissions\AbstractPermissions;
|
||||
use Mautic\UserBundle\Form\Type\PermissionListType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
|
||||
class UserPermissions extends AbstractPermissions
|
||||
{
|
||||
public function __construct($params)
|
||||
{
|
||||
parent::__construct($params);
|
||||
$this->permissions = [
|
||||
'profile' => [
|
||||
'editusername' => 1,
|
||||
'editemail' => 2,
|
||||
'editposition' => 4,
|
||||
'editname' => 8,
|
||||
'full' => 1024,
|
||||
],
|
||||
];
|
||||
$this->addStandardPermissions('users', false);
|
||||
$this->addStandardPermissions('roles', false);
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return 'user';
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface &$builder, array $options, array $data): void
|
||||
{
|
||||
$this->addStandardFormFields('user', 'users', $builder, $data, false);
|
||||
$this->addStandardFormFields('user', 'roles', $builder, $data, false);
|
||||
|
||||
$builder->add(
|
||||
'user:profile',
|
||||
PermissionListType::class,
|
||||
[
|
||||
'choices' => [
|
||||
'mautic.user.account.permissions.editname' => 'editname',
|
||||
'mautic.user.account.permissions.editusername' => 'editusername',
|
||||
'mautic.user.account.permissions.editemail' => 'editemail',
|
||||
'mautic.user.account.permissions.editposition' => 'editposition',
|
||||
'mautic.user.account.permissions.editall' => 'full',
|
||||
],
|
||||
'label' => 'mautic.user.permissions.profile',
|
||||
'data' => (!empty($data['profile']) ? $data['profile'] : []),
|
||||
'bundle' => 'user',
|
||||
'level' => 'profile',
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user