jsharmony-cms
Version:
422 lines (375 loc) • 17.7 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 _ = require('lodash');
var path = require('path');
var async = require('async');
module.exports = exports = function(module, funcs){
var exports = {};
var _t = module._t;
function parseSearchQuery(str){
str = (str || '').toString().trim().toLowerCase();
if(!str) return null;
var keywords = [];
var quotedKeywords = str.split('"');
for(var i=0;i<quotedKeywords.length;i++){
var quotedKeyword = quotedKeywords[i].trim();
if(!quotedKeyword) continue;
if((i%2)==1){
keywords.push(quotedKeyword.trim());
}
else {
var subKeywords = quotedKeyword.split(/[\s,]+/);
_.each(subKeywords, function(subKeyword){ subKeyword = subKeyword.trim(); if(subKeyword) keywords.push(subKeyword); });
}
}
return keywords;
}
exports.searchContent = function(query, str){
var score = [];
if(!str) return null;
str = str.toString().toLowerCase();
var hasScore = false;
for(var i=0;i<query.length;i++){
var keyword = query[i];
var keywordScore = 0;
var idx = -1;
while((idx = str.indexOf(keyword, idx+1)) >= 0) keywordScore++;
score.push(keywordScore * keyword.length);
if(keywordScore > 0) hasScore = true;
}
if(!hasScore) return null;
return score;
};
exports.mergeSearchScore = function(scoreSrc, scoreDest, factor){
if(typeof factor == 'undefined') factor = 1;
var rslt = scoreSrc;
if(!scoreSrc) rslt = [];
for(var i=0;i<scoreDest.length;i++){
if(i >= rslt.length) rslt.push(0);
rslt[i] += scoreDest[i] * factor;
}
return rslt;
};
exports.search = function(req, res, next){
var verb = req.method.toLowerCase();
if (!req.body) req.body = {};
var Q = req.query;
var P = {};
var jsh = module.jsh;
var appsrv = jsh.AppSrv;
var cms = module;
var dbtypes = appsrv.DB.types;
var model = jsh.getModel(req, module.namespace + 'Page_Search');
if (!Helper.hasModelAction(req, model, 'B')) return Helper.GenError(req, res, -11, _t('Invalid Model Access'));
//Validate parameters
if (!appsrv.ParamCheck('Q', Q, ['&query','|itemType'])) return Helper.GenError(req, res, -4, 'Invalid Parameters');
if (!appsrv.ParamCheck('P', P, [])) return Helper.GenError(req, res, -4, 'Invalid Parameters');
var query = parseSearchQuery(Q.query);
if(!query || !query.length) return Helper.GenError(req, res, -4, 'Invalid Parameters');
var itemType = Q.itemType || null;
if (verb == 'get') {
jsh.AppSrv.ExecRow(req._DBContext, cms.funcs.replaceSchema('select {schema}.my_current_site_id() site_id, {schema}.my_current_branch_id() branch_id'), [], {}, function (err, rslt) {
if(err) return Helper.GenError(req, res, -99999, err.toString());
if (!rslt || !rslt.length || !rslt[0] || !rslt[0].site_id) return Helper.GenError(req, res, -1, 'Please check out a site');
if (!rslt[0].branch_id) return Helper.GenError(req, res, -1, 'Please check out a revision');
var site_id = rslt[0].site_id;
var branch_id = rslt[0].branch_id;
funcs.validateBranchAccess(req, res, branch_id, 'R%', undefined, function(){
var searchData = {
_DBContext: req._DBContext,
query: query,
site_id: site_id,
branch_id: branch_id,
media_keys: {},
page_keys: {},
results: [],
site_config: {},
};
var sql_ptypes = [dbtypes.BigInt];
var sql_params = { 'branch_id': branch_id };
async.waterfall([
//Get site_config
function(search_cb){
funcs.getSiteConfig(req._DBContext, site_id, { continueOnConfigError: true }, function(err, siteConfig){
if(err) return search_cb(err);
searchData.site_config = siteConfig || {};
return search_cb();
});
},
//Search pages
function(search_cb){
async.eachOfSeries(cms.BranchItems, function(branch_item, branch_item_type, search_item_cb){
if(!branch_item.search) return search_item_cb();
Helper.execif(branch_item.search.columns,
function(f){
var sql = 'select branch_{item}.{item}_key,branch_{item}.{item}_id,';
sql += _.map(branch_item.search.columns || [], function(colname){ return '{item}.'+colname; }).join(',');
sql += ' from {tbl_branch_item} branch_{item}';
sql += ' left outer join {tbl_item} {item} on {item}.{item}_id = branch_{item}.{item}_id';
sql += ' where branch_{item}.branch_id=@branch_id and branch_{item}.{item}_id is not null';
appsrv.ExecRecordset(req._DBContext, cms.applyBranchItemSQL(branch_item_type, sql), sql_ptypes, sql_params, function (err, rslt) {
if (err != null) { err.sql = sql; err.model = model; appsrv.AppDBError(req, res, err); return; }
if(!rslt || !rslt.length || !rslt[0]){ return search_cb(new Error('Error loading revision data')); }
searchData[branch_item_type] = rslt[0];
return f();
});
},
search_item_cb
);
}, search_cb);
},
function(search_cb){
//Search items
async.eachOfSeries(cms.BranchItems, function(branch_item, branch_item_type, search_item_cb){
if(!branch_item.search) return search_item_cb();
Helper.execif(branch_item.search.onBeforeSearch,
function(f){
branch_item.search.onBeforeSearch(searchData[branch_item_type], searchData, f);
},
search_item_cb
);
}, search_cb);
},
function(search_cb){
//Search items
async.eachOfSeries(cms.BranchItems, function(branch_item, branch_item_type, search_item_cb){
if(itemType && (itemType != branch_item_type)) return search_item_cb();
if(!branch_item.search) return search_item_cb();
Helper.execif(branch_item.search.onSearch,
function(f){
branch_item.search.onSearch(searchData[branch_item_type], searchData, f);
},
search_item_cb
);
}, search_cb);
},
], function(err){
if(err) return Helper.GenError(req, res, -99999, err.toString());
for(var i=0;i<searchData.results.length;i++){
searchData.results[i].id = i+1;
searchData.results[i].score = Math.round(Math.log10(searchData.results[i].score)*10000)/100000;
}
res.end(JSON.stringify({ '_success': 1, results: searchData.results }));
});
});
});
return;
}
return Helper.GenError(req, res, -4, 'Invalid Operation');
};
exports.search_getPages = function(searchItems, searchData, callback){
var publish_params = {
site_default_page_filename: '',
page_subfolder: '',
media_subfolder: '',
url_prefix: '/',
};
_.each(searchItems, function(page){
var page_cmspath = '';
try{
var relativePath = funcs.getPageRelativePath(page, publish_params);
if(!relativePath) return;
page_cmspath = publish_params.url_prefix + publish_params.page_subfolder + relativePath;
if(!Helper.isNullUndefined(publish_params.url_prefix_page_override)){ page_cmspath = publish_params.url_prefix_page_override + relativePath; }
}
catch(ex){ /* Do nothing */ }
if(page_cmspath){
searchData.page_keys[page.page_key] = page_cmspath;
if(path.basename(page_cmspath)==publish_params.site_default_page_filename){
var base_page_dir = publish_params.url_prefix + publish_params.page_subfolder;
if(!Helper.isNullUndefined(publish_params.url_prefix_page_override)){ base_page_dir = publish_params.url_prefix_page_override; }
var page_dir = ((page_cmspath==base_page_dir+publish_params.site_default_page_filename) ? base_page_dir : path.dirname(page_cmspath)+'/');
if(!page_dir) page_dir = './';
searchData.page_keys[page.page_key] = page_dir;
}
}
});
return callback();
};
exports.search_getMedia = function(searchItems, searchData, callback){
var publish_params = {
site_default_page_filename: '',
media_subfolder: '',
url_prefix: '/',
};
for(var media_id in searchItems){
var media = searchItems[media_id];
var media_urlpath = '';
try{
var relativePath = funcs.getMediaRelativePath(media, publish_params);
if(!relativePath) return callback(new Error('Media has no path: '+media.media_key));
media_urlpath = publish_params.url_prefix + publish_params.media_subfolder + relativePath;
if(!Helper.isNullUndefined(publish_params.url_prefix_media_override)){ media_urlpath = publish_params.url_prefix_media_override + relativePath; }
}
catch(ex){ /* Do nothing */ }
if(media_urlpath) searchData.media_keys[media.media_key] = media_urlpath;
}
return callback();
};
exports.search_pages = function(searchItems, searchData, callback){
async.eachOf(searchItems, function(page, page_id, page_cb){
if(page.page_is_folder) return page_cb();
//Calculate page metadata score
var score = {};
_.each(['page_path','page_title','page_tags'], function(key){
var itemScore = exports.searchContent(searchData.query, page[key]);
if(itemScore) score[key] = itemScore;
});
Helper.execif(page.page_file_id,
function(done){
funcs.getClientPage(searchData._DBContext, page, null, searchData.site_id, { includeAllExtraContent: true, ignoreInvalidPageTemplate: true, pageTemplates: false }, function(err, clientPage){
if(err) return page_cb(err);
if(!clientPage || !clientPage.page) return done();
//Replace URLs
function replaceURLs(content, scoreKey){
//Search Component Attributes
function searchComponentAttribute(val){
var itemScore = exports.searchContent(searchData.query, val);
if(itemScore){
if(scoreKey.indexOf('content.')==0){
if(!score.content) score.content = [];
score.content[key] = exports.mergeSearchScore(score.content[key], itemScore, 0.5);
}
else {
score[key] = exports.mergeSearchScore(score[key], itemScore, 0.5);
}
}
return content;
}
var rslt = funcs.replaceBranchURLs(content, _.extend({ replaceComponents: true }, {
getMediaURL: function(media_key, thumbnail_id){
if(!(media_key in searchData.media_keys)) return '';
if(thumbnail_id) return funcs.appendThumbnail(searchData.media_keys[media_key], thumbnail_id, searchData.site_config.media_thumbnails && searchData.site_config.media_thumbnails[thumbnail_id]);
return searchData.media_keys[media_key];
},
getPageURL: function(page_key){
if(!(page_key in searchData.page_keys)) return '';
return searchData.page_keys[page_key];
},
onComponentData: searchComponentAttribute,
onComponentProperties: searchComponentAttribute,
searchData: searchData
}));
return rslt;
}
var contentPage = clientPage.page;
if(contentPage.content) for(var key in contentPage.content){ contentPage.content[key] = replaceURLs(contentPage.content[key], 'content.'+key); }
_.each(['css','header','footer'], function(key){
if(contentPage[key]) contentPage[key] = replaceURLs(contentPage[key], key);
});
//Calculate page content score
_.each(clientPage.page.content, function(val, key){
var itemScore = exports.searchContent(searchData.query, val);
if(itemScore){
if(!score.content) score.content = [];
score.content[key] = exports.mergeSearchScore(score.content[key], itemScore);
}
});
if(clientPage.page.seo){
_.each(['title','keywords','metadesc','canonical_url'], function(key){
if(!clientPage.page.seo[key]) return;
var itemScore = exports.searchContent(searchData.query, clientPage.page.seo[key]);
if(itemScore) score['seo_'+key] = itemScore;
});
}
_.each(['title','header','footer','css'], function(key){
if(!clientPage.page[key]) return;
var itemScore = exports.searchContent(searchData.query, clientPage.page[key]);
if(itemScore) score[key] = exports.mergeSearchScore(score[key], itemScore);
});
_.each(clientPage.page.properties, function(val, key){
var itemScore = exports.searchContent(searchData.query, val);
if(itemScore){
if(!score.properties) score.properties = [];
score.properties[key] = itemScore;
}
});
return done();
});
},
function(){
//Calculate weighted score
var weightedScore = [];
for(var key in score){
if(key=='content'){
for(var subkey in score.content){
exports.mergeSearchScore(weightedScore, score.content[subkey]);
}
}
else if(key=='properties'){
for(let subkey in score.properties){
exports.mergeSearchScore(weightedScore, score.properties[subkey]);
}
}
else if(_.includes(['header','footer','css'], key)){ exports.mergeSearchScore(weightedScore, score[key], 0.5); }
else exports.mergeSearchScore(weightedScore, score[key], 2);
}
var finalScore = exports.collapseSearchScore(searchData.query, weightedScore);
if(finalScore > 0){
searchData.results.push({
type: 'page',
type_desc: 'Page',
path: page.page_path,
title: page.page_title,
data: JSON.stringify(_.pick(page,['page_key','page_template_id','page_template_path','page_filename'])),
score: finalScore,
});
}
return page_cb(null);
}
);
}, callback);
};
exports.collapseSearchScore = function(query, mergedScore){
var finalScore = 0;
var missingScore = false;
for(var i=0;i<mergedScore.length;i++){
if(mergedScore[i]==0) missingScore = true;
finalScore += mergedScore[i];
}
if(missingScore || (mergedScore.length < query.length)) return 0;
return finalScore;
};
exports.search_media = function(searchItems, searchData, callback){
async.eachOf(searchItems, function(media, media_id, media_cb){
if(media.media_is_folder) return media_cb();
//Calculate media score
var score = {};
_.each(['media_path','media_ext','media_width','media_height','media_desc','media_tags','media_type'], function(key){
var itemScore = exports.searchContent(searchData.query, media[key]);
if(itemScore) score[key] = itemScore;
});
//Calculate weighted score
var weightedScore = [];
for(var key in score){
exports.mergeSearchScore(weightedScore, score[key], 3);
}
var finalScore = exports.collapseSearchScore(searchData.query, weightedScore);
if(finalScore > 0){
searchData.results.push({
type: 'media',
type_desc: 'Media',
path: media.media_path,
title: media.media_desc,
data: JSON.stringify(_.pick(media,['media_key','media_ext','media_width','media_height'])),
score: finalScore,
});
}
return media_cb(null);
}, callback);
};
return exports;
};