Skip to content

Conversation

@TatevikGr
Copy link
Contributor

@TatevikGr TatevikGr commented Dec 11, 2025

Summary by CodeRabbit

  • New Features

    • Rich HTML handling: improved HTML→plain-text conversion, URL rewriting to absolute links, remote page fetching, and smarter text parsing for links.
    • Message precache enhancements: template, image/logo handling and per-recipient personalization applied at send time.
  • Refactor

    • Reorganized messaging flow to centralize message data loading and precache preparation; updated email composition parameter naming for consistency.
  • Tests

    • Unit tests updated to reflect new personalization injection and precache/composition changes.
  • Chores

    • Added new environment-driven configuration options.

✏️ Tip: You can customize this high-level summary in your review settings.

Thanks for contributing to phpList!

@coderabbitai
Copy link

coderabbitai bot commented Dec 11, 2025

📝 Walkthrough

Walkthrough

Adds multiple new domain utilities (HtmlToText, HtmlUrlRewriter, RemotePageFetcher, TextParser), expands messaging precache and loading (MessagePrecacheDto, MessageDataLoader, MessagePrecacheService changes), shifts runtime personalization into CampaignProcessorMessageHandler, extends repositories/models, updates config/composer, and adjusts tests accordingly.

Changes

Cohort / File(s) Summary
Messaging — handler & personalization
src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php, src/Domain/Messaging/Service/MessageProcessingPreparator.php
Injects UserPersonalizer into CampaignProcessorMessageHandler, adds early exit when base content missing, applies personalization per-subscriber to text/footer; removes UserPersonalizer from MessageProcessingPreparator and strips its personalization calls.
Messaging — precache & data loader
src/Domain/Messaging/Model/Dto/MessagePrecacheDto.php, src/Domain/Messaging/Service/MessageDataLoader.php, src/Domain/Messaging/Service/MessagePrecacheService.php, src/Domain/Messaging/Service/MessagePrecacheService.php (constructor/signature changes)
Introduces MessagePrecacheDto; adds MessageDataLoader class; heavily refactors MessagePrecacheService constructor and getOrCacheBaseMessageContent to return ?MessagePrecacheDto, add logo handling, remote-fetching, template/image logic, and many helper methods.
Messaging — mail composition rename
src/Domain/Messaging/Service/RateLimitedCampaignMailer.php
Renames composeEmail() parameters: $processed$message, $content$processedContent; updates internal usages to reflect processed content vs. message entity.
Remote fetching & HTML utilities
src/Domain/Common/RemotePageFetcher.php, src/Domain/Common/HtmlUrlRewriter.php, src/Domain/Common/HtmlToText.php, src/Domain/Common/TextParser.php
Adds RemotePageFetcher (fetch + caching + DB url cache + rewrite), HtmlUrlRewriter (absolutize resources/srcset/CSS), HtmlToText (HTML→plain text with wrapping), and TextParser (URL/email→anchors) utilities.
Repositories — URL/cache/template helpers
src/Domain/Configuration/Repository/UrlCacheRepository.php, src/Domain/Messaging/Repository/TemplateImageRepository.php, src/Domain/Messaging/Repository/TemplateRepository.php, src/Domain/Messaging/Repository/MessageDataRepository.php
Adds repository helpers: UrlCache lookup by last-modified, TemplateImage finders, Template findOneById, and MessageData getForMessage.
Configuration & enums
config/parameters.yml.dist, composer.json, src/Domain/Configuration/Model/ConfigOption.php
Adds new env-driven parameters (messaging defaults, upload/public schema), adds ext-libxml/ext-gd to composer platform reqs, and extends ConfigOption enum with multiple new keys (wordwrap, remoteurl_append, organisation_logo, etc.).
Identity models & repositories
src/Domain/Identity/Model/AdminAttributeDefinition.php, src/Domain/Identity/Model/Administrator.php, src/Domain/Identity/Repository/AdminAttributeDefinitionRepository.php, src/Domain/Identity/Repository/AdministratorRepository.php
Adds OneToMany relations and getters (attributeValues, ownedLists); repository methods for admin-scoped attribute retrieval and admins related to a message.
Tests
tests/Unit/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandlerTest.php, tests/Unit/Domain/Messaging/Service/MessageProcessingPreparatorTest.php
Updates tests to inject/mock UserPersonalizer into CampaignProcessorMessageHandlerTest; removes UserPersonalizer mock from MessageProcessingPreparatorTest and adjusts content expectations.

Sequence Diagram(s)

sequenceDiagram
  participant Caller as Caller (Service)
  participant Remote as RemotePageFetcher
  participant HTTP as HttpClient
  participant Cache as CacheInterface
  participant DB as UrlCacheRepository / EntityManager
  participant Rewriter as HtmlUrlRewriter
  participant Logger as EventLogManager

  Caller->>Remote: invoke(url, userData)
  Remote->>Cache: get(hashedKey)
  alt cache hit (fresh)
    Cache-->>Remote: content
    Remote-->>Caller: content
  else cache miss / stale
    Remote->>DB: findByUrlAndLastModified(url, lastModified)
    DB-->>Remote: UrlCache or null
    Remote->>HTTP: fetchUrlDirect(url) (HEAD/GET)
    alt fetch success
      HTTP-->>Remote: html
      Remote->>Rewriter: addAbsoluteResources(html, baseUrl)
      Rewriter-->>Remote: rewrittenHtml
      Remote->>DB: persist/cleanup UrlCache entry
      Remote->>Cache: set(hashedKey, rewrittenHtml)
      Logger-->>Remote: log success
      Remote-->>Caller: rewrittenHtml
    else fetch fail
      HTTP-->>Remote: error/empty
      Logger-->>Remote: log failure
      Remote-->>Caller: "" (empty)
    end
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pay special attention to MessagePrecacheService (large constructor, new helper methods, cache key/DTO changes).
  • Review RemotePageFetcher flow and error handling, plus the expandURL/expandUrl naming discrepancy.
  • Verify HtmlUrlRewriter and HtmlToText correctness for edge cases (srcset, CSS urls, wrapping).
  • Confirm repository method queries and new enum additions do not break consumers.
  • Ensure tests updated correctly reflect DI changes and personalization behavior.

