"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 });
exports.processMetaRefreshContent = exports.updateScriptImportUrls = exports.prepareUrl = exports.omitDefaultPort = exports.ensureOriginTrailingSlash = exports.isValidUrl = exports.isRelativeUrl = exports.isSpecialPage = exports.ensureTrailingSlash = exports.processSpecialChars = exports.correctMultipleSlashes = exports.handleUrlsSet = exports.formatUrl = exports.resolveUrlAsDest = exports.isSupportedProtocol = exports.parseUrl = exports.getPathname = exports.parseProxyUrl = exports.getDomain = exports.getProxyUrl = exports.getURLString = exports.sameOriginCheck = exports.isSubDomain = exports.restoreShortOrigin = exports.getResourceTypeString = exports.parseResourceType = exports.Credentials = exports.HTTPS_DEFAULT_PORT = exports.HTTP_DEFAULT_PORT = exports.SPECIAL_PAGES = exports.SPECIAL_ERROR_PAGE = exports.SPECIAL_BLANK_PAGE = exports.TRAILING_SLASH_RE = exports.REQUEST_DESCRIPTOR_SESSION_INFO_VALUES_SEPARATOR = exports.REQUEST_DESCRIPTOR_VALUES_SEPARATOR = exports.HASH_RE = exports.SUPPORTED_PROTOCOL_RE = void 0;
const string_trim_1 = __importDefault(require("./string-trim"));
const URL_RE = /^\s*([\w-]+?:)?(?:\/\/(?:([^/]+)@)?(([^/%?;#: ]*)(?::(\d+))?))?(.*?)\s*$/;
const PROTOCOL_RE = /^([\w-]+?:)(\/\/|[^\\/]|$)/;
const QUERY_AND_HASH_RE = /(\?.+|#[^#]*)$/;
const PATH_AFTER_HOST_RE = /^\/([^/]+?)\/([\S\s]+)$/;
const HTTP_RE = /^https?:/;
const FILE_RE = /^file:/i;
const SHORT_ORIGIN_RE = /^http(s)?:\/\//;
const IS_SECURE_ORIGIN_RE = /^s\*/;
const META_REFRESH_RE = /^(.+?[;,]\s*(?:url\s*=\s*)?(['"])?)(.+?)?(\2)?$/i;
exports.SUPPORTED_PROTOCOL_RE = /^(?:https?|file):/i;
exports.HASH_RE = /^#/;
exports.REQUEST_DESCRIPTOR_VALUES_SEPARATOR = '!';
exports.REQUEST_DESCRIPTOR_SESSION_INFO_VALUES_SEPARATOR = '*';
exports.TRAILING_SLASH_RE = /\/$/;
exports.SPECIAL_BLANK_PAGE = 'about:blank';
exports.SPECIAL_ERROR_PAGE = 'about:error';
exports.SPECIAL_PAGES = [exports.SPECIAL_BLANK_PAGE, exports.SPECIAL_ERROR_PAGE];
exports.HTTP_DEFAULT_PORT = '80';
exports.HTTPS_DEFAULT_PORT = '443';
var Credentials;
(function (Credentials) {
    Credentials[Credentials["include"] = 0] = "include";
    Credentials[Credentials["sameOrigin"] = 1] = "sameOrigin";
    Credentials[Credentials["omit"] = 2] = "omit";
    Credentials[Credentials["unknown"] = 3] = "unknown";
})(Credentials = exports.Credentials || (exports.Credentials = {})); // eslint-disable-line no-shadow
const SPECIAL_PAGE_DEST_RESOURCE_INFO = {
    protocol: 'about:',
    host: '',
    hostname: '',
    port: '',
    partAfterHost: '',
};
const RESOURCE_TYPES = [
    { name: 'isIframe', flag: 'i' },
    { name: 'isForm', flag: 'f' },
    { name: 'isScript', flag: 's' },
    { name: 'isEventSource', flag: 'e' },
    { name: 'isHtmlImport', flag: 'h' },
    { name: 'isWebSocket', flag: 'w' },
    { name: 'isServiceWorker', flag: 'c' },
    { name: 'isAjax', flag: 'a' },
    { name: 'isObject', flag: 'o' },
];
function parseResourceType(resourceType) {
    const parsedResourceType = {};
    if (!resourceType)
        return parsedResourceType;
    for (const { name, flag } of RESOURCE_TYPES) {
        if (resourceType.indexOf(flag) > -1)
            parsedResourceType[name] = true;
    }
    return parsedResourceType;
}
exports.parseResourceType = parseResourceType;
function getResourceTypeString(parsedResourceType) {
    if (!parsedResourceType)
        return null;
    let resourceType = '';
    for (const { name, flag } of RESOURCE_TYPES) {
        if (parsedResourceType[name])
            resourceType += flag;
    }
    return resourceType || null;
}
exports.getResourceTypeString = getResourceTypeString;
function makeShortOrigin(origin) {
    return origin === 'null' ? '' : origin.replace(SHORT_ORIGIN_RE, (_, secure) => secure ? 's*' : '');
}
function restoreShortOrigin(origin) {
    if (!origin)
        return 'null';
    return IS_SECURE_ORIGIN_RE.test(origin) ? origin.replace(IS_SECURE_ORIGIN_RE, 'https://') : 'http://' + origin;
}
exports.restoreShortOrigin = restoreShortOrigin;
function isSubDomain(domain, subDomain) {
    domain = domain.replace(/^www./i, '');
    subDomain = subDomain.replace(/^www./i, '');
    if (domain === subDomain)
        return true;
    const index = subDomain.lastIndexOf(domain);
    return subDomain[index - 1] === '.' && subDomain.length === index + domain.length;
}
exports.isSubDomain = isSubDomain;
function sameOriginCheck(location, checkedUrl) {
    if (!checkedUrl)
        return true;
    const parsedCheckedUrl = parseUrl(checkedUrl);
    const isRelative = !parsedCheckedUrl.host;
    if (isRelative)
        return true;
    const parsedLocation = parseUrl(location);
    const parsedProxyLocation = parseProxyUrl(location);
    if (parsedCheckedUrl.host === parsedLocation.host && parsedCheckedUrl.protocol === parsedLocation.protocol)
        return true;
    const parsedDestUrl = parsedProxyLocation ? parsedProxyLocation.destResourceInfo : parsedLocation;
    if (!parsedDestUrl)
        return false;
    const isSameProtocol = !parsedCheckedUrl.protocol || parsedCheckedUrl.protocol === parsedDestUrl.protocol;
    const portsEq = !parsedDestUrl.port && !parsedCheckedUrl.port ||
        parsedDestUrl.port && parsedDestUrl.port.toString() === parsedCheckedUrl.port;
    return isSameProtocol && !!portsEq && parsedDestUrl.hostname === parsedCheckedUrl.hostname;
}
exports.sameOriginCheck = sameOriginCheck;
// NOTE: Convert the destination protocol and hostname to the lower case. (GH-1)
function convertHostToLowerCase(url) {
    const parsedUrl = parseUrl(url);
    parsedUrl.protocol = parsedUrl.protocol && parsedUrl.protocol.toLowerCase();
    parsedUrl.host = parsedUrl.host && parsedUrl.host.toLowerCase();
    return formatUrl(parsedUrl);
}
function getURLString(url) {
    // TODO: fix it
    // eslint-disable-next-line no-undef
    if (url === null && /iPad|iPhone/i.test(window.navigator.userAgent))
        return '';
    return String(url).replace(/[\n\t]/g, '');
}
exports.getURLString = getURLString;
function getProxyUrl(url, opts) {
    const sessionInfo = [opts.sessionId];
    if (opts.windowId)
        sessionInfo.push(opts.windowId);
    const params = [sessionInfo.join(exports.REQUEST_DESCRIPTOR_SESSION_INFO_VALUES_SEPARATOR)];
    if (opts.resourceType)
        params.push(opts.resourceType);
    if (opts.charset)
        params.push(opts.charset.toLowerCase());
    if (typeof opts.credentials === 'number')
        params.push(opts.credentials.toString());
    if (opts.reqOrigin)
        params.push(encodeURIComponent(makeShortOrigin(opts.reqOrigin)));
    const descriptor = params.join(exports.REQUEST_DESCRIPTOR_VALUES_SEPARATOR);
    const proxyProtocol = opts.proxyProtocol || 'http:';
    return `${proxyProtocol}//${opts.proxyHostname}:${opts.proxyPort}/${descriptor}/${convertHostToLowerCase(url)}`;
}
exports.getProxyUrl = getProxyUrl;
function getDomain(parsed) {
    if (parsed.protocol === 'file:')
        return 'null';
    return formatUrl({
        protocol: parsed.protocol,
        host: parsed.host,
        hostname: parsed.hostname,
        port: String(parsed.port || ''),
    });
}
exports.getDomain = getDomain;
function parseRequestDescriptor(desc) {
    const [sessionInfo, resourceType, ...resourceData] = desc.split(exports.REQUEST_DESCRIPTOR_VALUES_SEPARATOR);
    if (!sessionInfo)
        return null;
    const [sessionId, windowId] = sessionInfo.split(exports.REQUEST_DESCRIPTOR_SESSION_INFO_VALUES_SEPARATOR);
    const parsedDesc = { sessionId, resourceType: resourceType || null };
    if (windowId)
        parsedDesc.windowId = windowId;
    if (resourceType && resourceData.length) {
        const parsedResourceType = parseResourceType(resourceType);
        if (parsedResourceType.isScript || parsedResourceType.isServiceWorker)
            parsedDesc.charset = resourceData[0];
        else if (parsedResourceType.isWebSocket)
            parsedDesc.reqOrigin = decodeURIComponent(restoreShortOrigin(resourceData[0]));
        else if (parsedResourceType.isIframe && resourceData[0])
            parsedDesc.reqOrigin = decodeURIComponent(restoreShortOrigin(resourceData[0]));
        else if (parsedResourceType.isAjax) {
            parsedDesc.credentials = parseInt(resourceData[0], 10);
            if (resourceData.length === 2)
                parsedDesc.reqOrigin = decodeURIComponent(restoreShortOrigin(resourceData[1]));
        }
    }
    return parsedDesc;
}
function parseProxyUrl(proxyUrl) {
    // TODO: Remove it.
    const parsedUrl = parseUrl(proxyUrl);
    if (!parsedUrl.partAfterHost)
        return null;
    const match = parsedUrl.partAfterHost.match(PATH_AFTER_HOST_RE);
    if (!match)
        return null;
    const parsedDesc = parseRequestDescriptor(match[1]);
    // NOTE: We should have, at least, the job uid and the owner token.
    if (!parsedDesc)
        return null;
    let destUrl = match[2];
    // Browser can redirect to a special page with hash (GH-1671)
    const destUrlWithoutHash = destUrl.replace(/#[\S\s]*$/, '');
    if (!isSpecialPage(destUrlWithoutHash) && !exports.SUPPORTED_PROTOCOL_RE.test(destUrl))
        return null;
    let destResourceInfo;
    if (isSpecialPage(destUrlWithoutHash))
        destResourceInfo = SPECIAL_PAGE_DEST_RESOURCE_INFO;
    else {
        destUrl = omitDefaultPort(destUrl);
        destResourceInfo = parseUrl(destUrl);
    }
    return {
        destUrl,
        destResourceInfo,
        partAfterHost: parsedUrl.partAfterHost,
        proxy: {
            hostname: parsedUrl.hostname || '',
            port: parsedUrl.port || '',
        },
        sessionId: parsedDesc.sessionId,
        resourceType: parsedDesc.resourceType,
        charset: parsedDesc.charset,
        reqOrigin: parsedDesc.reqOrigin,
        windowId: parsedDesc.windowId,
        credentials: parsedDesc.credentials,
    };
}
exports.parseProxyUrl = parseProxyUrl;
function getPathname(path) {
    return path.replace(QUERY_AND_HASH_RE, '');
}
exports.getPathname = getPathname;
function parseUrl(url) {
    url = processSpecialChars(url);
    if (!url)
        return {};
    const urlMatch = url.match(URL_RE);
    return urlMatch ? {
        protocol: urlMatch[1],
        auth: urlMatch[2],
        host: urlMatch[3],
        hostname: urlMatch[4],
        port: urlMatch[5],
        partAfterHost: urlMatch[6],
    } : {};
}
exports.parseUrl = parseUrl;
function isSupportedProtocol(url) {
    url = (0, string_trim_1.default)(url || '');
    const isHash = exports.HASH_RE.test(url);
    if (isHash)
        return false;
    const protocol = url.match(PROTOCOL_RE);
    if (!protocol)
        return true;
    return exports.SUPPORTED_PROTOCOL_RE.test(protocol[0]);
}
exports.isSupportedProtocol = isSupportedProtocol;
function resolveUrlAsDest(url, getProxyUrlMeth, isUrlsSet = false) {
    if (isUrlsSet)
        return handleUrlsSet(resolveUrlAsDest, url, getProxyUrlMeth);
    getProxyUrlMeth = getProxyUrlMeth || getProxyUrl;
    if (isSupportedProtocol(url)) {
        const proxyUrl = getProxyUrlMeth(url);
        const parsedProxyUrl = parseProxyUrl(proxyUrl);
        return parsedProxyUrl ? formatUrl(parsedProxyUrl.destResourceInfo) : url;
    }
    return url;
}
exports.resolveUrlAsDest = resolveUrlAsDest;
function formatUrl(parsedUrl) {
    // NOTE: the URL is relative.
    if (parsedUrl.protocol !== 'file:' && parsedUrl.protocol !== 'about:' &&
        !parsedUrl.host && (!parsedUrl.hostname || !parsedUrl.port))
        return parsedUrl.partAfterHost || '';
    let url = parsedUrl.protocol || '';
    if (parsedUrl.protocol !== 'about:')
        url += '//';
    if (parsedUrl.auth)
        url += parsedUrl.auth + '@';
    if (parsedUrl.host)
        url += parsedUrl.host;
    else if (parsedUrl.hostname) {
        url += parsedUrl.hostname;
        if (parsedUrl.port)
            url += ':' + parsedUrl.port;
    }
    if (parsedUrl.partAfterHost)
        url += parsedUrl.partAfterHost;
    return url;
}
exports.formatUrl = formatUrl;
function handleUrlsSet(handler, url, ...args) {
    const resourceUrls = url.split(',');
    const replacedUrls = [];
    for (const fullUrlStr of resourceUrls) {
        const [urlStr, postUrlStr] = fullUrlStr.replace(/ +/g, ' ').trim().split(' ');
        if (urlStr) {
            const replacedUrl = handler(urlStr, ...args);
            replacedUrls.push(replacedUrl + (postUrlStr ? ` ${postUrlStr}` : ''));
        }
    }
    return replacedUrls.join(',');
}
exports.handleUrlsSet = handleUrlsSet;
function correctMultipleSlashes(url, pageProtocol = '') {
    // NOTE: Remove unnecessary slashes from the beginning of the url and after scheme.
    // For example:
    // "//////example.com" -> "//example.com" (scheme-less HTTP(S) URL)
    // "////home/testcafe/documents" -> "///home/testcafe/documents" (scheme-less unix file URL)
    // "http:///example.com" -> "http://example.com"
    //
    // And add missing slashes after the file scheme.
    // "file://C:/document.txt" -> "file:///C:/document.txt"
    if (url.match(FILE_RE) || pageProtocol.match(FILE_RE)) {
        return url
            .replace(/^(file:)?\/+(\/\/\/.*$)/i, '$1$2')
            .replace(/^(file:)?\/*([A-Za-z]):/i, '$1///$2:');
    }
    return url.replace(/^(https?:)?\/+(\/\/.*$)/i, '$1$2');
}
exports.correctMultipleSlashes = correctMultipleSlashes;
function processSpecialChars(url) {
    return correctMultipleSlashes(getURLString(url));
}
exports.processSpecialChars = processSpecialChars;
function ensureTrailingSlash(srcUrl, processedUrl) {
    if (!isValidUrl(processedUrl))
        return processedUrl;
    const srcUrlEndsWithTrailingSlash = exports.TRAILING_SLASH_RE.test(srcUrl);
    const processedUrlEndsWithTrailingSlash = exports.TRAILING_SLASH_RE.test(processedUrl);
    if (srcUrlEndsWithTrailingSlash && !processedUrlEndsWithTrailingSlash)
        processedUrl += '/';
    else if (srcUrl && !srcUrlEndsWithTrailingSlash && processedUrlEndsWithTrailingSlash)
        processedUrl = processedUrl.replace(exports.TRAILING_SLASH_RE, '');
    return processedUrl;
}
exports.ensureTrailingSlash = ensureTrailingSlash;
function isSpecialPage(url) {
    return exports.SPECIAL_PAGES.indexOf(url) !== -1;
}
exports.isSpecialPage = isSpecialPage;
function isRelativeUrl(url) {
    const parsedUrl = parseUrl(url);
    return parsedUrl.protocol !== 'file:' && !parsedUrl.host;
}
exports.isRelativeUrl = isRelativeUrl;
function isValidPort(port) {
    const parsedPort = parseInt(port, 10);
    return parsedPort > 0 && parsedPort <= 65535;
}
function isValidUrl(url) {
    const parsedUrl = parseUrl(url);
    return parsedUrl.protocol === 'file:' || parsedUrl.protocol === 'about:' ||
        !!parsedUrl.hostname && (!parsedUrl.port || isValidPort(parsedUrl.port));
}
exports.isValidUrl = isValidUrl;
function ensureOriginTrailingSlash(url) {
    // NOTE: If you request an url containing only port, host and protocol
    // then browser adds the trailing slash itself.
    const parsedUrl = parseUrl(url);
    if (!parsedUrl.partAfterHost && parsedUrl.protocol && HTTP_RE.test(parsedUrl.protocol))
        return url + '/';
    return url;
}
exports.ensureOriginTrailingSlash = ensureOriginTrailingSlash;
function omitDefaultPort(url) {
    // NOTE: If you request an url containing default port
    // then browser remove this one itself.
    const parsedUrl = parseUrl(url);
    const hasDefaultPort = parsedUrl.protocol === 'https:' && parsedUrl.port === exports.HTTPS_DEFAULT_PORT ||
        parsedUrl.protocol === 'http:' && parsedUrl.port === exports.HTTP_DEFAULT_PORT;
    if (hasDefaultPort) {
        parsedUrl.host = parsedUrl.hostname;
        parsedUrl.port = '';
        return formatUrl(parsedUrl);
    }
    return url;
}
exports.omitDefaultPort = omitDefaultPort;
function prepareUrl(url) {
    url = omitDefaultPort(url);
    url = ensureOriginTrailingSlash(url);
    return url;
}
exports.prepareUrl = prepareUrl;
function updateScriptImportUrls(cachedScript, serverInfo, sessionId, windowId) {
    const regExp = new RegExp('(' + serverInfo.protocol + '//' + serverInfo.hostname + ':(?:' + serverInfo.port + '|' +
        serverInfo.crossDomainPort + ')/)[^/' + exports.REQUEST_DESCRIPTOR_VALUES_SEPARATOR + ']+', 'g');
    const pattern = '$1' + sessionId + (windowId ? exports.REQUEST_DESCRIPTOR_SESSION_INFO_VALUES_SEPARATOR + windowId : '');
    return cachedScript.replace(regExp, pattern);
}
exports.updateScriptImportUrls = updateScriptImportUrls;
function processMetaRefreshContent(content, urlReplacer) {
    const match = content.match(META_REFRESH_RE);
    if (!match || !match[3])
        return content;
    return match[1] + urlReplacer(match[3]) + (match[4] || '');
}
exports.processMetaRefreshContent = processMetaRefreshContent;