const fs = require('fs') const path = require('path') const TE = require('./lib/art-template.js'); // B7-3: 发布页是对外公开的,数据源是后台账号填写的应用表单(管理员持久化注入面)。 // 模板本身含合法 HTML,不能全局开 escape —— 按字段做上下文相关清洗。 function escapeHtmlText(value) { if (value === undefined || value === null) return value return String(value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, ''') } // href/src 属性:转义 + URL 协议白名单(仅 http/https/云存储 fileID),javascript:/data: 等直接清空 function sanitizeUrl(value) { if (value === undefined || value === null) return value const s = String(value).trim() if (!s) return s if (/^(https?:\/\/|cloud:\/\/|data:image\/)/i.test(s)) return escapeHtmlText(s) return '' } // JS 单引号字符串上下文(window.$app):先走 URL/文本白名单,再做 JS 转义。 // 必须处理 突破(< → \x3c)与引号/换行逃逸 function sanitizeJsString(value) { if (value === undefined || value === null) return value return String(value) .replace(/\\/g, '\\\\') .replace(/'/g, "\\'") .replace(/"/g, '\\"') .replace(//g, '\\x3e') .replace(/\r/g, '\\r') .replace(/\n/g, '\\n') .replace(/\u2028/g, '\\u2028') .replace(/\u2029/g, '\\u2029') } // URL 在 JS 上下文:只做协议白名单(不做 HTML 转义,否则 & 会污染 JS 值),再做 JS 转义 function sanitizeUrlForJs(value) { if (value === undefined || value === null) return value const s = String(value).trim() if (!s) return s if (!/^(https?:\/\/|cloud:\/\/|data:image\/)/i.test(s)) return '' return sanitizeJsString(s) } // 文本节点字段统一走 HTML 转义(undefined/null 保持原值,让模板 if 判断不受影响) function sanitizeText(value) { return escapeHtmlText(value) } const success = { success: true } const fail = { success: false } async function translateTCB(_fileList = []) { if (!_fileList.length) return _fileList // 腾讯云和阿里云下载链接不同,需要处理一下,阿里云会原样返回 const { fileList } = await uniCloud.getTempFileURL({ fileList: _fileList }); return fileList.map((item, index) => item.tempFileURL ? item.tempFileURL : _fileList[index]) } function hasValue(value) { if (typeof value !== 'object') return !!value if (value instanceof Array) return !!value.length return !!(value && Object.keys(value).length) } module.exports = async function(id) { if (!id) { return { ...fail, code: -1, errMsg: 'id required' }; } // 根据sitemap配置加载页面模板,例如列表页,详情页 let templatePage = fs.readFileSync(path.resolve(__dirname, './template.html'), 'utf8'); if (!templatePage) { return { ...fail, code: -2, errMsg: 'page template no found' }; } const db = uniCloud.database() let dbPublishList try { dbPublishList = db.collection('opendb-app-list') } catch (e) {} if (!dbPublishList) return fail; const record = await dbPublishList.where({ _id: id }).get({ getOne: true }) if (record && record.data && record.data.length) { const appInfo = record.data[0] const defaultOptions = { hasApp: false, hasMP: false, hasH5: false, hasQuickApp: false } defaultOptions.mpNames = { 'mp_weixin': '微信', 'mp_alipay': '支付宝', 'mp_baidu': '百度', 'mp_toutiao': '字节', 'mp_qq': 'QQ', 'mp_dingtalk': '钉钉', 'mp_kuaishou': '快手', 'mp_lark': '飞书', 'mp_jd': '京东' } const imageList = []; ['app_android'].forEach(key => { if (!hasValue(appInfo[key])) return imageList.push({ key, urlKey: 'url', url: appInfo[key].url }) }) Object.keys(defaultOptions.mpNames).concat('quickapp').forEach(key => { if (!hasValue(appInfo[key])) return imageList.push({ key, urlKey: 'qrcode_url', url: appInfo[key].qrcode_url }) }); ['icon_url'].forEach(key => { if (!hasValue(appInfo[key])) return imageList.push({ key, url: appInfo[key] }) }) const filelist = await translateTCB(imageList.map(item => item.url)) imageList.forEach((item, index) => { if (item.urlKey) { appInfo[item.key][item.urlKey] = filelist[index] } else { appInfo[item.key] = filelist[index] } }) if (hasValue(appInfo.screenshot)) { appInfo.screenshot = await translateTCB(appInfo.screenshot) } { const appInfoKeys = Object.keys(appInfo) if (appInfoKeys.some(key => { return key.indexOf('app_') !== -1 && hasValue(appInfo[key]) })) { defaultOptions.hasApp = true } if (appInfoKeys.some(key => { return key.indexOf('mp') !== -1 && hasValue(appInfo[key]) })) { defaultOptions.hasMP = true } if (appInfo.h5 && appInfo.h5.url) { defaultOptions.hasH5 = true } if (appInfo.quickapp && appInfo.quickapp.qrcode_url) { defaultOptions.hasQuickApp = true } // app if (defaultOptions.hasApp && appInfo.app_android && appInfo.app_android.url) { defaultOptions.android_url = appInfo.app_android.url } else { defaultOptions.android_url = '' } if (defaultOptions.hasApp && appInfo.app_ios) { if (appInfo.app_ios.url) { defaultOptions.ios_url = appInfo.app_ios.url } if (appInfo.app_ios.abm_url) { defaultOptions.ios_abm_url = appInfo.app_ios.abm_url } } else { defaultOptions.ios_url = '' defaultOptions.ios_abm_url = '' } if (defaultOptions.hasApp && appInfo.app_harmony && appInfo.app_harmony.url) { defaultOptions.harmony_url = appInfo.app_harmony.url } else { defaultOptions.harmony_url = '' } // mp:只保留官方已知平台键,防止 DB 被写入 xmp