Skip to content
6 changes: 5 additions & 1 deletion config/services/messenger.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ services:
resource: '../../src/Domain/Subscription/MessageHandler'
tags: [ 'messenger.message_handler' ]

PhpList\Core\Domain\Messaging\MessageHandler\CampaignProcessorMessageHandler:
PhpList\Core\Domain\Messaging\MessageHandler\CampaignProcessor\CampaignProcessorMessageHandler:
autowire: true
autoconfigure: true

PhpList\Core\Domain\Messaging\MessageHandler\CampaignProcessor\TestCampaignProcessorMessageHandler:
autowire: true
autoconfigure: true
37 changes: 14 additions & 23 deletions config/services/resolvers.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
services:
_defaults:
autowire: true
autoconfigure: true

_instanceof:
PhpList\Core\Domain\Configuration\Service\Placeholder\PlaceholderValueResolverInterface:
tags: ['phplist.placeholder_resolver']
PhpList\Core\Domain\Configuration\Service\Placeholder\PatternValueResolverInterface:
tags: ['phplist.pattern_resolver']
PhpList\Core\Domain\Configuration\Service\Placeholder\SupportingPlaceholderResolverInterface:
tags: ['phplist.supporting_placeholder_resolver']

PhpList\Core\Domain\Subscription\Service\Resolver\AttributeValueResolver:
arguments:
$providers:
Expand All @@ -14,26 +26,5 @@ services:
arguments:
- !tagged_iterator { tag: 'phplist.bounce_action_handler' }

PhpList\Core\Domain\Configuration\Service\Placeholder\UnsubscribeUrlValueResolver:
autowire: true
autoconfigure: true

PhpList\Core\Domain\Configuration\Service\Placeholder\ConfirmationUrlValueResolver:
autowire: true
autoconfigure: true

PhpList\Core\Domain\Configuration\Service\Placeholder\PreferencesUrlValueResolver:
autowire: true
autoconfigure: true

PhpList\Core\Domain\Configuration\Service\Placeholder\SubscribeUrlValueResolver:
autowire: true
autoconfigure: true

_instanceof:
PhpList\Core\Domain\Configuration\Service\Placeholder\PlaceholderValueResolverInterface:
tags: ['phplist.placeholder_resolver']
PhpList\Core\Domain\Configuration\Service\Placeholder\PatternValueResolverInterface:
tags: [ 'phplist.pattern_resolver' ]
PhpList\Core\Domain\Configuration\Service\Placeholder\SupportingPlaceholderResolverInterface:
tags: [ 'phplist.supporting_placeholder_resolver' ]
PhpList\Core\Domain\Configuration\Service\Placeholder\:
resource: '../../src/Domain/Configuration/Service/Placeholder/*'
16 changes: 15 additions & 1 deletion src/DependencyInjection/PhpListCoreExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,26 @@

