Skip to content
Snippets Groups Projects
process.js 42.3 KiB
Newer Older
S.Listl's avatar
S.Listl committed
import("DocxTemplater_lib");
Johannes Hörmann's avatar
Johannes Hörmann committed
import("Binary_lib");
import("Communication_lib");
import("system.neon");
import("Employee_lib");
import("KeywordRegistry_basic");
import("Document_lib");
import("KeywordData_lib");
import("Sql_lib");
import("Address_lib");
import("system.process");
import("system.vars");
import("system.db");
import("system.util");
import("system.pack");
import("system.fileIO");
import("system.translate");
import("system.datetime");
import("system.question");
Johannes Hörmann's avatar
Johannes Hörmann committed
import("system.text");
import("system.mail");
import("Keyword_lib");
import("Placeholder_lib");
import("Email_lib");
import("MimeType_lib");
Johannes Hörmann's avatar
Johannes Hörmann committed

/**
 * Object for working with document templates, holds the content and type of the template.
 * Provides functions to replace placeholders in the content.
 * 
 * @param {String} pTemplateContent content, as base64 string (except for DocumentTemplate.types.PLAIN, then it's a normal string)
 * @param {String} pType type of the template, use the DocumentTemplate.types constants here
 * @param {String} [pFilename=undefined] file name of the template 
 * @param {Boolean} [pResolveSubtemplates=false] if true subtemplates are resolved (if the type fits)
 * @param {String} [pTemplateId=undefined] Provide it, if you have it, because this enables the template to load attachments associated by this ID
Johannes Hörmann's avatar
Johannes Hörmann committed
 * @param {String} [pMimeType=undefined] mimetype of the content. Only an additional information. Not mandatory.
Johannes Hörmann's avatar
Johannes Hörmann committed
 */
function DocumentTemplate (pTemplateContent, pType, pFilename, pResolveSubtemplates, pTemplateId, pMimeType)
Johannes Hörmann's avatar
Johannes Hörmann committed
{
    this.content = pTemplateContent;
    this.type = pType;
    this.options = DocumentTemplate.types.getDefaultTypeOptions(pType);
Johannes Hörmann's avatar
Johannes Hörmann committed
    this.filename = pFilename;
    this.templateId = pTemplateId;
Johannes Hörmann's avatar
Johannes Hörmann committed
    this.mimeType = pMimeType;
    this._attachmentCache = null;
    this._subtemplateResolvedContent = null;
    //cache used for .toString
    this._stringCache = {
        onlyContent : null,
        withSubtemplatesResolved : null
    };
Johannes Hörmann's avatar
Johannes Hörmann committed
    
    if (pResolveSubtemplates)
        this._resolveSubtemplates();
Johannes Hörmann's avatar
Johannes Hörmann committed
}

/**
 * @return {String} the text of the content
 */
