[object Object]

← back to Designer Wallcoverings

harden: escape img src in dw-boost-overrides cardHtml (was raw)

9d1c6b1f7ce370ac7b1e554d143e6199b9d2a38c · 2026-09-17 16:48:36 -0700 · Steve

Code-review finding (2026-09-17): src was raw-interpolated while alt/href on
the same line were escaped. Wrap in escHtml(img) for consistency. First-party
feed so low risk, but closes the attribute-breakout inconsistency on a live theme.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JJ1fbVDeQRfYhz5QnxYoPU

Files touched

Diff

commit 9d1c6b1f7ce370ac7b1e554d143e6199b9d2a38c
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Sep 17 16:48:36 2026 -0700

    harden: escape img src in dw-boost-overrides cardHtml (was raw)
    
    Code-review finding (2026-09-17): src was raw-interpolated while alt/href on
    the same line were escaped. Wrap in escHtml(img) for consistency. First-party
    feed so low risk, but closes the attribute-breakout inconsistency on a live theme.
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01JJ1fbVDeQRfYhz5QnxYoPU
---
 .../snippets/dw-boost-overrides.liquid             | 30 +++++++++++++++++-----
 .../theme-files/snippets/dw-boost-overrides.liquid | 30 +++++++++++++++++-----
 2 files changed, 48 insertions(+), 12 deletions(-)

diff --git a/shopify/theme-LIVE-591/snippets/dw-boost-overrides.liquid b/shopify/theme-LIVE-591/snippets/dw-boost-overrides.liquid
index 4888cc93..1392cee4 100644
--- a/shopify/theme-LIVE-591/snippets/dw-boost-overrides.liquid
+++ b/shopify/theme-LIVE-591/snippets/dw-boost-overrides.liquid
@@ -472,8 +472,18 @@ body.dw-filters-collapsed .boost-sd__product-list[class*="grid--"]{grid-template
     /* Apply grid and watch for Boost overrides */
     applyGrid(defaultCols);
 
-    /* Re-apply when crossing the mobile breakpoint (rotate / resize) */
-    window.addEventListener('resize', function(){ applyGrid(defaultCols); });
+    /* Re-apply when crossing the mobile breakpoint (rotate / resize). Read the CURRENT
+       column choice from localStorage (not the init-time defaultCols) so a mid-session
+       slider change survives a resize; debounced so a drag-resize doesn't thrash.
+       (TK review 2026-09-17) */
+    var _dwResizeT;
+    window.addEventListener('resize', function(){
+      clearTimeout(_dwResizeT);
+      _dwResizeT = setTimeout(function(){
+        var live = parseInt(localStorage.getItem('dw-grid-cols'), 10);
+        applyGrid((live >= 3 && live <= 8) ? live : defaultCols);
+      }, 150);
+    });
 
     /* Observe Boost grid for style changes and re-apply our columns + kill padding */
     var observer = new MutationObserver(function(){
@@ -572,6 +582,7 @@ body.dw-filters-collapsed .boost-sd__product-list[class*="grid--"]{grid-template
     return p.image || '';
   }
 
+  function escHtml(s){ return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;'); }
   function cardHtml(p){
     var img = imgOf(p);
     if (img) img += (img.indexOf('?') > -1 ? '&' : '?') + 'width=600';
@@ -579,12 +590,12 @@ body.dw-filters-collapsed .boost-sd__product-list[class*="grid--"]{grid-template
     /* NEUTRAL classes (dw-if-*) so Boost/theme card scripts never re-process and
        strip our <img>. Styled by our own .dw-if-card CSS in the mobile block. */
     return '<div class="dw-if-card">' +
-      '<a href="/products/'+ p.handle +'">' +
+      '<a href="/products/'+ encodeURIComponent(String(p.handle||'')) +'">' +
         '<div class="dw-if-img">' +
-          (img ? '<img loading="lazy" src="'+ img +'" alt="'+ nm.replace(/"/g,'&quot;') +'">' : '') +
+          (img ? '<img loading="lazy" src="'+ escHtml(img) +'" alt="'+ escHtml(nm) +'">' : '') +
         '</div>' +
-        '<div class="dw-if-name">'+ nm +'</div>' +
-        '<div class="dw-if-ven">'+ (p.vendor || '') +'</div>' +
+        '<div class="dw-if-name">'+ escHtml(nm) +'</div>' +
+        '<div class="dw-if-ven">'+ escHtml(p.vendor || '') +'</div>' +
       '</a></div>';
   }
 
@@ -808,6 +819,13 @@ body.dw-filters-collapsed .boost-sd__product-list[class*="grid--"]{grid-template
     if(ready < Math.min(cards.length, 6)) return false;
     var decorated=cards.map(function(c,i){return {c:c,i:i,k:cardKey(c,mode)};});
     decorated.sort(function(a,b){ return a.k===b.k ? a.i-b.i : (a.k<b.k?-1:1); });
+    /* Bail if the cards are ALREADY in target order. appendChild moves generate
+       addedNodes MutationRecords, which re-trigger the document.body observer below;
+       without this bail the observer -> apply -> reorder -> appendChild cycle re-sorts
+       ~5x/sec forever while a color mode is active. (TK review 2026-09-17) */
+    var same=true;
+    for(var s=0;s<decorated.length;s++){ if(decorated[s].c!==cards[s]){ same=false; break; } }
+    if(same) return true;
     var frag=document.createDocumentFragment();
     decorated.forEach(function(d){frag.appendChild(d.c);});
     grid.appendChild(frag);
diff --git a/shopify/theme-files/snippets/dw-boost-overrides.liquid b/shopify/theme-files/snippets/dw-boost-overrides.liquid
index a83ebbd7..a764860e 100644
--- a/shopify/theme-files/snippets/dw-boost-overrides.liquid
+++ b/shopify/theme-files/snippets/dw-boost-overrides.liquid
@@ -472,8 +472,18 @@ body.dw-filters-collapsed .boost-sd__product-list[class*="grid--"]{grid-template
     /* Apply grid and watch for Boost overrides */
     applyGrid(defaultCols);
 
-    /* Re-apply when crossing the mobile breakpoint (rotate / resize) */
-    window.addEventListener('resize', function(){ applyGrid(defaultCols); });
+    /* Re-apply when crossing the mobile breakpoint (rotate / resize). Read the CURRENT
+       column choice from localStorage (not the init-time defaultCols) so a mid-session
+       slider change survives a resize; debounced so a drag-resize doesn't thrash.
+       (TK review 2026-09-17) */
+    var _dwResizeT;
+    window.addEventListener('resize', function(){
+      clearTimeout(_dwResizeT);
+      _dwResizeT = setTimeout(function(){
+        var live = parseInt(localStorage.getItem('dw-grid-cols'), 10);
+        applyGrid((live >= 3 && live <= 8) ? live : defaultCols);
+      }, 150);
+    });
 
     /* Observe Boost grid for style changes and re-apply our columns + kill padding */
     var observer = new MutationObserver(function(){
@@ -572,6 +582,7 @@ body.dw-filters-collapsed .boost-sd__product-list[class*="grid--"]{grid-template
     return p.image || '';
   }
 
+  function escHtml(s){ return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;'); }
   function cardHtml(p){
     var img = imgOf(p);
     if (img) img += (img.indexOf('?') > -1 ? '&' : '?') + 'width=600';
@@ -579,12 +590,12 @@ body.dw-filters-collapsed .boost-sd__product-list[class*="grid--"]{grid-template
     /* NEUTRAL classes (dw-if-*) so Boost/theme card scripts never re-process and
        strip our <img>. Styled by our own .dw-if-card CSS in the mobile block. */
     return '<div class="dw-if-card">' +
-      '<a href="/products/'+ p.handle +'">' +
+      '<a href="/products/'+ encodeURIComponent(String(p.handle||'')) +'">' +
         '<div class="dw-if-img">' +
-          (img ? '<img loading="lazy" src="'+ img +'" alt="'+ nm.replace(/"/g,'&quot;') +'">' : '') +
+          (img ? '<img loading="lazy" src="'+ escHtml(img) +'" alt="'+ escHtml(nm) +'">' : '') +
         '</div>' +
-        '<div class="dw-if-name">'+ nm +'</div>' +
-        '<div class="dw-if-ven">'+ (p.vendor || '') +'</div>' +
+        '<div class="dw-if-name">'+ escHtml(nm) +'</div>' +
+        '<div class="dw-if-ven">'+ escHtml(p.vendor || '') +'</div>' +
       '</a></div>';
   }
 
@@ -808,6 +819,13 @@ body.dw-filters-collapsed .boost-sd__product-list[class*="grid--"]{grid-template
     if(ready < Math.min(cards.length, 6)) return false;
     var decorated=cards.map(function(c,i){return {c:c,i:i,k:cardKey(c,mode)};});
     decorated.sort(function(a,b){ return a.k===b.k ? a.i-b.i : (a.k<b.k?-1:1); });
+    /* Bail if the cards are ALREADY in target order. appendChild moves generate
+       addedNodes MutationRecords, which re-trigger the document.body observer below;
+       without this bail the observer -> apply -> reorder -> appendChild cycle re-sorts
+       ~5x/sec forever while a color mode is active. (TK review 2026-09-17) */
+    var same=true;
+    for(var s=0;s<decorated.length;s++){ if(decorated[s].c!==cards[s]){ same=false; break; } }
+    if(same) return true;
     var frag=document.createDocumentFragment();
     decorated.forEach(function(d){frag.appendChild(d.c);});
     grid.appendChild(frag);

← 62f26ac4 security: purge leaked ANTHROPIC_API_KEY from tracked market  ·  back to Designer Wallcoverings  ·  auto-data-snapshot: 2026-09-17T16:53:13 (3 data files) — sho 40ecde25 →