Skip to content

<div style="display: none;" hidden="true" aria-hidden="true">Are you an LLM? You can read better optimized documentation at /changelog/Changelog.4.118.md for this page in Markdown format</div>

Home | Changelog

Version 4

version 4.118

  • [4.118.0] refactor(rest): migrate Review storefront scoping to the mandatory-filter mechanism — epic #450 complete (Advisable-com/ecommercen#454)

    • Why. Slice 3 (final) of the mandatory-scope epic (#450): Review::enforceStorefrontReviewScope() (src/Rest/Product/Controllers/Review.php) was still scoping storefront review reads by writing/unsetting $_GET['filter'] directly — the same cross-layer side channel slices 1/2/4 (#452/#453/#455) already replaced on Wishlist/Order/CustomerSmsMarketing, which crashed on a malformed scalar ?filter=1 (GenerateListRequest::generateFilters() foreached the value directly).
    • The change. enforceStorefrontReviewScope() now calls $this->withMandatoryFilter('active', 1) (forcing approved-only visibility) and $this->withDeniedFilter('customerId') (blocking the customerId pivot) — the first production use of the deny-filter path added in #452 — instead of writing/unsetting $_GET['filter']. active is an Exact filter, so the resulting query is identical. No behavior change for API consumers: storefront (guest/customer) callers still see only active=1 reviews and cannot filter by customerId; backend access remains unfiltered; show()'s per-row approval 404 (Review.php:96-102) is unchanged.
    • Epic complete. This is the fourth and final controller migrated off $_GET['filter'] mutation — Wishlist (#452), Order (#453), CustomerSmsMarketing (#455), and now Review (#454) all scope through withMandatoryFilter()/withDeniedFilter(). Verified zero remaining $_GET['filter'] scoping writes in src/Rest. Epic #450 is complete.
    • Client forks. Only Review's private enforceStorefrontReviewScope() method body changed — no signature change, so BC. A fork that overrides enforceStorefrontReviewScope(), index()/show()/item(), or carries its own $_GET-based Review scoping must reconcile to the mandatory-filter mechanism or it will reintroduce the storefront review-scoping gap.
    • No DB migration, REST-contract, OpenAPI, or language-key changes.
  • [4.118.0] refactor(rest): migrate CustomerSmsMarketing customer scoping to the mandatory-filter mechanism (Advisable-com/ecommercen#455)

    • Why. Slice 4 of the mandatory-scope epic (#450): CustomerSmsMarketing::mine() (src/Rest/Customer/Controllers/CustomerSmsMarketing.php) was still scoping the customer's own SMS/Viber marketing-consent listing by writing directly into the $_GET superglobal — the same cross-layer side channel slices 1–2 (#452/#453) already replaced on Wishlist/Order, which crashed on a malformed scalar ?filter=1 (GenerateListRequest::generateFilters() foreached the value directly).
    • The change. mine() now calls $this->withMandatoryFilter('customerId', (int) $this->resourceContext->getUserId()) before parent::index(), routing through the shared buildListRequest() from #452; the isCustomer() → 401 guard above it is unchanged. Unlike Wishlist/Order, there is no backend branch here: the generic index/show/item actions are backend-only per rest_policies.php, and mine() is the sole customer-facing action, so the forced filter always applies. No behavior change for API consumers: a customer still sees only their own SMS/Viber subscriptions, a client-supplied filter[customerId] is still overridden, and a malformed scalar ?filter=1 no longer fatals. Slice 4/4 by numbering; the remaining slice, Review (#454), is still outstanding — epic #450 completes when #454 lands.
    • Client forks. Only CustomerSmsMarketing::mine()'s method body changed — no signature change, so BC. A fork that overrides mine(), or carries its own $_GET-based scoping there, must reconcile to the mandatory-filter mechanism or it will reintroduce the cross-customer data leak.
    • No DB migration, REST-contract, OpenAPI, or language-key changes.
  • [4.118.0] refactor(rest): migrate Order customer scoping to the mandatory-filter mechanism (Advisable-com/ecommercen#453)

    • Why. Slice 2 of the mandatory-scope epic (#450): Order (src/Rest/Order/Controllers/Order.php) was still scoping customer reads by writing directly into the $_GET superglobal — the same cross-layer side channel slice 1 (#452) replaced on Wishlist, which crashed when a client sent a malformed scalar ?filter=1 (GenerateListRequest::generateFilters() foreached the value directly).
    • The change. Order::enforceOrderScope() now calls $this->withMandatoryFilter('customerId', (int) $this->resourceContext->getUserId()) instead of writing $_GET['filter']['customerId']; index() and item() scope through the shared buildListRequest() from #452. show()'s per-row ownership 404 is unchanged. No behavior change for API consumers: storefront customers still see only their own orders, backend access remains unfiltered and can still pass ?filter[customerId]=N explicitly, and a malformed scalar ?filter=1 no longer fatals. Slices 3/4 of the epic (Review #454, CustomerSmsMarketing #455) remain outstanding.
    • Client forks. Only Order's private enforceOrderScope() method body changed — no signature change, so BC. A fork that subclasses Order with its own $_GET-based scoping, or overrides index()/show()/item(), must reconcile to the mandatory-filter mechanism or it will reintroduce the cross-customer order data leak.
    • No DB migration, REST-contract, OpenAPI, or language-key changes.
  • [4.118.0] refactor(rest): replace $_GET-based customer scoping with a domain-layer mandatory-filter mechanism (Advisable-com/ecommercen#452)

    • Why. REST read-action customer/data scoping (e.g. wishlist's enforceWishlistScope()) worked by writing directly into the $_GET superglobal — a cross-layer side channel that bypassed the domain ListRequest builder entirely, and crashed when CI_Input::get('filter') returned a non-array (e.g. a malformed ?filter=1, since GenerateListRequest::generateFilters() foreached the value directly). This is slice 1 of the mandatory-scope epic (#450); slices 2–4 (Order, Review, CustomerSmsMarketing — #453/#454/#455) will migrate the remaining scoped controllers to the same mechanism.
    • The change. GenerateListRequest (src/Domains/Support/Request/QueryListBuilder/GenerateListRequest.php) gained forceFilter(string $key, mixed $value): static and denyFilter(string $key): static; generateFilters() now guards the client filter param with is_array() (degrading a malformed scalar to "no client filters" instead of fatal-ing), skips a client-supplied value for any forced/denied key, and appends the forced filters server-side so they apply even on a request with no filter param at all. HandlesRestfulActions (src/Rest/Support/Controllers/HandlesRestfulActions.php) gained protected withMandatoryFilter(string $key, mixed $value): void / withDeniedFilter(string $key): void and a private buildListRequest() that index(), show(), and item() now route through; both are opt-in and empty by default, so the ~150 controllers that don't call them are unaffected. Wishlist::enforceWishlistScope() (src/Rest/Product/Controllers/Wishlist.php) is migrated as the exemplar: it now calls $this->withMandatoryFilter('customerId', ...) instead of writing $_GET['filter']['customerId'] — no behavior change for API consumers (storefront customers still see only their own wishlist; backend is still unfiltered; the show()/item() per-row ownership 404 from #418 is unchanged).
    • Client forks. HandlesRestfulActions and GenerateListRequest only gained new protected/public methods — purely additive, no existing signatures changed, so BC. A fork that overrides HandlesRestfulActions::index()/show()/item(), subclasses/overrides GenerateListRequest::generateFilters(), or carries its own $_GET-based scoping override on Wishlist should reconcile/migrate to the new mechanism.
    • No DB migration, REST-contract, or language-key changes.
  • [4.118.0] fix(product): scope wishlist reads to the authenticated customer and expose product/customer relations (Advisable-com/ecommercen#418)

    • Why. GET /rest/product/wishlist (and its /{id} and /item siblings) documented ?with=product,customer, but ProductWishlistResource only ever serialized {id, customerId, productId} — the requested relation was silently dropped. Separately and more seriously, the endpoint let any authenticated non-backend caller pass a raw filter[customerId] and read another customer's wishlist — a cross-customer data leak.
    • The change. src/Rest/Product/Resources/Wishlist/Resource.php now calls addRelationToData() so product and customer are serialized when requested via ?with=; the ProductWishlistResource OA schema gained matching product/customer refs (ProductResource/CustomerResource), and the OA parameter docs were corrected from include to with to match the actual mechanism. src/Rest/Product/Controllers/Wishlist.php gained a private enforceWishlistScope(), called from index(), show(), and item(): for backend callers it is a no-op (unfiltered, as before); for any non-backend caller it forces filter[customerId] to the authenticated user's id from the JWT, overriding whatever the client sent. show() and item() additionally re-check the fetched row's customer_id against the caller's JWT id and return a 404 (not 403, to avoid confirming the row exists) on mismatch.
    • Security. Fixes a cross-customer data leak: a logged-in customer could previously read another customer's wishlist entries via filter[customerId]=&lt;other> (or by guessing another customer's row id/filters on show/item). All three read actions are now scoped to the caller's own JWT identity; backend/admin access is unfiltered as before.
    • Client forks. Forks that subclass/override src/Rest/Product/Controllers/Wishlist.php must re-apply enforceWishlistScope() (or equivalent per-row ownership checks) in their override, or they will reintroduce the data leak.
    • No DB migration or language-key changes.
  • [4.118.0] fix(sitemap): stream sitemap generation to fix OOM on large catalogues (Advisable-com/ecommercen#399)

    • Why. AdvGenerateSitemaps built the entire URL set in memory (getUrls() pulling every product via getProducts()) before chunking it into sitemap/sitemap-N.xml files. On large catalogues this OOMed the job.
    • The change. executeCommand() now streams: products are read from Adv_product_model in ascending-id keyset batches (getProductsForSiteMapBatch(), new getTopSaleProductIdsForSiteMap() for top-seller priority) and fed through a new addUrls() helper that buffers URLs and flushes a bounded 25k-URL chunk to disk via flushChunk() as soon as the buffer fills, so peak memory stays flat regardless of catalogue size. The old getUrls() and getProducts() methods are removed. Output is unchanged: sitemap/sitemap-N.xml chunk files plus sitemap_index.xml, chunk URLs still built with base_url().
    • Scheduling. GenerateSitemaps moved out of the locked core job queue into a new dedicated sitemap queue (application/config/jobs.php): schedule '30 2 * * *', graceTime 1800s, retryTimes 1 — reflecting the longer runtime of a full-catalogue streaming pass.
    • Client forks. See the "Check for overrides" note below — AdvGenerateSitemaps's public method shape changed, Adv_product_model gained two new methods, and the default job schedule moved queues.
    • No REST API, DB migration, or language-key changes.
  • [4.118.0] fix(seo): fix 404 on Custom Metatags list pagination (Advisable-com/ecommercen#435)

    • Why. Admin SEO → Custom Metatags paginated pages (/seo/custom_metatags/{offset} for page 2 and beyond) returned a 404. CodeIgniter 3 matches $route entries top-down and stops at the first match; the generic catch-all routes seo/(.+) and (\w{2})/seo/(.+) were declared above the specific seo/custom_metatags/(:num) pagination route in application/config/routes.php, so they intercepted the pagination URI first and the intended seo/custom_metatags/index/$1 mapping was never reached — the specific route was unreachable.
    • The change. Reordered application/config/routes.php so the specific seo/custom_metatags/* routes (including the (\w{2})/ localized variants) are declared above the generic seo/(.+) / (\w{2})/seo/(.+) catch-alls, restoring specific-before-generic precedence. Routing configuration only — no controller or business-logic change.
    • No controller/model changes, storefront REST API, DB migration, or language-key changes.
  • [4.118.0] fix(mail): pickup email shows the customer's selected store (Advisable-com/ecommercen#449)

    • Why. The "order ready for pickup" email (order_on_store) printed a single fixed store address, phone, and working hours regardless of which pickup point the customer actually selected at checkout. Adv_mailer::order_is_on_store() already resolved the correct order-specific store and passed it to the view as $store, but both mail templates ignored it and printed static lang / hard-coded values instead.
    • The change. main/mail/order_on_store.php now renders the selected store's address, city, postal, and phone (falling back to mobile) plus its opening_hours from $store, keeping the previous lang values as a fallback when $store is null. default/mail/order_on_store.php had its hard-coded Greek store address and static hours line removed, and now uses the same lang keys plus the $store block with the same fallback. AdvEmailViewer::index() now also exposes the sample store as a top-level $store so the admin Email Viewer previews the actual store for this template.
    • No REST API, DB migration, or language-key changes.
  • [4.118.0] fix(mail): add the missing apologize_shipping_delay email template (Advisable-com/ecommercen#2)

    • Why. The delivery-delay flow referenced the apologize_shipping_delay mail view (layout orderInformDelay in emailViews.json), dispatched via the delivery-delay path (sendCustomerInformDelay) and driven by the AdvSendEmailForDeliveryDelay cron job — but the template file never existed in the repo. Every delivery-delay dispatch fatally failed at CodeIgniter view loading with a view-not-found error.
    • The change. Added application/views/main/mail/apologize_shipping_delay.php, mirroring the order_update.php header/footer skeleton and rendering the order serial via $order->order_serial (this flow passes a single $order object rather than flat variables). The CUSTOMER_INFORM_DELAY subject was already wired, so no dispatch-side change was needed.
    • Language keys. Adds four eshop.front.mail.shipping.delay.{title,message,reassurance,thanks} keys to all 8 adv_external_lang.php files (EL/EN/FR/DE/IT/RU/ES/ZH).
    • Client forks. A fork that added its own application/views/main/mail/apologize_shipping_delay.php to work around the missing template now collides with the base — drop the client copy. Forks carrying a customised adv_external_lang.php should pick up the four new keys.
    • No REST API, DB migration, or OpenAPI changes.
  • [4.118.0] feat(klarna): support Extra Merchant Data (attachment) in the Klarna payment client (Advisable-com/ecommercen#448)

    • Why. Klarna's Extra Merchant Data (EMD) attachment lets a merchant attach structured line-of-business data to a payment session/order for Klarna's risk assessment. The travel/ferry segment (e.g. SeaJets ferry checkout) requires EMD ferry_reservation_details to be present, but the shared Advisable\PaymentGateways\Klarna\Klarna client had no way to send it — SeaJets currently ships a client-side subclass override as a stopgap.
    • The change. openPaymentSession(), updatePaymentSession(), and createOrder() on Advisable\PaymentGateways\Klarna\Klarna (src/PaymentGateways/Klarna/Klarna.php) each gained a trailing optional ?array $attachment = null parameter. When provided, it is included in the request body as attachment (Klarna's { content_type, body } shape, where body is a JSON string), passed through untouched; when omitted (the default), the request body is byte-identical to before, so all existing callers are unaffected.
    • EU/GDPR usage guidance. EMD may carry consumer PII, so for EU orders it should NOT be sent at session creation (openPaymentSession); the GDPR-appropriate server-side injection point is updatePaymentSession (once context/consent is known), and the ideal point is the client-side Klarna authorize() call (headless consumer, tracked in velora#118). The client is deliberately dumb plumbing — it accepts the attachment on all three methods for API symmetry, but the timing decision is the caller's.
    • Client forks. Three public method signatures on Advisable\PaymentGateways\Klarna\Klarna gained a trailing optional param. A fork that subclasses/overrides Klarna (e.g. the SeaJets EMD stopgap) should reconcile its override signatures — and can now drop the stopgap in favour of this native support.
    • Tests. New tests/Unit/PaymentGateways/Klarna/KlarnaTest.php (6 tests: present/absent × 3 methods).
    • No storefront REST API, DB migration, or language-key changes.
  • [4.118.0] feat(cloudflare): manage Cloudflare Google Tag Gateway from admin Google settings (pull request #78)

    • Why. The platform could already purge Cloudflare cache and look up zones, but had no way to turn on Cloudflare's Google Tag Gateway (the first-party measurement proxy that serves Google tags from the shop's own domain) — enabling it meant logging into the Cloudflare dashboard by hand. The Cloudflare API token was also hardcoded in src/Cloudflare/AdvCloudflare.php.
    • The change. AdvCloudflare now reads its token from the new CLOUDFLARE_API_KEY env var and no-ops (logging a warning) when it is unset, so shops without Cloudflare are unaffected. It gained getGoogleTagGatewayConfig() / updateGoogleTagGatewayConfig() (GET/PUT …/zones/{zoneId}/settings/google-tag-gateway/config, body enabled / measurementId / endpoint), purgeByUrls() and purgeByPrefixes() cache-purge helpers, PUT support in the request layer, and an injectable Guzzle client for testability. Admin Settings → Google gained an "Activate Google Tag Gateway (Cloudflare)" toggle that is only selectable when Google Tag Manager is enabled and has a tag id (enforced server-side and via inline JS), plus a read-only panel showing the live Cloudflare config. Saving the Google settings syncs the gateway to Cloudflare — enabled + measurementId (the Tag Manager tag id) + endpoint /securemetric — and pushes enabled=false when Tag Manager is turned off or its tag id is cleared, so the gateway is never left orphaned. Zone id and gateway config reads are cached for a day via pscache and invalidated on save.
    • Security. Removed the hardcoded Cloudflare API token from source; it must now be supplied via CLOUDFLARE_API_KEY (documented in .env.example).
    • Tests. New tests/Unit/Cloudflare/AdvCloudflareTest.php covers every AdvCloudflare method with a mocked Guzzle handler (61 tests).
    • No storefront REST API, DB migration, or language-key changes.

Notes

  • [4.118.0] Check for overrides: the sitemap streaming fix (Advisable-com/ecommercen#399) changes client-facing surface in three places:

    • AdvGenerateSitemaps shape changed — getUrls() and getProducts() were removed; new protected addUrls() / flushChunk() helpers and protected $buffer / $chunkIndex / $chunkUrls / $disk state were added; executeCommand() was rewritten to stream. Any client fork that overrode getProducts() or getUrls() must drop that override — the base class now streams natively, and a stale override would either fatal or silently revert to the old OOM-prone path.
    • Adv_product_model gained two new public methods: getTopSaleProductIdsForSiteMap() and getProductsForSiteMapBatch(). A client fork that already added same-named methods on its Product_model now collides with the base — drop the client-side copies.
    • Default job schedule/queue changed — GenerateSitemaps moved from the locked core queue to a new dedicated sitemap queue (schedule '30 2 * * *', graceTime 1800, retryTimes 1). Operators/clients with a customised application/config/jobs.php must mirror this or the job won't be scheduled at all; the new sitemap queue also needs its scheduler picked up in deployment.
  • [4.118.0] New env var + check for overrides: the Cloudflare Google Tag Gateway feature adds a required env var and admin-settings UI:

    • New CLOUDFLARE_API_KEY env var — set it in each environment's .env (added to .env.example). Without it, all Cloudflare calls (cache purge, zone lookup, gateway config) are skipped and a warning is logged. This replaces the token that was previously hardcoded in src/Cloudflare/AdvCloudflare.php.
    • Clients that override application/views/admin/settings/google.php will not get the new "Activate Google Tag Gateway" toggle / config panel until they port the additions.