519 lines
26 KiB
JavaScript
519 lines
26 KiB
JavaScript
|
"use strict";
|
|||
|
// -------------------------------------------------------------
|
|||
|
// WARNING: this file is used by both the client and the server.
|
|||
|
// Do not use any browser or node-specific API!
|
|||
|
// -------------------------------------------------------------
|
|||
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|||
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|||
|
};
|
|||
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|||
|
const internal_attributes_1 = __importDefault(require("../../processing/dom/internal-attributes"));
|
|||
|
const class_name_1 = __importDefault(require("../../shadow-ui/class-name"));
|
|||
|
const script_1 = require("../script");
|
|||
|
const style_1 = __importDefault(require("../../processing/style"));
|
|||
|
const url_1 = require("../../utils/url");
|
|||
|
const string_trim_1 = __importDefault(require("../../utils/string-trim"));
|
|||
|
const builtin_header_names_1 = __importDefault(require("../../request-pipeline/builtin-header-names"));
|
|||
|
const namespaces_1 = require("./namespaces");
|
|||
|
const attributes_1 = require("./attributes");
|
|||
|
const CDATA_REG_EX = /^(\s)*\/\/<!\[CDATA\[([\s\S]*)\/\/\]\]>(\s)*$/;
|
|||
|
const HTML_COMMENT_POSTFIX_REG_EX = /(\/\/[^\n]*|\n\s*)-->[^\n]*([\n\s]*)?$/;
|
|||
|
const HTML_COMMENT_PREFIX_REG_EX = /^(\s)*<!--[^\n]*\n/;
|
|||
|
const HTML_COMMENT_SIMPLE_POSTFIX_REG_EX = /-->\s*$/;
|
|||
|
const JAVASCRIPT_PROTOCOL_REG_EX = /^\s*javascript\s*:/i;
|
|||
|
const EXECUTABLE_SCRIPT_TYPES_REG_EX = /^\s*(application\/(x-)?(ecma|java)script|text\/(javascript(1\.[0-5])?|((x-)?ecma|x-java|js|live)script)|module)\s*$/i;
|
|||
|
const SVG_XLINK_HREF_TAGS = [
|
|||
|
'animate', 'animateColor', 'animateMotion', 'animateTransform', 'mpath', 'set',
|
|||
|
'linearGradient', 'radialGradient', 'stop',
|
|||
|
'a', 'altglyph', 'color-profile', 'cursor', 'feimage', 'filter', 'font-face-uri', 'glyphref', 'image',
|
|||
|
'mpath', 'pattern', 'script', 'textpath', 'use', 'tref',
|
|||
|
];
|
|||
|
const INTEGRITY_ATTR_TAGS = ['script', 'link'];
|
|||
|
const IFRAME_FLAG_TAGS = ['a', 'form', 'area', 'input', 'button'];
|
|||
|
const PROCESSED_PRELOAD_LINK_CONTENT_TYPE = 'script';
|
|||
|
const MODULE_PRELOAD_LINK_REL = 'modulepreload';
|
|||
|
const ELEMENT_PROCESSED = 'hammerhead|element-processed';
|
|||
|
const AUTOCOMPLETE_ATTRIBUTE_ABSENCE_MARKER = 'hammerhead|autocomplete-attribute-absence-marker';
|
|||
|
class DomProcessor {
|
|||
|
constructor(adapter) {
|
|||
|
this.adapter = adapter;
|
|||
|
this.HTML_PROCESSING_REQUIRED_EVENT = 'hammerhead|event|html-processing-required';
|
|||
|
this.SVG_XLINK_HREF_TAGS = SVG_XLINK_HREF_TAGS;
|
|||
|
this.AUTOCOMPLETE_ATTRIBUTE_ABSENCE_MARKER = AUTOCOMPLETE_ATTRIBUTE_ABSENCE_MARKER;
|
|||
|
this.PROCESSED_PRELOAD_LINK_CONTENT_TYPE = PROCESSED_PRELOAD_LINK_CONTENT_TYPE;
|
|||
|
this.MODULE_PRELOAD_LINK_REL = MODULE_PRELOAD_LINK_REL;
|
|||
|
this.forceProxySrcForImage = false;
|
|||
|
this.allowMultipleWindows = false;
|
|||
|
this.proxyless = false;
|
|||
|
this.EVENTS = this.adapter.EVENTS;
|
|||
|
this.elementProcessorPatterns = this._createProcessorPatterns(this.adapter);
|
|||
|
}
|
|||
|
static isTagWithTargetAttr(tagName) {
|
|||
|
return !!tagName && attributes_1.TARGET_ATTR_TAGS.target.indexOf(tagName) > -1;
|
|||
|
}
|
|||
|
static isTagWithFormTargetAttr(tagName) {
|
|||
|
return !!tagName && attributes_1.TARGET_ATTR_TAGS.formtarget.indexOf(tagName) > -1;
|
|||
|
}
|
|||
|
static isTagWithIntegrityAttr(tagName) {
|
|||
|
return !!tagName && INTEGRITY_ATTR_TAGS.indexOf(tagName) !== -1;
|
|||
|
}
|
|||
|
static isIframeFlagTag(tagName) {
|
|||
|
return !!tagName && IFRAME_FLAG_TAGS.indexOf(tagName) !== -1;
|
|||
|
}
|
|||
|
static isAddedAutocompleteAttr(attrName, storedAttrValue) {
|
|||
|
return attrName === 'autocomplete' && storedAttrValue === AUTOCOMPLETE_ATTRIBUTE_ABSENCE_MARKER;
|
|||
|
}
|
|||
|
static processJsAttrValue(value, { isJsProtocol, isEventAttr }) {
|
|||
|
if (isJsProtocol)
|
|||
|
value = value.replace(JAVASCRIPT_PROTOCOL_REG_EX, '');
|
|||
|
value = (0, script_1.processScript)(value, false, isJsProtocol && !isEventAttr, void 0);
|
|||
|
if (isJsProtocol)
|
|||
|
value = 'javascript:' + value; // eslint-disable-line no-script-url
|
|||
|
return value;
|
|||
|
}
|
|||
|
static getStoredAttrName(attr) {
|
|||
|
return attr + internal_attributes_1.default.storedAttrPostfix;
|
|||
|
}
|
|||
|
static isJsProtocol(value) {
|
|||
|
return JAVASCRIPT_PROTOCOL_REG_EX.test(value);
|
|||
|
}
|
|||
|
static _isHtmlImportLink(tagName, relAttr) {
|
|||
|
return !!tagName && !!relAttr && tagName === 'link' && relAttr === 'import';
|
|||
|
}
|
|||
|
static isElementProcessed(el) {
|
|||
|
// @ts-ignore
|
|||
|
return el[ELEMENT_PROCESSED];
|
|||
|
}
|
|||
|
static setElementProcessed(el, processed) {
|
|||
|
// @ts-ignore
|
|||
|
el[ELEMENT_PROCESSED] = processed;
|
|||
|
}
|
|||
|
_getRelAttribute(el) {
|
|||
|
return String(this.adapter.getAttr(el, 'rel')).toLowerCase();
|
|||
|
}
|
|||
|
_getAsAttribute(el) {
|
|||
|
return String(this.adapter.getAttr(el, 'as')).toLowerCase();
|
|||
|
}
|
|||
|
_createProcessorPatterns(adapter) {
|
|||
|
const selectors = {
|
|||
|
HAS_HREF_ATTR: (el) => this.isUrlAttr(el, 'href'),
|
|||
|
HAS_SRC_ATTR: (el) => this.isUrlAttr(el, 'src'),
|
|||
|
HAS_SRCSET_ATTR: (el) => this.isUrlAttr(el, 'srcset'),
|
|||
|
HAS_ACTION_ATTR: (el) => this.isUrlAttr(el, 'action'),
|
|||
|
HAS_FORMACTION_ATTR: (el) => this.isUrlAttr(el, 'formaction'),
|
|||
|
HAS_FORMTARGET_ATTR: (el) => {
|
|||
|
return DomProcessor.isTagWithFormTargetAttr(adapter.getTagName(el)) && adapter.hasAttr(el, 'formtarget');
|
|||
|
},
|
|||
|
HAS_MANIFEST_ATTR: (el) => this.isUrlAttr(el, 'manifest'),
|
|||
|
HAS_DATA_ATTR: (el) => this.isUrlAttr(el, 'data'),
|
|||
|
HAS_SRCDOC_ATTR: (el) => {
|
|||
|
const tagName = this.adapter.getTagName(el);
|
|||
|
return (tagName === 'iframe' || tagName === 'frame') && adapter.hasAttr(el, 'srcdoc');
|
|||
|
},
|
|||
|
HTTP_EQUIV_META: (el) => {
|
|||
|
const tagName = adapter.getTagName(el);
|
|||
|
return tagName === 'meta' && adapter.hasAttr(el, 'http-equiv');
|
|||
|
},
|
|||
|
ALL: () => true,
|
|||
|
IS_SCRIPT: (el) => adapter.getTagName(el) === 'script',
|
|||
|
IS_LINK: (el) => adapter.getTagName(el) === 'link',
|
|||
|
IS_INPUT: (el) => adapter.getTagName(el) === 'input',
|
|||
|
IS_FILE_INPUT: (el) => {
|
|||
|
return adapter.getTagName(el) === 'input' &&
|
|||
|
adapter.hasAttr(el, 'type') &&
|
|||
|
adapter.getAttr(el, 'type').toLowerCase() === 'file';
|
|||
|
},
|
|||
|
IS_STYLE: (el) => adapter.getTagName(el) === 'style',
|
|||
|
HAS_EVENT_HANDLER: (el) => adapter.hasEventHandler(el),
|
|||
|
IS_SANDBOXED_IFRAME: (el) => {
|
|||
|
const tagName = adapter.getTagName(el);
|
|||
|
return (tagName === 'iframe' || tagName === 'frame') && adapter.hasAttr(el, 'sandbox');
|
|||
|
},
|
|||
|
IS_SVG_ELEMENT_WITH_XLINK_HREF_ATTR: (el) => {
|
|||
|
return adapter.isSVGElement(el) &&
|
|||
|
adapter.hasAttr(el, 'xlink:href') &&
|
|||
|
SVG_XLINK_HREF_TAGS.indexOf(adapter.getTagName(el)) !== -1;
|
|||
|
},
|
|||
|
IS_SVG_ELEMENT_WITH_XML_BASE_ATTR: (el) => adapter.isSVGElement(el) && adapter.hasAttr(el, 'xml:base'),
|
|||
|
};
|
|||
|
return [
|
|||
|
{
|
|||
|
selector: selectors.HAS_FORMTARGET_ATTR,
|
|||
|
targetAttr: 'formtarget',
|
|||
|
elementProcessors: [this._processTargetBlank],
|
|||
|
},
|
|||
|
{
|
|||
|
selector: selectors.HAS_HREF_ATTR,
|
|||
|
urlAttr: 'href',
|
|||
|
targetAttr: 'target',
|
|||
|
elementProcessors: [this._processTargetBlank, this._processUrlAttrs, this._processUrlJsAttr],
|
|||
|
},
|
|||
|
{
|
|||
|
selector: selectors.HAS_SRC_ATTR,
|
|||
|
urlAttr: 'src',
|
|||
|
targetAttr: 'target',
|
|||
|
elementProcessors: [this._processTargetBlank, this._processUrlAttrs, this._processUrlJsAttr],
|
|||
|
},
|
|||
|
{
|
|||
|
selector: selectors.HAS_SRCSET_ATTR,
|
|||
|
urlAttr: 'srcset',
|
|||
|
targetAttr: 'target',
|
|||
|
elementProcessors: [this._processTargetBlank, this._processUrlAttrs, this._processUrlJsAttr],
|
|||
|
},
|
|||
|
{
|
|||
|
selector: selectors.HAS_ACTION_ATTR,
|
|||
|
urlAttr: 'action',
|
|||
|
targetAttr: 'target',
|
|||
|
elementProcessors: [this._processTargetBlank, this._processUrlAttrs, this._processUrlJsAttr],
|
|||
|
},
|
|||
|
{
|
|||
|
selector: selectors.HAS_FORMACTION_ATTR,
|
|||
|
urlAttr: 'formaction',
|
|||
|
targetAttr: 'formtarget',
|
|||
|
elementProcessors: [this._processUrlAttrs, this._processUrlJsAttr],
|
|||
|
},
|
|||
|
{
|
|||
|
selector: selectors.HAS_MANIFEST_ATTR,
|
|||
|
urlAttr: 'manifest',
|
|||
|
elementProcessors: [this._processUrlAttrs, this._processUrlJsAttr],
|
|||
|
},
|
|||
|
{
|
|||
|
selector: selectors.HAS_DATA_ATTR,
|
|||
|
urlAttr: 'data',
|
|||
|
elementProcessors: [this._processUrlAttrs, this._processUrlJsAttr],
|
|||
|
},
|
|||
|
{
|
|||
|
selector: selectors.HAS_SRCDOC_ATTR,
|
|||
|
elementProcessors: [this._processSrcdocAttr],
|
|||
|
},
|
|||
|
{
|
|||
|
selector: selectors.HTTP_EQUIV_META,
|
|||
|
urlAttr: 'content',
|
|||
|
elementProcessors: [this._processMetaElement],
|
|||
|
},
|
|||
|
{
|
|||
|
selector: selectors.IS_SCRIPT,
|
|||
|
elementProcessors: [this._processScriptElement, this._processIntegrityAttr],
|
|||
|
},
|
|||
|
{ selector: selectors.ALL, elementProcessors: [this._processStyleAttr] },
|
|||
|
{
|
|||
|
selector: selectors.IS_LINK,
|
|||
|
relAttr: 'rel',
|
|||
|
elementProcessors: [this._processIntegrityAttr, this._processRelPrefetch],
|
|||
|
},
|
|||
|
{ selector: selectors.IS_STYLE, elementProcessors: [this._processStylesheetElement] },
|
|||
|
{ selector: selectors.IS_INPUT, elementProcessors: [this._processAutoComplete] },
|
|||
|
{ selector: selectors.IS_FILE_INPUT, elementProcessors: [this._processRequired] },
|
|||
|
{ selector: selectors.HAS_EVENT_HANDLER, elementProcessors: [this._processEvtAttr] },
|
|||
|
{ selector: selectors.IS_SANDBOXED_IFRAME, elementProcessors: [this._processSandboxedIframe] },
|
|||
|
{
|
|||
|
selector: selectors.IS_SVG_ELEMENT_WITH_XLINK_HREF_ATTR,
|
|||
|
urlAttr: 'xlink:href',
|
|||
|
elementProcessors: [this._processSVGXLinkHrefAttr, this._processUrlAttrs],
|
|||
|
},
|
|||
|
{
|
|||
|
selector: selectors.IS_SVG_ELEMENT_WITH_XML_BASE_ATTR,
|
|||
|
urlAttr: 'xml:base',
|
|||
|
elementProcessors: [this._processUrlAttrs],
|
|||
|
},
|
|||
|
];
|
|||
|
}
|
|||
|
// API
|
|||
|
processElement(el, urlReplacer) {
|
|||
|
if (DomProcessor.isElementProcessed(el))
|
|||
|
return;
|
|||
|
for (const pattern of this.elementProcessorPatterns) {
|
|||
|
if (pattern.selector(el) && !this._isShadowElement(el)) {
|
|||
|
for (const processor of pattern.elementProcessors)
|
|||
|
processor.call(this, el, urlReplacer, pattern);
|
|||
|
DomProcessor.setElementProcessed(el, true);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
// Utils
|
|||
|
getElementResourceType(el) {
|
|||
|
const tagName = this.adapter.getTagName(el);
|
|||
|
if (tagName === 'link' && (this._getAsAttribute(el) === PROCESSED_PRELOAD_LINK_CONTENT_TYPE ||
|
|||
|
this._getRelAttribute(el) === MODULE_PRELOAD_LINK_REL))
|
|||
|
return (0, url_1.getResourceTypeString)({ isScript: true });
|
|||
|
return (0, url_1.getResourceTypeString)({
|
|||
|
isIframe: tagName === 'iframe' || tagName === 'frame' || this._isOpenLinkInIframe(el),
|
|||
|
isForm: tagName === 'form' || tagName === 'input' || tagName === 'button',
|
|||
|
isScript: tagName === 'script',
|
|||
|
isHtmlImport: tagName === 'link' && this._getRelAttribute(el) === 'import',
|
|||
|
isObject: tagName === 'object',
|
|||
|
});
|
|||
|
}
|
|||
|
isUrlAttr(el, attr, ns) {
|
|||
|
const tagName = this.adapter.getTagName(el);
|
|||
|
attr = attr ? attr.toLowerCase() : attr;
|
|||
|
// @ts-ignore
|
|||
|
if (attributes_1.URL_ATTR_TAGS[attr] && attributes_1.URL_ATTR_TAGS[attr].indexOf(tagName) !== -1)
|
|||
|
return true;
|
|||
|
return this.adapter.isSVGElement(el) && (attr === 'xml:base' || attr === 'base' && ns === namespaces_1.XML_NAMESPACE);
|
|||
|
}
|
|||
|
getUrlAttr(el) {
|
|||
|
const tagName = this.adapter.getTagName(el);
|
|||
|
for (const urlAttr of attributes_1.URL_ATTRS) {
|
|||
|
// @ts-ignore
|
|||
|
if (attributes_1.URL_ATTR_TAGS[urlAttr].indexOf(tagName) !== -1)
|
|||
|
return urlAttr;
|
|||
|
}
|
|||
|
return null;
|
|||
|
}
|
|||
|
getTargetAttr(el) {
|
|||
|
const tagName = this.adapter.getTagName(el);
|
|||
|
for (const targetAttr of attributes_1.TARGET_ATTRS) {
|
|||
|
// @ts-ignore
|
|||
|
if (attributes_1.TARGET_ATTR_TAGS[targetAttr].indexOf(tagName) > -1)
|
|||
|
return targetAttr;
|
|||
|
}
|
|||
|
return null;
|
|||
|
}
|
|||
|
_isOpenLinkInIframe(el) {
|
|||
|
const tagName = this.adapter.getTagName(el);
|
|||
|
const targetAttr = this.getTargetAttr(el);
|
|||
|
const target = targetAttr ? this.adapter.getAttr(el, targetAttr) : null;
|
|||
|
const rel = this._getRelAttribute(el);
|
|||
|
if (target !== '_top') {
|
|||
|
const isImageInput = tagName === 'input' && this.adapter.getAttr(el, 'type') === 'image';
|
|||
|
const mustProcessTag = !isImageInput && DomProcessor.isIframeFlagTag(tagName) ||
|
|||
|
DomProcessor._isHtmlImportLink(tagName, rel);
|
|||
|
const isNameTarget = target ? target[0] !== '_' : false;
|
|||
|
if (target === '_parent')
|
|||
|
return mustProcessTag && !this.adapter.isTopParentIframe(el);
|
|||
|
if (mustProcessTag && (this.adapter.hasIframeParent(el) || isNameTarget && this.adapter.isExistingTarget(target, el)))
|
|||
|
return true;
|
|||
|
}
|
|||
|
return false;
|
|||
|
}
|
|||
|
_isShadowElement(el) {
|
|||
|
const className = this.adapter.getClassName(el);
|
|||
|
return typeof className === 'string' && className.indexOf(class_name_1.default.postfix) > -1;
|
|||
|
}
|
|||
|
// Element processors
|
|||
|
_processAutoComplete(el) {
|
|||
|
const storedUrlAttr = DomProcessor.getStoredAttrName('autocomplete');
|
|||
|
const processed = this.adapter.hasAttr(el, storedUrlAttr);
|
|||
|
const attrValue = this.adapter.getAttr(el, processed ? storedUrlAttr : 'autocomplete');
|
|||
|
if (!processed) {
|
|||
|
this.adapter.setAttr(el, storedUrlAttr, attrValue || attrValue ===
|
|||
|
''
|
|||
|
? attrValue
|
|||
|
: AUTOCOMPLETE_ATTRIBUTE_ABSENCE_MARKER);
|
|||
|
}
|
|||
|
this.adapter.setAttr(el, 'autocomplete', 'off');
|
|||
|
}
|
|||
|
_processRequired(el) {
|
|||
|
const storedRequired = DomProcessor.getStoredAttrName('required');
|
|||
|
const processed = this.adapter.hasAttr(el, storedRequired);
|
|||
|
if (!processed && this.adapter.hasAttr(el, 'required')) {
|
|||
|
const attrValue = this.adapter.getAttr(el, 'required');
|
|||
|
this.adapter.setAttr(el, storedRequired, attrValue);
|
|||
|
this.adapter.removeAttr(el, 'required');
|
|||
|
}
|
|||
|
}
|
|||
|
// NOTE: We simply remove the 'integrity' attribute because its value will not be relevant after the script
|
|||
|
// content changes (http://www.w3.org/TR/SRI/). If this causes problems in the future, we will need to generate
|
|||
|
// the correct SHA for the changed script.
|
|||
|
// In addition, we create stored 'integrity' attribute with the current 'integrity' attribute value. (GH-235)
|
|||
|
_processIntegrityAttr(el) {
|
|||
|
const storedIntegrityAttr = DomProcessor.getStoredAttrName('integrity');
|
|||
|
const processed = this.adapter.hasAttr(el, storedIntegrityAttr) && !this.adapter.hasAttr(el, 'integrity');
|
|||
|
const attrValue = this.adapter.getAttr(el, processed ? storedIntegrityAttr : 'integrity');
|
|||
|
if (attrValue)
|
|||
|
this.adapter.setAttr(el, storedIntegrityAttr, attrValue);
|
|||
|
if (!processed)
|
|||
|
this.adapter.removeAttr(el, 'integrity');
|
|||
|
}
|
|||
|
// NOTE: We simply remove the 'rel' attribute if rel='prefetch' and use stored 'rel' attribute, because the prefetch
|
|||
|
// resource type is unknown. https://github.com/DevExpress/testcafe/issues/2528
|
|||
|
_processRelPrefetch(el, _urlReplacer, pattern) {
|
|||
|
if (!pattern.relAttr)
|
|||
|
return;
|
|||
|
const storedRelAttr = DomProcessor.getStoredAttrName(pattern.relAttr);
|
|||
|
const processed = this.adapter.hasAttr(el, storedRelAttr) && !this.adapter.hasAttr(el, pattern.relAttr);
|
|||
|
const attrValue = this.adapter.getAttr(el, processed ? storedRelAttr : pattern.relAttr);
|
|||
|
if (attrValue) {
|
|||
|
const formatedValue = (0, string_trim_1.default)(attrValue.toLowerCase());
|
|||
|
if (formatedValue === 'prefetch') {
|
|||
|
this.adapter.setAttr(el, storedRelAttr, attrValue);
|
|||
|
if (!processed)
|
|||
|
this.adapter.removeAttr(el, pattern.relAttr);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
_processJsAttr(el, attrName, { isJsProtocol, isEventAttr }) {
|
|||
|
const storedUrlAttr = DomProcessor.getStoredAttrName(attrName);
|
|||
|
const processed = this.adapter.hasAttr(el, storedUrlAttr);
|
|||
|
const attrValue = this.adapter.getAttr(el, processed ? storedUrlAttr : attrName) || '';
|
|||
|
const processedValue = DomProcessor.processJsAttrValue(attrValue, { isJsProtocol, isEventAttr });
|
|||
|
if (attrValue !== processedValue) {
|
|||
|
this.adapter.setAttr(el, storedUrlAttr, attrValue);
|
|||
|
this.adapter.setAttr(el, attrName, processedValue);
|
|||
|
}
|
|||
|
}
|
|||
|
_processEvtAttr(el) {
|
|||
|
const events = this.adapter.EVENTS;
|
|||
|
for (let i = 0; i < events.length; i++) {
|
|||
|
const attrValue = this.adapter.getAttr(el, events[i]);
|
|||
|
if (attrValue) {
|
|||
|
this._processJsAttr(el, events[i], {
|
|||
|
isJsProtocol: DomProcessor.isJsProtocol(attrValue),
|
|||
|
isEventAttr: true,
|
|||
|
});
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
_processMetaElement(el, urlReplacer, pattern) {
|
|||
|
const httpEquivAttrValue = (this.adapter.getAttr(el, 'http-equiv') || '').toLowerCase();
|
|||
|
if (httpEquivAttrValue === builtin_header_names_1.default.refresh && pattern.urlAttr) {
|
|||
|
let attr = this.adapter.getAttr(el, pattern.urlAttr) || '';
|
|||
|
attr = (0, url_1.processMetaRefreshContent)(attr, urlReplacer);
|
|||
|
this.adapter.setAttr(el, pattern.urlAttr, attr);
|
|||
|
}
|
|||
|
// TODO: remove after https://github.com/DevExpress/testcafe-hammerhead/issues/244 implementation
|
|||
|
else if (httpEquivAttrValue === builtin_header_names_1.default.contentSecurityPolicy) {
|
|||
|
this.adapter.removeAttr(el, 'http-equiv');
|
|||
|
this.adapter.removeAttr(el, 'content');
|
|||
|
}
|
|||
|
}
|
|||
|
_processSandboxedIframe(el) {
|
|||
|
let attrValue = this.adapter.getAttr(el, 'sandbox') || '';
|
|||
|
const allowSameOrigin = attrValue.indexOf('allow-same-origin') !== -1;
|
|||
|
const allowScripts = attrValue.indexOf('allow-scripts') !== -1;
|
|||
|
const storedAttr = DomProcessor.getStoredAttrName('sandbox');
|
|||
|
this.adapter.setAttr(el, storedAttr, attrValue);
|
|||
|
if (!allowSameOrigin || !allowScripts) {
|
|||
|
attrValue += !allowSameOrigin ? ' allow-same-origin' : '';
|
|||
|
attrValue += !allowScripts ? ' allow-scripts' : '';
|
|||
|
}
|
|||
|
this.adapter.setAttr(el, 'sandbox', attrValue);
|
|||
|
}
|
|||
|
_processScriptElement(script, urlReplacer) {
|
|||
|
const scriptContent = this.adapter.getScriptContent(script);
|
|||
|
if (!scriptContent || !this.adapter.needToProcessContent(script))
|
|||
|
return;
|
|||
|
const scriptProcessedOnServer = (0, script_1.isScriptProcessed)(scriptContent);
|
|||
|
if (scriptProcessedOnServer)
|
|||
|
return;
|
|||
|
// NOTE: We do not process scripts that are not executed during page load. We process scripts of types like
|
|||
|
// text/javascript, application/javascript etc. (a complete list of MIME types is specified in the w3c.org
|
|||
|
// html5 specification). If the type is not set, it is considered 'text/javascript' by default.
|
|||
|
const scriptType = this.adapter.getAttr(script, 'type');
|
|||
|
const isExecutableScript = !scriptType || EXECUTABLE_SCRIPT_TYPES_REG_EX.test(scriptType);
|
|||
|
if (isExecutableScript) {
|
|||
|
let result = scriptContent;
|
|||
|
let commentPrefix = '';
|
|||
|
const commentPrefixMatch = result.match(HTML_COMMENT_PREFIX_REG_EX);
|
|||
|
let commentPostfix = '';
|
|||
|
let commentPostfixMatch = null;
|
|||
|
const hasCDATA = CDATA_REG_EX.test(result);
|
|||
|
if (commentPrefixMatch) {
|
|||
|
commentPrefix = commentPrefixMatch[0];
|
|||
|
commentPostfixMatch = result.match(HTML_COMMENT_POSTFIX_REG_EX);
|
|||
|
if (commentPostfixMatch)
|
|||
|
commentPostfix = commentPostfixMatch[0];
|
|||
|
else if (!HTML_COMMENT_SIMPLE_POSTFIX_REG_EX.test(commentPrefix))
|
|||
|
commentPostfix = '//-->';
|
|||
|
result = result.replace(commentPrefix, '').replace(commentPostfix, '');
|
|||
|
}
|
|||
|
if (hasCDATA)
|
|||
|
result = result.replace(CDATA_REG_EX, '$2');
|
|||
|
result = commentPrefix + (0, script_1.processScript)(result, true, false, urlReplacer) + commentPostfix;
|
|||
|
if (hasCDATA)
|
|||
|
result = '\n//<![CDATA[\n' + result + '//]]>';
|
|||
|
this.adapter.setScriptContent(script, result);
|
|||
|
}
|
|||
|
}
|
|||
|
_processStyleAttr(el, urlReplacer) {
|
|||
|
const style = this.adapter.getAttr(el, 'style');
|
|||
|
if (style)
|
|||
|
this.adapter.setAttr(el, 'style', style_1.default.process(style, urlReplacer, false));
|
|||
|
}
|
|||
|
_processStylesheetElement(el, urlReplacer) {
|
|||
|
let content = this.adapter.getStyleContent(el);
|
|||
|
if (content && urlReplacer && this.adapter.needToProcessContent(el)) {
|
|||
|
content = style_1.default.process(content, urlReplacer, true);
|
|||
|
this.adapter.setStyleContent(el, content);
|
|||
|
}
|
|||
|
}
|
|||
|
_processTargetBlank(el, _urlReplacer, pattern) {
|
|||
|
if (this.allowMultipleWindows || !pattern.targetAttr)
|
|||
|
return;
|
|||
|
const storedTargetAttr = DomProcessor.getStoredAttrName(pattern.targetAttr);
|
|||
|
const processed = this.adapter.hasAttr(el, storedTargetAttr);
|
|||
|
if (processed)
|
|||
|
return;
|
|||
|
let attrValue = this.adapter.getAttr(el, pattern.targetAttr);
|
|||
|
// NOTE: Value may have whitespace.
|
|||
|
attrValue = attrValue && attrValue.replace(/\s/g, '');
|
|||
|
if (attrValue === '_blank') {
|
|||
|
this.adapter.setAttr(el, pattern.targetAttr, '_top');
|
|||
|
this.adapter.setAttr(el, storedTargetAttr, attrValue);
|
|||
|
}
|
|||
|
}
|
|||
|
_processUrlAttrs(el, urlReplacer, pattern) {
|
|||
|
if (!pattern.urlAttr || this.proxyless)
|
|||
|
return;
|
|||
|
const storedUrlAttr = DomProcessor.getStoredAttrName(pattern.urlAttr);
|
|||
|
const resourceUrl = this.adapter.getAttr(el, pattern.urlAttr);
|
|||
|
const isSpecialPageUrl = !!resourceUrl && (0, url_1.isSpecialPage)(resourceUrl);
|
|||
|
const processedOnServer = this.adapter.hasAttr(el, storedUrlAttr);
|
|||
|
if ((!resourceUrl && resourceUrl !== '' || processedOnServer) || //eslint-disable-line @typescript-eslint/no-extra-parens
|
|||
|
!(0, url_1.isSupportedProtocol)(resourceUrl) && !isSpecialPageUrl)
|
|||
|
return;
|
|||
|
const elTagName = this.adapter.getTagName(el);
|
|||
|
const isIframe = elTagName === 'iframe' || elTagName === 'frame';
|
|||
|
const isScript = elTagName === 'script';
|
|||
|
const isAnchor = elTagName === 'a';
|
|||
|
const isUrlsSet = pattern.urlAttr === 'srcset';
|
|||
|
const target = pattern.targetAttr ? this.adapter.getAttr(el, pattern.targetAttr) : null;
|
|||
|
// NOTE: Elements with target=_parent shouldn’t be processed on the server,because we don't
|
|||
|
// know what is the parent of the processed page (an iframe or the top window).
|
|||
|
if (!this.adapter.needToProcessUrl(elTagName, target || ''))
|
|||
|
return;
|
|||
|
const resourceType = this.getElementResourceType(el) || '';
|
|||
|
const parsedResourceUrl = (0, url_1.parseUrl)(resourceUrl);
|
|||
|
const isRelativePath = parsedResourceUrl.protocol !== 'file:' && !parsedResourceUrl.host;
|
|||
|
const charsetAttrValue = isScript && this.adapter.getAttr(el, 'charset') || '';
|
|||
|
const isImgWithoutSrc = elTagName === 'img' && resourceUrl === '';
|
|||
|
const isIframeWithEmptySrc = isIframe && resourceUrl === '';
|
|||
|
const parsedProxyUrl = (0, url_1.parseProxyUrl)(urlReplacer('/'));
|
|||
|
let isCrossDomainSrc = false;
|
|||
|
let proxyUrl = resourceUrl;
|
|||
|
// NOTE: Only a non-relative iframe src can be cross-domain.
|
|||
|
if (isIframe && !isSpecialPageUrl && !isRelativePath && parsedProxyUrl)
|
|||
|
isCrossDomainSrc = !this.adapter.sameOriginCheck(parsedProxyUrl.destUrl, resourceUrl);
|
|||
|
if ((!isSpecialPageUrl || isAnchor) && !isImgWithoutSrc && !isIframeWithEmptySrc) {
|
|||
|
proxyUrl = elTagName === 'img' && !this.forceProxySrcForImage && !isUrlsSet
|
|||
|
? (0, url_1.resolveUrlAsDest)(resourceUrl, urlReplacer)
|
|||
|
: urlReplacer(resourceUrl, resourceType, charsetAttrValue, isCrossDomainSrc, isUrlsSet);
|
|||
|
}
|
|||
|
this.adapter.setAttr(el, storedUrlAttr, resourceUrl);
|
|||
|
this.adapter.setAttr(el, pattern.urlAttr, proxyUrl);
|
|||
|
}
|
|||
|
_processSrcdocAttr(el) {
|
|||
|
const storedAttr = DomProcessor.getStoredAttrName('srcdoc');
|
|||
|
const html = this.adapter.getAttr(el, 'srcdoc') || '';
|
|||
|
const processedHtml = this.adapter.processSrcdocAttr(html);
|
|||
|
this.adapter.setAttr(el, storedAttr, html);
|
|||
|
this.adapter.setAttr(el, 'srcdoc', processedHtml);
|
|||
|
}
|
|||
|
_processUrlJsAttr(el, _urlReplacer, pattern) {
|
|||
|
if (pattern.urlAttr && DomProcessor.isJsProtocol(this.adapter.getAttr(el, pattern.urlAttr) || ''))
|
|||
|
this._processJsAttr(el, pattern.urlAttr, { isJsProtocol: true, isEventAttr: false });
|
|||
|
}
|
|||
|
_processSVGXLinkHrefAttr(el, _urlReplacer, pattern) {
|
|||
|
if (!pattern.urlAttr)
|
|||
|
return;
|
|||
|
const attrValue = this.adapter.getAttr(el, pattern.urlAttr) || '';
|
|||
|
if (url_1.HASH_RE.test(attrValue)) {
|
|||
|
const storedUrlAttr = DomProcessor.getStoredAttrName(pattern.urlAttr);
|
|||
|
this.adapter.setAttr(el, storedUrlAttr, attrValue);
|
|||
|
}
|
|||
|
}
|
|||
|
}
|
|||
|
exports.default = DomProcessor;module.exports = exports.default;
|
|||
|
|