Core 1.4.0 Stable

Released on: Wednesday, 02 September 2026 19:12

A release about the hours spent inside the admin. The two Core 1.4 roadmap items land — an Email Queue screen so a failed send is no longer invisible, and a quick search box in the administrator header — and the product edit form is rebuilt around product types, so a product only ever shows the tabs and defaults that apply to what it is.

It is also the core Downloadable Products 1.0.0 (free) pins as its minimum. The invoicing that ships in core gained an approval workflow that actually gates PDF generation, the customer email and customer visibility, plus a privacy fix — cached invoice PDFs are no longer reachable by guessing an integer.

No schema changes.

Added

  • Email queue admin UI + resend order email — failed sends are no longer invisible. A new Email Queue screen (Components → Solidshop → Email Queue, also in the Solidshop admin-menu preset under System) lists every queued email with recipient, subject, context, the related order (linked), delivery status badge, attempt count and the last error; filter by status/context, search by recipient/subject (id: and order: prefixes supported), and click a subject to preview the exact stored email body in a new tab. Failed rows can be selected and retried from the toolbar — they return to pending with a fresh attempt budget and go out on the next plg_task_solidshopemailqueue run — and rows can be deleted (core.delete). The screen is gated through EmailqueueModel::authorise() (core.admin / core.manage.order — the per-store seam). The order detail toolbar gains Resend Confirmation: re-renders the confirmation from the order's current state (so address fixes, order edits and template fixes are picked up) and queues the customer copy only, guarded by the queue's per-recipient idempotency check; the action is logged to the order timeline as a new email_requeued entry. Library additions: Solidshop\Lib\Email\QueueStatus enum (unit-tested) and EmailQueueService::find()/retry()/deleteRows().
  • Admin quick search (mod_sshop_quicksearch) — a universal search box in the administrator header: type an order id/reference, customer name or email, product name, SKU or barcode (variant SKUs/barcodes included), or a discount name/coupon code and jump straight to the matching record, grouped by entity with status badges and per-group "View all" deep links into the pre-filtered list views. Ctrl+K / ⌘K focuses it from anywhere in the admin, / too when not already typing; full ARIA combobox pattern (activedescendant navigation, live result announcements). Query-shape detection with staged fallback (numeric → id/reference first, email → email columns, else broad LIKE) lives in the new Solidshop\Lib\Search domain (QueryClassifier, SearchQuery, SearchResultItem, SearchProviderRegistry — unit-tested), providers are thin query adapters in the component, and QuicksearchRegisterEvent (onSolidshopQuicksearchRegister) lets feature plugins register more result groups (invoices, subscriptions, reviews — follow-up work in their own plugins). Endpoint is task=quicksearch.search (CSRF + core.manage gated; each provider additionally checks its own core.manage.* action, so a user with partial rights just gets fewer groups; every result carries store identity per the multi-store rule). First administrator module in the bundle: build/build.php gained an admin-module packaging pass, and the package script auto-publishes the module to Atum's status position on fresh installs only — updaters get a pointer to the module manager instead. Customers group is deliberately restricted to users with commerce activity (an order or a saved address).
  • The product edit form follows the product type. Creating a product now starts with a set of icon cards — Physical plus whatever types the installed plugins register (Downloadable, Service, Subscription); on a saved product the control is the familiar dropdown. Changing the type reloads the form and preserves unsaved edits, so the tabs on screen always match the type, and the Shipping tab is gated on the type's Shippable capability instead of always being present. Track quantity now defaults from the type's Stockable capability (on for Physical, off for Downloadable/Service/Subscription/Virtual) and stays switchable everywhere — a service with limited seats can still track stock. The media field accepts drag-and-drop uploads straight onto the media area rather than requiring a trip through the media manager. Tabs and sidebar are reordered: SEO is its own tab (meta description, page title and the search-snippet preview) instead of sharing one with publishing data, and the sidebar groups status, product type, category, brand, tags, access and language in a single fieldset. The summary editor is height-capped so a long description no longer pushes the rest of the form off screen.

