From 88f5905c29fcfb9c26375f0f3ba9e44f2d7defbf Mon Sep 17 00:00:00 2001 From: Hanif Koh Date: Mon, 28 Sep 2026 15:23:31 +0800 Subject: [PATCH] Tighten Project Page Description Rendering and Keep More Formatting Link and image URLs in descriptions must now start with an http or https scheme as written and parse as such with the URL parser. Preview images are built as DOM nodes like the file list, and accessory names show their full text as a tooltip. Descriptions keep more plain formatting: del, ins, figure, figcaption, dl, dt, dd, caption, q, abbr, kbd and wbr, plus alt, title, width and height on images, colspan and rowspan on table cells and start on ordered lists. Numeric attributes must be plain integers. Embedded YouTube players become a link to the video. --- resources/web/model/model.js | 61 +++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 11 deletions(-) diff --git a/resources/web/model/model.js b/resources/web/model/model.js index a6918d15d2..0773f27204 100644 --- a/resources/web/model/model.js +++ b/resources/web/model/model.js @@ -281,16 +281,15 @@ function ShowModelInfo( pModel ) if(TotalPreview>0) { - let htmlPreview=''; + $('#ModelPreviewList').empty(); for(let pn=0;pn'; + $('#ModelPreviewList').append( $('
').append( $('').attr('src',FTmpPath) ) ); } - $('#ModelPreviewList').html(htmlPreview); $('#Model_Preview_Image').viewer({ title: false, fullsreen: false, @@ -462,7 +461,7 @@ function ConstructFileHtml( ID, pItem ) let pFileItem=$('
'); pFileItem.append( $('
').addClass(strClass).append(pIconImg) ); - pFileItem.append( $('
').append( $('
').text(tName) ) ); + pFileItem.append( $('
').append( $('
').text(tName).attr('title',tName) ) ); pFileItem.append( pMenu ); pBoard.append( pFileItem ); } @@ -474,15 +473,39 @@ function ConstructFileHtml( ID, pItem ) // Descriptions are untrusted 3MF metadata that may carry rich-text HTML (e.g. from MakerWorld). // Rebuild them from an inert parse, keeping only plain formatting tags and http(s) links and images. -var DescAllowedTags=['P','BR','B','STRONG','I','EM','U','S','STRIKE','SUB','SUP','SMALL','MARK', - 'H1','H2','H3','H4','H5','H6','UL','OL','LI','BLOCKQUOTE','PRE','CODE','HR','SPAN','DIV', - 'TABLE','THEAD','TBODY','TFOOT','TR','TH','TD','A','IMG']; +var DescAllowedTags=['P','BR','B','STRONG','I','EM','U','S','STRIKE','DEL','INS','SUB','SUP','SMALL','MARK', + 'Q','ABBR','KBD','WBR','H1','H2','H3','H4','H5','H6','UL','OL','LI','DL','DT','DD','BLOCKQUOTE','PRE','CODE', + 'HR','SPAN','DIV','FIGURE','FIGCAPTION','TABLE','CAPTION','THEAD','TBODY','TFOOT','TR','TH','TD','A','IMG']; +// Plain attributes kept per tag; the numeric ones must be plain non-negative integers. +var DescAllowedAttrs={'IMG':['alt','title','width','height'],'TD':['colspan','rowspan'],'TH':['colspan','rowspan'],'OL':['start']}; +var DescNumericAttrs=['width','height','colspan','rowspan','start']; // Dropped together with their content; any other unknown tag is unwrapped to its children. var DescDroppedTags=['SCRIPT','STYLE','TEMPLATE','NOSCRIPT','TEXTAREA','TITLE','IFRAME','FRAME','OBJECT','EMBED','SVG','MATH']; function IsHttpUrl( strUrl ) { - return /^\s*https?:\/\//i.test(strUrl||''); + // The scheme must be written as is, so nothing the URL parser would strip can precede or split it. + if( typeof strUrl!='string' || !/^https?:/i.test(strUrl) ) + return false; + try + { + let sProtocol=new URL(strUrl).protocol; + return sProtocol=='http:' || sProtocol=='https:'; + } + catch(e) + { + return false; + } +} + +// Embedded YouTube players become a plain link to the video. +function GetYouTubeEmbedUrl( pNode ) +{ + let sSrc=pNode.getAttribute('src'); + if( !IsHttpUrl(sSrc) ) + return null; + let pUrl=new URL(sSrc); + return ( pUrl.origin=='https://www.youtube.com' && pUrl.pathname.indexOf('/embed/')==0 ) ? pUrl.href : null; } function CopyDescNodes( pSrc, pDst ) @@ -498,6 +521,18 @@ function CopyDescNodes( pSrc, pDst ) continue; let sTag=pNode.nodeName.toUpperCase(); + if( sTag=='IFRAME' ) + { + let sVideoUrl=GetYouTubeEmbedUrl(pNode); + if( sVideoUrl!=null ) + { + let pLink=document.createElement('A'); + pLink.setAttribute('href',sVideoUrl); + pLink.textContent=sVideoUrl; + pDst.appendChild(pLink); + } + continue; + } if( $.inArray(sTag,DescDroppedTags)>=0 ) continue; if( $.inArray(sTag,DescAllowedTags)<0 ) @@ -515,6 +550,11 @@ function CopyDescNodes( pSrc, pDst ) continue; pElem.setAttribute('src',pNode.getAttribute('src')); } + $.each( DescAllowedAttrs[sTag]||[], function(i,sAttr){ + let sValue=pNode.getAttribute(sAttr); + if( sValue!=null && ( $.inArray(sAttr,DescNumericAttrs)<0 || /^\d+$/.test(sValue) ) ) + pElem.setAttribute(sAttr,sValue); + }); CopyDescNodes(pNode,pElem); pDst.appendChild(pElem); } @@ -553,15 +593,14 @@ function ShowProfilelInfo( pProfile ) if(TotalPreview>0) { - let htmlPreview=''; + $('#ProfilePreviewList').empty(); for(let pn=0;pn'; + $('#ProfilePreviewList').append( $('
').append( $('').attr('src',FTmpPath) ) ); } - $('#ProfilePreviewList').html(htmlPreview); $('#Profile_Preview_Image').viewer({ title: false, fullsreen: false,