Appearance
<div style="display: none;" hidden="true" aria-hidden="true">Are you an LLM? You can read better optimized documentation at /changelog/Changelog.4.107.md for this page in Markdown format</div>
Version 4
version 4.107
[4.107.6] fix(settings): expose
GLOBAL/STOREFRONT_BASE_URLin admin Settings — missing UI from #194/#327 (Advisable-com/ecommercen#335)- The gap. #194/#327 (shipped in 4.107.0) introduced the
storefront_url()helper and theGLOBAL/STOREFRONT_BASE_URLregistry key (migration20260612120000_add_storefront_base_url_registry_key, created empty) for headless deployments, but shipped no admin UI to set it — leaving the configuration surface for that feature incomplete, so operators had to edit theregistrytable by raw SQL and flush the cache manually. There is no generic registry editor in admin; each settings page persists a hardcoded set of keys. - What shipped. A "Storefront base URL" text field was added to the Global section of admin Settings, mirroring the existing
GLOBAL/CDN_URLfield exactly.ecommercen/settings/controllers/Adv_settings.phpnow savesSTOREFRONT_BASE_URL(POSTglobal_storefront_base_url), exposes its current value to the view, and registers atrimvalidation rule alongsideglobal_cdn_url. The viewapplication/views/admin/settings/settings.phpgained the input plus help text. - Behaviour. An empty value preserves the
base_url()fallback inStorefrontUrl(trim((string) value(...))), so single-domain deployments are unaffected. No manual cache flush is needed: the save redirects back to an admin request, whoseAdv_admin_controllerconstructor calls$this->registry->live()— refreshing theregistry_model::allpscache from the DB — identical to howCDN_URLalready works. - Localized. The field label and help text are translated via two new
t()keys —eshop.admin.settings.storefront.base.url.label/.help— added to all 8 locale files (ecommercen/language/*/adv_advisable_lang.php). (The sibling "CDN url" field remains hardcoded; the new field is fully i18n'd.) - Related: #194, #327.
- The gap. #194/#327 (shipped in 4.107.0) introduced the
[4.107.5] fix(auth): legacy recover-password form no longer discloses whether an email is registered (Advisable-com/ecommercen#323)
- The bug. The legacy storefront recover-password form confirmed account existence:
Adv_customer::recoverPasswordEmailValidation()was acallback_validation rule on the email field that showed a visible "email not found in the customer database" error for unknown addresses while known addresses got the success message — an account-enumeration oracle scriptable against the HTML form. The REST surface was fixed in #193 (API v1.10, generic 200 either way), but the storefront form was outside that scope. - Fix. Dropped the existence-check callback (kept
required|valid_email) and routed the request throughPasswordResetService::requestReset()— the same service backing the REST endpoint, which looks up the customer and silently skips unknown addresses (and guardsrecover_password_mail()against its null-deref on an unknown email). Both storefront themes now render the existing neutral confirmation (customer.password.recover.alert.success) whether or not the address exists; the oracle method was removed. No new lang key needed — the success message was already neutral. The silent-skip property is covered by the existingPasswordResetServiceTest. - Residual (unchanged, as on the REST path). A timing side-channel remains (known emails do a DB write + SMTP send). The legacy form still has no server-side rate limiting (the REST endpoint got
rest.auth.password_reset_throttlein #193) — noted as a follow-up; a per-IP limiter also depends on theip_address()trust fix (#322).
- The bug. The legacy storefront recover-password form confirmed account existence:
[4.107.4] fix(domains): translation
name.{lang}sort across the REST surface (Advisable-com/ecommercen#333)- The bug. A root sort by a translated column —
?sort=name.{lang}/slug.{lang}, declared in manyListRequest::setAllowedSorts()— resolved (viaGenerateListRequest's fallback) to a rootSortRequestwithfield = shop_*_mui.name,relation = translations, and broke three ways: (1) the locale was never carried onto theSortRequest(it had nolanguageproperty), so the per-domain branch passednullintoSortByTranslation's typedstring $locale→TypeErrorin the ~17 domains that have the branch; (2) the branch extracted the field withexplode('.', $sort->field)[0], which is the table (shop_*_mui), not the column; (3) Category and Vendor had no translation-sort branch at all, so a bareSorton the unjoined*_muicolumn returned empty (db_debugoff) or1054(on). This is the latent sibling of the #330 translation-filter fix. - Fix. Added
?string $languagetoSortRequestand populated it inGenerateListRequest(root-translation fallback = the trailing key segment; both relation-sort branches). Fixed the field extraction to take the column (trailing segment) in all 17 Services with the branch. AddedSortByTranslationspecs + therelation === 'translations'branch to Category and Vendor, mirroring #330'sFilterByTranslation. Additive — non-translation sorts are unchanged. Not reachable via the MCP connector (its list tools pass no sort); affects the REST surface. - Out of scope. The relation-sort form
translations.name.{lang}takes a different,relationPath-based path (locale carried viaSortRequest::$class) and is unchanged. - Tests: a config-free
GenerateListRequestunit test (locale carried on both root and relation translation sorts) + ascending/descending integration tests for Category, Vendor, and Cms/Page (the sampled domain for the shared 17-domain field-extraction fix).
- The bug. A root sort by a translated column —
[4.107.3] docs(mcp): complete the MCP connector onboarding guide — add the missing screenshots (Advisable-com/ecommercen#321)
- The Greek customer onboarding guide (
mcp-connector-onboarding-el.md) referenced three screenshots underimages/mcp-connector/that were never committed (the subfolder was missing and one file carried a doubled.png.pngextension), which broke the docs site build. The screenshots are now in place, so the onboarding guide renders and the docs deploy succeeds — closing the documentation deliverable of the MCP Connector MVP epic (#312).
- The Greek customer onboarding guide (
[4.107.2] fix(domains): translation
name.{lang}search on categories and brands (Advisable-com/ecommercen#330)- The bug. Searching categories (
list_categories) or brands (list_brands) by name — and any REST client filtering Category/Vendor byname.{lang}/slug.{lang}— fataled with MySQL 1054Unknown column 'shop_*_mui.name' in 'where clause'. TheirService::buildSpecifications()built a plainFilterfor every filter and ignoredFilterRequest::$relation, so a translation field emitted a bareWHEREagainst the*_muitable, which isn't in theFROMclause (BaseRepository does not auto-join on the relation). - Fix. Category and Vendor were the two product-domains missing the
FilterByTranslationsubquery handling that Product and ~15 other domains already use. Added their specs (locale-awareid IN (SELECT <fk> FROM <mui> WHERE lang = ? AND … LIKE ?), single-passescape_like_str+ESCAPE '!'so%/_stay literal) and routedrelation === 'translations'filters to them. Additive — non-translation filters are unchanged. Integration tests (4 each) cover match, language scoping, the%-escape regression, and no-match. - Latent sibling (not fixed here): sorting Category/Vendor by
name.{lang}still uses a plainSortand would 1054 the same way — not reachable via the connector; tracked separately.
- The bug. Searching categories (
[4.107.2] fix(mcp): load the language helper at the connector entry so get_shop_info returns the shop name (Advisable-com/ecommercen#332)
- Follow-up to the 4.107.1 #332 hardening (which stopped the
-32603but left the name empty). The now-wired SDK logger (#329) showed the real cause:Call to undefined function get_languages(). The Registry's model (Registry_model extends Adv_base_model) callsget_languages()/getAdminLanguages()in its constructor — both in thelanguagehelper — which the bare HMVCMcpcontroller never loaded (it skips the front/admin base-controller bootstrap).Mcp::connector()now loads thelanguagehelper before dispatch, soget_shop_inforeturns the actual shop name and anyAdv_base_model-derived model the tools touch works.
- Follow-up to the 4.107.1 #332 hardening (which stopped the
[4.107.1] fix(mcp): connector hardening from the first live smoke test (Advisable-com/ecommercen#329, #331, #332)
- get_shop_info no longer 500s (#332). A registry/CI lookup failure in
ShopTools::siteName()surfaced as a generic-32603and took down the orientation tool. The shop-name lookup now degrades to an empty name (and logs the cause) instead of failing the call; the previously-missingShopToolsTestcovers the pass-through and fallback. - DB errors no longer leak HTML (#331). With
db_debugon, a failing query rendered CodeIgniter's HTML "Database Error" page (SQL, table/column names, absolute server paths) over the MCP transport andexit()ed past the handler's try/catch. The connector entry (Mcp::connector()) now disablesdb_debugon the DICI_DB_query_builder, so a query error becomes a catchable failure returned as a JSON-RPC error — not HTML. Scoped to the connector; the shared config is untouched. - SDK tool faults are now traceable (#329). The mcp/sdk ran with a
NullLogger, so the real exception behind every-32603was silently discarded. ALogMessageLoggerPSR-3 adapter (→log_message(), the module's existing Monolog pipeline) is wired via->setLogger(); the client-facing-32603is unchanged, but the cause now reaches the server log. Never logs the connector token. - All three are inside the feature-flagged MCP surface (
src/Mcp/+ the MCP-only connector controller); thename.{lang}search SQL bug (#330) is a shared domain-layer fix tracked separately.
- get_shop_info no longer 500s (#332). A registry/CI lookup failure in
[4.107.0] feat(mcp): MCP Connector — conversational SEO & content editing (Advisable-com/ecommercen#312)
- What it is. A per-eshop MCP server (
src/Mcp/,Advisable\Mcp\) that lets an eshop owner connect Claude (claude.ai / Desktop / Code) and edit SEO & content conversationally — categories, products and brand pages. Edit-only (no create/delete), keyed per eshop, feature-flagged byAPP_MCP_CONNECTOR_ENABLED(default off). EndpointPOST /mcp/c/<token>, officialmcp/sdkover Streamable HTTP, tools calling the modernsrc/Domains/services in-process. - Auth. The secret is the URL: a random 32-byte token stored only as its sha256 hash; single active token, generate/rotate/revoke from Admin → Settings → MCP Connector (one-time URL reveal). A bad/revoked token returns
401(generic message, no oracle); the feature flag off returns404. 60 requests/min per token →429+Retry-After. Every write is audited touser_audit_logs(providerMCP, masked URL — never the token, before/after inparams). - Tools (11).
get_shop_info;list/get/update_category;search/get_product,update_product(title + meta + brand + the full category set),update_product_content(text blocks);list/get/update_brand. Guardrails: HTML sanitization (XSS-focused, keeps img/tables), soft meta-length warnings, empty-name rejection,no_changesidempotency, before/after in every write.TranslationMergerrewrites the FULL translation set so a single-language edit leaves other languages and non-editable columns (builder_block_id,vendor_info) byte-identical — platform translation writes are delete-all + re-insert. - Docs.
docs/guides/mcp-connector.md(admin how-to, connect steps, security, curl collection) anddocs/guides/mcp-connector-onboarding-el.md(Greek customer onboarding).
- What it is. A per-eshop MCP server (
[4.107.0] fix(domains): MuiWriteData dropped unmapped mui columns on translation writes (Advisable-com/ecommercen#315)
- Translation updates run through
WriteService::update()→MuiWriteRepository::replaceForEntity()(delete-all + re-insert) with rows built fromMuiWriteData::toArray(). Columns present on the mui tables but absent fromMuiWriteData(vendor_info,builder_block_id,exclusive_builder_block_id) were therefore silently NULLed on every REST/MCP translation update — e.g. a builder-designed brand page lost its block assignment after an innocuous name edit. The threeMuiWriteDataclasses now carry the missing columns (full-row round-trip tests pin the regression).
- Translation updates run through
[4.107.0] feat(vendor): SEO meta fields on shop_vendor_mui + domain/REST/storefront wiring (Advisable-com/ecommercen#314)
- Brand pages gained stored
meta_title/meta_keywords/meta_description(migration + VendorMuiEntity/MuiWriteData/MuiResource). The storefront brand page prefers stored meta when set and falls back to the existing dynamic composition (routed throughseo_lib::create_metatags, mirroringAdv_product_categories).
- Brand pages gained stored
[4.107.0] feat(product): category-pivot write support in the Product WriteService (Advisable-com/ecommercen#316)
shop_product_category_lpexisted only as a read relation in the modern layer. AddedCategoryPivotWriteRepository::syncForProduct()(delete-where + batch insert, UNIQUE-safe) and an optionalcategorieskey onProduct\WriteService::create()/update()inside the existing transaction — absent leaves categories untouched,[]clears. Ids are validated against the Category repository. This is the write path the MCPupdate_producttool (and REST) use.
[4.107.0] feat(url):
storefront_url()helper +StorefrontUrlfacade for headless customer-facing links (Advisable-com/ecommercen#194)- The gap.
site_url()/base_url()served two audiences with one value — PHP-internal URLs (admin, payment-gateway callbacks, webhooks) and customer-facing storefront links (order tracking, password reset, account, gift-card shares). In a headless deployment the storefront lives on a different domain than the PHP app, but the codebase had no way to express that split. - What shipped. A new autoloaded
storefront_url(string $path)helper and an injectableAdvisable\Support\Url\StorefrontUrlfacade resolve the base from a newGLOBAL/STOREFRONT_BASE_URLregistry key, falling back tobase_url()when it is empty — so single-domain deployments are unaffected. The helper mirrorsbase_url()(single-slash join, noindex_page/url_suffix) and builds from the storefront base directly rather than string-replacingsite_url()output.site_url()/base_url()are unchanged, so admin, payment-callback and webhook URLs keep targeting the PHP app. - First call site. The password-reset email link (
reset_password.php, both themes) now usesstorefront_url(), so the reset link lands on the storefront domain — unblocking the headless consumer Advisable-com/velora#160. - Tests.
tests/Unit/Support/Url/StorefrontUrlTest.php(7 cases) pins the path-join contract: trailing-/leading-slash dedup, empty path, and a base that carries a path prefix.
- The gap.
[4.107.0] feat(url): route customer-facing email links through
storefront_url()(Advisable-com/ecommercen#327)- What shipped. Follow-up to #194: 27 customer-facing email templates (main + default themes) now build their links with
storefront_url()instead ofsite_url()/base_url()— order tracking (order_created,order_update,order_on_store), product/vendor links (waiting_list_success), gift cards (gift_card,gift_card_inform_customer), review and blog-comment moderation mails,birthday_wishes,remaining_points, and thereset_passwordheader logo. Headless deployments now send every customer email link to the storefront domain. - Audited, not migrated.
contact_email,ask_us_emailandcontact_email_generatedare shop-inbound (customer→shop) and stay onsite_url().email_products_summaryand the SMS helpers (yuboto,routee) build nobase_url/site_urllinks. Storefront views were intentionally left out — legacy storefront pages aren't served to customers under headless (the Nuxt frontend replaces them) and equalbase_url()otherwise, so migrating their ~400 mostly-asset call sites has no runtime effect. - Behaviour-preserving.
storefront_url()omits the.htmurl_suffixthatsite_url()appends, butMY_URI::_set_uri_string()strips that suffix on input before routing — so the suffixless links still resolve on non-headless storefronts.
- What shipped. Follow-up to #194: 27 customer-facing email templates (main + default themes) now build their links with
[4.107.0] feat(rest): brute-force throttle on the customer register endpoint (Advisable-com/ecommercen#324)
- The gap.
POST /rest/auth/customer/registerhad no rate limiting — open to automated account-creation abuse and to email-enumeration probing of the409"email exists" response. - What shipped. A third
LoginThrottleinstance (realmcustomer-register, DIrest.auth.register_throttle): 3 attempts per email and 5 per IP per hour,429+Retry-After, counters in the shared L2 cache, fail-open with a logged warning. The gate runs after the cheap400validation, and every well-formed request is counted — including before the existence check, deliberately, since counting only not-exists requests would itself be an enumeration oracle. The raw duplicate-email check was replaced withcustomer_model->getCustomerBy(['mail' => …]). The409response is kept as-is (no generic-response change, to avoid altering the storefront signup UX). Tunable via env (REST API v1.11). - Caveat. The per-IP layer relies on
MY_Input::ip_address(), which still trusts client-controlled forwarding headers (#322) — until that lands, the per-email counter is the effective control.
- The gap.
Notes
- [4.107.0] REQUIRES
php migrator.php migrate:20260612120000_add_storefront_base_url_registry_key.php— adds theGLOBAL/STOREFRONT_BASE_URLregistry key (empty default) backingstorefront_url()(#194).20260612121500_add_meta_fields_to_shop_vendor_mui.php— brand-page SEO meta fields (#314).20260612130000_create_mcp_connector_tokens.php— MCP connector token store (#312).
- [4.107.0] Compiled DI container must be rebuilt (delete
cache/container.php) — picks up the newAdvisable\Support\Url\StorefrontUrlservice (#194) and the newAdvisable\Mcpmodule +CategoryPivotWriteRepositoryregistrations (#312/#316). Headless deployments setGLOBAL/STOREFRONT_BASE_URLto the storefront origin; an empty value preserves single-domain behaviour. - [4.107.0]
composer installrequired — addedmcp/sdk ~0.6.0and its transitive deps (resolved from Private Packagist) plus aconfig.allow-pluginsentry forphp-http/discovery(#313). - [4.107.0] New env var
APP_MCP_CONNECTOR_ENABLED(defaultfalse) gates the MCP connector endpoint; while off,/mcp/c/<token>returns404(#312). - [4.107.0] REST surface changed —
registernow returns429+Retry-Afterwhen throttled (REST API v1.11, #324). The trackedpublic/openapi*.json/public/api-versions.jsonare refreshed in this release's build commit. - [4.107.0] Check for overrides (#194 — headless URLs): clients overriding
application/config/autoload.phporapplication/config/container/modules.phpmust re-apply thestorefront_urlhelper load + thesrc/Support/Urlmodule registration.src/Support/Url/StorefrontUrl.phpsrc/Support/Url/container.phpapplication/helpers/storefront_url_helper.php