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

Home | Changelog

Version 4

version 4.106

  • [4.106.0] fix(theme): emit original_raw root-relative in the embedded product JSON (Advisable-com/ecommercen#325)

    • The bug. productImagesWithPathsForJson() shipped original_raw as a bare relative string (files/products/&lt;file>.jpg) inside the per-product JSON embedded in every storefront page (advAppContext.vueData.initialData.products). Crawlers and link-extractors resolve such strings against the current page URL, producing broken category-relative image URLs (/&lt;category>/&lt;sub>/files/products/&lt;file>.jpg) and steady 404 noise in Search Console and access logs. The rendered storefront was never affected — PHP views use assetUrl()/html_picture() and the Vue gallery re-prefixes — the breakage was purely the JSON payload.
    • Fix. The path is now root-relative (/files/products/&lt;file>.jpg), which resolves to the valid origin URL of the original image from any page. It is deliberately not absolutized: the only consumer, AdvProductGalleryAdvHtmlPicture, builds the mediastream srcset URLs from this path and already strips the leading slash (trimStart) before prefixing with advAppData.url.assets — so the gallery is byte-for-byte unaffected and no storefront rebuild is needed.
    • Tests. tests/Legacy/Eshop/ThemeHelperProductImagesJsonTest.php (21 tests) pins the per-image JSON contract in both directions: original_raw root-relative (never bare, never absolute), uri the bare filename (it doubles as the video id), original/large/medium/small assetUrl()-absolutized, plus is_main defaulting and failover-image behaviour.
  • [4.106.0] fix(auth): harden the customer password-reset flow — entropy, TTL, enumeration, throttle (Advisable-com/ecommercen#193)

    • Entropy. Reset tokens are now bin2hex(random_bytes(32)) (256 bits) in Adv_mailer::recover_password_mail(), replacing sha1(time() . customer_id) whose inputs were predictable enough to brute-force offline.
    • TTL. New shop_customer.active_token_expires_at column (migration); tokens expire 60 minutes after issue (Adv_customer_model::PASSWORD_RESET_TOKEN_TTL). isValidToken() requires a future expiry and resetPassword() repeats the condition in its UPDATE where-clause, so an expired token cannot reset a password even if a caller skips the pre-check. NULL expiry counts as expired — every pre-migration token (all the weak sha1 values) is invalidated at deploy.
    • Enumeration. POST /rest/auth/customer/forgot-password now returns 200 with the same generic message whether or not the account exists (was 422 "No account found with this email."). 422 remains only for a missing/malformed email. Behaviour change for API clients — frontends that displayed "email not found" must switch to the generic confirmation (REST API v1.10).
    • Rate limiting. forgot-password is throttled by a second LoginThrottle instance (realm forgot-password): 3 requests per email and 10 per IP per hour (REST_AUTH_RESET_THROTTLE_* env vars), every well-formed request counted, 429 + Retry-After. The token/TTL hardening lives in the shared legacy model methods, so the legacy storefront flow is covered too. Residual storefront-form and register enumeration tracked in #323/#324.
  • [4.106.0] feat(rest): brute-force throttle on the JWT login endpoints (Advisable-com/ecommercen#5)

    • The gap. POST /rest/auth/admin/login and /rest/auth/customer/login accepted unlimited credential attempts — no throttle, no lockout — leaving both surfaces open to online password guessing and credential stuffing.
    • What shipped. New Advisable\Rest\Auth\LoginThrottle (fixed-window, counters in the shared L2 cache so limits hold across pods): per submitted identifier 5 failed attempts per 15 min (counted whether or not the account exists, cleared on successful login) and per client IP 20 per 15 min (never cleared on success). Blocked requests get 429 + Retry-After; the response is identical for both counters and identifiers are counted regardless of existence, so a lockout never confirms an account exists. Only failed attempts count; missing-credential 401s never touch the cache. Fail-open with a logged warning if the cache backend is down. Limits tunable via REST_AUTH_THROTTLE_* env vars (REST API v1.9).
    • Scope. refresh stays unthrottled (refresh tokens are high-entropy random values). The per-IP layer relies on MY_Input::ip_address(), which currently trusts client-controlled forwarding headers (#322) — until that lands, the per-identifier counter is the effective control.
    • Tests. 16-case unit suite (limits, IP-independence, clear-on-success, realm isolation, normalization, window expiry, kill switch, fail-open) + a DI container resolution guard.
  • [4.106.0] fix(session): cache bot-detection regexes and use the bot-only parser (Advisable-com/ecommercen#285)

    • The bug. MY_Session::isBotSession() ran a full DeviceDetector parse on every request with no working cache, re-parsing the entire device-detector YAML regex set through the pure-PHP Spyc parser at runtime — work OPcache cannot absorb. Measured at ~3.3s per request for a real browser User-Agent locally; a previous cache wiring was doubly broken (passed the DI container to PSR6Bridge instead of a pool) and the error was silently swallowed, so it was removed, leaving no cache at all.
    • Fix. Switched to the standalone DeviceDetector\Parser\Bot with discardDetails() — it loads only bots.yml and stops at the combined pre-match regex, never walking the larger OS/client/device tree (behaviourally identical for isBot(); verified against googlebot/claudebot/chrome/curl). Wired a working cross-request cache via the pattern already proven in Adv_front_controllerPSR6Bridge(di()->get(CachePool::class)) — both call sites sharing the same bots-regex cache entry. The new botDetectorCache() seam logs a warning and falls back to uncached on failure instead of swallowing it. Result: ~3,325ms → ~24ms warm per request.
    • Tests. tests/Legacy/Session/MySessionBotDetectionTest.php — UA short-circuits, bot/browser classification through both the uncached fallback and the cached path, shared-pool population, and a warm-vs-cold timing guard.
  • [4.106.0] fix(search): log degraded search mode and expose a distinguishable flag (Advisable-com/ecommercen#43)

    • The bug. When Solr was down or the SQL fallback failed (e.g. missing FULLTEXT indexes in dev), product search silently returned empty results — HTTP 200, "no results", zero log lines — with nothing distinguishing "search engine down" from "no matches". AdvSolrClient::search()'s GuzzleException catch (where a ConnectException lands when Solr is unreachable) was an empty // todo.
    • Fix. Every failure point now logs with a greppable Search degraded: prefix (the Solr client adds the transport detail). Both search models (v1 + v2) return degraded => true inside the product-search result array — distinguishing a failed query from a zero-row query — and the controller exposes it as search_degraded in the render array (search page, live search, vendor live search) so themes can show a "temporarily unavailable" state. Reads are defensive (!empty()/??), so stale pscache entries and client-repo model overrides without the key stay safe. Degraded empty results are still cached for the search TTL (deliberate — the cache absorbs repeat traffic during an outage instead of every request burning the Solr connect timeout).
    • Also. Fixed a latent crash in the APP_SOLR_DEBUG blocks — data->getBody() on a null response threw an Error past a catch (Exception), so with debug on and Solr down the search page fataled instead of degrading; now catch (Throwable). Tests in tests/Legacy/Search/.
  • [4.106.0] fix(auth): secure task management endpoints — RBAC, ownership, CSRF (Advisable-com/ecommercen#21, #22, #23)

    • #21 — per-endpoint RBAC. tasks() (the list of all admins' tasks) is now gated to allowRole([ADVISABLE, ADMIN]), matching the admin-menu role gating; previously any authenticated admin could reach it by URL.
    • #22 — ownership. New canModifyTask() permits only a task's creator or assignee, or a supervisor (ADVISABLE/ADMIN), to edit/delete/complete/uncomplete it; others get 401. Previously any logged-in admin could mutate any task by guessing the id.
    • #23 — CSRF. The one-click destructive actions (taskDelete/taskCompleted/taskUncompleted) now require POST + a per-session synchronizer token instead of plain GET links. A shared authorizeTaskMutation() guard wires POST+token+ownership for the three actions. The stored-XSS in task descriptions (#24) is deferred pending a sanitizer-vs-escape decision.
    • Follow-up. The CSRF migration initially converted only the per-row modal controls to POST forms; the task-list table-row complete/uncomplete/delete buttons were left as GET anchor() links and so silently failed the new POST guard (dead buttons on the main list). They are now inline POST forms carrying the same taskCsrf token (edit stays a safe GET) — surfaced and fixed during the release doc-resync.
  • [4.106.0] fix(admin): route the DB backup button through the job queue instead of Dbutils

    • The dashboard "backup database" button hit Dbutils, which fataled with "Call to a member function userdata() on null" (it extends a bare CI controller without the admin session bootstrap), and even when it worked it ran the backup synchronously inside the web request. The button + admin_menu entry now point at advisable/queueBackup, which enqueues a BackUpDataBase job on the core queue. Added a flash-success block to the dashboard view, and the deprecated Dbutils web methods are neutralized (backup() redirects to the new route).
  • [4.106.0] fix(upload): validate image dimensions by feeding isImage() the content MIME

    • AdvUploadValidator's dimension check was dead — the call site passed the temp path to isImage(), which expects a MIME type, so the check never ran. It now passes the detected content MIME, restoring the dimension validation.
  • [4.106.0] fix(job): log the accurate cause on job failure instead of "json decode error"

    • AdvJob reported every construction/execution failure as "json decode error", masking the real cause. job_arguments decoding now has its own try/catch (a genuine JsonException is logged as such); executeCommand() failures are logged as "executeCommand failed for job <name>"; and the construction guard was widened from catch(Exception) to catch(Throwable) so a fatal during job construction is captured rather than mislabelled.
  • [4.106.0] fix(job): remove the dangling loadModels() call in the barcode-import job

    • The barcode-import job called a non-existent loadModels(), fataling on run. Removed the dead call so the job constructs and runs.

Notes

  • [4.106.0] REQUIRES php migrator.php migrate:
    • 20260611120000_add_active_token_expires_at_to_shop_customer.php — adds the password-reset token expiry column (#193).
  • [4.106.0] REST surface changed — OpenAPI specs regenerated at this cut. The throttle (#5) added 429 responses on the login endpoints (REST API v1.9) and the password-reset hardening (#193) changed forgot-password to a generic 200 (v1.10). The tracked public/openapi*.json and public/api-versions.json are refreshed in this release's build commit — nothing regenerates them at deploy.
  • [4.106.0] Compiled DI container must be rebuilt (delete cache/container.php) — the auth container gained the LoginThrottle services and parameters (#5/#193). K8s rollouts get this for free (fresh pods); bare-metal deploys should clear the cache.
  • [4.106.0] Check for overrides (#43 — degraded search):
    • ecommercen/search/controllers/Adv_search.php
    • ecommercen/search/models/Adv_search_model.php
    • ecommercen/search/models/Adv_search_model_v2.php
  • [4.106.0] Check for overrides (#5/#193 — REST auth):
    • src/Rest/Auth/AdminAuth.php
    • src/Rest/Auth/CustomerAuth.php
    • src/Rest/Auth/LoginThrottle.php
    • src/Rest/Auth/container.php
    • application/models/Adv_mailer.php
    • ecommercen/eshop/models/Adv_customer_model.php
    • ecommercen/eshop/models/Adv_sanitize_model.php
    • src/Domains/Customer/Customer/PasswordResetService.php
    • src/Domains/Customer/Customer/Repository/Entity.php
  • [4.106.0] Check for overrides (#21–#23 — task security):
    • ecommercen/auth/controllers/Adv_auth.php
  • [4.106.0] Check for overrides (DB-backup + job-logging fixes):
    • application/controllers/Dbutils.php
    • ecommercen/advisable/controllers/Adv_advisable.php
    • ecommercen/job/controllers/AdvJob.php