ayastreb/bandwidth-hero

View on GitHub
src/background.js

Summary

Maintainability
B
4 hrs
Test Coverage
import shouldCompress from './background/shouldCompress'
import patchContentSecurity from './background/patchContentSecurity'
import getHeaderValue from './background/getHeaderIntValue'
import parseUrl from './utils/parseUrl'
import deferredStateStorage from './utils/deferredStateStorage'
import defaultState from './defaults'
import axios from 'axios'
import isPrivateNetwork from './background/isPrivateNetwork';

chrome.storage.local.get(storedState => {
    const storage = deferredStateStorage()
    const compressed = new Set();
    let setupHasBeenOpened = false;
    let state = { ...defaultState, ...storedState };
    let currentPageUrl = null;
    let currentPageProtocol = null;

    if (/compressor\.bandwidth-hero\.com/i.test(storedState.proxyUrl)) {
        chrome.storage.local.set({ ...storedState, proxyUrl: '' })
    }

    checkWebpSupport().then(isSupported => {
        chrome.storage.local.set({ ...storedState, isWebpSupported: isSupported })
    })

    async function checkWebpSupport() {
        if (!self.createImageBitmap) return false
        const webpData = 'data:image/webp;base64,UklGRh4AAABXRUJQVlA4TBEAAAAvAAAAAAfQ//73v/+BiOh/AAA='
        const blob = await fetch(webpData).then(r => r.blob())
        return self.createImageBitmap(blob).then(() => true, () => false)
    }

    /**
     * Sets the icons based on the disabled parameter.
     */
    function setIcon() {
        const isEnabled = state.enabled && !isDisabledSite();
        if (chrome.browserAction.setIcon) {
            chrome.browserAction.setIcon({
                path: isEnabled ? "assets/icon-128.png" : "assets/icon-128-disabled.png"
            });
        }
    }

    /**
     * Checks if the proxy is disabled for the given url
     * @returns {boolean}
     */
    function isDisabledSite() {
        // If we don't have the URL or protocol we can't check if it is enabled.
        if (!currentPageUrl || !currentPageProtocol) {
            return true;
        }

        // Check if the page is a http/https page.
        const supportedProtocols = ['http:', 'https:'];
        if (!supportedProtocols.includes(currentPageProtocol)) {
            return true;
        }

        // We are disabled when on localhost or site is hosted on private IP.
        if (isPrivateNetwork(currentPageUrl)) {
            return true;
        }
        return state.disabledHosts.includes(currentPageUrl);
    }

    /**
     * Every time the storage of the browser changes we also update our in-memory state to keep up with the changes.
     *
     */
    function onStateChanged(changes) {
        const changedItems = Object.keys(changes);
        for (const item of changedItems) {
            if (state[item] !== changes[item].newValue) {
                state[item] = changes[item].newValue;
                stateItemChanged(item, state[item]);
            }
        }
    }

    /**
     * Perform actions for certain changes to the state. Like changing the icon if we enable/disable the extension.
     * @param key
     * @param newValue
     */
    function stateItemChanged(key, newValue) {
        switch (key) {
            case 'enabled':
            case 'disabledHosts':
                setIcon(); // Update icon
                break;
        }
    }

    function checkSetup() {
        if (!state.enabled) return;
        if (setupHasBeenOpened) return;
        if (state.proxyUrl === '' || /compressor\.bandwidth-hero\.com/i.test(state.proxyUrl)) {
            chrome.tabs.create({ url: 'setup.html' });
            setupHasBeenOpened = true;
        }
    }

    function onInstalled() {
        checkSetup();
    }

    /**
     * Intercept image loading request and decide if we need to compress it.
     */
    function onBeforeRequestListener({ url, documentUrl, type }) {
        checkSetup();

        // Occasionally currentPageUrl is not ready in time on FF
        const pageUrl = currentPageUrl || parseUrl(documentUrl).host;

        if (
            shouldCompress({
                imageUrl: url,
                pageUrl,
                compressed,
                proxyUrl: state.proxyUrl,
                disabledHosts: state.disabledHosts,
                enabled: state.enabled,
                type
            })
        ) {
            compressed.add(url)
            const redirectUrl = buildCompressUrl(url);

            if (!isFirefox()) return { redirectUrl }
            // Firefox allows onBeforeRequest event listener to return a Promise
            // and perform redirect when this Promise is resolved.
            // This allows us to run HEAD request before redirecting to compression
            // to make sure that the image should be compressed.
            return axios.head(url).then(res => {
                if (
                    res.status === 200 &&
                    res.headers['content-length'] > 1024 &&
                    res.headers['content-type'] &&
                    res.headers['content-type'].startsWith('image')
                ) {
                    return { redirectUrl }
                }
            }).catch(error => {
                if(error.response.status === 405)//HEAD method not allowed
                {
                    return { redirectUrl }
                }
            })
        }
    }

    /**
     * Builds up a redirect URL. The original URL is encode and added as query
     * parameter. If WebP isn't supported the url append a flag to let the
     * server know to return in a JPEG format instead. Other flags that are
     * added are: bw (this lets the proxy know to return image in grayscale
     * as those have less color information and thus saving more space) and
     * compression level.
     * @param url
     * @returns {string}
     */
    function buildCompressUrl(url) {
        let redirectUrl = '';
        redirectUrl += state.proxyUrl;
        redirectUrl += `?url=${encodeURIComponent(url)}`;
        redirectUrl += `&jpeg=${state.isWebpSupported ? 0 : 1}`;
        redirectUrl += `&bw=${state.convertBw ? 1 : 0}`;
        redirectUrl += `&l=${state.compressionLevel}`;
        return redirectUrl;
    }

    /**
     * Firefox user agent always has "rv:" and "Gecko"
     * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent/Firefox
     */
    function isFirefox() {
        return /rv\:.*Gecko/.test(window.navigator.userAgent)
    }

    /**
     * Retrieve saved bytes info from response headers, update statistics in
     * app storage and notify UI about state changes.
     */
    function onCompletedListener({ responseHeaders, fromCache }) {
        if (fromCache) return;
        const bytesSaved = getHeaderValue(responseHeaders, 'x-bytes-saved')
        const bytesProcessed = getHeaderValue(responseHeaders, 'x-original-size')
        if (bytesSaved !== false && bytesProcessed !== false) {
            state.statistics.filesProcessed += 1
            state.statistics.bytesProcessed += bytesProcessed
            state.statistics.bytesSaved += bytesSaved

            storage.set({statistics : state.statistics})
        }
    }

    function onTabActivated({tabId}) {
        chrome.tabs.get(tabId, tab => {
            const url = parseUrl(tab.url);
            currentPageUrl = url.hostname;
            currentPageProtocol = url.schema;
            compressed.clear(); // Reset our list of compressed images
            setIcon();
        });
    }

    // If we navigate to a new page within a tab and it is the same we have a
    // bug where it does not process images. Because the images are still in
    // compressed even though the page changed. With onTabUpdated we reset this.
    function onTabUpdated(){
      compressed.clear()
    }

    /**
     * Patch document's content security policy headers so that it will allow
     * images loading from our compression proxy URL.
     */
    function onHeadersReceivedListener({ responseHeaders }) {
        return {
            responseHeaders: patchContentSecurity(responseHeaders, state.proxyUrl)
        }
    }

    if (!chrome.webRequest.onBeforeRequest.hasListener(onBeforeRequestListener)) {
        chrome.webRequest.onBeforeRequest.addListener(
            onBeforeRequestListener,
            {
                urls: ['<all_urls>'],
                types: isFirefox() ? ['xmlhttprequest', 'imageset', 'image'] : ['image']
            },
            ['blocking']
        )
    }

    if (!chrome.webRequest.onCompleted.hasListener(onCompletedListener)) {
        chrome.webRequest.onCompleted.addListener(
            onCompletedListener,
            {
                urls: ['<all_urls>'],
                types: isFirefox() ? ['xmlhttprequest', 'imageset', 'image'] : ['image']
            },
            ['responseHeaders']
        )
    }

    if (!chrome.webRequest.onHeadersReceived.hasListener(onHeadersReceivedListener)) {
        chrome.webRequest.onHeadersReceived.addListener(
            onHeadersReceivedListener,
            {
                urls: ['<all_urls>'],
                types: ['main_frame', 'sub_frame', 'xmlhttprequest']
            },
            ['blocking', 'responseHeaders']
        )
    }

    if (!chrome.tabs.onActivated.hasListener(onTabActivated)) {
        chrome.tabs.onActivated.addListener(onTabActivated)
    }

    if (!chrome.tabs.onUpdated.hasListener(onTabUpdated)) {
        chrome.tabs.onUpdated.addListener(onTabUpdated)
    }

    if (!chrome.storage.onChanged.hasListener(onStateChanged)) {
        chrome.storage.onChanged.addListener(onStateChanged)
    }

    if (!chrome.runtime.onInstalled.hasListener(onInstalled)) {
        chrome.runtime.onInstalled.addListener(onInstalled)
    }
})