/*******************************************************************************

    uBlock Origin - a browser extension to block requests.
    Copyright (C) 2017-present Raymond Hill

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program 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 General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see {http://www.gnu.org/licenses/}.

    Home: https://github.com/gorhill/uBlock
*/

/* globals browser */

'use strict';

/******************************************************************************/

import {
    domainFromHostname,
    hostnameFromNetworkURL,
} from './uri-utils.js';

/******************************************************************************/

// Canonical name-uncloaking feature.
let cnameUncloakEnabled = browser.dns instanceof Object;
let cnameUncloakProxied = false;

// https://github.com/uBlockOrigin/uBlock-issues/issues/911
//   We detect here whether network requests are proxied, and if so,
//   de-aliasing of hostnames will be disabled to avoid possible
//   DNS leaks.
const proxyDetector = function(details) {
    if ( details.proxyInfo instanceof Object ) {
        cnameUncloakEnabled = false;
        proxyDetectorTryCount = 0;
    }
    if ( proxyDetectorTryCount === 0 ) {
        browser.webRequest.onHeadersReceived.removeListener(proxyDetector);
        return;
    }
    proxyDetectorTryCount -= 1;
};
let proxyDetectorTryCount = 0;

// Related issues:
// - https://github.com/gorhill/uBlock/issues/1327
// - https://github.com/uBlockOrigin/uBlock-issues/issues/128
// - https://bugzilla.mozilla.org/show_bug.cgi?id=1503721

// Extend base class to normalize as per platform.

