UNPKG

jsharmony-cms

Version:
1,153 lines (1,050 loc) 109 kB
/* Copyright 2021 apHarmony This file is part of jsHarmony. jsHarmony is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. jsHarmony is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this package. If not, see <http://www.gnu.org/licenses/>. */ var Helper = require('jsharmony/Helper'); var HelperFS = require('jsharmony/HelperFS'); var jshParser = require('jsharmony/lib/JSParser.js'); var _ = require('lodash'); var async = require('async'); var path = require('path'); var fs = require('fs'); var parse5 = require('parse5'); var crypto = require('crypto'); var urlparser = require('url'); module.exports = exports = function(module, funcs){ var exports = {}; exports.readPageTemplateConfig = function(templateContent, desc, options){ var templateParts = funcs.parseConfig(templateContent, 'cms-page-config', desc, options); return templateParts.config; }; exports.readComponentTemplateConfig = function(templateContent, desc, options){ var templateParts = funcs.parseConfig(templateContent, 'cms-component-config', desc, options); return templateParts.config; }; exports.parseComponentTemplateConfigExtensions = function(config){ if(!config) return; if(!config.properties) config.properties = {}; if(!config.data) config.data = {}; //Apply config.component_properties if(config.component_properties){ if(_.isArray(config.component_properties)) { config.properties.fields = (config.properties.fields||[]); if(!_.isArray(config.properties.fields)) throw new Error('Component config.properties.fields must be an array'); config.properties.fields = config.properties.fields.concat(config.component_properties); } else if(_.isObject(config.component_properties)){ config.properties = _.extend(config.properties, config.component_properties); } else throw new Error('Component config.component_properties must be an array or object'); delete config.component_properties; } //Apply config.item_properties if(config.item_properties){ if(_.isArray(config.item_properties)) { config.data.fields = (config.data.fields||[]); if(!_.isArray(config.data.fields)) throw new Error('Component config.data.fields must be an array'); config.data.fields = config.data.fields.concat(config.item_properties); } else if(_.isObject(config.item_properties)){ config.data = _.extend(config.data, config.item_properties); } else throw new Error('Component config.item_properties must be an array or object'); delete config.item_properties; } //Apply config.type if(!('multiple_items' in config)){ config.multiple_items = (config.data && ((config.data.layout == 'grid_preview') || (config.data.layout == 'grid'))) ? true : false; } //Apply config.data.layout if(!config.data.layout){ if(config.multiple_items) config.data.layout = 'grid_preview'; else config.data.layout = 'form'; } //Add config.editor_placeholder config.editor_placeholder = config.editor_placeholder || {}; if(!('items_empty' in config.editor_placeholder)) config.editor_placeholder.items_empty = true; if(!('invalid_fields' in config.editor_placeholder)) config.editor_placeholder.invalid_fields = true; //Add config.options config.options = config.options || {}; if(!('component_preview_size' in config.options)) config.options.component_preview_size = 'expand'; if(!('editor_container' in config.options)) config.options.editor_container = 'block'; //Apply config.caption if(!('caption' in config)){ config.caption = [config.title, config.title]; } //Generate component class if(!('className' in config)){ config.className = Helper.escapeCSSClass(config.id, { nodash: true }); } //Set default target to "page" if(!('target' in config)) config.target = 'content'; //Set default icon if((config.target == 'content') && !('icon' in config)){ config.icon = 'material:layers'; } if(!_.includes(['content', 'page', 'site'], config.target)) throw new Error('Invalid config.target - must be either "content", "page", or "site"'); //Set default field.control and field.type for htmleditor _.each(config.data.fields, function(field){ if(!field) return; if(field.control == 'htmleditor'){ field.control = 'hidden'; field.type = field.type || 'varchar'; } }); }; exports.parseConfig = function(content, configType, desc, options){ options = _.extend({ continueOnConfigError: false, extractFromContent: false, templateName: null, }, options); var rslt = { config: {}, content: content, }; var htdoc = null; try{ htdoc = new funcs.HTMLDoc(content, { extractEJS: 'parseOnly' }); htdoc.applyNodes([ { //Apply properties pred: function(node){ return (htdoc.isTag(node, 'script') && htdoc.hasAttr(node, 'type', 'text/'+configType)) || (htdoc.isTag(node, configType)); }, exec: function(node){ if(options.templateName){ //Check if config applies to this template var templateCond = htdoc.getAttr(node, 'cms-template'); if(options.templateName && templateCond && !funcs.evalBoolAttr(templateCond, function(val){ return val == options.templateName; })){ htdoc.removeNode(node); return; } } var configScript = htdoc.getNodeContent(node); htdoc.removeNode(node); var config = {}; try{ config = jshParser.ParseJSON(configScript, desc, { trimErrors: true }); } catch(ex){ if(options.continueOnConfigError) module.jsh.Log.info(new Error('Could not parse ' + configType + ' in ' + desc + ': ' + ex.toString())); else throw ex; } _.extend(rslt.config, config); } }, { pred: function(node){ return ((configType=='cms-page-config') && htdoc.hasAttr(node, 'cms-component-content')); }, exec: function(node){ try { var contentArea = htdoc.getAttr(node, 'cms-component-content'); var nodeContent = htdoc.getNodeContent(node, 'cms-component-content'); if(!htdoc.hasAttr(node, 'cms-component')) return; if(contentArea.indexOf('page.content.')==0) contentArea = contentArea.substr(('page.content.').length); if(!('content' in rslt.config)) rslt.config.content = {}; if(!(contentArea in rslt.config.content)){ var componentPropNames = ['cms-component','cms-component-properties','cms-component-data','cms-component-remove-container','cms-onRender','cms-menu-tag']; var componentProps = {}; _.each(componentPropNames, function(key){ if(htdoc.hasAttr(node, key)) componentProps[key] = htdoc.getAttr(node, key); }); var containerHtml = '<div '+_.map(componentProps, function(val, key){ return key+'="'+Helper.escapeHTML(val)+'"'; }).join(' ')+' cms-component-remove-container>'+nodeContent+'</div>'; rslt.config.content[contentArea] = containerHtml; } } catch(ex){ if(options.continueOnConfigError) module.jsh.Log.info(new Error('Error parsing page component in ' + desc + ': ' + ex.toString())); else throw ex; } } }, ]); } catch(ex){ let ex_det = new Error('Could not parse ' + configType + ' script tag in ' + desc + ': ' + ex.toString()); if(options.continueOnConfigError) module.jsh.Log.info(ex_det); else throw ex_det; } if(htdoc && options.extractFromContent){ try{ htdoc.trimRemoved(); } catch(ex){ let ex_det = new Error('Could not parse ' + configType + ' in ' + desc + ': ' + ex.toString()); if(options.continueOnConfigError) module.jsh.Log.info(ex_det); else throw ex_det; } rslt.content = htdoc.content; } return rslt; }; exports.getPageTemplate = function(dbcontext, site_id, template_id, options, callback){ funcs.getPageTemplates(dbcontext, site_id, _.extend({ target_template_id: template_id }, options), function(err, pageTemplates){ if(err) return callback(err); return callback(null, pageTemplates[template_id]); }); }; exports.getPageTemplates = function(dbcontext, site_id, options, callback){ var cms = module; options = _.extend({ site_template_type: 'PAGE', template_folder: 'pages', system_templates: cms.SystemPageTemplates, script_config_type: 'cms-page-config', get_auxiliary_attributes: function(templateName, template){ var rslt = { header: templateName + '.header.ejs', footer: templateName + '.footer.ejs', css: templateName + '.css', js: templateName + '.js', templates: { editor: templateName + '.templates.editor.ejs', publish: templateName + '.templates.publish.ejs', }, content: { } }; for(var key in (template.content_elements||{body:true})){ rslt.content[key] = templateName+'.'+key+'.ejs'; } return rslt; }, }, options); funcs.getSiteTemplates(dbcontext, site_id, options, function(err, rsltTemplates){ if(err) return callback(err); for(var key in rsltTemplates){ var template = rsltTemplates[key]; var newTemplate = { title: template.site_template_title, raw: false, location: template.site_template_location, path: template.site_template_path, }; if(template.site_template_location == 'REMOTE'){ if(template.site_template_path){ newTemplate.remote_templates = { editor: template.site_template_path }; } } if(template.site_template_config){ var templateConfig = {}; if(_.isString(template.site_template_config)){ try{ templateConfig = JSON.parse(template.site_template_config); } catch(ex){ /* Do nothing */ } } else{ templateConfig = template.site_template_config; } _.merge(newTemplate, templateConfig); } if(!newTemplate.content_elements && (template.site_template_location != 'REMOTE')) newTemplate.content_elements = { body: { type: 'htmleditor', title: 'Body' } }; if(!newTemplate.content) newTemplate.content = {}; if(!('title' in newTemplate)) newTemplate.title = key; rsltTemplates[key] = newTemplate; } return callback(null, rsltTemplates); }); }; exports.getComponentTemplates = function(dbcontext, site_id, options, callback){ var cms = module; options = _.extend({ site_template_type: 'COMPONENT', template_folder: 'components', system_templates: cms.SystemComponentTemplates, script_config_type: 'cms-component-config', get_auxiliary_attributes: function(templateName, template){ var rslt = { css: templateName + '.css', js: templateName + '.js', templates: { editor: templateName + '.templates.editor.ejs', publish: templateName + '.templates.publish.ejs', }, properties: { ejs: templateName + '.properties.ejs', css: templateName + '.properties.css', js: templateName + '.properties.js', }, data: { ejs: templateName + '.data.ejs', css: templateName + '.data.css', js: templateName + '.data.js', }, }; return rslt; }, withContent: false, includeLocalPath: false, recursive: true, }, options); var componentTemplates = []; async.waterfall([ //Get template definitions function(cb){ funcs.getSiteTemplates(dbcontext, site_id, options, function(err, _componentTemplates){ if(err) return cb(err); componentTemplates = _componentTemplates; for(var key in componentTemplates){ var template = componentTemplates[key]; var newTemplate = { id: template.site_template_name, title: template.site_template_title, location: template.site_template_location, templates: {}, properties: {}, data: {}, }; if(options.withContent){ if(typeof template.site_template_content != 'undefined'){ newTemplate.templates.editor = template.site_template_content; } } if(options.includeLocalPath) newTemplate.path = template.site_template_path; if(template.site_template_location == 'REMOTE'){ if(template.site_template_path){ newTemplate.remote_templates = { editor: template.site_template_path }; } } if(template.site_template_config){ var templateConfig = {}; if(_.isString(template.site_template_config)){ try{ templateConfig = JSON.parse(template.site_template_config); } catch(ex){ /* Do nothing */ } } else{ templateConfig = template.site_template_config; } delete templateConfig.id; _.merge(newTemplate, templateConfig); } if(!('title' in newTemplate)) newTemplate.title = key; componentTemplates[key] = newTemplate; } return cb(); }); }, ], function(err){ if(err) return callback(err); return callback(null, componentTemplates); }); }; exports.getSiteTemplates = function(dbcontext, site_id, options, callback){ options = _.extend({ target_template_id: null, //Optional - target only one template site_template_type: null, //Required template_folder: null, //Required system_templates: null, //Required script_config_type: null, //Required get_auxiliary_attributes: function(templateName, template){ return {}; }, continueOnConfigError: false, withContent: false, recursive: false, }, options); var jsh = module.jsh; var appsrv = jsh.AppSrv; var dbtypes = appsrv.DB.types; var cms = module; var rsltTemplates = { //id = { // title: '...', // raw: true, // location: '...', LOCAL,SYSTEM,REMOTE // path: '...', //} }; var systemTemplates = []; var localTemplates = []; var remoteTemplates = []; return async.parallel([ function(data_cb){ if(!site_id) return data_cb(); var sitePath = path.join(path.join(jsh.Config.datadir,'site'),site_id.toString()); var normalizedSitePath = path.normalize(sitePath); var templatePath = path.join(sitePath, 'templates', options.template_folder); var publishTemplatePaths = {}; var exportTemplatePaths = {}; //Get all local site templates from site\#\templates\[options.template_folder] fs.exists(templatePath, function (exists) { if (!exists) return data_cb(null); var files = []; var allfiles = []; HelperFS.funcRecursive(templatePath, function(filepath, filerelativepath, file_cb){ //per file var file = { filepath: filepath, filerelativepath: HelperFS.convertWindowsToPosix(filerelativepath) }; files.push(file); allfiles[file.filerelativepath] = file; return file_cb(); }, function(dirpath, dirrelativepath, cb){ //per directory if(!dirrelativepath) return cb(); if(options.recursive) return cb(); return cb(false); //Non-recursive }, { sort: function(a,b){ a = a||''; b = b||''; if(a > b) return 1; if(a < b) return -1; return 0; } }, function(err){ if(err) return data_cb(err); async.each(files, function(fileinfo, file_cb){ var file = fileinfo.filerelativepath; var ext = path.extname(file); if((ext=='.htm') || (ext=='.html')){ var templateName = file.substr(0, file.length - ext.length); //Do not read / parse template if it does not match target_template_id //Also helps to isolate options.continueOnConfigError to target template if(options.target_template_id && (templateName != options.target_template_id)) return file_cb(); var filepath = path.normalize(fileinfo.filepath); fs.readFile(filepath, 'utf8', function(err, templateContent){ if(err) return file_cb(err); //Read Template Config var templateParts = null; try{ templateParts = funcs.parseConfig(templateContent, options.script_config_type, 'local ' + options.site_template_type.toLowerCase() + ' template "' + file + '"', { continueOnConfigError: options.continueOnConfigError }); } catch(ex){ return file_cb(ex); } var templateConfig = templateParts.config; var templateTitle = templateConfig.title || templateName; var localTemplate = { site_template_type: options.site_template_type, site_template_name: templateName, site_template_title: templateTitle, site_template_path: '/templates/'+options.template_folder+'/' + file, site_template_config: templateConfig, site_template_location: 'LOCAL', }; if(options.withContent) localTemplate.site_template_content = templateContent; localTemplates.push(localTemplate); if(templateConfig && templateConfig.remote_templates && templateConfig.remote_templates.publish){ var publishTemplatePath = templateConfig.remote_templates.publish; if(publishTemplatePath.indexOf('//') < 0){ publishTemplatePath = path.normalize(path.join(path.dirname(filepath), publishTemplatePath)); if(publishTemplatePath.indexOf(normalizedSitePath+path.sep) == 0){ publishTemplatePaths[HelperFS.convertWindowsToPosix(publishTemplatePath.substr(normalizedSitePath.length))] = { source: localTemplate.site_template_path, }; } //Do not allow an editor template to reference itself as a publish template if(publishTemplatePath == filepath) return file_cb(new Error('Error processing local template ' + localTemplate.site_template_path + ': An Editor template cannot target itself as the Publish template. Remove the remote_templates.publish property to auto-generate a publish template based on the current editor template.')); } } if(templateConfig && templateConfig.export) for(var i=0;i<templateConfig.export.length;i++){ var exportItem = templateConfig.export[i]; var exportTemplatePath = exportItem.remote_template; if(!exportTemplatePath) continue; if(exportTemplatePath.indexOf('//') < 0){ exportTemplatePath = path.normalize(path.join(path.dirname(filepath), exportTemplatePath)); if(exportTemplatePath.indexOf(normalizedSitePath+path.sep) == 0){ exportTemplatePaths[HelperFS.convertWindowsToPosix(exportTemplatePath.substr(normalizedSitePath.length))] = { source: localTemplate.site_template_path, }; } //Do not allow an editor template to reference itself as an export template if(exportTemplatePath == filepath) return file_cb(new Error('Error processing local template ' + localTemplate.site_template_path + ': An Editor template cannot target itself as the Export template. Remove the export[].remote_template property to auto-generate an export template based on the current editor template.')); } } //Get list of Auxiliary Files function trimMissingAttributes(node){ _.each(_.keys(node), function(key){ var val = node[key]; if(_.isString(val)){ if(!(val in allfiles)) delete node[key]; } else{ trimMissingAttributes(val); if(_.isEmpty(val)) delete node[key]; } }); } var auxAttributes = options.get_auxiliary_attributes(templateName, templateConfig); trimMissingAttributes(auxAttributes); //Load Auxiliary Files function prependAuxiliaryFiles(obj, attributes, aux_cb){ async.eachOf(attributes, function(val, key, attr_cb){ if(_.isString(val)){ var attrfileinfo = allfiles[val]; var attrfilepath = path.normalize(attrfileinfo.filepath); fs.readFile(attrfilepath, 'utf8', function(err, attrContent){ if(err) return attr_cb(err); if(!obj[key] || _.isString(obj[key])){ if (key in obj) attrContent += '\r\n' + obj[key]; obj[key] = attrContent; } else if(_.isArray(obj[key])){ obj[key].unshift(attrContent); } return attr_cb(); }); return; } else if(val && !_.isEmpty(val)){ if(!obj[key]) obj[key] = {}; return prependAuxiliaryFiles(obj[key], val, attr_cb); } return attr_cb(); }, aux_cb); } prependAuxiliaryFiles(templateConfig, auxAttributes, file_cb); }); } else file_cb(); }, function(err){ if(err) return data_cb(err); //Go through each local template for(var i=0;i<localTemplates.length;i++){ var localTemplate = localTemplates[i]; //If local template is a publish template if(localTemplate.site_template_path in publishTemplatePaths){ //Do not allow publish templates to have a templateConfig if(!_.isEmpty(localTemplate.site_template_config)){ let source = publishTemplatePaths[localTemplate.site_template_path].source; return data_cb(new Error('Error processing publish template ' + localTemplate.site_template_path + ': Publish templates cannot contain a "' + options.script_config_type + '" script tag. This template is used as a publish template by '+source+' via the remote_templates.publish property.\r\n\r\nPlease make sure not to use Editor templates as Publish templates. Remove the remote_templates.publish property from the Editor template to auto-generate a Publish template based on the current editor template.')); } //Remove template localTemplates.splice(i, 1); i--; continue; } //If local template is an export template if(localTemplate.site_template_path in exportTemplatePaths){ //Do not allow export templates to have a templateConfig if(!_.isEmpty(localTemplate.site_template_config)){ let source = exportTemplatePaths[localTemplate.site_template_path].source; return data_cb(new Error('Error processing export template ' + localTemplate.site_template_path + ': Export templates cannot contain a "' + options.script_config_type + '" script tag. This template is used as a export template by '+source+' via the export[].remote_template property.\r\n\r\nPlease make sure not to use Editor templates as Export templates. Remove the export[].remote_template property from the Editor template to auto-generate an Export template based on the current editor template.')); } //Remove template localTemplates.splice(i, 1); i--; continue; } } return data_cb(); }); } ); }); }, //Add local system templates function(data_cb){ for(var key in options.system_templates){ if(options.target_template_id && (key != options.target_template_id)) continue; var systemTemplate = JSON.parse(JSON.stringify(options.system_templates[key])); var templatePath = ' '; if(systemTemplate.remote_templates){ templatePath = systemTemplate.remote_templates.editor || ' '; } systemTemplates.push({ site_template_type: options.site_template_type, site_template_name: key, site_template_title: systemTemplate.title, site_template_path: templatePath, site_template_config: systemTemplate, site_template_location: 'SYSTEM', }); } return data_cb(); }, //Generate SQL function(data_cb){ //Remote Templates var sql = 'select site_template_id,site_id,site_template_type,site_template_name,site_template_title,site_template_path,site_template_config from '+(cms.schema?cms.schema+'.':'')+'site_template where site_template_type=@site_template_type and site_id=@site_id'; appsrv.ExecRecordset(dbcontext, sql, [dbtypes.BigInt, dbtypes.VarChar(32)], { site_id: site_id, site_template_type: options.site_template_type }, function (err, rslt) { if(err) return data_cb(err); if(!rslt || !rslt.length || !rslt[0]){ return data_cb(new Error('Error loading remote '+options.site_template_type.toLowerCase()+' templates')); } _.each(rslt[0], function(row){ if(options.target_template_id && (row.site_template_name != options.target_template_id)) return; remoteTemplates.push({ site_template_type: options.site_template_type, site_template_name: row.site_template_name, site_template_title: row.site_template_title, site_template_path: row.site_template_path, site_template_config: row.site_template_config, site_template_location: 'REMOTE', }); }); return data_cb(); }); }, ], function(err){ if(err) return callback(err); //Combine all templates into one array _.each(remoteTemplates, function(template){ if(!(template.site_template_name in rsltTemplates)) rsltTemplates[template.site_template_name] = template; }); _.each(localTemplates, function(template){ if(!(template.site_template_name in rsltTemplates)) rsltTemplates[template.site_template_name] = template; }); _.each(systemTemplates, function(template){ if(!(template.site_template_name in rsltTemplates)) rsltTemplates[template.site_template_name] = template; }); if(options.withContent){ _.each(rsltTemplates, function(template){ if(!('site_template_content' in template)) template.site_template_content = undefined; }); } return callback(null, rsltTemplates); }); }; exports.getCurrentPageTemplatesLOV = function(dbcontext, values, options, cb){ options = _.extend({ blank:false }, options); var site_id = null; for(var i=0;i<values.length;i++){ if(values[i].code_txt=='site_id') site_id = parseInt(values[i].code_val); if(values[i].code_val){ values.splice(i, 1); i--; } } if(!site_id){ var rsltlov = []; if(options.blank) rsltlov.push({ code_val: '', code_txt: '(None)' }); cb(null, rsltlov); return false; } funcs.getPageTemplates(dbcontext, site_id, { continueOnConfigError: true }, function(err, pageTemplates){ if(err) return cb(err); if(!pageTemplates) return cb(new Error('Error loading page templates')); var rsltlov = []; if(options.blank) rsltlov.push({ code_val: '', code_txt: '(None)' }); for(var key in pageTemplates){ rsltlov.push({ code_val: key, code_txt: (pageTemplates[key].title||key||'').toString() }); } //Sort by name rsltlov = rsltlov.sort(function(a,b){ var ua = a.code_txt.toUpperCase(); var ub = b.code_txt.toUpperCase(); if(ua > ub) return 1; if(ua < ub) return -1; return 0; }); return cb(null, rsltlov); }); return false; }; exports.getSiteConfig = function(dbcontext, site_id, options, callback){ options = _.extend({ continueOnConfigError: false, }, options); var jsh = module.jsh; var appsrv = jsh.AppSrv; var dbtypes = appsrv.DB.types; var cms = module; var rsltConfig = { // title: '...', // path: '...', // menus: {}, }; var systemConfig = JSON.parse(JSON.stringify(cms.SystemSiteConfig)); var localConfig = {}; var dbConfig = {}; return (site_id ? async.parallel : async.waterfall)([ //Get config from database function(data_cb){ var sql = 'select site_config from {schema}.site where site_id=@site_id'; var sql_ptypes = [dbtypes.BigInt]; var sql_params = { site_id: site_id }; if(!site_id){ sql = 'select site_id, site_config from {schema}.site where site_id={schema}.my_current_site_id()'; sql_ptypes = []; sql_params = { }; } appsrv.ExecRecordset(dbcontext, funcs.replaceSchema(sql), sql_ptypes, sql_params, function (err, rslt) { if(err) return data_cb(err); if(!rslt || !rslt.length || !rslt[0] || !rslt[0].length){ return data_cb(new Error('Error loading database site config')); } site_id = rslt[0][0].site_id; var configText = rslt[0][0].site_config || ''; if(configText){ //Read Site Config try{ dbConfig = jshParser.ParseJSON(configText, 'Database site config', { trimErrors: true }); } catch(ex){ if(options.continueOnConfigError) module.jsh.Log.info(new Error('Could not parse database site config: ' + ex.toString())); else return data_cb(err); } } return data_cb(); }); }, //Get config from file system function(data_cb){ if(!site_id) return data_cb(); var sitePath = path.join(path.join(jsh.Config.datadir,'site'),site_id.toString()); var configPath = path.join(sitePath, 'templates', 'site_config.json'); //Get local site config from site\#\templates\site_config.json fs.exists(configPath, function (exists) { if (!exists) return data_cb(); fs.readFile(configPath, 'utf8', function(err, configText){ if(err) return data_cb(err); //Read Site Config try{ localConfig = jshParser.ParseJSON(configText, 'Local site config', { trimErrors: true }); } catch(ex){ if(options.continueOnConfigError) module.jsh.Log.info(new Error('Could not parse local site config: ' + ex.toString())); else return data_cb(err); } return data_cb(); }); }); }, ], function(err){ if(err) return callback(err); rsltConfig = funcs.mergeSiteConfig({ media_thumbnails: cms.Config.media_thumbnails, redirect_listing_path: cms.Config.redirect_listing_path, },systemConfig, localConfig, dbConfig); return callback(null, rsltConfig); }); }; exports.mergeSiteConfig = function(){ if(!arguments.length) return {}; var rslt = JSON.parse(JSON.stringify(arguments[0])) || {}; for(var i=1;i<arguments.length;i++){ var b = arguments[i]; for(var key in b){ if(!(key in rslt)) rslt[key] = b[key]; else { if(key=='menus') rslt[key] = rslt[key].concat(b[key]); else rslt[key] = b[key]; } } } return rslt; }; exports.HTMLDoc = function(_content, options){ options = _.extend({ extractEJS: false, noDuplicateAttributes: false, }, options); var _this = this; var whiteSpace = ' \t\n\r\v\f'; var JSHCMS_TAGS = [ 'jsh-for-item', 'jsh-for-item-variable', 'jsh-foreach-item', 'jsh-foreach-item-separator', 'jsh-foreach-item-start', 'jsh-foreach-item-end', 'jsh-foreach-item-skip', 'jsh-foreach-item-variable', 'jsh-foreach-item-index', 'jsh-group-items', 'jsh-group-items-into', 'jsh-group-items-by', 'jsh-group-items-separator', 'jsh-group-items-subgroup', 'jsh-group-items-index', 'jsh-template', 'cms-content-editor', 'cms-content-editor-type', 'cms-component-editor-remove-class', 'cms-component-editor-add-class', ]; this.origContent = _content; this.content = _content; this.offsets = []; //{ start, length } this.removed = []; //{ start, end } this.pendingTrim = []; //{ start, end, type { node, content, attr } } this.ejsScripts = []; this.nodes = []; this.extractEJS = function(){ var str = _this.content; var startIdx = str.indexOf('<%'); while(startIdx >= 0){ var endIdx = str.indexOf('%>', startIdx + 2); var nextStartIdx = str.indexOf('<%', startIdx + 2); if((endIdx < 0) || ((nextStartIdx >= 0) && (nextStartIdx < endIdx))) throw new Error('EJS missing closing "%>" tag at Line '+(_this.content.substr(0, startIdx).split('\n').length)); endIdx += 2; var scriptContent = str.substr(startIdx, endIdx - startIdx); var scriptType = 'standard'; if(scriptContent.substr(0,3)=='<%~') scriptType = 'containerSlurp'; //Extract string _this.ejsScripts.push({ start: startIdx, end: endIdx, content: scriptContent, scriptType: scriptType, container: null, //index //scriptType }); //Replace with spaces str = str.substr(0, startIdx) + Helper.pad('', ' ', endIdx - startIdx) + str.substr(endIdx); startIdx = nextStartIdx; } _this.content = str; }; //Find containers for each containerSlurp script this.findEJSContainers = function(){ var scripts = []; for(let i=0;i<_this.ejsScripts.length;i++){ var ejsScript = _this.ejsScripts[i]; if(ejsScript.container) continue; if(ejsScript.scriptType != 'containerSlurp') continue; //Check if script was removed var skip = false; for(let j=0;j<_this.removed.length;j++){ var removed = _this.removed[j]; if((removed.end - removed.start) <= 0) continue; if((removed.start < ejsScript.end) && (removed.end > ejsScript.start)){ skip = true; break; } } //Part of script was removed if(skip) continue; //Add to scripts array scripts.push(_.extend({type:'script',index:i}, ejsScript)); } if(!scripts.length) return; var allNodes = [].concat(scripts); _this.applyNodes([ { pred: function(node){ var nodeInfo = node.sourceCodeLocation; if(!nodeInfo) return; if(!nodeInfo.startTag) return; if(nodeInfo.startTag) allNodes.push({start: nodeInfo.startTag.startOffset, end: nodeInfo.startTag.endOffset, node: node, type: 'startTag'}); if(nodeInfo.endTag) allNodes.push({start: nodeInfo.endTag.startOffset, end: nodeInfo.endTag.endOffset, node: node, type: 'endTag'}); if(nodeInfo.startTag && nodeInfo.endTag) allNodes.push({start: nodeInfo.startTag.endOffset, end: nodeInfo.endTag.startOffset, node: node, type: 'nodeBody'}); if(nodeInfo.attrs){ for(var attrName in nodeInfo.attrs){ var attrInfo = nodeInfo.attrs[attrName]; allNodes.push({start: attrInfo.startOffset, end: attrInfo.endOffset, node: node, type: 'nodeAttr', attrName: attrName}); } } }, exec: function(node){ } }, ]); allNodes.sort(function(a,b){ if(a.start < b.start) return -1; if(a.start > b.start) return 1; if((a.type=='script') && (b.type!='script')) return 1; if((a.type!='script') && (b.type=='script')) return -1; if(a.end < b.end) return -1; if(a.end > b.end) return 1; return 0; }); for(let i=0;i<allNodes.length;i++){ let node = allNodes[i]; if(node.type=='script'){ for(let j=i-1;j>=0;j--){ var prevNode = allNodes[j]; if(node.end <= prevNode.end){ node.parent = prevNode; break; } } } } for(let i=0;i<scripts.length;i++){ var script = scripts[i]; try{ var container = { start: script.start, end: script.end }; var parentMatch = script.parent; //containerSlurp must be inside a parent element if(!parentMatch) throw new Error('EJS container slurp <%~ tag must be inside an HTML element'); let node = parentMatch.node; var nodeInfo = node.sourceCodeLocation; //Make sure parent node was not deleted if(node.removed) throw new Error('EJS container slurp <%~ tag should have been removed when node was removed'); if(parentMatch.type=='nodeAttr'){ if(!(parentMatch.attrName in nodeInfo.attrs)) throw new Error('EJS container slurp <%~ tag should have been removed when node attribute was removed'); var attrVal = _this.getAttr(node, parentMatch.attrName); //For nodeAttr, make sure attribute length matches if(attrVal != Helper.pad('', ' ', script.content.length)) throw new Error('One EJS container slurp <%~ tag must be the only text within the attribute value. Valid: <div class="<%~item.value%>"></div>. Invalid: <div class="static_class <%~item.value%>"></div>'); container = { start: nodeInfo.attrs[parentMatch.attrName].startOffset, end: nodeInfo.attrs[parentMatch.attrName].endOffset, offsetFrom: nodeInfo.attrs[parentMatch.attrName].offsetFrom, type: 'nodeAttr', }; } //For nodeBody, make sure content length matches if(parentMatch.type=='nodeBody'){ var nodeContent = _this.getNodeContent(node); //For nodeBody, make sure attribute length matches if(nodeContent != Helper.pad('', ' ', script.content.length)) throw new Error('One EJS container slurp <%~ tag must be the only text within the node content, Valid: <div><%~item.value%></div>. Invalid: <div>Additional Content <%~item.value%></div>'); container = { start: nodeInfo.startTag.startOffset, end: nodeInfo.endTag.endOffset, type: 'nodeBody', }; } //For startTag or endTag, show an error - cannot slurp if((parentMatch.type=='startTag')||(parentMatch.type=='endTag')){ throw new Error('EJS container slurp <%~ tags can only be used for attribute values or HTML element content'); } //Update script tag _this.ejsScripts[script.index].container = container; } catch(ex){ var errmsg = ex.message; var lines = _this.origContent.substr(0,script.start).split('\n'); var line = lines.length; var char = lines[lines.length-1].length; errmsg = 'EJS Error at line '+line+', char '+char+' - '+errmsg + ': ' + script.content; throw new Error(errmsg); } } }; this.restoreEJS = function(options){ options = _.extend({ containerSlurp: true }, options); if(options.containerSlurp) _this.findEJSContainers(); for(var i=0;i<_this.ejsScripts.length;i++){ var ejsScript = _this.ejsScripts[i]; var startIndex = _this.offsetIndex(ejsScript.start); var endIndex = startIndex + ejsScript.content.length; //Check if script was removed var skip = false; for(var j=0;j<_this.removed.length;j++){ var removed = _this.removed[j]; if((removed.end - removed.start) <= 0) continue; if((removed.start < ejsScript.end) && (removed.end > ejsScript.start)){ skip = true; break; } } //Part of script was removed if(skip) continue; //Add script back if(options.containerSlurp && (ejsScript.scriptType=='containerSlurp')){ //Container Slurp if(!ejsScript.container) throw new Error('EJS container slurp <%~ script container not found: '+ejsScript.content); /* Possible EJS script tags beginnings and endings: <% <%_ <%= <%- %> -%> _%> */ var expr = ''; var scriptContent = ejsScript.content; var firstFour = scriptContent.substr(0,4); var lastThree = scriptContent.substr(scriptContent.length-3,3); if(_.includes(['<%~-','<%~=','<%~_'], firstFour)){ scriptContent = '<%'+firstFour[3]+' '+scriptContent.substr(4); expr = scriptContent.substr(4); } else{ scriptContent = '<%=' + scriptContent.substr(3); expr = scriptContent.substr(3); } if(_.includes(['-%>','_%>'], lastThree)){ expr = expr.substr(0, expr.length - 3); } else { expr = expr.substr(0, expr.length - 2); } //Get container var container = ejsScript.container; var containerStartIndex = _this.offsetIndex(container.start, container.offsetFrom); var containerEndIndex = _this.offsetIndex(container.end, container.offsetFrom); var pre = '<% if(!isNullUndefinedEmpty('+expr+')){ %>'; var post = '<% } %>'; //Replace script _this.content = _this.content.substr(0, startIndex) + scriptContent + _this.content.substr(endIndex); //Slurp back spaces for attributes if(container.type == 'nodeAttr'){ while((containerStartIndex > 0) && whiteSpace.indexOf(_this.content[containerStartIndex-1])>=0) containerStartIndex--; } //Wrap script _this.content = _this.content.substr(0, containerStartIndex) + pre + _this.content.substr(containerStartIndex, containerEndIndex - containerStartIndex) + post + _this.content.substr(containerEndIndex); _this.offsets.push({ start: containerStartIndex, length: pre.length }); _this.offsets.push({ start: containerEndIndex + pre.length, length: post.length }); } else { //Standard Script _this.content = _this.content.substr(0, startIndex) + ejsScript.content + _this.content.substr(endIndex); } } _this.ejsScripts = []; }; this.trimRemoved = function(){ for(var i=0;i<_this.pendingTrim.length;i++){ var snip = _this.pendingTrim[i]; if(snip.type=='attr'){ var startOffset = _this.offsetIndex(snip.start); var endOffset = startOffset; startOffset--; while((startOffset>=0) && (whiteSpace.indexOf(_this.content[startOffset]) >= 0)) startOffset--; startOffset++; if(startOffset < endOffset){ _this.content = _this.content.substr(0, startOffset) + _this.content.substr(endOffset); _this.offsets.push({ start: startOffset, length: startOffset - endOffset }); _this.removed.push({ start: startOffset, end: endOffset }); } } } }; this.spliceContent = function(startIndex, endIndex, newContent){ newContent = newContent || ''; _this.content = _this.content.substr(0, startIndex) + newContent + _this.content.substr(endIndex); _this.offsets.push({ start: startIndex, length: (newContent.length + (startIndex - endIndex)) }); if(endIndex > startIndex){ _this.removed.push({ start: startIndex, end: endIndex }); _this.pendingTrim.push({ start: startIndex, end: endIndex, type: 'content' }); } }; this.offsetIndex = function(index, from){ if(!from) from = 0; for(var i=from;i<_this.offsets.length;i++){ if(index >= _this.offsets[i].start){ index += _this.offsets[i].length; if(index < 0) index = 0; } } return index; }; this.insertHtml = function(position, newContent){ newContent = newContent || ''; var startIndex = _this.offsetIndex(position); _this.content = _this.content.substr(0, startIndex) + newContent + _this.content.substr(startIndex); _this.offsets.push({ start: startIndex, length: (newContent.length) }); }; this.removeNode = function(node, desc){ if(node.removed) return; desc = desc || node.tagName; var nodeInfo = node.sourceCodeLocation; if(!nodeInfo) throw new Error('Error processing template: '+desc+' element was not fully parsed - missing sourceCodeLocation'); var startIndex = _this.offsetIndex(nodeInfo.startOffset, nodeInfo.offsetFrom); var endIndex = _this.offsetIndex(nodeInfo.endOffset, nodeInfo.offsetFrom); _this.content = _this.content.substr(0, startIndex) + _this.content.substr(endIndex); _this.offsets.push({ start: startIndex, length: (startIndex - endIndex) }); if(!nodeInfo.offsetFrom){ _this.removed.push({ start: nodeInfo.startOffset, end: nodeInfo.endOffset }); _this.pendingTrim.push({ start: nodeInfo.startOffset, end: nodeInfo.endOffset, type: 'node' }); } node.removed = true; }; this.getNodeContent = function(node, desc){ if(node.removed) return; desc = desc || node.tagName; var nodeInfo = node.sourceCodeLocation; if(!nodeInfo) throw new Error('Error processing template: '+desc+' element was not fully p