38 min readOct 16, 2026by jakub
Changelog
Current version: 1.5.0
1.5.0 — 2026-09-11
Product Labels
Added
- New
Api/LabelUpdateInterface::update()— the PUT route now binds to this instead of the sharedsave(), so the URL id is always authoritative (see Fixed) - The label indexer's insert chunk size (
INSERT ... ON DUPLICATE KEY UPDATEbatching, default 1000) is now a constructor argument onModel/Indexer/ProductLabels, matching the existing configurablebatchSize dev/tools/qoliber/validate.sh— a local, repeatable release-validation script (PHP 8.1 lint, a Rector downgrade floor guard,composer validate, the unit and integration suites, base-module and add-on install combinations — plain ProductLabels, +Creator, +Hyvä, all — productiondi:compile/static-content deploy per combination, and the full Playwright suite including the cache-required projects below) anddev/tools/qoliber/cache-mode.sh(switches the dev store between the built-in full page cache, Varnish and its normal disabled state for those proofs); documented indocs/validation.md- A Playwright end-to-end suite in
tests/playwrightproving that label edits reach cached storefronts with no manual flush, and that REST enforces its ACL and its id binding over real HTTP. The cache specs (specs/cache/**, projectscache-builtinandcache-varnish) require the built-in FPC or Varnish and fail rather than skip when it isn't active. They cover: a text edit on Luma and Hyvä product and category pages and on Luma and Hyvä CMS pages with a Catalog Products List widget (also when such a page was rebuilt from the cached widget block);stop_processing, disabling a label and movingactive_frominto the past on Luma and Hyvä product and category pages; a priority change on Luma and Hyvä category pages; and, built-in FPC only, the schedule-boundary cron reaching Luma and Hyvä category pages cached before a label started or ended. The REST specs (specs/api/**) exercise restricted ACL roles and the PUT id binding, including the query-string requests the old route got wrong - New integration suites proving the indexer, the scheduler and the admin Save through Magento's own machinery: cron execution via
crontab.xml/Magento\Cron\Model\Config(Test/Integration/Cron/FlushScheduledLabelCacheCronTest), mview consumption of product/category/stock changes in "Update by Schedule" mode (Test/Integration/Mview/LabelMviewTest), insert/page batch-boundary and mid-batch-failure rollback behaviour (Test/Integration/Edge/IndexerBatchBoundaryTest), multi-currency/composite-pricing label text variables (Test/Integration/Edge/PriceVariablesTest), and the admin label Save through real routing and DI, a failing reindex included (Test/Integration/Controller/Adminhtml/Label/SaveTest) - Label priority and "Stop Further Label Processing" (client request) — labels can now be reordered on the storefront instead of always sorting by id. A new
priorityfield (grid-sortable, admin form field,priority ASC, label_id ASCprocessing order) and astop_processingcheckbox let a merchant say "once this label is shown, render nothing lower in the stack." The flag only takes effect when its own label actually displays (a suppressed-by-trigger label never stops anything), and a higher-priority label always paints visually on top of lower-priority ones (Label::getStackOffset()drives the wrapperz-index). Both fields are exposed on the label grid (priority,stop_processingcolumns) and over REST (LabelInterface::STOP_PROCESSING+ getter/setter onLabelInterface) Api/LabelManagementInterface::create()— POST/V1/qoliber-productlabels/labelnow routes to a dedicated create-only service contract instead of the sharedsave(), so a POST can no longer silently overwrite an existing label by id- New
Api/ProductLabelIdentitiesInterface::getIdentitiesByProduct(), implemented byRendererPool— returns the cache identities of every label indexed for a product, built through the normal label-block pipeline (so anyafterGetIdentitiesplugin still applies) without an extra query for already-preloaded products - A schedule-boundary cron (
qoliber_productlabels_flush_scheduled_label_cache, every minute) purges both the page cache and the app cache for products whose enabled label just started or stopped being active, so a label'sactive_from/active_toboundary no longer waits for the next full reindex or product save to be reflected on cached pages. Products are purged one tag each up to 1000 products; above that the generic product tag is purged instead (a whole-catalogue label would otherwise send hundreds of PURGE requests to every Varnish node) - Storefront
{PRICE}/{SAVE_PERCENT}/{SAVE_AMOUNT}variables now read from the product's price info (tax/display aware) instead of the raw price attributes
Security
- SVG label sanitizer rewritten as an element/attribute allowlist (
Model/Label/SvgSanitizer) — the previous denylist missed real bypasses found this cycle: undeclared-namespacexlink:href, case-obfuscated attribute/element names (HREF,<set ATTRIBUTENAME="href">), HTML-parser integration points inside<desc>/<title>that let attacker markup re-parent the surrounding page, CDATA/comment breakout, and CSSurl()/image-set()escapes — including CRLF-terminated escapes that survived naive decoding. The sanitizer now case-folds ASCII-only (neverstrtolower//i, both locale-sensitive and reopen the SMILhrefvector under locales such astr_TR), rebuilds a fresh document from an explicit element and attribute-name allowlist, constrainsid/classto safe identifier syntax, and checks CSS in linear time (the first behavior-property check was quadratic and could take 8+ seconds on a crafted 100 KB stylesheet).<style>blocks are kept (Illustrator exports rely on them for.cls-*styling) but their CSS text is rejected wholesale if it contains@import, a non-localurl(),image-set,expression(,behavior/-moz-bindingor a stray backslash - Fixed stored XSS in the admin label grid — the preview and position columns rendered
label_text,label_text_color,label_base64andlabel_positionstraight from stored data with no escaping; a crafted value (or a row with malformedlabel_serialized) could inject markup into the grid or blank the whole grid response. Both columns now decode through the shared normalizer and escape every output (escapeHtml/escapeUrl/escapeHtmlAttr), and a bad row is skipped rather than breaking the page - Fixed storefront XSS via the base64, image and shape label templates — a crafted
data:URI, image URL or shape reference could break out of its HTML attribute. All three templates now escape throughescapeHtmlAttr/escapeUrland only render values the normalizer judged safe - Label data (
label_serialized,qty_trigger) is now normalized and validated on every write path — the admin Save controller, the resource model (so REST and any other writer gets the same guarantees), and again on every read — instead of trusting whatever was previously persisted - The admin form's custom-SVG preview is now rendered inside an
<img data-bind="attr: {src: ...}">instead of a knockouthtml:binding, so even a value that slipped past server-side sanitization can only ever be rasterized, never executed, in the preview pane - The admin label grid previews custom SVG as an
<img src="data:image/svg+xml;base64,...">too, instead of printing the sanitized SVG inline: inline, the<style>sheets and element ids the allowlist keeps applied to the whole admin page - Fixed CSRF-able label and design deletes —
Deletecontrollers accepted plain GET requests; they are now POST-only (HttpPostActionInterface), and the grid's delete action is submitted as POST - Tightened ACL —
Edit/NewActionnow declare their own resource instead of inheriting the parent controller's, the label listing'saclResourceisLabels_grid, and new condition rows are built by the module's ownNewConditionHtmlcontroller (Qoliber_ProductLabels::Label_save, POST) instead of the Cart Price Rules controller, so editing label conditions no longer needs the Cart Price Rules permission (Magento_SalesRule::quote). The SKU and category choosers inside a condition are still core Catalog Price Rule widgets and still needMagento_CatalogRule::promo_catalog - Hardened label image uploads — server-side image validation (
validateUploadFile), a 2 MB size cap enforced before the file is written, and an upload response that returns onlyname/file/url/size/type(no server paths)
Fixed
- A failed realtime label reindex is now recoverable, and Save reports it truthfully. A realtime ("Update on Save") reindex failure was logged and the label's own cache tag purged, but the indexer was never invalidated and nothing arranged a retry — a stale or missing index could only be fixed by a manual reindex, and the admin Save controller still showed "You saved the Product Label." (or, for Save and Apply, "The rule has been applied to products.") as if nothing had gone wrong. The observer now invalidates the indexer on a realtime failure (
indexer_reindex_all_invalid, so the next indexer cron run rebuilds it) and records the outcome; Save shows a warning instead — "The label was saved, but it could not be applied to products. The label index was marked for a rebuild, which runs with the next indexer cron." — for a plain Save and for Save and Apply alike, and Save and Apply no longer claims the label was applied. When the index could not be marked for a rebuild either — the recovery invalidate failed as well, or a scheduled-mode invalidate failed (a deadlock on the indexer state table, for example) — the failure is logged and the warning says so and asks for a manual reindex (bin/magento indexer:reindex qoliber_productlabels_indexer, or Save and Apply in "Update by Schedule" mode). A scheduled-mode Save and Apply in that case skips the warning and applies the label itself (which leaves nothing for the missed rebuild), reporting only the apply's own result; an observer failure before the indexer mode is known gets the manual-reindex warning and no apply. Otherwise Save and Apply works as in 1.4.x: in "Update by Schedule" mode it reindexes the label, also after an edit that could not change matching - Related, upsell and cross-sell product images keep their own labels. The image plugin suppressed label rendering for every product image on a page once the PDP's main gallery had rendered its own label, regardless of which product the image actually showed — a related or upsell item shown after the main gallery got no label at all. Suppression is now scoped to the same product only (the gallery's own product id vs. the image's product id); an image with no known product renders unmodified instead of risking a fatal
- Scheduled-mode label saves now purge pages after commit too. In "Update by Schedule" mode, a save that affects the index (status, conditions, store views) invalidated the indexer and returned without purging anything beyond core's own in-transaction purge, which runs before the commit — a request landing between that purge and the commit could re-cache the old label state, and nothing purged it again until the next full reindex. Disabling a label, for instance, could keep showing it on an already-cached page. The scheduled path now falls through to the same post-commit purge every other save path already runs
- Catalog Products List widget blocks are now tagged with their listed products' labels (
qlbl_<id>plus the genericqlbl), so a label edit that needs no reindex (text, priority,stop_processing, triggers) purges the widget instead of waiting for its block cache (up to 86400 s) to expire on its own. This also covers a page rebuilt while the widget's ownblock_htmlentry is still cached — the widget's complete identities are stored alongside that cache entry and merged back in on a hit, so a later label or date-window edit still purges it correctly - The schedule-boundary cron now ends its window on the database clock the storefront uses to decide whether a label is active, instead of PHP's clock: with PHP running ahead of the database, a label's start could be purged before the storefront actually started showing it, and the page cached in between would keep it hidden with nothing left to purge it later. Label expiry is likewise purged only once the label has actually disappeared from the storefront (in
[from, to)rather than treatingactive_toitself as already expired), so a page cached in that same second doesn't get purged while the label still shows PUT /V1/qoliber-productlabels/label/:labelIdnow always updates the label named in the URL. Magento's own body-id override only applies to a single-key body with no query string, so with a query string (?x=1) or several top-level body fields the body'slabel_iddecided which label was written: a different id updated that other label, and no id created a new one. The route now targets a dedicatedApi/LabelUpdateInterface::update(), which binds the URL id authoritatively; an unknown id now returns 404 instead of creating a new label- Full-page cache tags were dropped whenever a label rendered.
RendererPoolwrote the page'sX-Magento-Tagsheader directly, discarding whatever core's own cache-tag preprocessor had already collected (cat_p/cat_cand friends). Any page that rendered a label was effectively never purged by ordinary product/category saves. Label tags are now merged into core'sPageCacheTagsPreprocessorCompositeoutput through a plugin instead of being written independently - The indexer now resolves its table names through
ResourceConnection::getTableName(), which applies the configured table prefix (it used the database adapter'sgetTableName(), which does not, so on installs with a table prefix the indexer addressed a table that does not exist), rebuilds transaction-safely instead of truncating, and isolates one label's failure from the rest — a single label with an unsupported condition operator no longer empties the whole index; its previous rows are removed and every other label still indexes correctly - Per-product/per-store validation replaces a SQL pre-filter that over-matched: "SKU is X" no longer also matches "X-2" (the pre-filter used a
LIKE), and store-view-scoped attribute values are now respected instead of ignored. Negated conditions (!=, "ALL FALSE" groups) are validated correctly by skipping the pre-filter entirely when a condition tree can't be safely narrowed by SQL — merchants may see fewer labelled products where the old matching over-matched - Scheduled and expired labels are now indexed (previously excluded by the indexer's own date filter, duplicating
RendererPool's date check and hiding labels that should reappear once their window opens) - Store-view normalisation is now consistent everywhere —
[0](All Store Views) expands to every real store view id, and["0","1"]-style mixed string/int lists are normalised the same way on write, read, and in the indexer - Added
catalog_category_productandcatalog_product_websitemview subscriptions, so category and website reassignment now triggers a reindex the way product saves already did - Saving a label now keeps the index fresh — realtime mode ("Update on Save") reindexes that label immediately; scheduled mode ("Update by Schedule") invalidates the indexer, so the next cron run does a full reindex (as core Catalog Price Rules do), and "Save and Apply" additionally reindexes that label right away — instead of waiting for a manual reindex. Only saves that can change matching (a new label, its conditions, status or store views) touch the index
- Label saves that need no reindex (text, colour, image, priority, "Stop Further Label Processing", triggers) now purge the label's cached pages after the save is committed. Before, only core's purge inside the save transaction ran: a request between that purge and the commit could cache the old label again, and nothing purged it later; block-cached HTML tagged with the label (Hyvä list items) was never cleaned
- Moving a label's date window (
active_frominto the past, clearing it, extending an expiredactive_to) now purges the pages of the products the label is indexed for (per product up to 1000, the generic product tag above). Pages cached while the label was outside its window never showed it and carried no label tag, so these edits were never reflected until the pages expired - Partial REST updates (
PUTwith a subset of fields) no longer wipeconditions_serialized; the repository now merges a detached payload onto the stored label instead of overwriting it wholesale - An unresolvable
{ATTR:typo}text variable no longer fatals the page; it renders as empty text - Composite product prices in label text variables —
{PRICE}no longer shows0.00for configurable, bundle and grouped products; grouped products (no regular-price model of their own) show the minimal final price of the group, and their{SAVE_*}values are 0 (see Changed) - Countdown labels (
{SPDL}/{SPHL}) now use the store timezone consistently, actually keep ticking after the page loads (previously froze at the first render), and initialize without CSP violations on a strict-CSP Hyvä store - Labels now render for products that were never preloaded into the current request (for example, a CMS widget product list), not only for products loaded through the main collection
- The stock-quantity join used for the qty trigger and
{STOCK}now filters to the default stock, instead of potentially joining an unrelated stock row - The admin grid no longer breaks entirely when one row has malformed
label_serialized; that row is skipped and the rest of the grid still renders - Text-only labels (visual type "Text", or a label whose image, shape or design is missing) no longer render half outside the product image on Luma and Hyvä. With no artwork to centre over, the label box collapsed to 0x0 on its corner and the text was centred on that point (one word per line); the text is now laid out in normal flow and the label box is anchored at its position. Labels with artwork render as before
priorityandstop_processingare validated on every write path: a priority that is not a whole number of 0 or more is refused with "Priority must be a whole number of 0 or more." (REST clients used to get the raw SQL error, or a silently stored 0), andstop_processingis stored as0/1(a REST"5"was stored as 5 and shown blank)- Listing pages decode and sanitize each label's data once per request instead of once per product showing it (a 20 KB custom SVG label cost about 90 ms extra on a 36-product listing)
- Fixed several admin form-widget bugs: the quantity-trigger "enabled" checkbox now round-trips correctly regardless of whether the stored value is a boolean,
1or"1"; the text-size range slider and rotate rotary switch now stay in sync when the underlying observable changes programmatically (not only on user drag)
Changed
- Custom SVG is re-sanitized on read. Existing labels lose whatever falls outside the new allowlist:
<a>,<switch>,<foreignObject>,<feImage>,<mpath>,<metadata>, editor-namespace attributes, markup inside<desc>/<title>, and<image>hrefs that arehttp://or relative.<style>sheets are kept where their CSS passes the new safety checks. No data migration runs; this happens automatically the next time each label is rendered, saved, or shown in the grid - Labels now require at least one store view — REST clients (and the admin form) that previously saved a label with an empty
storesarray now get a validation error instead of a silently store-view-less label - Matching is stricter in general (see Fixed above): SKU "is" is exact, store-view attribute values are respected. Review your labels' conditions if you were relying on the old, looser matching
data-countdown-endreplacesdata-special-to-dateon the label wrapper markup. Any theme override oflabel/wrapper.phtml(Luma or Hyvä) that reads the old attribute must be updated, or its countdown will stop working{PRICE}/{SAVE_PERCENT}/{SAVE_AMOUNT}now come from price info rather than raw attributes, so they reflect tax display settings and (for grouped products) the minimal final price; grouped-product savings are always 0 since a grouped product has no own regular price to compare against- Constructor signature changes (not
@api, but affects any subclass or DI override that callsparent::__construct()positionally):Model\Label(theCombine $conditionFactoryfirst parameter is removed),Renderer\RendererPool(ResponseInterface/SvgSanitizerremoved;LabelDataNormalizerandLabelTagCollectoradded),Block\Renderer\Label(new requiredLabelDataNormalizerparameter),Controller\Adminhtml\Label\Save,DeleteandUpload,Model\Indexer\ProductLabels(newLoggerInterface,LabelDataNormalizer,CacheContext,Model\Indexer\PageCacheCleanerandModel\Label\Variable\Resolver\Stockarguments before$batchSize, plus a new trailing$cachePurgeThreshold),Model\ResourceModel\Label(now has a constructor:LabelDataNormalizerandJsonafter the resource context),Block\Adminhtml\Label\Edit\Tab\Conditions(newLabelInterfaceFactorybefore$data),Ui\Component\Listing\Column\LabelPreview(SvgSanitizerreplaced byLabelDataNormalizerandEscaper),Ui\Component\Listing\Column\LabelPosition(newLabelDataNormalizer,Magento\Framework\View\Asset\RepositoryandEscaper) andUi\DataProvider\Label\Form\Modifier\ProductLabelHydrator(newLabelDataNormalizer). The unusedBlock\Adminhtml\Label\Edit\DuplicateButtonwas removed - Custom implementations of
Api\Data\LabelInterfacemust add the newgetStopProcessing()/setStopProcessing()methods - REST: a
PUT /V1/qoliber-productlabels/label/:idfor an id that does not exist now returns 404 instead of answering with success without saving anything, and an invalidpriorityreturns 400 with a readable message - The label wrapper templates (
label/wrapper.phtml, Luma and Hyvä) render the visual-type template before the wrapper and mark text-only labels (Luma:_text-onlyclass on.product-label; Hyvä: the text dropsabsolute inset-0). Stores that styled around the old text-only layout will see those labels move inside the image - Known limitations:
- A full reindex still runs inside a single database transaction. This is strictly safer than the previous truncate-then-rebuild (readers never see a half-built index), but a very large catalog produces a longer-lived transaction during a full reindex. A chunked/replica-swap approach is a candidate follow-up
- Labels rendered inside Varnish ESI blocks do not add their label tags to the ESI response, so label edits that need no reindex do not purge those blocks
- In "Update on Save" mode nothing reindexes labels or purges their pages when products or stock change; only label saves do
- A failed Varnish PURGE is logged by Magento core and not retried (the schedule-boundary cron included) — a purge that a Varnish node drops for any reason is not resent until the next event that purges the same tag
- The
qoliber/mysql-utilsdependency floor is raised to^1.1,magento/module-page-cacheto^100.4.1(needed for the cache-tag preprocessor plugin),magento/module-catalog-widget(the widget label-tags plugin — Magento_CatalogWidget must stay installed and enabled), and requires onmagento/module-catalog-inventory,magento/module-config,magento/module-cron,magento/module-eavandmagento/module-webapiwere added to reflect code the module already used but did not declare
Upgrade notes
- Run
setup:upgradefor the newstop_processingcolumn and the composite(product_id, store_id)index on the label-to-product table, then reindex (or let the scheduled indexer catch up) so priority ordering and the new store-scoped matching take effect - Web API contract changes: regenerate SOAP clients and clients generated from the REST schema.
POST /V1/qoliber-productlabels/labelandPUT /V1/qoliber-productlabels/label/:labelIdno longer callLabelRepositoryInterface::save()butLabelManagementInterface::create()andLabelUpdateInterface::update()(see Added and Fixed); URLs and request and response bodies are unchanged. SOAP: the operationqoliberProductLabelsLabelRepositoryV1Saveis gone, because no route referencessave()any more; creating isqoliberProductLabelsLabelManagementV1Createand updating isqoliberProductLabelsLabelUpdateV1Update, on the new servicesqoliberProductLabelsLabelManagementV1andqoliberProductLabelsLabelUpdateV1. REST schema (/rest/<store>/schema): both operations are now tagged with their new service. Magento 2.4.4 builds operationIds (and body parameter names) from the service and method, so thereqoliberProductLabelsLabelRepositoryV1SavePost/qoliberProductLabelsLabelRepositoryV1SavePutbecomeqoliberProductLabelsLabelManagementV1CreatePost/qoliberProductLabelsLabelUpdateV1UpdatePut; later releases build them from the HTTP method and path, which are unchanged - Flush the full page cache (built-in or Varnish) after upgrading. Pages cached under 1.4.0 kept only the
qlbl_*label tags in theX-Magento-Tagsheader the page cache reads (see Fixed), so product and category saves would never purge them - If any theme overrides
label/wrapper.phtml, update it fordata-countdown-endand for the text-only label layout (see Changed) - Use "Update by Schedule" for the label indexer, in particular for stock-driven labels (stock or qty triggers,
{STOCK}): in "Update on Save" mode the label indexer receives no product or stock change events, so their pages are not purged when stock changes - The
Qoliber_ProductLabelsMsiadd-on is not part of this release's fixes. Its stock provider joinsinventory_stock_<id>.product_id, a column only the default-stock view (inventory_stock_1) has; the index tables of custom MSI stocks are keyed by SKU, so a website assigned to a custom stock gets a database error. Keep the add-on disabled until it gets its own fix pass - Environments running the
creatuity/magento2-interceptorscompiled interceptor generator: only onearoundplugin per method is honoured, and adding a plugin can require clearinggenerated/staticcachefor it to take effect. This release addedafterGetIdentities/afterProcess-style plugins only (no newaroundplugins), but see that package's known limitations if you add your own Magento_CatalogWidgetmust remain installed and enabled — the module now hard-requiresmagento/module-catalog-widgetfor the Catalog Products List label-tags plugin (see Changed)rector/rectoris only a require-dev of this repository's own development install (the floor guard used bydev/tools/qoliber/validate.sh); it is not, and should not be, a dependency of any shipped module
Label Designer (Creator)
Added
- New
Test/Integration/Model/CreatorVisualTypeTest— asserts visual type 5 ("Creator Design") is offered after the core types and that theLabelblock maps it toQoliber_ProductLabelsCreator::label/type/creator.phtml, moved out of ProductLabels' ownVisualTypesDiTestso ProductLabels' integration suite passes on an install without this module (a base-only install previously failed on assertions that only hold with Creator present) - Storefront and admin-preview label images from a Creator design now carry
width/heightattributes, sized from the design's ownpreview_width/preview_height(recorded at save time as half the canvas size) instead of rendering at native canvas resolution. The admin label-form preview also shows the design at this storefront size instead of at 2x. Pre-1.5.0 designs keepNULLsizes and render exactly as before Api/DesignSearchResultsInterfaceis now actually implemented (Model/DesignSearchResults) —DesignRepository::getList()previously threw aTypeErroron every call because it returned a plainSearchResultswhere the interface required the typed one- A dedicated grid collection (
Model/ResourceModel/Design/Grid/Collection) selects only the columns the design grid needs, instead of loading fullcanvas_dataper row
Security
- Design delete is now POST-only (
Controller/Adminhtml/Design/DeleteimplementsHttpPostActionInterface), closing a CSRF-able GET delete - A design still referenced by a label (visual type 5 only — a label switched away from Creator no longer blocks the design it used to point at) can no longer be deleted; the admin sees which labels use it and must change those first
- Design preview uploads are now validated: must decode as a
data:image/…;base64,URI, be a real PNG or JPEG (checked by content, not the claimed MIME type), at most 5 MB decoded and 4096px per side — rejected before any file or database row is written - Preview files are saved atomically with a content-addressed, cache-busting filename (
design_<hash>.png/.jpg) written before the database row is committed; a failed save releases the new file instead of leaving an orphan. The preview a design had before an update is kept: file names are content hashes, so an old URL keeps showing the old preview, and block-cached storefront HTML (Hyvä product list items, catalog widgets) can still reference it until it is rebuilt. Deleting a design removes its current preview file, unless another design has the identical preview - The storefront Creator template (
label/type/creator.phtml) only passes int or string design IDs to the preview resolver: any other storedcreator_design_id(an array, a boolean) renders nothing instead of throwing aTypeErrorthat turned the product page into a 500 - The editor's generated config JSON is now emitted through
JsonHexTaginstead of plainjson_encode, closing a</script>breakout in the admin editor page
Fixed
Design::getList()/search — see Added; this also fixes the admin design grid, which relied on the same broken search-results contract- Fabric.js 7 compatibility: the editor's legacy type checks (
'FabricText', string-based type comparisons) no longer match fabric 7's lower-cased instance types ('rect','i-text', etc.), which had silently broken the image-filter panel, group recolouring, text re-measurement after font load, and loading any pre-1.5.0 saved design (loadFromJSONrejected it outright). All type checks are now class-based (instanceof) or use fabric 7's actual lower-case type strings, and custom per-object controls (resize/rotate handles) are re-registered for fabric 7'screateControls()API - A design whose stored canvas data fails to load (legacy incompatible shape, or corrupted JSON) no longer lets the admin save over it with a blank canvas — Save is disabled until the stored design finishes loading, and stays disabled with an explicit error message if loading fails
- Save is also disabled while a stored design's web fonts are still loading (capped at 8 seconds), so a save can no longer bake in a fallback font instead of the intended one
- Fixed the blank-canvas overwrite in general: the editor previously allowed Save while a design was still being parsed
- Design name search / references to a stale
Model\Designwhen the requested design id doesn't exist now show "This design no longer exists." and redirect to the grid, instead of a fatal - Admin preview size for Creator designs on the label form now matches storefront size (was previously 2x the storefront rendering)
- Saving or deleting a design now refreshes the pages and cached blocks that show it. A plugin (
Plugin\Renderer\AddDesignIdentityPlugin) addsqlblc_design_<id>(what a design save purges from the full page cache) and the genericqlblc_design(what it cleans from the block cache) to the cache tags of every Creator label; before, cached pages and Hyvä product list items kept showing the old design - Images uploaded into the editor are scaled down so their longest side is at most twice the canvas's longest side (a 2000 px photo on a 300 px canvas is embedded at 600 px), which keeps the stored canvas data and the previews small
Changed
- Preview upload cap raised from an initial 2 MB to 5 MB decoded (a full-resolution export of a photo-heavy 1200px canvas can exceed 2 MB); a base64-encoded 5 MB preview is roughly 7 MB over the wire, so installs with a low
post_max_size/client_max_body_sizemay need to raise it (Magento's own recommendation ofpost_max_size >= 64Malready covers this) - The editor now exports previews at 1x canvas size (was exporting the display multiplier); design dimensions elsewhere are derived from the recorded
preview_width/preview_height - Removed 110 unreferenced images from
view/adminhtml/web/images/creator/and two dead JS modules (js/editor/drawing.js,js/editor/custom-selects.js) that no template or script referenced - Added a Creator-specific admin CSP whitelist (
etc/adminhtml/csp_whitelist.xml) for the Google Fonts hosts the editor loads - Constructor signature changes (not
@api):DesignRepository(newPreviewImageStorage),Controller\Adminhtml\Design\Save(Filesystemreplaced byPreviewImageStorageandLoggerInterface),Delete(newDesignUsage),Edit(newDesignRepositoryInterface),Plugin\Listing\LabelPreviewCreator(newEscaper) andBlock\Adminhtml\Editor(newJsonHexTagbefore$data). Any subclass callingparent::__construct()positionally needs updating - Replaced design preview files are no longer deleted when a design is updated (see Security), so earlier previews of edited designs stay in
pub/media/qoliber/productlabelscreator qoliber/product-labelsdependency raised to^1.5.0- Version bump to align with ProductLabels 1.5.0
Upgrade notes
- Run
setup:upgrade— this module previously shipped without adb_schema_whitelist.json; one is now generated so its declarative schema is enforced the same way as every other Qoliber module - If you deep-link to any of the 110 removed
images/creator/*assets from custom code or CMS content, re-add them before upgrading; grep confirmed no reference from this suite's own code
Hyvä Compatibility
Added
- Registered
ViewModel\ProductListItemLabelTagsas anadditional_item_renderer_processorsentry on Hyvä'sproduct_list_itemblock. Without it, a Hyvä product-list item served from Hyvä's ownblock_htmlcache never re-ran the label renderer, so the page'sX-Magento-Tags(and the cached item's owncache_tags) lacked the product'sqlbl_*tags — a label edit that didn't trigger a reindex (text, colour, image, priority,stop_processing, a qty trigger) would leave stale cached listing items on Hyvä storefronts. The processor now runs on every list-item render, cache hit or miss, and adds the item's label identities plus the genericqlbltag to both the page's tag collector and the item's owncache_tags - New
js/label-countdown.phtml, registered inbefore.body.end: an Alpine component (qoliberLabelCountdown) that replaces the old inline countdown markup and reads the shareddata-countdown-endattribute (see ProductLabels changelog); it keeps ticking after the initial render and registers withHyvaCspso it runs cleanly under a strict Content-Security-Policy
Fixed
- The Hyvä label wrapper's z-index is now driven by the label's processing order (
stack_index/stack_sizefrom ProductLabels), so a higher-priority label reliably paints on top of a lower-priority one on the storefront; the wrapper's fixedz-10class is dropped in favour of an inline value, capped below the fullscreen gallery overlay's own z-index - Label countdown text (
{SPDL}/{SPHL}) previously rendered once and never updated; it now ticks every 60 seconds via the new Alpine component - Text-only labels (visual type "Text", or a label whose image, shape or design is missing) no longer render half outside the product image. The wrapper renders the visual-type template first; when there is no artwork the label text drops
absolute inset-0and stays in normal flow, so it gives the label box its size and the position classes anchor it. No new Tailwind classes (only classes the template already used), and the inline z-index is unchanged
Changed
- Escaping in the label wrapper template moved from
escapeHtml()(the label and position class names were escaped twice) toescapeHtmlAttr()for every attribute value (the class names, thestyleattribute anddata-countdown-end); the label text is still printed throughescapeHtml(). Escaping only — no behaviour change for well-formed label data - If you have overridden
label/wrapper.phtmlin a child theme, also port the text-only handling (render the type template first, dropabsolute inset-0from the text when it outputs nothing), or text-only labels keep the old layout data-countdown-end(epoch milliseconds) replacesdata-special-to-dateon the label wrapper. If you have overriddenlabel/wrapper.phtmlin a child theme, update it — the old attribute is no longer emitted and the new Alpine countdown component only reads the new onehyva-themes/magento2-theme-moduledependency raised to^1.3.11(the floor that shipsHyvaCsp/strict-CSP support, needed by the countdown component, and confirmed to already include theadditional_item_renderer_processorsblock argument used above, which itself shipped in 1.1.15)qoliber/hyva-module-registrationconstraint tightened from an unbounded>=1.0.0to^1.0qoliber/product-labelsdependency raised to^1.5.0
Upgrade notes
- Requires ProductLabels 1.5.0 or later — the countdown attribute rename and the label cache-tag collector this module plugs into both ship there
- Known dependency mismatch (unresolved, flagged for the
hyva-module-registrationpackage owner): the installedqoliber/hyva-module-registration1.0.1 caps PHP at~8.4.0, while this suite's modules declare~8.5.0. Not fixed here — no PHP constraint was touched in this release
1.4.0 — 2026-07-31
Product Labels
Security
- Stored XSS via Custom SVG label —
label_custom_svgwas taken straight from the admin POST body, stored unmodified, and rendered unescaped on the storefront (label/type/svg.phtml) and in the admin grid preview (Ui/Component/Listing/Column/LabelPreview.php). SVG is an active document format:<script>,on*event handlers and<foreignObject>all execute. Any admin holdingQoliber_ProductLabels::Label_savecould put persistent JavaScript on every product page the label matched, and on the label grid seen by other admins - Added
Model/Label/SvgSanitizer— parses SVG as XML and removes<script>,<foreignObject>,<iframe>,<embed>,<object>,<handler>,<listener>,<audio>and<video>elements, everyon*event-handler attribute, and any attribute carrying ajavascript:,vbscript:ordata:text/htmlURI (whitespace- and case-obfuscated variants included). Markup with a doctype, markup that is not well-formed XML, and markup whose root is not<svg>are rejected outright - SVG is sanitized on save and on read — rows written before this release can still contain active markup, so
RendererPool::getLabelDataByProductIds()re-sanitizes on the way out. No data migration is required - Removed
svgfrom the label image uploader's allowed extensions (Controller/Adminhtml/Label/Upload.php). Uploads are served frommedia/on the storefront origin, where an SVG carrying script executes with same-origin privileges. Use the Custom SVG field, which is sanitized - Label grid was reachable without the Product Labels permission —
Controller/Adminhtml/Label/Index.phpextendedMagento\Backend\App\Actiondirectly instead of the module's abstractLabelcontroller, so it inheritedMagento_Backend::admin, which every authenticated admin satisfies. The menu entry was gated onQoliber_ProductLabels::Labels_grid, but that resource was enforced nowhere — an admin role without Product Labels access could openqoliber_productlabels/label/indexdirectly and read every label. The controller now declaresLabels_grid
Fixed
- Three Web API routes rejected every caller, including full-admin tokens —
etc/webapi.xmlguarded the two GET routes and the PUT route onQoliber_ProductLabels::Label_viewandQoliber_ProductLabels::Label_update, neither of which existed inetc/acl.xml. An undefined ACL resource makesisAllowed()return false unconditionally. Both resources are now declared - Deleting a non-existent label reported success —
Controller/Adminhtml/Label/Delete.phploaded and deleted without checking the model had loaded, so a stalelabel_idproducedWHERE label_id = NULL, removed nothing, and still showed "You deleted the Label." - Removed
ReflectionMethod::setAccessible()fromTest/Unit/Renderer/RendererPoolTriggerTest.php— a no-op since PHP 8.1 and deprecated in 8.5, where the Magento unit bootstrap promotes deprecations to errors. It failed 36 tests on PHP 8.5
Added
- Unit test suite for
SvgSanitizer— 21 tests covering script elements, event handlers at root and nested depth,<foreignObject>,javascript:/vbscript:URIs, obfuscated schemes,javascript:insidestyle, doctype entity payloads, malformed and non-SVG input, preservation of legitimate artwork (includingcurrentColor, which the module's CSS-variable theming depends on), and idempotency
Changed
- Verified compatibility across the declared PHP range: the Rector downgrade floor guard reports no syntax above PHP 8.1, and the unit suite passes on 8.1–8.5
- Declared PHP 8.5 support (
~8.5.0added to thephpconstraint). Note this is unit-level verification:magento/frameworkstill constrains to~8.2.0||~8.3.0||~8.4.0, so a full Magento install on 8.5 is not yet possible and the integration suite has not been exercised there
Removed
- Dropped the
qoliber/coredependency. Nothing in this module referenced it: the admin menu is parented onMagento_Backend::marketing, the ACL tree onMagento_Backend::admin, there is nosystem.xmlusing Core'sqoliberconfig tab, no PHP references theQoliber\Corenamespace, andetc/module.xmlnever sequencedQoliber_Core. The only remaining link is a<sequence>entry inQoliber_MySqlUtils, which Magento tolerates when the module is absent (Loader::expandSequence()resolves a missing entry to an empty list)
Label Designer (Creator)
Changed
- Version bump to align with ProductLabels 1.4.0
- Updated dependency: requires
qoliber/product-labels>= 1.4.0 - Declared PHP 8.5 support (
~8.5.0added to thephpconstraint). Note this is unit-level verification:magento/frameworkstill constrains to~8.2.0||~8.3.0||~8.4.0, so a full Magento install on 8.5 is not yet possible and the integration suite has not been exercised there
Removed
- Deleted the unreferenced duplicate asset
view/adminhtml/web/images/creator/animate-arrow - Copy.png. The space in the filename is hostile tostatic-content:deployand to web servers servingpub/static
Hyvä Compatibility
Fixed
- Labels rendered unpositioned on Hyvä 1.3 (Tailwind 3) — the module shipped no
view/frontend/tailwind/directory. On Tailwind 4 (Hyvä 1.4+) that is harmless, becausehyva-sourcesemits an automatic@sourcefor any module without amodule.css. On Tailwind 3 there is no automatic scanning: a module contributes content globs only through its owntailwind.config.js, and the Hyvä theme config ships with itsapp/codeglob commented out. Every utility used solely inlabel/wrapper.phtmlwas therefore purged — silently, with no build error — and since the wrapper does all of its positioning with utilities (absolute,top-0,left-1/2,inset-0,z-10/z-20,flex), labels fell into normal document flow below the product image - Added
view/frontend/tailwind/tailwind.config.jsdeclaring content globs fortemplates/,layout/andPlugin/. ThePlugin/glob covers therelativewrapper class emitted fromGalleryPlugin.php, which the*.phtml/*.xmlglobs do not reach
Changed
- Version bump to align with ProductLabels 1.4.0
- Updated dependency: requires
qoliber/product-labels>= 1.4.0 - Declared PHP 8.5 support (
~8.5.0added to thephpconstraint). Note this is unit-level verification:magento/frameworkstill constrains to~8.2.0||~8.3.0||~8.4.0, so a full Magento install on 8.5 is not yet possible and the integration suite has not been exercised there
Notes
- No
module.cssis shipped, deliberately. The module has no CSS of its own, and adding amodule.csswould suppress the automatic@sourcethat Tailwind 4 relies on to scan this module's templates - Label content elements (
.label-image,.label-svg,.label-shape-img,.label-base64-img,.label-creator-image) still have no Hyvä styling — their Luma rules live in LESS, which Hyvä does not compile. Sizing is currently bounded only by Tailwind's preflightimg { max-width: 100% }, which resolves against the gallery rather than the label box
MSI Stock Support
Changed
- Version bump to align with ProductLabels 1.4.0
- Updated dependency: requires
qoliber/product-labels>= 1.4.0 - Declared PHP 8.5 support (
~8.5.0added to thephpconstraint). Verified by the Rector downgrade floor guard (no syntax above the 8.1 floor) and by running the unit suite on PHP 8.5. Note this is unit-level verification:magento/frameworkstill constrains to~8.2.0||~8.3.0||~8.4.0, so a full Magento install on 8.5 is not yet possible and the integration suite has not been exercised there
1.3.0 — 2026-03-22
Product Labels
Changed
- Removed MSI (Multi-Source Inventory) hard dependency — module now works on any Magento 2.4.6+ installation regardless of MSI presence
- Qty trigger evaluation moved from per-product MSI API calls to SQL JOIN on
cataloginventory_stock_iteminRendererPool::getLabelDataByProductIds()— category pages now use only 2 SQL queries total regardless of product count {STOCK}variable now works on all page types (category, product, widgets) — previously limited to product detail pages only. Uses pre-loaded stock qty from the RendererPool JOIN with zero additional queries{STOCK}variable resolver simplified to zero dependencies — readslabel_stock_qtyset on product by RendererPool, no DB connection or request object needed- Stock qty data source extracted to
StockQtyProviderInterface— pluggable via DI preference, defaults tocataloginventory_stock_item - Labels with qty trigger enabled are now hidden on composite product types (configurable, bundle, grouped) — uses Magento's
isComposite()from product type config, not hardcoded types. Composite products have qty=0 in cataloginventory, making qty-based evaluation impossible - Label Designer moved from Catalog menu to Marketing > Product Labels menu
Fixed
- Qty trigger now works on Magento installations without MSI modules
- Fixed qty_trigger JS component not syncing default
operatorfield on initial save — operator was missing from DB, causing trigger to be silently skipped on frontend - Added PHP fallback: missing
operatordefaults to<=for backward compatibility with existing data - Fixed stale index data — full reindex now truncates the index table before rebuilding; per-label reindex deletes old rows before checking if label is still active, so disabled/expired labels are properly cleaned up
- Added
WHERE label.status = 1safety check to storefront label query — prevents disabled labels from rendering even if stale index rows exist - Fixed ACL resources —
SaveandUploadcontrollers now declareQoliber_ProductLabels::Label_save,DeletedeclaresLabel_delete; base controller corrected from non-existenttop_leveltoQoliber_ProductLabels::Label - Labels now render in priority order — added
ORDER BY label.priority ASCto storefront query - Fixed base64 template XSS — validates
data:image/prefix before rendering - Fixed N+1 query in Creator admin grid — designs now batch-loaded via single collection query instead of per-ID repository calls
- Fixed N+1 in Creator storefront
DesignPreviewResolver— refactored to support batch preloading with single query - Fixed
VisualTypesDiTestintegration test — added@magentoAppArea frontendannotation after typeTemplates moved to frontend DI scope
Added
StockQtyProviderInterface— extensible stock qty source, allowing MSI or custom inventory modules to override stock data via DI preferenceStockQtyProvider— default implementation usingcataloginventory_stock_item(works without MSI)- Admin notice on qty trigger field explaining that labels are hidden on composite product types
CreatorACL resource for Label Designer access control- Integration test suite for qty_trigger save/load cycle (12 tests covering all operators, edge cases, update/disable flows)
- Unit tests for composite product hiding, missing operator default, boolean enabled, zero/negative qty, null stock,
{STOCK}variable on all page types, batch design loading
Label Designer (Creator)
Changed
- Version bump to align with ProductLabels 1.3.0
- Updated dependency: requires
qoliber/product-labels>= 1.3.0 - Label Designer moved from Catalog menu to Marketing > Product Labels menu
Fixed
- Fixed N+1 query in admin grid — design preview images now batch-loaded via single collection query instead of per-ID repository calls
- Refactored
DesignPreviewResolverto support batch preloading with single query and per-request caching
Hyvä Compatibility
Changed
- Version bump to align with ProductLabels 1.3.0
- Updated dependency: requires
qoliber/product-labels>= 1.3.0 - Removed unnecessary
qoliber/coreandqoliber/mysql-utilsdependencies
MSI Stock Support
Added
- Initial release of Product Labels MSI compatibility module
MsiStockQtyProvider— implementsStockQtyProviderInterfaceusing MSIinventory_stock_{X}views for accurate multi-source salable quantity- Automatic website → stock ID resolution via
inventory_stock_sales_channeltable - Falls back to stock ID 1 when no sales channel mapping exists
- Stock ID cached per request to avoid repeated lookups
- DI preference overrides core
StockQtyProviderwhen module is installed - Unit tests for stock ID resolution, qty retrieval, JOIN generation, and fallback behavior
- Integration tests verifying DI wiring, qty retrieval from inventory views, and SELECT JOIN correctness
1.2.0 — 2026-03-02
Product Labels
Added
- DI-extensible visual type system —
VisualTypesnow accepts$additionalTypesvia constructor injection, allowing other modules to register new label types without modifying core code - DI-extensible template mapping —
LabelblockgetTypeTemplate()replaced hardcodedmatchwith injected$typeTemplatesarray, enabling modules to register custom render templates viadi.xml - Creator Design support (visual type 5) —
LabelPreviewgrid column andSavecontroller now handlecreator_design_idfield - Unit and integration test suite (159 tests):
- Unit tests for
TextProcessor, all 7 variable resolvers,RendererPooltrigger logic,ProductLabelHydrator,StockStatus,VisualTypes,Labelblock,LabelPreview,ProductLabelsProxy - Integration tests for
LabelRepositoryCRUD, indexer conditions (SKU, price, special price, dates, booleannew, categories, combined AND/OR, attribute sets),TextProcessorvariable resolution with real products, DI wiring verification
- Unit tests for
Fixed
- Stock trigger logic in
RendererPool::shouldDisplayLabel()— stock_trigger=1 (In Stock Only) and stock_trigger=2 (Out of Stock Only) were both hiding labels for salable products; now correctly distinguished
1.1.0 — 2026-02-22
Product Labels
Added
active_tofield onLabelInterfacewith getter/setter for label end-date scheduling- Dynamic label text system with template variable support:
TextProcessorservice with pluggable variable resolvers via DI- Built-in resolvers:
{PRICE},{SPECIAL_PRICE},{SAVE_PERCENT},{SAVE_AMOUNT},{SKU},{STOCK},{NEW_FOR},{ATTR:code},{BR}(line break) - Countdown placeholders
{SPDL}(days left) and{SPHL}(hours left) for special price expiry
- Cache identity support —
Labelmodel andLabelblock now implementIdentityInterfacewithqlblcache tag _eventPrefixand_cacheTagproperties on Label model for proper Magento cache invalidation- CSS custom properties (
--label-rotate,--label-text-color,--label-text-size) on label blocks replacing inline style generation - Admin help template (
variables-help.html) for label text variable reference in the form UI - Indexer
INSERT_CHUNK_SIZEconstant (1000) — batch inserts are now chunked to avoid oversized SQL statements - Date-range filtering in indexer — only labels within their
active_from/active_towindow are indexed - Date-range filtering in frontend
RendererPoolquery — expired/future labels excluded at render time typecolumn now fetched inRendererPoolquery and passed to label blocks- Product is now set on label renderer block, enabling dynamic text resolution
label_iddata set on renderer block for cache identity- RequireJS config and
labelCountdownwidget for Luma countdown labels - Luma
_module.lessnow uses CSS custom properties for rotation, text color and font size
Fixed
- Admin conditions tab
fieldset_idkey renamed tofield_set_idto match Magento core expectations setConditionFormNamenow propagatesjsFormObjectto conditions, fixing broken condition rule UI in admin- Variable shadowing bug in indexer where
$productIdswas overwritten inside batch loop - Incorrect assignment in
Label::getConditionsInstance()that overwrote$conditionFactoryproperty - Inverted stock trigger logic in
RendererPool::shouldDisplayLabel()— labels with stock trigger now correctly hide for salable products - Unserialization error in
RendererPoolwhenqty_triggeris null/empty - Potential null access on
label_positionkey inLabelPositionandLabelTypeUI listing columns - Foreign key constraint definitions in
db_schema.xmlreferencing wrong table name (qoliber_productlabels_to_entity->qoliber_productlabels_label_to_entity) Combinecondition type now set to parent\Magento\CatalogRule\Model\Rule\Condition\Combine::classinstead of self-referencingpreview.js—isImage()now safely checks for array and length before accessingimage[0].url- Removed debug placeholder text from
default.phtmltemplate - Duplicate
validate-no-html-tagsvalidation rule in label form XML - Null-safe defaults for all serialized label data fields in
Savecontroller - Indexer now uses
getColumnValues('entity_id')instead ofgetAllIds()for reliable product ID extraction
Changed
- Replaced
ObjectManagerusage withLabelFactoryDI inDelete,Edit, andSaveadmin controllers - Constructor property promotion applied across
Index,NewAction, andEditcontrollers - Return type declarations added to
execute()methods across all admin controllers - Observer event changed from
catalog_block_product_list_collectiontocatalog_product_collection_load_afterfor more reliable label data fetching - Admin menu moved from
Qoliber_Core::menutoMagento_Backend::marketingwith updated sort orders - Removed
Qoliber_Coremodule dependency frommodule.xmlsequence - Label type field notice updated with clearer static vs dynamic explanation
- Label text variables help moved from inline notice to dedicated HTML template component
- Luma wrapper template refactored — replaced inline
<style>blocks with CSS custom properties and consolidated positioning logic - Code style: added blank line after
<?phpopening tag across all PHP files, removed unnecessary blank lines inside classes
MySQL Utils — 2026-09-11
Added
Sql\BatchInsertStatementGenerator::createInsertOnDuplicateStatement()now accepts an optional?AdapterInterface $connectionparameter, so callers on a non-default connection get their insert-on-duplicate statement prepared against that connection instead of always the default one. Backward compatible: omitting it keeps the old behaviourflatten()now accepts an optional$columnsparameter to emit row values in a given column order; without it, behaviour is unchanged. When$columnsis given, a row missing one of those columns now throwsInvalidArgumentExceptioninstead of silently shifting values into the wrong bind position
Fixed
- Cached insert statements are now re-prepared after a database reconnect.
createInsertOnDuplicateStatement()cached prepared statements keyed by the adapter object's identity, but the adapter survives a reconnect (closeConnection(), an application server's_resetState(), or the adapter's own retry after "MySQL server has gone away") while its underlying PDO handle does not — the cached statement kept the old, closed session alive by reference and, if reused, executed against it instead of the caller's current session and transaction. In an isolation-enabled test this surfaced as the cached insert running under the previous test's session. The cache key now tracks the live PDO handle's identity, so a reconnect always re-prepares the statement instead of reusing a stale one - Stale statements from a previous connection generation are now evicted from the cache instead of accumulating: every reconnect on a given adapter drops that adapter's earlier cache entries (other adapters' entries are untouched), so a long-lived process that reconnects repeatedly no longer leaks PDOStatement/PDO objects and orphaned server-side connections
- The prepared-statement cache key for
createInsertOnDuplicateStatement()now includes the connection identity (spl_object_id) alongside table, columns, row count and update columns, so two different connections used through the same generator instance no longer share (and corrupt) each other's cached statement - Table and column identifiers are now quoted (with embedded backticks escaped) when building the generated SQL, instead of being interpolated raw
createInsertOnDuplicateStatement()no longer requires Zend_Db'sgetConnection(), whichAdapterInterfacedoes not declare: an adapter without it (anotherAdapterInterfaceimplementation, or a test double of the interface) is cached on its own identity instead of failing with "Call to undefined method". The adapter still has to provideprepare(), as every Magento adapter does
1.0.7 — 2025-05-27
Qoliber Core
Changed
- Updated author information from individual developer email to generic Qoliber team email across all XML files
- Standardized author attribution in module.xml, acl.xml, adminhtml menu.xml, and system.xml files
- Updated registration.php author information for consistency
1.0.6 — 2025-04-16
Qoliber Core
Added
- Added new "Manage" submenu item to Qoliber admin menu with sortOrder 10
- Submenu provides dedicated section for managing Qoliber extensions under the main Qoliber menu
1.0.5 — 2024-10-10
Qoliber Core
Added
- Added adminhtml LESS module file (_module.less) with Qoliber logo as base64 encoded PNG image for menu branding
- Custom styling for Qoliber menu items in admin panel
Changed
- Changed Qoliber admin menu sortOrder from 10 to 60 to reposition menu in admin sidebar
- Updated Qoliber configuration tab sortOrder from 10 to 950 for better positioning in system configuration
- Added CSS class "qoliber-tab" to system configuration tab for custom styling
1.0.4 — 2024-10-10
Qoliber Core
Changed
- Migrated repository from GitLab to GitHub ([email protected]:qoliber/core.git)
- Changed license from proprietary Qoliber license to MIT open source license
- Updated LICENSE.md from custom Qoliber EULA to standard OSL 3.0 (Open Software License)
- Updated composer.json license field to reflect MIT license
1.0.3 — 2024-08-01
Qoliber Core
Added
- Added "Configuration" submenu item to Qoliber admin menu
- Submenu positioned with sortOrder 100 under the main Qoliber menu item
1.0.2 — 2024-08-01
Qoliber Core
Added
- Added source repository information to composer.json (GitLab)
- Added git reference field set to "master" branch
Changed
- Updated license URL from store.qoliber.com to qoliber.com domain
1.0.1 — 2025-05-11
Qoliber Core — 2024-08-01
Added
- Added comprehensive LICENSE.md file with Qoliber Extensions User License Agreement
- License defines usage terms, ownership, refund policy, and breach conditions
- Includes terms for yearly support subscriptions and multi-year support packages
Changed
- Updated composer.json with license reference to LICENSE.md
- Set license field to point to https://store.qoliber.com/license/
MySQL Utils
Added
- Added BatchProvider service class (Sql/BatchProvider.php) for efficient batch processing of large datasets
- Implemented fetchEntityBatches() method to retrieve entity ID ranges in configurable batch sizes (default 1000)
- Uses SQL FLOOR() function with grouping to create optimized batches based on MIN/MAX entity IDs
- Utilizes Zend_Db_Expr for database-level calculations to minimize memory usage
- Returns array of batch ranges with 'from' and 'to' boundaries for chunked processing
- Useful for processing large tables without loading all records into memory
Changed
- Cleaned up composer.json formatting and structure
- Updated module version from initial release to 1.0.1
1.0.0 — 2025-05-16
Product Labels
Added
- Initial release of Product Labels module
- Complete product label management system with advanced admin UI
- Multiple label visualization types:
- Text labels with customizable styling
- Shape-based labels with 20+ predefined shapes
- Custom image upload labels
- SVG label support
- Base64 encoded image labels
- Advanced label designer in admin panel:
- Live preview functionality showing label on product images
- Interactive position selector with 9 placement options (top-left, top-center, top-right, middle-left, center, middle-right, bottom-left, bottom-center, bottom-right)
- Visual shape selector with thumbnail previews
- Rotary color picker switch for label styling
- Font size range slider with visual feedback
- Real-time label preview on light/dark backgrounds
- Desktop and mobile preview modes
- Label condition system:
- Rule-based product selection using Magento's condition builder
- Stock status triggers (In Stock, Out of Stock)
- Quantity-based triggers with configurable thresholds
- Date-based activation (active_from and active_to)
- Priority system for multiple label handling
- Label type configuration:
- Category labels
- Product labels
- Custom label types
- Admin UI components:
- Label listing grid with live preview thumbnails
- Label form with multiple fieldsets
- Custom UI components:
- Position selector with SVG icons
- Shape selector with image gallery
- Preview component with product image overlay
- Quantity trigger component
- Range slider for numeric values
- Rotary switch for color selection
- Visual type switcher
- Save and Apply button for immediate indexing
- Save and Continue button for iterative editing
- Duplicate label functionality
- Indexer system:
- Product label indexer for performance optimization
- MView integration for real-time updates
- Label-to-product relationship index table
- Store-specific label assignment
- Efficient product-label mapping
- Frontend label rendering:
- Automatic label display on product images
- Product listing page (PLP) label integration
- Product detail page (PDP) label display
- Image factory plugin for label injection
- Gallery plugin for product image labels
- Renderer pool pattern for label type handling
- Label shapes library:
- 20+ custom Qoliber shapes (ribbons, badges, banners, flags)
- Circle, hexagon, triangle shapes
- Bestseller and New Arrival badges
- Seasonal and promotional shapes
- Quarter-circle corner labels
- List and note style shapes
- Custom JavaScript components:
- position.js - Interactive position selector
- preview.js - Live label preview
- qty-trigger.js - Quantity trigger configuration
- range-slider.js - Numeric range input
- rotary-switch.js - Color picker rotary
- shape-selector.js - Shape gallery selector
- visual_type.js - Label type switcher
- save-and-apply.js - Custom save action with indexing
- Admin styling:
- Custom Less modules for all UI components
- Label preview styling
- Shape selector grid
- Slider controls
- Rotary switch animations
- Position selector grid
- Database schema:
- Label configuration table
- Label-to-entity index table with store support
- Foreign key constraints for data integrity
- Serialized condition storage
- Serialized label configuration storage
- Observer system:
- Product collection after-load observer for label injection
- Plugin system:
- Product image block plugins for label rendering
- Image factory plugin for label overlay
- Gallery plugin for multiple images
- Repository pattern:
- LabelRepository for CRUD operations
- Search result interface for label queries
- Renderer system:
- RendererPool for extensible label rendering
- Type-specific renderers (text, shape, image, SVG, base64)
- Default fallback renderer
- UI columns:
- Label actions column
- Label position indicator
- Label preview thumbnail
- Label type badge
- Status badge with color coding
- API interfaces:
- LabelInterface for label data
- LabelRepositoryInterface for operations
- RendererPoolInterface for rendering
- LabelSearchResultsInterface for search
- Admin templates:
- Column templates for grid display
- Form element templates for custom components
- Preview templates with product image simulation
- Frontend templates:
- Label wrapper template
- Type-specific rendering templates
- Module styling:
- Frontend Less module for label display
- Admin Less modules for form components
- Web API integration for label management
- ACL permissions for label administration
- Condition combine model for rule building
- Data provider with product label hydration
- Shape modifier for UI component
- Dependent on Qoliber MySQL Utils module
Label Designer (Creator) — 2026-03-02
Added
- Initial release of Product Labels Creator module
- Canvas-based label designer with drag-and-drop support
- Design templates library for quick label creation
- Visual type 5 (Creator Design) — renders labels from saved canvas designs
- Design preview resolver for frontend label rendering
- Admin grid for managing saved designs with image previews
- Integration with ProductLabels core via DI-extensible type system
- Unit test coverage for design repository, save controller, and preview resolver
Hyvä Compatibility
Added
- Initial release of Product Labels Hyva compatibility module
- Hyva theme support for Product Labels module
- Alpine.js integration for reactive label rendering
- Hyva-specific label wrapper template with stock quantity handling
- Block renderer plugin for Hyva theme compatibility:
- Custom label rendering for Hyva product images
- Integration with Hyva's image rendering system
- Product data extraction for label conditions
- Reactive label display based on:
- Product stock quantity changes
- Stock status updates
- Dynamic quantity triggers
- Frontend DI configuration for Hyva-specific plugins
- Template overrides for Hyva theme label display
- Seamless integration with Product Labels module core functionality
- Dependency management:
- Requires Qoliber Core module
- Requires Qoliber MySQL Utils module
- Requires Qoliber Product Labels module
Qoliber Core — 2024-08-01
Added
- Initial release of Qoliber Core module
- Created base module structure with module.xml, registration.php, and composer.json
- Added ACL configuration (etc/acl.xml) with "Qoliber_Core::menu" resource
- Created admin menu structure with main "Qoliber" menu item positioned at sortOrder 10
- Added system configuration tab "qoliber" for all Qoliber modules
- Added English translations (i18n/en_US.csv)
- Set module dependency on magento/module-backend
MySQL Utils — 2025-05-11
Added
- Initial release of MySQL Utils module (QM-117)
- Created utility module for MySQL/MariaDB database operations
- Set up module structure with module.xml and registration.php
- Configured PSR-4 autoloading for Qoliber\MySqlUtils namespace
- Added composer.json with module metadata
- Set source repository to [email protected]:qoliber/mysqlu-tils
- Provides foundation for MySQL utility functions and helpers