feat(media): AI image labeling for Art. 50(4) AI Act compliance #10

Merged
csaeum merged 5 commits from feature/ai-image-labeling-media-custom-fields into main 2026-08-02 07:57:13 +00:00
Owner

Summary

Implements KI-Bildkennzeichnung (AI image labeling) required by Art. 50(4) AI Act
(effective 2026-08-02): AI-generated/-modified product images must be visibly
labeled in the shop. Covers all plugin-side work; imagor infrastructure and
shop-level config (#8, #9) live in separate Docker-Stacks repos and are tracked
there.

  • #4 — New media custom field set (ai_status auto-flag, ai_type manual
    classification incl. false-positive correction option), CustomFieldsInstaller
    generalized to support multiple field sets. Also fixes a pre-existing
    services.xml bug (missing Connection argument) and repairs the previously
    broken CustomFieldsInstallerTest.
  • #5 — Async C2PA upload scan: MediaUploadedEvent subscriber dispatches a
    non-blocking Messenger message (routed to the existing async transport, no
    new worker needed), handler shells out to c2patool to detect AI
    digitalSourceType and sets ai_status without ever clobbering a manual
    ai_type correction.
  • #6 — ImagorUrlBuilder + RemoteThumbnailUrlSubscriber hook into
    Shopware's ResolveRemoteThumbnailUrlExtension (verified against
    developer.shopware.com and the installed 6.7.10.2 core) to deliver
    watermarked thumbnails via imagor.
  • #7 — ImagorMediaUrlGenerator decorates AbstractMediaUrlGenerator so the
    storefront <img src> fallback (not just srcset thumbnails) is also
    watermarked — confirmed via compiled-container inspection that this is a
    separate core extension point from #6.
  • Bonus fix: ProductPageSubscriberTest never actually ran (PHPUnit 9 API
    addMethods(), removed in PHPUnit 10+, silently masked since PHPUnit wasn't
    installed in the DDEV shop at all before this branch). Fixed to mock the real
    MetaInformation class, which also exposed a second stale-test bug (wrong
    SystemConfigService key) that's fixed alongside.

Closes #4, closes #5, closes #6, closes #7.

Test plan

  • php -l on all new/changed files
  • Container compiles cleanly after every step (plugin:update)
  • Real DB verification in DDEV: both custom field sets + correct
    entity_name relations + field types + select options
  • debug:messenger confirms ScanMediaForAiContentMessage → handled by
    ScanMediaForAiContentHandler
  • debug:event-dispatcher confirms MediaUploadSubscriber wired to
    media.uploaded
  • debug:container confirms AbstractMediaUrlGenerator is aliased to
    ImagorMediaUrlGenerator and used by all real core consumers
    (MediaUrlLoader, RemoteThumbnailLoader, DownloadResponseGenerator, ...)
  • Full plugin unit test suite green: 41/41, 109 assertions
  • Full C2PA positive-path test (real image with manifest → ai_status=true)
    blocked on c2patool being available in a container — tracked in #9
  • End-to-end watermark rendering in a real browser blocked on imagor
    instance + shopware.media.remote_thumbnails.enable=true — tracked in #8/#9

https://claude.ai/code/session_013ErZKviNQ71DNFxYYU1Y32

