rbutrcom/rbutr-browser-extension

View on GitHub
src/background.js

Summary

Maintainability
F
4 days
Test Coverage
/*!
 * Rbutr Browser Extension v0.12.0
 * https://github.com/rbutrcom/rbutr-browser-extension
 *
 * Copyright 2012-2017 The Rbutr Community
 * Licensed under LGPL-3.0
 */

/*global browser,b64_md5,RbutrUtils,RbutrApi*/
/*jslint browser:true,esnext:true */



/**
 * @description Multi-Browser support
 */
window.browser = (() => {

    'use strict';

    return window.msBrowser ||
        window.browser ||
        window.chrome;
})();



/**
 * @method Rbutr
 * @description Class constructor with variable initialisation
 *
 * @return {Object} Public object methods
 */
const Rbutr = () => {

    'use strict';

    let properties = {

        // The to-be-rebutted URLs
        sourceUrls : [],
        // The rebuttal URLs
        rebuttalUrls : [],
        comment : [],
        tags : [],
        recordedClicks : [],
        // Keyed by tabId
        rebuttals : [],
        // Keyed by tabId
        rebuttalCount : [],
        canonicalUrls : [],
        plainUrls : [],
        urlIsCanonical : [],
        pageTitle : [],
        submittingRebuttal : false,
        loggedIn : false,
        submitError : '',
        direct : false
    };

    const ZERO = 0;
    const FIRST_ARRAY_ELEMENT = 0;



    /**
     * @property utils
     * @description Composing RbutrUtils object into Rbutr for easier usage
     *
     * @type {Object} RbutrUtils object
     */
    const utils = RbutrUtils();

    /**
     * @property api
     * @description Composing RbutrApi object into Rbutr for easier usage
     *
     * @type {Object} RbutrApi object
     */
    const api = RbutrApi(utils);



    /**
     * @method getProp
     * @description Property getter
     *
     * @param {String} name - Object name in variable "properties"
     * @param {(Number|String)} key - The key of an element in "properties.<name>"
     * @return {*} Property value
     */
    const getProp = (name, key) => {
        let result = null;

        if (Array.isArray(properties[name])) {
            result = key !== null && key !== undefined ? properties[name][key]: properties[name];
        } else {
            result = properties[name];
        }

        return result === undefined ? null : result;
    };



    /**
     * @method setProp
     * @description Property setter; If value = null, delete element;
     *
     * @param {String} name - Object name in variable "properties"
     * @param {(Number|String)} key - The key of an element in "properties.<name>"
     * @param {*} value - The value to be set in "properties.<name>"
     * @return {void}
     */
    const setProp = (name, key, value) => {

        const DELETE_ELEMENT_COUNT = 1;

        if (Array.isArray(properties[name])) {
            if (value === null) {
                properties[name].splice(key, DELETE_ELEMENT_COUNT);
            } else {
                properties[name][key] = value;
            }
        } else {
            properties[name] = value;
        }
    };



    /**
     * @method getPropLen
     * @description Get length of property array
     *
     * @param {String} name - Object name in variable "properties"
     * @return {Number} Length of selected array or string
     */
    const getPropLen = (name) => {
        if (Array.isArray(properties[name])) {
            return Object.keys(properties[name]).length;
        } else {
            return properties[name].length;
        }
    };



    /**
     * @method initialize
     * @description Initialize rbutr
     *
     * @return {void}
     */
    const initialize = () => {

        utils.log('log','Initialised on', new Date());
    };



    /**
     * @method alreadyExists
     * @description Check if given url already exists in property arrays
     *
     * @param {String} url - URL to be checked
     * @return {Boolean} State of url existence
     */
    const alreadyExists = (url) => {

        for (let i = 0; i < getPropLen('sourceUrls'); i++) {
            if (getProp('sourceUrls', i) === url) {
                return true;
            }
        }
        for (let j = 0; j < getPropLen('rebuttalUrls'); j++) {
            if (getProp('rebuttalUrls', j) === url) {
                return true;
            }
        }
        return false;
    };



    /**
     * @method getPageTitle
     * @description Get page title for the given url
     *
     * @param {String} url - URL to get the title from
     * @return {String} A website title
     */
    const getPageTitle = (url) => {

        let pageTitle = getProp('pageTitle', url);
        if (pageTitle) {
            return pageTitle;
        } else {
            return 'No title';
        }
    };



    /**
     * @method getPopup
     * @description Get currently displayed popup
     *
     * @return {?Object} Returns a view object or null
     */
    const getPopup = () => {

        let popups = browser.extension.getViews({type: 'popup'});
        if (popups.length >= ZERO) {
            return popups[FIRST_ARRAY_ELEMENT];
        } else {
            return null;
        }
    };



    /**
     * @method displayMessage
     * @description Display a message in the popup
     *
     * @param {String} type - The message type
     * @param {String} message - The message to be displayed
     * @return {void}
     */
    const displayMessage = (type, message) => {

        let popup = getPopup();

        if (popup === null) {
            utils.log('error', 'Popup was null, couldn\'t display:', message);
        } else {
            popup.msg.add(type, message);
        }
    };



    /**
     * @method postMessage
     * @description Post a message to content script to request an action
     *
     * @param {String} action - Action to be executed in contentScript
     * @param {Object} data - Date to be send
     * @return {void}
     */
    const postMessage = (action, data) => {

        // Get the current active tab in the lastly focused window
        browser.tabs.query({
            active: true,
            lastFocusedWindow: true
        }, (tab) => {
            let $data = Object.assign({}, {rbutr: rbutr, action: action}, data);

            utils.log('debug', 'Active tab:', tab[FIRST_ARRAY_ELEMENT]);
            browser.tabs.sendMessage(tab[FIRST_ARRAY_ELEMENT].id, $data);
        });
    };



    /**
     * @method getRecordedClickByToUrl
     * @description Get recorded click object by given rebuttalUrl
     *
     * @param {String} rebuttalUrl - Rebuttal page URL
     * @return {?Object} Return click object for URL or null
     */
    const getRecordedClickByToUrl = (rebuttalUrl) => {

        let recClicks = getProp('recordedClicks', rebuttalUrl);
        return recClicks ? recClicks : null;
    };



    /**
     * @method getMenu
     * @description Submit rebuttal data to server
     *
     * @return {void}
     */
    const getMenu = () => {
        api.getMenu((success, result) => {
            if (success === true) {
                popupPort.postMessage({response: 'getMenu', status: 'success', result: result});
            } else {
                popupPort.postMessage({response: 'getMenu', status: 'error', result: `Menu could not be loaded: ${result}`});
            }
        });
    };



    /**
     * @method getRebuttals
     * @description Load rebuttal data via API
     *
     * @param {Number} tabId - Browser tab id
     * @param {String} url - Browser tab URL
     * @return {void}
     */
    const getRebuttals = (tabId, url) => {

        setProp('rebuttals', tabId, null);
        let canVote = false;
        let recordedClick = getRecordedClickByToUrl(getProp('canonicalUrls', tabId));
        // Don't show voting after you've voted
        if (recordedClick !== null && recordedClick.yourVote && recordedClick.yourVote === ZERO) {
            canVote = true;
        }


        api.getRebuttals(b64_md5(url), (success, result) => {
            if (success === true) {
                let titleMessage = '';

                const hasRebuttals = result.match(/No Rebuttals/g) === null;
                rbutr.setProp('loggedIn', null, result.match(/id="not-logged-in"/g) === null);
                rbutr.setProp('rebuttals', tabId, result);

                let matches = result.match(/class="thumbsUp"/g);
                let count = matches === null ? ZERO : matches.length;
                rbutr.setProp('rebuttalCount', tabId, count);

                if (hasRebuttals) {
                    if (canVote && rbutr.getProp('loggedIn')) {
                        titleMessage = `You can vote on this, and there are also ${count} rebuttal(s).`;
                        utils.updateExtBadge(tabId, titleMessage, 'V ' + count, 'rose');
                    } else {
                        titleMessage = `This page has ${count} rebuttal(s).`;
                        utils.updateExtBadge(tabId, titleMessage, count, 'red');
                    }

                    rbutr.postMessage('showMessageBox', {message: titleMessage, url: rbutr.getProp('canonicalUrls', tabId)});

                } else {
                    if (canVote && rbutr.getProp('loggedIn')) {
                        titleMessage = 'You can vote on this.';
                        utils.updateExtBadge(tabId, titleMessage, 'Vote', 'pink');
                        rbutr.postMessage('showMessageBox', {message: titleMessage, url: rbutr.getProp('canonicalUrls', tabId)});
                    } else {
                        titleMessage = 'RbutR - There are no rebuttals to this page, do you know of one?';
                        utils.updateExtBadge(tabId, titleMessage, '', '');
                    }
                }

                popupPort.postMessage({response: 'getRebuttals', status: 'success', result: result});

            } else {
                popupPort.postMessage({response: 'getRebuttals', status: 'error', result: `Rebuttals could not be loaded: ${result}`});
            }
        });
    };



    /**
     * @method submitRebuttals
     * @description Submit rebuttal data to server
     *
     * @param {Number} tabId - Browser tab id
     * @return {void}
     */
    const submitRebuttals = (tabId) => {

        let sourcePageTitles = [];
        let rebuttalPageTitles = [];
        let canonicalSourcePages = [];
        let canonicalRebuttalPages = [];

        for (let i = 0; i < getPropLen('rebuttalUrls'); i++) {
            rebuttalPageTitles[i] = getProp('pageTitle', getProp('rebuttalUrls', i));
            canonicalRebuttalPages[i] = getProp('urlIsCanonical', getProp('rebuttalUrls', i));
        }

        for (let j = 0; j < getPropLen('sourceUrls'); j++) {
            sourcePageTitles[j] = getProp('pageTitle', getProp('sourceUrls', j));
            canonicalSourcePages[j] = getProp('urlIsCanonical', getProp('sourceUrls', j));
        }

        const submitParameters = {
            submitLinks: true,
            fromUrls: getProp('sourceUrls'),
            toUrls: getProp('rebuttalUrls'),
            fromPageTitles: sourcePageTitles,
            toPageTitles: rebuttalPageTitles,
            comments: getProp('comment'),
            canonicalFromPages: canonicalSourcePages,
            canonicalToPages: canonicalRebuttalPages,
            direct: getProp('direct'),
            tags: getProp('tags'),
        };

        api.submitRebuttals(submitParameters, (success, result) => {
            if (success === true) {
                rbutr.getRebuttals(tabId, getProp('canonicalUrls', tabId)); // This will reload the data for the tab, and set the badge.
                popupPort.postMessage({response: 'submitRebuttals', status: 'success', result: result});
            } else {
                utils.log('error', 'Rebuttal could not be submitted:', result);
                popupPort.postMessage({response: 'submitRebuttals', status: 'error', result: `Rebuttal could not be submitted: ${result}`});
            }
        });
    };



    /**
     * @method submitIdea
     * @description Submit rebuttal data to server
     *
     * @param {Integer} tabId - ID of the popup's tab
     * @param {String} data - Value of the idea input in popup
     * @return {void}
     */
    const submitIdea = (tabId, data) => {
        const submitParameters = {
            url: getProp('canonicalUrls', tabId),
            title: getProp('pageTitle', getProp('canonicalUrls', tabId)),
            idea: data,
        };

        api.submitIdea(submitParameters, (success, result) => {
            if (success === true) {
                popupPort.postMessage({response: 'submitIdea', status: 'success', result: result});
            } else {
                popupPort.postMessage({response: 'submitIdea', status: 'error', result: `Idea could not be submitted: ${result}`});
            }
        });
    };



    /**
     * @method submitRebuttalRequest
     * @description Submit rebuttal request data to server
     *
     * @param {Integer} tabId - ID of the popup's tab
     * @return {void}
     */
    const submitRebuttalRequest = (tabId) => {

        const submitParameters = {
            subscribeToPage: getProp('canonicalUrls', tabId),
            title: getProp('pageTitle', getProp('canonicalUrls', tabId)),
            tags: getProp('tags'),
            pageIsCanonical: getProp('urlIsCanonical', getProp('canonicalUrls', tabId)),
        };

        api.submitRebuttalRequest(submitParameters, (success, result) => {
            if (success === true) {
                popupPort.postMessage({response: 'submitRebuttalRequest', status: 'success', result: result});
            } else {
                popupPort.postMessage({response: 'submitRebuttalRequest', status: 'error', result: `Rebuttal request could not be submitted: ${result}`});
            }
        });

    };



    /**
     * @method updateVotes
     * @description Update votes for current page
     *
     * @param {Integer} tabId - ID of the popup's tab
     * @param {Integer} voteScore - Upvote or downvote
     * @return {void}
     */
    const updateVotes = (tabId, voteScore) => {

        let recordedClick = getRecordedClickByToUrl(getProp('canonicalUrls', tabId));
        const submitParameters = {
            linkId: recordedClick.linkId,
            vote: voteScore,
        };

        if(recordedClick !== null) {
            api.updateVotes(submitParameters, (success, result) => {
                if (success === true) {
                    recordedClick.score += voteScore;
                    recordedClick.yourVote = voteScore;
                    popupPort.postMessage({response: 'updateVotes', status: 'success', result: result});
                } else {
                    popupPort.postMessage({response: 'updateVotes', status: 'error', result: `Votes could not be updated: ${result}`});
                }
            });

        } else {
            popupPort.postMessage({response: 'updateVotes', status: 'error', result: 'Votes could not be updated: No recorded click'});
        }

    };



    /**
     * @method startSubmission
     * @description Prepare data submission
     *
     * @param {Number} tabId - Browser tab id
     * @param {String} fromTo - Type of URLs that should be submitted
     * @return {void}
     */
    const startSubmission = (tabId, fromTo) => {

        let canonUrlByTab = getProp('canonicalUrls', tabId);

        setProp('submittingRebuttal', true);

        if (fromTo === 'from') {
            setProp('sourceUrls', FIRST_ARRAY_ELEMENT, canonUrlByTab);
        } else if (fromTo === 'to') {
            setProp('rebuttalUrls', FIRST_ARRAY_ELEMENT, canonUrlByTab);
        }

        setProp('comment', []);
        setProp('submitError', '');
        setProp('tags', []);
    };



    /**
     * @method stopSubmission
     * @description Cleanup after data submission
     *
     * @return {void}
     */
    const stopSubmission = () => {

        setProp('submittingRebuttal', false);
        setProp('sourceUrls', []);
        setProp('rebuttalUrls', []);
        setProp('comment', []);
        setProp('tags', []);
    };



    /**
     * @method removeTag
     * @description Remove a tag from tag list
     *
     * @param {String} tagText - Content of the tag
     * @return {void}
     */
    const removeTag = (tagText) => {

        let index = getProp('tags').indexOf(tagText);
        setProp('tags', index, null);
    };



    /**
     * @method addTag
     * @description Add a tag to tag list
     *
     * @param {String} tagText - Content of the tag
     * @return {void}
     */
    const addTag = (tagText) => {

        const MAX_TAG_COUNT = 6;

        if (getPropLen('tags') >= MAX_TAG_COUNT) {
            return;
        } else {
            removeTag(tagText); // Avoid duplicates.
            setProp('tags', getPropLen('tags'), tagText);
        }
    };



    /**
     * @method recordLinkClick
     * @description Record click from rbutr website
     *
     * @param {Object} clickData - Click event data
     * @return {void}
     */
    const recordLinkClick = (clickData) => {

        let data = {
            fromTabId: null,
            linkId: clickData.linkId,
            linkFromUrl: clickData.linkFromUrl,
            linkToUrl: clickData.linkToUrl,
            score: clickData.score,
            yourVote: clickData.yourVote
        };

        setProp('recordedClicks', clickData.linkToUrl, data);
    };


    return {utils, api, getProp, setProp, getPropLen, initialize, alreadyExists, getPageTitle, getPopup, displayMessage, postMessage, getRecordedClickByToUrl, getMenu, getRebuttals, submitRebuttals, submitIdea, submitRebuttalRequest, updateVotes, startSubmission, stopSubmission, removeTag, addTag, recordLinkClick};
};



