UNPKG

@microsoft/sp-webpart-base

Version:

SharePoint Framework support for building web parts

379 lines 19.6 kB
"use strict"; // Copyright (c) Microsoft. All rights reserved. Object.defineProperty(exports, "__esModule", { value: true }); exports.WebPartDataConverter = void 0; var tslib_1 = require("tslib"); var lodash = tslib_1.__importStar(require("@microsoft/sp-lodash-subset")); var sp_core_library_1 = require("@microsoft/sp-core-library"); var sp_component_base_1 = require("@microsoft/sp-component-base"); var SPWebPartError_1 = require("./error/SPWebPartError"); /** * On the client, we need to support both HTML and and JSON format of the web part data. This is a utility * class to perform conversion between the two formats. * * @internal */ var WebPartDataConverter = /** @class */ (function () { function WebPartDataConverter() { } Object.defineProperty(WebPartDataConverter, "_parsingDocument", { /** * A temporary document detached from the main document for HTML parsing (call createElement on this) * * Note: Using document.createElement will create the element on the running document of the page which is * dangerous, because when you set innerHTML on the element the content will immediately run on the page. * That causes a security issue because we might be parsing something that has a <script> tag (XSS attack). * In case of <img> tags, the image gets downloaded immediately which is also unwanted behavior. So, for * parsing purposes, we should never use document.createElement and insead use this._parsingDocument.createElement. * */ get: function () { if (!this._tempDoc) { this._tempDoc = document.implementation.createHTMLDocument('tempDocument'); } return this._tempDoc; }, enumerable: false, configurable: true }); /** * Is this string a html web part data ? */ WebPartDataConverter.isWebPartHtml = function (htmlString) { sp_core_library_1.Validate.isNonemptyString(htmlString, 'htmlString'); return (htmlString.indexOf('<div') === 0 && htmlString.indexOf(WebPartDataConverter._webPartDataAttribute) !== -1); }; /** * Converts an instance of IWebPartData to is corresponding persisted HTML element. * See WebPartDataConverter tests for examples. */ WebPartDataConverter.convertWebPartDataToHtml = function (webpartData) { sp_core_library_1.Validate.isNotNullOrUndefined(webpartData, 'web part data'); // Clone web part data because we will modify it for conversion var wpdata = lodash.cloneDeep(webpartData); WebPartDataConverter._initializeIfNeeded(); // Add the component id so the GUIDs get search indexed and we can look them up in search var componentIdDiv = WebPartDataConverter._wpComponentIdDiv.cloneNode(); componentIdDiv.textContent = wpdata.id; var htmlPropsDiv = WebPartDataConverter._wpHtmlPropsDiv.cloneNode(); htmlPropsDiv.innerHTML = WebPartDataConverter.convertServerProcessedDataToHtml(wpdata.serverProcessedContent); // Server-processed data is translated to html, so clear it out in the IWebPartData object to avoid duplication wpdata.serverProcessedContent = undefined; var wpHtmlDiv = WebPartDataConverter._wpDiv.cloneNode(); // We don't need any attribute encoding because dom parser inherently encode/decodes when dealing with innerHTML wpHtmlDiv.setAttribute(WebPartDataConverter._webPartDataAttribute, JSON.stringify(wpdata)); wpHtmlDiv.appendChild(componentIdDiv); wpHtmlDiv.appendChild(htmlPropsDiv); var wrapper = WebPartDataConverter._parsingDocument.createElement('div'); wrapper.appendChild(wpHtmlDiv); return wrapper.innerHTML; }; /** * Converts persisted html element for a web part to its corresponding IWebPartData instance. * * @remarks * Returns undefined in case of bad input. * See WebPartDataConverter tests for examples * * @param htmlString - html formatted web part data. * @param links - (optional) Array of the fixed up links. If provided, the values in this array * take over the values in the HTML markup. */ WebPartDataConverter.convertHtmlToWebPartData = function (htmlString, links) { var wpdata; var wrapper = WebPartDataConverter._parsingDocument.createElement('div'); wrapper.innerHTML = htmlString.trim(); // Use children (instead of childNodes) to avoid getting text nodes var wpHtmlDiv = wrapper.children[0]; if (wpHtmlDiv && wpHtmlDiv.hasAttribute(WebPartDataConverter._webPartAttribute)) { var wpHtmlDivWebPartAttributeData = wpHtmlDiv.getAttribute(WebPartDataConverter._webPartDataAttribute); if (wpHtmlDivWebPartAttributeData) { wpdata = JSON.parse(wpHtmlDivWebPartAttributeData); } // In case of bad input, wpdata will be null if (wpdata) { var htmlPropsDiv = wpHtmlDiv.querySelector("[".concat(WebPartDataConverter._htmlPropertiesAttribute, "]")); wpdata.serverProcessedContent = WebPartDataConverter.convertServerProcessedHtmlToData(htmlPropsDiv.innerHTML, links); } } return wpdata || undefined; }; /** * Convert server process data to an equivalent HTML stirng format that the SharePoint server * can process for search indexing, link fixup and SafeHTML processing. * * @remarks * HtmlStrings are search indexed. Links and ImageSources are setup for link fixup. All of these are * search indexed and passed through SafeHtml processing to sanitize the content. * * This method is expected to provide reverse processing as compared to `convertHtmltoServerProcessedData`. * * Input: * * ``` * { * htmlStrings: { 'prop1': 'value_of_prop1' }, * links: { 'prop2': 'http://www.contoso.com/page1.aspx' }, * imageSources: { 'prop3': 'http://www.contoso.com/imag.png' } * } * ``` * * Output: * * ``` * "<div data-sp-prop-name='prop1'>value_of_prop1</div> * <link data-sp-prop-name='prop2' href='http://www.contoso.com/page1.aspx'> * <img data-sp-prop-name='prop3' src='http://www.contoso.com/image.png'>" * ``` */ WebPartDataConverter.convertServerProcessedDataToHtml = function (serverContent) { var result = ''; if (serverContent) { if (serverContent.htmlStrings) { result += WebPartDataConverter._convertServerProcessedDataToHtmlByType(serverContent.htmlStrings, sp_component_base_1._ServerProcessedDataType.htmlString); } if (serverContent.searchablePlainTexts) { result += WebPartDataConverter._convertServerProcessedDataToHtmlByType(serverContent.searchablePlainTexts, sp_component_base_1._ServerProcessedDataType.searchablePlainText); } if (serverContent.links) { result += WebPartDataConverter._convertServerProcessedDataToHtmlByType(serverContent.links, sp_component_base_1._ServerProcessedDataType.link); } if (serverContent.imageSources) { result += WebPartDataConverter._convertServerProcessedDataToHtmlByType(serverContent.imageSources, sp_component_base_1._ServerProcessedDataType.imageSource); } } return result; }; /** * Convert an HTML string to its equivalent ISerializedServerProcessedData structure format. * * @remarks * This method is expected to provide reverse processing as compared to convertServerProcessedDataToHtml. * * Input: * * ``` * "<div data-sp-prop-name='prop1'>value_of_prop1</div> * <link data-sp-prop-name='prop2' href='http://www.contoso.com/page1.aspx'> * <img data-sp-prop-name='prop3' src='http://www.contoso.com/image.png'>" * ``` * * Output: * * ``` * { * htmlStrings: { 'prop1': 'value_of_prop1' }, * links: { 'prop2': 'http://www.contoso.com/page1.aspx' }, * imageSources: { 'prop3': 'http://www.contoso.com/imag.png' } * } * ``` * * Array of the fixed up links. If provided, the values in this array take over the values in the HTML markup. */ WebPartDataConverter.convertServerProcessedHtmlToData = function (htmlString, links) { var serverContent = { htmlStrings: {}, searchablePlainTexts: {}, links: {}, imageSources: {} }; if (!htmlString || htmlString === '') { return serverContent; } var tempNode = WebPartDataConverter._parsingDocument.createElement('DIV'); tempNode.innerHTML = htmlString; var nodes = tempNode.children; for (var i = 0; i < nodes.length; i++) { var node = nodes[i]; var key = node.getAttribute(WebPartDataConverter._propNameAttribute); if (key) { switch (node.tagName) { case 'DIV': if (node.hasAttribute(WebPartDataConverter._searchablePlainTextAttribute) && serverContent.searchablePlainTexts) { serverContent.searchablePlainTexts[key] = node.textContent; } else if (serverContent.htmlStrings) { serverContent.htmlStrings[key] = node.innerHTML; } break; case 'LINK': case 'A': if (links) { WebPartDataConverter._extractSPLink(node, links, serverContent, key, false); } else { var aTagAttribute = node.getAttribute('href'); if (aTagAttribute && serverContent.links) { serverContent.links[key] = aTagAttribute; } } break; // Look for SPIMG because Canvas may replace IMG tags with SPIMG to prevent browser pre-loading case 'IMG': case 'SPIMG': if (links) { WebPartDataConverter._extractSPLink(node, links, serverContent, key, true); } else { var srcAttribute = node.getAttribute('src'); if (serverContent.imageSources && srcAttribute) { serverContent.imageSources[key] = srcAttribute; } // This is a temporary fix to make pages published with the mobile app functional. // The mobile app sets the href attribute on the image tag instead of the src attribute. // (SPPPLAT VSO#289988) tracks removal of this code. var hrefAttribute = node.getAttribute('href'); if (hrefAttribute && serverContent && serverContent.imageSources && (serverContent.imageSources[key] === undefined || serverContent.imageSources[key] === null)) { serverContent.imageSources[key] = hrefAttribute; } } break; } } } return serverContent; }; WebPartDataConverter._convertServerProcessedDataToHtmlByType = function (properties, type) { var result = ''; for (var _i = 0, _a = Object.entries(properties); _i < _a.length; _i++) { var _b = _a[_i], propPath = _b[0], value = _b[1]; result += WebPartDataConverter._getHtmlString(propPath, type, value); } return result; }; /** * Get the HTML equivalent string for a server processed prop type. */ WebPartDataConverter._getHtmlString = function (propName, propType, propValue) { var htmlPropsString = ''; if (propName && typeof propValue === 'string' && propValue) { switch (propType) { case sp_component_base_1._ServerProcessedDataType.htmlString: var htmlDiv = this._parsingDocument.createElement('DIV'); htmlDiv.setAttribute(WebPartDataConverter._propNameAttribute, propName); var sanitizedValue = WebPartDataConverter._normalizeHTML(propValue); htmlDiv.innerHTML = sanitizedValue; htmlPropsString = htmlDiv.outerHTML; break; case sp_component_base_1._ServerProcessedDataType.searchablePlainText: var plainTextDiv = this._parsingDocument.createElement('DIV'); plainTextDiv.setAttribute(WebPartDataConverter._propNameAttribute, propName); plainTextDiv.setAttribute(WebPartDataConverter._searchablePlainTextAttribute, 'true'); plainTextDiv.textContent = propValue; htmlPropsString = plainTextDiv.outerHTML; break; case sp_component_base_1._ServerProcessedDataType.link: var anchorDiv = this._parsingDocument.createElement('A'); anchorDiv.setAttribute(WebPartDataConverter._propNameAttribute, propName); anchorDiv.setAttribute('href', propValue); htmlPropsString = anchorDiv.outerHTML; break; case sp_component_base_1._ServerProcessedDataType.imageSource: var imgDiv = this._parsingDocument.createElement('IMG'); imgDiv.setAttribute(WebPartDataConverter._propNameAttribute, propName); imgDiv.setAttribute('src', propValue); htmlPropsString = imgDiv.outerHTML; break; } } return htmlPropsString; }; /** * We need to send valid html from client, because server should understand it to perform services. This method * normalizes html by doing basic validations and removing script tags. Returns empty string if passed invalid HTML. * Note that this is not a strict html validation, it just needs to make sure the page doesn't break so the * html value (or a valid part of it) gets to server for proper validation and sanitization */ WebPartDataConverter._normalizeHTML = function (htmlString) { if (!htmlString || htmlString === '') { return htmlString; } var tempDiv = WebPartDataConverter._parsingDocument.createElement('DIV'); /* This is a trick to detect invalid html. We put the html string inside a simple structure and check if the DOM created for the structure is as expected. If there are unexpected closing tags or characters in the html string the structure of this element will be messed up and one our checks would fail */ tempDiv.innerHTML = "<div class='child1'></div><div class='main'>".concat(htmlString, "</div><div class='child3'></div>"); var children = tempDiv.children; if (!children[0] || children[0].className !== 'child1' || !children[1] || children[1].className !== 'main' || !children[2] || children[2].className !== 'child3') { return ''; } // Remove script tags // @todo #286930 Make this more robust var mainDiv = children[1]; var scriptTags = mainDiv.querySelectorAll('script'); for (var i = 0; i < scriptTags.length; i++) { var scriptTag = scriptTags[0]; if (scriptTag && scriptTag.parentElement) { scriptTag.parentElement.removeChild(scriptTag); } } return mainDiv.innerHTML; }; WebPartDataConverter._initializeIfNeeded = function () { if (!WebPartDataConverter._wpDiv) { WebPartDataConverter._wpDiv = WebPartDataConverter._parsingDocument.createElement('div'); WebPartDataConverter._wpDiv.setAttribute(WebPartDataConverter._webPartAttribute, ''); WebPartDataConverter._wpDiv.setAttribute(WebPartDataConverter._webPartDataVersionAttribute, '1.0'); // Note: data-sp-componentid attribute is looked up by the server for module pre-loading WebPartDataConverter._wpComponentIdDiv = WebPartDataConverter._parsingDocument.createElement('div'); WebPartDataConverter._wpComponentIdDiv.setAttribute(WebPartDataConverter._componentIdAttribute, ''); WebPartDataConverter._wpHtmlPropsDiv = WebPartDataConverter._parsingDocument.createElement('div'); WebPartDataConverter._wpHtmlPropsDiv.setAttribute(WebPartDataConverter._htmlPropertiesAttribute, ''); } }; /** * Extract the link by processing the links array and the index in the data-sp-splink attribute whose * value should be of the format `__SPLINK__<index>__` where index is the index in the links array. */ WebPartDataConverter._extractSPLink = function (node, links, serverContent, key, isImage) { if (links.length <= 0) { return; } var spLinkAttribute = node.getAttribute('data-sp-splink'); if (spLinkAttribute) { var result = WebPartDataConverter._linkPlaceHolderRegex.exec(spLinkAttribute); if (result) { var index = parseInt(result[1], 10); if (!isNaN(index) && !!links[index]) { if (isImage && serverContent.imageSources) { serverContent.imageSources[key] = links[index]; } else if (serverContent.links) { serverContent.links[key] = links[index]; } } else { throw SPWebPartError_1.SPWebPartError.create(SPWebPartError_1.SPWebPartErrorCode.InvalidSPLinkIndex, result[1]); } } else { throw SPWebPartError_1.SPWebPartError.create(SPWebPartError_1.SPWebPartErrorCode.InvalidSPLinkAttributeFormat, spLinkAttribute); } } }; WebPartDataConverter._componentIdAttribute = 'data-sp-componentid'; WebPartDataConverter._htmlPropertiesAttribute = 'data-sp-htmlproperties'; WebPartDataConverter._propNameAttribute = 'data-sp-prop-name'; WebPartDataConverter._searchablePlainTextAttribute = 'data-sp-searchableplaintext'; WebPartDataConverter._webPartAttribute = 'data-sp-webpart'; WebPartDataConverter._webPartDataAttribute = 'data-sp-webpartdata'; WebPartDataConverter._webPartDataVersionAttribute = 'data-sp-webpartdataversion'; /* * Regular expression used to extract the integer value from the __SPLINK__<number>__ placeholder. */ WebPartDataConverter._linkPlaceHolderRegex = /^__SPLINK__(\d+)__$/; return WebPartDataConverter; }()); exports.WebPartDataConverter = WebPartDataConverter; exports.default = WebPartDataConverter; //# sourceMappingURL=WebPartDataConverter.js.map