DocumentTemplate.prototype.toString = function (pWithSubtemplates)
Johannes Hörmann's avatar
Johannes Hörmann committed
{    
    var stringCachePosition = pWithSubtemplates ? "withSubtemplatesResolved" : "onlyContent";
    if (this._stringCache[stringCachePosition] == null)
Johannes Hörmann's avatar
Johannes Hörmann committed
    {
        var content = pWithSubtemplates && this._subtemplateResolvedContent || this.content;
Johannes Hörmann's avatar
Johannes Hörmann committed
        if (this.type == DocumentTemplate.types.PLAIN)
            this._stringCache[stringCachePosition] = content;
Johannes Hörmann's avatar
Johannes Hörmann committed
        else
            this._stringCache[stringCachePosition] = text.parseDocument(content);
Johannes Hörmann's avatar
Johannes Hörmann committed
    }
    return this._stringCache[stringCachePosition];
/**
 * Defines options for the DocumentTemplate, the given options will be appended to the existing options and options that 
 * already exist will be overridden. The options that can be used may vary depending on the type of the DocumentTemplate, 
 * that's why they are wrapped inside an object.
 * 
 * @param {Object} pOptions The options to set for the object. Some options only affect special types, for example:
 *      <ul>
 *          <li>base64 (boolean): If the replaced content should be base 64 encoded (does not work for docx and odt, these are always encoded)</li>
 *          <li>onlyBody (boolean): If set to true, only use the body of an eml (option only for eml)</li>
 *      </ul>
 * @return {DocumentTemplate} current object
 */
DocumentTemplate.prototype.setOptions = function (pOptions)
{
    Object.assign(this.options, pOptions);
    return this;
}

/**
 * resolves sub-template placeholders
 */
DocumentTemplate.prototype._resolveSubtemplates = function ()
Johannes Hörmann's avatar
Johannes Hörmann committed
{
    // currently we support only txt and html as others would need special caution.
    if (this.content != null && (this.type == DocumentTemplate.types.TXT || this.type == DocumentTemplate.types.HTML))
Johannes Hörmann's avatar
Johannes Hörmann committed
    {
        var replacedContent = util.decodeBase64String(this.content);
Johannes Hörmann's avatar
Johannes Hörmann committed
        
        var templates = [];
        // then load the possible replacement names
        
        if (this.type == DocumentTemplate.types.HTML)
Johannes Hörmann's avatar
Johannes Hörmann committed
        {
S.Listl's avatar
S.Listl committed
            templates = templates.concat(newSelect("DOCUMENTTEMPLATEID, REPLACEMENTNAME").from("DOCUMENTTEMPLATE")
                .where("DOCUMENTTEMPLATE.KIND", $KeywordRegistry.documentTemplateType$textModular())
                .and("DOCUMENTTEMPLATE.CLASSIFICATION", $KeywordRegistry.documentTemplateTypeCategory$htmlTemplate())
                .table());
        var alias = SqlUtils.getBinariesAlias();
Johannes Hörmann's avatar
Johannes Hörmann committed

        // We use callbacks which are called by pString.replace
        placeholders = templates.map(function(pTemplate) {
            // add function for each placeholder so that the db.getBinaryContent is only called, if the placeholder is replaced.
            return [pTemplate[1], function(matched, index, original) {
                var templateDocument = db.getBinaryMetadata("DOCUMENTTEMPLATE", "DOCUMENT", pTemplate[0], false, alias, null);
                var binaryId = templateDocument[0][db.BINARY_ID];
                return util.decodeBase64String(db.getBinaryContent(binaryId, alias));
            }];
        });

        // Note: some embedded templates in embedded templates may be replaced, but this is NOT SUPPORTED, as it only works if the second template is not already replaced at that time.
        placeholders.forEach(function(pPlaceholder) 
        {
            replacedContent = StringUtils.replaceAll(replacedContent, PlaceholderUtils.formatPlaceholder(pPlaceholder[0]), pPlaceholder[1], "g");
Johannes Hörmann's avatar
Johannes Hörmann committed
        }, this);
        
        this._subtemplatedContent = util.encodeBase64String(replacedContent);
/**
 * @return {DocumentTemmplate[]} if the templateId exists, it returns all attachments associated by the id as DocumentTemplate array else it just returns an empty array.
 */
DocumentTemplate.prototype.getAttachments = function ()
{
    if (!this.templateId && this._attachmentCache == null)
        return [];
    }
    
    if (this._attachmentCache == null)
    {
S.Listl's avatar
S.Listl committed
        var attachmentIds = newSelect("DOCUMENTTEMPLATE_ID_CHILD")
            .from("DOCUMENTTEMPLATELINK")
            .where("DOCUMENTTEMPLATELINK.DOCUMENTTEMPLATE_ID_PARENT", this.templateId)
            .arrayColumn();
        this._attachmentCache = attachmentIds.map(function(pAttachmentId)
        {
            return DocumentTemplateUtils.getTemplate(pAttachmentId, false);
        });
    return this._attachmentCache;
}

/**
 * @param {DocumentTemplate[]} pAttachments array of templates which should be used as attachment
 */
DocumentTemplate.prototype.setAttachments = function (pAttachments)
{
    this._attachmentCache = pAttachments;
Johannes Hörmann's avatar
Johannes Hörmann committed
/**
 * The types a DocumentTemplate can have. Depending on the type,
 * the correct method for replacing the placeholders can be chosen
 * 
 * @enum {String}
 */
DocumentTemplate.types = {
    TXT : "txt",
    HTML : "html",
    EML : "eml",
    ODT : "odt",
    DOCX : "docx",
Johannes Hörmann's avatar
Johannes Hörmann committed
    DOCM : "docm",
Johannes Hörmann's avatar
Johannes Hörmann committed
    PLAIN : "plain", //for simple strings
    
    /**
     * chooses the type depending on the file extension
     */
    fromFileExtension : function (pFileExtension)
    {
        switch (pFileExtension)
        {
            case "html":
            case "htm":
                return this.HTML;
            case "eml":
                return this.EML;
            case "odt":
                return this.ODT;
            case "docx":
                return this.DOCX;
Johannes Hörmann's avatar
Johannes Hörmann committed
            case "docm":
                return this.DOCM;
Johannes Hörmann's avatar
Johannes Hörmann committed
            case "txt":
                return this.TXT;
            case "msg":
            case "oft":
            default:
                return null;
        }
    },
    /**
     * chooses the type depending on the mime type
     */
    fromMimeType : function (pMimetype)
    {
        switch (pMimetype)
        {
            case MimeTypes.HTML():
                return this.HTML;
            case MimeTypes.EML():
                return this.EML;
            case MimeTypes.ODT():
                return this.ODT;
            case MimeTypes.DOCX():
                return this.DOCX;
Johannes Hörmann's avatar
Johannes Hörmann committed
            case MimeTypes.DOCM():
                return this.DOCM;
Johannes Hörmann's avatar
Johannes Hörmann committed
            case MimeTypes.TXT():
                return this.TXT;
            case MimeTypes.MSG():
            default:
                return null;
        }
    },
    /**
     * chooses the type depending on the extension in the metadata. If the extension doesn't work, try mimetype
     * @param {String[]} pBinaryMetadata the binary metadata from system.db
     */
    fromBinaryMetadata : function (pBinaryMetadata)
    {
        let filename = pBinaryMetadata[db.BINARY_FILENAME].split(".");
        let type = DocumentTemplate.types.fromFileExtension(filename[filename.length - 1]);
        if (type == null)
            type = DocumentTemplate.types.fromMimeType(pBinaryMetadata[db.BINARY_MIMETYPE]);
        
        return type;
    },
    /**
     * Returns the default options for the given type.
     * 
     * @param {String} pType the type
     * @return {Object} object containing the default options
     */
    getDefaultTypeOptions : function (pType)
    {
        switch (pType)
        {
            case this.EML:
                return {
                    base64 : false,
                    onlyBody : false,
                    placeholderRegExp : /\{\s*(=\r?\n)?@(.(?!{@)|(\r?\n))+?@\s*(=\r?\n)?\}/gi,
                    parsePlaceholderFn : function (pPlaceholder)
                    {
                        return pPlaceholder.replace(/\s*(=\r?\n)?/, "");
                    }
                };
            case this.TXT:
            case this.HTML:
                return {
                    base64 : false
                };
            case this.DOCX:
            case this.DOCM:
                return {
                    startDelimiter : "{@",
                    endDelimiter : "@}"
                };
            case this.ODT:
Johannes Hörmann's avatar
Johannes Hörmann committed
    }
};

/**
 * Loads the content of a document template and creates a new DocumentTemplate object with that.
 * 
 * @param {String} pAssignmentRowId id of the assignment (in most cases the document template id)
 * @param {String} [pAssignmentTable="DOCUMENTTEMPLATE"] the LOB assignment table
 * @param {Boolean} [pResolveSubtemplates=true] if true subtemplates are resolved (if the type fits)
 * @return {DocumentTemplate} template object
 */
DocumentTemplate.loadTemplate = function (pAssignmentRowId, pAssignmentTable, pResolveSubtemplates)
    var alias = SqlUtils.getBinariesAlias();
Johannes Hörmann's avatar
Johannes Hörmann committed
    if (!pAssignmentTable)
        pAssignmentTable = "DOCUMENTTEMPLATE";
    
    // if the templateId is accessable, use it, to enable the templateto load attachments
    var templateId;
    if (pAssignmentTable == "DOCUMENTTEMPLATE")
        templateId = pAssignmentRowId;
    
    if (templateId)
    {
        var template = DocumentTemplateUtils.getTemplate(templateId, pResolveSubtemplates);
        if (template == null)
        {
            if(vars.getString("$sys.isserver") == "true")
                throw new Error("loadTemplate: File from this template could not be found anymore. "
                                + "Please go to the template and upload a new file. TemplateID: '" + templateId + "'")
            else
                question.showMessage(translate.text("File from this template could not be found anymore. "
                                + "Please go to the template and upload a new file."), question.ERROR, translate.text("Error"));
            
    var templateDocument;
    if(pAssignmentTable == "SERIALLETTER")
    {
        templateDocument = db.getBinaryMetadata(pAssignmentTable, "SERIALLETTERFILE", pAssignmentRowId, false, alias, null);
    }
    else
    {
        templateDocument = db.getBinaryMetadata(pAssignmentTable, "DOCUMENT", pAssignmentRowId, false, alias, null);
    }
Johannes Hörmann's avatar
Johannes Hörmann committed
    if (!templateDocument[0])
        return new DocumentTemplate(undefined, undefined, undefined, undefined, templateId);
Johannes Hörmann's avatar
Johannes Hörmann committed
    var binaryId = templateDocument[0][db.BINARY_ID];
    var filename = templateDocument[0][db.BINARY_FILENAME];
Johannes Hörmann's avatar
Johannes Hörmann committed
    var mimeType = templateDocument[0][db.BINARY_MIMETYPE];
    var type = DocumentTemplate.types.fromBinaryMetadata(templateDocument[0]);
    if (pResolveSubtemplates == undefined)
        pResolveSubtemplates = true;
Johannes Hörmann's avatar
Johannes Hörmann committed
    return new DocumentTemplate(db.getBinaryContent(binaryId, alias), type, filename, pResolveSubtemplates, templateId, mimeType);
Johannes Hörmann's avatar
Johannes Hörmann committed
}

/**
 * makes a DocumentTemplate from a upload value
 * 
 * @param {FileUpload} pDocumentUpload FileUpload object
 * @return {DocumentTemplate} a document template
 */
DocumentTemplate.fromUpload = function (pDocumentUpload)
{
    var type;
    
    //if the mimetype couldn't be determined, check the file extension
    if (pDocumentUpload.mimeType == MimeTypes.BIN())
        type = DocumentTemplate.types.fromFileExtension(pDocumentUpload.fileExtension);
    else
        type = DocumentTemplate.types.fromMimeType(pDocumentUpload.mimeType);
    
    return new DocumentTemplate(pDocumentUpload.bindata, type, pDocumentUpload.filename, true);
}

/**
 * Function that helps to get the correct template when editing a bulk mail.
 * pUploadValue is preferred over pTemplateId, and if pEditedContent is provided,
 * it will overwrite the content of the template (but the type will remain the same as
 * defined by the upload or templateId, if both are empty, pDefaultType is used)
 * 
 * @param {String} pTemplateId
 * @param {FileUpload} pDocumentUpload
 * @param {String} [pEditedContent]
 * @param {String} [pDefaultType]
 * @return {DocumentTemplate} the document template
 */
DocumentTemplate.getSelectedTemplate = function (pTemplateId, pDocumentUpload, pEditedContent, pDefaultType)
{
    if (pDocumentUpload.isFilled())
        template = DocumentTemplate.fromUpload(pDocumentUpload);
    else if (pTemplateId)
Johannes Hörmann's avatar
Johannes Hörmann committed
        template = DocumentTemplate.loadTemplate(pTemplateId);
Johannes Hörmann's avatar
Johannes Hörmann committed
    else
        template = new DocumentTemplate(null, pDefaultType || DocumentTemplate.types.TXT, null, true);
    
    if (pEditedContent)
    {
        if (template.type == DocumentTemplate.types.EML || template.type == DocumentTemplate.types.HTML)
            pEditedContent = "<html>" + pEditedContent + "</html>";
        template.content = util.encodeBase64String(pEditedContent);
    }
    return template;
}

/** 
 * Returns the template content with replaced placeholders by choosing the right
 * replace function for the type.
 * 
 * @param {Object} pReplacements map, the structure is {placeholder : value}
 * 
 * @return {String} the replaced content
 */
DocumentTemplate.prototype.getReplacedContent = function (pReplacements)
Johannes Hörmann's avatar
Johannes Hörmann committed
{
    // if there exists a _subtemplatedContent we use it because then I assume that the replacements are already based on content + subtemplates
    var content = this._subtemplateResolvedContent || this.content;
Johannes Hörmann's avatar
Johannes Hörmann committed
    switch (this.type)
    {
Johannes Hörmann's avatar
Johannes Hörmann committed
        case DocumentTemplate.types.EML:
            let emlContent
            if (this.options.onlyBody)
Johannes Hörmann's avatar
Johannes Hörmann committed
            {
                // get only body and treat it as html (next case)
                var email = Email.fromRFC(content);
                content = util.encodeBase64String(email.body);
            }
            else
            {
                emlContent = util.decodeBase64String(content);
                emlContent = this._replaceText(emlContent, pReplacements);
                if (this.options.base64)
Johannes Hörmann's avatar
Johannes Hörmann committed
                    emlContent = util.encodeBase64String(emlContent);
                return emlContent;
            }
Johannes Hörmann's avatar
Johannes Hörmann committed
        case DocumentTemplate.types.HTML:
            // replaces ä, ö, ü, ... with html escape signs
Johannes Hörmann's avatar
Johannes Hörmann committed
            for (let i in pReplacements)
                pReplacements[i] = text.text2html(pReplacements[i], false);
        case DocumentTemplate.types.TXT:
            let decodedContent = util.decodeBase64String(content);
            let encodedContent = this._replaceText(decodedContent, pReplacements);
            if (this.options.base64)
Johannes Hörmann's avatar
Johannes Hörmann committed
                encodedContent = util.encodeBase64String(encodedContent);
            return encodedContent;
        case DocumentTemplate.types.ODT:
            return this._getReplacedODT(pReplacements);
Johannes Hörmann's avatar
Johannes Hörmann committed
        case DocumentTemplate.types.DOCX:
Johannes Hörmann's avatar
Johannes Hörmann committed
        case DocumentTemplate.types.DOCM:
            return this._getReplacedDOCX(pReplacements);
Johannes Hörmann's avatar
Johannes Hörmann committed
        case DocumentTemplate.types.PLAIN:
            let plainText = this._replaceText(this.content, pReplacements);
            if (this.options.base64)
Johannes Hörmann's avatar
Johannes Hörmann committed
                plainText = util.encodeBase64String(plainText);
            return plainText;
        default:
            return null;
    }
}

/**
 * replaces the placeholders with data from one contact and returns the result
 * 
 * @param {String} pContactId contact id
 * @param {Placeholder[]} pAdditionalPlaceholders Additional placeholders that should be used. You can use placeholders with the 
 *      types FIXEDVALUE and CALLBACKFUNCTION if you want to calculate the replacement values yourself.
 * 
 * @return {String} replaced content
Johannes Hörmann's avatar
Johannes Hörmann committed
 */
DocumentTemplate.prototype.getReplacedContentByContactId = function (pContactId, pAdditionalPlaceholders) 
Johannes Hörmann's avatar
Johannes Hörmann committed
{
    var replacements = this.getReplacementsByContactIds([pContactId], pAdditionalPlaceholders)[pContactId]; 
    var content = this.getReplacedContent(replacements);
Johannes Hörmann's avatar
Johannes Hörmann committed
    
    return content;
}

/**
 * replaces the placeholders with data from the contacts and returns the result
 * 
 * @param {Array} pContactIds contact ids
 * @param {Placeholder[]} pAdditionalPlaceholders Additional placeholders that should be used. You can use placeholders with the 
 *      types FIXEDVALUE and CALLBACKFUNCTION if you want to calculate the replacement values yourself.
Johannes Hörmann's avatar
Johannes Hörmann committed
 * 
 * @return {Object} replaced content for every contactId
 */
DocumentTemplate.prototype.getReplacedContentByContactIds = function (pContactIds, pAdditionalPlaceholders) 
Johannes Hörmann's avatar
Johannes Hörmann committed
{
    var replacements = this.getReplacementsByContactIds(pContactIds, pAdditionalPlaceholders);
Johannes Hörmann's avatar
Johannes Hörmann committed
    var contents = {};
    for (let contactId in replacements)
    {
        contents[contactId] = this.getReplacedContent(replacements[contactId]);
Johannes Hörmann's avatar
Johannes Hörmann committed
    }
    return contents;
}

/**
 * Replaces the placeholders with data from the contacts and returns a serial letter, works
 * only for ODT
 * 
 * @param {Array} pContactIds contact ids
 * @param {Object[][][]} pTableData Table data for the document, as a three-dimensional array 
 *          of objects (dimensions are: document, table in that document, rows of the table). For the format, see example.
 * @param {Placeholder[]} pAdditionalPlaceholders Additional placeholders that should be used. You can use placeholders with the 
 *      types FIXEDVALUE and CALLBACKFUNCTION if you want to calculate the replacement values yourself.
Johannes Hörmann's avatar
Johannes Hörmann committed
 * 
 * @example 
 * var contacts = newSelect("FIRSTNAME, LASTNAME")
 *                      .from("CONTACT")
 *                      .join("PERSON", "CONTACT.PERSON_ID = PERSON.PERSONID")
 *                      .where("CONTACT.ORGANISATION_ID", orgId)
 *                      .table();
Johannes Hörmann's avatar
Johannes Hörmann committed
 *
 *   var tblRows = [];
 *   fnamePlaceholder = PlaceholderUtils.formatPlaceholder("fname");
 *   lnamePlaceholder = PlaceholderUtils.formatPlaceholder("lname");
 *
 *   for (let i = 0; i < contacts.length; i++)
 *   {
 *       let names = {};
 *       names[fnamePlaceholder] = contacts[i][0];
 *       names[lnamePlaceholder] = contacts[i][1];
 *       tblRows.push(names);
 *   }
 *
 *   var tables = [tblRows];
 *   var template = DocumentTemplate.loadTEmplate(templateId);
 *   var letter = template.getSerialLetterByContactIds([contactId], [tables]);
 * 
 * @return {Object} the content of the replaced ODT
 */
DocumentTemplate.prototype.getSerialLetterByContactIds = function (pContactIds, pTableData, pAdditionalPlaceholders)
Johannes Hörmann's avatar
Johannes Hörmann committed
{
    if (this.type == DocumentTemplate.types.ODT)
    {
        let replacements = this.getReplacementsByContactIds(pContactIds, pAdditionalPlaceholders);
        let replaceArray = pContactIds.map(function (contactId)
        {
            return replacements[contactId];
        });
        return this._getReplacedODT(replaceArray, pTableData);
Johannes Hörmann's avatar
Johannes Hörmann committed
    }
    question.showMessage(DocumentTemplate.getSerialLetterODTOnlyMessage(), question.INFORMATION, translate.text("Action not supported"))
Johannes Hörmann's avatar
Johannes Hörmann committed
    return null;
}

DocumentTemplate.getSerialLetterODTOnlyMessage = function() 
{
    return translate.text("Only .odt files are supported for bulkletters.");
}

Johannes Hörmann's avatar
Johannes Hörmann committed
/**
 * Replaces the placeholders with data from the contacts and returns the resulting Emails.
 * 
 * @param {Array} pContactIds contact ids
 * @param {Placeholder[]} pAdditionalPlaceholders Additional placeholders that should be used. You can use placeholders with the 
 *      types FIXEDVALUE and CALLBACKFUNCTION if you want to calculate the replacement values yourself.
Johannes Hörmann's avatar
Johannes Hörmann committed
 * 
 * @return {Object} Object containing the contact ids as keys and the corresponding Email
 *                   objects as values
 */
DocumentTemplate.prototype.getReplacedEmailsByContactIds = function (pContactIds, pAdditionalPlaceholders) 
Johannes Hörmann's avatar
Johannes Hörmann committed
{
S.Listl's avatar
S.Listl committed
    var emailObjects = {};
    var isEML = this.type == DocumentTemplate.types.EML;
    this.setOptions({base64 : isEML});
Sebastian Listl's avatar
Sebastian Listl committed
    var emailContents = this.getReplacedContentByContactIds(pContactIds, pAdditionalPlaceholders);
S.Listl's avatar
S.Listl committed
    
    for (let contactId in emailContents)
Johannes Hörmann's avatar
Johannes Hörmann committed
    {
S.Listl's avatar
S.Listl committed
        if (isEML)
Johannes Hörmann's avatar
Johannes Hörmann committed
        {
S.Listl's avatar
S.Listl committed
            emailObjects[contactId] = Email.fromRFC(emailContents[contactId]);
Johannes Hörmann's avatar
Johannes Hörmann committed
        }
        else
        {
S.Listl's avatar
S.Listl committed
            // convert to HTML if needed
Johannes Hörmann's avatar
Johannes Hörmann committed
            if (this.type == DocumentTemplate.types.TXT || this.type == DocumentTemplate.types.PLAIN)
S.Listl's avatar
S.Listl committed
                emailContents[contactId] = text.text2html(emailContents[contactId], false);
            
            emailObjects[contactId] = new Email(emailContents[contactId]);
Johannes Hörmann's avatar
Johannes Hörmann committed
        }
S.Listl's avatar
S.Listl committed

        // adding the templates to each mail should be no memory-problem as it is only a reference to pTemplate._attachmentCache, so no problem here
        emailObjects[contactId].attachmentTemplates = this.getAttachments();
Johannes Hörmann's avatar
Johannes Hörmann committed
    }
S.Listl's avatar
S.Listl committed
    return emailObjects;
/**
 * replaces placeholders in the given string
 */
DocumentTemplate.prototype._replaceText = function (pText, pReplacements)
Johannes Hörmann's avatar
Johannes Hörmann committed
{
    var placeholderRegExp = this.options.placeholderRegExp || PlaceholderUtils.getRegexpMatchAll();
    var that = this;
    pText = pText.replace(placeholderRegExp, function (pFound) 
    {
        let foundFiltered = typeof that.options.parsePlaceholderFn === "function"
            ? that.options.parsePlaceholderFn(pFound)
            : pFound;
        return pReplacements[foundFiltered] ? pReplacements[foundFiltered] : pFound;
 * @param {String[]} pForcedPlaceholders these placeholders are always loaded
 * @param {Placeholder[]} pAdditionalPlaceholders Additional placeholders that should be used. You can use placeholders with the 
 *      types FIXEDVALUE and CALLBACKFUNCTION if you want to calculate the replacement values yourself.
 * @return {Object[]} all placeholders needed in this template
Johannes Hörmann's avatar
Johannes Hörmann committed
 * @private
 */
DocumentTemplate.prototype._getRequiredPlaceholders = function (pForcedPlaceholders, pAdditionalPlaceholders)
    var content = this.toString(true);
    var placeholders = PlaceholderUtils.getPlaceholders();
    if (pAdditionalPlaceholders)
        placeholders = placeholders.concat(pAdditionalPlaceholders);
    // get all placeholders which matches the placeholder pattern
    var foundPlaceholders = content.match(this.options.placeholderRegExp || PlaceholderUtils.getRegexpMatchAll());
    if (foundPlaceholders == null)
        foundPlaceholders = [];
    
    // clean placeholder from the spechial strings (e.g. to filter '=' in emls)
    if (typeof this.options.parsePlaceholderFn === "function")
        foundPlaceholders = foundPlaceholders.map(this.options.parsePlaceholderFn);
    // filter the possible placeholders by all placeholders found
    placeholders = placeholders.filter(function(placeholder)
        return foundPlaceholders.includes(placeholder.getFormattedName()) || pForcedPlaceholders.includes(placeholder.getFormattedName());
    return placeholders;
 * Builds an object with the placeholder replacement data for multiple contacts
Johannes Hörmann's avatar
Johannes Hörmann committed
 * 
 * @param {Array} pContactIds contact ids
 * @param {Placeholder[]} pAdditionalPlaceholders Additional placeholders that should be used. You can use placeholders with the 
 *      types FIXEDVALUE and CALLBACKFUNCTION if you want to calculate the replacement values yourself.
Johannes Hörmann's avatar
Johannes Hörmann committed
 * 
 * @return {Object} Object containing the data. The structure is like {contactId : {placeholderName : replacementValue, ...}, ...}
 */
DocumentTemplate.prototype.getReplacementsByContactIds = function (pContactIds, pAdditionalPlaceholders)
Johannes Hörmann's avatar
Johannes Hörmann committed
{ 
    var placeholders = this._getRequiredPlaceholders(["{@firstname@}", "{@lastname@}"], pAdditionalPlaceholders);
    var contactPlaceholders = [];
    var additionalPlaceholders = {};
    var isUserRequired = false;
    placeholders.forEach(function (placeholder)
    {
        switch (placeholder.type)
        {
            case Placeholder.types.ADDRESSFORMAT:
            case Placeholder.types.SQLPART:
            case Placeholder.types.SQLPARTFUNCTION:
                contactPlaceholders.push(placeholder);
                break;
            case Placeholder.types.FIXEDVALUE:
            case Placeholder.types.CALLBACKFUNCTION:
                additionalPlaceholders[placeholder.getFormattedName()] = placeholder;
Johannes Hörmann's avatar
Johannes Hörmann committed
    var contactIdPlaceholder = new Placeholder("contactId", Placeholder.types.SQLPART, "CONTACT.CONTACTID");
    contactPlaceholders = [contactIdPlaceholder].concat(contactPlaceholders);
    
    var addressData = getAddressesData(pContactIds, contactPlaceholders, EmployeeUtils.getCurrentContactId()); //TODO: add sender selection
Johannes Hörmann's avatar
Johannes Hörmann committed
    var replacements = {};
    var placeholderNames = addressData[0];
    var contactIdIndex = placeholderNames.indexOf(contactIdPlaceholder.toString());
    for (let i = 1; i < addressData.length; i++)
    {
        let contactId = addressData[i][contactIdIndex];
        if (!(contactId in replacements))
            replacements[contactId] = {};
        placeholderNames.forEach(function (placeholderName, ii)
Johannes Hörmann's avatar
Johannes Hörmann committed
        {
            replacements[contactId][placeholderName] = addressData[i][ii];
        });
        for (let placeholderName in additionalPlaceholders)
        {
            var placeholder = additionalPlaceholders[placeholderName];
            if (placeholder.type === Placeholder.types.FIXEDVALUE)
                replacements[contactId][placeholderName] = placeholder.valueDefinition;
            else if (placeholder.type === Placeholder.types.CALLBACKFUNCTION)
                replacements[contactId][placeholderName] = placeholder.valueDefinition.call(this, contactId);
Johannes Hörmann's avatar
Johannes Hörmann committed
        }
    }
    return replacements;
}

/*
 * replaces a given Odt-File on the server and returns the replaced base64-file
 *
 * @param {Object} pReplacements map of placeholders and replacements
 * @param {Array} pTableData
 *
 * @return {String} base64-encoded replaced file
 * 
 * @private
 */
DocumentTemplate.prototype._getReplacedODT = function (pReplacements, pTableData)
Johannes Hörmann's avatar
Johannes Hörmann committed
{
    var filename =  this.filename;
    if (!filename)
        filename = "dummyname.odt";
    
Johannes Hörmann's avatar
Johannes Hörmann committed
    //save the file on the server so it can be unzipped via pack.getFromZip
S.Listl's avatar
S.Listl committed
    var serverFilePath = vars.get("$sys.servertemp") + "/clientid_" + (vars.exists("$sys.clientid") ? vars.get("$sys.clientid") : 0)
        + "/" + util.getNewUUID() + "/" + filename.replace(/\\/g, "/");
    fileIO.storeData(serverFilePath, this.content, util.DATA_BINARY, false);
    var replacedFileData = null;
    try 
    {
        if (!_replaceODTFile(pReplacements, serverFilePath, pTableData))
            return null;
        replacedFileData = fileIO.getData(serverFilePath, util.DATA_BINARY);
        
    }
    finally 
    {
        fileIO.remove(serverFilePath);
    }
Johannes Hörmann's avatar
Johannes Hörmann committed

    return replacedFileData;
    
    /**
    * replaces placeholders in a odt file
    *
    * @param {Object} pReplacements replacement object
    * @param {String} pODTFileName filename of the odt
    * @param {Array} pTableData
    * 
    * @return {Boolean}
    */
    function _replaceODTFile (pReplacements, pODTFileName, pTableData)
    {
        var senderRelId = EmployeeUtils.getCurrentContactId();
        if (senderRelId == null)
            return false;
        if (!Array.isArray(pReplacements))
Johannes Hörmann's avatar
Johannes Hörmann committed
            pReplacements = [pReplacements];
        if (!pTableData)
            pTableData = [];
        var tablePlaceholders = [];
        if (pTableData.length > 0)
        {
            //pTableData[0] = first document
            tablePlaceholders = pTableData[0].map(function (tblData)
            {
                if (tblData && tblData.length > 0)
                    return new Set(Object.keys(tblData[0])); //tblData[0] = first row
                return new Set();
            });
        }
Johannes Hörmann's avatar
Johannes Hörmann committed
        
        if (pReplacements.length !== 0)
        {
            //replace placeholders in content.xml
            var contentXml = util.decodeBase64String(pack.getFromZip(pODTFileName, "content.xml"));
            var bodybegin = contentXml.indexOf("<office:body>");
            var bodyend =  contentXml.indexOf("</office:body>") + 14;
            
            var bodyTemplate = contentXml.slice(bodybegin, bodyend);
Johannes Hörmann's avatar
Johannes Hörmann committed
            var fullBody = "";  //body that contains all pages (required when the replacing is done for several contacts)
            var beforeBody = contentXml.slice(0, bodybegin);
            var afterBody = contentXml.slice(bodyend);
Johannes Hörmann's avatar
Johannes Hörmann committed
            
            for (let i = 0, l = pReplacements.length; i < l; i++)
            {
                let replacements = pReplacements[i];
                let currentBody = bodyTemplate;
            
                /* This only works if the text of the placeholders in the odt were not edited since they were written.
                 * If you edit the odt and change a placeholder (for example: you change '{@addres@}' to '{@address@}'),
                 * the text is saved in different XML tags and won't be replaced correctly.
                 */
                for (let placeholder in replacements)
                {
                    currentBody = StringUtils.replaceAll(currentBody, placeholder, 
                        replacements[placeholder].replace(/\n/ig, "<text:line-break/>").replace(/&/ig, "&amp;"), "ig");
Johannes Hörmann's avatar
Johannes Hörmann committed
                }
                
                
                let tables = pTableData[i] || [];
                let tableEnd = 0;
                //for (let tblIndex = 0; tblIndex < tables.length; tblIndex++) //iterate over all tables in the document
                if (tables.length > 0)
Johannes Hörmann's avatar
Johannes Hörmann committed
                {
                    let hasMoreTables = currentBody.includes("</table:table>");
                    for (let tblI = 0; tblI < 10 && hasMoreTables; tblI++)
Johannes Hörmann's avatar
Johannes Hörmann committed
                    {
                        tableEnd = currentBody.indexOf("</table:table>", tableEnd);
                        if (tableEnd !== -1) //stop if there is no table
Johannes Hörmann's avatar
Johannes Hörmann committed
                        {
                            tableEnd += 14;
                            let rowBegin = currentBody.slice(0, tableEnd).lastIndexOf("<table:table-row");
                            let rowEnd =  currentBody.indexOf("</table:table-row>", rowBegin) + 18;

                            let tableRow = currentBody.slice(rowBegin, rowEnd);

                            let rowPlaceholders = tableRow.match(PlaceholderUtils.getRegexpMatchAll());
                            //find the table data that contains all required placeholders
                            let tableDataIndex = !rowPlaceholders ? -1 : tablePlaceholders.findIndex(function (placeholderSet)
                            {
                                return rowPlaceholders.every(function (placeholderName)
                                {
                                    return placeholderSet.has(placeholderName);
                                });
                            });
                            if (tableDataIndex !== -1)
                            {
                                let afterTable = currentBody.slice(rowEnd);
                                currentBody = currentBody.slice(0, rowBegin);
                                tableEnd -= tableRow.length;
                                let tableData = tables[tableDataIndex];
                                for (let rowIndex = 0; rowIndex < tableData.length; rowIndex++)
                                {
                                    let tableRowData = tableData[rowIndex];
                                    let replacedRow = that._replaceText(tableRow, tableRowData);
                                    currentBody += replacedRow;
                                    tableEnd += replacedRow.length;
                                }
                                currentBody += afterTable;
                            }
Johannes Hörmann's avatar
Johannes Hörmann committed
                        }
                        else
                            hasMoreTables = false;
Johannes Hörmann's avatar
Johannes Hörmann committed
                    }
                }
                fullBody += currentBody;
            }   
            contentXml = beforeBody + fullBody + afterBody;
            pack.addToZip(pODTFileName, "content.xml", util.encodeBase64String(contentXml));
            
            //replace placeholders in styles.xml
            var styles = util.decodeBase64String(pack.getFromZip(pODTFileName, "styles.xml"));
            for (let placeholder in pReplacements[0])
            {
                styles = StringUtils.replaceAll(styles, placeholder,
                    pReplacements[0][placeholder].replace(/\n/ig, "<text:line-break/>").replace(/&/ig, "&amp;"), "ig");
Johannes Hörmann's avatar
Johannes Hörmann committed
            }
            pack.addToZip(pODTFileName, "styles.xml", util.encodeBase64String(styles));
            return true;
        }
        return false;
    }
}

/*
 * This function is used to replace placeholders via DocXTemplater
 * 
 * @param {Object} pReplacements - Must contain an object, which holds the placeholders
 * 
 * @return {String} returns the modified document in a BASE64 coded string
 * 
 * @private
 */
DocumentTemplate.prototype._getReplacedDOCX = function (pReplacements)
Johannes Hörmann's avatar
Johannes Hörmann committed
{
    var replacements = {};
    var startDelimiter = this.options.startDelimiter;
    var endDelimiter = this.options.endDelimiter;
Johannes Hörmann's avatar
Johannes Hörmann committed
    for (let placeholder in pReplacements)  //removes the prefix and postfix, the process needs it like this
        replacements[placeholder.slice(startDelimiter.length, -endDelimiter.length)] = pReplacements[placeholder];
    var documentData = DocxtemplaterUtils.generateDocument(this.content, replacements, startDelimiter, endDelimiter);
Johannes Hörmann's avatar
Johannes Hörmann committed
    return documentData;
}

/**
 * functions for working with letters (mails)
 */
function LetterUtils () {}

/**
 * opens a new letter
 * 
 * @param {String} pContactId id of the contact to fetch the data from
 * @param {String} pComingFrom source from where you started (e.g. "PERSON", "ORGANISATION" )
Johannes Hörmann's avatar
Johannes Hörmann committed
 */
LetterUtils.openNewLetter = function (pContactId, pComingFrom)
Johannes Hörmann's avatar
Johannes Hörmann committed
{
    var params = {
        "ContactId_param" : pContactId,
        "ComingFrom_param" : pComingFrom
Johannes Hörmann's avatar
Johannes Hörmann committed
    };
    neon.openContext("Letter", "LetterEdit_view", null, neon.OPERATINGSTATE_VIEW, params);
Johannes Hörmann's avatar
Johannes Hörmann committed
}

/**
 * utility functions for the DocumentTemplate_entity
 */
function DocumentTemplateUtils () {}

/**
 * if pText is provided, it is used as template, otherwise pFileUpload
 * 
 * @param {FileUpload} pFileUpload upload value
 * @param {String} pKind kind of template
Johannes Hörmann's avatar
Johannes Hörmann committed
 * @param {String} pText text input
 * @param {String} pClassification the classification type. Used if pText is not empty. Defines if it is saved as txt or html.
 * @param {String} pTemplateName name of the template
 * @return {FileUpload} a FileUpload object with the data
 */
DocumentTemplateUtils.chooseSuppliedTemplate = function (pFileUpload, pKind, pText, pClassification, pTemplateName)
Johannes Hörmann's avatar
Johannes Hörmann committed
{
    if (pFileUpload.isFilled() && pText != "")
    {
        // use fileUpload but use the custom text as bindata
        pFileUpload.bindata = util.encodeBase64String(pText);
    }
m.wachsmuth's avatar
m.wachsmuth committed
    else if (!pFileUpload.isFilled() && pText != "" && pKind == $KeywordRegistry.documentTemplateType$textModular() || pKind == $KeywordRegistry.documentTemplateType$mail()) // edit is only allowed in modular templates
Johannes Hörmann's avatar
Johannes Hörmann committed
    {
        pFileUpload.filename = pTemplateName;
        
        // if it is a htmlTemplate save it with the html extension
        if (pClassification == $KeywordRegistry.documentTemplateTypeCategory$htmlTemplate())
Johannes Hörmann's avatar
Johannes Hörmann committed
            pFileUpload.fileExtension = "html";
Johannes Hörmann's avatar
Johannes Hörmann committed
            pFileUpload.fileExtension = "txt";
        
        pFileUpload.bindata  = util.encodeBase64String(pText);
    }
    
    var fileUploadType = DocumentTemplate.types.fromMimeType(pFileUpload.mimeType);
    
    // treat txt as html for emails
    if (pFileUpload.fileExtension == "txt" && pKind == $KeywordRegistry.documentTemplateType$mail())
    {
        pFileUpload.fileExtension = "html";
        // convert text to html
        pFileUpload.bindata = util.encodeBase64String(text.text2html(util.decodeBase64String(pFileUpload.bindata), true));
    }
    
Johannes Hörmann's avatar
Johannes Hörmann committed
    return pFileUpload;
}

/**
 * inserts a template from a document template into ASYS_BINARIES
 */
DocumentTemplateUtils.insertTemplateData = function (pTemplateId, pFileUpload, pKind, pText, pClassification, pTemplateName)
{
    pFileUpload = DocumentTemplateUtils.chooseSuppliedTemplate(pFileUpload, pKind, pText, pClassification, pTemplateName)
Johannes Hörmann's avatar
Johannes Hörmann committed
    if (pFileUpload.isFilled())
    {
        db.insertBinary("DOCUMENTTEMPLATE", "DOCUMENT", pTemplateId, 
            "", pFileUpload.bindata, pFileUpload.filename, "", "TEMPLATE", SqlUtils.getBinariesAlias());
Johannes Hörmann's avatar
Johannes Hörmann committed
    }
}

/**
 * updates a template from a document template in ASYS_BINARIES
 */
DocumentTemplateUtils.updateTemplateData = function (pTemplateId, pFileUpload, pKind, pText, pClassification, pTemplateName)
Johannes Hörmann's avatar
Johannes Hörmann committed
{
    pFileUpload = DocumentTemplateUtils.chooseSuppliedTemplate(pFileUpload, pKind, pText, pClassification, pTemplateName)
Johannes Hörmann's avatar
Johannes Hörmann committed
    if (pFileUpload.isFilled())
    {
        var assignmentTable = "DOCUMENTTEMPLATE";
        var assignmentName= "DOCUMENT";
        var keyword = "TEMPLATE";
        var binMeta = db.getBinaryMetadata(assignmentTable, assignmentName, pTemplateId, false, SqlUtils.getBinariesAlias(), keyword);
Johannes Hörmann's avatar
Johannes Hörmann committed
        if (binMeta.length == 0)
            SingleBinaryUtils.insert(assignmentTable, assignmentName, pTemplateId, pFileUpload.bindata, pFileUpload.filename, null, "TEMPLATE", SqlUtils.getBinariesAlias());
Johannes Hörmann's avatar
Johannes Hörmann committed
        else
            db.updateBinary(binMeta[0][db.BINARY_ID], "", pFileUpload.bindata, pFileUpload.filename, "", keyword, SqlUtils.getBinariesAlias());
    }
}

/**
 * loads content from a fileUpload or if it's empty from a template in ASYS_BINARIES
 * @param {String} pTemplateId the id of the template
 * @param {FileUpload} pFileUpload upload object
 * 
 * @return {String[]} [content, type] or ["", type] or ["", null] 
 *                    Content is only set, if it is HTML, TXT or EML
 */
DocumentTemplateUtils.getTemplateContent = function (pTemplateId, pFileUpload)
{
    var type;
    var bindata;
    if (pFileUpload.isFilled())
    {
        type = DocumentTemplate.types.fromFileExtension(pFileUpload.fileExtension);
        bindata = pFileUpload.bindata
    }
    else
    {
        var template = DocumentTemplateUtils.getTemplate(pTemplateId, false);
        if (template != null)
        {
            bindata = template.content;
            type = template.type;
        }
    }