Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
//ConfigBundle
|
||||
|
||||
Mautic.removeConfigValue = function(action, el) {
|
||||
Mautic.executeAction(action, function(response) {
|
||||
if (response.success) {
|
||||
mQuery(el).parent().addClass('hide');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns string|false
|
||||
*/
|
||||
Mautic.parseQuery = function (query) {
|
||||
var vars = query.split('&');
|
||||
var queryString = {};
|
||||
for (var i = 0; i < vars.length; i++) {
|
||||
var pair = vars[i].split('=');
|
||||
var key = decodeURIComponent(pair[0]);
|
||||
var value = decodeURIComponent(pair[1]);
|
||||
// If first entry with this name
|
||||
if (typeof queryString[key] === 'undefined') {
|
||||
queryString[key] = decodeURIComponent(value);
|
||||
// If second entry with this name
|
||||
} else if (typeof queryString[key] === 'string') {
|
||||
var arr = [queryString[key], decodeURIComponent(value)];
|
||||
queryString[key] = arr;
|
||||
// If third or later entry with this name
|
||||
} else {
|
||||
queryString[key].push(decodeURIComponent(value));
|
||||
}
|
||||
}
|
||||
return queryString;
|
||||
}
|
||||
|
||||
Mautic.parseUrlHashParameter = function(url) {
|
||||
var url = url.split('#');
|
||||
if ('undefined' != typeof url[1]) {
|
||||
return url[1];
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Mautic.observeConfigTabs = function() {
|
||||
|
||||
if (!mQuery('#config_coreconfig_last_shown_tab').length) {
|
||||
return;
|
||||
}
|
||||
|
||||
var parameters = Mautic.parseQuery(window.location.search.substr(1));
|
||||
if ('undefined' != typeof parameters['tab']) {
|
||||
mQuery('#config_coreconfig_last_shown_tab').val(parameters['tab']);
|
||||
mQuery('a[data-toggle="tab"]').each(function (i, tab) {
|
||||
if (mQuery(tab).attr('href') == ('#' + parameters['tab'])) {
|
||||
mQuery(tab).tab('show');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mQuery('a[data-toggle="tab"]').on('show.bs.tab', function (e) {
|
||||
var tab = Mautic.parseUrlHashParameter(e.target.href);
|
||||
if (tab) {
|
||||
mQuery('#config_coreconfig_last_shown_tab').val(tab);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Mautic.resetEmailsToNotification = function(obj) {
|
||||
const send_to_owner = obj.value;
|
||||
if (parseInt(send_to_owner, 10) === 1)
|
||||
{
|
||||
mQuery(obj).closest('.panel-body').find('.notification_email_addresses').val('');
|
||||
}
|
||||
};
|
||||
|
||||
Mautic.configDsnTestExecute = function(element, action, key) {
|
||||
const $button = mQuery(element),
|
||||
$container = $button.closest('.config-dsn-container');
|
||||
|
||||
$container.find('.ri-loader-3-line').removeClass('hide');
|
||||
|
||||
Mautic.ajaxActionRequest(action, {key: key}, function(response) {
|
||||
const theClass = (response.success) ? 'has-success' : 'has-error',
|
||||
theMessage = response.message;
|
||||
$container.find('.config-dsn-test-container').removeClass('has-success has-error').addClass(theClass);
|
||||
$container.find('.help-block .status-msg').html(theMessage);
|
||||
$container.find('.ri-loader-3-line').addClass('hide');
|
||||
});
|
||||
};
|
||||
|
||||
Mautic.configDsnTestDisable = function(element) {
|
||||
const $container = mQuery(element).closest('.config-dsn-container');
|
||||
|
||||
$container.find('.help-block .status-msg').html('');
|
||||
$container.find('.help-block .save-config-msg').removeClass('hide');
|
||||
$container.find('.config-dsn-test-button').prop('disabled', true).addClass('disabled');
|
||||
};
|
||||
|
||||
|
||||
Mautic.showAnonymizeWarningMessage = function(anonymize_ip) {
|
||||
if (mQuery(anonymize_ip).siblings('.toggle__label').attr('aria-checked') === 'true') {
|
||||
mQuery('.anonymize_ip_address').addClass('hide');
|
||||
} else {
|
||||
mQuery('.anonymize_ip_address').removeClass('hide');
|
||||
}
|
||||
};
|
||||
|
||||
mQuery(Mautic.observeConfigTabs);
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'routes' => [
|
||||
'main' => [
|
||||
'mautic_config_action' => [
|
||||
'path' => '/config/{objectAction}/{objectId}',
|
||||
'controller' => 'Mautic\ConfigBundle\Controller\ConfigController::executeAction',
|
||||
],
|
||||
'mautic_sysinfo_index' => [
|
||||
'path' => '/sysinfo',
|
||||
'controller' => 'Mautic\ConfigBundle\Controller\SysinfoController::indexAction',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'menu' => [
|
||||
'admin' => [
|
||||
'mautic.config.menu.index' => [
|
||||
'route' => 'mautic_config_action',
|
||||
'routeParameters' => ['objectAction' => 'edit'],
|
||||
'iconClass' => 'ri-settings-5-line',
|
||||
'id' => 'mautic_config_index',
|
||||
'parent' => 'mautic.core.general',
|
||||
'access' => 'admin',
|
||||
'priority' => 16,
|
||||
],
|
||||
'mautic.sysinfo.menu.index' => [
|
||||
'route' => 'mautic_sysinfo_index',
|
||||
'iconClass' => 'ri-information-2-line',
|
||||
'id' => 'mautic_sysinfo_index',
|
||||
'parent' => 'mautic.core.general',
|
||||
'access' => 'admin',
|
||||
'priority' => 04,
|
||||
'checks' => [
|
||||
'parameters' => [
|
||||
'sysinfo_disabled' => false,
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
'parameters' => [
|
||||
'config_allowed_parameters' => [
|
||||
'kernel.project_dir',
|
||||
'kernel.logs_dir',
|
||||
],
|
||||
],
|
||||
];
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Mautic\CoreBundle\DependencyInjection\MauticCoreExtension;
|
||||
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
|
||||
|
||||
return function (ContainerConfigurator $configurator): void {
|
||||
$services = $configurator->services()
|
||||
->defaults()
|
||||
->autowire()
|
||||
->autoconfigure()
|
||||
->public();
|
||||
|
||||
$excludes = [
|
||||
'Form/DataTransformer/DsnTransformer.php',
|
||||
];
|
||||
|
||||
$services->load('Mautic\\ConfigBundle\\', '../')
|
||||
->exclude('../{'.implode(',', array_merge(MauticCoreExtension::DEFAULT_EXCLUDES, $excludes)).'}');
|
||||
|
||||
$services->get(Mautic\ConfigBundle\Form\Type\EscapeTransformer::class)->arg('$allowedParameters', '%mautic.config_allowed_parameters%');
|
||||
$services->get(Mautic\ConfigBundle\Form\Helper\RestrictionHelper::class)->arg('$restrictedFields', '%mautic.security.restrictedConfigFields%');
|
||||
$services->get(Mautic\ConfigBundle\Form\Helper\RestrictionHelper::class)->arg('$displayMode', '%mautic.security.restrictedConfigFields.displayMode%');
|
||||
|
||||
// @deprecated Remove all aliases in Mautic 6. Use FQCN instead.
|
||||
$services->alias('mautic.config.model.sysinfo', Mautic\ConfigBundle\Model\SysinfoModel::class);
|
||||
$services->alias('mautic.config.mapper', Mautic\ConfigBundle\Mapper\ConfigMapper::class);
|
||||
$services->alias('mautic.config.config_change_logger', Mautic\ConfigBundle\Service\ConfigChangeLogger::class);
|
||||
$services->alias('mautic.config.form.escape_transformer', Mautic\ConfigBundle\Form\Type\EscapeTransformer::class);
|
||||
$services->alias('mautic.config.form.restriction_helper', Mautic\ConfigBundle\Form\Helper\RestrictionHelper::class);
|
||||
};
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ConfigBundle;
|
||||
|
||||
/**
|
||||
* Events available for ConfigBundle.
|
||||
*/
|
||||
final class ConfigEvents
|
||||
{
|
||||
/**
|
||||
* The mautic.config_on_generate event is thrown when the configuration form is generated.
|
||||
*
|
||||
* The event listener receives a
|
||||
* Mautic\ConfigBundle\Event\ConfigGenerateEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const CONFIG_ON_GENERATE = 'mautic.config_on_generate';
|
||||
|
||||
/**
|
||||
* The mautic.config_pre_save event is thrown right before config data are saved.
|
||||
*
|
||||
* The event listener receives a Mautic\ConfigBundle\Event\ConfigEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const CONFIG_PRE_SAVE = 'mautic.config_pre_save';
|
||||
|
||||
/**
|
||||
* The mautic.config_post_save event is thrown right after config data are saved.
|
||||
*
|
||||
* The event listener receives a Mautic\ConfigBundle\Event\ConfigEvent instance.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const CONFIG_POST_SAVE = 'mautic.config_post_save';
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ConfigBundle\Controller;
|
||||
|
||||
use Mautic\ConfigBundle\ConfigEvents;
|
||||
use Mautic\ConfigBundle\Event\ConfigBuilderEvent;
|
||||
use Mautic\ConfigBundle\Event\ConfigEvent;
|
||||
use Mautic\ConfigBundle\Form\Type\ConfigType;
|
||||
use Mautic\ConfigBundle\Mapper\ConfigMapper;
|
||||
use Mautic\CoreBundle\Configurator\Configurator;
|
||||
use Mautic\CoreBundle\Controller\FormController;
|
||||
use Mautic\CoreBundle\Helper\BundleHelper;
|
||||
use Mautic\CoreBundle\Helper\CacheHelper;
|
||||
use Mautic\CoreBundle\Helper\EncryptionHelper;
|
||||
use Mautic\CoreBundle\Helper\PathsHelper;
|
||||
use Mautic\UserBundle\Entity\User;
|
||||
use Symfony\Component\Form\FormError;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||
|
||||
class ConfigController extends FormController
|
||||
{
|
||||
/**
|
||||
* Controller action for editing the application configuration.
|
||||
*
|
||||
* @return JsonResponse|Response
|
||||
*/
|
||||
public function editAction(Request $request, BundleHelper $bundleHelper, Configurator $configurator, CacheHelper $cacheHelper, PathsHelper $pathsHelper, ConfigMapper $configMapper, TokenStorageInterface $tokenStorage)
|
||||
{
|
||||
// admin only allowed
|
||||
if (!$this->user->isAdmin()) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$event = new ConfigBuilderEvent($bundleHelper);
|
||||
$dispatcher = $this->dispatcher;
|
||||
$dispatcher->dispatch($event, ConfigEvents::CONFIG_ON_GENERATE);
|
||||
$fileFields = $event->getFileFields();
|
||||
$formThemes = $event->getFormThemes();
|
||||
|
||||
$formConfigs = $configMapper->bindFormConfigsWithRealValues($event->getForms());
|
||||
|
||||
$this->mergeParamsWithLocal($formConfigs, $pathsHelper);
|
||||
|
||||
// Create the form
|
||||
$action = $this->generateUrl('mautic_config_action', ['objectAction' => 'edit']);
|
||||
$form = $this->formFactory->create(
|
||||
ConfigType::class,
|
||||
$formConfigs,
|
||||
[
|
||||
'action' => $action,
|
||||
'fileFields' => $fileFields,
|
||||
]
|
||||
);
|
||||
|
||||
$originalNormData = $form->getNormData();
|
||||
|
||||
$isWritable = $configurator->isFileWritable();
|
||||
$openTab = null;
|
||||
|
||||
// Check for a submitted form and process it
|
||||
if ('POST' == $request->getMethod()) {
|
||||
if (!$cancelled = $this->isFormCancelled($form)) {
|
||||
$isValid = false;
|
||||
if ($isWritable && $isValid = $this->isFormValid($form)) {
|
||||
// Bind request to the form
|
||||
$post = $request->request;
|
||||
|
||||
/** @var mixed[] $formData */
|
||||
$formData = $form->getData();
|
||||
|
||||
// Dispatch pre-save event. Bundles may need to modify some field values like passwords before save
|
||||
$configEvent = new ConfigEvent($formData, $post);
|
||||
$configEvent
|
||||
->setOriginalNormData($originalNormData)
|
||||
->setNormData($form->getNormData());
|
||||
$dispatcher->dispatch($configEvent, ConfigEvents::CONFIG_PRE_SAVE);
|
||||
$formValues = $configEvent->getConfig();
|
||||
|
||||
$errors = $configEvent->getErrors();
|
||||
$fieldErrors = $configEvent->getFieldErrors();
|
||||
|
||||
if ($errors || $fieldErrors) {
|
||||
foreach ($errors as $message => $messageVars) {
|
||||
$form->addError(
|
||||
new FormError($this->translator->trans($message, $messageVars, 'validators'))
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($fieldErrors as $key => $fields) {
|
||||
foreach ($fields as $field => $fieldError) {
|
||||
$form[$key][$field]->addError(
|
||||
new FormError($this->translator->trans($fieldError[0], $fieldError[1], 'validators'))
|
||||
);
|
||||
}
|
||||
}
|
||||
$isValid = false;
|
||||
} else {
|
||||
// Prevent these from getting overwritten with empty values
|
||||
$unsetIfEmpty = $configEvent->getPreservedFields();
|
||||
$unsetIfEmpty = array_merge($unsetIfEmpty, $fileFields);
|
||||
|
||||
// Merge each bundle's updated configuration into the local configuration
|
||||
foreach ($formValues as $object) {
|
||||
$checkThese = array_intersect(array_keys($object), $unsetIfEmpty);
|
||||
foreach ($checkThese as $checkMe) {
|
||||
if (empty($object[$checkMe])) {
|
||||
unset($object[$checkMe]);
|
||||
}
|
||||
}
|
||||
|
||||
$configurator->mergeParameters($object);
|
||||
}
|
||||
|
||||
try {
|
||||
// Ensure the config has a secret key
|
||||
$params = $configurator->getParameters();
|
||||
if (empty($params['secret_key'])) {
|
||||
$configurator->mergeParameters(['secret_key' => EncryptionHelper::generateKey()]);
|
||||
}
|
||||
|
||||
$configurator->write();
|
||||
$dispatcher->dispatch($configEvent, ConfigEvents::CONFIG_POST_SAVE);
|
||||
|
||||
$this->addFlashMessage('mautic.config.config.notice.updated');
|
||||
|
||||
$cacheHelper->refreshConfig();
|
||||
|
||||
if (!empty($formData['coreconfig']['last_shown_tab'])) {
|
||||
$openTab = $formData['coreconfig']['last_shown_tab'];
|
||||
}
|
||||
} catch (\RuntimeException $exception) {
|
||||
$this->addFlashMessage('mautic.config.config.error.not.updated', ['%exception%' => $exception->getMessage()], 'error');
|
||||
}
|
||||
|
||||
$this->setLocale($request, $tokenStorage, $params);
|
||||
}
|
||||
} elseif (!$isWritable) {
|
||||
$form->addError(
|
||||
new FormError(
|
||||
$this->translator->trans('mautic.config.notwritable')
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// If the form is saved or cancelled, redirect back to the dashboard
|
||||
if ($cancelled || $isValid) {
|
||||
if (!$cancelled && $this->isFormApplied($form)) {
|
||||
$redirectParameters = ['objectAction' => 'edit'];
|
||||
if ($openTab) {
|
||||
$redirectParameters['tab'] = $openTab;
|
||||
}
|
||||
|
||||
return $this->delegateRedirect($this->generateUrl('mautic_config_action', $redirectParameters));
|
||||
} else {
|
||||
return $this->delegateRedirect($this->generateUrl('mautic_dashboard_index'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$tmpl = $request->isXmlHttpRequest() ? $request->get('tmpl', 'index') : 'index';
|
||||
|
||||
return $this->delegateView(
|
||||
[
|
||||
'viewParameters' => [
|
||||
'tmpl' => $tmpl,
|
||||
'security' => $this->security,
|
||||
'form' => $form->createView(),
|
||||
'formThemes' => $formThemes,
|
||||
'formConfigs' => $formConfigs,
|
||||
'isWritable' => $isWritable,
|
||||
],
|
||||
'contentTemplate' => '@MauticConfig/Config/form.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_config_index',
|
||||
'mauticContent' => 'config',
|
||||
'route' => $this->generateUrl('mautic_config_action', ['objectAction' => 'edit']),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
*/
|
||||
public function downloadAction(Request $request, BundleHelper $bundleHelper, $objectId)
|
||||
{
|
||||
// admin only allowed
|
||||
if (!$this->user->isAdmin()) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$event = new ConfigBuilderEvent($bundleHelper);
|
||||
$dispatcher = $this->dispatcher;
|
||||
$dispatcher->dispatch($event, ConfigEvents::CONFIG_ON_GENERATE);
|
||||
|
||||
// Extract and base64 encode file contents
|
||||
$fileFields = $event->getFileFields();
|
||||
|
||||
if (!in_array($objectId, $fileFields)) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$content = $this->coreParametersHelper->get($objectId);
|
||||
$filename = $request->get('filename', $objectId);
|
||||
|
||||
if ($decoded = base64_decode($content)) {
|
||||
$response = new Response($decoded);
|
||||
$response->headers->set('Content-Type', 'application/force-download');
|
||||
$response->headers->set('Content-Type', 'application/octet-stream');
|
||||
$response->headers->set('Content-Disposition', 'attachment; filename="'.$filename);
|
||||
$response->headers->set('Expires', '0');
|
||||
$response->headers->set('Cache-Control', 'must-revalidate');
|
||||
$response->headers->set('Pragma', 'public');
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
return $this->notFound();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array|JsonResponse|\Symfony\Component\HttpFoundation\RedirectResponse|Response
|
||||
*/
|
||||
public function removeAction(BundleHelper $bundleHelper, Configurator $configurator, CacheHelper $cacheHelper, $objectId)
|
||||
{
|
||||
// admin only allowed
|
||||
if (!$this->user->isAdmin()) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
$success = 0;
|
||||
$event = new ConfigBuilderEvent($bundleHelper);
|
||||
$dispatcher = $this->dispatcher;
|
||||
$dispatcher->dispatch($event, ConfigEvents::CONFIG_ON_GENERATE);
|
||||
|
||||
// Extract and base64 encode file contents
|
||||
$fileFields = $event->getFileFields();
|
||||
|
||||
if (in_array($objectId, $fileFields)) {
|
||||
$configurator->mergeParameters([$objectId => null]);
|
||||
try {
|
||||
$configurator->write();
|
||||
|
||||
$cacheHelper->refreshConfig();
|
||||
$success = 1;
|
||||
} catch (\Exception) {
|
||||
}
|
||||
}
|
||||
|
||||
return new JsonResponse(['success' => $success]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges default parameters from each subscribed bundle with the local (real) params.
|
||||
*/
|
||||
private function mergeParamsWithLocal(array &$forms, PathsHelper $pathsHelper): void
|
||||
{
|
||||
$doNotChange = $this->coreParametersHelper->get('mautic.security.restrictedConfigFields');
|
||||
$localConfigFile = $pathsHelper->getLocalConfigurationFile();
|
||||
|
||||
// Import the current local configuration, $parameters is defined in this file
|
||||
|
||||
$parameters = [];
|
||||
include $localConfigFile;
|
||||
|
||||
/** @var mixed[] $parameters */
|
||||
$localParams = $parameters;
|
||||
|
||||
foreach ($forms as &$form) {
|
||||
// Merge the bundle params with the local params
|
||||
foreach ($form['parameters'] as $key => $value) {
|
||||
if (in_array($key, $doNotChange)) {
|
||||
unset($form['parameters'][$key]);
|
||||
} elseif (array_key_exists($key, $localParams)) {// @phpstan-ignore function.impossibleType (Not sure what this is about)
|
||||
$paramValue = $localParams[$key];
|
||||
$form['parameters'][$key] = $paramValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $params
|
||||
*/
|
||||
private function setLocale(Request $request, TokenStorageInterface $tokenStorage, array $params): void
|
||||
{
|
||||
$me = $tokenStorage->getToken()->getUser();
|
||||
assert($me instanceof User);
|
||||
$locale = $me->getLocale();
|
||||
|
||||
if (empty($locale)) {
|
||||
$locale = $params['locale'] ?? $this->coreParametersHelper->get('locale');
|
||||
}
|
||||
|
||||
$request->getSession()->set('_locale', $locale);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ConfigBundle\Controller;
|
||||
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Mautic\ConfigBundle\Model\SysinfoModel;
|
||||
use Mautic\CoreBundle\Controller\FormController;
|
||||
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\FormBundle\Helper\FormFieldHelper;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Form\FormFactoryInterface;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\RequestStack;
|
||||
|
||||
class SysinfoController extends FormController
|
||||
{
|
||||
public function __construct(
|
||||
FormFactoryInterface $formFactory,
|
||||
FormFieldHelper $fieldHelper,
|
||||
private SysinfoModel $sysinfoModel,
|
||||
ManagerRegistry $doctrine,
|
||||
ModelFactory $modelFactory,
|
||||
UserHelper $userHelper,
|
||||
CoreParametersHelper $coreParametersHelper,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
Translator $translator,
|
||||
FlashBag $flashBag,
|
||||
RequestStack $requestStack,
|
||||
CorePermissions $security,
|
||||
) {
|
||||
parent::__construct($formFactory, $fieldHelper, $doctrine, $modelFactory, $userHelper, $coreParametersHelper, $dispatcher, $translator, $flashBag, $requestStack, $security);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return JsonResponse|\Symfony\Component\HttpFoundation\Response
|
||||
*/
|
||||
public function indexAction()
|
||||
{
|
||||
if (!$this->user->isAdmin() || $this->coreParametersHelper->get('sysinfo_disabled')) {
|
||||
return $this->accessDenied();
|
||||
}
|
||||
|
||||
return $this->delegateView([
|
||||
'viewParameters' => [
|
||||
'phpInfo' => $this->sysinfoModel->getPhpInfo(),
|
||||
'requirements' => $this->sysinfoModel->getRequirements(),
|
||||
'recommendations' => $this->sysinfoModel->getRecommendations(),
|
||||
'folders' => $this->sysinfoModel->getFolders(),
|
||||
'log' => $this->sysinfoModel->getLogTail(200),
|
||||
'dbInfo' => $this->sysinfoModel->getDbInfo(),
|
||||
],
|
||||
'contentTemplate' => '@MauticConfig/Sysinfo/index.html.twig',
|
||||
'passthroughVars' => [
|
||||
'activeLink' => '#mautic_sysinfo_index',
|
||||
'mauticContent' => 'sysinfo',
|
||||
'route' => $this->generateUrl('mautic_sysinfo_index'),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\ConfigBundle\DependencyInjection;
|
||||
|
||||
use Symfony\Component\Config\FileLocator;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
use Symfony\Component\DependencyInjection\Extension\Extension;
|
||||
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
|
||||
|
||||
class MauticConfigExtension 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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ConfigBundle\Event;
|
||||
|
||||
use Mautic\CoreBundle\Helper\BundleHelper;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
class ConfigBuilderEvent extends Event
|
||||
{
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
private array $forms = [];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private array $formThemes = [
|
||||
'@MauticConfig/FormTheme/_config_file_row.html.twig',
|
||||
'@MauticConfig/FormTheme/dsn_row.html.twig',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
protected array $encodedFields = [];
|
||||
|
||||
public function __construct(
|
||||
private BundleHelper $bundleHelper,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set new form to the forms array.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addForm(array $form)
|
||||
{
|
||||
if (isset($form['formTheme'])) {
|
||||
$this->formThemes[] = $form['formTheme'];
|
||||
}
|
||||
|
||||
$this->forms[$form['formAlias']] = $form;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a form to the forms array.
|
||||
*
|
||||
* @param string $formAlias
|
||||
*/
|
||||
public function removeForm($formAlias): bool
|
||||
{
|
||||
if (isset($this->forms[$formAlias])) {
|
||||
unset($this->forms[$formAlias]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the forms array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getForms()
|
||||
{
|
||||
return $this->forms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the formThemes array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFormThemes()
|
||||
{
|
||||
return $this->formThemes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default parameters from config defined in bundles.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getParametersFromConfig($bundle)
|
||||
{
|
||||
static $allBundles;
|
||||
|
||||
if (empty($allBundles)) {
|
||||
$allBundles = $this->bundleHelper->getMauticBundles(true);
|
||||
}
|
||||
|
||||
if (isset($allBundles[$bundle]) && $allBundles[$bundle]['config']['parameters']) {
|
||||
return $allBundles[$bundle]['config']['parameters'];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return $this
|
||||
*/
|
||||
public function addFileFields($fields)
|
||||
{
|
||||
$this->encodedFields = array_merge($this->encodedFields, (array) $fields);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getFileFields()
|
||||
{
|
||||
return $this->encodedFields;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ConfigBundle\Event;
|
||||
|
||||
use Mautic\CoreBundle\Event\CommonEvent;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\ParameterBag;
|
||||
|
||||
class ConfigEvent extends CommonEvent
|
||||
{
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
private array $preserve = [];
|
||||
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
private array $errors = [];
|
||||
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
private array $fieldErrors = [];
|
||||
|
||||
/**
|
||||
* Data got from build form before update.
|
||||
*/
|
||||
private ?array $originalNormData = null;
|
||||
|
||||
/**
|
||||
* Data got from build form after update.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private $normData;
|
||||
|
||||
/**
|
||||
* @param mixed[]|null $config
|
||||
*/
|
||||
public function __construct(
|
||||
private ?array $config,
|
||||
private ParameterBag $post,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the config array.
|
||||
*
|
||||
* @param string $key
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getConfig($key = null)
|
||||
{
|
||||
if ($key) {
|
||||
return $this->config[$key] ?? [];
|
||||
}
|
||||
|
||||
return $this->config;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the config array.
|
||||
*
|
||||
* @param string $key
|
||||
*/
|
||||
public function setConfig(array $config, $key = null): void
|
||||
{
|
||||
if ($key) {
|
||||
$this->config[$key] = $config;
|
||||
} else {
|
||||
$this->config = $config;
|
||||
}
|
||||
}
|
||||
|
||||
public function getPost(): ParameterBag
|
||||
{
|
||||
return $this->post;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set fields such as passwords that will not overwrite existing values
|
||||
* if the current is empty.
|
||||
*
|
||||
* @param array|string $fields
|
||||
*/
|
||||
public function unsetIfEmpty($fields): void
|
||||
{
|
||||
if (!is_array($fields)) {
|
||||
$fields = [$fields];
|
||||
}
|
||||
|
||||
$this->preserve = array_merge($this->preserve, $fields);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return array of fields to unset if empty so that existing values are not
|
||||
* overwritten if empty.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getPreservedFields()
|
||||
{
|
||||
return $this->preserve;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set error message.
|
||||
*
|
||||
* @param string $message (untranslated)
|
||||
* @param array $messageVars for translation
|
||||
* @param string|null $key
|
||||
* @param string|null $field
|
||||
*
|
||||
* @return ConfigEvent
|
||||
*/
|
||||
public function setError($message, $messageVars = [], $key = null, $field = null)
|
||||
{
|
||||
if (!empty($key) && !empty($field)) {
|
||||
if (!isset($this->errors[$key])) {
|
||||
$this->fieldErrors[$key] = [];
|
||||
}
|
||||
|
||||
$this->fieldErrors[$key][$field] = [
|
||||
$message,
|
||||
$messageVars,
|
||||
];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->errors[$message] = $messageVars;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error messages.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getErrors()
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getFieldErrors()
|
||||
{
|
||||
return $this->fieldErrors;
|
||||
}
|
||||
|
||||
public function getFileContent(UploadedFile $file): string
|
||||
{
|
||||
$tmpFile = $file->getRealPath();
|
||||
$content = trim(file_get_contents($tmpFile));
|
||||
@unlink($tmpFile);
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
public function encodeFileContents($content): string
|
||||
{
|
||||
return base64_encode($content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getOriginalNormData()
|
||||
{
|
||||
return $this->originalNormData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ConfigEvent
|
||||
*/
|
||||
public function setOriginalNormData(array $normData)
|
||||
{
|
||||
$this->originalNormData = $normData;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
public function getNormData()
|
||||
{
|
||||
return $this->normData;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $normData
|
||||
*/
|
||||
public function setNormData($normData): void
|
||||
{
|
||||
$this->normData = $normData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ConfigBundle\EventListener;
|
||||
|
||||
use Mautic\ConfigBundle\ConfigEvents;
|
||||
use Mautic\ConfigBundle\Event\ConfigEvent;
|
||||
use Mautic\ConfigBundle\Service\ConfigChangeLogger;
|
||||
use Mautic\CoreBundle\Entity\AuditLogRepository;
|
||||
use Mautic\CoreBundle\Entity\IpAddressRepository;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class ConfigSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ConfigChangeLogger $configChangeLogger,
|
||||
private IpAddressRepository $ipAddressRepository,
|
||||
private CoreParametersHelper $coreParametersHelper,
|
||||
private AuditLogRepository $auditLogRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
ConfigEvents::CONFIG_POST_SAVE => ['onConfigPostSave', 0],
|
||||
];
|
||||
}
|
||||
|
||||
public function onConfigPostSave(ConfigEvent $event): void
|
||||
{
|
||||
if ($originalNormData = $event->getOriginalNormData()) {
|
||||
$normData = $event->getNormData();
|
||||
// We have something to log
|
||||
$this->configChangeLogger
|
||||
->setOriginalNormData($originalNormData)
|
||||
->log($normData);
|
||||
|
||||
if (!isset($originalNormData['trackingconfig']) && !isset($normData['trackingconfig'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$oldAnonymizeIp = $originalNormData['trackingconfig']['parameters']['anonymize_ip'];
|
||||
$newAnonymizeIp = $normData['trackingconfig']['anonymize_ip'];
|
||||
|
||||
if ($oldAnonymizeIp !== $newAnonymizeIp && $newAnonymizeIp && !$this->coreParametersHelper->get('anonymize_ip_address_in_background', false)) {
|
||||
$this->ipAddressRepository->anonymizeAllIpAddress();
|
||||
$this->auditLogRepository->anonymizeAllIpAddress();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ConfigBundle\Exception;
|
||||
|
||||
class BadFormConfigException extends \Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\ConfigBundle\Form\DataTransformer;
|
||||
|
||||
use Mautic\ConfigBundle\Form\Type\EscapeTransformer;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\Dsn\Dsn;
|
||||
use Symfony\Component\Form\DataTransformerInterface;
|
||||
|
||||
/**
|
||||
* @implements DataTransformerInterface<string, array>
|
||||
*/
|
||||
class DsnTransformer implements DataTransformerInterface
|
||||
{
|
||||
private const PASSWORD_MASK = '🔒';
|
||||
|
||||
public function __construct(
|
||||
private CoreParametersHelper $coreParametersHelper,
|
||||
private EscapeTransformer $escapeTransformer,
|
||||
private string $configKey,
|
||||
private bool $allowEmpty,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function transform($value): array
|
||||
{
|
||||
// unescape the DSN before the transformation to array
|
||||
$value = $this->escapeTransformer->transform((string) $value);
|
||||
|
||||
try {
|
||||
$dsn = Dsn::fromString($value);
|
||||
} catch (\InvalidArgumentException) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [
|
||||
'scheme' => $dsn->getScheme(),
|
||||
'host' => $dsn->getHost(),
|
||||
'user' => $dsn->getUser(),
|
||||
'password' => $dsn->getPassword() ? self::PASSWORD_MASK : null,
|
||||
'port' => $dsn->getPort(),
|
||||
'path' => $dsn->getPath(),
|
||||
'options' => $dsn->getOptions(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $value
|
||||
*/
|
||||
public function reverseTransform($value): string
|
||||
{
|
||||
if ($this->allowEmpty && !array_filter($value)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// unescape the values as they are escaped by the escape transformer applied to the child elements
|
||||
$value = $this->escapeTransformer->transform($value);
|
||||
|
||||
$dsn = new Dsn(
|
||||
(string) $value['scheme'],
|
||||
(string) $value['host'],
|
||||
$value['user'],
|
||||
$value['password'],
|
||||
$value['port'] ? (int) $value['port'] : null,
|
||||
$value['path'],
|
||||
$value['options'],
|
||||
);
|
||||
|
||||
if (self::PASSWORD_MASK === $dsn->getPassword()) {
|
||||
$previousDsn = Dsn::fromString($this->coreParametersHelper->get($this->configKey));
|
||||
$dsn = $dsn->setPassword($previousDsn->getPassword());
|
||||
}
|
||||
|
||||
// escape the DSN to prevent "missing parameter" errors
|
||||
return $this->escapeTransformer->reverseTransform((string) $dsn);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\ConfigBundle\Form\DataTransformer;
|
||||
|
||||
use Mautic\ConfigBundle\Form\Type\EscapeTransformer;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
|
||||
class DsnTransformerFactory
|
||||
{
|
||||
public function __construct(
|
||||
private CoreParametersHelper $coreParametersHelper,
|
||||
private EscapeTransformer $escapeTransformer,
|
||||
) {
|
||||
}
|
||||
|
||||
public function create(string $configKey, bool $allowEmpty): DsnTransformer
|
||||
{
|
||||
return new DsnTransformer($this->coreParametersHelper, $this->escapeTransformer, $configKey, $allowEmpty);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ConfigBundle\Form\Helper;
|
||||
|
||||
use Mautic\ConfigBundle\Mapper\Helper\RestrictionHelper as FieldHelper;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class RestrictionHelper
|
||||
{
|
||||
public const MODE_REMOVE = 'remove';
|
||||
|
||||
public const MODE_MASK = 'mask';
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private array $restrictedFields;
|
||||
|
||||
public function __construct(
|
||||
private TranslatorInterface $translator,
|
||||
array $restrictedFields,
|
||||
private string $displayMode,
|
||||
) {
|
||||
$this->restrictedFields = FieldHelper::prepareRestrictions($restrictedFields);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormInterface<mixed> $childType
|
||||
* @param FormInterface<mixed> $parentType
|
||||
*/
|
||||
public function applyRestrictions(FormInterface $childType, FormInterface $parentType, ?array $restrictedFields = null): void
|
||||
{
|
||||
if (null === $restrictedFields) {
|
||||
$restrictedFields = $this->restrictedFields;
|
||||
}
|
||||
|
||||
$fieldName = $childType->getName();
|
||||
if (array_key_exists($fieldName, $restrictedFields)) {
|
||||
if (is_array($restrictedFields[$fieldName])) {
|
||||
// Part of the collection of fields are restricted
|
||||
foreach ($childType as $grandchild) {
|
||||
$this->applyRestrictions($grandchild, $childType, $restrictedFields[$fieldName]);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$this->restrictField($childType, $parentType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormInterface<mixed> $childType
|
||||
* @param FormInterface<mixed> $parentType
|
||||
*/
|
||||
private function restrictField(FormInterface $childType, FormInterface $parentType): void
|
||||
{
|
||||
switch ($this->displayMode) {
|
||||
case self::MODE_MASK:
|
||||
$parentType->add(
|
||||
$childType->getName(),
|
||||
$childType->getConfig()->getType()->getInnerType()::class,
|
||||
array_merge(
|
||||
$childType->getConfig()->getOptions(),
|
||||
[
|
||||
'required' => false,
|
||||
'mapped' => false,
|
||||
'disabled' => true,
|
||||
'attr' => array_merge($childType->getConfig()->getOptions()['attr'] ?? [], [
|
||||
'placeholder' => $this->translator->trans('mautic.config.restricted'),
|
||||
'readonly' => true,
|
||||
]),
|
||||
]
|
||||
)
|
||||
);
|
||||
break;
|
||||
case self::MODE_REMOVE:
|
||||
$parentType->remove($childType->getName());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ConfigBundle\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\FileType;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<mixed>
|
||||
*/
|
||||
class ConfigFileType extends AbstractType
|
||||
{
|
||||
public function getParent(): ?string
|
||||
{
|
||||
return FileType::class;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ConfigBundle\Form\Type;
|
||||
|
||||
use Mautic\ConfigBundle\Form\Helper\RestrictionHelper;
|
||||
use Mautic\CoreBundle\Form\Type\FormButtonsType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<mixed>
|
||||
*/
|
||||
class ConfigType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
private RestrictionHelper $restrictionHelper,
|
||||
private EscapeTransformer $escapeTransformer,
|
||||
) {
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
// TODO very dirty quick fix for https://github.com/mautic/mautic/issues/8854
|
||||
if (isset($options['data']['apiconfig']['parameters']['api_oauth2_access_token_lifetime'])
|
||||
&& 3600 === $options['data']['apiconfig']['parameters']['api_oauth2_access_token_lifetime']
|
||||
) {
|
||||
$options['data']['apiconfig']['parameters']['api_oauth2_access_token_lifetime'] = 60;
|
||||
}
|
||||
|
||||
if (isset($options['data']['apiconfig']['parameters']['api_oauth2_refresh_token_lifetime'])
|
||||
&& 1_209_600 === $options['data']['apiconfig']['parameters']['api_oauth2_refresh_token_lifetime']
|
||||
) {
|
||||
$options['data']['apiconfig']['parameters']['api_oauth2_refresh_token_lifetime'] = 14;
|
||||
}
|
||||
|
||||
foreach ($options['data'] as $config) {
|
||||
if (isset($config['formAlias']) && !empty($config['parameters'])) {
|
||||
$checkThese = array_intersect(array_keys($config['parameters']), $options['fileFields']);
|
||||
foreach ($checkThese as $checkMe) {
|
||||
// Unset base64 encoded values
|
||||
unset($config['parameters'][$checkMe]);
|
||||
}
|
||||
$builder->add(
|
||||
$config['formAlias'],
|
||||
$config['formType'],
|
||||
[
|
||||
'data' => $config['parameters'],
|
||||
]
|
||||
);
|
||||
|
||||
$this->addTransformers($builder->get($config['formAlias']));
|
||||
}
|
||||
}
|
||||
|
||||
$builder->addEventListener(
|
||||
FormEvents::PRE_SET_DATA,
|
||||
function (FormEvent $event): void {
|
||||
$form = $event->getForm();
|
||||
|
||||
foreach ($form as $configForm) {
|
||||
foreach ($configForm as $child) {
|
||||
$this->restrictionHelper->applyRestrictions($child, $configForm);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'buttons',
|
||||
FormButtonsType::class,
|
||||
[
|
||||
'apply_onclick' => 'Mautic.activateBackdrop()',
|
||||
'save_onclick' => 'Mautic.activateBackdrop()',
|
||||
]
|
||||
);
|
||||
|
||||
if (!empty($options['action'])) {
|
||||
$builder->setAction($options['action']);
|
||||
}
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults(
|
||||
[
|
||||
'fileFields' => [],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
private function addTransformers(FormBuilderInterface $builder): void
|
||||
{
|
||||
if (0 === $builder->count()) {
|
||||
$builder->addModelTransformer($this->escapeTransformer);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($builder as $childBuilder) {
|
||||
$this->addTransformers($childBuilder);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\ConfigBundle\Form\Type;
|
||||
|
||||
use Mautic\ConfigBundle\Form\DataTransformer\DsnTransformerFactory;
|
||||
use Mautic\CoreBundle\Form\Type\SortableListType;
|
||||
use Mautic\CoreBundle\Form\Type\StandAloneButtonType;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\Dsn\Dsn;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
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\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* @extends AbstractType<array>
|
||||
*/
|
||||
class DsnType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
private DsnTransformerFactory $dsnTransformerFactory,
|
||||
private CoreParametersHelper $coreParametersHelper,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param FormBuilderInterface<array<mixed>|null> $builder
|
||||
* @param array<string, mixed> $options
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$name = $builder->getName();
|
||||
$onChange = 'Mautic.configDsnTestDisable(this)';
|
||||
$attr = [
|
||||
'class' => 'form-control',
|
||||
'onchange' => $onChange,
|
||||
];
|
||||
|
||||
$builder->add(
|
||||
'scheme',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.config.dsn.scheme',
|
||||
'required' => $options['required'],
|
||||
'attr' => $attr,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'host',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.config.dsn.host',
|
||||
'required' => false,
|
||||
'attr' => $attr,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'port',
|
||||
NumberType::class,
|
||||
[
|
||||
'label' => 'mautic.config.dsn.port',
|
||||
'required' => false,
|
||||
'html5' => true,
|
||||
'attr' => $attr,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'user',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.config.dsn.user',
|
||||
'required' => false,
|
||||
'attr' => $attr,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'password',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.config.dsn.password',
|
||||
'required' => false,
|
||||
'attr' => $attr,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'path',
|
||||
TextType::class,
|
||||
[
|
||||
'label' => 'mautic.config.dsn.path',
|
||||
'required' => false,
|
||||
'attr' => $attr,
|
||||
]
|
||||
);
|
||||
|
||||
$builder->add(
|
||||
'options',
|
||||
SortableListType::class,
|
||||
[
|
||||
'required' => false,
|
||||
'label' => 'mautic.config.dsn.options',
|
||||
'attr' => [
|
||||
'onchange' => $onChange,
|
||||
],
|
||||
'option_required' => false,
|
||||
'with_labels' => true,
|
||||
'key_value_pairs' => true,
|
||||
]
|
||||
);
|
||||
|
||||
if ($options['test_button']['action'] && $this->getCurrentDsn($name)) {
|
||||
$builder->add(
|
||||
'test_button',
|
||||
StandAloneButtonType::class,
|
||||
[
|
||||
'label' => $options['test_button']['label'],
|
||||
'required' => false,
|
||||
'attr' => [
|
||||
'class' => 'btn btn-tertiary btn-sm config-dsn-test-button',
|
||||
'onclick' => sprintf('Mautic.configDsnTestExecute(this, "%s", "%s")', $options['test_button']['action'], $name),
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$builder->addModelTransformer($this->dsnTransformerFactory->create($name, !$options['required']));
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'label' => false,
|
||||
'error_mapping' => [
|
||||
'.' => 'scheme',
|
||||
],
|
||||
'test_button' => [
|
||||
'action' => null,
|
||||
'label' => null,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @phpstan-ignore-next-line
|
||||
*/
|
||||
public function finishView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
$view->vars['currentDsn'] = $this->getCurrentDsn($form->getName());
|
||||
}
|
||||
|
||||
private function getCurrentDsn(string $name): ?Dsn
|
||||
{
|
||||
$dsn = (string) $this->coreParametersHelper->get($name);
|
||||
|
||||
try {
|
||||
$dsn = Dsn::fromString($dsn);
|
||||
} catch (\InvalidArgumentException) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($dsn->getPassword()) {
|
||||
$dsn = $dsn->setPassword('SECRET');
|
||||
}
|
||||
|
||||
return $dsn;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\ConfigBundle\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\DataTransformerInterface;
|
||||
|
||||
/**
|
||||
* @implements DataTransformerInterface<array<string|int|float|array<string|int|float>>|string|int|float, array<string|int|float|array<string|int|float>>|string|int|float>
|
||||
*/
|
||||
class EscapeTransformer implements DataTransformerInterface
|
||||
{
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private array $allowedParameters;
|
||||
|
||||
public function __construct(array $allowedParameters)
|
||||
{
|
||||
$this->allowedParameters = array_filter($allowedParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string|int|float|array<string|int|float>>|string|int|float $value
|
||||
*
|
||||
* @return array<string|int|float|array<string|int|float>>|string|int|float
|
||||
*/
|
||||
public function transform(mixed $value): mixed
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return array_map(fn ($value) => $this->unescape($value), $value);
|
||||
}
|
||||
|
||||
return $this->unescape($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string|int|float|array<string|int|float>>|string|int|float $value
|
||||
*
|
||||
* @return array<string|int|float|array<string|int|float>>|string|int|float
|
||||
*/
|
||||
public function reverseTransform(mixed $value): mixed
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return array_map(fn ($value) => $this->escape($value), $value);
|
||||
}
|
||||
|
||||
return $this->escape($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
private function unescape($value)
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
return str_replace('%%', '%', $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
private function escape($value)
|
||||
{
|
||||
if (!is_string($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$escaped = str_replace('%', '%%', $value);
|
||||
|
||||
return $this->allowParameters($escaped);
|
||||
}
|
||||
|
||||
private function allowParameters(string $escaped): string
|
||||
{
|
||||
if (!$this->allowedParameters) {
|
||||
return $escaped;
|
||||
}
|
||||
|
||||
$search = array_map(fn (string $value): string => "%%{$value}%%", $this->allowedParameters);
|
||||
$replace = array_map(fn (string $value): string => "%{$value}%", $this->allowedParameters);
|
||||
|
||||
return str_ireplace($search, $replace, $escaped);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ConfigBundle\Mapper;
|
||||
|
||||
use Mautic\ConfigBundle\Exception\BadFormConfigException;
|
||||
use Mautic\ConfigBundle\Mapper\Helper\ConfigHelper;
|
||||
use Mautic\ConfigBundle\Mapper\Helper\RestrictionHelper;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
|
||||
class ConfigMapper
|
||||
{
|
||||
/**
|
||||
* @var mixed[]
|
||||
*/
|
||||
private array $restrictedParameters;
|
||||
|
||||
public function __construct(
|
||||
private CoreParametersHelper $parametersHelper,
|
||||
array $restrictedParameters = [],
|
||||
) {
|
||||
$this->restrictedParameters = RestrictionHelper::prepareRestrictions($restrictedParameters);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws BadFormConfigException
|
||||
*/
|
||||
public function bindFormConfigsWithRealValues(array $forms): array
|
||||
{
|
||||
foreach ($forms as $bundle => $config) {
|
||||
if (!isset($config['parameters'])) {
|
||||
throw new BadFormConfigException();
|
||||
}
|
||||
|
||||
$forms[$bundle]['parameters'] = $this->mergeWithLocalParameters($forms[$bundle]['parameters']);
|
||||
}
|
||||
|
||||
return $forms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges default parameters from each subscribed bundle with the local (real) params.
|
||||
*/
|
||||
private function mergeWithLocalParameters(array $formParameters): array
|
||||
{
|
||||
$formParameters = RestrictionHelper::applyRestrictions($formParameters, $this->restrictedParameters);
|
||||
|
||||
// All config values are stored at root level of the config
|
||||
foreach ($formParameters as $formKey => $defaultValue) {
|
||||
$configValue = $this->parametersHelper->get($formKey);
|
||||
|
||||
if (null === $configValue) {
|
||||
// Nothing has been locally configured so keep default
|
||||
continue;
|
||||
}
|
||||
|
||||
// Form field is a collection of parameters
|
||||
if (is_array($configValue)) {
|
||||
// Apply nested restrictions to nested config values
|
||||
$configValue = RestrictionHelper::applyRestrictions($configValue, $this->restrictedParameters, $formKey);
|
||||
|
||||
// Bind configured values with defaults
|
||||
$formParameters[$formKey] = ConfigHelper::bindNestedConfigValues($configValue, $defaultValue);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Form field
|
||||
$formParameters[$formKey] = $configValue;
|
||||
}
|
||||
|
||||
return $formParameters;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ConfigBundle\Mapper\Helper;
|
||||
|
||||
class ConfigHelper
|
||||
{
|
||||
/**
|
||||
* Map local config values with form fields.
|
||||
*
|
||||
* @param mixed $defaults
|
||||
*/
|
||||
public static function bindNestedConfigValues(array $configValues, $defaults): array
|
||||
{
|
||||
if (!is_array($defaults)) {
|
||||
// Return all config values
|
||||
return $configValues;
|
||||
}
|
||||
|
||||
foreach ($defaults as $key => $defaultValue) {
|
||||
if (isset($configValues[$key]) && is_array($configValues[$key])) {
|
||||
$configValues[$key] = self::bindNestedConfigValues($configValues[$key], $defaultValue);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$configValues[$key] ??= $defaultValue;
|
||||
}
|
||||
|
||||
return $configValues;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ConfigBundle\Mapper\Helper;
|
||||
|
||||
class RestrictionHelper
|
||||
{
|
||||
/**
|
||||
* Ensure that the array has string indexes for congruency with a nested array similar to ['db_host', 'monitored_email' => ['EmailBundle_bounces'];.
|
||||
*/
|
||||
public static function prepareRestrictions(array $restrictedParameters): array
|
||||
{
|
||||
$prepared = [];
|
||||
foreach ($restrictedParameters as $key => $value) {
|
||||
$newKey = (is_numeric($key)) ? $value : $key;
|
||||
$prepared[$newKey] = (is_array($value)) ? self::prepareRestrictions($value) : $value;
|
||||
}
|
||||
|
||||
return $prepared;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove fields that are restricted.
|
||||
*/
|
||||
public static function applyRestrictions(array $configParameters, array $restrictedParameters, $restrictedParentKey = null): array
|
||||
{
|
||||
if ($restrictedParentKey) {
|
||||
if (!isset($restrictedParameters[$restrictedParentKey])) {
|
||||
// No restrictions
|
||||
return $configParameters;
|
||||
}
|
||||
|
||||
$restrictedParameters = $restrictedParameters[$restrictedParentKey];
|
||||
}
|
||||
|
||||
foreach ($configParameters as $key => $value) {
|
||||
// The entire form type is restricted
|
||||
if (isset($restrictedParameters[$key]) && !is_array($restrictedParameters[$key])) {
|
||||
unset($configParameters[$key]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// A sub type of the form type is restricted
|
||||
if (is_array($value)) {
|
||||
$configParameters[$key] = self::applyRestrictions($value, $restrictedParameters, $key);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise no restrictions are in place
|
||||
}
|
||||
|
||||
return $configParameters;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ConfigBundle;
|
||||
|
||||
use Symfony\Component\HttpKernel\Bundle\Bundle;
|
||||
|
||||
class MauticConfigBundle extends Bundle
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ConfigBundle\Model;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Helper\PathsHelper;
|
||||
use Mautic\CoreBundle\Loader\ParameterLoader;
|
||||
use Mautic\InstallBundle\Configurator\Step\CheckStep;
|
||||
use Mautic\InstallBundle\Install\InstallService;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
class SysinfoModel
|
||||
{
|
||||
/**
|
||||
* @var string|null
|
||||
*/
|
||||
protected $phpInfo;
|
||||
|
||||
/**
|
||||
* @var array<string,bool>|null
|
||||
*/
|
||||
protected $folders;
|
||||
|
||||
public function __construct(
|
||||
protected PathsHelper $pathsHelper,
|
||||
protected CoreParametersHelper $coreParametersHelper,
|
||||
private TranslatorInterface $translator,
|
||||
protected Connection $connection,
|
||||
private InstallService $installService,
|
||||
private CheckStep $checkStep,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get the PHP info.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPhpInfo()
|
||||
{
|
||||
if (!is_null($this->phpInfo)) {
|
||||
return $this->phpInfo;
|
||||
}
|
||||
|
||||
if (function_exists('phpinfo') && 'cli' !== php_sapi_name()) {
|
||||
ob_start();
|
||||
$currentTz = date_default_timezone_get();
|
||||
date_default_timezone_set('UTC');
|
||||
phpinfo(INFO_GENERAL | INFO_CONFIGURATION | INFO_MODULES);
|
||||
$phpInfo = ob_get_contents();
|
||||
ob_end_clean();
|
||||
preg_match_all('#<body[^>]*>(.*)</body>#siU', $phpInfo, $output);
|
||||
$output = preg_replace('#<table[^>]*>#', '<table class="table table-bordered">', $output[1][0]);
|
||||
$output = preg_replace('#(\w),(\w)#', '\1, \2', $output);
|
||||
$output = preg_replace('#<hr />#', '', $output);
|
||||
$output = str_replace('<div class="center">', '', $output);
|
||||
$output = preg_replace('#<tr class="h">(.*)<\/tr>#', '<thead><tr class="h">$1</tr></thead><tbody>', $output);
|
||||
$output = str_replace('</table>', '</tbody></table>', $output);
|
||||
$output = str_replace('</div>', '', $output);
|
||||
$this->phpInfo = $output;
|
||||
// ensure TZ is set back to default
|
||||
date_default_timezone_set($currentTz);
|
||||
} elseif (function_exists('phpversion')) {
|
||||
$this->phpInfo = $this->translator->trans('mautic.sysinfo.phpinfo.phpversion', ['%phpversion%' => phpversion()]);
|
||||
} else {
|
||||
$this->phpInfo = $this->translator->trans('mautic.sysinfo.phpinfo.missing');
|
||||
}
|
||||
|
||||
return $this->phpInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getRecommendations(): array
|
||||
{
|
||||
return $this->installService->checkOptionalSettings($this->checkStep);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getRequirements(): array
|
||||
{
|
||||
return $this->installService->checkRequirements($this->checkStep);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to get important folders with a writable flag.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getFolders()
|
||||
{
|
||||
if (!is_null($this->folders)) {
|
||||
return $this->folders;
|
||||
}
|
||||
|
||||
$importantFolders = [
|
||||
ParameterLoader::getLocalConfigFile($this->pathsHelper->getSystemPath('root').'/app'),
|
||||
$this->coreParametersHelper->get('cache_path'),
|
||||
$this->coreParametersHelper->get('log_path'),
|
||||
$this->coreParametersHelper->get('upload_dir'),
|
||||
$this->pathsHelper->getSystemPath('images', true),
|
||||
$this->pathsHelper->getSystemPath('translations', true),
|
||||
];
|
||||
|
||||
foreach ($importantFolders as $folder) {
|
||||
$folderPath = realpath($folder);
|
||||
$folderKey = $folderPath ?: $folder;
|
||||
$isWritable = $folderPath && is_writable($folderPath);
|
||||
|
||||
$this->folders[$folderKey] = $isWritable;
|
||||
}
|
||||
|
||||
return $this->folders;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to tail (a few last rows) of a file.
|
||||
*
|
||||
* @param int $lines
|
||||
*/
|
||||
public function getLogTail($lines = 10): ?string
|
||||
{
|
||||
$log = $this->coreParametersHelper->get('log_path').'/mautic_'.MAUTIC_ENV.'-'.date('Y-m-d').'.php';
|
||||
|
||||
if (!file_exists($log)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->tail($log, $lines);
|
||||
}
|
||||
|
||||
public function getDbInfo(): array
|
||||
{
|
||||
return [
|
||||
'version' => $this->connection->executeQuery('SELECT VERSION()')->fetchOne(),
|
||||
'driver' => $this->connection->getParams()['driver'],
|
||||
'platform' => $this->connection->getDatabasePlatform()::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to tail (a few last rows) of a file.
|
||||
*
|
||||
* @param int $lines
|
||||
* @param int $buffer
|
||||
*/
|
||||
public function tail($filename, $lines = 10, $buffer = 4096): string
|
||||
{
|
||||
$f = fopen($filename, 'rb');
|
||||
$output = '';
|
||||
|
||||
fseek($f, -1, SEEK_END);
|
||||
|
||||
if ("\n" != fread($f, 1)) {
|
||||
--$lines;
|
||||
}
|
||||
|
||||
while (ftell($f) > 0 && $lines >= 0) {
|
||||
$seek = min(ftell($f), $buffer);
|
||||
fseek($f, -$seek, SEEK_CUR);
|
||||
$output = ($chunk = fread($f, $seek)).$output;
|
||||
fseek($f, -mb_strlen($chunk, '8bit'), SEEK_CUR);
|
||||
$lines -= substr_count($chunk, "\n");
|
||||
}
|
||||
|
||||
while ($lines++ < 0) {
|
||||
$output = substr($output, strpos($output, "\n") + 1);
|
||||
}
|
||||
|
||||
fclose($f);
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
{#
|
||||
Variables
|
||||
- tmpl
|
||||
- security
|
||||
- form
|
||||
- formThemes
|
||||
- formConfigs
|
||||
- isWritable
|
||||
#}
|
||||
{% if formThemes is not empty and formThemes is iterable %}
|
||||
{% form_theme form with formThemes %}
|
||||
{% elseif formThemes is not empty and formThemes is string %}
|
||||
{% form_theme form formThemes %}
|
||||
{% endif %}
|
||||
|
||||
{% set isIndex = 'index' == tmpl ? true : false %}
|
||||
{% set tmpl = 'list' %}
|
||||
{% extends isIndex ? '@MauticCore/Default/content.html.twig' : '@MauticCore/Default/raw_output.html.twig' %}
|
||||
|
||||
{% block mauticContent %}config{% endblock %}
|
||||
|
||||
{% block headerTitle %}{{ 'mautic.config.header.index'|trans }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- start: box layout -->
|
||||
<div class="container">
|
||||
<!-- step container -->
|
||||
<div class="row">
|
||||
<div class="col-md-3 height-auto">
|
||||
<div class="">
|
||||
{% if not isWritable %}
|
||||
<div class="alert alert-danger">{{ 'mautic.config.notwritable'|trans }}</div>
|
||||
{% endif %}
|
||||
<!-- Nav tabs -->
|
||||
<ul class="list-group list-group-tabs" role="tablist">
|
||||
{% for key in form.children|keys|filter(v => formConfigs[v] is defined and form[v].children|length > 0) %}
|
||||
<li role="presentation" class="list-group-item {% if loop.first %}in active{% endif %}">
|
||||
{% set containsErrors = formContainsErrors(form[key]) %}
|
||||
<a href="#{{ key }}" aria-controls="{{ key }}" role="tab" data-toggle="tab" class="list-group-item-text steps {% if formContainsErrors(form[key]) %}text-danger{% endif %}">
|
||||
{{ ('mautic.config.tab.' ~ key)|trans }}
|
||||
{% if formContainsErrors(form[key]) %}
|
||||
<i class="ri-alert-line"></i>
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- container -->
|
||||
<div class="col-md-9 height-auto">
|
||||
{{ form_start(form) }}
|
||||
<!-- Tab panes -->
|
||||
<div class="tab-content">
|
||||
{% for key in form.children|keys|filter(v => formConfigs[v] is defined) %}
|
||||
{% if form[key].children|length > 0 %}
|
||||
<div role="tabpanel" class="tab-pane fade {% if loop.first %}in active{% endif %} bdr-w-0" id="{{ key }}">
|
||||
<div>
|
||||
<div class="row pa-md bdr-b">
|
||||
<h4 class="fw-sb">{{ ('mautic.config.tab.' ~ key)|trans }}</h4>
|
||||
</div>
|
||||
{{ form_widget(form[key], {'formConfig': formConfigs[key]}) }}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
{% do form[key].setRendered() %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,27 @@
|
||||
{%- set hasErrors = form.vars.errors|length %}
|
||||
{%- set feedbackClass = hasErrors > 0 ? 'has-error' : '' %}
|
||||
{%- set field = form.vars.name %}
|
||||
{%- set hide = fieldValue is not defined or (fieldValue is defined and fieldValue is empty) ? 'hide' : '' %}
|
||||
{%- set filename = inputAlphanum(form.vars.label|trans, true, '_') %}
|
||||
{%- set downloadUrl = path('mautic_config_action', {'objectAction': 'download', 'objectId': field, 'filename': filename}) %}
|
||||
{%- set removeUrl = path('mautic_config_action', {'objectAction': 'remove', 'objectId': field}) %}
|
||||
<div class="row">
|
||||
<div class="form-group col-xs-12 {{ feedbackClass }}">
|
||||
{{ form_label(form, form.vars.label) }}
|
||||
<span class="small pull-right {{ hide }}">
|
||||
<a
|
||||
data-toggle="confirmation"
|
||||
href="{{ removeUrl }}"
|
||||
data-message="{{ 'mautic.config.remove_file_contents'|trans|e }}"
|
||||
data-confirm-text="{{ 'mautic.core.remove'|trans|e }}"
|
||||
data-confirm-callback="removeConfigValue"
|
||||
data-cancel-text="{{ 'mautic.core.form.cancel'|trans|e }}">
|
||||
{{ 'mautic.core.remove'|trans }}
|
||||
</a>
|
||||
<span> | </span>
|
||||
<a href="{{ downloadUrl }}">{{ 'mautic.core.download'|trans }}</a>
|
||||
</span>
|
||||
{{ form_widget(form) }}
|
||||
{{ form_errors(form) }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,58 @@
|
||||
{% block dsn_row %}
|
||||
<div class="config-dsn-container">
|
||||
{% if form.test_button is defined %}
|
||||
<div class="help-block">
|
||||
<span class="ri-loader-3-line ri-spin hide pull-left"></span>
|
||||
<div class="status-msg"></div>
|
||||
<div class="alert alert-warning save-config-msg hide">{{ 'mautic.config.dsn.save_to_test'|trans }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(form.scheme) }}
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(form.host, { 'attr': {'preaddon_text': '://'} }) }}
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(form.port, { 'attr': {'preaddon_text': ':'} }) }}
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(form.path, { 'attr': {'preaddon_text': '/'} }) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(form.user) }}
|
||||
</div>
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(form.password, { 'attr': {'preaddon_text': ':'} }) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="config-dsn-test-container">
|
||||
<div class="form-group">
|
||||
{% if form.test_button is defined %}
|
||||
{{ form_widget(form.test_button) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="form-control-static ml-10">
|
||||
<span class="text-muted">{{ 'mautic.config.dsn.using_current_dsn'|trans }}:</span>
|
||||
{% include '@MauticCore/Components/code-snippet.html.twig' with {
|
||||
variant: 'inline',
|
||||
innerText: form.vars.currentDsn|default('n/a'),
|
||||
} %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div class="col-xs-12">
|
||||
{{ form_row(form.options) }}
|
||||
</div>
|
||||
</div>
|
||||
{{ form_rest(form) }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,142 @@
|
||||
{% extends '@MauticCore/Default/content.html.twig' %}
|
||||
|
||||
{% block mauticContent %}sysinfo{% endblock %}
|
||||
|
||||
{% block headerTitle %}{{ 'mautic.sysinfo.header.index'|trans }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- start: box layout -->
|
||||
<div class="box-layout">
|
||||
<!-- step container -->
|
||||
<div class="col-md-3 height-auto">
|
||||
<div class="pr-lg pl-lg pt-md pb-md">
|
||||
<!-- Nav tabs -->
|
||||
<ul class="list-group list-group-tabs" role="tablist">
|
||||
<li role="presentation" class="list-group-item in active">
|
||||
<a href="#phpinfo" aria-controls="phpinfo" role="tab" data-toggle="tab" class="list-group-item-heading">
|
||||
{{ 'mautic.sysinfo.tab.phpinfo'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation" class="list-group-item">
|
||||
<a href="#recommendations" aria-controls="phpinfo" role="tab" data-toggle="tab" class="list-group-item-heading">
|
||||
{{ 'mautic.sysinfo.tab.recommendations'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation" class="list-group-item">
|
||||
<a href="#folders" aria-controls="folders" role="tab" data-toggle="tab" class="list-group-item-heading">
|
||||
{{ 'mautic.sysinfo.tab.folders'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation" class="list-group-item">
|
||||
<a href="#log" aria-controls="log" role="tab" data-toggle="tab" class="list-group-item-heading">
|
||||
{{ 'mautic.sysinfo.tab.log'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation" class="list-group-item">
|
||||
<a href="#dbinfo" aria-controls="dbinfo" role="tab" data-toggle="tab" class="list-group-item-heading">
|
||||
{{ 'mautic.sysinfo.tab.dbinfo'|trans }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- container -->
|
||||
<div class="col-md-9 height-auto bdr-l">
|
||||
|
||||
<!-- Tab panes -->
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane fade in active bdr-w-0" id="phpinfo">
|
||||
<div class="pt-md pr-md pl-md pb-md">
|
||||
{{ phpInfo|raw }}
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane fade bdr-w-0" id="recommendations">
|
||||
<div class="pt-md pr-md pl-md pb-md">
|
||||
{% if recommendations is empty and requirements is empty %}
|
||||
<div class="alert alert-info">
|
||||
{{ 'mautic.sysinfo.no.recommendations'|trans }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% for requirement in requirements %}
|
||||
<div class="alert alert-danger">
|
||||
{{ requirement|raw }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% for recommendation in recommendations %}
|
||||
<div class="alert alert-warning">
|
||||
{{ recommendation|raw }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane fade bdr-w-0" id="folders">
|
||||
<div class="pt-md pr-md pl-md pb-md">
|
||||
<h2 class="pb-md">{{ 'mautic.sysinfo.folders.title'|trans }}</h2>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'mautic.sysinfo.folder.path'|trans }}</th>
|
||||
<th>{{ 'mautic.sysinfo.is.writable'|trans }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% for folder, isWritable in folders %}
|
||||
<tr class="{% if isWritable %}bg-success{% else %}bg-danger{% endif %}">
|
||||
<td>{{ folder }}</td>
|
||||
<td>
|
||||
{% if isWritable %}
|
||||
{{ 'mautic.sysinfo.writable'|trans }}
|
||||
{% else %}
|
||||
{{ 'mautic.sysinfo.unwritable'|trans }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane fade bdr-w-0" id="log">
|
||||
<div class="pt-md pr-md pl-md pb-md">
|
||||
<h2 class="pb-md">{{ 'mautic.sysinfo.log.title'|trans }}</h2>
|
||||
{% if log is defined and log is not empty %}
|
||||
<pre>{{ log }}</pre>
|
||||
{% else %}
|
||||
<div class="alert alert-info" role="alert">
|
||||
{{ 'mautic.sysinfo.log.missing'|trans }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane fade bdr-w-0" id="dbinfo">
|
||||
<div class="pt-md pr-md pl-md pb-md">
|
||||
<h2 class="pb-md">{{ 'mautic.sysinfo.dbinfo.title'|trans }}</h2>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'mautic.sysinfo.dbinfo.property'|trans }}</th>
|
||||
<th>{{ 'mautic.sysinfo.dbinfo.value'|trans }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ 'mautic.sysinfo.dbinfo.version'|trans }}</td>
|
||||
<td id="dbinfo-version">{{ dbInfo.version }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ 'mautic.sysinfo.dbinfo.driver'|trans }}</td>
|
||||
<td id="dbinfo-driver">{{ dbInfo.driver }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ 'mautic.sysinfo.dbinfo.platform'|trans }}</td>
|
||||
<td id="dbinfo-platform">{{ dbInfo.platform }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ConfigBundle\Service;
|
||||
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
/**
|
||||
* Compare normalized for data and log changes.
|
||||
*/
|
||||
class ConfigChangeLogger
|
||||
{
|
||||
/**
|
||||
* Keys to remove from log.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private array $filterKeys = [
|
||||
'transifex_password',
|
||||
'mailer_is_owner',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var mixed[]|null
|
||||
*/
|
||||
private ?array $originalNormData = null;
|
||||
|
||||
public function __construct(
|
||||
private IpLookupHelper $ipLookupHelper,
|
||||
private AuditLogModel $auditLogModel,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ConfigChangeLogger
|
||||
*/
|
||||
public function setOriginalNormData(array $originalNormData)
|
||||
{
|
||||
$this->originalNormData = $originalNormData;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log changes if something was changed.
|
||||
* Diff is based on form normalized data before and after post.
|
||||
*
|
||||
* @see Form::getNormData()
|
||||
*/
|
||||
public function log(array $postNormData): void
|
||||
{
|
||||
if (null === $this->originalNormData) {
|
||||
throw new \RuntimeException('Set original normalized data at first');
|
||||
}
|
||||
|
||||
$originalData = $this->normalizeData($this->originalNormData);
|
||||
$postData = $this->filterData($this->normalizeData($postNormData));
|
||||
|
||||
$diff = [];
|
||||
foreach ($postData as $key => $value) {
|
||||
if (array_key_exists($key, $originalData) && $originalData[$key] != $value) {
|
||||
if ($value instanceof UploadedFile) {
|
||||
$value = $value->getFilename();
|
||||
}
|
||||
|
||||
$diff[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($diff)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$log = [
|
||||
'bundle' => 'config',
|
||||
'object' => 'config',
|
||||
'objectId' => 0,
|
||||
'action' => 'update',
|
||||
'details' => $diff,
|
||||
'ipAddress' => $this->ipLookupHelper->getIpAddressFromRequest(),
|
||||
];
|
||||
|
||||
$this->auditLogModel->writeToLog($log);
|
||||
}
|
||||
|
||||
/**
|
||||
* Some form data (AssetBundle) has 'parameters' inside array too.
|
||||
* Normalize all.
|
||||
*/
|
||||
private function normalizeData(array $data): array
|
||||
{
|
||||
$key = 'parameters';
|
||||
|
||||
$normData = [];
|
||||
foreach ($data as $values) {
|
||||
if (array_key_exists($key, $values)) {
|
||||
$normData = array_merge($normData, $values[$key]);
|
||||
} else {
|
||||
$normData = array_merge($normData, $values);
|
||||
}
|
||||
}
|
||||
|
||||
return $normData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter unused keys from post data.
|
||||
*/
|
||||
private function filterData(array $data): array
|
||||
{
|
||||
$keys = $this->filterKeys;
|
||||
|
||||
return array_filter($data, fn ($key): bool => !in_array($key, $keys),
|
||||
ARRAY_FILTER_USE_KEY);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\ConfigBundle\Tests\Controller;
|
||||
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\DomCrawler\Field\ChoiceFormField;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class ConfigControllerFunctionalTest extends MauticMysqlTestCase
|
||||
{
|
||||
private const SUBDOMAIN_URL = 'subdomain_url.com';
|
||||
|
||||
private string $prefix;
|
||||
|
||||
protected $useCleanupRollback = false;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->configParams['config_allowed_parameters'] = [
|
||||
'kernel.project_dir',
|
||||
];
|
||||
|
||||
$this->configParams['locale'] = 'en_US';
|
||||
$this->configParams['subdomain_url'] = self::SUBDOMAIN_URL;
|
||||
|
||||
parent::setUp();
|
||||
|
||||
$this->prefix = MAUTIC_TABLE_PREFIX;
|
||||
}
|
||||
|
||||
public function testValuesAreEscapedProperly(): void
|
||||
{
|
||||
$trackIps = "%ip1%\n%ip2%\n%kernel.project_dir%";
|
||||
$googleAnalytics = 'reveal pass: %mautic.db_password%';
|
||||
|
||||
// request config edit page
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/config/edit');
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Find save & close button
|
||||
$buttonCrawler = $crawler->selectButton('config[buttons][save]');
|
||||
$form = $buttonCrawler->form();
|
||||
$form->setValues(
|
||||
[
|
||||
'config[coreconfig][site_url]' => 'https://mautic-community.local', // required
|
||||
'config[coreconfig][do_not_track_ips]' => $trackIps,
|
||||
'config[pageconfig][google_analytics]' => $googleAnalytics,
|
||||
'config[leadconfig][contact_columns]' => ['name', 'email', 'id'],
|
||||
]
|
||||
);
|
||||
|
||||
$crawler = $this->client->submit($form);
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
// Check for a flash error
|
||||
$response = $this->client->getResponse()->getContent();
|
||||
$message = $crawler->filterXPath("//div[@id='flashes']//span")->count()
|
||||
?
|
||||
$crawler->filterXPath("//div[@id='flashes']//span")->first()->text()
|
||||
:
|
||||
'';
|
||||
Assert::assertStringNotContainsString('Could not save updated configuration:', $response, $message);
|
||||
|
||||
// Check values are escaped properly in the config file
|
||||
$configParameters = $this->getConfigParameters();
|
||||
Assert::assertArrayHasKey('do_not_track_ips', $configParameters);
|
||||
Assert::assertSame(
|
||||
[
|
||||
$this->escape('%ip1%'),
|
||||
$this->escape('%ip2%'),
|
||||
'%kernel.project_dir%',
|
||||
],
|
||||
$configParameters['do_not_track_ips']
|
||||
);
|
||||
Assert::assertArrayHasKey('google_analytics', $configParameters);
|
||||
Assert::assertSame($this->escape($googleAnalytics), $configParameters['google_analytics']);
|
||||
// Check values are unescaped properly in the edit form
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/config/edit');
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$buttonCrawler = $crawler->selectButton('config[buttons][save]');
|
||||
$form = $buttonCrawler->form();
|
||||
Assert::assertEquals($trackIps, $form['config[coreconfig][do_not_track_ips]']->getValue());
|
||||
Assert::assertEquals($googleAnalytics, $form['config[pageconfig][google_analytics]']->getValue());
|
||||
}
|
||||
|
||||
private function getConfigPath(): string
|
||||
{
|
||||
return static::getContainer()->get('kernel')->getLocalConfigFile();
|
||||
}
|
||||
|
||||
private function getConfigParameters(): array
|
||||
{
|
||||
$parameters = [];
|
||||
include $this->getConfigPath();
|
||||
|
||||
return $parameters;
|
||||
}
|
||||
|
||||
private function escape(string $value): string
|
||||
{
|
||||
return str_replace('%', '%%', $value);
|
||||
}
|
||||
|
||||
public function testConfigNotFoundPageConfiguration(): void
|
||||
{
|
||||
// insert published record
|
||||
$this->connection->insert($this->prefix.'pages', [
|
||||
'is_published' => 1,
|
||||
'date_added' => (new \DateTime())->format('Y-m-d H:i:s'),
|
||||
'title' => 'page1',
|
||||
'alias' => 'page1',
|
||||
'template' => 'blank',
|
||||
'custom_html' => 'Page1 Test Html',
|
||||
'hits' => 0,
|
||||
'unique_hits' => 0,
|
||||
'variant_hits' => 0,
|
||||
'revision' => 0,
|
||||
'lang' => 'en',
|
||||
]);
|
||||
$page1 = $this->connection->lastInsertId();
|
||||
|
||||
// insert unpublished record
|
||||
$this->connection->insert($this->prefix.'pages', [
|
||||
'is_published' => 0,
|
||||
'date_added' => (new \DateTime())->format('Y-m-d H:i:s'),
|
||||
'title' => 'page2',
|
||||
'alias' => 'page2',
|
||||
'template' => 'blank',
|
||||
'custom_html' => 'Page2 Test Html',
|
||||
'hits' => 0,
|
||||
'unique_hits' => 0,
|
||||
'variant_hits' => 0,
|
||||
'revision' => 0,
|
||||
'lang' => 'en',
|
||||
]);
|
||||
$this->connection->lastInsertId();
|
||||
|
||||
// insert published record
|
||||
$this->connection->insert($this->prefix.'pages', [
|
||||
'is_published' => 1,
|
||||
'date_added' => (new \DateTime())->format('Y-m-d H:i:s'),
|
||||
'title' => 'page3',
|
||||
'alias' => 'page3',
|
||||
'template' => 'blank',
|
||||
'custom_html' => 'Page3 Test Html',
|
||||
'hits' => 0,
|
||||
'unique_hits' => 0,
|
||||
'variant_hits' => 0,
|
||||
'revision' => 0,
|
||||
'lang' => 'en',
|
||||
]);
|
||||
$page3 = $this->connection->lastInsertId();
|
||||
|
||||
// request config edit page
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/config/edit');
|
||||
|
||||
// Find save & close button
|
||||
$buttonCrawler = $crawler->selectButton('config[buttons][save]');
|
||||
$form = $buttonCrawler->form();
|
||||
|
||||
// Fetch available option for 404_page field
|
||||
$availableOptions = $form['config[coreconfig][404_page]']->availableOptionValues();
|
||||
|
||||
// page 2 should not be available in option list because it is unpublished
|
||||
$this->assertEquals(['', $page1, $page3], $availableOptions);
|
||||
|
||||
// page 3 for 404_page
|
||||
$form->setValues(
|
||||
[
|
||||
'config[coreconfig][site_url]' => 'https://mautic-community.local', // required
|
||||
'config[leadconfig][contact_columns]' => ['name', 'email', 'id'],
|
||||
'config[coreconfig][404_page]' => $page3,
|
||||
]
|
||||
);
|
||||
|
||||
$crawler = $this->client->submit($form);
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/config/edit');
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$buttonCrawler = $crawler->selectButton('config[buttons][save]');
|
||||
$form = $buttonCrawler->form();
|
||||
Assert::assertEquals($page3, $form['config[coreconfig][404_page]']->getValue());
|
||||
// re-create the Symfony client to make config changes applied
|
||||
$this->setUpSymfony($this->configParams);
|
||||
|
||||
// Request not found url page3 page content should be rendered
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/notfoundurlblablabla');
|
||||
$this->assertResponseStatusCodeSame(Response::HTTP_NOT_FOUND);
|
||||
$this->assertStringContainsString('Page3 Test Html', $crawler->text());
|
||||
}
|
||||
|
||||
public function testConfigNotificationConfiguration(): void
|
||||
{
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/config/edit');
|
||||
|
||||
$buttonCrawler = $crawler->selectButton('config[buttons][save]');
|
||||
$form = $buttonCrawler->form();
|
||||
|
||||
$send_notification_to_author = '0';
|
||||
$campaign_notification_email_addresses = 'a@test.com, b@test.com';
|
||||
$webhook_notification_email_addresses = 'a@webhook.com, b@webhook.com';
|
||||
|
||||
$form->setValues(
|
||||
[
|
||||
'config[coreconfig][site_url]' => 'https://mautic-community.local', // required
|
||||
'config[leadconfig][contact_columns]' => ['name', 'email', 'id'],
|
||||
'config[notification_config][campaign_send_notification_to_author]' => $send_notification_to_author,
|
||||
'config[notification_config][campaign_notification_email_addresses]' => $campaign_notification_email_addresses,
|
||||
'config[notification_config][webhook_send_notification_to_author]' => $send_notification_to_author,
|
||||
'config[notification_config][webhook_notification_email_addresses]' => $webhook_notification_email_addresses,
|
||||
]
|
||||
);
|
||||
|
||||
$this->client->submit($form);
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/config/edit');
|
||||
$this->assertResponseIsSuccessful();
|
||||
|
||||
$buttonCrawler = $crawler->selectButton('config[buttons][save]');
|
||||
$form = $buttonCrawler->form();
|
||||
|
||||
Assert::assertEquals($send_notification_to_author, $form['config[notification_config][campaign_send_notification_to_author]']->getValue());
|
||||
Assert::assertEquals($campaign_notification_email_addresses, $form['config[notification_config][campaign_notification_email_addresses]']->getValue());
|
||||
Assert::assertEquals($send_notification_to_author, $form['config[notification_config][webhook_send_notification_to_author]']->getValue());
|
||||
Assert::assertEquals($webhook_notification_email_addresses, $form['config[notification_config][webhook_notification_email_addresses]']->getValue());
|
||||
}
|
||||
|
||||
public function testUserAndSystemLocale(): void
|
||||
{
|
||||
// 1. Change user locale in account - should change _locale session
|
||||
$accountCrawler = $this->client->request(Request::METHOD_GET, '/s/account');
|
||||
$this->assertResponseIsSuccessful();
|
||||
$accountSaveButton = $accountCrawler->selectButton('user[buttons][save]');
|
||||
$accountForm = $accountSaveButton->form();
|
||||
$accountForm->setValues(
|
||||
[
|
||||
'user[locale]' => 'en_US',
|
||||
]
|
||||
);
|
||||
$this->client->submit($accountForm);
|
||||
$this->assertResponseIsSuccessful();
|
||||
Assert::assertSame('en_US', $this->client->getRequest()->getSession()->get('_locale'));
|
||||
|
||||
// 2. Change system locale in configuration - should not change _locale session
|
||||
$configCrawler = $this->client->request(Request::METHOD_GET, '/s/config/edit');
|
||||
$configSaveButton = $configCrawler->selectButton('config[buttons][save]');
|
||||
$configForm = $configSaveButton->form();
|
||||
$configForm->setValues(
|
||||
[
|
||||
'config[coreconfig][locale]' => 'en_US',
|
||||
'config[coreconfig][site_url]' => 'https://mautic-cloud.local', // required
|
||||
]
|
||||
);
|
||||
$this->client->submit($configForm);
|
||||
$this->assertResponseIsSuccessful();
|
||||
Assert::assertSame('en_US', $this->client->getRequest()->getSession()->get('_locale'));
|
||||
|
||||
// 3. Change user locale to system default in account - should change _locale session to system default
|
||||
$accountCrawler = $this->client->request(Request::METHOD_GET, '/s/account');
|
||||
$accountSaveButton = $accountCrawler->selectButton('user[buttons][save]');
|
||||
$accountForm = $accountSaveButton->form();
|
||||
$accountForm->setValues(
|
||||
[
|
||||
'user[locale]' => '',
|
||||
]
|
||||
);
|
||||
$this->client->submit($accountForm);
|
||||
$this->assertResponseIsSuccessful();
|
||||
Assert::assertSame('en_US', $this->client->getRequest()->getSession()->get('_locale'));
|
||||
|
||||
// 2. Change system locale in configuration to en_US - should change _locale session
|
||||
$configCrawler = $this->client->request(Request::METHOD_GET, '/s/config/edit');
|
||||
$configSaveButton = $configCrawler->selectButton('config[buttons][save]');
|
||||
$configForm = $configSaveButton->form();
|
||||
$configForm->setValues(
|
||||
[
|
||||
'config[coreconfig][locale]' => 'en_US',
|
||||
'config[coreconfig][site_url]' => 'https://mautic-cloud.local', // required
|
||||
]
|
||||
);
|
||||
$this->client->submit($configForm);
|
||||
$this->assertResponseIsSuccessful();
|
||||
Assert::assertSame('en_US', $this->client->getRequest()->getSession()->get('_locale'));
|
||||
}
|
||||
|
||||
public function testSSOSettingEntityId(): void
|
||||
{
|
||||
$configCrawler = $this->client->request(Request::METHOD_GET, '/s/config/edit');
|
||||
$configSaveButton = $configCrawler->selectButton('config[buttons][apply]');
|
||||
$configForm = $configSaveButton->form();
|
||||
|
||||
/** @var ChoiceFormField $entityIdField */
|
||||
$entityIdField = $configForm['config[userconfig][saml_idp_entity_id]'];
|
||||
$availableOptions = $entityIdField->availableOptionValues();
|
||||
Assert::assertCount(3, $availableOptions);
|
||||
$configForm->setValues(
|
||||
[
|
||||
'config[userconfig][saml_idp_entity_id]' => $availableOptions[1],
|
||||
'config[coreconfig][site_url]' => 'https://mautic-cloud.local', // required
|
||||
]
|
||||
);
|
||||
$this->client->submit($configForm);
|
||||
$this->assertResponseIsSuccessful();
|
||||
Assert::assertEquals($availableOptions[1], $configForm['config[userconfig][saml_idp_entity_id]']->getValue());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\ConfigBundle\Tests\Controller;
|
||||
|
||||
use Mautic\ConfigBundle\Model\SysinfoModel;
|
||||
use Mautic\CoreBundle\Test\MauticMysqlTestCase;
|
||||
use PHPUnit\Framework\Assert;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
|
||||
class SysinfoControllerTest extends MauticMysqlTestCase
|
||||
{
|
||||
public function testDbInfoIsShown(): void
|
||||
{
|
||||
$sysinfoModel = static::getContainer()->get(SysinfoModel::class);
|
||||
\assert($sysinfoModel instanceof SysinfoModel);
|
||||
$dbInfo = $sysinfoModel->getDbInfo();
|
||||
|
||||
// Request sysinfo page
|
||||
$crawler = $this->client->request(Request::METHOD_GET, '/s/sysinfo');
|
||||
Assert::assertTrue($this->client->getResponse()->isOk());
|
||||
|
||||
$dbVersion = $crawler->filterXPath("//td[@id='dbinfo-version']")->text();
|
||||
$dbDriver = $crawler->filterXPath("//td[@id='dbinfo-driver']")->text();
|
||||
$dbPlatform = $crawler->filterXPath("//td[@id='dbinfo-platform']")->text();
|
||||
$recommendations = $crawler->filter('#recommendations');
|
||||
|
||||
Assert::assertSame($dbInfo['version'], $dbVersion);
|
||||
Assert::assertSame($dbInfo['driver'], $dbDriver);
|
||||
Assert::assertSame($dbInfo['platform'], $dbPlatform);
|
||||
Assert::assertGreaterThan(0, $recommendations->count());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ConfigBundle\Tests\Event;
|
||||
|
||||
use Mautic\ConfigBundle\Event\ConfigBuilderEvent;
|
||||
use Mautic\CoreBundle\Tests\CommonMocks;
|
||||
|
||||
class ConfigBuilderEventTest extends CommonMocks
|
||||
{
|
||||
public function testAddForm(): void
|
||||
{
|
||||
$event = $this->initEvent();
|
||||
$form = ['formAlias' => 'testform'];
|
||||
$result = $event->addForm($form);
|
||||
|
||||
$this->assertTrue($result instanceof ConfigBuilderEvent);
|
||||
|
||||
$forms = $event->getForms();
|
||||
|
||||
$this->assertEquals($form, $forms[$form['formAlias']]);
|
||||
}
|
||||
|
||||
public function testRemoveForm(): void
|
||||
{
|
||||
$event = $this->initEvent();
|
||||
$form = ['formAlias' => 'testform'];
|
||||
|
||||
$event->addForm($form);
|
||||
|
||||
$result = $event->removeForm($form['formAlias']);
|
||||
$forms = $event->getForms();
|
||||
|
||||
$this->assertEquals([], $forms);
|
||||
$this->assertTrue($result);
|
||||
}
|
||||
|
||||
protected function initEvent()
|
||||
{
|
||||
return new ConfigBuilderEvent($this->getBundleHelperMock());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ConfigBundle\Tests\Event;
|
||||
|
||||
use Mautic\ConfigBundle\Event\ConfigEvent;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\ParameterBag;
|
||||
|
||||
class ConfigEventTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testGetSetConfig(): void
|
||||
{
|
||||
// Config not defined
|
||||
$config = [];
|
||||
$paramBag = $this->createMock(ParameterBag::class);
|
||||
$event = new ConfigEvent($config, $paramBag);
|
||||
$key = 'undefined';
|
||||
$this->assertEquals([], $event->getConfig($key));
|
||||
|
||||
// Config defined with setter
|
||||
$key = 'defined';
|
||||
$config = ['config' => []];
|
||||
$event->setConfig($config, $key);
|
||||
$this->assertEquals($config, $event->getConfig($key));
|
||||
|
||||
// Config not found by key so complete config returned;
|
||||
$undefinedKey = 'undefined';
|
||||
$this->assertEquals([], $event->getConfig($undefinedKey));
|
||||
|
||||
// Get complete config
|
||||
$config = [$key => $config];
|
||||
$this->assertEquals($config, $event->getConfig());
|
||||
}
|
||||
|
||||
public function testGetSetPreserved(): void
|
||||
{
|
||||
$config = [];
|
||||
$paramBag = $this->createMock(ParameterBag::class);
|
||||
$event = new ConfigEvent($config, $paramBag);
|
||||
|
||||
$this->assertEquals([], $event->getPreservedFields());
|
||||
|
||||
$preserved = 'preserved';
|
||||
$result = [$preserved];
|
||||
$event->unsetIfEmpty($preserved);
|
||||
$this->assertEquals($result, $event->getPreservedFields());
|
||||
|
||||
$preserved = ['preserved' => 'value'];
|
||||
$result = array_merge($result, $preserved);
|
||||
$event->unsetIfEmpty($preserved);
|
||||
$this->assertEquals($result, $event->getPreservedFields());
|
||||
}
|
||||
|
||||
public function testGetSetErrors(): void
|
||||
{
|
||||
$config = [];
|
||||
$paramBag = $this->createMock(ParameterBag::class);
|
||||
$event = new ConfigEvent($config, $paramBag);
|
||||
|
||||
$this->assertEquals([], $event->getErrors());
|
||||
|
||||
$message = 'message';
|
||||
$messages = [$message => []];
|
||||
$this->assertEquals($event, $event->setError($message));
|
||||
$this->assertEquals($messages, $event->getErrors());
|
||||
|
||||
$message = 'message';
|
||||
$messageVars = ['var' => 'value'];
|
||||
$messages = [$message => $messageVars];
|
||||
$this->assertEquals($event, $event->setError($message, $messageVars));
|
||||
$this->assertEquals($messages, $event->getErrors());
|
||||
|
||||
$message = 'message';
|
||||
$messageVars = ['var' => 'value'];
|
||||
$key = 'key';
|
||||
$field = 'field';
|
||||
$fieldErrors[$key][$field] = [
|
||||
$message,
|
||||
$messageVars,
|
||||
];
|
||||
$this->assertEquals($event, $event->setError($message, $messageVars, $key, $field));
|
||||
$this->assertEquals($fieldErrors, $event->getFieldErrors());
|
||||
}
|
||||
|
||||
public function testGetFileContent(): void
|
||||
{
|
||||
$config = [];
|
||||
$paramBag = $this->createMock(ParameterBag::class);
|
||||
$event = new ConfigEvent($config, $paramBag);
|
||||
|
||||
$fileContent = 'content';
|
||||
$fileHandler = tmpfile();
|
||||
$realPath = stream_get_meta_data($fileHandler)['uri'];
|
||||
fwrite($fileHandler, ' '.$fileContent);
|
||||
|
||||
$uploadedFile = $this->createMock(UploadedFile::class);
|
||||
$uploadedFile->expects($this->once())
|
||||
->method('getRealPath')
|
||||
->willReturn($realPath);
|
||||
|
||||
$this->assertEquals($fileContent, $event->getFileContent($uploadedFile));
|
||||
$this->assertFalse(file_exists($realPath));
|
||||
}
|
||||
|
||||
public function testEncodeFileContents(): void
|
||||
{
|
||||
$config = [];
|
||||
$paramBag = $this->createMock(ParameterBag::class);
|
||||
$event = new ConfigEvent($config, $paramBag);
|
||||
|
||||
$string = 'řčžýřžýčř';
|
||||
$result = 'xZnEjcW+w73FmcW+w73EjcWZ';
|
||||
$this->assertEquals($result, $event->encodeFileContents($string));
|
||||
}
|
||||
|
||||
public function testNormalizedDataGetSet(): void
|
||||
{
|
||||
$config = [];
|
||||
$paramBag = $this->createMock(ParameterBag::class);
|
||||
$event = new ConfigEvent($config, $paramBag);
|
||||
|
||||
$origNormData = ['orig'];
|
||||
|
||||
$this->assertInstanceOf(ConfigEvent::class, $event->setOriginalNormData($origNormData));
|
||||
$this->assertEquals($origNormData, $event->getOriginalNormData());
|
||||
|
||||
$normData = ['norm'];
|
||||
|
||||
$event->setNormData($normData);
|
||||
$this->assertEquals($normData, $event->getNormData());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Mautic\ConfigBundle\Tests\EventListener;
|
||||
|
||||
use Mautic\ConfigBundle\ConfigEvents;
|
||||
use Mautic\ConfigBundle\Event\ConfigEvent;
|
||||
use Mautic\ConfigBundle\EventListener\ConfigSubscriber;
|
||||
use Mautic\ConfigBundle\Service\ConfigChangeLogger;
|
||||
use Mautic\CoreBundle\Entity\AuditLogRepository;
|
||||
use Mautic\CoreBundle\Entity\IpAddressRepository;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ConfigSubscriberTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @var ConfigChangeLogger|MockObject
|
||||
*/
|
||||
private MockObject $logger;
|
||||
|
||||
private ConfigSubscriber $subscriber;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->logger = $this->createMock(ConfigChangeLogger::class);
|
||||
$ipAddressRepo = $this->createMock(IpAddressRepository::class);
|
||||
$coreParamHelper = $this->createMock(CoreParametersHelper::class);
|
||||
$auditLogRepo = $this->createMock(AuditLogRepository::class);
|
||||
$this->subscriber = new ConfigSubscriber($this->logger, $ipAddressRepo, $coreParamHelper, $auditLogRepo);
|
||||
}
|
||||
|
||||
public function testGetSubscribedEvents(): void
|
||||
{
|
||||
$this->assertEquals(
|
||||
[
|
||||
ConfigEvents::CONFIG_POST_SAVE => ['onConfigPostSave', 0],
|
||||
],
|
||||
$this->subscriber->getSubscribedEvents()
|
||||
);
|
||||
}
|
||||
|
||||
public function testNothingToLogOnConfigPostSave(): void
|
||||
{
|
||||
// Test nothing to log
|
||||
$this->logger->expects($this->never())
|
||||
->method('log');
|
||||
$event = $this->createMock(ConfigEvent::class);
|
||||
$event->expects($this->once())
|
||||
->method('getOriginalNormData')
|
||||
->willReturn(null);
|
||||
|
||||
$this->subscriber->onConfigPostSave($event);
|
||||
}
|
||||
|
||||
public function testSomethingToLogOnConfigPostSave(): void
|
||||
{
|
||||
// Test something to log
|
||||
$originalNormData = ['orig'];
|
||||
$normData = ['norm'];
|
||||
|
||||
$event = $this->createMock(ConfigEvent::class);
|
||||
$event->expects($this->once())
|
||||
->method('getOriginalNormData')
|
||||
->willReturn($originalNormData);
|
||||
$event->expects($this->once())
|
||||
->method('getNormData')
|
||||
->willReturn($normData);
|
||||
$this->logger->expects($this->once())
|
||||
->method('setOriginalNormData')
|
||||
->with($originalNormData)
|
||||
->willReturn($this->logger);
|
||||
$this->logger->expects($this->once())
|
||||
->method('log')
|
||||
->with($normData);
|
||||
|
||||
$this->subscriber->onConfigPostSave($event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ConfigBundle\Tests\Form\Helper;
|
||||
|
||||
use Mautic\ConfigBundle\Form\DataTransformer\DsnTransformerFactory;
|
||||
use Mautic\ConfigBundle\Form\Helper\RestrictionHelper;
|
||||
use Mautic\ConfigBundle\Form\Type\ConfigType;
|
||||
use Mautic\ConfigBundle\Form\Type\DsnType;
|
||||
use Mautic\ConfigBundle\Form\Type\EscapeTransformer;
|
||||
use Mautic\CoreBundle\Form\Type\ButtonGroupType;
|
||||
use Mautic\CoreBundle\Form\Type\FormButtonsType;
|
||||
use Mautic\CoreBundle\Form\Type\StandAloneButtonType;
|
||||
use Mautic\CoreBundle\Form\Type\YesNoButtonGroupType;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
use Mautic\CoreBundle\Translation\Translator;
|
||||
use Mautic\EmailBundle\EventListener\ProcessBounceSubscriber;
|
||||
use Mautic\EmailBundle\EventListener\ProcessUnsubscribeSubscriber;
|
||||
use Mautic\EmailBundle\Form\Type\ConfigMonitoredEmailType;
|
||||
use Mautic\EmailBundle\Form\Type\ConfigMonitoredMailboxesType;
|
||||
use Mautic\EmailBundle\Form\Type\ConfigType as EmailConfigType;
|
||||
use Mautic\EmailBundle\MonitoredEmail\Mailbox;
|
||||
use Mautic\EmailBundle\MonitoredEmail\Processor\Bounce;
|
||||
use Mautic\EmailBundle\MonitoredEmail\Processor\FeedbackLoop;
|
||||
use Mautic\EmailBundle\MonitoredEmail\Processor\Unsubscribe;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
|
||||
use Symfony\Component\Form\Form;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\Forms;
|
||||
use Symfony\Component\Form\PreloadedExtension;
|
||||
use Symfony\Component\Form\Test\TypeTestCase;
|
||||
use Symfony\Component\Validator\ConstraintViolationList;
|
||||
use Symfony\Component\Validator\Mapping\ClassMetadata;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
/**
|
||||
* Mocking a representative ConfigForm by leveraging Symfony's TypeTestCase to test RestrictionHelper.
|
||||
*/
|
||||
#[\PHPUnit\Framework\Attributes\CoversClass(RestrictionHelper::class)]
|
||||
class RestrictionHelperTest extends TypeTestCase
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $displayMode = RestrictionHelper::MODE_REMOVE;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $restrictedFields = [
|
||||
'monitored_email' => [
|
||||
'EmailBundle_bounces',
|
||||
'EmailBundle_unsubscribes' => [
|
||||
'address',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
private $forms = [
|
||||
'emailconfig' => [
|
||||
'bundle' => 'EmailBundle',
|
||||
'formAlias' => 'emailconfig',
|
||||
'formType' => EmailConfigType::class,
|
||||
'formTheme' => 'MauticEmailBundle:FormTheme\\Config',
|
||||
'parameters' => [
|
||||
'mailer_from_name' => 'Mautic',
|
||||
'mailer_from_email' => 'email@yoursite.com',
|
||||
'mailer_return_path' => null,
|
||||
'mailer_transport' => 'mail',
|
||||
'mailer_append_tracking_pixel' => true,
|
||||
'mailer_convert_embed_images' => false,
|
||||
'mailer_dsn' => 'smtp://null:25',
|
||||
'messenger_dsn_email' => 'doctrine://default',
|
||||
'messenger_retry_strategy_max_retries' => 3,
|
||||
'messenger_retry_strategy_delay' => 1000,
|
||||
'messenger_retry_strategy_multiplier' => 2,
|
||||
'messenger_retry_strategy_max_delay' => 0,
|
||||
'unsubscribe_text' => null,
|
||||
'webview_text' => null,
|
||||
'unsubscribe_message' => null,
|
||||
'resubscribe_message' => null,
|
||||
'monitored_email' => [
|
||||
'general' => [
|
||||
'address' => null,
|
||||
'host' => null,
|
||||
'port' => '993',
|
||||
'encryption' => '/ssl',
|
||||
'user' => null,
|
||||
'password' => null,
|
||||
],
|
||||
'EmailBundle_bounces' => [
|
||||
'address' => null,
|
||||
'host' => null,
|
||||
'port' => '993',
|
||||
'encryption' => '/ssl',
|
||||
'user' => null,
|
||||
'password' => null,
|
||||
'override_settings' => 0,
|
||||
'folder' => null,
|
||||
],
|
||||
'EmailBundle_unsubscribes' => [
|
||||
'address' => null,
|
||||
'host' => null,
|
||||
'port' => '993',
|
||||
'encryption' => '/ssl',
|
||||
'user' => null,
|
||||
'password' => null,
|
||||
'override_settings' => 0,
|
||||
'folder' => null,
|
||||
],
|
||||
'EmailBundle_replies' => [
|
||||
'address' => null,
|
||||
'host' => null,
|
||||
'port' => '993',
|
||||
'encryption' => '/ssl',
|
||||
'user' => null,
|
||||
'password' => null,
|
||||
'override_settings' => 0,
|
||||
'folder' => null,
|
||||
],
|
||||
],
|
||||
'mailer_is_owner' => false,
|
||||
'default_signature_text' => null,
|
||||
'email_frequency_number' => null,
|
||||
'email_frequency_time' => null,
|
||||
'show_contact_preferences' => false,
|
||||
'show_contact_frequency' => false,
|
||||
'show_contact_pause_dates' => false,
|
||||
'show_contact_preferred_channels' => false,
|
||||
'show_contact_categories' => false,
|
||||
'show_contact_segments' => false,
|
||||
'mailer_mailjet_sandbox' => false,
|
||||
'mailer_mailjet_sandbox_default_mail' => null,
|
||||
'disable_trackable_urls' => false,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Test that the restricted fields are removed from the config')]
|
||||
public function testRestrictedFieldsAreRemoved(): void
|
||||
{
|
||||
$form = $this->factory->create(ConfigType::class, $this->forms);
|
||||
|
||||
$this->assertTrue($form->has('emailconfig'));
|
||||
|
||||
$emailConfig = $form->get('emailconfig');
|
||||
|
||||
// monitored_email is partially restricted so should be included
|
||||
$this->assertTrue($emailConfig->has('monitored_email'));
|
||||
|
||||
$monitoredEmail = $emailConfig->get('monitored_email');
|
||||
|
||||
// EmailBundle_bounces is restricted in entirety and thus should not be included
|
||||
$this->assertFalse($monitoredEmail->has('EmailBundle_bounces'));
|
||||
|
||||
// EmailBundle_unsubscribes is partially restricted so should be included
|
||||
$this->assertTrue($monitoredEmail->has('EmailBundle_unsubscribes'));
|
||||
|
||||
$unsubscribes = $monitoredEmail->get('EmailBundle_unsubscribes');
|
||||
|
||||
// address under EmailBundle_unsubscribes is restricted so should not be included
|
||||
$this->assertFalse($unsubscribes->has('address'));
|
||||
|
||||
// host under EmailBundle_unsubscribes is not restricted so should be included
|
||||
$this->assertTrue($unsubscribes->has('host'));
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Test that the restricted fields are masked')]
|
||||
public function testRestrictedFieldsAreMasked(): void
|
||||
{
|
||||
$this->displayMode = RestrictionHelper::MODE_MASK;
|
||||
|
||||
// Rebuild factory to get updated RestrictionHelper
|
||||
$this->factory = Forms::createFormFactoryBuilder()
|
||||
->addExtensions($this->getExtensions())
|
||||
->getFormFactory();
|
||||
|
||||
$form = $this->factory->create(ConfigType::class, $this->forms);
|
||||
/** @var FormInterface<mixed> $address */
|
||||
$address = $form['emailconfig']['monitored_email']['EmailBundle_unsubscribes']['address'];
|
||||
|
||||
$this->assertTrue($address->getConfig()->getOption('attr')['readonly']);
|
||||
$this->assertTrue($address->getConfig()->getOption('disabled'));
|
||||
$this->assertEquals(
|
||||
[
|
||||
'class' => 'form-control',
|
||||
'tooltip' => 'mautic.email.config.monitored_email_address.tooltip',
|
||||
'data-show-on' => '{"config_emailconfig_monitored_email_EmailBundle_unsubscribes_override_settings_1": "checked"}',
|
||||
'placeholder' => 'mautic.config.restricted',
|
||||
'readonly' => true,
|
||||
],
|
||||
$address->getConfig()->getOption('attr')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
protected function getExtensions()
|
||||
{
|
||||
$translator = $this->createMock(Translator::class);
|
||||
$translator->method('trans')
|
||||
->willReturnCallback(
|
||||
fn ($key) => $key
|
||||
);
|
||||
|
||||
$validator = $this->createMock(ValidatorInterface::class);
|
||||
$validator
|
||||
->method('validate')
|
||||
->willReturn(new ConstraintViolationList());
|
||||
$validator
|
||||
->method('getMetadataFor')
|
||||
->willReturn(new ClassMetadata(Form::class));
|
||||
|
||||
$imapHelper = $this->createMock(Mailbox::class);
|
||||
|
||||
// Register monitored email listeners
|
||||
$dispatcher = new EventDispatcher();
|
||||
$bouncer = $this->createMock(Bounce::class);
|
||||
$dispatcher->addSubscriber(new ProcessBounceSubscriber($bouncer));
|
||||
|
||||
$unsubscriber = $this->createMock(Unsubscribe::class);
|
||||
$looper = $this->createMock(FeedbackLoop::class);
|
||||
$dispatcher->addSubscriber(new ProcessUnsubscribeSubscriber($unsubscriber, $looper));
|
||||
|
||||
// This is what we're really testing here
|
||||
$restrictionHelper = new RestrictionHelper($translator, $this->restrictedFields, $this->displayMode);
|
||||
$escapeTransformer = new EscapeTransformer([]);
|
||||
|
||||
return [
|
||||
// register the type instances with the PreloadedExtension
|
||||
new PreloadedExtension(
|
||||
[
|
||||
new TextType(),
|
||||
new ChoiceType(),
|
||||
new YesNoButtonGroupType(),
|
||||
new PasswordType(),
|
||||
new StandAloneButtonType(),
|
||||
new NumberType(),
|
||||
new FormButtonsType(),
|
||||
new ButtonGroupType(),
|
||||
new EmailConfigType($translator),
|
||||
new DsnType($this->createMock(DsnTransformerFactory::class), $this->createMock(CoreParametersHelper::class)),
|
||||
new ConfigMonitoredEmailType($dispatcher),
|
||||
new ConfigMonitoredMailboxesType($imapHelper),
|
||||
new ConfigType($restrictionHelper, $escapeTransformer),
|
||||
],
|
||||
[]
|
||||
),
|
||||
new ValidatorExtension($validator),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ConfigBundle\Tests\Mapper;
|
||||
|
||||
use Mautic\ConfigBundle\Exception\BadFormConfigException;
|
||||
use Mautic\ConfigBundle\Mapper\ConfigMapper;
|
||||
use Mautic\CoreBundle\Helper\CoreParametersHelper;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\CoversClass(BadFormConfigException::class)]
|
||||
#[\PHPUnit\Framework\Attributes\CoversClass(ConfigMapper::class)]
|
||||
class ConfigMapperTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
private $forms = [
|
||||
'emailconfig' => [
|
||||
'bundle' => 'EmailBundle',
|
||||
'formAlias' => 'emailconfig',
|
||||
'formTheme' => 'MauticEmailBundle:FormTheme\\Config',
|
||||
'parameters' => [
|
||||
'mailer_from_name' => 'Mautic',
|
||||
'mailer_from_email' => 'email@yoursite.com',
|
||||
'mailer_return_path' => null,
|
||||
'mailer_transport' => 'mail',
|
||||
'mailer_append_tracking_pixel' => true,
|
||||
'mailer_convert_embed_images' => false,
|
||||
'mailer_dsn' => 'smtp://null:25',
|
||||
'messenger_dsn_email' => 'doctrine://default',
|
||||
'messenger_retry_strategy_max_retries' => 3,
|
||||
'messenger_retry_strategy_delay' => 1000,
|
||||
'messenger_retry_strategy_multiplier' => 2,
|
||||
'messenger_retry_strategy_max_delay' => 0,
|
||||
'unsubscribe_text' => null,
|
||||
'webview_text' => null,
|
||||
'unsubscribe_message' => null,
|
||||
'resubscribe_message' => null,
|
||||
'monitored_email' => [
|
||||
'general' => [
|
||||
'address' => null,
|
||||
'host' => null,
|
||||
'port' => '993',
|
||||
'encryption' => '/ssl',
|
||||
'user' => null,
|
||||
'password' => null,
|
||||
],
|
||||
'EmailBundle_bounces' => [
|
||||
'address' => null,
|
||||
'host' => null,
|
||||
'port' => '993',
|
||||
'encryption' => '/ssl',
|
||||
'user' => null,
|
||||
'password' => null,
|
||||
'override_settings' => 0,
|
||||
'folder' => null,
|
||||
],
|
||||
'EmailBundle_unsubscribes' => [
|
||||
'address' => null,
|
||||
'host' => null,
|
||||
'port' => '993',
|
||||
'encryption' => '/ssl',
|
||||
'user' => null,
|
||||
'password' => null,
|
||||
'override_settings' => 0,
|
||||
'folder' => null,
|
||||
],
|
||||
'EmailBundle_replies' => [
|
||||
'address' => null,
|
||||
'host' => null,
|
||||
'port' => '993',
|
||||
'encryption' => '/ssl',
|
||||
'user' => null,
|
||||
'password' => null,
|
||||
'override_settings' => 0,
|
||||
'folder' => null,
|
||||
],
|
||||
],
|
||||
'mailer_is_owner' => false,
|
||||
'default_signature_text' => null,
|
||||
'email_frequency_number' => null,
|
||||
'email_frequency_time' => null,
|
||||
'show_contact_preferences' => false,
|
||||
'show_contact_frequency' => false,
|
||||
'show_contact_pause_dates' => false,
|
||||
'show_contact_preferred_channels' => false,
|
||||
'show_contact_categories' => false,
|
||||
'show_contact_segments' => false,
|
||||
'mailer_mailjet_sandbox' => false,
|
||||
'mailer_mailjet_sandbox_default_mail' => null,
|
||||
'disable_trackable_urls' => false,
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
private $config = [
|
||||
'db_host' => 'dbhost',
|
||||
'db_user' => 'dbuser',
|
||||
'monitored_email' => [
|
||||
'general' => [
|
||||
'address' => 'test@test.com',
|
||||
'host' => 'test.com',
|
||||
'port' => '143',
|
||||
'encryption' => '/tls/novalidate-cert',
|
||||
'user' => 'test@test.com',
|
||||
'password' => 'password',
|
||||
],
|
||||
'EmailBundle_bounces' => [
|
||||
'address' => 'test2@test.com',
|
||||
'host' => 'test2.com',
|
||||
'port' => '143',
|
||||
'encryption' => '/tls/novalidate-cert',
|
||||
'user' => 'test2@test.com',
|
||||
'password' => 'password',
|
||||
'override_settings' => 1,
|
||||
'folder' => 'INBOX',
|
||||
],
|
||||
'EmailBundle_unsubscribes' => [
|
||||
'address' => 'test3@test.com',
|
||||
'host' => null,
|
||||
'port' => '993',
|
||||
'encryption' => '/ssl',
|
||||
'user' => null,
|
||||
'password' => null,
|
||||
'override_settings' => 0,
|
||||
'folder' => 'INBOX',
|
||||
],
|
||||
'EmailBundle_replies' => [
|
||||
'address' => 'test4@test.com',
|
||||
'host' => null,
|
||||
'port' => '993',
|
||||
'encryption' => '/ssl',
|
||||
'user' => null,
|
||||
'password' => null,
|
||||
'override_settings' => 0,
|
||||
'folder' => 'INBOX',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Exception should be thrown if parameters key is not found in a form config')]
|
||||
public function testExceptionIsThrownOnBadFormConfig(): void
|
||||
{
|
||||
$this->expectException(BadFormConfigException::class);
|
||||
|
||||
$forms = [
|
||||
'emailconfig' => [
|
||||
'bundle' => 'EmailBundle',
|
||||
'formAlias' => 'emailconfig',
|
||||
'formTheme' => 'MauticEmailBundle:FormTheme\Config',
|
||||
],
|
||||
];
|
||||
|
||||
$parameterHelper = $this->createMock(CoreParametersHelper::class);
|
||||
|
||||
$mapper = new ConfigMapper($parameterHelper, []);
|
||||
|
||||
$mapper->bindFormConfigsWithRealValues($forms);
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Defaults should be bound when local config has no values')]
|
||||
public function testParametersAreBoundToDefaults(): void
|
||||
{
|
||||
$parameterHelper = $this->createMock(CoreParametersHelper::class);
|
||||
|
||||
$mapper = new ConfigMapper($parameterHelper, []);
|
||||
|
||||
$processedForms = $mapper->bindFormConfigsWithRealValues($this->forms);
|
||||
|
||||
$this->assertEquals($this->forms, $processedForms);
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Defaults should be merged with local config values')]
|
||||
public function testParametersAreBoundToDefaultsWithLocalConfig(): void
|
||||
{
|
||||
$parameterHelper = $this->createMock(CoreParametersHelper::class);
|
||||
|
||||
$parameterHelper->method('get')
|
||||
->willReturnCallback(
|
||||
fn ($param, $defaultValue) => array_key_exists($param, $this->config) ? $this->config[$param] : $defaultValue
|
||||
);
|
||||
|
||||
$mapper = new ConfigMapper($parameterHelper, []);
|
||||
|
||||
$forms = $this->forms;
|
||||
$processedForms = $mapper->bindFormConfigsWithRealValues($forms);
|
||||
|
||||
// Update expected
|
||||
$forms['emailconfig']['parameters']['monitored_email'] = [
|
||||
'general' => [
|
||||
'address' => 'test@test.com',
|
||||
'host' => 'test.com',
|
||||
'port' => '143',
|
||||
'encryption' => '/tls/novalidate-cert',
|
||||
'user' => 'test@test.com',
|
||||
'password' => 'password',
|
||||
],
|
||||
'EmailBundle_bounces' => [
|
||||
'address' => 'test2@test.com',
|
||||
'host' => 'test2.com',
|
||||
'port' => '143',
|
||||
'encryption' => '/tls/novalidate-cert',
|
||||
'user' => 'test2@test.com',
|
||||
'password' => 'password',
|
||||
'override_settings' => 1,
|
||||
'folder' => 'INBOX',
|
||||
],
|
||||
'EmailBundle_unsubscribes' => [
|
||||
'address' => 'test3@test.com',
|
||||
'host' => null,
|
||||
'port' => '993',
|
||||
'encryption' => '/ssl',
|
||||
'user' => null,
|
||||
'password' => null,
|
||||
'override_settings' => 0,
|
||||
'folder' => 'INBOX',
|
||||
],
|
||||
'EmailBundle_replies' => [
|
||||
'address' => 'test4@test.com',
|
||||
'host' => null,
|
||||
'port' => '993',
|
||||
'encryption' => '/ssl',
|
||||
'user' => null,
|
||||
'password' => null,
|
||||
'override_settings' => 0,
|
||||
'folder' => 'INBOX',
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertEquals($forms, $processedForms);
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Defaults should be merged with local config values but restricted fields should be removed')]
|
||||
public function testParametersAreBoundToDefaultsWithLocalConfigAndRestrictionsAppied(): void
|
||||
{
|
||||
$parameterHelper = $this->createMock(CoreParametersHelper::class);
|
||||
|
||||
$parameterHelper->method('get')
|
||||
->willReturnCallback(
|
||||
fn ($param, $defaultValue) => array_key_exists($param, $this->config) ? $this->config[$param] : $defaultValue
|
||||
);
|
||||
|
||||
$mapper = new ConfigMapper($parameterHelper, ['monitored_email']);
|
||||
|
||||
$forms = $this->forms;
|
||||
$processedForms = $mapper->bindFormConfigsWithRealValues($forms);
|
||||
|
||||
// Expected should have had monitored_email unset due to it being restricted
|
||||
unset($forms['emailconfig']['parameters']['monitored_email']);
|
||||
|
||||
$this->assertEquals($forms, $processedForms);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ConfigBundle\Tests\Mapper\Helper;
|
||||
|
||||
use Mautic\ConfigBundle\Mapper\Helper\ConfigHelper;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\CoversClass(ConfigHelper::class)]
|
||||
class ConfigHelperTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Ensure a mixed numeric/string keyed array is formatted to all string based keys')]
|
||||
public function testNestedLocalParametersAreBoundCorrectly(): void
|
||||
{
|
||||
$defaults = [
|
||||
'db_host' => null,
|
||||
'db_user' => null,
|
||||
'api_enabled' => 1,
|
||||
'monitored_email' => [
|
||||
'general' => [
|
||||
'address' => null,
|
||||
'host' => null,
|
||||
'port' => '993',
|
||||
'encryption' => '/ssl',
|
||||
'user' => null,
|
||||
'password' => null,
|
||||
],
|
||||
'EmailBundle_bounces' => [
|
||||
'address' => 'test2@test.com',
|
||||
'host' => 'test2.com',
|
||||
'port' => '143',
|
||||
'encryption' => '/tls/novalidate-cert',
|
||||
'user' => 'test2@test.com',
|
||||
'password' => 'password',
|
||||
'override_settings' => 1,
|
||||
'folder' => 'INBOX',
|
||||
],
|
||||
'EmailBundle_unsubscribes' => [
|
||||
'address' => 'test3@test.com',
|
||||
'host' => null,
|
||||
'port' => '993',
|
||||
'encryption' => '/ssl',
|
||||
'user' => null,
|
||||
'password' => null,
|
||||
'override_settings' => 0,
|
||||
'folder' => 'INBOX',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$config = [
|
||||
'db_host' => 'dbhost',
|
||||
'db_user' => 'dbuser',
|
||||
'monitored_email' => [
|
||||
'general' => [
|
||||
'address' => 'test@test.com',
|
||||
'host' => 'test.com',
|
||||
'port' => '143',
|
||||
'encryption' => '/tls/novalidate-cert',
|
||||
'user' => 'test@test.com',
|
||||
'password' => 'password',
|
||||
],
|
||||
'EmailBundle_bounces' => null,
|
||||
'EmailBundle_unsubscribes' => [
|
||||
'address' => 'test3@test.com',
|
||||
'host' => null,
|
||||
'port' => '993',
|
||||
'encryption' => '/ssl',
|
||||
'user' => null,
|
||||
'password' => null,
|
||||
'override_settings' => 0,
|
||||
'folder' => 'INBOX',
|
||||
],
|
||||
'EmailBundle_replies' => [
|
||||
'address' => 'test4@test.com',
|
||||
'host' => null,
|
||||
'port' => '993',
|
||||
'encryption' => '/ssl',
|
||||
'user' => null,
|
||||
'password' => null,
|
||||
'override_settings' => 0,
|
||||
'folder' => 'INBOX',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$expected = [
|
||||
// from config
|
||||
'db_host' => 'dbhost',
|
||||
'db_user' => 'dbuser',
|
||||
// from defaults
|
||||
'api_enabled' => 1,
|
||||
'monitored_email' => [
|
||||
// from config
|
||||
'general' => [
|
||||
'address' => 'test@test.com',
|
||||
'host' => 'test.com',
|
||||
'port' => '143',
|
||||
'encryption' => '/tls/novalidate-cert',
|
||||
'user' => 'test@test.com',
|
||||
'password' => 'password',
|
||||
],
|
||||
'EmailBundle_bounces' => [
|
||||
// from defaults
|
||||
'address' => 'test2@test.com',
|
||||
'host' => 'test2.com',
|
||||
'port' => '143',
|
||||
'encryption' => '/tls/novalidate-cert',
|
||||
'user' => 'test2@test.com',
|
||||
'password' => 'password',
|
||||
'override_settings' => 1,
|
||||
'folder' => 'INBOX',
|
||||
],
|
||||
// from config
|
||||
'EmailBundle_unsubscribes' => [
|
||||
'address' => 'test3@test.com',
|
||||
'host' => null,
|
||||
'port' => '993',
|
||||
'encryption' => '/ssl',
|
||||
'user' => null,
|
||||
'password' => null,
|
||||
'override_settings' => 0,
|
||||
'folder' => 'INBOX',
|
||||
],
|
||||
// from config
|
||||
'EmailBundle_replies' => [
|
||||
'address' => 'test4@test.com',
|
||||
'host' => null,
|
||||
'port' => '993',
|
||||
'encryption' => '/ssl',
|
||||
'user' => null,
|
||||
'password' => null,
|
||||
'override_settings' => 0,
|
||||
'folder' => 'INBOX',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, ConfigHelper::bindNestedConfigValues($config, $defaults));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ConfigBundle\Tests\Mapper\Helper;
|
||||
|
||||
use Mautic\ConfigBundle\Mapper\Helper\RestrictionHelper;
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\CoversClass(RestrictionHelper::class)]
|
||||
class RestrictionHelperTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $restrictedFields = [
|
||||
'db_host',
|
||||
'db_user',
|
||||
'monitored_email' => [
|
||||
'EmailBundle_bounces',
|
||||
'EmailBundle_unsubscribes' => [
|
||||
'address',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Ensure a mixed numeric/string keyed array is formatted to all string based keys')]
|
||||
public function testRestrictedConfigArrayIsFormattedCorrectly(): void
|
||||
{
|
||||
$expected = [
|
||||
'db_host' => 'db_host',
|
||||
'db_user' => 'db_user',
|
||||
'monitored_email' => [
|
||||
'EmailBundle_bounces' => 'EmailBundle_bounces',
|
||||
'EmailBundle_unsubscribes' => [
|
||||
'address' => 'address',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertEquals($expected, RestrictionHelper::prepareRestrictions($this->restrictedFields));
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\TestDox('Ensure a restrictions are recursively applied')]
|
||||
public function testApplyingRestrictionsToConfigArray(): void
|
||||
{
|
||||
$config = [
|
||||
'db_host' => 'dbhost',
|
||||
'db_user' => 'dbuser',
|
||||
'api_enabled' => 1,
|
||||
'monitored_email' => [
|
||||
'general' => [
|
||||
'address' => 'test@test.com',
|
||||
'host' => 'test.com',
|
||||
'port' => '143',
|
||||
'encryption' => '/tls/novalidate-cert',
|
||||
'user' => 'test@test.com',
|
||||
'password' => 'password',
|
||||
],
|
||||
'EmailBundle_bounces' => [
|
||||
'address' => '',
|
||||
'host' => '',
|
||||
'port' => '993',
|
||||
'encryption' => '/ssl',
|
||||
'user' => '',
|
||||
'password' => '',
|
||||
'override_settings' => 0,
|
||||
'folder' => 'INBOX',
|
||||
],
|
||||
'EmailBundle_unsubscribes' => [
|
||||
'address' => null,
|
||||
'host' => null,
|
||||
'port' => '993',
|
||||
'encryption' => '/ssl',
|
||||
'user' => null,
|
||||
'password' => null,
|
||||
'override_settings' => 0,
|
||||
'folder' => 'INBOX',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$expected = [
|
||||
'api_enabled' => 1,
|
||||
'monitored_email' => [
|
||||
'general' => [
|
||||
'address' => 'test@test.com',
|
||||
'host' => 'test.com',
|
||||
'port' => '143',
|
||||
'encryption' => '/tls/novalidate-cert',
|
||||
'user' => 'test@test.com',
|
||||
'password' => 'password',
|
||||
],
|
||||
'EmailBundle_unsubscribes' => [
|
||||
'host' => null,
|
||||
'port' => '993',
|
||||
'encryption' => '/ssl',
|
||||
'user' => null,
|
||||
'password' => null,
|
||||
'override_settings' => 0,
|
||||
'folder' => 'INBOX',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$restrictedFields = RestrictionHelper::prepareRestrictions($this->restrictedFields);
|
||||
$this->assertEquals($expected, RestrictionHelper::applyRestrictions($config, $restrictedFields));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<?php
|
||||
|
||||
namespace Mautic\ConfigBundle\Tests\Service;
|
||||
|
||||
use Mautic\ConfigBundle\Service\ConfigChangeLogger;
|
||||
use Mautic\CoreBundle\Helper\IpLookupHelper;
|
||||
use Mautic\CoreBundle\Model\AuditLogModel;
|
||||
|
||||
class ConfigChangeLoggerTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
public function testSetOriginalNormData(): void
|
||||
{
|
||||
$ipLookupHelper = $this->createMock(IpLookupHelper::class);
|
||||
$auditLogModel = $this->createMock(AuditLogModel::class);
|
||||
$logger = new ConfigChangeLogger($ipLookupHelper, $auditLogModel);
|
||||
|
||||
$this->assertEquals($logger, $logger->setOriginalNormData([]));
|
||||
}
|
||||
|
||||
public function testOriginalNormDataExpected(): void
|
||||
{
|
||||
$this->expectException(\RuntimeException::class);
|
||||
|
||||
$ipLookupHelper = $this->createMock(IpLookupHelper::class);
|
||||
$ipLookupHelper->expects($this->never())->method('getIpAddressFromRequest');
|
||||
$auditLogModel = $this->createMock(AuditLogModel::class);
|
||||
$auditLogModel->expects($this->never())->method('writeToLog');
|
||||
$logger = new ConfigChangeLogger($ipLookupHelper, $auditLogModel);
|
||||
$logger->log([]);
|
||||
}
|
||||
|
||||
public function testNothingToLog(): void
|
||||
{
|
||||
$ipLookupHelper = $this->createMock(IpLookupHelper::class);
|
||||
$ipLookupHelper->expects($this->never())->method('getIpAddressFromRequest');
|
||||
$auditLogModel = $this->createMock(AuditLogModel::class);
|
||||
$auditLogModel->expects($this->never())->method('writeToLog');
|
||||
$logger = new ConfigChangeLogger($ipLookupHelper, $auditLogModel);
|
||||
|
||||
$originalData = $postData = [
|
||||
'bundle' => [
|
||||
'key' => 'value',
|
||||
],
|
||||
];
|
||||
|
||||
$this->assertEquals($logger, $logger->setOriginalNormData($originalData));
|
||||
$logger->log($postData);
|
||||
}
|
||||
|
||||
public function testLog(): void
|
||||
{
|
||||
$change = [
|
||||
'key2' => 'changedValue',
|
||||
];
|
||||
|
||||
$filterMe = [
|
||||
'transifex_password' => 'dhjsakjfda',
|
||||
'mailer_is_owner' => 'lksajhd',
|
||||
];
|
||||
|
||||
$log = [
|
||||
'bundle' => 'config',
|
||||
'object' => 'config',
|
||||
'objectId' => 0,
|
||||
'action' => 'update',
|
||||
'details' => $change,
|
||||
'ipAddress' => null,
|
||||
];
|
||||
|
||||
$ipLookupHelper = $this->createMock(IpLookupHelper::class);
|
||||
$ipLookupHelper->expects($this->once())->method('getIpAddressFromRequest');
|
||||
$auditLogModel = $this->createMock(AuditLogModel::class);
|
||||
$auditLogModel->expects($this->once())->method('writeToLog')->with($log);
|
||||
$logger = new ConfigChangeLogger($ipLookupHelper, $auditLogModel);
|
||||
|
||||
$originalData = [
|
||||
'bundle' => [
|
||||
'key' => 'value',
|
||||
],
|
||||
'bundle2' => [
|
||||
'parameters' => [
|
||||
'key2' => 'value2',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
$postData = [
|
||||
'bundle' => [
|
||||
'key' => 'value',
|
||||
],
|
||||
'bundle2' => array_merge($change, $filterMe),
|
||||
];
|
||||
|
||||
$this->assertEquals($logger, $logger->setOriginalNormData($originalData));
|
||||
$logger->log($postData);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
mautic.config.config.error.not.updated="Could not save updated configuration: %exception%"
|
||||
mautic.config.config.notice.updated="Configuration successfully updated"
|
||||
@@ -0,0 +1,37 @@
|
||||
mautic.config.header.index="Configuration"
|
||||
mautic.config.menu.index="Configuration"
|
||||
mautic.config.restricted="Set by system"
|
||||
mautic.config.notwritable="The configuration file is not writable! Changes will not be saved."
|
||||
mautic.config.remove_file_contents="Remove stored contents for this setting?"
|
||||
mautic.sysinfo.header.index="System Info"
|
||||
mautic.sysinfo.menu.index="System Info"
|
||||
mautic.sysinfo.tab.phpinfo="PHP Info"
|
||||
mautic.sysinfo.tab.recommendations="Recommendations"
|
||||
mautic.sysinfo.no.recommendations="There are no recommendations for you right now. Your server is configured properly!"
|
||||
mautic.sysinfo.tab.folders="Folder & File Permissions"
|
||||
mautic.sysinfo.folders.title="The following folders and files must be writable for Mautic to work correctly."
|
||||
mautic.sysinfo.folder.path="Folder/File Path"
|
||||
mautic.sysinfo.is.writable="Is writable"
|
||||
mautic.sysinfo.writable="Writable"
|
||||
mautic.sysinfo.unwritable="Unwritable"
|
||||
mautic.sysinfo.tab.log="Log"
|
||||
mautic.sysinfo.log.title="Current Error Log"
|
||||
mautic.sysinfo.log.missing="Today's Mautic error log is empty. Check server error log for error messages Mautic didn't have a chance to catch."
|
||||
mautic.sysinfo.phpinfo.missing="Information is not available. PHP function phpinfo() is disabled on your server."
|
||||
mautic.sysinfo.phpinfo.phpversion="PHP function phpinfo() is disabled on your server. Your PHP version is <b>%phpversion%</b>."
|
||||
mautic.sysinfo.tab.dbinfo="Database info"
|
||||
mautic.sysinfo.dbinfo.title="Database info"
|
||||
mautic.sysinfo.dbinfo.property="Property"
|
||||
mautic.sysinfo.dbinfo.value="Value"
|
||||
mautic.sysinfo.dbinfo.version="Version"
|
||||
mautic.sysinfo.dbinfo.driver="Doctrine driver"
|
||||
mautic.sysinfo.dbinfo.platform="Doctrine database platform (automatically detected)"
|
||||
mautic.config.dsn.scheme="Scheme"
|
||||
mautic.config.dsn.host="Host"
|
||||
mautic.config.dsn.port="Port"
|
||||
mautic.config.dsn.user="User"
|
||||
mautic.config.dsn.password="Password"
|
||||
mautic.config.dsn.path="Path"
|
||||
mautic.config.dsn.options="Options"
|
||||
mautic.config.dsn.using_current_dsn="Using currently saved DSN"
|
||||
mautic.config.dsn.save_to_test="Save changes to test the DSN."
|
||||
Reference in New Issue
Block a user