"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.shouldUseSecureCrossSiteCookies = exports.getInputData = void 0;
const cookie_1 = __importDefault(require("cookie"));
const is_in_subnet_1 = require("is-in-subnet");
const utils_1 = require("../utils");
const detect_1 = __importDefault(require("../lib/detect"));
const cookie_2 = require("../utils/cookie");
const compare_version_1 = require("../lib/compare_version");
const time_utils_1 = require("../lib/time_utils");
const constants_1 = require("../constants");
/**
 * @typedef {Object} ParsedInputData
 * @property {boolean}         requestType         The RequestType (editor, preview, or just standard) for this worker request.
 * @property {Object}          cookies             The visitor's cookies (via request header)
 * @property {URLSearchParams} queryParams         The query params of the page the visitor is requesting (via parent page querystring)
 * @property {string}          userAgent           The user agent of the visitor (via request headers)
 * @property {string}          uid                 The userId of the visitor if one has already been assigned (via request querystring or request headers)
 * @property {string}          url                 The url of the page the visitor is requesting
 */
/**
 * Given an incoming worker request, parse its query string and headers for
 * relevant input data to our decider.
 *
 * @param {Request} request
 * @return {ParsedInputData}
 */
function getInputData(request, logger) {
    let cookies = {}, cookieDomain, cookieExpirationDays, language, priorRedirectCookie, redirectInfo, requestHeaders, requestParams, requestType, requestUrlObject, requestUrlString, undecodedCookies, userAgent, parsedUA;
    let visitorIp, optOutCookieValue;
    let bypassLogSampling, forceVariationIds, inputParseError, isDebugMode, logLevel, metadata, optIn, webManifestS3Bucket, bustSnippetCache;
    let params = {};
    let isEs6Compatible = true;
    let first_session = true;
    try {
        requestUrlString = request.url;
        logger.updateMetadata('targetPageURLString', requestUrlString);
        try {
            requestUrlObject = requestUrlString
                ? new URL(requestUrlString)
                : null;
        }
        catch (e) {
            // This is the only type of error URL constructor should throw: https://developer.mozilla.org/en-US/docs/Web/API/URL/URL#Syntax
            if (e instanceof TypeError) {
                throw new Error(`Target page URL is not a valid URL string: ${requestUrlString}`);
            }
            throw e;
        }
        logger.updateMetadata('targetPageURL', requestUrlObject);
        if (!requestUrlObject) {
            throw new Error('Target page URL is null.');
        }
        requestHeaders = request.headers;
        // The URLSearchParams constructor is not available, so use an instance of URL to create an empty one if needed.
        requestParams =
            requestUrlObject?.searchParams ||
                new URL('http://localhost').searchParams; // This should only be encountered in local development
        userAgent = requestHeaders.get('User-Agent') || '';
        if (!userAgent) {
            throw new Error('Please provide the User-Agent header. See here for more details: ' +
                'https://docs.developers.optimizely.com/performance-edge/reference/edge-decider');
        }
        parsedUA = detect_1.default.parseUA(userAgent);
        isEs6Compatible = isBrowserEs6Compatible(parsedUA.browser, parsedUA.engine);
        // TODO(CJS-3774): Return an error if this header is not given
        const xForwardedFor = requestHeaders.get('X-forwarded-for')
            ? requestHeaders.get('X-forwarded-for') || ''
            : requestHeaders.get('CF-Connecting-IP') || '';
        const xForwardedForIP = xForwardedFor.split(',')[0].trim();
        if (xForwardedForIP && (0, is_in_subnet_1.isIP)(xForwardedForIP)) {
            visitorIp = xForwardedForIP;
        }
        visitorIp = visitorIp || null;
        language = (0, utils_1.getLanguageString)(requestHeaders.get('Accept-Language'));
        cookies = cookie_1.default.parse(requestHeaders.get('Cookie') || '');
        let session_cookie = cookies[constants_1.OptimizelyCookies.OPTIMIZELY_SESSION];
        logger.debug('Session cookie:', session_cookie);
        if (!session_cookie) {
            first_session = true;
        }
        else {
            if (session_cookie == '0') {
                first_session = false;
            }
            else {
                const sessionDate = new Date(session_cookie);
                first_session =
                    Date.now() - sessionDate.getTime() <= constants_1.SESSION_LENGTH_MS;
            }
        }
        logger.setLogKey('first_session', first_session);
        optOutCookieValue = cookies[constants_1.OptimizelyCookies.OPT_OUT];
        undecodedCookies = cookie_1.default.parse(requestHeaders.get('Cookie') || '', {
            decode: a => a,
        });
        // Check whether a X Web redirect (delivered via an Edge microsnippet *or* a regular snipppet) brought the visitor
        // to this page.
        priorRedirectCookie = undecodedCookies.optimizelyRedirectData;
        // TODO: Maybe port all of relay.parsePersistedString from client-js?
        //       That would allow us to properly validate the redirect cookie,
        //       and would allow us to parse the redirect referrer in a more maintainable manner.
        if (priorRedirectCookie) {
            redirectInfo = {
                experimentId: '',
                referrer: '',
                variationId: '',
            };
            const segments = priorRedirectCookie.split('&');
            segments.forEach(segment => {
                const segmentSegments = segment.split('=');
                if (segmentSegments.length === 2) {
                    if ((0, utils_1.safeDecodeURIComponent)(segmentSegments[0]) === 'r') {
                        redirectInfo.referrer = (0, utils_1.safeDecodeURIComponent)(segmentSegments[1]);
                    }
                    else if ((0, utils_1.safeDecodeURIComponent)(segmentSegments[0]) === 'v') {
                        redirectInfo.variationId = (0, utils_1.safeDecodeURIComponent)(segmentSegments[1]);
                    }
                    else if ((0, utils_1.safeDecodeURIComponent)(segmentSegments[0]) === 'x') {
                        redirectInfo.experimentId = (0, utils_1.safeDecodeURIComponent)(segmentSegments[1]);
                    }
                }
            });
        }
        metadata = {
            userAgent: userAgent || null,
            parsedUA,
        };
        // Metadata was already logged by getRequestIdentifiers so just add to it
        logger.updateMetadata('userAgent', metadata.userAgent);
        logger.updateMetadata('parsedUA', metadata.parsedUA);
        logger.setLogKey('isEs6Compatible', isEs6Compatible);
        if (!isEs6Compatible) {
            throw new Error(`Provided User-Agent is not recognized as a supported browser: "${userAgent}". Please ensure that you are forwarding the User-Agent from the client, and see here for more details about which browsers are supported: https://docs.developers.optimizely.com/performance-edge/docs/supported-browsers.`);
        }
        cookieDomain =
            requestParams.get(constants_1.RequestQueryParameters.COOKIE_DOMAIN) ||
                (0, cookie_2.getCookieDomain)(requestUrlObject.href, logger);
        // Allow undefined cookie domains
        // if (!cookieDomain) {
        //     throw new Error(
        //         `Target page URL does not have valid domain: ${requestUrlString}`
        //     );
        // }
        cookieExpirationDays = Number(requestParams.get(constants_1.RequestQueryParameters.COOKIE_EXPIRATION));
        if (cookieExpirationDays && isNaN(cookieExpirationDays)) {
            throw new Error(`Cookie expiration days provided is an invalid number`);
        }
        // Process target page params. These include Optimizely query parameters like the force variation parameter,
        // and Edge-specific query parameters like the S3 bucket from which to fetch the Web Manifest.
        // See https://help.optimizely.com/QA_Campaigns_and_Experiments/Force_behaviors_in_Optimizely_X_using_query_parameters
        params = processTargetPageParams(requestParams);
        ({
            bypassLogSampling,
            forceVariationIds,
            isDebugMode,
            logLevel,
            webManifestS3Bucket,
            bustSnippetCache,
        } = params);
        optIn = params.optOut === 'false';
        // Get the request type.
        requestType = getRequestType(requestParams, {
            optOutCookieValue,
        });
        // Update logs that couldn't be written before this point
        logger.setLogKey('isDebugMode', isDebugMode);
        logger.setLogKey('requestType', requestType);
    }
    catch (e) {
        inputParseError = e;
    }
    // Throw error, if found
    if (inputParseError) {
        throw inputParseError;
    }
    const timeContext = {
        current: Date.now(),
        timezone: request.cf?.timezone || null,
        timezoneOffset: (0, time_utils_1.tzIdentifierToOffset)(request.cf?.timezone),
    };
    const inputs = {
        /******************************************************************************************************************/
        /*    KEY          |               PRIMARY VALUE                      |               DEFAULT                     */
        /******************************************************************************************************************/
        // Environment info
        requestType: requestType /* RequestType.STANDARD */,
        // Additional metadata
        metadata: metadata,
        // Visitor request info
        cookies,
        cookieDomain: cookieDomain || undefined,
        cookieExpirationDays,
        first_session,
        language,
        queryParams: requestParams,
        requestHeaders,
        redirectInfo,
        url: requestUrlObject && requestUrlObject.href,
        userAgent: userAgent,
        visitorIp: visitorIp || undefined,
        referrer: requestHeaders?.get('Referer') || null,
        time: timeContext,
        // Optimizely parameters
        bypassLogSampling,
        forceVariationIds: forceVariationIds || [],
        logLevel,
        priorRedirectCookie: priorRedirectCookie,
        webManifestS3Bucket: webManifestS3Bucket || null,
        bustSnippetCache,
        // handle all the different optout/optin combos/cases
        optIn,
        isDebugMode: isDebugMode || false,
    };
    return inputs;
}
exports.getInputData = getInputData;
function processTargetPageParams(params) {
    const bypassLogSampling = params.get(constants_1.EdgeQueryParams.BYPASS_LOG_SAMPLING) === 'true';
    const forceVariationIds = params.get(constants_1.OptimizelyQueryParams.FORCE_VARIATIONS) &&
        params.get(constants_1.OptimizelyQueryParams.FORCE_VARIATIONS).split(',');
    const logParam = params.get(constants_1.OptimizelyQueryParams.LOG);
    const logLevel = (Object.values(constants_1.OptimizelyLogLevel).includes(logParam) && logParam) ||
        null;
    const isDebugMode = logParam === constants_1.OptimizelyLogLevel.INFO ||
        logParam === constants_1.OptimizelyLogLevel.DEBUG;
    const optOut = params.get(constants_1.OptimizelyQueryParams.OPT_OUT);
    const webManifestS3Bucket = params.get(constants_1.EdgeQueryParams.OPTIMIZELY_MANIFEST_BUCKET) || null;
    // TODO: maybe support busting cache for other kinds of resources
    const bustSnippetCache = params.get(constants_1.EdgeQueryParams.BUST_CACHE) === 'true';
    return {
        bypassLogSampling,
        forceVariationIds,
        isDebugMode,
        logLevel,
        optOut,
        webManifestS3Bucket,
        bustSnippetCache,
    };
}
/**
 * Parse the worker requests query params to see which request type were dealing with.
 * @param {URLSearchParams} params
 * @param {Object} cookies
 * @return {string} RequestType
 */