## Summary Implements KI-Bildkennzeichnung (AI image labeling) required by Art. 50(4) AI Act (effective 2026-08-02): AI-generated/-modified product images must be visibly labeled in the shop. Covers all plugin-side work; imagor infrastructure and shop-level config (#8, #9) live in separate Docker-Stacks repos and are tracked there. - **#4** — New `media` custom field set (`ai_status` auto-flag, `ai_type` manual classification incl. false-positive correction option), `CustomFieldsInstaller` generalized to support multiple field sets. Also fixes a pre-existing `services.xml` bug (missing `Connection` argument) and repairs the previously broken `CustomFieldsInstallerTest`. - **#5** — Async C2PA upload scan: `MediaUploadedEvent` subscriber dispatches a non-blocking Messenger message (routed to the existing `async` transport, no new worker needed), handler shells out to `c2patool` to detect AI `digitalSourceType` and sets `ai_status` without ever clobbering a manual `ai_type` correction. - **#6** — `ImagorUrlBuilder` + `RemoteThumbnailUrlSubscriber` hook into Shopware's `ResolveRemoteThumbnailUrlExtension` (verified against developer.shopware.com and the installed 6.7.10.2 core) to deliver watermarked thumbnails via imagor. - **#7** — `ImagorMediaUrlGenerator` decorates `AbstractMediaUrlGenerator` so the storefront `<img src>` fallback (not just `srcset` thumbnails) is also watermarked — confirmed via compiled-container inspection that this is a separate core extension point from #6. - Bonus fix: `ProductPageSubscriberTest` never actually ran (PHPUnit 9 API `addMethods()`, removed in PHPUnit 10+, silently masked since PHPUnit wasn't installed in the DDEV shop at all before this branch). Fixed to mock the real `MetaInformation` class, which also exposed a second stale-test bug (wrong `SystemConfigService` key) that's fixed alongside. Closes #4, closes #5, closes #6, closes #7. ## Test plan - [x] `php -l` on all new/changed files - [x] Container compiles cleanly after every step (`plugin:update`) - [x] Real DB verification in DDEV: both custom field sets + correct `entity_name` relations + field types + select options - [x] `debug:messenger` confirms `ScanMediaForAiContentMessage` → handled by `ScanMediaForAiContentHandler` - [x] `debug:event-dispatcher` confirms `MediaUploadSubscriber` wired to `media.uploaded` - [x] `debug:container` confirms `AbstractMediaUrlGenerator` is aliased to `ImagorMediaUrlGenerator` and used by all real core consumers (`MediaUrlLoader`, `RemoteThumbnailLoader`, `DownloadResponseGenerator`, ...) - [x] Full plugin unit test suite green: 41/41, 109 assertions - [ ] Full C2PA positive-path test (real image with manifest → `ai_status=true`) blocked on `c2patool` being available in a container — tracked in #9 - [ ] End-to-end watermark rendering in a real browser blocked on imagor instance + `shopware.media.remote_thumbnails.enable=true` — tracked in #8/#9 https://claude.ai/code/session_013ErZKviNQ71DNFxYYU1Y32
Generalize CustomFieldsInstaller to manage multiple custom field sets
and add a new "media" set with ai_status (auto-detected via upcoming
C2PA scan) and ai_type (manual admin classification) fields, required
by Art. 50(4) AI Act image labeling. Also fixes the services.xml
CustomFieldsInstaller definition (missing Connection argument) and the
pre-existing broken CustomFieldsInstallerTest (constructor mismatch,
outdated assertions).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ErZKviNQ71DNFxYYU1Y32
Add a MediaUploadedEvent subscriber that dispatches a non-blocking
Messenger message (routed to the existing "async" transport) on every
upload. The handler shells out to c2patool to check for an embedded
C2PA/Content Credentials manifest with an AI-related digitalSourceType
and sets ai_status accordingly, never overwriting a manual ai_type
correction. c2patool itself is an infrastructure dependency tracked
separately (#9).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ErZKviNQ71DNFxYYU1Y32
Add ImagorUrlBuilder as the single place building imagor URLs from a
source URL, target size and media custom fields, applying a German
text watermark (label filter) based on ai_type - or a generic fallback
label when ai_status was auto-detected but not yet manually classified.
Hook it into Shopware's ResolveRemoteThumbnailUrlExtension (verified
against the installed 6.7.10.2 core and developer.shopware.com) via
RemoteThumbnailUrlSubscriber, which requires stopPropagation() after
setting the result or Shopware's default pattern replacement overwrites
it again. New imagorBaseUrl sales-channel-scoped config field; empty by
default so image delivery is unchanged until an imagor instance (#8) is
configured.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ErZKviNQ71DNFxYYU1Y32
ResolveRemoteThumbnailUrlExtension (#6) only covers thumbnail sizes;
the storefront's <img src> fallback renders the base media URL via
AbstractMediaUrlGenerator, a separate core extension point (verified
against developer.shopware.com and the installed 6.7.10.2 source).
Without decorating it, the unwatermarked original would still be
served as the src fallback. ImagorMediaUrlGenerator decorates the core
generator, batch-loads ai_status/ai_type for the requested media IDs
in one query (UrlParams carries no Context/language), and only touches
UrlParamsSource::MEDIA entries - thumbnails stay on the #6 path.
Confirmed via compiled-container inspection that RemoteThumbnailLoader
also calls this generator for base URLs even with remote thumbnails
enabled, so this closes the gap in both modes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ErZKviNQ71DNFxYYU1Y32
MockBuilder::addMethods() was removed in PHPUnit 10+, so the test
never ran at all with a modern PHPUnit (it just wasn't caught before
since PHPUnit wasn't installed in the DDEV shop). Mock the real
MetaInformation class instead of stdClass+addMethods(). Getting the
suite to actually execute for the first time also exposed a second,
previously invisible bug: the robots-inheritance test mocked the wrong
SystemConfigService key (robotsRules instead of the real metaRobots
constant used by ProductPageSubscriber).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013ErZKviNQ71DNFxYYU1Y32
csaeum merged commit d843e986a4 into main 2026-08-02 07:57:13 +00:00
csaeum deleted branch feature/ai-image-labeling-media-custom-fields 2026-08-02 07:57:13 +00:00
csaeum referenced this pull request from a commit 2026-08-02 08:10:08 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
SW-Plugins/wsc_swplugin_aiseotools!10
No description provided.