// Necessary usage of 'var' because popup will not get variable if it's 'const' or 'let' in Chrome
var rbutr = Rbutr();
let popupPort = null;


document.addEventListener('DOMContentLoaded', () => {

    rbutr.initialize();


    browser.runtime.onMessage.addListener((request, sender, sendResponse) => {

        'use strict';

        if (request.action) {

            let tab = request.tab || sender.tab;
            let canonicalUrl = rbutr.utils.getCanonicalUrl(tab.url);
            let url = canonicalUrl || tab.url;

            if (request.action === 'setCanonical') {

                if (!/^http/.test(canonicalUrl)) {
                    return;
                }

                rbutr.setProp('urlIsCanonical', url, Boolean(canonicalUrl));
                rbutr.setProp('canonicalUrls', tab.id, canonicalUrl);
                rbutr.setProp('plainUrls', tab.id, tab.url);

                rbutr.setProp('pageTitle', url, tab.title);
                return true;

            } else if (request.action === 'setClick') {
                let click = request.click;
                rbutr.recordLinkClick(click);
                rbutr.utils.log('debug', 'Click recorded:', click.linkToUrl);

            } else if (request.action === 'getCid') {
                sendResponse(rbutr.api.getCid());
                return true;

            } else if (request.action === 'getServerUrl') {
                sendResponse(rbutr.api.getServerUrl());
                return true;

            } else if (request.action === 'getServerDomain') {
                sendResponse(rbutr.api.getServerUrl(true));
                return true;

            } else if (request.action === 'getVersion') {
                sendResponse(browser.runtime.getManifest().version);
                return true;
            }
        }

    });



    // tab is going away, remove the canonical data for it
    browser.tabs.onRemoved.addListener((tabId) => {

        'use strict';

        rbutr.setProp('canonicalUrls', tabId, null);
        rbutr.setProp('plainUrls', tabId, null);
    });



    /**
     * @description Fires when a tab is updated
     */
    browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {

        'use strict';

        // ensure that the url data lives for the life of the page, not the tab
        if (changeInfo.status === 'loading') {
            if (tab.url === rbutr.getProp('plainUrls', tabId)) {
                return;
            }

            rbutr.setProp('canonicalUrls', tabId, null);
            rbutr.setProp('plainUrls', tabId, null);
        }
    });



    browser.runtime.onConnect.addListener((port) => {
        port.onMessage.addListener((msg) => {
            if (port.name === 'popup-background') {
                popupPort = port;

                if (msg.request === 'getRebuttals') {
                    let canonicalUrl = rbutr.utils.getCanonicalUrl(msg.tab.url);
                    let url = canonicalUrl || msg.tab.url;
                    rbutr.getRebuttals(msg.tab.id, url);
                } else if (msg.request === 'getMenu') {
                    rbutr.getMenu();
                } else if (msg.request === 'submitIdea') {
                    rbutr.submitIdea(msg.tab.id, msg.data);
                } else if (msg.request === 'submitRebuttalRequest') {
                    rbutr.submitRebuttalRequest(msg.tab.id, msg.data);
                } else if (msg.request === 'updateVotes') {
                    rbutr.updateVotes(msg.tab.id, msg.data);
                }
            }
        });
    });

});