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.107.md for this page in Markdown format</div>

Home | Changelog

Version 4

version 4.107

  • [4.107.6] fix(settings): expose GLOBAL/STOREFRONT_BASE_URL in 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 the GLOBAL/STOREFRONT_BASE_URL registry key (migration 20260612120000_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 the registry table 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_URL field exactly. ecommercen/settings/controllers/Adv_settings.php now saves STOREFRONT_BASE_URL (POST global_storefront_base_url), exposes its current value to the view, and registers a trim validation rule alongside global_cdn_url. The view application/views/admin/settings/settings.php gained the input plus help text.
    • Behaviour. An empty value preserves the base_url() fallback in StorefrontUrl (trim((string) value(...))), so single-domain deployments are unaffected. No manual cache flush is needed: the save redirects back to an admin request, whose Adv_admin_controller constructor calls $this->registry->live() — refreshing the registry_model::all pscache from the DB — identical to how CDN_URL already 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.
  • [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 a callback_ 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 through PasswordResetService::requestReset() — the same service backing the REST endpoint, which looks up the customer and silently skips unknown addresses (and guards recover_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 existing PasswordResetServiceTest.
    • 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_throttle in #193) — noted as a follow-up; a per-IP limiter also depends on the ip_address() trust fix (#322).
  • [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 many ListRequest::setAllowedSorts() — resolved (via GenerateListRequest's fallback) to a root SortRequest with field = shop_*_mui.name, relation = translations, and broke three ways: (1) the locale was never carried onto the SortRequest (it had no language property), so the per-domain branch passed null into SortByTranslation's typed string $localeTypeError in the ~17 domains that have the branch; (2) the branch extracted the field with explode('.', $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 bare Sort on the unjoined *_mui column returned empty (db_debug off) or 1054 (on). This is the latent sibling of the #330 translation-filter fix.
    • Fix. Added ?string $language to SortRequest and populated it in GenerateListRequest (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. Added SortByTranslation specs + the relation === 'translations' branch to Category and Vendor, mirroring #330's FilterByTranslation. 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 via SortRequest::$class) and is unchanged.
    • Tests: a config-free GenerateListRequest unit 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).
  • [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 under images/mcp-connector/ that were never committed (the subfolder was missing and one file carried a doubled .png.png extension), 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).
  • [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 by name.{lang}/slug.{lang} — fataled with MySQL 1054 Unknown column 'shop_*_mui.name' in 'where clause'. Their Service::buildSpecifications() built a plain Filter for every filter and ignored FilterRequest::$relation, so a translation field emitted a bare WHERE against the *_mui table, which isn't in the FROM clause (BaseRepository does not auto-join on the relation).
    • Fix. Category and Vendor were the two product-domains missing the FilterByTranslation subquery handling that Product and ~15 other domains already use. Added their specs (locale-aware id IN (SELECT &lt;fk> FROM &lt;mui> WHERE lang = ? AND … LIKE ?), single-pass escape_like_str + ESCAPE '!' so %/_ stay literal) and routed relation === '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 plain Sort and would 1054 the same way — not reachable via the connector; tracked separately.
  • [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 -32603 but 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) calls get_languages()/getAdminLanguages() in its constructor — both in the language helper — which the bare HMVC Mcp controller never loaded (it skips the front/admin base-controller bootstrap). Mcp::connector() now loads the language helper before dispatch, so get_shop_info returns the actual shop name and any Adv_base_model-derived model the tools touch works.
  • [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 -32603 and 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-missing ShopToolsTest covers the pass-through and fallback.
    • DB errors no longer leak HTML (#331). With db_debug on, a failing query rendered CodeIgniter's HTML "Database Error" page (SQL, table/column names, absolute server paths) over the MCP transport and exit()ed past the handler's try/catch. The connector entry (Mcp::connector()) now disables db_debug on the DI CI_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 -32603 was silently discarded. A LogMessageLogger PSR-3 adapter (→ log_message(), the module's existing Monolog pipeline) is wired via ->setLogger(); the client-facing -32603 is 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); the name.{lang} search SQL bug (#330) is a shared domain-layer fix tracked separately.
  • [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 by APP_MCP_CONNECTOR_ENABLED (default off). Endpoint POST /mcp/c/&lt;token>, official mcp/sdk over Streamable HTTP, tools calling the modern src/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 returns 404. 60 requests/min per token → 429 + Retry-After. Every write is audited to user_audit_logs (provider MCP, masked URL — never the token, before/after in params).
    • 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_changes idempotency, before/after in every write. TranslationMerger rewrites 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) and docs/guides/mcp-connector-onboarding-el.md (Greek customer onboarding).
  • [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 from MuiWriteData::toArray(). Columns present on the mui tables but absent from MuiWriteData (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 three MuiWriteData classes now carry the missing columns (full-row round-trip tests pin the regression).
  • [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 + Vendor MuiEntity / MuiWriteData / MuiResource). The storefront brand page prefers stored meta when set and falls back to the existing dynamic composition (routed through seo_lib::create_metatags, mirroring Adv_product_categories).
  • [4.107.0] feat(product): category-pivot write support in the Product WriteService (Advisable-com/ecommercen#316)

    • shop_product_category_lp existed only as a read relation in the modern layer. Added CategoryPivotWriteRepository::syncForProduct() (delete-where + batch insert, UNIQUE-safe) and an optional categories key on Product\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 MCP update_product tool (and REST) use.
  • [4.107.0] feat(url): storefront_url() helper + StorefrontUrl facade 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 injectable Advisable\Support\Url\StorefrontUrl facade resolve the base from a new GLOBAL/STOREFRONT_BASE_URL registry key, falling back to base_url() when it is empty — so single-domain deployments are unaffected. The helper mirrors base_url() (single-slash join, no index_page/url_suffix) and builds from the storefront base directly rather than string-replacing site_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 uses storefront_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.
  • [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 of site_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 the reset_password header logo. Headless deployments now send every customer email link to the storefront domain.
    • Audited, not migrated. contact_email, ask_us_email and contact_email_generated are shop-inbound (customer→shop) and stay on site_url(). email_products_summary and the SMS helpers (yuboto, routee) build no base_url/site_url links. Storefront views were intentionally left out — legacy storefront pages aren't served to customers under headless (the Nuxt frontend replaces them) and equal base_url() otherwise, so migrating their ~400 mostly-asset call sites has no runtime effect.
    • Behaviour-preserving. storefront_url() omits the .htm url_suffix that site_url() appends, but MY_URI::_set_uri_string() strips that suffix on input before routing — so the suffixless links still resolve on non-headless storefronts.
  • [4.107.0] feat(rest): brute-force throttle on the customer register endpoint (Advisable-com/ecommercen#324)

    • The gap. POST /rest/auth/customer/register had no rate limiting — open to automated account-creation abuse and to email-enumeration probing of the 409 "email exists" response.
    • What shipped. A third LoginThrottle instance (realm customer-register, DI rest.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 cheap 400 validation, 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 with customer_model->getCustomerBy(['mail' => …]). The 409 response 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.

Notes

  • [4.107.0] REQUIRES php migrator.php migrate:
    • 20260612120000_add_storefront_base_url_registry_key.php — adds the GLOBAL/STOREFRONT_BASE_URL registry key (empty default) backing storefront_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 new Advisable\Support\Url\StorefrontUrl service (#194) and the new Advisable\Mcp module + CategoryPivotWriteRepository registrations (#312/#316). Headless deployments set GLOBAL/STOREFRONT_BASE_URL to the storefront origin; an empty value preserves single-domain behaviour.
  • [4.107.0] composer install required — added mcp/sdk ~0.6.0 and its transitive deps (resolved from Private Packagist) plus a config.allow-plugins entry for php-http/discovery (#313).
  • [4.107.0] New env var APP_MCP_CONNECTOR_ENABLED (default false) gates the MCP connector endpoint; while off, /mcp/c/&lt;token> returns 404 (#312).
  • [4.107.0] REST surface changedregister now returns 429 + Retry-After when throttled (REST API v1.11, #324). The tracked public/openapi*.json / public/api-versions.json are refreshed in this release's build commit.
  • [4.107.0] Check for overrides (#194 — headless URLs): clients overriding application/config/autoload.php or application/config/container/modules.php must re-apply the storefront_url helper load + the src/Support/Url module registration.
    • src/Support/Url/StorefrontUrl.php
    • src/Support/Url/container.php
    • application/helpers/storefront_url_helper.php