domma-cms 1.10.20 → 1.10.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/public/js/form-files.mjs +1 -1
- package/server/routes/api/forms.js +3 -1
- package/server/routes/public.js +6 -2
- package/server/services/cache/index.js +4 -0
- package/server/services/content.js +15 -2
- package/server/services/docs.js +2 -1
- package/server/services/renderer.js +9 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.10.21] - 2026-10-25
|
|
4
|
+
|
|
5
|
+
- **A form in a footer block keeps cached pages up to date.** What a footer block draws (a `[form]`, its file cap) now tags every cached page that shows it, so saving the form or changing the cap reaches those pages at once instead of after the cache expires. Producers can add tags to a plain cached value with `{_cacheValue, _cacheTags}`.
|
|
6
|
+
- **A visitor's file named just `json`, `md` or the like is taken** - it is stored with a prefix and served as a download, so refusing it was wrong; the page and the server agree.
|
|
7
|
+
- **A plugin's media listener that throws no longer fails an upload** (or leaves a visitor's file behind with an error): `content:mediaUploaded` / `content:mediaDeleted` listener errors are logged instead.
|
|
8
|
+
|
|
3
9
|
## [1.10.20] - 2026-10-25
|
|
4
10
|
|
|
5
11
|
- Forms support versioned JSON definition import and export from the list banner and item actions. Imports create a fresh private submissions collection, keep existing forms and collections, and assign a new slug on conflicts.
|
package/package.json
CHANGED
package/public/js/form-files.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const DEFAULT_MAX_SIZE=5242880,DEFAULT_MAX_FILES=5,MAX_FILES_LIMIT=20;export function fileRules(e){const t=e&&e.file||{},s=!!(e&&e.multiple),n=Math.floor(Number(t.maxFiles));return{accept:String(t.accept||"").split(",").map(l=>l.trim()).filter(Boolean),maxSize:Number(t.maxSize)||5242880,multiple:s,maxFiles:s?Math.min(n>=1?n:5,20):1}}function f(e){return e===1?"one file":`${e} files`}export function countRefusal(e,t){const{maxFiles:s,multiple:n}=fileRules(e);if(Number(t)<=s)return null;const l=e&&(e.label||e.name)||"File";return n?`"${l}" takes at most ${f(s)}.`:`"${l}" takes one file.`}export function fileRefs(e){return(Array.isArray(e)?e:[e]).filter(s=>s&&typeof s=="object"&&!Array.isArray(s)&&typeof s.url=="string"&&(typeof s.name=="string"||s.name===void 0))}export function isFileValue(e){const t=fileRefs(e);return t.length>0&&t.length===(Array.isArray(e)?e.length:1)}export function fileValueText(e,{base:t="",urls:s=!0}={}){return fileRefs(e).map(n=>{const l=n.name||n.url.split("/").pop()||"file";return!s||!n.url?l:`${l} (${fileUrl(n,t)})`}).join("; ")}export function fileUrl(e,t=""){const s=String(e&&e.url||""),n=String(t||"").replace(/\/+$/,"");return n&&s.startsWith("/")?`${n}${s}`:s}export function mimeAccepted(e,t){if(!e||!e.length)return!0;const s=String(t||"");return e.some(n=>n.endsWith("/*")?s.startsWith(n.slice(0,-1)):n===s)}export function cleanUploadName(e){const t=s=>s.replace(/^[-.]+|[-.]+$/g,"");return t(t(String(e||"upload").toLowerCase().replace(/[^\w.-]+/g,"-").replace(/-+/g,"-")).slice(0,200))||"upload"}const z=/^(text\/html|text\/xml|text\/xsl|text\/javascript|text\/ecmascript|application\/(x-)?javascript|application\/ecmascript|application\/xml|application\/[\w.-]+\+xml|image\/svg\+xml)\b/i,y=/\.(html?|xht(ml)?|shtml?|svgz?|xml|xsd|xslt?|xul|rdf|owl|atom|rss|wsdl|smil?|kml|opf|xlf|gpx|ttml|mathml|mml|xop|m?js|mht(ml)?)$/i,w=new Set("123 1km 3dml 3mf aab aam aas abw ac acc ace acu acutc aep afm afp ahead ai air ait ami apk appcache application apr arc arj asc asm aso atc atom atomcat atomdeleted atomsvc atx aw azf azs azw bat bcpio bdf bdm bdoc bed bh2 blb blorb bmi bmml book box boz bsp bz bz2 c c11amc c11amz c4d c4f c4g c4p c4u cap car cb7 cba cbr cbt cbz cc cco cct ccxml cdbcmsg cdf cdfx cdkey cdmia cdmic cdmid cdmio cdmiq cdx cdxml cdy cer cfs chat chrt cif cii cjs cla class clkk clkp clkt clkw clkx clp cmc cmdf cml cmp cod coffee com cpio cpp cpt crd crl crt crx cryptonote csh csl csml csp css cst cu curl cww cxt cxx dae daf dart dataless davmount dbf dbk dcr dcurl dd2 ddd ddf der dfac dgc dic dir dis disposition-notification dmp dna dp dpg dsc dssc dtb dtd dvi dwd dwf dxp dxr ear ecma edm edx efif ei6 eml emma emotionml emz eps es es3 esa esf et3 etx eva evy exi exp ext ez ez2 ez3 f f77 f90 fcdt fcs fdf fdt fe_launch fg5 fgd fig flo flw flx fly fm fnc fo for frame fsc ftc fti fxp fxpl fzs g2w g3w gac gam gbr gca gdl gdoc geo geojson gex ggb ggt ghf gim glb gltf gml gmx gnumeric gph gpx gqf gqs gram gramps gre grv grxml gsf gsheet gslides gtar gtm gtw gv gxf gxt h hal hbci hbs hdd hdf held hh hjson hlp hpgl hpid hps hqx htc htke htm html hvd hvp hvs i2g icc ice icm ics ifb ifm iges igl igm igs igx iif imp ink inkml install iota ipfix ipk irm irp itp its ivp ivu jad jade jam jar jardiff java jisp jlt jnlp joda js json json5 jsonld jsonml jsx karbon kdbx key kfo kia kml kmz kne knp kon kpr kpt kpxx ksp ktr ktz kwd kwt lasxml latex lbd lbe les less lgr lha link66 list3820 listafp litcoffee lnk lostxml ltf lua luac lwp lzh m13 m14 m21 m3u8 m4p ma mads maei mag maker man manifest map markdown mathml mb mbk mbox mc1 mcd mcurl md mdb mdx me mesh meta4 metalink mets mfm mft mgp mgz mie mif mime mjs mkd mlp mmd mmf mml mny mobi mods mp21 mp4s mpc mpd mpkg mpm mpn mpy mqy mrc mrcx ms mscml mseed mseq msf msh msl msty mtl mts mus musd musicxml mvb mvt mwf mxf mxl mxml mxs n-gage n3 nb nbp nc ncx nfo ngdat nitf nlu nml nnd nns nnw nq nsc nsf nt ntf numbers nzb oa2 oa3 oas obd obgx obj oda ogex ogx omdoc onepkg onetmp onetoc onetoc2 opf opml oprc org osf osfpvg osm otf ova ovf owl oxps oxt p p10 p12 p7b p7c p7m p7r p7s p8 pac pages pas paw pbd pcap pcf pcl pclxl pcurl pdb pde pem pfa pfb pfm pfr pfx pgn pgp php pki pkipath pkpass pl plb plc plf pls pm pml portpkg ppd pqa prc pre prf provx ps psb psf pskcxml ptid pub pvb pwn qam qbo qfx qps qwd qwt qxb qxd qxl qxt raml rapd rcprofile rdf rdz relo rep res rif ris rl rld rm rms rmvb rnc rng roa roff rp9 rpm rpss rpst rq rs rsat rsd rsheet rss rtx run rusd s saf sass sbml sc scd scm scq scs scss scurl sda sdc sdd sdkd sdkm sdp sdw sea see seed sema semd semf senmlx sensmlx ser setpay setreg sfd-hdstx sfs sfv sgl sgm sgml sh shar shex shf shtml sieve sig silo sis sisx sit sitx siv skd skm skp skt slim slm sls slt sm smf smi smil smzip snf spc spdx spf spl spot spp spq sql src srt sru srx ssdl sse ssf ssml st stc std stf sti stk stl stpx stpxz stpz str stw styl stylus sub sus susp sv4cpio sv4crc svc svd svg svgz swa swf swi swidtag sxc sxd sxg sxi sxm sxw t t3 taglet tao tcap tcl td teacher tei teicorpus tex texi texinfo tfi tfm tk tmo toml torrent tpl tpt tr tra trig trm tsd tsv ttc ttf ttl ttml twd twds txd txf u32 u8dsn u8hdr u8mdn u8msg ubj udeb ufd ufdl ulx umj unityweb uoml uri uris urls usdz ustar utz uu uvd uvf uvt uvvd uvvf uvvt uvvx uvvz uvx uvz vbox vbox-extpack vcard vcd vcf vcg vcs vcx vdi vds vhd vis vmdk vor vox vrml vsd vsf vss vst vsw vtt vtu vxml w3d wad wadl war wasm wbs wbxml webapp webmanifest wg wgt wmd wml wmlc wmls wmlsc wmz woff woff2 wpd wqd wri wrl wsc wsdl wspolicy wtb x32 x3d x3db x3dbz x3dv x3dvz x3dz x_b x_t xaml xap xar xav xbap xbd xca xcs xdf xdm xdp xdssc xdw xel xenc xer xfdf xfdl xht xhtml xhvml xlf xml xns xo xop xpi xpl xpr xpw xpx xsd xsl xslt xsm xspf xul xvm xvml xyz xz yaml yang yin yml ymp z1 z2 z3 z4 z5 z6 z7 z8 zaz zir zirz zmm ".trim().split(" "));export function isRefusedKind(e){const t=cleanUploadName(e&&(e.filename??e.name)||"");return w.has(t.replace(/^.*\./,""))}function h(e){const t=/\.[^.]+$/.exec(cleanUploadName(e&&(e.filename??e.name)||""));return t?t[0]:""}export function isActiveFile(e){const t=String(e&&(e.mimetype??e.type)||""),s=e&&(e.filename??e.name)||"";return z.test(t)||y.test(cleanUploadName(s))}export function fileRefusal(e,t){const s=fileRules(e),n=e&&(e.label||e.name)||"File";if(Number(t&&t.size)>s.maxSize)return`"${n}" file is too large (max ${Math.round(s.maxSize/1024)} KB).`;const l=t&&(t.mimetype??t.type)||"";return isActiveFile(t)?`"${n}" file type "${l||h(t)}" not allowed (it could run as a page on this site).`:isRefusedKind(t)?`"${n}" file type "${h(t)||l}" cannot be uploaded here.`:s.accept.length&&!mimeAccepted(s.accept,l)?`"${n}" file type "${l}" not allowed (expected: ${String(e.file.accept).trim()}).`:null}const v=[[/wordprocessingml|^msword$/,"Word"],[/spreadsheetml|ms-excel/,"Excel"],[/presentationml|ms-powerpoint/,"PowerPoint"],[/^plain$/,"text"]];export function describeAccept(e){const t=[];return(e||[]).forEach(s=>{const[n,l]=String(s).split("/");let o;l==="*"?o={image:"images",audio:"audio",video:"video",text:"text files"}[n]||`${n} files`:l?o=v.find(([i])=>i.test(l))?.[1]||l.replace(/^(x-|vnd\.)/,"").split(/[.+]/)[0].toUpperCase():o=String(s),o&&!t.includes(o)&&t.push(o)}),t.length<2?t[0]||"":`${t.slice(0,-1).join(", ")} or ${t[t.length-1]}`}export function formatSize(e){const t=Number(e)||0;if(t<1024)return`${t} B`;const s=["KB","MB","GB"];let n=t,l=-1;do n/=1024,l++;while(n>=1024&&l<s.length-1);return`${n>=10?Math.round(n):Math.round(n*10)/10} ${s[l]}`}export function fileHint(e){const t=fileRules(e),s=describeAccept(t.accept),n=t.multiple?`up to ${f(t.maxFiles)}, ${formatSize(t.maxSize)} each`:`up to ${formatSize(t.maxSize)}`;return s?`${s[0].toUpperCase()}${s.slice(1)} - ${n}`:`Any file - ${n}`}export function dropzoneOptions(e){const t=fileRules(e);return{accept:t.accept.length?t.accept.join(","):null,maxSize:t.maxSize,multiple:t.multiple,maxFiles:t.multiple?t.maxFiles:null,labels:{prompt:t.multiple?"Drop files here, paste, or click to browse":"Drop a file here, paste, or click to browse",hint:fileHint(e),required:"Please add a file",type:refusalMessage(e,{name:"{name}"},"type"),size:refusalMessage(e,{name:"{name}"},"size"),count:refusalMessage(e,{name:"{name}"},"count")}}}export function fileBlueprint(e,t){return{...t,type:"dropzone",formConfig:{...t.formConfig||{},...dropzoneOptions(e)}}}export function refusalMessage(e,t,s,n){const l=fileRules(e),o=t&&t.name||"That file";if(s==="total")return`${o} was not added: the files together would be more than ${formatSize(n)}.`;if(s==="size")return`${o} was not added: it is larger than ${formatSize(l.maxSize)}.`;if(s==="type"){const i=describeAccept(l.accept);return`${o} was not added: ${i?`only ${i} can be uploaded here`:"that type of file is not accepted"}.`}return s==="active"?`${o} was not added: web pages, SVG images and scripts cannot be uploaded.`:s==="kind"?`${o} was not added: that kind of file cannot be uploaded here.`:`${o} was not added: only ${f(l.maxFiles)} can be added here.`}function k(e){return Array.isArray(e)&&e.length>0&&typeof File<"u"&&e.every(t=>t instanceof File)}export function splitFiles(e,t,s){const n={...e||{}},l=new Map,o=new Set((t||[]).filter(i=>i&&i.type==="file"&&i.name).map(i=>i.name));for(const[i,r]of Object.entries(n))k(r)?(l.set(i,r),delete n[i]):o.has(i)&&(Array.isArray(r)||r==null)&&(n[i]="");for(const[i,r]of s||[])l.set(i,r||[]);for(const[i,r]of l)r.length?delete n[i]:(l.delete(i),(o.has(i)||i in n)&&(n[i]=""));return{values:n,files:l}}export function submitBody(e,t){if(![...(t||new Map).values()].some(l=>l&&l.length))return{body:JSON.stringify(e),headers:{"Content-Type":"application/json"},multipart:!1};const n=new FormData;for(const[l,o]of Object.entries(e))o==null||t.has(l)||n.append(l,typeof o=="object"?JSON.stringify(o):String(o));for(const[l,o]of t)for(const i of o||[])n.append(l,i,i.name);return{body:n,headers:{},multipart:!0}}export function captureFileFields(e,t){!e||!t||e.querySelectorAll('input[type="file"]').forEach(s=>{s.name&&t.set(s.name,Array.from(s.files||[]))})}export function restoreFileFields(e,t){!e||!t||!t.size||e.querySelectorAll('input[type="file"]').forEach(s=>{const n=t.get(s.name);if(!n||!n.length||s.files&&s.files.length)return;const l=b(s);if(l){l.addFiles(n);return}try{const o=new DataTransfer;n.forEach(i=>o.items.add(i)),s.files=o.files}catch{}})}function b(e){const t=globalThis.E;if(!t||typeof t.get!="function")return null;const s=e.closest("[data-dropzone-field]"),n=s&&t.get(s,"dropzone")||t.get(e,"dropzone");return n&&typeof n.addFiles=="function"?n:null}function p(e,t){const s=String(t&&t.name||"").replace(/\[\]$/,"");return(e||[]).find(n=>n&&n.name===s)||{name:s}}function S(e){let t=e.nextElementSibling;return(!t||!t.classList.contains("fb-file-refusal"))&&(t=document.createElement("p"),t.className="fb-file-refusal",t.hidden=!0,e.after(t)),t}export function submissionTooLarge(e){const t=e/1048576;return`The files sent are too large together (at most ${t>=1?`${Math.round(t*10)/10} MB`:`${Math.round(e/1024)} KB`} in one submission). Send fewer or smaller files.`}export function setFileLimits(e,t,s){e&&(e._fbFileLimits=t||null,s&&(e._fbRetained=s))}function u(e){return Number(e&&e._fbFileLimits&&e._fbFileLimits.maxTotalBytes)||0}function m(e){let t=0;for(const s of e||[])t+=Number(s&&s.size)||0;return t}export function totalTooLarge(e,t){const s=u(e);if(!s||!t)return null;let n=0;for(const l of t.values())n+=m(l);return n>s?submissionTooLarge(s):null}function $(e,t){const s=globalThis.E,n=a=>s&&typeof s.get=="function"?s.get(a,"dropzone"):null,l=new Set;e.querySelectorAll('.dm-dropzone, [data-dropzone-field], input[type="file"]').forEach(a=>{const c=n(a);c&&l.add(c)});const o=new Set,i=new Set;let r=0;for(const a of l)a.input&&(o.add(a.input),a.input.name&&i.add(a.input.name)),a.zone&&a.zone.querySelectorAll('input[type="file"]').forEach(c=>o.add(c)),a!==t&&(r+=m(a.getFiles()));e.querySelectorAll('input[type="file"]').forEach(a=>{o.has(a)||(a.name&&i.add(a.name),r+=m(a.files))});for(const[a,c]of e._fbRetained||[])i.has(a)||(r+=m(c));return r}export function wireFileFields(e,t){if(!e||e._fbFilesWired)return;e._fbFilesWired=!0;const s=new WeakMap,n=(o,i,r)=>{const a=S(o.zone),c=s.get(o.zone)||[];c.push(refusalMessage(p(t,o.input),i,r,u(e))),s.set(o.zone,c),a.textContent=c.join(" "),a.hidden=!1,o.zone.classList.add("is-invalid"),queueMicrotask(()=>s.delete(o.zone))};e.addEventListener("dm:dropzone:reject",o=>{const i=o.detail&&o.detail.instance,[r,a]=o.detail&&o.detail.args||[];i&&i.zone&&n(i,r,a)});const l=new Map;e.addEventListener("dm:dropzone:beforeadd",o=>{const i=o.detail&&o.detail.instance,[r]=o.detail&&o.detail.args||[];if(!i||!i.zone||!r)return;const a=r.file||r;if(isActiveFile(a)){o.preventDefault(),n(i,r,"active");return}if(isRefusedKind(a)){o.preventDefault(),n(i,r,"kind");return}const c=u(e);if(!c)return;const d=i.options&&i.options.multiple===!1,x=Number(r.size??a.size)||0,g=d?0:l.get(i)||0;if($(e,d?i:null)+g+x>c){o.preventDefault(),n(i,r,"total");return}d||(l.has(i)||queueMicrotask(()=>l.delete(i)),l.set(i,g+x))}),e.addEventListener("dm:dropzone:change",o=>{const i=o.detail&&o.detail.instance;i&&l.delete(i)}),e.addEventListener("dm:dropzone:add",o=>{const i=o.detail&&o.detail.instance;if(!i||!i.zone||s.get(i.zone))return;const r=i.zone.nextElementSibling;r&&r.classList.contains("fb-file-refusal")&&(r.hidden=!0,r.textContent="")}),e.addEventListener("dm:dropzone:change",o=>{const i=o.detail&&o.detail.instance;if(!i||!i.input)return;i.input.dispatchEvent(new Event("input",{bubbles:!0}));const r=i.input.closest(".fb-field-invalid");r&&i.getFiles().length&&r.classList.remove("fb-field-invalid")})}export function enhanceFileInputs(e,t){const s=globalThis.E;!e||!s||typeof s.dropzone!="function"||(e.querySelectorAll('input[type="file"]').forEach(n=>{b(n)||s.dropzone(n,dropzoneOptions(p(t,n)))}),wireFileFields(e,t))}export function checkFileInputs(e,t){const s=[];return e&&e.querySelectorAll('input[type="file"]').forEach(n=>{const l=p(t,n),o=Array.from(n.files||[]),i=countRefusal(l,o.length);i&&s.push(i),o.forEach(r=>{const a=fileRefusal(l,r);a&&s.push(a)})}),s}export function destroyDropzones(e){const t=globalThis.E;!e||!t||typeof t.get!="function"||e.querySelectorAll('.dm-dropzone, input[type="file"]').forEach(s=>{const n=t.get(s,"dropzone");n&&typeof n.destroy=="function"&&n.destroy()})}
|
|
1
|
+
export const DEFAULT_MAX_SIZE=5242880,DEFAULT_MAX_FILES=5,MAX_FILES_LIMIT=20;export function fileRules(e){const t=e&&e.file||{},s=!!(e&&e.multiple),n=Math.floor(Number(t.maxFiles));return{accept:String(t.accept||"").split(",").map(l=>l.trim()).filter(Boolean),maxSize:Number(t.maxSize)||5242880,multiple:s,maxFiles:s?Math.min(n>=1?n:5,20):1}}function f(e){return e===1?"one file":`${e} files`}export function countRefusal(e,t){const{maxFiles:s,multiple:n}=fileRules(e);if(Number(t)<=s)return null;const l=e&&(e.label||e.name)||"File";return n?`"${l}" takes at most ${f(s)}.`:`"${l}" takes one file.`}export function fileRefs(e){return(Array.isArray(e)?e:[e]).filter(s=>s&&typeof s=="object"&&!Array.isArray(s)&&typeof s.url=="string"&&(typeof s.name=="string"||s.name===void 0))}export function isFileValue(e){const t=fileRefs(e);return t.length>0&&t.length===(Array.isArray(e)?e.length:1)}export function fileValueText(e,{base:t="",urls:s=!0}={}){return fileRefs(e).map(n=>{const l=n.name||n.url.split("/").pop()||"file";return!s||!n.url?l:`${l} (${fileUrl(n,t)})`}).join("; ")}export function fileUrl(e,t=""){const s=String(e&&e.url||""),n=String(t||"").replace(/\/+$/,"");return n&&s.startsWith("/")?`${n}${s}`:s}export function mimeAccepted(e,t){if(!e||!e.length)return!0;const s=String(t||"");return e.some(n=>n.endsWith("/*")?s.startsWith(n.slice(0,-1)):n===s)}export function cleanUploadName(e){const t=s=>s.replace(/^[-.]+|[-.]+$/g,"");return t(t(String(e||"upload").toLowerCase().replace(/[^\w.-]+/g,"-").replace(/-+/g,"-")).slice(0,200))||"upload"}const z=/^(text\/html|text\/xml|text\/xsl|text\/javascript|text\/ecmascript|application\/(x-)?javascript|application\/ecmascript|application\/xml|application\/[\w.-]+\+xml|image\/svg\+xml)\b/i,y=/\.(html?|xht(ml)?|shtml?|svgz?|xml|xsd|xslt?|xul|rdf|owl|atom|rss|wsdl|smil?|kml|opf|xlf|gpx|ttml|mathml|mml|xop|m?js|mht(ml)?)$/i,w=new Set("123 1km 3dml 3mf aab aam aas abw ac acc ace acu acutc aep afm afp ahead ai air ait ami apk appcache application apr arc arj asc asm aso atc atom atomcat atomdeleted atomsvc atx aw azf azs azw bat bcpio bdf bdm bdoc bed bh2 blb blorb bmi bmml book box boz bsp bz bz2 c c11amc c11amz c4d c4f c4g c4p c4u cap car cb7 cba cbr cbt cbz cc cco cct ccxml cdbcmsg cdf cdfx cdkey cdmia cdmic cdmid cdmio cdmiq cdx cdxml cdy cer cfs chat chrt cif cii cjs cla class clkk clkp clkt clkw clkx clp cmc cmdf cml cmp cod coffee com cpio cpp cpt crd crl crt crx cryptonote csh csl csml csp css cst cu curl cww cxt cxx dae daf dart dataless davmount dbf dbk dcr dcurl dd2 ddd ddf der dfac dgc dic dir dis disposition-notification dmp dna dp dpg dsc dssc dtb dtd dvi dwd dwf dxp dxr ear ecma edm edx efif ei6 eml emma emotionml emz eps es es3 esa esf et3 etx eva evy exi exp ext ez ez2 ez3 f f77 f90 fcdt fcs fdf fdt fe_launch fg5 fgd fig flo flw flx fly fm fnc fo for frame fsc ftc fti fxp fxpl fzs g2w g3w gac gam gbr gca gdl gdoc geo geojson gex ggb ggt ghf gim glb gltf gml gmx gnumeric gph gpx gqf gqs gram gramps gre grv grxml gsf gsheet gslides gtar gtm gtw gv gxf gxt h hal hbci hbs hdd hdf held hh hjson hlp hpgl hpid hps hqx htc htke htm html hvd hvp hvs i2g icc ice icm ics ifb ifm iges igl igm igs igx iif imp ink inkml install iota ipfix ipk irm irp itp its ivp ivu jad jade jam jar jardiff java jisp jlt jnlp joda js json json5 jsonld jsonml jsx karbon kdbx key kfo kia kml kmz kne knp kon kpr kpt kpxx ksp ktr ktz kwd kwt lasxml latex lbd lbe les less lgr lha link66 list3820 listafp litcoffee lnk lostxml ltf lua luac lwp lzh m13 m14 m21 m3u8 m4p ma mads maei mag maker man manifest map markdown mathml mb mbk mbox mc1 mcd mcurl md mdb mdx me mesh meta4 metalink mets mfm mft mgp mgz mie mif mime mjs mkd mlp mmd mmf mml mny mobi mods mp21 mp4s mpc mpd mpkg mpm mpn mpy mqy mrc mrcx ms mscml mseed mseq msf msh msl msty mtl mts mus musd musicxml mvb mvt mwf mxf mxl mxml mxs n-gage n3 nb nbp nc ncx nfo ngdat nitf nlu nml nnd nns nnw nq nsc nsf nt ntf numbers nzb oa2 oa3 oas obd obgx obj oda ogex ogx omdoc onepkg onetmp onetoc onetoc2 opf opml oprc org osf osfpvg osm otf ova ovf owl oxps oxt p p10 p12 p7b p7c p7m p7r p7s p8 pac pages pas paw pbd pcap pcf pcl pclxl pcurl pdb pde pem pfa pfb pfm pfr pfx pgn pgp php pki pkipath pkpass pl plb plc plf pls pm pml portpkg ppd pqa prc pre prf provx ps psb psf pskcxml ptid pub pvb pwn qam qbo qfx qps qwd qwt qxb qxd qxl qxt raml rapd rcprofile rdf rdz relo rep res rif ris rl rld rm rms rmvb rnc rng roa roff rp9 rpm rpss rpst rq rs rsat rsd rsheet rss rtx run rusd s saf sass sbml sc scd scm scq scs scss scurl sda sdc sdd sdkd sdkm sdp sdw sea see seed sema semd semf senmlx sensmlx ser setpay setreg sfd-hdstx sfs sfv sgl sgm sgml sh shar shex shf shtml sieve sig silo sis sisx sit sitx siv skd skm skp skt slim slm sls slt sm smf smi smil smzip snf spc spdx spf spl spot spp spq sql src srt sru srx ssdl sse ssf ssml st stc std stf sti stk stl stpx stpxz stpz str stw styl stylus sub sus susp sv4cpio sv4crc svc svd svg svgz swa swf swi swidtag sxc sxd sxg sxi sxm sxw t t3 taglet tao tcap tcl td teacher tei teicorpus tex texi texinfo tfi tfm tk tmo toml torrent tpl tpt tr tra trig trm tsd tsv ttc ttf ttl ttml twd twds txd txf u32 u8dsn u8hdr u8mdn u8msg ubj udeb ufd ufdl ulx umj unityweb uoml uri uris urls usdz ustar utz uu uvd uvf uvt uvvd uvvf uvvt uvvx uvvz uvx uvz vbox vbox-extpack vcard vcd vcf vcg vcs vcx vdi vds vhd vis vmdk vor vox vrml vsd vsf vss vst vsw vtt vtu vxml w3d wad wadl war wasm wbs wbxml webapp webmanifest wg wgt wmd wml wmlc wmls wmlsc wmz woff woff2 wpd wqd wri wrl wsc wsdl wspolicy wtb x32 x3d x3db x3dbz x3dv x3dvz x3dz x_b x_t xaml xap xar xav xbap xbd xca xcs xdf xdm xdp xdssc xdw xel xenc xer xfdf xfdl xht xhtml xhvml xlf xml xns xo xop xpi xpl xpr xpw xpx xsd xsl xslt xsm xspf xul xvm xvml xyz xz yaml yang yin yml ymp z1 z2 z3 z4 z5 z6 z7 z8 zaz zir zirz zmm ".trim().split(" "));export function isRefusedKind(e){const t=cleanUploadName(e&&(e.filename??e.name)||""),s=t.lastIndexOf(".");return s>=0&&w.has(t.slice(s+1))}function h(e){const t=/\.[^.]+$/.exec(cleanUploadName(e&&(e.filename??e.name)||""));return t?t[0]:""}export function isActiveFile(e){const t=String(e&&(e.mimetype??e.type)||""),s=e&&(e.filename??e.name)||"";return z.test(t)||y.test(cleanUploadName(s))}export function fileRefusal(e,t){const s=fileRules(e),n=e&&(e.label||e.name)||"File";if(Number(t&&t.size)>s.maxSize)return`"${n}" file is too large (max ${Math.round(s.maxSize/1024)} KB).`;const l=t&&(t.mimetype??t.type)||"";return isActiveFile(t)?`"${n}" file type "${l||h(t)}" not allowed (it could run as a page on this site).`:isRefusedKind(t)?`"${n}" file type "${h(t)||l}" cannot be uploaded here.`:s.accept.length&&!mimeAccepted(s.accept,l)?`"${n}" file type "${l}" not allowed (expected: ${String(e.file.accept).trim()}).`:null}const v=[[/wordprocessingml|^msword$/,"Word"],[/spreadsheetml|ms-excel/,"Excel"],[/presentationml|ms-powerpoint/,"PowerPoint"],[/^plain$/,"text"]];export function describeAccept(e){const t=[];return(e||[]).forEach(s=>{const[n,l]=String(s).split("/");let o;l==="*"?o={image:"images",audio:"audio",video:"video",text:"text files"}[n]||`${n} files`:l?o=v.find(([i])=>i.test(l))?.[1]||l.replace(/^(x-|vnd\.)/,"").split(/[.+]/)[0].toUpperCase():o=String(s),o&&!t.includes(o)&&t.push(o)}),t.length<2?t[0]||"":`${t.slice(0,-1).join(", ")} or ${t[t.length-1]}`}export function formatSize(e){const t=Number(e)||0;if(t<1024)return`${t} B`;const s=["KB","MB","GB"];let n=t,l=-1;do n/=1024,l++;while(n>=1024&&l<s.length-1);return`${n>=10?Math.round(n):Math.round(n*10)/10} ${s[l]}`}export function fileHint(e){const t=fileRules(e),s=describeAccept(t.accept),n=t.multiple?`up to ${f(t.maxFiles)}, ${formatSize(t.maxSize)} each`:`up to ${formatSize(t.maxSize)}`;return s?`${s[0].toUpperCase()}${s.slice(1)} - ${n}`:`Any file - ${n}`}export function dropzoneOptions(e){const t=fileRules(e);return{accept:t.accept.length?t.accept.join(","):null,maxSize:t.maxSize,multiple:t.multiple,maxFiles:t.multiple?t.maxFiles:null,labels:{prompt:t.multiple?"Drop files here, paste, or click to browse":"Drop a file here, paste, or click to browse",hint:fileHint(e),required:"Please add a file",type:refusalMessage(e,{name:"{name}"},"type"),size:refusalMessage(e,{name:"{name}"},"size"),count:refusalMessage(e,{name:"{name}"},"count")}}}export function fileBlueprint(e,t){return{...t,type:"dropzone",formConfig:{...t.formConfig||{},...dropzoneOptions(e)}}}export function refusalMessage(e,t,s,n){const l=fileRules(e),o=t&&t.name||"That file";if(s==="total")return`${o} was not added: the files together would be more than ${formatSize(n)}.`;if(s==="size")return`${o} was not added: it is larger than ${formatSize(l.maxSize)}.`;if(s==="type"){const i=describeAccept(l.accept);return`${o} was not added: ${i?`only ${i} can be uploaded here`:"that type of file is not accepted"}.`}return s==="active"?`${o} was not added: web pages, SVG images and scripts cannot be uploaded.`:s==="kind"?`${o} was not added: that kind of file cannot be uploaded here.`:`${o} was not added: only ${f(l.maxFiles)} can be added here.`}function k(e){return Array.isArray(e)&&e.length>0&&typeof File<"u"&&e.every(t=>t instanceof File)}export function splitFiles(e,t,s){const n={...e||{}},l=new Map,o=new Set((t||[]).filter(i=>i&&i.type==="file"&&i.name).map(i=>i.name));for(const[i,r]of Object.entries(n))k(r)?(l.set(i,r),delete n[i]):o.has(i)&&(Array.isArray(r)||r==null)&&(n[i]="");for(const[i,r]of s||[])l.set(i,r||[]);for(const[i,r]of l)r.length?delete n[i]:(l.delete(i),(o.has(i)||i in n)&&(n[i]=""));return{values:n,files:l}}export function submitBody(e,t){if(![...(t||new Map).values()].some(l=>l&&l.length))return{body:JSON.stringify(e),headers:{"Content-Type":"application/json"},multipart:!1};const n=new FormData;for(const[l,o]of Object.entries(e))o==null||t.has(l)||n.append(l,typeof o=="object"?JSON.stringify(o):String(o));for(const[l,o]of t)for(const i of o||[])n.append(l,i,i.name);return{body:n,headers:{},multipart:!0}}export function captureFileFields(e,t){!e||!t||e.querySelectorAll('input[type="file"]').forEach(s=>{s.name&&t.set(s.name,Array.from(s.files||[]))})}export function restoreFileFields(e,t){!e||!t||!t.size||e.querySelectorAll('input[type="file"]').forEach(s=>{const n=t.get(s.name);if(!n||!n.length||s.files&&s.files.length)return;const l=b(s);if(l){l.addFiles(n);return}try{const o=new DataTransfer;n.forEach(i=>o.items.add(i)),s.files=o.files}catch{}})}function b(e){const t=globalThis.E;if(!t||typeof t.get!="function")return null;const s=e.closest("[data-dropzone-field]"),n=s&&t.get(s,"dropzone")||t.get(e,"dropzone");return n&&typeof n.addFiles=="function"?n:null}function p(e,t){const s=String(t&&t.name||"").replace(/\[\]$/,"");return(e||[]).find(n=>n&&n.name===s)||{name:s}}function S(e){let t=e.nextElementSibling;return(!t||!t.classList.contains("fb-file-refusal"))&&(t=document.createElement("p"),t.className="fb-file-refusal",t.hidden=!0,e.after(t)),t}export function submissionTooLarge(e){const t=e/1048576;return`The files sent are too large together (at most ${t>=1?`${Math.round(t*10)/10} MB`:`${Math.round(e/1024)} KB`} in one submission). Send fewer or smaller files.`}export function setFileLimits(e,t,s){e&&(e._fbFileLimits=t||null,s&&(e._fbRetained=s))}function u(e){return Number(e&&e._fbFileLimits&&e._fbFileLimits.maxTotalBytes)||0}function m(e){let t=0;for(const s of e||[])t+=Number(s&&s.size)||0;return t}export function totalTooLarge(e,t){const s=u(e);if(!s||!t)return null;let n=0;for(const l of t.values())n+=m(l);return n>s?submissionTooLarge(s):null}function $(e,t){const s=globalThis.E,n=a=>s&&typeof s.get=="function"?s.get(a,"dropzone"):null,l=new Set;e.querySelectorAll('.dm-dropzone, [data-dropzone-field], input[type="file"]').forEach(a=>{const c=n(a);c&&l.add(c)});const o=new Set,i=new Set;let r=0;for(const a of l)a.input&&(o.add(a.input),a.input.name&&i.add(a.input.name)),a.zone&&a.zone.querySelectorAll('input[type="file"]').forEach(c=>o.add(c)),a!==t&&(r+=m(a.getFiles()));e.querySelectorAll('input[type="file"]').forEach(a=>{o.has(a)||(a.name&&i.add(a.name),r+=m(a.files))});for(const[a,c]of e._fbRetained||[])i.has(a)||(r+=m(c));return r}export function wireFileFields(e,t){if(!e||e._fbFilesWired)return;e._fbFilesWired=!0;const s=new WeakMap,n=(o,i,r)=>{const a=S(o.zone),c=s.get(o.zone)||[];c.push(refusalMessage(p(t,o.input),i,r,u(e))),s.set(o.zone,c),a.textContent=c.join(" "),a.hidden=!1,o.zone.classList.add("is-invalid"),queueMicrotask(()=>s.delete(o.zone))};e.addEventListener("dm:dropzone:reject",o=>{const i=o.detail&&o.detail.instance,[r,a]=o.detail&&o.detail.args||[];i&&i.zone&&n(i,r,a)});const l=new Map;e.addEventListener("dm:dropzone:beforeadd",o=>{const i=o.detail&&o.detail.instance,[r]=o.detail&&o.detail.args||[];if(!i||!i.zone||!r)return;const a=r.file||r;if(isActiveFile(a)){o.preventDefault(),n(i,r,"active");return}if(isRefusedKind(a)){o.preventDefault(),n(i,r,"kind");return}const c=u(e);if(!c)return;const d=i.options&&i.options.multiple===!1,x=Number(r.size??a.size)||0,g=d?0:l.get(i)||0;if($(e,d?i:null)+g+x>c){o.preventDefault(),n(i,r,"total");return}d||(l.has(i)||queueMicrotask(()=>l.delete(i)),l.set(i,g+x))}),e.addEventListener("dm:dropzone:change",o=>{const i=o.detail&&o.detail.instance;i&&l.delete(i)}),e.addEventListener("dm:dropzone:add",o=>{const i=o.detail&&o.detail.instance;if(!i||!i.zone||s.get(i.zone))return;const r=i.zone.nextElementSibling;r&&r.classList.contains("fb-file-refusal")&&(r.hidden=!0,r.textContent="")}),e.addEventListener("dm:dropzone:change",o=>{const i=o.detail&&o.detail.instance;if(!i||!i.input)return;i.input.dispatchEvent(new Event("input",{bubbles:!0}));const r=i.input.closest(".fb-field-invalid");r&&i.getFiles().length&&r.classList.remove("fb-field-invalid")})}export function enhanceFileInputs(e,t){const s=globalThis.E;!e||!s||typeof s.dropzone!="function"||(e.querySelectorAll('input[type="file"]').forEach(n=>{b(n)||s.dropzone(n,dropzoneOptions(p(t,n)))}),wireFileFields(e,t))}export function checkFileInputs(e,t){const s=[];return e&&e.querySelectorAll('input[type="file"]').forEach(n=>{const l=p(t,n),o=Array.from(n.files||[]),i=countRefusal(l,o.length);i&&s.push(i),o.forEach(r=>{const a=fileRefusal(l,r);a&&s.push(a)})}),s}export function destroyDropzones(e){const t=globalThis.E;!e||!t||typeof t.get!="function"||e.querySelectorAll('.dm-dropzone, input[type="file"]').forEach(s=>{const n=t.get(s,"dropzone");n&&typeof n.destroy=="function"&&n.destroy()})}
|
|
@@ -121,7 +121,9 @@ function safeFilename(raw) {
|
|
|
121
121
|
|
|
122
122
|
/** Why a stored name would be served as something a form may not collect, or null. */
|
|
123
123
|
function servedTypeRefusal(fieldCfg, filename) {
|
|
124
|
-
|
|
124
|
+
// As stored: safeFilename puts a prefix in front, so a name with no dot ("json") has
|
|
125
|
+
// no type and is served as a download, like any other name without an extension.
|
|
126
|
+
const served = servedType(`00000000-${cleanUploadName(filename)}`);
|
|
125
127
|
if (!served || formMayCollect(served)) return null;
|
|
126
128
|
const label = fieldCfg.label || fieldCfg.name;
|
|
127
129
|
return `"${label}" file type "${served}" cannot be uploaded here.`;
|
package/server/routes/public.js
CHANGED
|
@@ -208,7 +208,7 @@ export async function publicRoutes(fastify) {
|
|
|
208
208
|
// Re-parse with user context so the body's [menu] shortcode sees the
|
|
209
209
|
// visitor's role for visibility filtering. The first getPage() above
|
|
210
210
|
// was anonymous - it only needed the page's metadata.
|
|
211
|
-
const render = async () => {
|
|
211
|
+
const render = async (extra = {}) => {
|
|
212
212
|
let expiry = null;
|
|
213
213
|
if (!isDraft && page.hasExpiry) {
|
|
214
214
|
const rawCookies = Object.fromEntries(String(request.headers.cookie || '').split(';').map(part => {
|
|
@@ -225,6 +225,7 @@ export async function publicRoutes(fastify) {
|
|
|
225
225
|
}
|
|
226
226
|
const pageForRole = await getPage(page.urlPath, {user: userObj, ...(expiry && {expiry})}) || page;
|
|
227
227
|
return renderPage(pageForRole, {
|
|
228
|
+
...extra,
|
|
228
229
|
baseUrl,
|
|
229
230
|
user: userObj,
|
|
230
231
|
...(isDraft && {preview: {status: page.status || 'draft', urlPath: page.urlPath || urlPath, user: userObj}})
|
|
@@ -271,7 +272,10 @@ export async function publicRoutes(fastify) {
|
|
|
271
272
|
// x-dm-cache says which way this response went - hit, miss, off (the
|
|
272
273
|
// cache is disabled) or bypass (a draft, an expiring or a per-viewer
|
|
273
274
|
// page, never cached) - so a slow page can be read from the browser.
|
|
274
|
-
|
|
275
|
+
// The footer blocks' own tags (a [form] in one) are known only once it is drawn.
|
|
276
|
+
const drawnTags = new Set();
|
|
277
|
+
const renderTagged = async () => ({_cacheValue: await render({cacheTags: drawnTags}), _cacheTags: [...drawnTags]});
|
|
278
|
+
const {value: html, status} = await cache.wrapStatus(cacheKey, renderTagged, {tags: cacheTags});
|
|
275
279
|
reply.header('x-dm-cache', status);
|
|
276
280
|
hooks.emit('content:pageViewed', {urlPath, title: page.title || ''});
|
|
277
281
|
// cached: this HTML is shared, so its compressed body is kept too (htmlResponse.js).
|
|
@@ -239,6 +239,9 @@ function _vk(key) {
|
|
|
239
239
|
|
|
240
240
|
function _extractCacheTags(raw) {
|
|
241
241
|
if (raw && typeof raw === 'object' && Array.isArray(raw._cacheTags)) {
|
|
242
|
+
// {_cacheValue, _cacheTags}: a value that is not an object (a page's HTML) with tags
|
|
243
|
+
// only its production could know.
|
|
244
|
+
if ('_cacheValue' in raw) return {value: raw._cacheValue, extraTags: raw._cacheTags};
|
|
242
245
|
const {_cacheTags, ...value} = raw;
|
|
243
246
|
return {value, extraTags: _cacheTags};
|
|
244
247
|
}
|
|
@@ -247,6 +250,7 @@ function _extractCacheTags(raw) {
|
|
|
247
250
|
|
|
248
251
|
function _stripCacheTags(raw) {
|
|
249
252
|
if (raw && typeof raw === 'object' && Array.isArray(raw._cacheTags)) {
|
|
253
|
+
if ('_cacheValue' in raw) return raw._cacheValue;
|
|
250
254
|
const {_cacheTags, ...value} = raw;
|
|
251
255
|
return value;
|
|
252
256
|
}
|
|
@@ -409,6 +409,19 @@ export async function listMedia() {
|
|
|
409
409
|
return stats.sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
410
410
|
}
|
|
411
411
|
|
|
412
|
+
/**
|
|
413
|
+
* Emit a media event without letting a listener undo the work: the hooks are a plain
|
|
414
|
+
* EventEmitter, so a plugin's listener that throws would throw out of saveMedia after the
|
|
415
|
+
* file was written - the caller would report a failure and never clean the file up.
|
|
416
|
+
*/
|
|
417
|
+
function tellListeners(event, data) {
|
|
418
|
+
try {
|
|
419
|
+
hooks.emit(event, data);
|
|
420
|
+
} catch (err) {
|
|
421
|
+
console.warn(`[media] A ${event} listener failed: ${err?.message || err}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
412
425
|
/**
|
|
413
426
|
* Save an uploaded file to the media directory.
|
|
414
427
|
*
|
|
@@ -431,7 +444,7 @@ export async function saveMedia(filename, buffer, {exclusive = false} = {}) {
|
|
|
431
444
|
} else {
|
|
432
445
|
await fs.writeFile(filePath, buffer);
|
|
433
446
|
}
|
|
434
|
-
|
|
447
|
+
tellListeners('content:mediaUploaded', {filename});
|
|
435
448
|
return { name: filename, url: `/media/${filename}` };
|
|
436
449
|
}
|
|
437
450
|
|
|
@@ -444,7 +457,7 @@ export async function saveMedia(filename, buffer, {exclusive = false} = {}) {
|
|
|
444
457
|
export async function deleteMedia(filename) {
|
|
445
458
|
const filePath = path.join(MEDIA_DIR, filename);
|
|
446
459
|
await fs.unlink(filePath);
|
|
447
|
-
|
|
460
|
+
tellListeners('content:mediaDeleted', {filename});
|
|
448
461
|
}
|
|
449
462
|
|
|
450
463
|
/**
|
package/server/services/docs.js
CHANGED
|
@@ -360,7 +360,8 @@ export async function renderDocPage(docPath, opts = {}) {
|
|
|
360
360
|
sidebar: false,
|
|
361
361
|
tags: ['docs']
|
|
362
362
|
};
|
|
363
|
-
|
|
363
|
+
const drawn = new Set();
|
|
364
|
+
return {_cacheValue: await renderPage(page, {baseUrl, user, cacheTags: drawn}), _cacheTags: [...drawn]};
|
|
364
365
|
},
|
|
365
366
|
// The shell's menus and footer blocks too, so editing them reaches these pages.
|
|
366
367
|
{tags: ['docs', 'site', 'nav', ...footerBlockCacheTags(getConfig('site'))]}
|
|
@@ -8,7 +8,6 @@ import {fileURLToPath} from 'url';
|
|
|
8
8
|
import {getConfig} from '../config.js';
|
|
9
9
|
import {getInjectionSnippets} from './plugins.js';
|
|
10
10
|
import {applyTransforms} from './hooks.js';
|
|
11
|
-
import {FILE_LIMITS_TAG} from './forms.js';
|
|
12
11
|
import {contentsPaint, readContentsOverride, resolveContents} from './pageContents.js';
|
|
13
12
|
import {applyMenuOverride, hasThemeSwitcherItem, resolveLocation, resolveMenuDecorations, resolveOverlaysForPage} from './menus.js';
|
|
14
13
|
import {buildMenuNav} from './menuRender.js';
|
|
@@ -173,13 +172,9 @@ export function footerBlockNames(site) {
|
|
|
173
172
|
* and expiring shortcodes are never run - those stages come before [block] - so
|
|
174
173
|
* the block names are the whole of what a footer depends on beyond `site`.)
|
|
175
174
|
*/
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
|
|
179
|
-
export const footerBlockCacheTags = (site) => {
|
|
180
|
-
const names = footerBlockNames(site);
|
|
181
|
-
return names.length ? [...names.map(n => `block:${n}`), FILE_LIMITS_TAG] : [];
|
|
182
|
-
};
|
|
175
|
+
// What a block's own shortcodes depend on (a [form]'s `form:<slug>`, its file cap) is
|
|
176
|
+
// only known once it is drawn: renderPage hands those to `opts.cacheTags`.
|
|
177
|
+
export const footerBlockCacheTags = (site) => footerBlockNames(site).map(n => `block:${n}`);
|
|
183
178
|
|
|
184
179
|
/**
|
|
185
180
|
* The blocks in the footer (site.json footer.blocks - Site Settings > Footer,
|
|
@@ -192,7 +187,8 @@ export const footerBlockCacheTags = (site) => {
|
|
|
192
187
|
* Both render paths need this - the standing trap in this file.
|
|
193
188
|
*
|
|
194
189
|
* @param {object} site - site.json
|
|
195
|
-
* @param {{user?: object|null, urlPath?: string, project?: string|null}} [ctx]
|
|
190
|
+
* @param {{user?: object|null, urlPath?: string, project?: string|null, tags?: Set<string>}} [ctx]
|
|
191
|
+
* tags: receives the cache tags of what the blocks drew (a [form] in one, its file cap)
|
|
196
192
|
* @returns {Promise<string>} '' when there are none
|
|
197
193
|
*/
|
|
198
194
|
export async function buildFooterBlocks(site, ctx = {}) {
|
|
@@ -203,6 +199,7 @@ export async function buildFooterBlocks(site, ctx = {}) {
|
|
|
203
199
|
try {
|
|
204
200
|
const out = await parseMarkdown(`[block template="${name}" /]`, {user: ctx.user || null, urlPath: ctx.urlPath || '/', project: ctx.project ?? null});
|
|
205
201
|
const html = String(out.html || '').trim();
|
|
202
|
+
for (const t of out.tags || []) ctx.tags?.add(t);
|
|
206
203
|
// An unknown block renders nothing at all: no empty column for it.
|
|
207
204
|
if (html) cols.push(`<div class="cms-footer-block">${html}</div>`);
|
|
208
205
|
} catch (err) {
|
|
@@ -331,6 +328,8 @@ async function buildOverlayMenus(menuCtx, user) {
|
|
|
331
328
|
* @param {boolean} [opts.noTrack] - Mark the render as a preview for client-side
|
|
332
329
|
* analytics without drawing a banner. Implied by `opts.preview`; set directly
|
|
333
330
|
* by the editor's full-page preview, which has no banner.
|
|
331
|
+
* @param {Set<string>} [opts.cacheTags] - Receives the cache tags of what the shell
|
|
332
|
+
* drew beyond the page body (the footer blocks' shortcodes), for the caller's cache entry.
|
|
334
333
|
* @returns {Promise<string>}
|
|
335
334
|
*/
|
|
336
335
|
export async function renderPage(page, opts = {}) {
|
|
@@ -449,7 +448,7 @@ export async function renderPage(page, opts = {}) {
|
|
|
449
448
|
|
|
450
449
|
const themeView = await buildThemeView(page);
|
|
451
450
|
const searchAssets = buildSearchAssets();
|
|
452
|
-
const footerBlocksHtml = preset.footer !== false ? await buildFooterBlocks(site, {user: opts.user || null, urlPath: menuCtx.urlPath, project: menuCtx.project}) : '';
|
|
451
|
+
const footerBlocksHtml = preset.footer !== false ? await buildFooterBlocks(site, {user: opts.user || null, urlPath: menuCtx.urlPath, project: menuCtx.project, tags: opts.cacheTags}) : '';
|
|
453
452
|
const switcherAssets = buildThemeSwitcherAssets(themeView.cfg, {
|
|
454
453
|
html: `${page.html || ''}${overlayMenus}${footerBlocksHtml}`,
|
|
455
454
|
menus: [navItemsDecorated, footerPrimaryDecorated, footerLegalDecorated]
|