/************************************************************************ * This file is part of EspoCRM. * * EspoCRM – Open Source CRM application. * Copyright (C) 2014-2025 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko * Website: https://www.espocrm.com * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero 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 Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License version 3, * these Appropriate Legal Notices must retain the display of the "EspoCRM" word. ************************************************************************/ /** @module view-helper */ import {marked} from 'marked'; import DOMPurify from 'dompurify'; import Handlebars from 'handlebars'; /** * A view helper. */ class ViewHelper { constructor() { this._registerHandlebarsHelpers(); /** @private */ this.mdBeforeList = [ /*{ regex: /```\n?([\s\S]*?)```/g, value: (s, string) => { return '```\n' + string.replace(/\\\>/g, '>') + '```'; }, },*/ { // Also covers triple-backtick blocks. regex: /`([\s\S]*?)`/g, value: (s, string) => { // noinspection RegExpRedundantEscape return '`' + string.replace(/\\\ 99) { node.removeAttribute('start'); } if (node instanceof HTMLFormElement) { if (node.action) { node.removeAttribute('action'); } if (node.hasAttribute('method')) { node.removeAttribute('method'); } } if (node instanceof HTMLButtonElement) { if (node.type === 'submit') { node.type = 'button'; } } }); DOMPurify.addHook('afterSanitizeAttributes', function (node) { if (node instanceof HTMLAnchorElement) { const href = node.getAttribute('href'); if (href && !href.startsWith('#')) { node.setAttribute('rel', 'noopener noreferrer'); } if (node.targetBlank) { node.setAttribute('target', '_blank'); node.setAttribute('rel', 'noopener noreferrer'); } } }); DOMPurify.addHook('uponSanitizeAttribute', (node, data) => { if (data.attrName === 'style') { const style = data.attrValue .split(';') .map(s => s.trim()) .filter(rule => { const [property, value] = rule.split(':') .map(s => s.trim().toLowerCase()); if ( property === 'position' && ['absolute', 'fixed', 'sticky'].includes(value) ) { return false; } return true; }); data.attrValue = style.join('; '); } }); } /** * A layout manager. * * @type {module:layout-manager} */ layoutManager = null /** * A config. * * @type {module:models/settings} */ settings = null /** * A config. * * @type {module:models/settings} */ config = null /** * A current user. * * @type {module:models/user} */ user = null /** * A preferences. * * @type {module:models/preferences} */ preferences = null /** * An ACL manager. * * @type {module:acl-manager} */ acl = null /** * A model factory. * * @type {module:model-factory} */ modelFactory = null /** * A collection factory. * * @type {module:collection-factory} */ collectionFactory = null /** * A router. * * @type {module:router} */ router = null /** * A storage. * * @type {module:storage} */ storage = null /** * A session storage. * * @type {module:session-storage} */ sessionStorage = null /** * A date-time util. * * @type {module:date-time} */ dateTime = null /** * A language. * * @type {module:language} */ language = null /** * A metadata. * * @type {module:metadata} */ metadata = null /** * A field-manager util. * * @type {module:field-manager} */ fieldManager = null /** * A cache. * * @type {module:cache} */ cache = null /** * A theme manager. * * @type {module:theme-manager} */ themeManager = null /** * A web-socket manager. Null if not enabled. * * @type {?module:web-socket-manager} */ webSocketManager = null /** * A number util. * * @type {module:num-util} */ numberUtil = null /** * A page-title util. * * @type {module:page-title} */ pageTitle = null /** * A broadcast channel. * * @type {?module:broadcast-channel} */ broadcastChannel = null /** * A base path. * * @type {string} */ basePath = '' /** * Application parameters. * * @type {import('app-params').default|null} */ appParams = null /** * @private */ _registerHandlebarsHelpers() { Handlebars.registerHelper('img', img => { return new Handlebars.SafeString(`img`); }); Handlebars.registerHelper('prop', (object, name) => { if (object === undefined) { console.warn(`Undefined value passed to 'prop' helper.`); return undefined; } if (name in object) { return object[name]; } return undefined; }); Handlebars.registerHelper('var', (name, context, options) => { if (typeof context === 'undefined') { return null; } let contents = context[name]; if (options.hash.trim) { contents = contents.trim(); } return new Handlebars.SafeString(contents); }); Handlebars.registerHelper('concat', function (left, right) { return left + right; }); Handlebars.registerHelper('ifEqual', function (left, right, options) { // noinspection EqualityComparisonWithCoercionJS if (left == right) { return options.fn(this); } return options.inverse(this); }); Handlebars.registerHelper('ifNotEqual', function (left, right, options) { // noinspection EqualityComparisonWithCoercionJS if (left != right) { return options.fn(this); } return options.inverse(this); }); Handlebars.registerHelper('ifPropEquals', function (object, property, value, options) { // noinspection EqualityComparisonWithCoercionJS if (object[property] == value) { return options.fn(this); } return options.inverse(this); }); Handlebars.registerHelper('ifAttrEquals', function (model, attr, value, options) { // noinspection EqualityComparisonWithCoercionJS if (model.get(attr) == value) { return options.fn(this); } return options.inverse(this); }); Handlebars.registerHelper('ifAttrNotEmpty', function (model, attr, options) { const value = model.get(attr); if (value !== null && typeof value !== 'undefined') { return options.fn(this); } return options.inverse(this); }); Handlebars.registerHelper('get', (model, name) => model.get(name)); Handlebars.registerHelper('length', arr => arr.length); Handlebars.registerHelper('translate', (name, options) => { const scope = options.hash.scope || null; const category = options.hash.category || null; if (name === 'null') { return ''; } return this.language.translate(name, category, scope); }); Handlebars.registerHelper('dropdownItem', (name, options) => { const scope = options.hash.scope || null; const label = options.hash.label; const labelTranslation = options.hash.labelTranslation; const data = options.hash.data; const hidden = options.hash.hidden; const disabled = options.hash.disabled; const title = options.hash.title; const link = options.hash.link; const action = options.hash.action || name; const iconHtml = options.hash.iconHtml; const iconClass = options.hash.iconClass; let html = options.hash.html || options.hash.text || ( labelTranslation ? this.language.translatePath(labelTranslation) : this.language.translate(label, 'labels', scope) ); if (!options.hash.html) { html = this.escapeString(html); } if (iconHtml) { html = iconHtml + ' ' + html; } else if (iconClass) { const iconHtml = $('').addClass(iconClass).get(0).outerHTML; html = iconHtml + ' ' + html; } const $li = $('
  • ') .addClass(hidden ? 'hidden' : '') .addClass(disabled ? 'disabled' : ''); const $a = $('') .attr('role', 'button') .attr('tabindex', '0') .attr('data-name', name) .addClass(options.hash.className || '') .addClass('action') .html(html); if (action) { $a.attr('data-action', action); } $li.append($a); link ? $a.attr('href', link) : $a.attr('role', 'button'); if (data) { for (const key in data) { $a.attr('data-' + Espo.Utils.camelCaseToHyphen(key), data[key]); } } if (disabled) { $li.attr('disabled', 'disabled'); } if (title) { $a.attr('title', title); } return new Handlebars.SafeString($li.get(0).outerHTML); }); Handlebars.registerHelper('button', (name, options) => { const style = options.hash.style || 'default'; const scope = options.hash.scope || null; const label = options.hash.label || name; const labelTranslation = options.hash.labelTranslation; const link = options.hash.link; const iconHtml = options.hash.iconHtml; const iconClass = options.hash.iconClass; let html = options.hash.html || options.hash.text || ( labelTranslation ? this.language.translatePath(labelTranslation) : this.language.translate(label, 'labels', scope) ); if (!options.hash.html) { html = this.escapeString(html); } if (iconHtml) { html = iconHtml + ' ' + '' + html + ''; } else if (iconClass) { const iconHtml = $('').addClass(iconClass).get(0).outerHTML; html = iconHtml + ' ' + '' + html + ''; } const tag = link ? '' : '