Appearance
<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>
Version 4
version 4.106
[4.106.0] fix(theme): emit
original_rawroot-relative in the embedded product JSON (Advisable-com/ecommercen#325)- The bug.
productImagesWithPathsForJson()shippedoriginal_rawas a bare relative string (files/products/<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 (/<category>/<sub>/files/products/<file>.jpg) and steady 404 noise in Search Console and access logs. The rendered storefront was never affected — PHP views useassetUrl()/html_picture()and the Vue gallery re-prefixes — the breakage was purely the JSON payload. - Fix. The path is now root-relative (
/files/products/<file>.jpg), which resolves to the valid origin URL of the original image from any page. It is deliberately not absolutized: the only consumer,AdvProductGallery→AdvHtmlPicture, builds the mediastream srcset URLs from this path and already strips the leading slash (trimStart) before prefixing withadvAppData.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_rawroot-relative (never bare, never absolute),urithe bare filename (it doubles as the video id),original/large/medium/smallassetUrl()-absolutized, plusis_maindefaulting and failover-image behaviour.
- The bug.
[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) inAdv_mailer::recover_password_mail(), replacingsha1(time() . customer_id)whose inputs were predictable enough to brute-force offline. - TTL. New
shop_customer.active_token_expires_atcolumn (migration); tokens expire 60 minutes after issue (Adv_customer_model::PASSWORD_RESET_TOKEN_TTL).isValidToken()requires a future expiry andresetPassword()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 weaksha1values) is invalidated at deploy. - Enumeration.
POST /rest/auth/customer/forgot-passwordnow returns200with the same generic message whether or not the account exists (was422 "No account found with this email.").422remains 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-passwordis throttled by a secondLoginThrottleinstance (realmforgot-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 andregisterenumeration tracked in #323/#324.
- Entropy. Reset tokens are now
[4.106.0] feat(rest): brute-force throttle on the JWT login endpoints (Advisable-com/ecommercen#5)
- The gap.
POST /rest/auth/admin/loginand/rest/auth/customer/loginaccepted 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 get429+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-credential401s never touch the cache. Fail-open with a logged warning if the cache backend is down. Limits tunable viaREST_AUTH_THROTTLE_*env vars (REST API v1.9). - Scope.
refreshstays unthrottled (refresh tokens are high-entropy random values). The per-IP layer relies onMY_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.
- The gap.
[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 fullDeviceDetectorparse 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 toPSR6Bridgeinstead 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\BotwithdiscardDetails()— it loads onlybots.ymland stops at the combined pre-match regex, never walking the larger OS/client/device tree (behaviourally identical forisBot(); verified against googlebot/claudebot/chrome/curl). Wired a working cross-request cache via the pattern already proven inAdv_front_controller—PSR6Bridge(di()->get(CachePool::class))— both call sites sharing the same bots-regex cache entry. The newbotDetectorCache()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.
- The bug.
[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()'sGuzzleExceptioncatch (where aConnectExceptionlands 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) returndegraded => trueinside the product-search result array — distinguishing a failed query from a zero-row query — and the controller exposes it assearch_degradedin 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_DEBUGblocks —data->getBody()on a null response threw anErrorpast acatch (Exception), so with debug on and Solr down the search page fataled instead of degrading; nowcatch (Throwable). Tests intests/Legacy/Search/.
- 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".
[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 toallowRole([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 sametaskCsrftoken (edit stays a safe GET) — surfaced and fixed during the release doc-resync.
- #21 — per-endpoint RBAC.
[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_menuentry now point atadvisable/queueBackup, which enqueues aBackUpDataBasejob on thecorequeue. Added a flash-success block to the dashboard view, and the deprecatedDbutilsweb methods are neutralized (backup()redirects to the new route).
- The dashboard "backup database" button hit
[4.106.0] fix(upload): validate image dimensions by feeding
isImage()the content MIMEAdvUploadValidator's dimension check was dead — the call site passed the temp path toisImage(), 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"
AdvJobreported every construction/execution failure as "json decode error", masking the real cause.job_argumentsdecoding now has its own try/catch (a genuineJsonExceptionis logged as such);executeCommand()failures are logged as "executeCommand failed for job <name>"; and the construction guard was widened fromcatch(Exception)tocatch(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.
- The barcode-import job called a non-existent
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
429responses on the login endpoints (REST API v1.9) and the password-reset hardening (#193) changedforgot-passwordto a generic 200 (v1.10). The trackedpublic/openapi*.jsonandpublic/api-versions.jsonare 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 theLoginThrottleservices 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.phpecommercen/search/models/Adv_search_model.phpecommercen/search/models/Adv_search_model_v2.php
- [4.106.0] Check for overrides (#5/#193 — REST auth):
src/Rest/Auth/AdminAuth.phpsrc/Rest/Auth/CustomerAuth.phpsrc/Rest/Auth/LoginThrottle.phpsrc/Rest/Auth/container.phpapplication/models/Adv_mailer.phpecommercen/eshop/models/Adv_customer_model.phpecommercen/eshop/models/Adv_sanitize_model.phpsrc/Domains/Customer/Customer/PasswordResetService.phpsrc/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.phpecommercen/advisable/controllers/Adv_advisable.phpecommercen/job/controllers/AdvJob.php