function getRequestType(params, cookies = {}) {
    if (params.get(constants_1.OptimizelyQueryParams.DISABLE) === 'true') {
        return constants_1.RequestType.DISABLE;
    }
    if (params.get(constants_1.OptimizelyQueryParams.EDITOR_V2) === 'true') {
        return constants_1.RequestType.EDITOR;
    }
    else if (params.get(constants_1.OptimizelyQueryParams.TOKEN)) {
        // The presence of this param indicates preview mode.
        return constants_1.RequestType.PREVIEW;
    }
    else if (params.get(constants_1.OptimizelyQueryParams.OPT_OUT) === 'true') {
        return constants_1.RequestType.OPT_OUT_VIA_QUERY_PARAM;
    }
    else if (cookies && !!cookies.optOutCookieValue) {
        return constants_1.RequestType.OPT_OUT_VIA_COOKIE;
    }
    return constants_1.RequestType.STANDARD;
}
/**
 * Returns whether or not the request was made from a client that implemented Edge with Edge Subdomain.
 * @param {String} requestHost
 * @return {boolean}
 */
function isEdgeSubdomainRequest(requestHost) {
    return requestHost !== constants_1.OPTIMIZELY_EDGE_HOSTNAME;
}
/**
 * Is the browser compatible with es6 code
 * Based on can I use: https://caniuse.com/#feat=es6
 * @param {Object} browser user agent browser as parsed by lib/detect.js
 * @param {Object} engine browser engine as parsed by lib/detect.js
 * @return {boolean}
 */
