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

    uMatrix - 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/uMatrix
*/

// For background page

'use strict';

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

(( ) => {
    // https://github.com/uBlockOrigin/uBlock-issues/issues/407
    if ( vAPI.webextFlavor.soup.has('firefox') === false ) { return; }

    // https://github.com/gorhill/uBlock/issues/2950
    // Firefox 56 does not normalize URLs to ASCII, uBO must do this itself.
    // https://bugzilla.mozilla.org/show_bug.cgi?id=945240
    const evalMustPunycode = ( ) => {
        return vAPI.webextFlavor.soup.has('firefox') &&
               vAPI.webextFlavor.major < 57;
    };

    let mustPunycode = evalMustPunycode();

    // The real actual webextFlavor value may not be set in stone, so listen
    // for possible future changes.
    window.addEventListener('webextFlavor', ( ) => {
        mustPunycode = evalMustPunycode();
    }, { once: true });

    const punycode = self.punycode;
    const reAsciiHostname  = /^https?:\/\/[0-9a-z_.:@-]+[/?#]/;
    const parsedURL = new URL('about:blank');

    // 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.cnames = new Map([ [ '', '' ] ]);
            this.cnameIgnoreList = null;
            this.cnameIgnore1stParty = true;
            this.cnameIgnoreExceptions = true;
            this.cnameIgnoreRootDocument = true;
            this.cnameMaxTTL = 60;
            this.cnameReplayFullURL = false;
            this.cnameTimer = undefined;
            this.canRevealCNAME = browser.dns instanceof Object;
        }
        setOptions(options) {
            super.setOptions(options);
            this.cnameIgnoreList = this.regexFromStrList(options.cnameIgnoreList);
            this.cnameIgnore1stParty = options.cnameIgnore1stParty !== false;
            this.cnameIgnoreExceptions = options.cnameIgnoreExceptions !== false;
            this.cnameIgnoreRootDocument = options.cnameIgnoreRootDocument !== false;
            this.cnameMaxTTL = options.cnameMaxTTL || 120;
            this.cnameReplayFullURL = options.cnameReplayFullURL === true;
            this.cnames.clear(); this.cnames.set('', '');
        }
        normalizeDetails(details) {
            if ( mustPunycode && !reAsciiHostname.test(details.url) ) {
                parsedURL.href = details.url;
                details.url = details.url.replace(
                    parsedURL.hostname,
                    punycode.toASCII(parsedURL.hostname)
                );
            }

            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);
        }
        processCanonicalName(hn, cn, details) {
            const hnBeg = details.url.indexOf(hn);
            if ( hnBeg === -1 ) { return; }
            const oldURL = details.url;
            let newURL = oldURL.slice(0, hnBeg) + cn;
            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) {
            let cname =
                typeof record.canonicalName === 'string' &&
                record.canonicalName !== hn
                    ? record.canonicalName
                    : '';
            if (
                cname !== '' &&
                this.cnameIgnore1stParty &&
                vAPI.domainFromHostname(cname) === vAPI.domainFromHostname(hn)
            ) {
                cname = '';
            }
            if (
                cname !== '' &&
                this.cnameIgnoreList !== null &&
                this.cnameIgnoreList.test(cname)
            ) {
                cname = '';
            }
            this.cnames.set(hn, cname);
            if ( this.cnameTimer === undefined ) {
                this.cnameTimer = self.setTimeout(
                    ( ) => {
                        this.cnameTimer = undefined;
                        this.cnames.clear(); this.cnames.set('', '');
                    },
                    this.cnameMaxTTL * 60000
                );
            }
            return cname;
        }
        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 ( this.canRevealCNAME === false ) { return r; }
            if ( r !== undefined ) {
                if ( r.cancel === false ) { return; }
                if (
                    r.cancel === true ||
                    r.redirectUrl !== undefined ||
                    this.cnameIgnoreExceptions
                ) {
                    return r;
                }
            }
            if (
                details.type === 'main_frame' &&
                this.cnameIgnoreRootDocument
            ) {
                return;
            }
            const hn = vAPI.hostnameFromNetworkURL(details.url);
            const cname = this.cnames.get(hn);
            if ( cname === '' ) { return; }
            if ( cname !== undefined ) {
                return this.processCanonicalName(hn, cname, details);
            }
            return browser.dns.resolve(hn, [ 'canonical_name' ]).then(
                rec => {
                    const cname = this.recordCanonicalName(hn, rec);
                    if ( cname === '' ) { return; }
                    return this.processCanonicalName(hn, cname, details);
                },
                ( ) => {
                    this.cnames.set(hn, '');
                }
            );
        }
        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() {
            const pendingRequests = this.pendingRequests;
            this.pendingRequests = [];
            for ( const entry of pendingRequests ) {
                entry.resolve(this.onBeforeSuspendableRequest(entry.details));
            }
        }
        canSuspend() {
            return true;
        }
    };
})();

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