Fixed

  • Cached invoice PDFs were reachable by guessing an integer. Generated PDFs sat at files/shop/{store}/invoice/{id}.pdf inside the web root with no deny rules — walking the ids exposed other customers' invoices (name, address, order contents). InvoicePdf::cachedPath() now appends a 64-bit HMAC token keyed with the site secret, and secureDir() drops .htaccess (Apache 2.2/2.4/LiteSpeed), web.config (IIS) and a blank index.html into the cache directory — called from saveToFile() and, self-healing for existing installs, from cachedPath(). The first securing of a directory sweeps legacy token-less {id}.pdf files; they regenerate on demand at the tokened name, and pdf_path stays write-only so stale DB values are inert. On nginx, which honours neither guard file, the unguessable filename is the protection.
  • The invoice approval workflow did not gate what it claimed to. The PDF/email task now only processes ISSUED invoices; approve() re-queues the PDF so it carries the approval-stamped invoice date, and the customer email goes out strictly post-approval. The admin PDF download preserves a queued row instead of swallowing the pending email. Customers no longer see or download draft/pending invoices anywhere (account list, print, PDF, order-detail button). Approve is also offered on drafts so a rejected invoice is not a dead end, the approval message/toolbar language keys are fixed, and the admin list gained the missing Pending Approval filter option.
  • Invoice items now carry the per-line tax aggregated from the order's tax rows, so the Detailed layout's tax column shows real values instead of zeros. Credit notes store all money columns negated, matching the documented behaviour and what an accounting import expects. Layout resolution is unified in InvoiceLayoutResolver with an on-disk existence check, so the PDF task can no longer email a blank PDF for a stale layout key.
  • Emails rendered from a payment callback were incomplete. Email::dispatchBeforeRender() now imports the solidshop / solidshoppayment plugin groups itself: renders triggered from com_ajax payment flows (the PayPal capture, for one) never pass through the component dispatcher, so plugin listeners were silently absent and plugin-contributed blocks and the bank-transfer instructions never rendered.
  • Public order-tracking URLs are SEF-routed. The tracking view was never registered with the component router, so its links stayed in raw index.php?option=… form on sites with SEF URLs enabled.
  • A phantom empty media entry produced an empty storefront slideshow. MediaManagerField seeded its hidden input with [""] for a new product, so saving without ever opening the picker stored a blank entry in the media column; the site model hydrated it into a bogus slide and the downloadable/service templates gated their gallery on the raw entry count. The field no longer seeds blank entries, the admin model strips blank rows on save (healing existing records), the site model filters them when decoding, and both templates (component + Foundra) gate on the typed media counts like product/default.php does.
  • Cart page: the Estimated total row is laid out like the Subtotal row above it (space-between instead of a right-aligned pair with a margin), so label and amount line up down the summary column.
  • Product form: the brand field's placeholder typo is fixed, and the variants checkbox label is properly associated with its input.

Changed

  • Plugin-owned account pages moved their queries out of the templates. The subscriptions, downloads and bookings account templates each carried their own SQL, copy-pasted verbatim into the Foundra override (and, for downloads, into the component stub as well). Each plugin now ships a site model (DownloadModel, SubscriptionModel, BookingModel) under site/com_solidshop/src/Model/, shadowed into the component namespace by the plugin autoloader and reached through the component's MVCFactory — the pattern the review plugin already used. All seven templates now ask a model and hold no query. The download paid/expired/limit predicate is extracted into the library as DownloadAvailability + DownloadState, replacing four independent copies (three templates and Download.file), so the account page and the controller agree on both verdict and reason; unit-tested. SubscriptionController::processPlanChange() and the plan picker now share one definition of a switchable plan, the picker is scoped to the subscription's own store, and the Foundra subscriptions/bookings overrides render the Plugin Required notice instead of querying tables that do not exist when the plugin is absent.
  • The Payment received email no longer promises a shipment on an order with nothing to ship: the intro and next-step notice gained NO_SHIPPING variants (23 locales), used when the order carries no shipping rate — the shape of a digital-only order.
  • Email seed versions bumped: order.order_completed → 1.0.12, order.payment_received → 1.0.2. Both defaults gained a {% include 'downloadable/downloads.twig' ignore missing %} line, so a store running the Downloadable plugin gets a "Your downloads" block and a store without it renders exactly as before.

Upgrade notes

  • No schema changes — 1.4.0.sql is empty. Nothing to migrate, nothing to back out.
  • Publish the quick-search module. Joomla installs administrator modules unpublished and position-less. Fresh installs get it placed automatically; on update the package script enqueues a notice instead, because an existing module layout is the merchant's. Publish mod_sshop_quicksearch to the Atum status position under System → Administrator Modules.
  • Stores that customised the Order confirmation (seed 1.0.11 → 1.0.12) or Payment received (1.0.1 → 1.0.2) bodies will see a drift banner. Reset to the default, or add the {% include 'downloadable/downloads.twig' ignore missing %} line by hand where the download block should appear; the payment-received default also picked up the no-shipping wording.
  • Existing invoice PDF caches are secured lazily: the guard files are dropped and legacy token-less files swept the first time the directory is touched after the update. On nginx, add a deny rule for files/shop/*/invoice/ (or move the directory outside the web root) — nginx honours neither .htaccess nor web.config, so there the tokened filename is the only protection.
  • Downloadable Products 1.0.0 pins 1.4.0 as its minimum core and refuses to install against anything older; update core first.

Solidshop Core v1.4.0

Joomla! 6.0 Joomla! 6.1

File size 9.22 Mb
SHA-256 Signature e36495d3902076e550e0723c8fba270482148cf93adb74fc00d83602c1af9125
Compatibility Joomla! 6.0 Joomla! 6.1