Initial commit: CloudOps infrastructure platform

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

View File

@@ -0,0 +1,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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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