Source: html/Markdown.js

/**
 * Copyright (C) 2005-2016 Alfresco Software Limited.
 *
 * This file is part of Alfresco
 *
 * Alfresco 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.
 *
 * Alfresco 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 Alfresco. If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * <p>A simple markdown rendering widget. This module uses [Showdown]{@link https://github.com/showdownjs/showdown} to convert
 * markdown to HTML. In order to prevent any malicious content from being added to the browser DOM, all generated HTML is
 * passed through the "stripUnsafeHTML" function provided on the server by Surf. This does require an XHR call to made which
 * may reduce rendering speed - but ensures that the widget is not prone to XSS style attacks.</p>
 * <p>It is possible to provide initial rendering via the [markdown]{@link module:alfresco/html/Markdown#markdown} attribute
 * and it is also possible to allow markdown to be dynamically updated by configuring one or more 
 * [subscriptionTopics]{@link module:alfresco/html/Markdown#subscriptionTopics}.</p>
 *
 * @example <caption>Simple markdown example:</caption>
 * {
 *   name: "alfresco/html/Markdown",
 *   config: {
 *     markdown: "# H1\n## H2"
 *   }
 * }
 *
 * @example <caption>Example with subscription topics for dynamic updates:</caption>
 * {
 *   name: "alfresco/html/Markdown",
 *   config: {
 *     markdown: "# H1\n## H2",
 *     subscriptionTopics: ["UPDATE_MARKDOWN","CHANGE_CONTENT"]
 *   }
 * }
 * 
 * @module alfresco/html/Markdown
 * @extends external:dijit/_WidgetBase
 * @mixes external:dojo/_TemplatedMixin
 * @mixes module:alfresco/core/CoreXhr
 * @author Dave Draper
 * @since 1.0.53
 */