function isBrowserEs6Compatible(browser, engine) {
    /**
     * FB uses the local browser engine. We need to detect this version
     * to make an accurate decision about the support.
     */
    if (browser.id === 'facebook') {
        const safari10WebkitVersion = '602.1.50';
        const gc51BlinkVersion = '51';
        return ((engine.name === 'webkit' &&
            (0, compare_version_1.compareVersion)(engine.version, safari10WebkitVersion) >= 0) ||
            (engine.name === 'blink' &&
                (0, compare_version_1.compareVersion)(engine.version, gc51BlinkVersion) >= 0));
    }
    let majorVersion = 0;
    try {
        majorVersion =
            browser.version &&
                Number(browser.version.replace(/[^\d\.]/g, '').split('.')[0]);
    }
    catch (e) {
        // Unable to determine majorversion
        // don't do anything for now
    }
    return ((browser.id === 'gc' && majorVersion >= 51) ||
        (browser.id === 'chromeWebview' && majorVersion >= 51) ||
        (browser.id === 'ff' && majorVersion >= 54) ||
        (browser.id === 'safari' && majorVersion >= 10) ||
        (browser.id === 'opera' && majorVersion >= 46) ||
        (browser.id === 'edge' && majorVersion >= 15) ||
        (browser.id === 'samsung' && majorVersion >= 11));
}
function shouldUseSecureCrossSiteCookies(defines) {
    return '__SET_SECURE_CROSSSITE_COOKIES__' in defines
        ? defines['__SET_SECURE_CROSSSITE_COOKIES__']
        : false;
}
exports.shouldUseSecureCrossSiteCookies = shouldUseSecureCrossSiteCookies;
exports.default = {
    getInputData,
    shouldUseSecureCrossSiteCookies,
};
