Initial commit: CloudOps infrastructure platform
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user