class PhpListCoreExtension extends Extension
{
private array $configFiles = [
'builders.yml',
'commands.yml',
'managers.yml',
'mappers.yml',
'messengers.yml',
'processors.yml',
'providers.yml',
'repositories.yml',
'resolvers.yml',
'services.yml',
'validators.yml',
];

public function load(array $configs, ContainerBuilder $container): void
{
$loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../../config/services'));

// Load core service definitions if present (keep optional to avoid breaking consumers)
foreach (['services.yml', 'builders.yml', 'managers.yml'] as $file) {
foreach ($this->configFiles as $file) {
$path = __DIR__ . '/../../config/services/' . $file;
if (is_file($path) && is_readable($path)) {
$loader->load($file);
Expand Down
16 changes: 8 additions & 8 deletions src/Domain/Analytics/Service/LinkTrackService.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace PhpList\Core\Domain\Analytics\Service;

use Doctrine\ORM\EntityManagerInterface;
use PhpList\Core\Core\ParameterProvider;
use PhpList\Core\Domain\Analytics\Exception\MissingMessageIdException;
use PhpList\Core\Domain\Analytics\Model\LinkTrack;
Expand All @@ -12,13 +13,11 @@

class LinkTrackService
{
private LinkTrackRepository $linkTrackRepository;
private ParameterProvider $paramProvider;

public function __construct(LinkTrackRepository $linkTrackRepository, ParameterProvider $paramProvider)
{
$this->linkTrackRepository = $linkTrackRepository;
$this->paramProvider = $paramProvider;
public function __construct(
private readonly LinkTrackRepository $linkTrackRepository,
private readonly ParameterProvider $paramProvider,
private readonly EntityManagerInterface $entityManager,
) {
}

public function getUrlById(int $id): ?string
Expand Down Expand Up @@ -58,7 +57,6 @@ public function extractAndSaveLinks(MessagePrecacheDto $content, int $userId, ?i
$links = array_unique($links);

$savedLinks = [];

foreach ($links as $url) {
$existingLinkTrack = $this->linkTrackRepository->findByUrlUserIdAndMessageId($url, $userId, $messageId);
if ($existingLinkTrack !== null) {
Expand All @@ -74,6 +72,8 @@ public function extractAndSaveLinks(MessagePrecacheDto $content, int $userId, ?i
$savedLinks[] = $linkTrack;
}

$this->entityManager->flush();

return $savedLinks;
}

Expand Down
7 changes: 5 additions & 2 deletions src/Domain/Common/RemotePageFetcher.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,16 @@ public function __invoke(string $url, array $userData): string

if (!empty($content)) {
$content = $this->htmlUrlRewriter->addAbsoluteResources($content, $url);
$this->eventLogManager->log(page: 'unknown page', entry:'Fetching '.$url.' success');
$this->eventLogManager->log(page: 'unknown page', entry: 'Fetching ' . $url . ' success');

$caches = $this->urlCacheRepository->getByUrl($url);
foreach ($caches as $cache) {
$this->entityManager->remove($cache);
}
$urlCache = (new UrlCache())->setUrl($url)->setContent($content)->setLastModified($lastModified);
$urlCache = (new UrlCache())
->setUrl($url)
->setContent($content)
->setLastModified($lastModified);
$this->urlCacheRepository->persist($urlCache);

$this->cache->set($cacheKey, [
Expand Down
59 changes: 55 additions & 4 deletions src/Domain/Configuration/Service/MessagePlaceholderProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,8 @@ public function process(
name: 'ORGANIZATION_NAME',
resolver: fn(PlaceholderContext $ctx) => $this->config->getValue(ConfigOption::OrganisationName) ?? ''
);

foreach ($this->placeholderResolvers as $placeholderResolver) {
$resolver->register($placeholderResolver->name(), $placeholderResolver);
}

$this->registerNestedResolvers($resolver);

foreach ($this->patternResolvers as $patternResolver) {
$resolver->registerPattern($patternResolver->pattern(), $patternResolver);
Expand Down Expand Up @@ -136,4 +134,57 @@ private function registerAttributeResolvers(
);
}
}

private function maskFooterPlaceholders(string $value, array &$placeholderMap): string
{
$placeholderMap = [];
$index = 0;

return preg_replace_callback(
'/\[FOOTER(?:%%[^\]]+)?\]/i',
function (array $matches) use (&$placeholderMap, &$index): string {
$token = sprintf('__PHPLIST_FOOTER_TOKEN_%d__', $index++);
$placeholderMap[$token] = $matches[0];

return $token;
},
$value
) ?? $value;
}

/** @param array<string, string> $placeholderMap */
private function restoreFooterPlaceholders(string $value, array $placeholderMap): string
{
if ($placeholderMap === []) {
return $value;
}

return strtr($value, $placeholderMap);
}

private function registerNestedResolvers(PlaceholderResolver $resolver): void
{
foreach ($this->placeholderResolvers as $placeholderResolver) {
if (strtoupper($placeholderResolver->name()) !== 'FOOTER') {
$resolver->register($placeholderResolver->name(), $placeholderResolver);
continue;
}

$resolver->register(
$placeholderResolver->name(),
function (PlaceholderContext $ctx) use ($placeholderResolver, $resolver): string {
$footer = (string) $placeholderResolver($ctx);
if (!str_contains($footer, '[')) {
return $footer;
}

$placeholderMap = [];
$maskedFooter = $this->maskFooterPlaceholders($footer, $placeholderMap);
$resolvedFooter = $resolver->resolve($maskedFooter, $ctx);

return $this->restoreFooterPlaceholders($resolvedFooter, $placeholderMap);
}
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
use PhpList\Core\Domain\Configuration\Model\ConfigOption;
use PhpList\Core\Domain\Configuration\Service\Provider\ConfigProvider;
use PhpList\Core\Domain\Configuration\Model\Dto\PlaceholderContext;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

final class FooterValueResolver implements PlaceholderValueResolverInterface
{
public function __construct(
private readonly ConfigProvider $config,
private readonly bool $forwardAlternativeContent,
#[Autowire('%messaging.forward_alternative_content%')] private readonly bool $forwardAlternativeContent,
) {
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,15 @@

use PhpList\Core\Domain\Configuration\Model\Dto\PlaceholderContext;
use PhpList\Core\Domain\Subscription\Repository\SubscriberListRepository;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Contracts\Translation\TranslatorInterface;

final class ListsValueResolver implements PlaceholderValueResolverInterface
{
public function __construct(
private readonly SubscriberListRepository $subscriberListRepository,
private readonly TranslatorInterface $translator,
private readonly bool $showPrivateLists = false,
#[Autowire('%app.preference_page_show_private_lists%')] private readonly bool $showPrivateLists = false,
) {
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
use PhpList\Core\Domain\Configuration\Model\ConfigOption;
use PhpList\Core\Domain\Configuration\Service\Provider\ConfigProvider;
use PhpList\Core\Domain\Configuration\Model\Dto\PlaceholderContext;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

final class SignatureValueResolver implements PlaceholderValueResolverInterface
{
public function __construct(
private readonly ConfigProvider $config,
private readonly bool $emailTextCredits = false,
#[Autowire('%messaging.email_text_credits%')] private readonly bool $emailTextCredits = false,
) {
}

Expand Down
5 changes: 2 additions & 3 deletions src/Domain/Identity/Service/PermissionChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use PhpList\Core\Domain\Common\Model\Interfaces\OwnableInterface;
use PhpList\Core\Domain\Identity\Model\Administrator;
use PhpList\Core\Domain\Identity\Model\PrivilegeFlag;
use PhpList\Core\Domain\Messaging\Model\ListMessage;
use PhpList\Core\Domain\Messaging\Model\Message;
use PhpList\Core\Domain\Subscription\Model\Subscriber;
use PhpList\Core\Domain\Subscription\Model\SubscriberList;
Expand Down Expand Up @@ -70,9 +71,7 @@ private function resolveRelatedEntity(DomainModel $resource, string $relatedClas
}

if ($resource instanceof Message && $relatedClass === SubscriberList::class) {
// todo: check which one is correct
// return $resource->getListMessages()->map(fn(ListMessage $lm) => $lm->getList())->toArray();
return $resource->getListMessages()->map(fn($lm) => $lm->getSubscriberList())->toArray();
return $resource->getListMessages()->map(fn(ListMessage $lm) => $lm->getList())->toArray();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Filter nullable list relations before ownership checks.

At Line 74, ListMessage::getList() is nullable, so this array may contain null. That can break owns(...) checks in checkRelatedResources() or cause false negatives.

Suggested patch
-            return $resource->getListMessages()->map(fn(ListMessage $lm) => $lm->getList())->toArray();
+            return $resource->getListMessages()
+                ->map(fn (ListMessage $lm) => $lm->getList())
+                ->filter(fn (?SubscriberList $list) => $list !== null)
+                ->toArray();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return $resource->getListMessages()->map(fn(ListMessage $lm) => $lm->getList())->toArray();
return $resource->getListMessages()
->map(fn (ListMessage $lm) => $lm->getList())
->filter(fn (?SubscriberList $list) => $list !== null)
->toArray();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Domain/Identity/Service/PermissionChecker.php` at line 74, The returned
array from PermissionChecker:: (the expression using
$resource->getListMessages()->map(fn(ListMessage $lm) =>
$lm->getList())->toArray()) can include null because ListMessage::getList() is
nullable; update this to filter out null list relations before returning so
checkRelatedResources() and the owns(...) checks never receive nulls — e.g.,
only include non-null results of getList() (or call ->filter(...) on
getListMessages() or apply array_filter after mapping) so all elements passed
into owns(...) are valid list objects.

}

return [];
Expand Down
2 changes: 1 addition & 1 deletion src/Domain/Messaging/Command/ProcessQueueCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use Doctrine\ORM\EntityManagerInterface;
use PhpList\Core\Domain\Configuration\Model\ConfigOption;
use PhpList\Core\Domain\Configuration\Service\Provider\ConfigProvider;
use PhpList\Core\Domain\Messaging\Message\CampaignProcessorMessage;
use PhpList\Core\Domain\Messaging\Message\CampaignProcessor\CampaignProcessorMessage;
use PhpList\Core\Domain\Messaging\Model\Message\MessageStatus;
use PhpList\Core\Domain\Messaging\Repository\MessageRepository;
use PhpList\Core\Domain\Messaging\Service\MessageProcessingPreparator;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace PhpList\Core\Domain\Messaging\Message;
namespace PhpList\Core\Domain\Messaging\Message\CampaignProcessor;

class CampaignProcessorMessage implements CampaignProcessorMessageInterface
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace PhpList\Core\Domain\Messaging\Message;
namespace PhpList\Core\Domain\Messaging\Message\CampaignProcessor;

interface CampaignProcessorMessageInterface
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace PhpList\Core\Domain\Messaging\Message;
namespace PhpList\Core\Domain\Messaging\Message\CampaignProcessor;

class SyncCampaignProcessorMessage implements CampaignProcessorMessageInterface
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

declare(strict_types=1);

namespace PhpList\Core\Domain\Messaging\Message\CampaignProcessor;

class TestCampaignProcessorMessage implements CampaignProcessorMessageInterface
{
private int $messageId;
private array $listIds;
private array $subscriberEmails;

public function __construct(int $messageId, ?array $listIds = [], ?array $subscriberEmails = [])
{
$this->messageId = $messageId;
$this->listIds = $listIds ?? [];
$this->subscriberEmails = $subscriberEmails ?? [];
}

public function getMessageId(): int
{
return $this->messageId;
}

public function getListIds(): array
{
return $this->listIds;
}

public function getSubscriberEmails(): array
{
return $this->subscriberEmails;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace PhpList\Core\Domain\Messaging\MessageHandler;
namespace PhpList\Core\Domain\Messaging\MessageHandler\CampaignProcessor;

use DateTime;
use DateTimeImmutable;
Expand All @@ -13,8 +13,8 @@
use PhpList\Core\Domain\Messaging\Exception\AttachmentCopyException;
use PhpList\Core\Domain\Messaging\Exception\MessageCacheMissingException;
use PhpList\Core\Domain\Messaging\Exception\MessageSizeLimitExceededException;
use PhpList\Core\Domain\Messaging\Message\CampaignProcessorMessage;
use PhpList\Core\Domain\Messaging\Message\SyncCampaignProcessorMessage;
use PhpList\Core\Domain\Messaging\Message\CampaignProcessor\CampaignProcessorMessage;
use PhpList\Core\Domain\Messaging\Message\CampaignProcessor\SyncCampaignProcessorMessage;
use PhpList\Core\Domain\Messaging\Model\Dto\MessagePrecacheDto;
use PhpList\Core\Domain\Messaging\Model\Message;
use PhpList\Core\Domain\Messaging\Model\Message\MessageStatus;
Expand Down Expand Up @@ -109,7 +109,11 @@ public function __invoke(CampaignProcessorMessage|SyncCampaignProcessorMessage $
// $userSelection = $loadedMessageData['userselection'];

$cacheKey = sprintf('messaging.message.base.%d.%d', $campaign->getId(), 0);
if (!$this->precacheService->precacheMessage($campaign, $loadedMessageData)) {
if (!$this->precacheService->precacheMessage(
campaign: $campaign,
loadedMessageData: $loadedMessageData,
isTest: false
)) {
$this->updateMessageStatus($campaign, MessageStatus::Suspended);

return;
Expand Down Expand Up @@ -199,7 +203,7 @@ private function handleEmailSending(
UserMessage $userMessage,
MessagePrecacheDto $precachedContent,
): void {
// todo: check at which point link tracking should be applied (maybe after constructing ful text?)
// todo: check at which point link tracking should be applied (maybe after constructing full text?)
$processed = $this->messagePreparator->processMessageLinks(
campaignId: $campaign->getId(),
cachedMessageDto: $precachedContent,
Expand Down
Loading
Loading