jsharmony-cms
Version:
1,153 lines (1,050 loc) • 109 kB
JavaScript
/*
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