From d122f44c0d2e87d4edd90eb47ee76c0f3f6c2c3a Mon Sep 17 00:00:00 2001 From: Christoph Schaefer Date: Mon, 22 Jun 2026 16:32:15 +0200 Subject: [PATCH 1/2] fix(files): don't expose WebDAV XML-attribute artifacts as DOM attributes genFileInfo() flattens every DAV property and runs camelcase() on each key. Since Nextcloud 33, a file's nc:system-tags property contains elements that carry XML attributes (can-assign, id, user-visible, ...). The WebDAV parser represents those attributes with a leading "@", and camelcase() preserves it, so genFileInfo produced keys such as "@canAssign". When the resulting object is bound via v-bind in Viewer.vue, Vue calls setAttribute("@canAssign", ...), which throws "InvalidCharacterError: Invalid qualified name" on Firefox and Safari (Chrome silently ignores it). The result is that tagged office files cannot be opened in those browsers. Skip the structured system-tags subtree (it is not scalar file metadata) and, as a defensive backstop, drop any camelCased key that still starts with "@", so XML-attribute artifacts never reach the DOM. Ref: https://github.com/nextcloud/richdocuments/issues/5490 Assisted-by: ClaudeCode:Opus-4.8 Signed-off-by: Christoph Schaefer --- src/utils/fileUtils.ts | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/utils/fileUtils.ts b/src/utils/fileUtils.ts index 9d6653daf..c1026825f 100644 --- a/src/utils/fileUtils.ts +++ b/src/utils/fileUtils.ts @@ -133,23 +133,41 @@ export function genFileInfo(obj: FileStat): FileInfo { Object.keys(obj).forEach(key => { const data = obj[key] + // Skip structured DAV sub-trees that are not scalar file metadata + // (e.g. nc:system-tags). Flattening them would camelCase the parsed + // XML-attribute keys (prefixed with "@" by the WebDAV parser) into + // invalid attribute names like "@canAssign", which crash v-bind / + // setAttribute on Firefox and Safari. See richdocuments#5490. + if (key === 'system-tags') { + return + } + + const ccKey = camelcase(key) + + // Never expose XML-attribute artifacts: the WebDAV parser prefixes + // element attributes with "@", which camelcase preserves, yielding + // invalid DOM qualified names that throw in setAttribute. + if (ccKey.startsWith('@')) { + return + } + // flatten object if any if (!!data && typeof data === 'object' && !Array.isArray(data)) { Object.assign(fileInfo, genFileInfo(data)) } else { // format key and add it to the fileInfo if (data === 'false') { - fileInfo[camelcase(key)] = false + fileInfo[ccKey] = false } else if (data === 'true') { - fileInfo[camelcase(key)] = true + fileInfo[ccKey] = true } else { // preserve string typed properties as string (FileStat interface in webdav) const stringTypedProperties = ['filename', 'basename', 'owner-id'] if (stringTypedProperties.includes(key)) { - fileInfo[camelcase(key)] = String(data) + fileInfo[ccKey] = String(data) return } - fileInfo[camelcase(key)] = isNumber(data) + fileInfo[ccKey] = isNumber(data) ? Number(data) : data } From 907f7dd82cc988eb2f96b7a623850e56af940d24 Mon Sep 17 00:00:00 2001 From: skjnldsv Date: Mon, 20 Jul 2026 17:12:59 +0200 Subject: [PATCH 2/2] chore: compile assets Signed-off-by: skjnldsv --- ....mjs => NcActionButton-Du6_W7I7.chunk.mjs} | 4 +- ...NcActionButton-Du6_W7I7.chunk.mjs.license} | 0 ... => NcActionButton-Du6_W7I7.chunk.mjs.map} | 2 +- ...tionButton-Du6_W7I7.chunk.mjs.map.license} | 0 ...conSvgWrapper-Bui9PhAS-W2Dfg5UF.chunk.mjs} | 4 +- ...apper-Bui9PhAS-W2Dfg5UF.chunk.mjs.license} | 0 ...vgWrapper-Bui9PhAS-W2Dfg5UF.chunk.mjs.map} | 2 +- ...r-Bui9PhAS-W2Dfg5UF.chunk.mjs.map.license} | 0 ...9.chunk.mjs => NcModal-BFefwFFg.chunk.mjs} | 4 +- ...nse => NcModal-BFefwFFg.chunk.mjs.license} | 0 ...mjs.map => NcModal-BFefwFFg.chunk.mjs.map} | 2 +- ...=> NcModal-BFefwFFg.chunk.mjs.map.license} | 0 ...8Cq.chunk.mjs => index-Dywoknmd.chunk.mjs} | 4 +- ...cense => index-Dywoknmd.chunk.mjs.license} | 0 ...k.mjs.map => index-Dywoknmd.chunk.mjs.map} | 2 +- ...e => index-Dywoknmd.chunk.mjs.map.license} | 0 ...nk.mjs => previewUtils-BwCqt6br.chunk.mjs} | 52 +++++++++---------- ...> previewUtils-BwCqt6br.chunk.mjs.license} | 0 ...ap => previewUtils-BwCqt6br.chunk.mjs.map} | 2 +- ...eviewUtils-BwCqt6br.chunk.mjs.map.license} | 0 js/viewer-init.mjs | 6 +-- js/viewer-main.mjs | 6 +-- 22 files changed, 45 insertions(+), 45 deletions(-) rename js/{NcActionButton-DCT2v0Xh.chunk.mjs => NcActionButton-Du6_W7I7.chunk.mjs} (92%) rename js/{NcActionButton-DCT2v0Xh.chunk.mjs.license => NcActionButton-Du6_W7I7.chunk.mjs.license} (100%) rename js/{NcActionButton-DCT2v0Xh.chunk.mjs.map => NcActionButton-Du6_W7I7.chunk.mjs.map} (99%) rename js/{NcActionButton-DCT2v0Xh.chunk.mjs.map.license => NcActionButton-Du6_W7I7.chunk.mjs.map.license} (100%) rename js/{NcIconSvgWrapper-Bui9PhAS-CLt_vPUg.chunk.mjs => NcIconSvgWrapper-Bui9PhAS-W2Dfg5UF.chunk.mjs} (94%) rename js/{NcIconSvgWrapper-Bui9PhAS-CLt_vPUg.chunk.mjs.license => NcIconSvgWrapper-Bui9PhAS-W2Dfg5UF.chunk.mjs.license} (100%) rename js/{NcIconSvgWrapper-Bui9PhAS-CLt_vPUg.chunk.mjs.map => NcIconSvgWrapper-Bui9PhAS-W2Dfg5UF.chunk.mjs.map} (99%) rename js/{NcIconSvgWrapper-Bui9PhAS-CLt_vPUg.chunk.mjs.map.license => NcIconSvgWrapper-Bui9PhAS-W2Dfg5UF.chunk.mjs.map.license} (100%) rename js/{NcModal-Bcu1kK89.chunk.mjs => NcModal-BFefwFFg.chunk.mjs} (99%) rename js/{NcModal-Bcu1kK89.chunk.mjs.license => NcModal-BFefwFFg.chunk.mjs.license} (100%) rename js/{NcModal-Bcu1kK89.chunk.mjs.map => NcModal-BFefwFFg.chunk.mjs.map} (99%) rename js/{NcModal-Bcu1kK89.chunk.mjs.map.license => NcModal-BFefwFFg.chunk.mjs.map.license} (100%) rename js/{index-WMT0v8Cq.chunk.mjs => index-Dywoknmd.chunk.mjs} (99%) rename js/{index-WMT0v8Cq.chunk.mjs.license => index-Dywoknmd.chunk.mjs.license} (100%) rename js/{index-WMT0v8Cq.chunk.mjs.map => index-Dywoknmd.chunk.mjs.map} (99%) rename js/{index-WMT0v8Cq.chunk.mjs.map.license => index-Dywoknmd.chunk.mjs.map.license} (100%) rename js/{previewUtils-Cipmg400.chunk.mjs => previewUtils-BwCqt6br.chunk.mjs} (78%) rename js/{previewUtils-Cipmg400.chunk.mjs.license => previewUtils-BwCqt6br.chunk.mjs.license} (100%) rename js/{previewUtils-Cipmg400.chunk.mjs.map => previewUtils-BwCqt6br.chunk.mjs.map} (77%) rename js/{previewUtils-Cipmg400.chunk.mjs.map.license => previewUtils-BwCqt6br.chunk.mjs.map.license} (100%) diff --git a/js/NcActionButton-DCT2v0Xh.chunk.mjs b/js/NcActionButton-Du6_W7I7.chunk.mjs similarity index 92% rename from js/NcActionButton-DCT2v0Xh.chunk.mjs rename to js/NcActionButton-Du6_W7I7.chunk.mjs index e790035a6..47e25476e 100644 --- a/js/NcActionButton-DCT2v0Xh.chunk.mjs +++ b/js/NcActionButton-Du6_W7I7.chunk.mjs @@ -1,2 +1,2 @@ -import{N as i,m as s,b as a}from"./NcIconSvgWrapper-Bui9PhAS-CLt_vPUg.chunk.mjs";import{A as n}from"./actionText-BMig9Egt-DrBqWVOB.chunk.mjs";import{n as o}from"./_plugin-vue2_normalizer-DU4iP6Vu-CHYf0Z4t.chunk.mjs";import"./previewUtils-Cipmg400.chunk.mjs";const l={name:"NcActionButton",components:{NcIconSvgWrapper:i},mixins:[n],inject:{isInSemanticMenu:{from:"NcActions:isSemanticMenu",default:!1}},props:{ariaHidden:{type:Boolean,default:null},disabled:{type:Boolean,default:!1},isMenu:{type:Boolean,default:!1},type:{type:String,default:"button",validator:t=>["button","checkbox","radio","reset","submit"].includes(t)},modelValue:{type:[Boolean,String],default:null},value:{type:String,default:null},description:{type:String,default:""}},setup(){return{mdiCheck:a,mdiChevronRight:s}},computed:{isFocusable(){return!this.disabled},isChecked(){return this.type==="radio"&&typeof this.modelValue!="boolean"?this.modelValue===this.value:this.modelValue},nativeType(){return this.type==="submit"||this.type==="reset"?this.type:"button"},buttonAttributes(){const t={};return this.isInSemanticMenu?(t.role="menuitem",this.type==="radio"?(t.role="menuitemradio",t["aria-checked"]=this.isChecked?"true":"false"):(this.type==="checkbox"||this.nativeType==="button"&&this.modelValue!==null)&&(t.role="menuitemcheckbox",t["aria-checked"]=this.modelValue===null?"mixed":this.modelValue?"true":"false")):this.modelValue!==null&&this.nativeType==="button"&&(t["aria-pressed"]=this.modelValue?"true":"false"),t}},methods:{handleClick(t){this.onClick(t),(this.modelValue!==null||this.type!=="button")&&(this.type==="radio"?typeof this.modelValue!="boolean"?this.isChecked||this.$emit("update:modelValue",this.value):this.$emit("update:modelValue",!this.isChecked):this.$emit("update:modelValue",!this.isChecked))}}};var c=function(){var t=this,e=t._self._c;return e("li",{staticClass:"action",class:{"action--disabled":t.disabled},attrs:{role:t.isInSemanticMenu&&"presentation"}},[e("button",t._b({staticClass:"action-button button-vue",class:{"action-button--active":t.isChecked,focusable:t.isFocusable},attrs:{"aria-label":t.ariaLabel,disabled:t.disabled,title:t.title,type:t.nativeType},on:{click:t.handleClick}},"button",t.buttonAttributes,!1),[t._t("icon",function(){return[e("span",{staticClass:"action-button__icon",class:[t.isIconUrl?"action-button__icon--url":t.icon],style:{backgroundImage:t.isIconUrl?`url(${t.icon})`:null},attrs:{"aria-hidden":"true"}})]}),e("span",{staticClass:"action-button__longtext-wrapper"},[t.name?e("strong",{staticClass:"action-button__name"},[t._v(" "+t._s(t.name)+" ")]):t._e(),t.isLongText?e("span",{staticClass:"action-button__longtext",domProps:{textContent:t._s(t.text)}}):e("span",{staticClass:"action-button__text"},[t._v(" "+t._s(t.text)+" ")]),t.description?e("span",{staticClass:"action-button__description",domProps:{textContent:t._s(t.description)}}):t._e()]),t.isMenu?e("NcIconSvgWrapper",{staticClass:"action-button__menu-icon",attrs:{directional:"",path:t.mdiChevronRight}}):t.isChecked?e("NcIconSvgWrapper",{staticClass:"action-button__pressed-icon",attrs:{path:t.mdiCheck}}):t.isChecked===!1?e("span",{staticClass:"action-button__pressed-icon material-design-icon"}):t._e(),t._e()],2)])},u=[],r=o(l,c,u,!1,null,"ab2ff78b");const b=r.exports;export{b as default}; -//# sourceMappingURL=NcActionButton-DCT2v0Xh.chunk.mjs.map +import{N as i,m as s,b as a}from"./NcIconSvgWrapper-Bui9PhAS-W2Dfg5UF.chunk.mjs";import{A as n}from"./actionText-BMig9Egt-DrBqWVOB.chunk.mjs";import{n as o}from"./_plugin-vue2_normalizer-DU4iP6Vu-CHYf0Z4t.chunk.mjs";import"./previewUtils-BwCqt6br.chunk.mjs";const l={name:"NcActionButton",components:{NcIconSvgWrapper:i},mixins:[n],inject:{isInSemanticMenu:{from:"NcActions:isSemanticMenu",default:!1}},props:{ariaHidden:{type:Boolean,default:null},disabled:{type:Boolean,default:!1},isMenu:{type:Boolean,default:!1},type:{type:String,default:"button",validator:t=>["button","checkbox","radio","reset","submit"].includes(t)},modelValue:{type:[Boolean,String],default:null},value:{type:String,default:null},description:{type:String,default:""}},setup(){return{mdiCheck:a,mdiChevronRight:s}},computed:{isFocusable(){return!this.disabled},isChecked(){return this.type==="radio"&&typeof this.modelValue!="boolean"?this.modelValue===this.value:this.modelValue},nativeType(){return this.type==="submit"||this.type==="reset"?this.type:"button"},buttonAttributes(){const t={};return this.isInSemanticMenu?(t.role="menuitem",this.type==="radio"?(t.role="menuitemradio",t["aria-checked"]=this.isChecked?"true":"false"):(this.type==="checkbox"||this.nativeType==="button"&&this.modelValue!==null)&&(t.role="menuitemcheckbox",t["aria-checked"]=this.modelValue===null?"mixed":this.modelValue?"true":"false")):this.modelValue!==null&&this.nativeType==="button"&&(t["aria-pressed"]=this.modelValue?"true":"false"),t}},methods:{handleClick(t){this.onClick(t),(this.modelValue!==null||this.type!=="button")&&(this.type==="radio"?typeof this.modelValue!="boolean"?this.isChecked||this.$emit("update:modelValue",this.value):this.$emit("update:modelValue",!this.isChecked):this.$emit("update:modelValue",!this.isChecked))}}};var c=function(){var t=this,e=t._self._c;return e("li",{staticClass:"action",class:{"action--disabled":t.disabled},attrs:{role:t.isInSemanticMenu&&"presentation"}},[e("button",t._b({staticClass:"action-button button-vue",class:{"action-button--active":t.isChecked,focusable:t.isFocusable},attrs:{"aria-label":t.ariaLabel,disabled:t.disabled,title:t.title,type:t.nativeType},on:{click:t.handleClick}},"button",t.buttonAttributes,!1),[t._t("icon",function(){return[e("span",{staticClass:"action-button__icon",class:[t.isIconUrl?"action-button__icon--url":t.icon],style:{backgroundImage:t.isIconUrl?`url(${t.icon})`:null},attrs:{"aria-hidden":"true"}})]}),e("span",{staticClass:"action-button__longtext-wrapper"},[t.name?e("strong",{staticClass:"action-button__name"},[t._v(" "+t._s(t.name)+" ")]):t._e(),t.isLongText?e("span",{staticClass:"action-button__longtext",domProps:{textContent:t._s(t.text)}}):e("span",{staticClass:"action-button__text"},[t._v(" "+t._s(t.text)+" ")]),t.description?e("span",{staticClass:"action-button__description",domProps:{textContent:t._s(t.description)}}):t._e()]),t.isMenu?e("NcIconSvgWrapper",{staticClass:"action-button__menu-icon",attrs:{directional:"",path:t.mdiChevronRight}}):t.isChecked?e("NcIconSvgWrapper",{staticClass:"action-button__pressed-icon",attrs:{path:t.mdiCheck}}):t.isChecked===!1?e("span",{staticClass:"action-button__pressed-icon material-design-icon"}):t._e(),t._e()],2)])},u=[],r=o(l,c,u,!1,null,"ab2ff78b");const b=r.exports;export{b as default}; +//# sourceMappingURL=NcActionButton-Du6_W7I7.chunk.mjs.map diff --git a/js/NcActionButton-DCT2v0Xh.chunk.mjs.license b/js/NcActionButton-Du6_W7I7.chunk.mjs.license similarity index 100% rename from js/NcActionButton-DCT2v0Xh.chunk.mjs.license rename to js/NcActionButton-Du6_W7I7.chunk.mjs.license diff --git a/js/NcActionButton-DCT2v0Xh.chunk.mjs.map b/js/NcActionButton-Du6_W7I7.chunk.mjs.map similarity index 99% rename from js/NcActionButton-DCT2v0Xh.chunk.mjs.map rename to js/NcActionButton-Du6_W7I7.chunk.mjs.map index f508e447f..169353bbb 100644 --- a/js/NcActionButton-DCT2v0Xh.chunk.mjs.map +++ b/js/NcActionButton-Du6_W7I7.chunk.mjs.map @@ -1 +1 @@ -{"version":3,"file":"NcActionButton-DCT2v0Xh.chunk.mjs","sources":["../node_modules/@nextcloud/vue/dist/chunks/NcActionButton-D7uypboC.mjs"],"sourcesContent":["import '../assets/NcActionButton-CwGeOQFe.css';\nimport { m as mdiChevronRight, a as mdiCheck } from \"./mdi-DkJglNiS.mjs\";\nimport { N as NcIconSvgWrapper } from \"./NcIconSvgWrapper-Bui9PhAS.mjs\";\nimport { A as ActionTextMixin } from \"./actionText-BMig9Egt.mjs\";\nimport { n as normalizeComponent } from \"./_plugin-vue2_normalizer-DU4iP6Vu.mjs\";\nconst _sfc_main = {\n name: \"NcActionButton\",\n components: {\n NcIconSvgWrapper\n },\n mixins: [ActionTextMixin],\n inject: {\n isInSemanticMenu: {\n from: \"NcActions:isSemanticMenu\",\n default: false\n }\n },\n props: {\n /**\n * @deprecated To be removed in @nextcloud/vue 9. Migration guide: remove ariaHidden prop from NcAction* components.\n * @todo Add a check in @nextcloud/vue 9 that this prop is not provided,\n * otherwise root element will inherit incorrect aria-hidden.\n */\n ariaHidden: {\n type: Boolean,\n // eslint-disable-next-line vue/no-boolean-default\n default: null\n },\n /**\n * disabled state of the action button\n */\n disabled: {\n type: Boolean,\n default: false\n },\n /**\n * If this is a menu, a chevron icon will\n * be added at the end of the line\n */\n isMenu: {\n type: Boolean,\n default: false\n },\n /**\n * The button's behavior, by default the button acts like a normal button with optional toggle button behavior if `modelValue` is `true` or `false`.\n * But you can also set to checkbox button behavior with tri-state or radio button like behavior.\n * This extends the native HTML button type attribute.\n */\n type: {\n type: String,\n default: \"button\",\n validator: (behavior) => [\"button\", \"checkbox\", \"radio\", \"reset\", \"submit\"].includes(behavior)\n },\n /**\n * The buttons state if `type` is 'checkbox' or 'radio' (meaning if it is pressed / selected).\n * For checkbox and toggle button behavior - boolean value.\n * For radio button behavior - could be a boolean checked or a string with the value of the button.\n * Note: Unlike native radio buttons, NcActionButton are not grouped by name, so you need to connect them by bind correct modelValue.\n *\n * **This is not availabe for `type='submit'` or `type='reset'`**\n *\n * If using `type='checkbox'` a `model-value` of `true` means checked, `false` means unchecked and `null` means indeterminate (tri-state)\n * For `type='radio'` `null` is equal to `false`\n */\n modelValue: {\n type: [Boolean, String],\n default: null\n },\n /**\n * The value used for the `modelValue` when this component is used with radio behavior\n * Similar to the `value` attribute of ``\n */\n value: {\n type: String,\n default: null\n },\n /**\n * Small underlying text content of the entry\n */\n description: {\n type: String,\n default: \"\"\n }\n },\n setup() {\n return {\n mdiCheck,\n mdiChevronRight\n };\n },\n computed: {\n /**\n * determines if the action is focusable\n *\n * @return {boolean} is the action focusable ?\n */\n isFocusable() {\n return !this.disabled;\n },\n /**\n * The current \"checked\" or \"pressed\" state for the model behavior\n */\n isChecked() {\n if (this.type === \"radio\" && typeof this.modelValue !== \"boolean\") {\n return this.modelValue === this.value;\n }\n return this.modelValue;\n },\n /**\n * The native HTML type to set on the button\n */\n nativeType() {\n if (this.type === \"submit\" || this.type === \"reset\") {\n return this.type;\n }\n return \"button\";\n },\n /**\n * HTML attributes to bind to the