Possibly related PRs

  • Feat/check max mail size #373 — Overlapping changes to messaging classes and method signatures (CampaignProcessorMessageHandler, MessagePrecacheService, MessageProcessingPreparator, RateLimitedCampaignMailer).

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 19.61% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title is vague and doesn't accurately reflect the scope of changes. While CampaignProcessorMessageHandler was modified, the PR involves extensive refactoring across messaging services, new utility classes, configuration updates, and model changes across the codebase. Consider a more specific title that captures the main objective, such as 'Refactor message processing with personalization and content loading' or 'Restructure message precaching and campaign composition pipeline'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ref/campaign-processing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (1)

157-166: Guard personalization against null text/footer to avoid runtime errors

Right now you call personalize() on getText() and getFooter() unconditionally. Given MessageContent::getText() / getFooter() are treated as nullable elsewhere, this can blow up with a TypeError if either is null (and tests hide this by forcing non-null mocks).

A small null-check keeps this safe:

     private function handleEmailSending(
         Message $campaign,
         Subscriber $subscriber,
         UserMessage $userMessage,
         Message\MessageContent $precachedContent,
     ): void {
-        $processed = $this->messagePreparator->processMessageLinks($campaign->getId(), $precachedContent, $subscriber);
-        $processed->setText($this->userPersonalizer->personalize($processed->getText(), $subscriber->getEmail()));
-        $processed->setFooter($this->userPersonalizer->personalize($processed->getFooter(), $subscriber->getEmail()));
+        $processed = $this->messagePreparator->processMessageLinks(
+            $campaign->getId(),
+            $precachedContent,
+            $subscriber
+        );
+
+        $htmlText = $processed->getText();
+        if ($htmlText !== null) {
+            $processed->setText(
+                $this->userPersonalizer->personalize($htmlText, $subscriber->getEmail())
+            );
+        }
+
+        $footer = $processed->getFooter();
+        if ($footer !== null) {
+            $processed->setFooter(
+                $this->userPersonalizer->personalize($footer, $subscriber->getEmail())
+            );
+        }

This matches how MessageProcessingPreparator already treats these fields and avoids surprising failures when campaigns have no HTML or footer.

🧹 Nitpick comments (1)
tests/Unit/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandlerTest.php (1)

175-205: Content mocks align with new personalization flow; consider adding a null‑footer case

Stubbing getText() / getFooter() in these tests to return concrete HTML/footer strings is a good way to keep the new personalization path exercised without changing expectations.

Once you add null-guards in the handler, it’d be worth adding a small test where getFooter() (and/or getText()) returns null to lock in that behavior and prevent regressions.

Also applies to: 236-269, 291-325

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 83431b1 and a72d2e9.

📒 Files selected for processing (5)
  • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (3 hunks)
  • src/Domain/Messaging/Service/MessageProcessingPreparator.php (1 hunks)
  • src/Domain/Messaging/Service/RateLimitedCampaignMailer.php (1 hunks)
  • tests/Unit/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandlerTest.php (6 hunks)
  • tests/Unit/Domain/Messaging/Service/MessageProcessingPreparatorTest.php (0 hunks)
💤 Files with no reviewable changes (1)
  • tests/Unit/Domain/Messaging/Service/MessageProcessingPreparatorTest.php
🧰 Additional context used
📓 Path-based instructions (2)
src/Domain/**

⚙️ CodeRabbit configuration file

src/Domain/**: You are reviewing PHP domain-layer code. Enforce domain purity, with a relaxed policy for DynamicListAttr:

  • ❌ Do not allow persistence or transaction side effects here for normal domain models.

  • Flag ANY usage of Doctrine persistence APIs on regular domain entities, especially:

    • $entityManager->flush(...), $this->entityManager->flush(...)
    • $em->persist(...), $em->remove(...)
    • $em->beginTransaction(), $em->commit(), $em->rollback()
  • ✅ Accessing Doctrine metadata, schema manager, or read-only schema info is acceptable
    as long as it does not modify state or perform writes.

  • Relaxed rule for DynamicListAttr-related code:

    • DynamicListAttr is a special case dealing with dynamic tables/attrs.
    • It is acceptable for DynamicListAttr repositories/services to:
      • Create/update/drop DynamicListAttr tables/columns.
      • Use Doctrine persistence APIs (persist, remove, flush, etc.)
        as part of managing DynamicListAttr data and schema.
    • Do not flag persistence or schema-creation calls that are clearly scoped
      to DynamicListAttr tables or their management.
    • Still prefer keeping this logic well-encapsulated (e.g. in dedicated services/repos),
      not scattered across unrelated domain objects.
  • ⚠️ For non-DynamicListAttr code:

    • If code is invoking actual table-creation, DDL execution, or schema synchronization,
      then request moving that to the Infrastructure or Application layer (e.g. MessageHandler).
    • Repositories in Domain should be abstractions without side effects; they should express intent,
      not perform flush/transactional logic.

Files:

  • src/Domain/Messaging/Service/RateLimitedCampaignMailer.php
  • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php
  • src/Domain/Messaging/Service/MessageProcessingPreparator.php
src/**/MessageHandler/**

⚙️ CodeRabbit configuration file

src/**/MessageHandler/**: Background jobs/workers may perform persistence and schema management.

  • ✅ Allow $entityManager->flush() when the job is the orchestration boundary.
  • ✅ Allow table creation, migration, or schema synchronization (e.g. via Doctrine SchemaTool or SchemaManager),
    as this is considered infrastructure-level orchestration.
  • For DynamicListAttr-related jobs, it is fine to orchestrate both data and schema changes here,
    as long as responsibilities remain clear and behavior is predictable.
  • Verify idempotency for schema operations where practical — e.g., check if a table exists before creating.
  • Ensure domain-layer code invoked by the job (outside the DynamicListAttr exception) remains free of persistence calls.
  • Batch flush operations where practical.

Files:

  • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php
🧬 Code graph analysis (1)
src/Domain/Messaging/Service/RateLimitedCampaignMailer.php (1)
src/Domain/Messaging/Message/AsyncEmailMessage.php (1)
  • getReplyTo (49-52)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: phpList on PHP 8.1 [Build, Test]
  • GitHub Check: phpList on PHP 8.1 [Build, Test]
🔇 Additional comments (4)
src/Domain/Messaging/Service/MessageProcessingPreparator.php (1)

76-87: No behavioral change here; TODO still relevant

This is just a comment tweak; existing logic around getText() / getFooter() and link replacement remains consistent and safe. All good.

src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (1)

7-9: UserPersonalizer wiring looks good

Importing and injecting UserPersonalizer into the handler keeps responsibilities in the message handler (where persistence and orchestration already live) and lines up with the new personalization flow. No issues here.

Also applies to: 44-62

tests/Unit/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandlerTest.php (1)

10-11: Test wiring for UserPersonalizer is solid

Injecting a UserPersonalizer mock and stubbing personalize() to be identity keeps existing assertions unchanged while exercising the new dependency. The constructor args line up with the handler changes, so this looks good.

Also applies to: 48-92

src/Domain/Messaging/Service/RateLimitedCampaignMailer.php (1)

23-42: composeEmail refactor improves clarity and matches new pipeline

Using $message for options and $processedContent for subject/text/HTML matches the new “preprocess then personalize” flow and keeps the method signature self-explanatory. The handler call site aligns with this, so the change looks clean.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 19

🧹 Nitpick comments (7)
src/Domain/Messaging/Repository/TemplateRepository.php (1)

16-19: Consider using inherited find() method.

AbstractRepository likely already provides a find($id) method (inherited from Doctrine's EntityRepository) that does exactly this.

If your goal is to add clarity, this wrapper is fine. Otherwise, callers could use $repository->find($id) directly.

src/Domain/Common/TextParser.php (1)

22-26: Protocol stripping is a partial XSS mitigation.

The logic strips colons from non-standard protocols but still allows javascript URLs through (e.g., javascript window.open(...)). Consider using an allowlist approach or a more robust sanitization library for URL schemes.

src/Domain/Common/HtmlToText.php (2)

61-66: Regex for <a> tags after strip_tags() is likely dead code.

strip_tags($text) at line 38 removes all HTML tags, so this regex pattern matching <a href=...> will never find matches. If this is intentional for edge cases, a comment would help clarify.


74-80: Consider limiting iterations on whitespace reduction loops.

These while loops could theoretically run many times on pathological input. For robustness, consider a reasonable iteration cap or using preg_replace with a quantifier pattern like / {2,}/ to collapse in one pass.

         // reduce whitespace
-        while (preg_match('/  /', $text)) {
-            $text = preg_replace('/  /', ' ', $text);
-        }
-        while (preg_match("/\n\s*\n\s*\n/", $text)) {
-            $text = preg_replace("/\n\s*\n\s*\n/", "\n\n", $text);
-        }
+        $text = preg_replace('/ {2,}/', ' ', $text);
+        $text = preg_replace("/(\n\s*){3,}/", "\n\n", $text);
src/Domain/Messaging/Service/MessageDataLoader.php (1)

101-101: Consider restricting unserialize() with allowed_classes.

Deserializing stored data without class restrictions can be a security risk if the data source is ever compromised. Pass ['allowed_classes' => false] or specify expected classes.

-                $unserialized = unserialize(substr($storedMessageDatum->getData(), 4));
+                $unserialized = unserialize(substr($storedMessageDatum->getData(), 4), ['allowed_classes' => false]);
src/Domain/Common/RemotePageFetcher.php (1)

31-43: utf8_encode/utf8_decode are deprecated + URL substitution is risky (Line 31–Line 43, Line 56–Line 66, Line 69–Line 87).

  • utf8_encode() / utf8_decode() are deprecated in modern PHP; replace with safe string handling (or mbstring) and avoid double transcoding.
  • Consider validating/allowlisting schemes/hosts before fetching remote content (admin mistakes can still become SSRF-like incidents).
    Suggested direction:
-                $url = utf8_encode(str_ireplace("[$key]", urlencode($val), utf8_decode($url)));
+                $url = str_ireplace("[$key]", rawurlencode((string) $val), $url);

Also applies to: 56-66, 69-87

src/Domain/Common/HtmlUrlRewriter.php (1)

18-21: Tighten API surface + avoid leaking global libxml state (Line 18–Line 21, Line 122–Line 143).

  • libxml_use_internal_errors(true) changes global state; please restore the previous setting after parsing.
  • normalizePath() should declare visibility (and ideally private) + return type.

Also applies to: 122-143

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a72d2e9 and 5fe45ab.

📒 Files selected for processing (19)
  • composer.json (1 hunks)
  • config/parameters.yml.dist (1 hunks)
  • src/Domain/Common/HtmlToText.php (1 hunks)
  • src/Domain/Common/HtmlUrlRewriter.php (1 hunks)
  • src/Domain/Common/RemotePageFetcher.php (1 hunks)
  • src/Domain/Common/TextParser.php (1 hunks)
  • src/Domain/Configuration/Model/ConfigOption.php (1 hunks)
  • src/Domain/Configuration/Repository/UrlCacheRepository.php (1 hunks)
  • src/Domain/Identity/Model/AdminAttributeDefinition.php (4 hunks)
  • src/Domain/Identity/Model/Administrator.php (4 hunks)
  • src/Domain/Identity/Repository/AdminAttributeDefinitionRepository.php (2 hunks)
  • src/Domain/Identity/Repository/AdministratorRepository.php (1 hunks)
  • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (4 hunks)
  • src/Domain/Messaging/Model/Dto/MessagePrecacheDto.php (1 hunks)
  • src/Domain/Messaging/Repository/MessageDataRepository.php (1 hunks)
  • src/Domain/Messaging/Repository/TemplateImageRepository.php (1 hunks)
  • src/Domain/Messaging/Repository/TemplateRepository.php (1 hunks)
  • src/Domain/Messaging/Service/MessageDataLoader.php (1 hunks)
  • src/Domain/Messaging/Service/MessagePrecacheService.php (2 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
src/Domain/**

⚙️ CodeRabbit configuration file

src/Domain/**: You are reviewing PHP domain-layer code. Enforce domain purity, with a relaxed policy for DynamicListAttr:

  • ❌ Do not allow persistence or transaction side effects here for normal domain models.

  • Flag ANY usage of Doctrine persistence APIs on regular domain entities, especially:

    • $entityManager->flush(...), $this->entityManager->flush(...)
    • $em->persist(...), $em->remove(...)
    • $em->beginTransaction(), $em->commit(), $em->rollback()
  • ✅ Accessing Doctrine metadata, schema manager, or read-only schema info is acceptable
    as long as it does not modify state or perform writes.

  • Relaxed rule for DynamicListAttr-related code:

    • DynamicListAttr is a special case dealing with dynamic tables/attrs.
    • It is acceptable for DynamicListAttr repositories/services to:
      • Create/update/drop DynamicListAttr tables/columns.
      • Use Doctrine persistence APIs (persist, remove, flush, etc.)
        as part of managing DynamicListAttr data and schema.
    • Do not flag persistence or schema-creation calls that are clearly scoped
      to DynamicListAttr tables or their management.
    • Still prefer keeping this logic well-encapsulated (e.g. in dedicated services/repos),
      not scattered across unrelated domain objects.
  • ⚠️ For non-DynamicListAttr code:

    • If code is invoking actual table-creation, DDL execution, or schema synchronization,
      then request moving that to the Infrastructure or Application layer (e.g. MessageHandler).
    • Repositories in Domain should be abstractions without side effects; they should express intent,
      not perform flush/transactional logic.

Files:

  • src/Domain/Messaging/Repository/MessageDataRepository.php
  • src/Domain/Identity/Repository/AdministratorRepository.php
  • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php
  • src/Domain/Configuration/Repository/UrlCacheRepository.php
  • src/Domain/Messaging/Repository/TemplateImageRepository.php
  • src/Domain/Common/TextParser.php
  • src/Domain/Identity/Model/AdminAttributeDefinition.php
  • src/Domain/Common/HtmlToText.php
  • src/Domain/Identity/Repository/AdminAttributeDefinitionRepository.php
  • src/Domain/Messaging/Service/MessageDataLoader.php
  • src/Domain/Identity/Model/Administrator.php
  • src/Domain/Messaging/Repository/TemplateRepository.php
  • src/Domain/Configuration/Model/ConfigOption.php
  • src/Domain/Messaging/Service/MessagePrecacheService.php
  • src/Domain/Common/RemotePageFetcher.php
  • src/Domain/Common/HtmlUrlRewriter.php
  • src/Domain/Messaging/Model/Dto/MessagePrecacheDto.php
src/**/MessageHandler/**

⚙️ CodeRabbit configuration file

src/**/MessageHandler/**: Background jobs/workers may perform persistence and schema management.

  • ✅ Allow $entityManager->flush() when the job is the orchestration boundary.
  • ✅ Allow table creation, migration, or schema synchronization (e.g. via Doctrine SchemaTool or SchemaManager),
    as this is considered infrastructure-level orchestration.
  • For DynamicListAttr-related jobs, it is fine to orchestrate both data and schema changes here,
    as long as responsibilities remain clear and behavior is predictable.
  • Verify idempotency for schema operations where practical — e.g., check if a table exists before creating.
  • Ensure domain-layer code invoked by the job (outside the DynamicListAttr exception) remains free of persistence calls.
  • Batch flush operations where practical.

Files:

  • src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php
🧬 Code graph analysis (3)
src/Domain/Common/TextParser.php (1)
src/Domain/Common/HtmlToText.php (1)
  • __invoke (18-84)
src/Domain/Common/HtmlToText.php (1)
src/Domain/Configuration/Service/Provider/ConfigProvider.php (1)
  • ConfigProvider (12-82)
src/Domain/Messaging/Service/MessageDataLoader.php (2)
src/Domain/Configuration/Service/Provider/ConfigProvider.php (1)
  • ConfigProvider (12-82)
src/Domain/Messaging/Repository/MessageDataRepository.php (2)
  • MessageDataRepository (12-26)
  • getForMessage (22-25)
🔇 Additional comments (15)
src/Domain/Identity/Model/AdminAttributeDefinition.php (1)

42-47: LGTM! Relationship setup is clean.

The OneToMany relationship is properly configured with orphanRemoval, correctly initialized in the constructor, and provides appropriate read access via the getter.

Also applies to: 63-63, 136-140

src/Domain/Identity/Model/Administrator.php (1)

71-72: LGTM! Relationship properly configured.

The OneToMany relationship to SubscriberList is correctly set up as the inverse side (mappedBy: 'owner'), properly initialized, and provides clean read access.

Also applies to: 79-79, 222-228

src/Domain/Messaging/Repository/TemplateImageRepository.php (1)

16-24: LGTM! Query methods are well-scoped.

Both methods correctly filter by ti.template = 0 to isolate non-template images, which aligns with the stated purpose of finding images by filename or ID that aren't associated with specific templates.

Also applies to: 26-34

src/Domain/Configuration/Repository/UrlCacheRepository.php (1)

16-25: LGTM! Cache query methods look solid.

The findByUrlAndLastModified method correctly uses a greater-than comparison to locate cached entries newer than the provided timestamp, and getByUrl provides straightforward retrieval by URL.

Also applies to: 27-31

src/Domain/Identity/Repository/AdministratorRepository.php (1)

59-71: LGTM! Query logic looks sound.

The join path Administrator → ownedLists → listMessages → message is clean and correctly mapped. The DISTINCT clause appropriately handles potential duplicates.

src/Domain/Messaging/Repository/MessageDataRepository.php (1)

22-25: The original review comment is incorrect. The getForMessage() method query is actually correct.

The MessageData entity uses a composite primary key of (id, name), where the id field holds the message ID. The method getForMessage(int $messageId) correctly queries findBy(['id' => $messageId]) to retrieve all MessageData entries for a given message. Since id is part of the composite key, querying on it alone returns all rows with that message ID, regardless of their name value.

The suggestion to change the filter to ['message' => $messageId] would not work—no such property exists on the entity.

Likely an incorrect or invalid review comment.

src/Domain/Messaging/MessageHandler/CampaignProcessorMessageHandler.php (2)

60-60: LGTM on dependency injection of UserPersonalizer.

Clean addition of the UserPersonalizer dependency to support per-subscriber personalization at the handler level.


79-83: Good defensive early exit pattern.

Suspending the campaign and returning early when getOrCacheBaseMessageContent returns null is a solid safeguard against processing campaigns without valid content.

src/Domain/Common/TextParser.php (2)

47-50: Potential regex injection via $j placeholder.

The $j variable is an integer, so it's safe here, but the pattern /\%\%$j\%\%/ embeds a variable directly. This is fine for integers but worth noting for future maintainers if this pattern is copied elsewhere.


56-58: LGTM on placeholder handling.

The bracket and dollar sign placeholder pattern is a reasonable workaround for regex limitations with special characters.

src/Domain/Identity/Repository/AdminAttributeDefinitionRepository.php (1)

22-36: LGTM on getForAdmin implementation.

Clean use of COALESCE for defaulting null values and proper left join filtering by administrator.

src/Domain/Common/HtmlToText.php (1)

18-38: LGTM on HTML transformation pipeline.

The placeholder-based approach for preserving links during strip_tags is a solid pattern for HTML-to-text conversion.

src/Domain/Messaging/Service/MessageDataLoader.php (2)

28-86: LGTM on default message data initialization.

Comprehensive defaults with config-driven values. The structure is clear and maintainable.


141-178: LGTM on fromfield parsing logic.

Thorough handling of various email/name formats with good edge case coverage for sanitization.

src/Domain/Common/HtmlUrlRewriter.php (1)

13-73: Solid approach: DOM + CSS rewriting covers the important cases (Line 13–Line 73).
The wrapping/unwrapping pattern is a practical way to handle fragments, and rewriting srcset + CSS url(...)/@import is a nice completeness touch.

Comment on lines +92 to +100
messaging.default_message_age: '%%env(DEFAULT_MESSAGEAGE)%%'
env(DEFAULT_MESSAGEAGE): '691200'
messaging.use_manual_text_part : '%%env(USE_MANUAL_TEXT_PART)%%'
env(USE_MANUAL_TEXT_PART): 0

phplist.upload_images_dir: '%%env(PHPLIST_UPLOADIMAGES_DIR)%%'
env(PHPLIST_UPLOADIMAGES_DIR): 'images'
phplist.public_schema: '%%env(PUBLIC_SCHEMA)%%'
env(PUBLIC_SCHEMA): 'http'
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Minor formatting inconsistency.

Line 94 has a space before the colon: messaging.use_manual_text_part : while all other parameter keys follow the pattern key: with no space.

Apply this diff for consistency:

-    messaging.use_manual_text_part : '%%env(USE_MANUAL_TEXT_PART)%%'
+    messaging.use_manual_text_part: '%%env(USE_MANUAL_TEXT_PART)%%'
📝 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
messaging.default_message_age: '%%env(DEFAULT_MESSAGEAGE)%%'
env(DEFAULT_MESSAGEAGE): '691200'
messaging.use_manual_text_part : '%%env(USE_MANUAL_TEXT_PART)%%'
env(USE_MANUAL_TEXT_PART): 0
phplist.upload_images_dir: '%%env(PHPLIST_UPLOADIMAGES_DIR)%%'
env(PHPLIST_UPLOADIMAGES_DIR): 'images'
phplist.public_schema: '%%env(PUBLIC_SCHEMA)%%'
env(PUBLIC_SCHEMA): 'http'
messaging.default_message_age: '%%env(DEFAULT_MESSAGEAGE)%%'
env(DEFAULT_MESSAGEAGE): '691200'
messaging.use_manual_text_part: '%%env(USE_MANUAL_TEXT_PART)%%'
env(USE_MANUAL_TEXT_PART): 0
phplist.upload_images_dir: '%%env(PHPLIST_UPLOADIMAGES_DIR)%%'
env(PHPLIST_UPLOADIMAGES_DIR): 'images'
phplist.public_schema: '%%env(PUBLIC_SCHEMA)%%'
env(PUBLIC_SCHEMA): 'http'
🤖 Prompt for AI Agents
In config/parameters.yml.dist around lines 92 to 100, there's a formatting
inconsistency at line 94 where the key "messaging.use_manual_text_part :" has an
extra space before the colon; update that line to remove the space so the key
follows the same "key:" pattern as the other parameters, keeping the existing
value/token unchanged and preserving surrounding indentation and newline
structure.

Comment on lines +81 to +83
$ww = $this->configProvider->getValue(ConfigOption::WordWrap) ?? self::WORD_WRAP;

return wordwrap($text, $ww);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Type mismatch: wordwrap expects int, but config returns string.

getValue() returns ?string, so $ww could be "75" (string). Cast to int for type safety.

-        $ww = $this->configProvider->getValue(ConfigOption::WordWrap) ?? self::WORD_WRAP;
+        $ww = (int) ($this->configProvider->getValue(ConfigOption::WordWrap) ?? self::WORD_WRAP);

         return wordwrap($text, $ww);
📝 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
$ww = $this->configProvider->getValue(ConfigOption::WordWrap) ?? self::WORD_WRAP;
return wordwrap($text, $ww);
$ww = (int) ($this->configProvider->getValue(ConfigOption::WordWrap) ?? self::WORD_WRAP);
return wordwrap($text, $ww);
🤖 Prompt for AI Agents
In src/Domain/Common/HtmlToText.php around lines 81 to 83, the value retrieved
from configProvider is ?string but wordwrap requires an int; cast the config
value to int (e.g. $ww =
(int)($this->configProvider->getValue(ConfigOption::WordWrap) ??
self::WORD_WRAP);) or use intval with the same fallback so wordwrap always
receives an integer.

Comment on lines +19 to +28
public function __construct(
private readonly HttpClientInterface $httpClient,
private readonly CacheInterface $cache,
private readonly ConfigProvider $configProvider,
private readonly UrlCacheRepository $urlCacheRepository,
private readonly EventLogManager $eventLogManager,
private readonly HtmlUrlRewriter $htmlUrlRewriter,
private readonly EntityManagerInterface $entityManager,
private readonly int $defaultTtl = 300,
) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd RemotePageFetcher.php

Repository: phpList/core

Length of output: 95


🏁 Script executed:

cat -n src/Domain/Common/RemotePageFetcher.php

Repository: phpList/core

Length of output: 5109


Domain purity breach: persistence side effects in src/Domain/**
Lines 75 and 78 call $this->entityManager->remove() and $this->entityManager->persist() directly. This violates domain layer purity; move UrlCache write logic to Infrastructure or Application (e.g., a dedicated service or handler) and inject a repository interface that abstracts away the persistence concern.

🤖 Prompt for AI Agents
In src/Domain/Common/RemotePageFetcher.php around lines 19 and 28 (and
specifically lines ~75 and ~78) the domain class directly calls
$this->entityManager->remove() and $this->entityManager->persist(), creating
persistence side effects inside the Domain layer; replace direct EntityManager
usage by depending on an abstract repository interface (e.g.,
UrlCacheRepositoryInterface) injected into the constructor, move the concrete
EntityManager-based implementation into the Infrastructure layer, and refactor
RemotePageFetcher to call only repository methods (e.g.,
deleteUrlCache()/saveUrlCache()) so all persistence logic lives in
Infrastructure/Application and the Domain remains pure.

Comment on lines +89 to +103
private function fetchUrlDirect(string $url): string
{
try {
$response = $this->httpClient->request('GET', $url, [
// 'timeout' => 10,
'timeout' => 600,
'allowRedirects' => 1,
'method' => 'HEAD',
]);

return $response->getContent(false);
} catch (Throwable $e) {
return '';
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Symfony HttpClient request method options allowRedirects max_redirects

💡 Result:

Short answer: Symfony HttpClient uses the request option max_redirects (not allowRedirects). It controls how many redirects the client will follow (default 20). Set max_redirects: 0 to disable following redirects; set to an integer N to allow up to N redirects. You can set it per request or as a global default_options in framework.http_client. [1][2]

Examples:

  • Per request:
    $response = $client->request('GET', 'https://...', ['max_redirects' => 0]); [1]
  • Global (config/packages/framework.yaml):
    framework:
    http_client:
    default_options:
    max_redirects: 7 [1]

Sources: Symfony HttpClient docs and HttpClient interface defaults. [1][2]


🌐 Web query:

Symfony HttpClient API reference options parameters

💡 Result:

Here are the request option keys supported by Symfony's HttpClient (from HttpClientInterface::OPTIONS_DEFAULTS) with short notes on their expected values:

  • auth_basic — array|string (username[:password]) for HTTP Basic auth. [1]
  • auth_bearer — string bearer token. [1]
  • query — array of query params to merge with the URL. [1]
  • headers — iterable|string[]|string[][] request headers. [1]
  • body — array|string|resource|\Traversable|\Closure (form payload, raw body, or streaming callback). [1]
  • json — mixed (will set body to JSON and Content-Type if not provided). [1]
  • user_data — mixed (opaque value accessible via $response->getInfo('user_data')). [1]
  • max_redirects — int (max redirects; <=0 = no redirects). [1]
  • http_version — string (e.g. "1.1", "2.0"). [1]
  • base_uri — string (base URI for resolving relative URLs). [1]
  • buffer — bool|resource|\Closure (whether/how to buffer response body). [1]
  • on_progress — callable(int $dlNow, int $dlSize, array $info) called during progress. [1]
  • resolve — array map of host => IP to override DNS. [1]
  • proxy — string proxy URL (environment vars may be honored by implementations). [1]
  • no_proxy — string (comma-separated hosts to bypass proxy). [1]
  • timeout — float (idle timeout). [1]
  • max_duration — float (maximum total time for request+response; <=0 = unlimited). [1]
  • bindto — string (interface or local socket to bind to). [1]
  • verify_peer, verify_host, cafile, capath, local_cert, local_pk, passphrase, ciphers, peer_fingerprint, capture_peer_cert_chain — SSL/peer verification and cert options. [1]
  • extra — array (additional custom options that may be ignored by some implementations). [1]

For usage examples (headers, query, auth, redirects, proxies, withOptions()/default_options, HttpOptions helper), see the Symfony HttpClient docs. [2]

Sources: Symfony HttpClientInterface options listing and the HttpClient docs. [1][2]


HTTP request options are incorrect; code likely won't work as intended (Lines 89–103).

You call request('GET', ...) but pass invalid options: 'allowRedirects' doesn't exist in Symfony HttpClient (should be max_redirects), and 'method' => 'HEAD' isn't a valid option—it's silently ignored. The 600s timeout also masks the commented-out 10s, and swallowing all exceptions silently makes debugging hard.

Fix to:

-            $response = $this->httpClient->request('GET', $url, [
-                'timeout' => 600,
-                'allowRedirects' => 1,
-                'method' => 'HEAD',
-            ]);
+            $response = $this->httpClient->request('GET', $url, [
+                'timeout' => 10,
+                'max_redirects' => 5,
+            ]);

Also log the exception (at least at debug level) instead of silently returning ''.

🤖 Prompt for AI Agents
In src/Domain/Common/RemotePageFetcher.php around lines 89-103, the HTTP options
and method are wrong and exceptions are swallowed; replace the options with
Symfony HttpClient-compatible ones (use 'max_redirects' instead of
'allowRedirects'), remove the invalid 'method' option and call
$this->httpClient->request('HEAD', $url, [...]) if you intend a HEAD request (or
keep 'GET' and call request('GET', ...)), reduce the timeout back to a
reasonable value (e.g. 10), and in the catch block log the exception at debug
level (e.g. $this->logger->debug or the class logger) before returning an empty
string so errors are not silently discarded.

Comment on lines +25 to +41
public function __construct(
private readonly CacheInterface $cache,
private readonly MessageDataLoader $messageDataLoader,
private readonly ConfigProvider $configProvider,
private readonly HtmlToText $htmlToText,
private readonly TextParser $textParser,
private readonly TemplateRepository $templateRepository,
private readonly TemplateImageRepository $templateImageRepository,
private readonly RemotePageFetcher $remotePageFetcher,
private readonly EventLogManager $eventLogManager,
private readonly AdminAttributeDefinitionRepository $adminAttributeDefRepository,
private readonly AdministratorRepository $adminRepository,
private readonly EntityManagerInterface $entityManager,
private readonly bool $useManualTextPart,
private readonly string $uploadImageDir,
private readonly string $publicSchema,
) {
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd src/Domain/Messaging/Service && wc -l MessagePrecacheService.php

Repository: phpList/core

Length of output: 86


🏁 Script executed:

cd src/Domain/Messaging/Service && cat -n MessagePrecacheService.php | sed -n '25,41p'

Repository: phpList/core

Length of output: 1110


🏁 Script executed:

cd src/Domain/Messaging/Service && cat -n MessagePrecacheService.php | sed -n '280,310p'

Repository: phpList/core

Length of output: 1118


🏁 Script executed:

cd src/Domain/Messaging/Service && rg '\$entityManager\->(persist|remove|flush|begin|commit|rollback)' MessagePrecacheService.php -A 3 -B 1

Repository: phpList/core

Length of output: 38


🏁 Script executed:

cd src/Domain/Messaging/Service && cat -n MessagePrecacheService.php | sed -n '250,305p'

Repository: phpList/core

Length of output: 2852


🏁 Script executed:

cd src/Domain/Messaging/Service && cat -n MessagePrecacheService.php | sed -n '230,250p'

Repository: phpList/core

Length of output: 1063


Domain purity breach: DB writes in src/Domain/** (lines 282, 302).
MessagePrecacheService.createCachedLogoImage() directly calls $entityManager->remove() and persist() for TemplateImage entities. Per coding guidelines, persistence side-effects belong in Infrastructure/Application layer, not Domain. Move these calls to a dedicated service/repository outside Domain, or wrap via an interface implemented in the Infrastructure layer.

🤖 Prompt for AI Agents
In src/Domain/Messaging/Service/MessagePrecacheService.php around lines 25–41
and specifically where createCachedLogoImage() does direct DB writes at lines
~282 and ~302, remove direct EntityManager->persist()/remove() calls from the
Domain service and instead delegate persistence to an abstraction in the Domain
(e.g., TemplateImagePersistenceInterface) that exposes methods like
saveTemplateImage(TemplateImage) and deleteTemplateImage(TemplateImage);
implement that interface in the Infrastructure layer using the EntityManager and
register it for DI, then inject the interface into MessagePrecacheService (or
use the existing TemplateImageRepository if moved/extended to the
Infrastructure-backed persistence interface) and replace the direct
persist/remove calls with calls to the new interface methods so all
side-effecting DB operations live in Infrastructure.

Comment on lines +48 to 55
public function getOrCacheBaseMessageContent(Message $campaign, ?bool $forwardContent = false): ?MessagePrecacheDto
{
$cacheKey = sprintf('messaging.message.base.%d', $campaign->getId());

$cached = $this->getFromCache($cacheKey);
$cached = $this->cache->get($cacheKey);
if ($cached !== null) {
return $cached;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Blocker: cache key ignores $forwardContent and can return the wrong content (Line 48–Line 55, Line 85–Line 95).
$subject/$content/$footer depend on $forwardContent, but the cache key doesn’t—so whichever call runs first “poisons” the cache for the other mode.
Fix:

-        $cacheKey = sprintf('messaging.message.base.%d', $campaign->getId());
+        $cacheKey = sprintf('messaging.message.base.%d.%d', $campaign->getId(), (int) $forwardContent);

Also applies to: 85-95

🤖 Prompt for AI Agents
In src/Domain/Messaging/Service/MessagePrecacheService.php around lines 48–55
and again around lines 85–95 the cache key currently ignores the $forwardContent
flag so cached base message content for one mode can be returned for the other;
include $forwardContent (e.g. cast to int or '1'/'0') in the sprintf cache key
so keys differ by forward mode, and apply the same change to the second block so
both cache reads/writes use the forward-aware key.

Comment on lines +52 to 55
$cached = $this->cache->get($cacheKey);
if ($cached !== null) {
return $cached;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

fd -e php MessagePrecacheService

Repository: phpList/core

Length of output: 111


🏁 Script executed:

cat -n src/Domain/Messaging/Service/MessagePrecacheService.php

Repository: phpList/core

Length of output: 16937


🏁 Script executed:

fd -e yaml -e xml -e php config | grep -i cache | head -20

Repository: phpList/core

Length of output: 38


🏁 Script executed:

fd -name "*.yaml" -o -name "*.yml" -o -name "services.php" | head -20

Repository: phpList/core

Length of output: 286


🏁 Script executed:

fd '\.(yaml|yml)$'

Repository: phpList/core

Length of output: 644


🏁 Script executed:

grep -r "cache" config/ --include="*.yml" --include="*.yaml" -i | grep -E "(service|backend|psr|adapter)" | head -20

Repository: phpList/core

Length of output: 500


🏁 Script executed:

cat config/services/services.yml | head -100

Repository: phpList/core

Length of output: 3022


🏁 Script executed:

rg "cache\.app" config/

Repository: phpList/core

Length of output: 248


🏁 Script executed:

grep -A 10 "cache:" config/config*.yml

Repository: phpList/core

Length of output: 38


🏁 Script executed:

fd MessagePrecacheDto

Repository: phpList/core

Length of output: 109


🏁 Script executed:

cat -n src/Domain/Messaging/Model/Dto/MessagePrecacheDto.php

Repository: phpList/core

Length of output: 1066


🏁 Script executed:

grep -r "framework:" config/ | head -5

Repository: phpList/core

Length of output: 193


🏁 Script executed:

cat config/config.yml | grep -A 20 "^framework:"

Repository: phpList/core

Length of output: 627


Consider adding TTL and cache key versioning (Lines 52–55, 193–195).
The cache stores the DTO object directly with no TTL, so it persists indefinitely across deployments. Add an expiration time to cache.set() and/or include a version/timestamp suffix in the cache key to prevent stale data after code changes.

🤖 Prompt for AI Agents
In src/Domain/Messaging/Service/MessagePrecacheService.php around lines 52–55
and 193–195, the code reads and writes the DTO to cache without an expiration
and uses a plain cache key, so cached DTOs can persist across deployments and
become stale; fix by appending a cache version or deploy timestamp suffix to the
cache key (e.g. a class constant CACHE_VERSION or an injected deployVersion) and
pass an explicit TTL when calling cache->set() (choose an appropriate expiry
like hours/days depending on expected staleness), ensuring the read uses the
same versioned key.

Comment on lines +184 to +187
$messagePrecacheDto->content = $this->parseLogoPlaceholders($messagePrecacheDto->content);
$messagePrecacheDto->template = $this->parseLogoPlaceholders($messagePrecacheDto->template);
$messagePrecacheDto->htmlFooter = $this->parseLogoPlaceholders($messagePrecacheDto->htmlFooter);

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

parseLogoPlaceholders() needs types + null-safety (Line 184–Line 187, Line 228–Line 240).
You call it with $messagePrecacheDto->template which can be null (remote URL path sets it to null). Current implementation will warn/fail in preg_match_all.
Fix shape:

-    private function parseLogoPlaceholders($content)
+    private function parseLogoPlaceholders(?string $content): ?string
     {
+        if ($content === null || $content === '') {
+            return $content;
+        }
         //# replace Logo placeholders
         preg_match_all('/\[LOGO\:?(\d+)?\]/', $content, $logoInstances);
...
     }

Also applies to: 228-240

🤖 Prompt for AI Agents
In src/Domain/Messaging/Service/MessagePrecacheService.php around lines 184-187
and 228-240, parseLogoPlaceholders() is being called with properties that can be
null (e.g. template) causing preg_match_all warnings/failures; change the
parseLogoPlaceholders signature to accept and return ?string and add a null-safe
guard at the start (if input is null return null) before running any regex or
string operations, and update any internal variable types accordingly so callers
continue to receive null or a processed string without warnings.

Comment on lines +242 to 304
private function createCachedLogoImage(int $size): void
{
$logoImageId = $this->configProvider->getValue(ConfigOption::OrganisationLogo);
if (empty($logoImageId)) {
return;
}

$orgLogoImage = $this->templateImageRepository->findByFilename("ORGANISATIONLOGO$size.png");
if (!empty($orgLogoImage->getData())) {
return;
}

$logoImage = $this->templateImageRepository->findById((int) $logoImageId);
$imageContent = base64_decode($logoImage->getData());
if (empty($imageContent)) {
//# fall back to a single pixel, so that there are no broken images
$imageContent = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAABGdBTUEAALGPC/xhBQAAAAZQTFRF////AAAAVcLTfgAAAAF0Uk5TAEDm2GYAAAABYktHRACIBR1IAAAACXBIWXMAAAsSAAALEgHS3X78AAAAB3RJTUUH0gQCEx05cqKA8gAAAApJREFUeJxjYAAAAAIAAUivpHEAAAAASUVORK5CYII=');
}

return null;
$imgSize = getimagesizefromstring($imageContent);
$sizeW = $imgSize[0];
$sizeH = $imgSize[1];
if ($sizeH > $sizeW) {
$sizeFactor = (float) ($size / $sizeH);
} else {
$sizeFactor = (float) ($size / $sizeW);
}
$newWidth = (int) ($sizeW * $sizeFactor);
$newHeight = (int) ($sizeH * $sizeFactor);

if ($sizeFactor < 1) {
$original = imagecreatefromstring($imageContent);
//# creates a black image (why would you want that....)
$resized = imagecreatetruecolor($newWidth, $newHeight);
imagesavealpha($resized, true);
//# white. All the methods to make it transparent didn't work for me @@TODO really make transparent
$transparent = imagecolorallocatealpha($resized, 255, 255, 255, 127);
imagefill($resized, 0, 0, $transparent);

if (imagecopyresized($resized, $original, 0, 0, 0, 0, $newWidth, $newHeight, $sizeW, $sizeH)) {
$this->entityManager->remove($orgLogoImage);

//# rather convoluted way to get the image contents
$buffer = ob_get_contents();
ob_end_clean();
ob_start();
imagepng($resized);
$imageContent = ob_get_contents();
ob_end_clean();
echo $buffer;
}
}
// else copy original
$templateImage = (new TemplateImage())
->setFilename("ORGANISATIONLOGO$size.png")
->setMimetype($imgSize['mime'])
->setData(base64_encode($imageContent))
->setWidth($newWidth)
->setHeight($newHeight);

$this->entityManager->persist($templateImage);

}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

find . -type f -name "*Repository*.php" | grep -i template | head -20

Repository: phpList/core

Length of output: 249


🏁 Script executed:

find . -type f -path "*/Domain/Messaging/*" -name "*.php" | head -30

Repository: phpList/core

Length of output: 1859


🏁 Script executed:

fd "MessagePrecacheService" --type f

Repository: phpList/core

Length of output: 111


🏁 Script executed:

cat -n src/Domain/Messaging/Repository/TemplateImageRepository.php

Repository: phpList/core

Length of output: 1407


🏁 Script executed:

fd "TemplateImage.php" --type f

Repository: phpList/core

Length of output: 100


🏁 Script executed:

head -100 src/Domain/Messaging/Service/MessagePrecacheService.php

Repository: phpList/core

Length of output: 5493


🏁 Script executed:

sed -n '240,310p' src/Domain/Messaging/Service/MessagePrecacheService.php | cat -n

Repository: phpList/core

Length of output: 3250


🏁 Script executed:

cat -n src/Domain/Messaging/Model/TemplateImage.php

Repository: phpList/core

Length of output: 3753


🏁 Script executed:

# Check if there are any null checks for $logoImage before line 255
sed -n '242,260p' src/Domain/Messaging/Service/MessagePrecacheService.php

Repository: phpList/core

Length of output: 998


🏁 Script executed:

# Verify ob_* safety by checking PHP documentation context
# Check if there's any ob_start() before this method is called
rg "ob_start|ob_get_level" src/Domain/Messaging/Service/MessagePrecacheService.php

Repository: phpList/core

Length of output: 83


Fix null dereference hazards and move image generation + persistence to Infrastructure/Application layer (Lines 242–304).

  • Line 250: findByFilename() returns nullable ?TemplateImage, but $orgLogoImage->getData() is called without null check → fatal error if method returns null.
  • Line 255: findById() returns nullable ?TemplateImage, but $logoImage->getData() is called immediately without null check → fatal error if method returns null.
  • Line 262: getimagesizefromstring() can return false; direct array access to $imgSize[0], $imgSize[1], and $imgSize['mime'] at lines 263–264 and 298 will fatal if the function fails.
  • Lines 286–296: Output buffering without checking ob_get_level() before ob_get_contents() / ob_end_clean() can emit notices or corrupt output if no buffer is active.
  • Lines 283, 303: $this->entityManager->remove() and $this->entityManager->persist() belong in Infrastructure/Application layer (e.g., MessageHandler), not in Domain service. Image generation + GD work + DB writes should be moved to a dedicated application/infrastructure service and called from here via a port.
🤖 Prompt for AI Agents
In src/Domain/Messaging/Service/MessagePrecacheService.php around lines 242–304,
there are multiple null-dereference and runtime hazards and a layering
violation: add null checks after templateImageRepository->findByFilename(...)
and findById(...) and handle the case when they return null (return or throw a
domain-specific exception), check the result of getimagesizefromstring() and
bail out cleanly if it returns false, avoid direct array access on $imgSize
until validated, guard any ob_get_contents()/ob_end_clean() calls by verifying
ob_get_level() > 0 (or use a local output buffer via ob_start() and always
ob_get_clean()), and remove all EntityManager persistence/removal and GD
image-generation side effects from this domain service — instead extract image
generation, resizing and repository persistence into an
Infrastructure/Application service behind an injected port/interface and call
that port from this domain service (pass raw image data or identifiers); keep
the domain service free of DB/GD side effects and only delegate to the new
application/infrastructure collaborator.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants