diff --git a/custom_components/houseplan/decor_assets.py b/custom_components/houseplan/decor_assets.py index 7c7191e5..6f924239 100644 --- a/custom_components/houseplan/decor_assets.py +++ b/custom_components/houseplan/decor_assets.py @@ -12,6 +12,7 @@ import math import re import struct import xml.etree.ElementTree as ET +from xml.parsers import expat from dataclasses import dataclass from pathlib import Path from typing import Any @@ -33,6 +34,7 @@ MAX_RASTER_PIXELS = (128 * 1024 * 1024) // 4 MAX_SVG_ELEMENTS = 5_000 MAX_SVG_DEPTH = 64 MAX_SVG_ATTR_CHARS = 512_000 +MAX_SVG_ATTR_VALUE_CHARS = 65_536 _SVG_TAGS = frozenset({ "svg", "g", "defs", "title", "desc", "path", "rect", "circle", "ellipse", "line", "polyline", "polygon", "clipPath", "mask", "linearGradient", @@ -49,6 +51,9 @@ _SVG_ATTRS = frozenset({ _LOCAL_REF = re.compile(r"^url\(#[A-Za-z_][A-Za-z0-9_.:-]*\)$") _LENGTH = re.compile(r"^\s*([0-9]+(?:\.[0-9]+)?)\s*(?:px)?\s*$", re.I) _SVG_NAMESPACE = "http://www.w3.org/2000/svg" +_SVG_UNIT_INTERVAL_ATTRS = frozenset({ + "opacity", "fill-opacity", "stroke-opacity", "stop-opacity", "offset", +}) class DecorAssetError(ValueError): @@ -157,15 +162,45 @@ def _svg_number(value: str | None) -> float | None: return float(match.group(1)) if match else None -def _validate_svg(data: bytes) -> ValidatedAsset: - lowered = data.lower() - if any(token in lowered for token in (b" None: + """Reject non-finite and out-of-range opacity/gradient values.""" + raw = value.strip() + percent = raw.endswith("%") + try: + number = float(raw[:-1] if percent else raw) + except ValueError as err: + raise DecorAssetError("invalid_image", f"The SVG {name} value is invalid") from err + limit = 100 if percent else 1 + if not math.isfinite(number) or number < 0 or number > limit: + raise DecorAssetError("invalid_image", f"The SVG {name} value is out of range") + + +def _reject_svg_declarations(data: bytes) -> None: + """Parse with encoding-aware expat guards before building an XML tree.""" + parser = expat.ParserCreate() + + def reject(*_args: Any) -> None: raise DecorAssetError( "invalid_image", "DTD, entities and processing instructions are forbidden", ) - without_declaration = re.sub(br"^\s*<\?xml\b[^?]*\?>", b"", data, count=1, flags=re.I) - if b"" in without_declaration: - raise DecorAssetError("invalid_image", "XML processing instructions are forbidden") + + parser.StartDoctypeDeclHandler = reject + parser.EntityDeclHandler = reject + parser.ExternalEntityRefHandler = reject + parser.ProcessingInstructionHandler = reject + try: + parser.Parse(data, True) + except DecorAssetError: + raise + except (expat.ExpatError, UnicodeError) as err: + raise DecorAssetError("invalid_image", "The SVG is not valid XML") from err + + +def _validate_svg(data: bytes) -> ValidatedAsset: + # Byte-substring checks are bypassable with UTF-16/UTF-32. Expat detects + # the declared/input encoding first and our handlers reject declarations + # before entity expansion can allocate an ElementTree. + _reject_svg_declarations(data) try: root = ET.fromstring(data) except (ET.ParseError, UnicodeError) as err: @@ -196,6 +231,8 @@ def _validate_svg(data: bytes) -> ValidatedAsset: if tag not in {"title", "desc"} or len(node.text) > 4096: raise DecorAssetError("unsupported_image", "Unsupported or oversized SVG text") for raw_name, value in node.attrib.items(): + if len(value) > MAX_SVG_ATTR_VALUE_CHARS: + raise DecorAssetError("too_large", "An SVG attribute exceeds the safety limit") if raw_name.startswith("{"): raise DecorAssetError("unsupported_image", "Namespaced SVG attributes are unsupported") name = raw_name @@ -204,6 +241,8 @@ def _validate_svg(data: bytes) -> ValidatedAsset: raise DecorAssetError("too_large", "The SVG attributes exceed the safety limit") if name.lower().startswith("on") or name not in _SVG_ATTRS: raise DecorAssetError("unsupported_image", f"Unsupported SVG attribute: {name}") + if name in _SVG_UNIT_INTERVAL_ATTRS: + _validate_svg_unit_interval(name, value) low = value.strip().lower() if any(token in low for token in ("javascript:", "data:", "http:", "https:", "//")): raise DecorAssetError("invalid_image", "External SVG resources are forbidden") diff --git a/custom_components/houseplan/frontend/houseplan-assets.json b/custom_components/houseplan/frontend/houseplan-assets.json index ac648cea..6a98f15d 100644 --- a/custom_components/houseplan/frontend/houseplan-assets.json +++ b/custom_components/houseplan/frontend/houseplan-assets.json @@ -1,125 +1,125 @@ { "schema": 1, - "fingerprint": "91d90ad5fe8f4f44a45669312cea92fc5c5c061316d8382ea8ec58dbef1603d8", + "fingerprint": "4386b4e9f688f89cce5a1680c9f21b315933dd897345287b8c64700400b11c11", "entry": "houseplan-card.js", "initialViewFiles": [ - "houseplan-assets/houseplan-card-ruQDQ4Jw.js", + "houseplan-assets/houseplan-card-qfwHeLJR.js", "houseplan-card.js" ], - "initialViewGzipBytes": 291026, + "initialViewGzipBytes": 291041, "lazyFiles": [ - "houseplan-assets/backdrop-pick-CsM7GqVe.js", - "houseplan-assets/de-MNYUkgT1.js", - "houseplan-assets/editor-Ih3Tgb49.js", - "houseplan-assets/fr-BB7LeC_Q.js", - "houseplan-assets/houseplan-editor-runtime-ipAn0IA6.js", - "houseplan-assets/houseplan-onboarding-runtime-DB29lThG.js" + "houseplan-assets/backdrop-pick-Di7bhm6e.js", + "houseplan-assets/de-Dc1N8boU.js", + "houseplan-assets/editor-DWFSXWYH.js", + "houseplan-assets/fr-Cw28JMxl.js", + "houseplan-assets/houseplan-editor-runtime-BQ2FUiZP.js", + "houseplan-assets/houseplan-onboarding-runtime-OhzyGbC7.js" ], - "lazyGzipBytes": 213816, + "lazyGzipBytes": 213868, "lazyEditorFiles": [ - "houseplan-assets/backdrop-pick-CsM7GqVe.js", - "houseplan-assets/editor-Ih3Tgb49.js", - "houseplan-assets/houseplan-editor-runtime-ipAn0IA6.js" + "houseplan-assets/backdrop-pick-Di7bhm6e.js", + "houseplan-assets/editor-DWFSXWYH.js", + "houseplan-assets/houseplan-editor-runtime-BQ2FUiZP.js" ], - "lazyEditorGzipBytes": 158012, + "lazyEditorGzipBytes": 158042, "lazyOnboardingFiles": [ - "houseplan-assets/backdrop-pick-CsM7GqVe.js", - "houseplan-assets/houseplan-onboarding-runtime-DB29lThG.js" + "houseplan-assets/backdrop-pick-Di7bhm6e.js", + "houseplan-assets/houseplan-onboarding-runtime-OhzyGbC7.js" ], - "lazyOnboardingGzipBytes": 14130, + "lazyOnboardingGzipBytes": 14131, "lazyLocaleFiles": [ - "houseplan-assets/de-MNYUkgT1.js", - "houseplan-assets/fr-BB7LeC_Q.js" + "houseplan-assets/de-Dc1N8boU.js", + "houseplan-assets/fr-Cw28JMxl.js" ], - "lazyLocaleGzipBytes": 48848, + "lazyLocaleGzipBytes": 48871, "files": [ { - "path": "houseplan-assets/backdrop-pick-CsM7GqVe.js", - "sha256": "136838d9a113801d3d59b5198facdf568a90e9384f62133d84ee8ccfbcf91725", + "path": "houseplan-assets/backdrop-pick-Di7bhm6e.js", + "sha256": "a166e065bb3b3d4905c6e8e46c5c5157c32d03d58b8ece49caa661d78a4bf4a9", "rawBytes": 20974, - "gzipBytes": 7174, + "gzipBytes": 7176, "isEntry": false, "imports": [ - "houseplan-assets/houseplan-card-ruQDQ4Jw.js" + "houseplan-assets/houseplan-card-qfwHeLJR.js" ], "dynamicImports": [] }, { - "path": "houseplan-assets/de-MNYUkgT1.js", - "sha256": "8414e28bb23e0cc2e8d777a97b217f8a971d002109ce38168c0890b0bd4a38ff", + "path": "houseplan-assets/de-Dc1N8boU.js", + "sha256": "d5707d18ebb3f3f423037557e1692cf038337ac60655fa6684f3b596afbce5bf", "rawBytes": 81798, - "gzipBytes": 24696, + "gzipBytes": 24707, "isEntry": false, "imports": [], "dynamicImports": [] }, { - "path": "houseplan-assets/editor-Ih3Tgb49.js", - "sha256": "56f7be4294a42cb92bec171b3ea409425d70fb7ff5a4ca07ffae9190d9ea74d4", + "path": "houseplan-assets/editor-DWFSXWYH.js", + "sha256": "dcbb29ac4c28f8639044dee93688768b67d3044e6ec06dff59d028665a9e73e3", "rawBytes": 3826, - "gzipBytes": 1580, + "gzipBytes": 1584, "isEntry": false, "imports": [ - "houseplan-assets/houseplan-card-ruQDQ4Jw.js" + "houseplan-assets/houseplan-card-qfwHeLJR.js" ], "dynamicImports": [] }, { - "path": "houseplan-assets/fr-BB7LeC_Q.js", - "sha256": "63b7104bab6abf083d5478736b8f5fe16ed7ca09edf52fa8cdeda418aa041bf8", + "path": "houseplan-assets/fr-Cw28JMxl.js", + "sha256": "8209da223a0b5fe0ea3681a1a8df7f6d2a28ea4d6376953b21be47a5b2257397", "rawBytes": 83973, - "gzipBytes": 24152, + "gzipBytes": 24164, "isEntry": false, "imports": [], "dynamicImports": [] }, { - "path": "houseplan-assets/houseplan-card-ruQDQ4Jw.js", - "sha256": "99538870102416fc6844acba2719f23a26f08d9f12fb2619d2788a83ee7d9a5b", - "rawBytes": 1033593, - "gzipBytes": 290230, + "path": "houseplan-assets/houseplan-card-qfwHeLJR.js", + "sha256": "2bd83b9350a1b4e702d28660cdc3b38a5daa3da16f9de47ddd5a839417443cbc", + "rawBytes": 1033605, + "gzipBytes": 290242, "isEntry": false, "imports": [], "dynamicImports": [ - "houseplan-assets/de-MNYUkgT1.js", - "houseplan-assets/editor-Ih3Tgb49.js", - "houseplan-assets/fr-BB7LeC_Q.js", - "houseplan-assets/houseplan-editor-runtime-ipAn0IA6.js", - "houseplan-assets/houseplan-onboarding-runtime-DB29lThG.js" + "houseplan-assets/de-Dc1N8boU.js", + "houseplan-assets/editor-DWFSXWYH.js", + "houseplan-assets/fr-Cw28JMxl.js", + "houseplan-assets/houseplan-editor-runtime-BQ2FUiZP.js", + "houseplan-assets/houseplan-onboarding-runtime-OhzyGbC7.js" ] }, { - "path": "houseplan-assets/houseplan-editor-runtime-ipAn0IA6.js", - "sha256": "924d285e33b3254880331d8f944030705b1ba87f920fe4ba1f50f1d60a98207c", - "rawBytes": 572891, - "gzipBytes": 149258, + "path": "houseplan-assets/houseplan-editor-runtime-BQ2FUiZP.js", + "sha256": "504fe0626f810cd221ba57a8b9807e0d4250da17545cd8ba0609d44bef967c88", + "rawBytes": 572919, + "gzipBytes": 149282, "isEntry": false, "imports": [ - "houseplan-assets/backdrop-pick-CsM7GqVe.js", - "houseplan-assets/houseplan-card-ruQDQ4Jw.js" + "houseplan-assets/backdrop-pick-Di7bhm6e.js", + "houseplan-assets/houseplan-card-qfwHeLJR.js" ], "dynamicImports": [] }, { - "path": "houseplan-assets/houseplan-onboarding-runtime-DB29lThG.js", - "sha256": "c59a3e4ee8c2b799568e3ed82b1f35526bf72b9e75e0f9769d033cb67aa7b716", + "path": "houseplan-assets/houseplan-onboarding-runtime-OhzyGbC7.js", + "sha256": "54105c7da5f96f26c7ee199726687ef046058bcb58f3ef27a2ff7b51e73c7415", "rawBytes": 28088, - "gzipBytes": 6956, + "gzipBytes": 6955, "isEntry": false, "imports": [ - "houseplan-assets/backdrop-pick-CsM7GqVe.js", - "houseplan-assets/houseplan-card-ruQDQ4Jw.js" + "houseplan-assets/backdrop-pick-Di7bhm6e.js", + "houseplan-assets/houseplan-card-qfwHeLJR.js" ], "dynamicImports": [] }, { "path": "houseplan-card.js", - "sha256": "f6e82e778343cf243de470eeafd5671d9d7746a730d385249fee0ca56d0a3b56", + "sha256": "ea553cfdc707a127f7098a3302bf8a6037ba8378b6afa8d8e3f0ea9601f19e41", "rawBytes": 1183, - "gzipBytes": 796, + "gzipBytes": 799, "isEntry": true, "imports": [ - "houseplan-assets/houseplan-card-ruQDQ4Jw.js" + "houseplan-assets/houseplan-card-qfwHeLJR.js" ], "dynamicImports": [] } diff --git a/custom_components/houseplan/frontend/houseplan-assets/backdrop-pick-CsM7GqVe.js b/custom_components/houseplan/frontend/houseplan-assets/backdrop-pick-Di7bhm6e.js similarity index 94% rename from custom_components/houseplan/frontend/houseplan-assets/backdrop-pick-CsM7GqVe.js rename to custom_components/houseplan/frontend/houseplan-assets/backdrop-pick-Di7bhm6e.js index d61d7308..1baad548 100644 --- a/custom_components/houseplan/frontend/houseplan-assets/backdrop-pick-CsM7GqVe.js +++ b/custom_components/houseplan/frontend/houseplan-assets/backdrop-pick-Di7bhm6e.js @@ -1,6 +1,6 @@ -globalThis.__HOUSEPLAN_BUILD_FINGERPRINT__="91d90ad5fe8f4f44a45669312cea92fc5c5c061316d8382ea8ec58dbef1603d8";import{dd as e,c as t,A as i,a as o,b as n,de as s}from"./houseplan-card-ruQDQ4Jw.js";function r(e){const t=e.visualViewport;return t&&Number.isFinite(t.width)&&Number.isFinite(t.height)?{left:Number(t.offsetLeft)||0,top:Number(t.offsetTop)||0,width:Math.max(0,Number(t.width)||0),height:Math.max(0,Number(t.height)||0)}:{left:0,top:0,width:e.innerWidth,height:e.innerHeight}}function a(e,t,i,o=7,n=8){const s=i.left+n,r=i.top+n,a=Math.max(s,i.left+i.width-n),l=Math.max(r,i.top+i.height-n),h=Math.max(0,a-s),d=Math.max(0,l-r),c=Math.min(Math.max(0,t.width),h),u=Math.min(Math.max(0,t.height),d);let p=e.left;p+c>a&&(p=e.right-c),p=Math.min(Math.max(s,p),Math.max(s,a-c));const _=e.bottom+o,g=e.top-o-u;let m="bottom",f=_;return _+u>l&&g>=r?(m="top",f=Math.min(Math.max(r,g),Math.max(r,l-u))):f=Math.min(Math.max(r,_),Math.max(r,l-u)),{left:Math.round(p),top:Math.round(f),side:m,maxWidth:Math.round(h),maxHeight:Math.round(d)}}class l{constructor(e,t,i){this.owner=e,this.name=t,this.keydown=i,this.host=null,this.root=null}window(){return this.owner.ownerDocument.defaultView}dialog(){return this.owner.closest("hp-dialog")}usesPopover(e=!1){const t=this.window();return!e&&!!t&&function(e){const t=e.HTMLElement?.prototype;return"function"==typeof t?.showPopover&&"function"==typeof t?.hidePopover}(t)}get hasFallback(){return!!this.host}containsPath(e){return e.includes(this.owner)||!!this.host&&e.includes(this.host)}ownsActiveElement(){let e=this.owner.ownerDocument.activeElement;for(;e?.shadowRoot?.activeElement;)e=e.shadowRoot.activeElement;return!(!e||e!==this.owner&&!this.owner.shadowRoot?.contains(e)&&!this.root?.contains(e))}renderFallback(i,o){const n=this.ensureFallback();return n?(e(t`${i}`,n),n):null}surface(e,t){return t?this.owner.shadowRoot?.querySelector(e)||null:this.root?.querySelector(e)||null}destroy(){this.root&&e(i,this.root),this.host?.remove(),this.host=null,this.root=null}ensureFallback(){if(this.root?.isConnected)return this.root;const e=this.dialog()?.overlayPortal()||this.owner.ownerDocument.body;if(!e)return null;const t=this.owner.ownerDocument.createElement("div");return t.dataset.hpOverlay=this.name,t.style.display="contents",this.keydown&&t.addEventListener("keydown",this.keydown,!0),e.append(t),this.host=t,this.root=t.attachShadow({mode:"open"}),this.root}}let h=0;class d extends n{constructor(){super(...arguments),this.text="",this.ariaLabel="",this.pointerHover=!1,this._open=!1,this._openTimer=0,this._closeTimer=0,this._positionRaf=0,this._forceFallback=!1,this._hoverOwned=!1,this._overlayDispose=null,this._scrollDialog=null,this._descriptionId="hp-help-description-"+ ++h,this._surfacePointerEnter=()=>{const e=this._window();this._closeTimer&&e?.clearTimeout(this._closeTimer),this._closeTimer=0},this._surfacePointerLeave=()=>{this._scheduleClose()},this._outsidePointerDown=e=>{if(!this._open)return;const t=e.composedPath();this._floating.containsPath(t)||this._closeHelp(!1,"outside")},this._dialogScroll=e=>{const t=this._floating.containsPath(e.composedPath()),i=e.target,o=this._dialog(),n=i instanceof Node&&(i===o||i instanceof Element&&i.contains(this));var s;this._open&&(s=n,!t&&s)&&this._closeHelp(!1,"scroll")},this._keyDown=e=>{if(!this._open||e.defaultPrevented)return;if("Escape"===e.key)return e.preventDefault(),e.stopImmediatePropagation(),void this._closeHelp(this._floating.ownsActiveElement(),"escape");const t=this.renderRoot.querySelector(".trigger");if(!t||!e.composedPath().includes(t))return;const i=this._surface();if(!i||i.scrollHeight<=i.clientHeight)return;const o=Math.max(40,.8*i.clientHeight),n={ArrowDown:40,ArrowUp:-40,PageDown:o,PageUp:-o};if("Home"===e.key)i.scrollTop=0;else if("End"===e.key)i.scrollTop=i.scrollHeight;else{if(!(e.key in n))return;i.scrollBy({top:n[e.key],behavior:"auto"})}e.preventDefault(),e.stopImmediatePropagation()},this._floating=new l(this,"help",this._keyDown),this._queuePosition=()=>{if(!this._open||this._positionRaf)return;const e=this._window();e&&(this._positionRaf=e.requestAnimationFrame(()=>{this._positionRaf=0,this._position()||this._closeHelp()}))}}connectedCallback(){super.connectedCallback(),this.addEventListener("keydown",this._keyDown,!0)}willUpdate(){this.toggleAttribute("data-has-content",this._hasContent())}disconnectedCallback(){const e=this.ownerDocument.defaultView;this._unsubscribeOpenListeners(),this.removeEventListener("keydown",this._keyDown,!0),this._clearTimers(),this._positionRaf&&e?.cancelAnimationFrame(this._positionRaf),this._positionRaf=0,this._closeHelp(!1,"disconnect"),super.disconnectedCallback()}updated(e){if(e.has("pointerHover")&&!this.pointerHover){const e=this._window();this._openTimer&&e?.clearTimeout(this._openTimer),this._openTimer=0,this._hoverOwned&&this._closeHelp(!1,"outside")}this._hasContent()||!this._open?this._open&&e.has("text")&&(this._usesPopover()||this._renderFallback(),this._queuePosition()):this._closeHelp()}_window(){return this._floating.window()}_dialog(){return this._floating.dialog()}_usesPopover(){return this._floating.usesPopover(this._forceFallback)}_hasContent(){return e=this.text,t=this.ariaLabel,"string"==typeof e&&e.trim().length>0&&"string"==typeof t&&t.trim().length>0;var e,t}_clearTimers(){const e=this._window();this._openTimer&&e?.clearTimeout(this._openTimer),this._closeTimer&&e?.clearTimeout(this._closeTimer),this._openTimer=0,this._closeTimer=0}_scheduleOpen(){const e=this._window();this.pointerHover&&e&&!this._open&&!this._openTimer&&(this._closeTimer&&e.clearTimeout(this._closeTimer),this._closeTimer=0,this._openTimer=e.setTimeout(()=>{this._openTimer=0,this.isConnected&&(this._hoverOwned=!0,this._openHelp())},300))}_scheduleClose(){const e=this._window(),t=this.renderRoot.querySelector(".trigger");e&&!t?.matches(":focus-visible")&&(this._openTimer&&e.clearTimeout(this._openTimer),this._closeTimer&&e.clearTimeout(this._closeTimer),this._openTimer=0,this._closeTimer=e.setTimeout(()=>{this._closeTimer=0,this.isConnected&&this._closeHelp()},150))}_triggerPointerEnter(e){this.pointerHover&&"mouse"===e.pointerType&&this._scheduleOpen()}_triggerPointerLeave(e){"mouse"===e.pointerType&&this._scheduleClose()}_triggerFocus(){queueMicrotask(()=>{const e=this.renderRoot.querySelector(".trigger");e?.matches(":focus-visible")&&(this._hoverOwned=!1,this._openHelp())})}_triggerBlur(){this._scheduleClose()}_triggerClick(){this._open?this._closeHelp():(this._hoverOwned=!1,this._openHelp())}_subscribeOpenListeners(){this.ownerDocument.addEventListener("pointerdown",this._outsidePointerDown,!0),this.ownerDocument.addEventListener("keydown",this._keyDown,!0);const e=this._window();e?.addEventListener("resize",this._queuePosition),e?.addEventListener("orientationchange",this._queuePosition),e?.visualViewport?.addEventListener("resize",this._queuePosition),e?.visualViewport?.addEventListener("scroll",this._queuePosition),this._scrollDialog=this._dialog(),this._scrollDialog?.addEventListener("scroll",this._dialogScroll,!0)}_unsubscribeOpenListeners(){this.ownerDocument.removeEventListener("pointerdown",this._outsidePointerDown,!0),this.ownerDocument.removeEventListener("keydown",this._keyDown,!0);const e=this._window();e?.removeEventListener("resize",this._queuePosition),e?.removeEventListener("orientationchange",this._queuePosition),e?.visualViewport?.removeEventListener("resize",this._queuePosition),e?.visualViewport?.removeEventListener("scroll",this._queuePosition),this._scrollDialog?.removeEventListener("scroll",this._dialogScroll,!0),this._scrollDialog=null}async _openHelp(){!this._open&&this._hasContent()&&(this._clearTimers(),this._forceFallback=!1,this._open=!0,this._subscribeOpenListeners(),await this.updateComplete,this._open&&(this._usesPopover()||this._renderFallback(),this._position()?this._overlayDispose=this._dialog()?.registerOverlay({owner:this,group:"transient",close:e=>this._closeHelp("escape"===e&&this._floating.ownsActiveElement(),e)})||null:this._closeHelp()))}_closeHelp(e=!1,t="exclusive"){if(!this._open&&!this._floating.hasFallback)return;this._clearTimers();const i=this._overlayDispose;this._overlayDispose=null,i?.();const o=this.renderRoot.querySelector(".tooltip");if(o?.hidePopover)try{o.matches(":popover-open")&&o.hidePopover()}catch{}this._open=!1,this._hoverOwned=!1,this._unsubscribeOpenListeners(),this._floating.destroy(),e&&this.updateComplete.then(()=>this.renderRoot.querySelector(".trigger")?.focus())}_tooltipTemplate(e){return t`