- /*
- Copyright (C) 2016 PencilBlue, LLC
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program 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 General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-
- //dependencies
- var util = require('../../../util.js');
- var async = require('async');
- var HtmlEncoder = require('htmlencode');
-
- module.exports = function(pb) {
-
- //pb dependencies
- var DAO = pb.DAO;
- var Localization = pb.Localization;
- var ClientJs = pb.ClientJs;
-
- /**
- * Renders a 1 or more pieces of content such as articles or pages
- * @class ContentViewLoader
- * @constructor
- * @param {Object} context
- * @param {TemplateService} context.ts
- * @param {Localization} context.ls
- * @param {Object} [context.contentSettings]
- * @param {Object} context.session
- * @param {ContentObjectService} context.service
- * @param {String} context.activeTheme
- * @param {CommentService} [context.commentService]
- * @param {object} context.siteObj
- */
- function ContentViewLoader(context) {
- this.ts = context.ts;
- this.ls = context.ls;
- this.req = context.req;
- this.contentSettings = context.contentSettings;
- this.session = context.session;
- this.service = context.service;
- this.site = context.site;
- this.siteObj = context.siteObj;
- this.hostname = context.hostname;
- this.onlyThisSite = context.onlyThisSite;
- this.activeTheme = context.activeTheme;
-
- /**
- * @property commentService
- * @type {CommentService}
- */
- this.commentService = context.commentService || new pb.CommentService(context);
- }
-
- /**
- *
- * @private
- * @static
- * @property DISPLAY_NONE_STYLE_ATTR
- * @type {String}
- */
- var DISPLAY_NONE_STYLE_ATTR = 'display:none;';
-
- /**
- *
- * @method renderSingle
- * @param {Object} content
- * @param {Object} options
- * @param {Function} cb
- */
- ContentViewLoader.prototype.renderSingle = function(content, options, cb) {
- this.render([content], options, cb);
- };
-
- /**
- *
- * @method render
- * @param {Array} contentArray
- * @param {Object} options
- * @param {Boolean} [options.useDefaultTemplate] Forces the default theme template to be selected
- * @param {Object} [options.topic] The topic represented by the collection of content to be rendered
- * @param {Object} [options.section] The section represented by the collection of content to be rendered
- * @param {Function} cb
- */
- ContentViewLoader.prototype.render = function(contentArray, options, cb) {
- var self = this;
-
- this.gatherData(contentArray, options, function(err, data) {
- if (util.isError(err)) {
- return cb(err);
- }
-
- self.setMetaInfo(data.meta, options);
- self.ts.registerLocal('current_url', self.req.url);
- self.ts.registerLocal('navigation', new pb.TemplateValue(data.nav.navigation, false));
- self.ts.registerLocal('account_buttons', new pb.TemplateValue(data.nav.accountButtons, false));
- self.ts.registerLocal('infinite_scroll', function(flag, cb) {
- self.onInfiniteScroll(contentArray, options, cb);
- });
- self.ts.registerLocal('page_name', function(flag, cb) {
- self.onPageName(contentArray, options, cb);
- });
- self.ts.registerLocal('angular', function(flag, cb) {
- self.onAngular(contentArray, options, cb);
- });
- self.ts.registerLocal('articles', function(flag, cb) {
- self.onContent(contentArray, options, cb);
- });
-
- self.getTemplate(contentArray, options, function(err, template) {
- if (util.isError(err)) {
- return cb(err);
- }
-
- self.ts.load(template, cb);
- });
- });
- };
-
- /**
- *
- * @method getTemplate
- * @param {Array|Object} content
- * @param {Object} options
- * @param {Boolean} [options.useDefaultTemplate] Forces the default theme template to be selected
- * @param {Object} [options.topic] The topic represented by the collection of content to be rendered
- * @param {Object} [options.section] The section represented by the collection of content to be rendered
- * @param {Function} cb
- */
- ContentViewLoader.prototype.getTemplate = function(content, options, cb) {
-
- //check if we should just use whatever default there is.
- //this could fall back to an active theme or the default pencilblue theme.
- if (options.useDefaultTemplate || util.isObject(options.topic) || util.isObject(options.section)) {
- return cb(null, this.getDefaultTemplatePath());
- }
-
- //now we are dealing with a single page or article. the template will be
- //judged based off the article's preference.
- if (util.isArray(content) && content.length > 0) {
- content = content[0];
- }
- var uidAndTemplate = content.template;
-
- //when no template is specified or is empty we no that the article has no
- //preference and we can fall back on the default (index). We depend on the
- //template service to determine who has priority based on the active theme
- //then defaulting back to pencilblue.
- if (!pb.validation.isNonEmptyStr(uidAndTemplate, true)) {
- var defautTemplatePath = this.getDefaultTemplatePath();
- pb.log.silly("ContentController: No template specified, defaulting to %s.", defautTemplatePath);
- return cb(null, defautTemplatePath);
- }
-
- //we now know that the template was specified. We have to split the value
- //to extract the intended theme and the template path
- var pieces = uidAndTemplate.split('|');
-
- //for backward compatibility we let the template service determine where to
- //find the template when no template is specified. This mostly catches the
- //default case of "index"
- if (pieces.length === 1) {
-
- pb.log.silly("ContentController: No theme specified, Template Service will delegate [%s]", pieces[0]);
- return cb(null, pieces[0]);
- }
- else if (pieces.length <= 0) {
-
- //shit's broke. This should never be the case but better safe than sorry
- return cb(new Error("The content's template property provided an invalid value of ["+content.template+']'), null);
- }
-
- //the theme is specified, we ensure that the theme is installed and
- //initialized otherwise we let the template service figure out how to
- //delegate.
- if (!pb.PluginService.isActivePlugin(pieces[0])) {
- pb.log.silly("ContentController: Theme [%s] is not active, Template Service will delegate [%s]", pieces[0], pieces[1]);
- return cb(null, pieces[1]);
- }
-
- //the theme is OK. We don't gaurantee that the template is on the disk but we can testify that it SHOULD. We set the
- //prioritized theme for the template service.
- pb.log.silly("ContentController: Prioritizing Theme [%s] for template [%s]", pieces[0], pieces[1]);
- this.ts.setTheme(pieces[0]);
- cb(null, pieces[1]);
- };
-
- /**
- *
- * @method getDefaultTemplatePath
- * @return {String}
- */
- ContentViewLoader.prototype.getDefaultTemplatePath = function() {
- return 'index';
- };
-
- /**
- *
- * @method onContent
- * @param {Array} contentArray
- * @param {Object} options
- * @param {Function} cb
- */
- ContentViewLoader.prototype.onContent = function(contentArray, options, cb) {
- var self = this;
- var limit = Math.min(this.contentSettings.articles_per_page, contentArray.length);
-
- var tasks = util.getTasks(contentArray, function(contentArray, i) {
- return function(callback) {
- if (i >= limit) {
- return callback(null, '');
- }
- self.renderContent(contentArray[i], options, callback);
- };
- });
- async.series(tasks, function(err, content) {
- cb(err, new pb.TemplateValue(content.join(''), false));
- });
- };
-
- /**
- *
- * @method gatherData
- * @param {Array} contentArray
- * @param {Object} options
- * @param {Function} cb
- */
- ContentViewLoader.prototype.gatherData = function(contentArray, options, cb) {
- var self = this;
- var tasks = {
-
- //navigation
- nav: function(callback) {
-
- var opts = {
- currUrl: self.req.url,
- session: self.session,
- ls: self.ls,
- site: self.site,
- activeTheme: self.activeTheme
- };
- var topMenuService = new pb.TopMenuService();
- topMenuService.getNavItems(opts, callback);
- },
-
- meta: function(callback) {
- self.getMetaInfo(contentArray, options, callback);
- },
-
- contentSettings: function(callback) {
- if (util.isObject(self.contentSettings)) {
- return callback(null, self.contentSettings);
- }
-
- var contentService = new pb.ContentService({site: self.site, onlyThisSite: self.onlyThisSite});
- contentService.getSettings(function(err, contentSettings) {
- self.contentSettings = contentSettings;
- callback(err, contentSettings);
- });
- }
- };
- async.parallel(tasks, cb);
- };
-
- /**
- *
- * @method onAngular
- * @param {Array} contentArray
- * @param {Object} options
- * @param {Function} cb
- */
- ContentViewLoader.prototype.onAngular = function(contentArray, options, cb) {
- var objects = {
- trustHTML: 'function(string){return $sce.trustAsHtml(string);}'
- };
- var angularData = pb.ClientJs.getAngularController(objects, ['ngSanitize']);
- cb(null, angularData);
- };
-
- /**
- *
- * @method onPageName
- * @param {Array} contentArray
- * @param {Object} options
- * @param {Function} cb
- */
- ContentViewLoader.prototype.onPageName = function(contentArray, options, cb) {
- var content = contentArray[0];
- if (!util.isObject(content)) {
- return cb(null, options.metaTitle || this.siteObj.displayName);
- }
-
- var name = '';
- if(util.isObject(options.section)) {
- name = options.section.name;
- }
- else if (util.isObject(options.topic)) {
- name = options.topic.name;
- }
- else if (contentArray.length === 1) {
- name = content.headline;
- }
- else {
- name = options.metaTitle || '';
- }
-
- cb(null, name ? name + ' | ' + this.siteObj.displayName : this.siteObj.displayName);
- };
-
- /**
- *
- * @method onInfiniteScroll
- * @param {Array} contentArray
- * @param {Object} options
- * @param {Function} cb
- */
- ContentViewLoader.prototype.onInfiniteScroll = function(contentArray, options, cb) {
- if(contentArray.length <= 1) {
- return cb(null, '');
- }
-
- var infiniteScrollScript = pb.ClientJs.includeJS('/js/infinite_article_scroll.js');
- if(util.isObject(options.section)) {
- infiniteScrollScript += pb.ClientJs.getJSTag('var infiniteScrollSection = "' + options.section[pb.DAO.getIdField()] + '";');
- }
- else if(util.isObject(options.topic)) {
- infiniteScrollScript += pb.ClientJs.getJSTag('var infiniteScrollTopic = "' + options.topic.topic[pb.DAO.getIdField()] + '";');
- }
-
- var val = new pb.TemplateValue(infiniteScrollScript, false);
- cb(null, val);
- };
-
- /**
- *
- * @method setMetaInfo
- * @param {Object} options
- */
- ContentViewLoader.prototype.setMetaInfo = function(meta, options) {
- this.ts.registerLocal('meta_keywords', meta.keywords);
- this.ts.registerLocal('meta_desc', options.metaDescription || meta.description);
- this.ts.registerLocal('meta_title', options.metaTitle || meta.title);
- this.ts.registerLocal('meta_thumbnail', meta.thumbnail || '');
- this.ts.registerLocal('meta_lang', options.metaLang || this.ls.language);
- };
-
- /**
- *
- * @method getMetaInfo
- * @param {Array} contentArray
- * @param {Object} options
- * @param {Function} cb
- */
- ContentViewLoader.prototype.getMetaInfo = function(contentArray, options, cb) {
- if (contentArray.length === 0) {
- return cb(null, {});
- }
- this.service.getMetaInfo(contentArray[0], cb);
- };
-
- /**
- *
- * @method renderContent
- * @param {Object} content
- * @param {Object} options
- * @param {Function} cb
- */
- ContentViewLoader.prototype.renderContent = function(content, options, cb) {
- var self = this;
-
- //set recurring params
- if (util.isNullOrUndefined(options.contentIndex)) {
- options.contentIndex = 0;
- }
-
- var isPage = this.service.getType() === 'page';
- var showByLine = this.contentSettings.display_bylines && !isPage;
- var showTimestamp = this.contentSettings.display_timestamp && !isPage;
- var ats = self.ts.getChildInstance();
- var contentUrlPrefix = '/' + this.service.getType() + '/';
- self.ts.reprocess = false;
- ats.registerLocal('article_permalink', function(flag, cb) {
- self.onContentPermalink(content, options, cb);
- });
- ats.registerLocal('article_headline', function(flag, cb) {
- self.onContentHeadline(content, options, cb);
- });
- ats.registerLocal('article_headline_nolink', content.headline);
- ats.registerLocal('article_subheading', ContentViewLoader.valOrEmpty(content.subheading));
- ats.registerLocal('article_subheading_display', ContentViewLoader.getDisplayAttr(content.subheading));
- ats.registerLocal('article_id', content[pb.DAO.getIdField()] + '');
- ats.registerLocal('article_index', options.contentIndex);
- ats.registerLocal('article_timestamp', showTimestamp && content.timestamp ? content.timestamp : '');
- ats.registerLocal('article_timestamp_display', ContentViewLoader.getDisplayAttr(showTimestamp));
- ats.registerLocal('article_layout', new pb.TemplateValue(content.layout, false));
- ats.registerLocal('article_url', content.url);
- ats.registerLocal('display_byline', ContentViewLoader.getDisplayAttr(showByLine));
- ats.registerLocal('author_photo', ContentViewLoader.valOrEmpty(content.author_photo));
- ats.registerLocal('author_photo_display', ContentViewLoader.getDisplayAttr(content.author_photo));
- ats.registerLocal('author_name', ContentViewLoader.valOrEmpty(content.author_name));
- ats.registerLocal('author_position', ContentViewLoader.valOrEmpty(content.author_position));
- ats.registerLocal('media_body_style', ContentViewLoader.valOrEmpty(content.media_body_style));
- ats.registerLocal('comments', function(flag, cb) {
- if (isPage || !pb.ArticleService.allowComments(self.contentSettings, content)) {
- return cb(null, '');
- }
-
- var ts = ats.getChildInstance();
- self.renderComments(content, ts, function(err, comments) {
- cb(err, new pb.TemplateValue(comments, false));
- });
- });
- ats.load(self.getDefaultContentTemplatePath(), cb);
-
- options.contentIndex++;
- };
-
- /**
- *
- * @method getDefaultContentTemplatePath
- * @return {String}
- */
- ContentViewLoader.prototype.getDefaultContentTemplatePath = function() {
- return 'elements/article';
- };
-
- /**
- *
- * @method renderComments
- * @param {Object} content
- * @param {TemplateService} ts
- * @param {Function} cb
- */
- ContentViewLoader.prototype.renderComments = function(content, ts, cb) {
- var self = this;
- var commentingUser = null;
- if(pb.security.isAuthenticated(this.session)) {
- commentingUser = this.commentService.getCommentingUser(this.session.authentication.user);
- }
-
- ts.registerLocal('user_photo', function(flag, cb) {
- self.onCommentingUserPhoto(content, commentingUser, cb);
- });
- ts.registerLocal('user_position', function(flag, cb) {
- self.onCommentingUserPosition(content, commentingUser, cb);
- });
- ts.registerLocal('user_name', commentingUser ? commentingUser.name : '');
- ts.registerLocal('display_submit', commentingUser ? 'block' : 'none');
- ts.registerLocal('display_login', commentingUser ? 'none' : 'block');
- ts.registerLocal('comments_length', util.isArray(content.comments) ? content.comments.length : 0);
- ts.registerLocal('individual_comments', function(flag, cb) {
- if (!util.isArray(content.comments) || content.comments.length === 0) {
- return cb(null, '');
- }
-
- var tasks = util.getTasks(content.comments, function(comments, i) {
- return function(callback) {
-
- var cts = ts.getChildInstance();
- self.renderComment(comments[i], cts, callback);
- };
- });
- async.parallel(tasks, function(err, results) {
- cb(err, new pb.TemplateValue(results.join(''), false));
- });
- });
- ts.load(self.getDefaultCommentsTemplatePath(), cb);
- };
-
- /**
- *
- * @method getDefaultCommentsTemplatePath
- * @return {String}
- */
- ContentViewLoader.prototype.getDefaultCommentsTemplatePath = function() {
- return 'elements/comments';
- };
-
- /**
- *
- * @method renderComment
- * @param {Object} comment
- * @param {TemplateService} cts
- * @param {Function} cb
- */
- ContentViewLoader.prototype.renderComment = function(comment, cts, cb) {
-
- cts.reprocess = false;
- cts.registerLocal('commenter_photo', comment.commenter_photo ? comment.commenter_photo : '');
- cts.registerLocal('display_photo', comment.commenter_photo ? 'block' : 'none');
- cts.registerLocal('commenter_name', comment.commenter_name);
- cts.registerLocal('commenter_position', comment.commenter_position ? ', ' + comment.commenter_position : '');
- cts.registerLocal('content', comment.content);
- cts.registerLocal('timestamp', comment.timestamp);
- cts.load(this.getDefaultCommentTemplatePath(), cb);
- };
-
- /**
- *
- * @method getDefaultCommentTemplatePath
- * @return {String}
- */
- ContentViewLoader.prototype.getDefaultCommentTemplatePath = function() {
- return 'elements/comments/comment';
- };
-
- /**
- *
- * @method onCommentingUserPhoto
- * @param {Object} content
- * @param {Object} commentingUser
- * @param {Function} cb
- */
- ContentViewLoader.prototype.onCommentingUserPhoto = function(content, commentingUser, cb) {
- var val = '';
- if (commentingUser) {
- val = commentingUser.photo || '';
- }
- cb(null, val);
- };
-
- /**
- *
- * @method onCommentingUserPosition
- * @param {Object} content
- * @param {Object} options
- * @param {Function} cb
- */
- ContentViewLoader.prototype.onCommentingUserPosition = function(content, commentingUser, cb) {
- var val = '';
- if (commentingUser && util.isArray(commentingUser.position) && commentingUser.position.length > 0) {
- val = ', ' + commentingUser.position;
- }
- cb(null, val);
- };
-
- /**
- *
- * @method onContentPermalink
- * @param {Object} content
- * @param {Object} options
- * @param {Function} cb
- */
- ContentViewLoader.prototype.onContentPermalink = function(content, options, cb) {
- cb(null, this.createContentPermalink(content));
- };
-
- /**
- *
- * @method onContentHeadline
- * @param {Object} content
- * @param {Object} options
- * @param {Function} cb
- */
- ContentViewLoader.prototype.onContentHeadline = function(content, options, cb) {
- var url = this.createContentPermalink(content);
- var val = new pb.TemplateValue('<a href="' + url + '">' + HtmlEncoder.htmlEncode(content.headline) + '</a>', false);
- cb(null, val);
- };
-
- /**
- *
- * @method createContentPermalink
- * @param {Object} content
- * @return {String}
- */
- ContentViewLoader.prototype.createContentPermalink = function(content) {
- var prefix = '/' + this.service.getType();
- return pb.UrlService.createSystemUrl(pb.UrlService.urlJoin(prefix, content.url), { hostname: this.hostname });
- };
-
- /**
- *
- * @static
- * @method getDisplayAttr
- * @param {*} val
- * @return {String}
- */
- ContentViewLoader.getDisplayAttr = function(val) {
- return val ? '' : DISPLAY_NONE_STYLE_ATTR;
- };
-
- /**
- * When passed a value it is evaluated as a boolean. If evaluated to TRUE
- * the value is returned, if FALSE empty string is returned
- * @static
- * @method valOrEmpty
- * @param {*} val
- * @return {*}
- */
- ContentViewLoader.valOrEmpty = function(val) {
- return val ? val : '';
- };
-
- return ContentViewLoader;
- };
-
-