Skip to content

Commit 959e8e2

Browse files
committed
Merge branch '7.1' into 7.1.1-to-7.x
2 parents 122ace2 + 8299627 commit 959e8e2

14 files changed

Lines changed: 684 additions & 20 deletions

File tree

app/bundles/AssetBundle/Controller/AssetController.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,12 @@ public function indexAction(Request $request, CoreParametersHelper $parametersHe
4040

4141
$this->setListFilters();
4242

43-
$limit = $request->getSession()->get('mautic.asset.limit', $parametersHelper->get('default_assetlimit'));
43+
// Remove the "default_assetlimit" in Mautic 8.
44+
$limit = $request->getSession()->get(
45+
'mautic.asset.limit',
46+
$parametersHelper->get('default_assetlimit', $parametersHelper->get('default_pagelimit'))
47+
);
48+
4449
$start = (1 === $page) ? 0 : (($page - 1) * $limit);
4550
if ($start < 0) {
4651
$start = 0;
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
<div>
2-
Hello
3-
{{- include('@MauticAsset/Asset/preview.html.twig', {'activeAsset' : event.extra.asset, 'assetDownloadUrl' : url(
2+
{{- include('@MauticAsset/Modules/preview.html.twig', {'activeAsset' : event.extra.asset, 'assetDownloadUrl' : url(
43
'mautic_asset_action',
54
{'objectAction' : 'preview', 'objectId' : event.extra.asset.getId()}
65
)}) -}}
7-
</div>
6+
</div>

app/bundles/CoreBundle/Form/Type/ConfigType.php

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,8 +202,54 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
202202
'class' => 'form-control',
203203
'tooltip' => 'mautic.core.config.form.trusted.hosts.tooltip',
204204
],
205-
'help' => 'mautic.core.config.form.trusted_hosts.help',
206-
'required' => false,
205+
'help' => 'mautic.core.config.form.trusted_hosts.help',
206+
'required' => false,
207+
'constraints' => [
208+
new Callback(static function (?array $values, ExecutionContextInterface $context): void {
209+
if (null === $values) {
210+
return;
211+
}
212+
213+
if ([] === $values) {
214+
return;
215+
}
216+
217+
foreach ($values as $value) {
218+
// Valid domain name. @see https://stackoverflow.com/questions/10306690/what-is-a-regular-expression-which-will-match-a-valid-domain-name-without-a-subd
219+
if (0 !== preg_match('/^(((?!-))(xn--|_)?[a-z0-9-]{0,61}[a-z0-9]{1,1}\.)*(xn--)?([a-z0-9][a-z0-9\-]{0,60}|[a-z0-9-]{1,30}\.[a-z]{2,})$/', $value)) {
220+
continue;
221+
}
222+
223+
// Allowed characters are a-z, 0-9 and "-".
224+
if (0 === preg_match('/^[a-z0-9\-.]+$/i', $value)) {
225+
// Regexp given
226+
227+
// In environments, which set the `error_reporting` to ALL the code need to be executed without warnings.
228+
$regexpError = false;
229+
set_error_handler(static function (int $errorNo, string $errorString) use (&$regexpError): bool {
230+
$regexpError = str_contains($errorString, 'preg_match');
231+
232+
return true;
233+
});
234+
235+
// There is no way Mautic can validate rexep better than PCRE library.
236+
$pregMatchResult = @preg_match('/'.$value.'/', '');
237+
238+
restore_error_handler();
239+
240+
if (false === $pregMatchResult || $regexpError) {
241+
$context->buildViolation('mautic.core.config.form.trusted_hosts.invalid.regexp')->atPath('trusted_hosts')->addViolation();
242+
243+
break;
244+
}
245+
} else {
246+
$context->buildViolation('mautic.core.config.form.trusted_hosts.invalid.domain')->atPath('trusted_hosts')->addViolation();
247+
248+
break;
249+
}
250+
}
251+
}),
252+
],
207253
]
208254
)->addViewTransformer($arrayStringTransformer)
209255
);

app/bundles/CoreBundle/Tests/Unit/Form/Type/ConfigTypeTest.php

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
use Mautic\PageBundle\Entity\PageRepository;
1212
use Mautic\PageBundle\Form\Type\PageListType;
1313
use Mautic\PageBundle\Model\PageModel;
14+
use PHPUnit\Framework\Attributes\DataProvider;
1415
use Symfony\Component\Form\Extension\Validator\ValidatorExtension;
1516
use Symfony\Component\Form\FormBuilderInterface;
1617
use Symfony\Component\Form\PreloadedExtension;
@@ -31,6 +32,72 @@ protected function setUp(): void
3132
parent::setUp();
3233
}
3334

35+
public function testSubmitEmptyTrustedHosts(): void
36+
{
37+
$formData = [
38+
'site_url' => 'http://example.com',
39+
'cache_path' => 'tmp',
40+
'log_path' => '/var/log',
41+
'image_path' => 'media/images/',
42+
'cached_data_timeout' => 30000,
43+
'date_format_full' => 'F j, Y g:i:s a T',
44+
'date_format_short' => 'D, M d - g:i:s a',
45+
'date_format_dateonly' => 'F j, Y',
46+
'date_format_timeonly' => 'g:i:s a',
47+
'trusted_hosts' => '',
48+
];
49+
50+
// $formData will retrieve data from the form submission; pass it as the second argument
51+
$form = $this->factory->create(ConfigType::class, $formData);
52+
53+
// submit the data to the form directly
54+
$form->submit($formData);
55+
56+
// This check ensures there are no transformation failures
57+
$this->assertTrue($form->isSynchronized());
58+
59+
// check that $formData was modified as expected when the form was submitted
60+
$this->assertTrue($form->isValid());
61+
}
62+
63+
#[DataProvider('provideInvalidHostAndRegexp')]
64+
public function testSubmitInvalidTrustedHost(string $invalidHost): void
65+
{
66+
$formData = [
67+
'site_url' => 'http://example.com',
68+
'cache_path' => 'tmp',
69+
'log_path' => '/var/log',
70+
'image_path' => 'media/images/',
71+
'cached_data_timeout' => 30000,
72+
'date_format_full' => 'F j, Y g:i:s a T',
73+
'date_format_short' => 'D, M d - g:i:s a',
74+
'date_format_dateonly' => 'F j, Y',
75+
'date_format_timeonly' => 'g:i:s a',
76+
'trusted_hosts' => $invalidHost,
77+
];
78+
79+
// $formData will retrieve data from the form submission; pass it as the second argument
80+
$form = $this->factory->create(ConfigType::class, $formData);
81+
82+
// submit the data to the form directly
83+
$form->submit($formData);
84+
85+
// This check ensures there are no transformation failures
86+
$this->assertTrue($form->isSynchronized());
87+
88+
// check that $formData was modified as expected when the form was submitted
89+
$this->assertFalse($form->isValid());
90+
}
91+
92+
public static function provideInvalidHostAndRegexp(): \Generator
93+
{
94+
yield 'trusted..com' => ['trusted..com']; // Invalid host.
95+
yield 'trusted.com' => ['trusted.com/']; // Host with trailing slash
96+
yield 'trusted.com\\' => ['trusted.com\\']; // Host with trailing backslash
97+
yield '[trusted.com' => ['[trusted.com']; // Invalid regexp #1
98+
yield 'trusted(.com' => ['trusted(.com']; // Invalid regexp #2
99+
}
100+
34101
public function testSubmitValidData(): void
35102
{
36103
$formData = [
@@ -43,6 +110,7 @@ public function testSubmitValidData(): void
43110
'date_format_short' => 'D, M d - g:i:s a',
44111
'date_format_dateonly' => 'F j, Y',
45112
'date_format_timeonly' => 'g:i:s a',
113+
'trusted_hosts' => '.*\.?trusted.com$,trusted.com,example.com, example.?om,sub1.sub2.sub3.example.com',
46114
];
47115

48116
// $formData will retrieve data from the form submission; pass it as the second argument

app/bundles/CoreBundle/Translations/en_US/messages.ini

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,8 @@ mautic.core.config.form.site.url.tooltip="Set the URL to this site here. This is
196196
mautic.core.config.form.theme="Default landing page and form theme"
197197
mautic.core.config.form.theme.tooltip="Set the default theme used by the system for rendering landing pages and forms. HINT: Changing this will only affect new pages and won't change existing pages."
198198
mautic.core.config.form.trusted.hosts="Trusted hosts"
199-
mautic.core.config.form.trusted.hosts.tooltip="Explicitely whitelist the hosts allowed to send requests to Mautic. You can use regex and separate multiple hosts with a comma. I.e. .*\.?trusted.com$ If left empty, Mautic will respond to all hosts."
200-
mautic.core.config.form.trusted_hosts.help="Enter the domain name where your Mautic is installed (e.g., mautic.yourdomain.com)."
199+
mautic.core.config.form.trusted.hosts.tooltip="Whitelist hosts allowed to send requests to Mautic (comma-separated, regex will be enclosed with '/', i.e. /[your regexp]/ will be /.*\.?trusted.com$/). Leave empty to allow all hosts."
200+
mautic.core.config.form.trusted_hosts.help="Enter the domain name where your Mautic is installed (e.g., mautic.yourdomain.com). CAUTION: setting this incorrectly can prevent access."
201201
mautic.core.config.form.trusted.proxies="Trusted proxies"
202202
mautic.core.config.form.trusted.proxies.tooltip="Configures the IP addresses that should be trusted as proxies. This setting is mandatory when using Mautic behind an SSL terminating proxy. Separate multiple IP addresses by a comma. i.e 127.0.0.1, 10.0.0.0/8, fc00::/7"
203203
mautic.core.config.form.brand_name="Brand name"

app/bundles/CoreBundle/Translations/en_US/validators.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,5 @@ mautic.core.config.form.image.path.invalid="The image path is invalid."
2424
mautic.core.config.form.image.path.invalid.not.media="The image path is invalid. It must begin with media/."
2525
mautic.core.config.form.image.path.invalid.folder.not.exists="The image path is invalid. The folder does not exist."
2626
mautic.core.remote_url_not_allowed="The remote domain in the URL is not allowed due to security reasons. To allow the given domain, add that to the list located at \"Configuration\" -> \"System Settings\" -> \"Miscellaneous Settings\" -> \"Allowed remote domains\"."
27+
mautic.core.config.form.trusted_hosts.invalid.regexp="The trusted hosts field contains invalid regexp."
28+
mautic.core.config.form.trusted_hosts.invalid.domain="The trusted hosts field contains invalid domain."

app/bundles/LeadBundle/Entity/OperatorListTrait.php

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@ trait OperatorListTrait
3535
OperatorOptions::NOT_REGEXP,
3636
OperatorOptions::INCLUDING_ANY,
3737
OperatorOptions::EXCLUDING_ANY,
38-
OperatorOptions::INCLUDING_ALL,
39-
OperatorOptions::EXCLUDING_ALL,
4038
],
4139
],
4240
'bool' => [

app/bundles/LeadBundle/EventListener/SegmentOperatorQuerySubscriber.php

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,17 +72,36 @@ public function onNegativeOperators(SegmentOperatorQueryBuilderEvent $event): vo
7272
'neq',
7373
'notLike',
7474
'notBetween', // Used only for date with week combination (NOT EQUAL [this week, next week, last week])
75-
'notIn'
75+
'notIn',
76+
OperatorOptions::EXCLUDING_ALL, // For non-multiselect fields (e.g. select/country), treat as notIn
7677
)) {
7778
return;
7879
}
7980