vAPI.Net = class extends vAPI.Net {
    constructor() {
        super();
        this.pendingRequests = [];
        this.canUncloakCnames = browser.dns instanceof Object;
        this.cnames = new Map([ [ '', null ] ]);
        this.cnameIgnoreList = null;
        this.cnameIgnore1stParty = true;
        this.cnameIgnoreExceptions = true;
        this.cnameIgnoreRootDocument = true;
        this.cnameMaxTTL = 120;
        this.cnameReplayFullURL = false;
        this.cnameFlushTime = Date.now() + this.cnameMaxTTL * 60000;
    }
    setOptions(options) {
        super.setOptions(options);
        if ( 'cnameUncloakEnabled' in options ) {
            cnameUncloakEnabled =
                this.canUncloakCnames &&
                options.cnameUncloakEnabled !== false;
        }
        if ( 'cnameUncloakProxied' in options ) {
            cnameUncloakProxied = options.cnameUncloakProxied === true;
        }
        if ( 'cnameIgnoreList' in options ) {
            this.cnameIgnoreList =
                this.regexFromStrList(options.cnameIgnoreList);
        }
        if ( 'cnameIgnore1stParty' in options ) {
            this.cnameIgnore1stParty =
                options.cnameIgnore1stParty !== false;
        }
        if ( 'cnameIgnoreExceptions' in options ) {
            this.cnameIgnoreExceptions =
                options.cnameIgnoreExceptions !== false;
        }
        if ( 'cnameIgnoreRootDocument' in options ) {
            this.cnameIgnoreRootDocument =
                options.cnameIgnoreRootDocument !== false;
        }
        if ( 'cnameMaxTTL' in options ) {
            this.cnameMaxTTL = options.cnameMaxTTL || 120;
        }
        if ( 'cnameReplayFullURL' in options ) {
            this.cnameReplayFullURL = options.cnameReplayFullURL === true;
        }
        this.cnames.clear(); this.cnames.set('', null);
        this.cnameFlushTime = Date.now() + this.cnameMaxTTL * 60000;
        // https://github.com/uBlockOrigin/uBlock-issues/issues/911
        //   Install/remove proxy detector.
        if ( vAPI.webextFlavor.major < 80 ) {
            const wrohr = browser.webRequest.onHeadersReceived;
            if ( cnameUncloakEnabled === false || cnameUncloakProxied ) {
                if ( wrohr.hasListener(proxyDetector) ) {
                    wrohr.removeListener(proxyDetector);
                }
            } else if ( wrohr.hasListener(proxyDetector) === false ) {
                wrohr.addListener(
                    proxyDetector,
                    { urls: [ '*://*/*' ] },
                    [ 'blocking' ]
                );
            }
            proxyDetectorTryCount = 32;
        }
    }
    normalizeDetails(details) {
        const type = details.type;

        if ( type === 'imageset' ) {
            details.type = 'image';
            return;
        }

        // https://github.com/uBlockOrigin/uBlock-issues/issues/345
        //   Re-categorize an embedded object as a `sub_frame` if its
        //   content type is that of a HTML document.
        if ( type === 'object' && Array.isArray(details.responseHeaders) ) {
            for ( const header of details.responseHeaders ) {
                if ( header.name.toLowerCase() === 'content-type' ) {
                    if ( header.value.startsWith('text/html') ) {
                        details.type = 'sub_frame';
                    }
                    break;
                }
            }
        }
    }
    denormalizeTypes(types) {
        if ( types.length === 0 ) {
            return Array.from(this.validTypes);
        }
        const out = new Set();
        for ( const type of types ) {
            if ( this.validTypes.has(type) ) {
                out.add(type);
            }
            if ( type === 'image' && this.validTypes.has('imageset') ) {
                out.add('imageset');
            }
            if ( type === 'sub_frame' ) {
                out.add('object');
            }
        }
        return Array.from(out);
    }
    canonicalNameFromHostname(hn) {
        const cnRecord = this.cnames.get(hn);
        if ( cnRecord !== undefined && cnRecord !== null ) {
            return cnRecord.cname;
        }
    }
    processCanonicalName(hn, cnRecord, details) {
        if ( cnRecord === null ) { return; }
        if ( cnRecord.isRootDocument ) { return; }
        const hnBeg = details.url.indexOf(hn);
        if ( hnBeg === -1 ) { return; }
        const oldURL = details.url;
        let newURL = oldURL.slice(0, hnBeg) + cnRecord.cname;
        const hnEnd = hnBeg + hn.length;
        if ( this.cnameReplayFullURL ) {
            newURL += oldURL.slice(hnEnd);
        } else {
            const pathBeg = oldURL.indexOf('/', hnEnd);
            if ( pathBeg !== -1 ) {
                newURL += oldURL.slice(hnEnd, pathBeg + 1);
            }
        }
        details.url = newURL;
        details.aliasURL = oldURL;
        return super.onBeforeSuspendableRequest(details);
    }
    recordCanonicalName(hn, record, isRootDocument) {
        if ( (this.cnames.size & 0b111111) === 0 ) {
            const now = Date.now();
            if ( now >= this.cnameFlushTime ) {
                this.cnames.clear(); this.cnames.set('', null);
                this.cnameFlushTime = now + this.cnameMaxTTL * 60000;
            }
        }
        let cname =
            typeof record.canonicalName === 'string' &&
            record.canonicalName !== hn
                ? record.canonicalName
                : '';
        if (
            cname !== '' &&
            this.cnameIgnore1stParty &&
            domainFromHostname(cname) === domainFromHostname(hn)
        ) {
            cname = '';
        }
        if (
            cname !== '' &&
            this.cnameIgnoreList !== null &&
            this.cnameIgnoreList.test(cname)
        ) {
            cname = '';
        }
        const cnRecord = cname !== '' ? { cname, isRootDocument } : null;
        this.cnames.set(hn, cnRecord);
        return cnRecord;
    }
    regexFromStrList(list) {
        if (
            typeof list !== 'string' ||
            list.length === 0 ||
            list === 'unset' ||
            browser.dns instanceof Object === false
        ) {
            return null;
        }
        if ( list === '*' ) {
            return /^./;
        }
        return new RegExp(
            '(?:^|\.)(?:' +
            list.trim()
                .split(/\s+/)
                .map(a => a.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
                .join('|') +
            ')$'
        );
    }
    onBeforeSuspendableRequest(details) {
        const r = super.onBeforeSuspendableRequest(details);
        if ( cnameUncloakEnabled === false ) { return r; }
        if ( r !== undefined ) {
            if (
                r.cancel === true ||
                r.redirectUrl !== undefined ||
                this.cnameIgnoreExceptions
            ) {
                return r;
            }
        }
        const hn = hostnameFromNetworkURL(details.url);
        const cnRecord = this.cnames.get(hn);
        if ( cnRecord !== undefined ) {
            return this.processCanonicalName(hn, cnRecord, details);
        }
        const documentUrl = details.documentUrl || details.url;
        const isRootDocument = this.cnameIgnoreRootDocument &&
            hn === hostnameFromNetworkURL(documentUrl);
        return browser.dns.resolve(hn, [ 'canonical_name' ]).then(
            rec => {
                const cnRecord = this.recordCanonicalName(hn, rec, isRootDocument);
                return this.processCanonicalName(hn, cnRecord, details);
            },
            ( ) => {
                this.cnames.set(hn, null);
            }
        );
    }
    suspendOneRequest(details) {
        const pending = {
            details: Object.assign({}, details),
            resolve: undefined,
            promise: undefined
        };
        pending.promise = new Promise(resolve => {
            pending.resolve = resolve;
        });
        this.pendingRequests.push(pending);
        return pending.promise;
    }
    unsuspendAllRequests(discard = false) {
        const pendingRequests = this.pendingRequests;
        this.pendingRequests = [];
        for ( const entry of pendingRequests ) {
            entry.resolve(
                discard !== true
                    ? this.onBeforeSuspendableRequest(entry.details)
                    : undefined
            );
        }
    }
    static canSuspend() {
        return true;
    }
};

/******************************************************************************/

vAPI.scriptletsInjector = ((doc, details) => {
    let script, url;
    try {
        const blob = new self.Blob(
            [ details.scriptlets ],
            { type: 'text/javascript; charset=utf-8' }
        );
        url = self.URL.createObjectURL(blob);
        script = doc.createElement('script');
        script.async = false;
        script.src = url;
        (doc.head || doc.documentElement || doc).append(script);
        self.uBO_scriptletsInjected = details.filters;
    } catch (ex) {
    }
    if ( url ) {
        if ( script ) { script.remove(); }
        self.URL.revokeObjectURL(url);
    }
}).toString();

/******************************************************************************/