define(["dojo/_base/declare",
        "dijit/_WidgetBase", 
        "dijit/_TemplatedMixin",
        "dojo/text!./templates/Markdown.html",
        "alfresco/core/CoreXhr",
        "webscripts/defaults",
        "service/constants/Default",
        "dojo/_base/lang",
        "dojo/_base/array",
        "showdown"], 
        function(declare, _WidgetBase, _TemplatedMixin, template, CoreXhr, webScriptDefaults, AlfConstants, lang, array, showdown) {
   
   return declare([_WidgetBase, _TemplatedMixin, CoreXhr], {

      /**
       * The HTML template to use for the widget.
       * 
       * @instance
       * @type {String}
       */
      templateString: template,
      
      /**
       * Some initial markdown content to convert to HTML.
       * 
       * @instance
       * @type {string}
       * @default
       */
      markdown: null,

      /**
       * An array of topics to subscribe to that when published on will update the data. Payloads published on the topics must
       * contain an attribute called 'markdown' in order to the requested data to be rendered.
       * 
       * @instance
       * @type {string[]}
       * @default
       */
      subscriptionTopics: null,

      /**
       * This is used to store the last requested markdown update when a [request is in progress]{@link module:alfresco/html/Markdown#_requestInProgress}
       * to sanitize the HTML generated from the last markdown update request.
       *
       * @instance
       * @type {string}
       * @default
       */
      _pendingMarkdown: null,

      /**
       * This boolean flag is used internally to indicate whether or not a request is currently being made to sanitize the HTML
       * rendered for markdown provided. If this flag is set to true then markdown update requests will be stored assigned to 
       * [_pendingMarkdown]{@link module:alfresco/html/Markdown#_pendingMarkdown} and will be converted to HTML and sanitized
       * once the request in progress is made.
       * 
       * @instance
       * @type {boolean}
       * @default
       */
      _requestInProgress: false,

      /**
       * Subscribes to any [subscriptionTopics]{@link module:alfresco/html/Markdown#subscriptionTopics}, initializes the 
       * markdown converter and if any [markdown]{@link module:alfresco/html/Markdown#markdown} has been initially provided calls 
       * [updateMarkdown]{@link module:alfresco/html/Markdown#updateMarkdown} to render and sanitize the HTML for it.
       * 
       * @instance
       */
      postCreate: function alfresco_html_Markdown__postCreate() {
         if (this.subscriptionTopics)
         {
            array.forEach(this.subscriptionTopics, function(topic) {
               this.alfSubscribe(topic, lang.hitch(this, this.onMarkdownUpdate));
            }, this);
         }
         
         this.converter = new showdown.Converter({
            strikethrough: true,
            tables: true
         });
         this._converterReady = true;

         // If we have initial markdown then ensure that it is safe to be used...
         this.markdown && this.updateMarkdown(this.markdown);
      },

      /**
       * Converts the supplied markdown into HTML and then makes an XHR request to Surf to sanitize the
       * generated HTML of any malicious content in order to prevent XSS-style attacks.
       * 
       * @instance
       * @param {string} markdown The markdown to convert to HTML
       */
      updateMarkdown: function alfresco_html_Markdown__updateMarkdown(markdown) {
         if (markdown)
         {
            if (this._requestInProgress)
            {
               this._pendingMarkdown = markdown;
            }
            else
            {
              // Set the flag to indicate that a request is about to be made...
              // TODO: We could potentially optimize this code by logging a timestamp in the request (or similar) and allow requests to be
              //       processed together and then just render the last request...
              this._requestInProgress = true;

               // Convert markdown to HTML...
               var html = this.converter.makeHtml(markdown);

               // Sanitize the output to ensure it is safe to render...
               var url = AlfConstants.URL_SERVICECONTEXT + webScriptDefaults.WEBSCRIPT_VERSION + "/sanitize/data";
               if (url) {
                  this.serviceXhr({
                     url: url,
                     data: {
                        data: html
                     },
                     method: "POST",
                     successCallback: this.sanitizeSuccess,
                     failureCallback: this.sanitizeFailure,
                     callbackScope: this
                  });
               }
            }
         }
      },

      /**
       * This is called on both successful and failing attempts to sanitize the HTML rendered from the requested
       * markdown. It resets the [_requestInProgress]{@link module:alfresco/html/Markdown#_requestInProgress} flag
       * so that calls to [updateMarkdown]{@link module:alfresco/html/Markdown#updateMarkdown} can be processed.
       * If data has been stored in [_pendingMarkdown]{@link module:alfresco/html/Markdown#_pendingMarkdown} from
       * a request that was made whilst the last markdown request was being sanitized then that data will be
       * passed to [updateMarkdown]{@link module:alfresco/html/Markdown#updateMarkdown}.
       * 
       * @instance
       */
      checkForPendingMarkdown: function alfresco_html_Markdown__checkForPendingMarkdown() {
         this._requestInProgress = false;
         if (this._pendingMarkdown)
         {
            this.updateMarkdown(this._pendingMarkdown);
            this._pendingMarkdown = null;
         }
      },

      /**
       * Handles requests to render new markdown that are provided through the publication on a topic defined within the
       * [subscriptionTopics]{@link module:alfresco/html/Markdown#subscriptionTopics}.
       * 
       * @instance
       * @param {object} payload A payload containing a 'markdown' attribute with the markdown to convert to HTML
       */
      onMarkdownUpdate: function alfresco_html_Markdown__onMarkdownUpdate(payload) {
         payload.markdown && this.updateMarkdown(payload.markdown);
      },

      /**
       * @instance
       * @param  {object} response The reponse of the request to sanitize the converted markdown.
       * @param  {object} originalRequestConfig The configuration used to make the sanitize XHR request.
       */
      sanitizeSuccess: function alfresco_html_Markdown__sanitizeSuccess(response, /*jshint unused:false*/ originalRequestConfig) {
         this.domNode.innerHTML = response.data;
         this.checkForPendingMarkdown();
      },

      /**
       * This function is called when requests to sanitize the HTML generated from markdown cannot be processed. It simply
       * outputs a warning. Nothing is updated.
       * 
       * @instance
       * @param  {object} response The reponse of the request to sanitize the converted markdown.
       * @param  {object} originalRequestConfig The configuration used to make the sanitize XHR request.
       */
      sanitizeFailure: function alfresco_html_Markdown__sanitizeFailure(response, originalRequestConfig) {
         this.alfLog("warn", "It was not possible to sanitize the converted markdown", response, originalRequestConfig, this);
         this.checkForPendingMarkdown();
      }
   });
});