Appearance
<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>
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 onWishlist/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 thecustomerIdpivot) — the first production use of the deny-filter path added in #452 — instead of writing/unsetting$_GET['filter'].activeis anExactfilter, so the resulting query is identical. No behavior change for API consumers: storefront (guest/customer) callers still see onlyactive=1reviews and cannot filter bycustomerId; 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 throughwithMandatoryFilter()/withDeniedFilter(). Verified zero remaining$_GET['filter']scoping writes insrc/Rest. Epic #450 is complete. - Client forks. Only
Review'sprivate enforceStorefrontReviewScope()method body changed — no signature change, so BC. A fork that overridesenforceStorefrontReviewScope(),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.
- Why. Slice 3 (final) of the mandatory-scope epic (#450):
[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$_GETsuperglobal — the same cross-layer side channel slices 1–2 (#452/#453) already replaced onWishlist/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())beforeparent::index(), routing through the sharedbuildListRequest()from #452; theisCustomer()→ 401 guard above it is unchanged. UnlikeWishlist/Order, there is no backend branch here: the genericindex/show/itemactions are backend-only perrest_policies.php, andmine()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-suppliedfilter[customerId]is still overridden, and a malformed scalar?filter=1no 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 overridesmine(), 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.
- Why. Slice 4 of the mandatory-scope epic (#450):
[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$_GETsuperglobal — the same cross-layer side channel slice 1 (#452) replaced onWishlist, 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()anditem()scope through the sharedbuildListRequest()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]=Nexplicitly, and a malformed scalar?filter=1no longer fatals. Slices 3/4 of the epic (Review #454, CustomerSmsMarketing #455) remain outstanding. - Client forks. Only
Order'sprivate enforceOrderScope()method body changed — no signature change, so BC. A fork that subclassesOrderwith its own$_GET-based scoping, or overridesindex()/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.
- Why. Slice 2 of the mandatory-scope epic (#450):
[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$_GETsuperglobal — a cross-layer side channel that bypassed the domainListRequestbuilder entirely, and crashed whenCI_Input::get('filter')returned a non-array (e.g. a malformed?filter=1, sinceGenerateListRequest::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) gainedforceFilter(string $key, mixed $value): staticanddenyFilter(string $key): static;generateFilters()now guards the clientfilterparam withis_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 nofilterparam at all.HandlesRestfulActions(src/Rest/Support/Controllers/HandlesRestfulActions.php) gained protectedwithMandatoryFilter(string $key, mixed $value): void/withDeniedFilter(string $key): voidand a privatebuildListRequest()thatindex(),show(), anditem()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; theshow()/item()per-row ownership 404 from #418 is unchanged). - Client forks.
HandlesRestfulActionsandGenerateListRequestonly gained newprotected/publicmethods — purely additive, no existing signatures changed, so BC. A fork that overridesHandlesRestfulActions::index()/show()/item(), subclasses/overridesGenerateListRequest::generateFilters(), or carries its own$_GET-based scoping override onWishlistshould reconcile/migrate to the new mechanism. - No DB migration, REST-contract, or language-key changes.
- Why. REST read-action customer/data scoping (e.g. wishlist's
[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/itemsiblings) documented?with=product,customer, butProductWishlistResourceonly 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 rawfilter[customerId]and read another customer's wishlist — a cross-customer data leak. - The change.
src/Rest/Product/Resources/Wishlist/Resource.phpnow callsaddRelationToData()soproductandcustomerare serialized when requested via?with=; theProductWishlistResourceOA schema gained matchingproduct/customerrefs (ProductResource/CustomerResource), and the OA parameter docs were corrected fromincludetowithto match the actual mechanism.src/Rest/Product/Controllers/Wishlist.phpgained a privateenforceWishlistScope(), called fromindex(),show(), anditem(): for backend callers it is a no-op (unfiltered, as before); for any non-backend caller it forcesfilter[customerId]to the authenticated user's id from the JWT, overriding whatever the client sent.show()anditem()additionally re-check the fetched row'scustomer_idagainst 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]=<other>(or by guessing another customer's row id/filters onshow/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.phpmust re-applyenforceWishlistScope()(or equivalent per-row ownership checks) in their override, or they will reintroduce the data leak. - No DB migration or language-key changes.
- Why.
[4.118.0] fix(sitemap): stream sitemap generation to fix OOM on large catalogues (Advisable-com/ecommercen#399)
- Why.
AdvGenerateSitemapsbuilt the entire URL set in memory (getUrls()pulling every product viagetProducts()) before chunking it intositemap/sitemap-N.xmlfiles. On large catalogues this OOMed the job. - The change.
executeCommand()now streams: products are read fromAdv_product_modelin ascending-id keyset batches (getProductsForSiteMapBatch(), newgetTopSaleProductIdsForSiteMap()for top-seller priority) and fed through a newaddUrls()helper that buffers URLs and flushes a bounded 25k-URL chunk to disk viaflushChunk()as soon as the buffer fills, so peak memory stays flat regardless of catalogue size. The oldgetUrls()andgetProducts()methods are removed. Output is unchanged:sitemap/sitemap-N.xmlchunk files plussitemap_index.xml, chunk URLs still built withbase_url(). - Scheduling.
GenerateSitemapsmoved out of the lockedcorejob queue into a new dedicatedsitemapqueue (application/config/jobs.php):schedule '30 2 * * *',graceTime1800s,retryTimes1 — 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_modelgained two new methods, and the default job schedule moved queues. - No REST API, DB migration, or language-key changes.
- Why.
[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$routeentries top-down and stops at the first match; the generic catch-all routesseo/(.+)and(\w{2})/seo/(.+)were declared above the specificseo/custom_metatags/(:num)pagination route inapplication/config/routes.php, so they intercepted the pagination URI first and the intendedseo/custom_metatags/index/$1mapping was never reached — the specific route was unreachable. - The change. Reordered
application/config/routes.phpso the specificseo/custom_metatags/*routes (including the(\w{2})/localized variants) are declared above the genericseo/(.+)/(\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.
- Why. Admin SEO → Custom Metatags paginated pages (
[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.phpnow renders the selected store's address, city, postal, and phone (falling back to mobile) plus itsopening_hoursfrom$store, keeping the previous lang values as a fallback when$storeis null.default/mail/order_on_store.phphad its hard-coded Greek store address and static hours line removed, and now uses the same lang keys plus the$storeblock with the same fallback.AdvEmailViewer::index()now also exposes the sample store as a top-level$storeso the admin Email Viewer previews the actual store for this template. - No REST API, DB migration, or language-key changes.
- Why. The "order ready for pickup" email (
[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_delaymail view (layoutorderInformDelayinemailViews.json), dispatched via the delivery-delay path (sendCustomerInformDelay) and driven by theAdvSendEmailForDeliveryDelaycron 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 theorder_update.phpheader/footer skeleton and rendering the order serial via$order->order_serial(this flow passes a single$orderobject rather than flat variables). TheCUSTOMER_INFORM_DELAYsubject 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 8adv_external_lang.phpfiles (EL/EN/FR/DE/IT/RU/ES/ZH). - Client forks. A fork that added its own
application/views/main/mail/apologize_shipping_delay.phpto work around the missing template now collides with the base — drop the client copy. Forks carrying a customisedadv_external_lang.phpshould pick up the four new keys. - No REST API, DB migration, or OpenAPI changes.
- Why. The delivery-delay flow referenced the
[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)
attachmentlets 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 EMDferry_reservation_detailsto be present, but the sharedAdvisable\PaymentGateways\Klarna\Klarnaclient had no way to send it — SeaJets currently ships a client-side subclass override as a stopgap. - The change.
openPaymentSession(),updatePaymentSession(), andcreateOrder()onAdvisable\PaymentGateways\Klarna\Klarna(src/PaymentGateways/Klarna/Klarna.php) each gained a trailing optional?array $attachment = nullparameter. When provided, it is included in the request body asattachment(Klarna's{ content_type, body }shape, wherebodyis 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 isupdatePaymentSession(once context/consent is known), and the ideal point is the client-side Klarnaauthorize()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
publicmethod signatures onAdvisable\PaymentGateways\Klarna\Klarnagained a trailing optional param. A fork that subclasses/overridesKlarna(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.
- Why. Klarna's Extra Merchant Data (EMD)
[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.
AdvCloudflarenow reads its token from the newCLOUDFLARE_API_KEYenv var and no-ops (logging a warning) when it is unset, so shops without Cloudflare are unaffected. It gainedgetGoogleTagGatewayConfig()/updateGoogleTagGatewayConfig()(GET/PUT…/zones/{zoneId}/settings/google-tag-gateway/config, bodyenabled/measurementId/endpoint),purgeByUrls()andpurgeByPrefixes()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 pushesenabled=falsewhen 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 viapscacheand 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.phpcovers everyAdvCloudflaremethod with a mocked Guzzle handler (61 tests). - No storefront REST API, DB migration, or language-key changes.
- 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
Notes
[4.118.0] Check for overrides: the sitemap streaming fix (Advisable-com/ecommercen#399) changes client-facing surface in three places:
AdvGenerateSitemapsshape changed —getUrls()andgetProducts()were removed; newprotected addUrls()/flushChunk()helpers andprotected$buffer/$chunkIndex/$chunkUrls/$diskstate were added;executeCommand()was rewritten to stream. Any client fork that overrodegetProducts()orgetUrls()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_modelgained two new public methods:getTopSaleProductIdsForSiteMap()andgetProductsForSiteMapBatch(). A client fork that already added same-named methods on itsProduct_modelnow collides with the base — drop the client-side copies.- Default job schedule/queue changed —
GenerateSitemapsmoved from the lockedcorequeue to a new dedicatedsitemapqueue (schedule '30 2 * * *',graceTime1800,retryTimes1). Operators/clients with a customisedapplication/config/jobs.phpmust mirror this or the job won't be scheduled at all; the newsitemapqueue 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_KEYenv 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 insrc/Cloudflare/AdvCloudflare.php. - Clients that override
application/views/admin/settings/google.phpwill not get the new "Activate Google Tag Gateway" toggle / config panel until they port the additions.
- New