81+
// Handle excluding_all for non-multiselect fields (e.g. select/country)
82+
if ($event->operatorIsOneOf(OperatorOptions::EXCLUDING_ALL)) {
83+
$parameterHolder = $event->getParameterHolder();
84+
85+
// For single-select fields, "excluding all of [A, B, ...]" with multiple values is always true
86+
// because a single-select field cannot contain all values simultaneously.
87+
// If only one value is selected, fall through and treat as notIn.
88+
if (is_array($parameterHolder) && count($parameterHolder) > 1) {
89+
$event->addExpression('1 = 1');
90+
$event->stopPropagation();
91+
92+
return;
93+
}
94+
}
95+
8096
$leadsTableAlias = $event->getLeadsTableAlias();
8197

98+
// Treat EXCLUDING_ALL with a single value as notIn
99+
$operator = $event->operatorIsOneOf(OperatorOptions::EXCLUDING_ALL) ? 'notIn' : $event->getFilter()->getOperator();
100+
82101
$event->addExpression(
83102
$event->getQueryBuilder()->expr()->or(
84103
$event->getQueryBuilder()->expr()->isNull($leadsTableAlias.'.'.$event->getFilter()->getField()),
85-
$event->getQueryBuilder()->expr()->{$event->getFilter()->getOperator()}(
104+
$event->getQueryBuilder()->expr()->{$operator}(
86105
$leadsTableAlias.'.'.$event->getFilter()->getField(),
87106
$event->getParameterHolder()
88107
)
@@ -163,15 +182,34 @@ public function onDefaultOperators(SegmentOperatorQueryBuilderEvent $event): voi
163182
'in',
164183
'between', // Used only for date with week combination (EQUAL [this week, next week, last week])
165184
'regexp',
166-
'notRegexp' // Different behaviour from 'notLike' because of BC (do not use condition for NULL). Could be changed in Mautic 3.
185+
'notRegexp', // Different behaviour from 'notLike' because of BC (do not use condition for NULL). Could be changed in Mautic 3.
186+
OperatorOptions::INCLUDING_ALL, // For non-multiselect fields (e.g. select/country), treat as in
167187
)) {
168188
return;
169189
}
170190

191+
// Handle including_all for non-multiselect fields (e.g. select/country)
192+
if ($event->operatorIsOneOf(OperatorOptions::INCLUDING_ALL)) {
193+
$parameterHolder = $event->getParameterHolder();
194+
195+
// For single-select fields, "including all of [A, B, ...]" with multiple values is always false
196+
// because a single-select field cannot contain all values simultaneously.
197+
// If only one value is selected, treat as in.
198+
if (is_array($parameterHolder) && count($parameterHolder) > 1) {
199+
// Always false - no contact can have all values in a single-select field
200+
$event->addExpression('1 = 0');
201+
$event->stopPropagation();
202+
203+
return;
204+
}
205+
}
206+
171207
$leadsTableAlias = $event->getLeadsTableAlias();
172208

209+
$operator = $event->operatorIsOneOf(OperatorOptions::INCLUDING_ALL) ? 'in' : $event->getFilter()->getOperator();
210+
173211
$event->addExpression(
174-
$event->getQueryBuilder()->expr()->{$event->getFilter()->getOperator()}(
212+
$event->getQueryBuilder()->expr()->{$operator}(
175213
$leadsTableAlias.'.'.$event->getFilter()->getField(),
176214
$event->getParameterHolder()
177215
)

app/bundles/LeadBundle/Form/Type/FilterType.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Mautic\LeadBundle\Model\ListModel;
66
use Mautic\LeadBundle\Provider\FormAdjustmentsProviderInterface;
7+
use Mautic\LeadBundle\Segment\OperatorOptions;
78
use Symfony\Component\Form\AbstractType;
89
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
910
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
@@ -59,6 +60,18 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
5960
$operator = array_key_first($operators);
6061
}
6162

63+
// Keep legacy operators available for existing saved segments, but not for new filters.
64+
// @see https://github.com/mautic/mautic/pull/16012
65+
$legacyOperators = [OperatorOptions::INCLUDING_ALL, OperatorOptions::EXCLUDING_ALL];
66+
$isLegacyOperator = null !== $operator && in_array($operator, $legacyOperators, true);
67+
68+
if ($isLegacyOperator && !in_array($operator, $operators, true)) {
69+
$deprecatedOperatorTypes = $this->listModel->getOperatorsForFieldType([
70+
'include' => $legacyOperators,
71+
]);
72+
$operators += array_filter($deprecatedOperatorTypes, static fn ($v) => $v === $operator);
73+
}
74+
6275
$form->add(
6376
'operator',
6477
ChoiceType::class,

0 commit comments

Comments
 (0)