From ceea4bd7f5eba358b2732402efa85647672f4097 Mon Sep 17 00:00:00 2001 From: Eduardo Rosendo Date: Wed, 7 Aug 2024 17:39:52 -0400 Subject: [PATCH] refactor(background): Extract helper functions from background script Improves code readability and maintainability by extracting reusable logic into dedicated helper functions. --- src/background.js | 197 +++------------------------- src/utils/background.js | 102 ++++++++++++++ src/utils/toolbar_button.js | 174 ++++++++++++++++++++++++ src/utils/url_and_cookie_helpers.js | 32 +++++ 4 files changed, 329 insertions(+), 176 deletions(-) create mode 100644 src/utils/background.js create mode 100644 src/utils/toolbar_button.js create mode 100644 src/utils/url_and_cookie_helpers.js diff --git a/src/background.js b/src/background.js index 304c0deb..d6228f71 100644 --- a/src/background.js +++ b/src/background.js @@ -1,172 +1,14 @@ - - -// Make services callable from content scripts. -exportInstance(Notifier); -exportInstance(Recap); -exportInstance(Acms); - -function chooseVariant(details) { - const options = ['A-A', 'A-C', 'B-B', 'B-D']; - const randomIndex = Math.floor(Math.random() * options.length); - let variant = options[randomIndex]; - chrome.storage.local.set({ variant: variant }); -} - -function saveOptionsAndUpdateToolbar(options) { - chrome.storage.local.set({ options: options }, function () { - chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { - updateToolbarButton(tabs[0]); - }); - }); -} - -function setDefaultOptions(details) { - // Set options to their default values. - console.debug('RECAP: Setting default options after install/upgrade.'); - chrome.storage.local.get('options', function (items) { - console.debug('RECAP: Attempted to get \'options\' key from local ' + - `storage. Got: ${items}`); - let defaults = { - external_pdf: false, - recap_enabled: true, - recap_link_popups: true, - show_notifications: true, - - // Radio button - ia_style_filenames: false, - lawyer_style_filenames: true, - }; - if ($.isEmptyObject(items)) { - console.debug('RECAP: New install. Attempting to set defaults.'); - saveOptionsAndUpdateToolbar(defaults) - console.debug('RECAP: Set the defaults on new install successfully.'); - } else { - console.debug('RECAP: Existing install. Attempting to set new ' + - 'defaults, if any'); - - // it's weird that we have a `recap_disabled` option - // when it should be `recap_enabled`. - // - // In order to flip the polarity, we'll read out the - // `recap_disabled` option (which has previously been set, - // so everyone should have it) - let optionToUpgrade = 'recap_disabled'; - // if the option is a Boolean (as it should be) - if (typeof(items.options[optionToUpgrade]) === 'boolean') { - // set the inverse option `recap_enabled` to - // the inverse of `recap_disabled` - items.options.recap_enabled = !(items.options[optionToUpgrade]); - } else { - // if for some reason it's _not_ a boolean, let's default to uploading. - items.options.recap_enabled = true; - } - - // okay now set the rest of the defaults that are missing. - for (let key in defaults) { - if (!(key in items.options)) { - items.options[key] = defaults[key]; - } - } - console.debug('RECAP: Persisting new settings object.'); - saveOptionsAndUpdateToolbar(items.options) - } - }); -} - -function showNotificationTab(details){ - // Show some kind of notification tab to the user after install/upgrade. - console.debug('RECAP: showing install/upgrade notification if ' + - 'version matches'); - let currentVersion = chrome.runtime.getManifest().version; - if (details.reason === 'update' && currentVersion === '1.2.3'){ - // This version is when we pushed for donations. Show that page. - chrome.tabs.create({ - url: 'https://free.law/fundraisers/2017/recap/' - }); - } else if (details.reason === 'update' && currentVersion === '1.2.10'){ - chrome.tabs.create({ - url: 'https://free.law/fundraisers/2018/recap/' - }); - } else if (details.reason === 'update' && currentVersion === '1.2.15'){ - chrome.tabs.create({ - url: 'https://free.law/fundraisers/2019/recap/' - }); - } else if (details.reason === 'update' && currentVersion === '1.2.27'){ - chrome.tabs.create({ - url: 'https://free.law/fundraiser/2021/recap' - }); - } else if (details.reason === 'update' && currentVersion === '1.2.31'){ - chrome.tabs.create({ - url: 'https://free.law/fundraiser/2022/recap' - }); - } else if (details.reason === 'update' && currentVersion === '2.4.2'){ - chrome.tabs.create({ - url: 'https://donate.free.law/forms/11' - }); - } -} - -function executeScripts(tabId, injectDetailsArray) { - // This method uses programmatic injection to load content scripts - // and waits until the helper finishes injecting a script before - // loading the succeeding one into the active tab. - // - // This helper uses the callback parameter of the executeScript and a - // recursive approach to define the sequence of operations that - // guarantees the files are loaded in the same order they are listed - // in the injectDetailsArray parameter. - // - // You can read more on this at this [PR](https://github.com/freelawproject/recap-chrome/pull/340). - - function createCallback(tabId, injectDetails, innerCallback) { - return function () { - chrome.tabs.executeScript(tabId, injectDetails, innerCallback); - }; - } - - var callback = null; - - for (var fileDetails of injectDetailsArray.reverse()) { - callback = createCallback(tabId, fileDetails, callback); - } - - if (callback !== null) callback(); // execute outermost function -} - -async function injectContentScript(tabId, status, url) { - if (status == 'complete' && PACER.getCourtFromUrl(url)) { - chrome.tabs.sendMessage(tabId, { message: 'content_script_status' }, async function (msg) { - if (chrome.runtime.lastError) { - console.info(`RECAP: Content scripts are not loaded yet`); - } - msg = msg || {}; - if (msg.status != 'loaded') { - console.info(`RECAP: Injecting content scripts`); - executeScripts(tabId, [ - { file: "InjectManager.js" }, - { file: "assets/js/jquery.js" }, - { file: "assets/js/FileSaver.js" }, - { file: "assets/js/FileSaver.js" }, - { file: 'assets/js/moment.js'}, - { file: 'assets/js/livestamp.js'}, - { file: 'assets/js/bootstrap.bundle.js'}, - { file: 'action_button.js'}, - { file: 'pdf_upload.js'}, - { file: 'utils.js'}, - { file: 'notifier.js'}, - { file: 'toolbar_button.js'}, - { file: 'pacer.js'}, - { file: 'recap.js'}, - { file: 'content_delegate.js'}, - { file: 'appellate/utils.js'}, - { file: 'appellate/appellate.js'}, - { file: 'appellate/acms_api.js'}, - { file: 'content.js'}, - ]) - } - }); - } -} +import { + getTabById, + saveOptionsAndUpdateToolbar, + setDefaultOptions, + updateToolbarButton, +} from './utils/toolbar_button.js'; +import { + chooseVariant, + injectContentScript, + showNotificationTab, +} from './utils/background.js'; chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { if (msg.message === 'requestTabId') { @@ -178,16 +20,19 @@ chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { saveOptionsAndUpdateToolbar(items.options); }); } - if (msg.message === 'upload'){ + if (msg.message === 'upload') { let recap = new Recap(); let notifier = new Notifier(); - let callback = function(ok){ + let callback = function (ok) { if (ok) { - notifier.showUpload('Free PDF uploaded to the public RECAP Archive.', () => {}); + notifier.showUpload( + 'Free PDF uploaded to the public RECAP Archive.', + () => {} + ); } - } - callback.tab = sender.tab - let options = msg.options + }; + callback.tab = sender.tab; + let options = msg.options; recap.uploadDocument( options.court, options.pacer_case_id, @@ -209,7 +54,7 @@ chrome.tabs.onUpdated.addListener(async function (tabId, details, tab) { updateToolbarButton(tab); await injectContentScript(tabId, details.status, tab.url); }); -chrome.tabs.onActivated.addListener(function(activeInfo){ +chrome.tabs.onActivated.addListener(function (activeInfo) { getTabById(activeInfo.tabId, updateToolbarButton); }); diff --git a/src/utils/background.js b/src/utils/background.js new file mode 100644 index 00000000..669e734a --- /dev/null +++ b/src/utils/background.js @@ -0,0 +1,102 @@ +import { getCourtFromUrl } from './url_and_cookie_helpers.js'; + +export function chooseVariant(details) { + const options = ['A-A', 'A-C', 'B-B', 'B-D']; + const randomIndex = Math.floor(Math.random() * options.length); + let variant = options[randomIndex]; + chrome.storage.local.set({ variant: variant }); +} + +export function executeScripts(tabId, injectDetailsArray) { + // This method uses programmatic injection to load content scripts + // and waits until the helper finishes injecting a script before + // loading the succeeding one into the active tab. + // + // This helper uses the callback parameter of the executeScript and a + // recursive approach to define the sequence of operations that + // guarantees the files are loaded in the same order they are listed + // in the injectDetailsArray parameter. + // + // You can read more on this at: + // https://github.com/freelawproject/recap-chrome/pull/340 + let reversedList = injectDetailsArray.map(function (e) { + return e['file']; + }); + chrome.scripting + .executeScript({ + target: { tabId: tabId }, + files: reversedList, + }) + .then(() => console.log('injected script file')); +} + +export async function injectContentScript(tabId, status, url) { + if (status == 'complete' && getCourtFromUrl(url)) { + chrome.tabs.sendMessage( + tabId, + { message: 'content_script_status' }, + async function (msg) { + if (chrome.runtime.lastError) { + console.info('RECAP: Content scripts are not loaded yet'); + } + msg = msg || {}; + if (msg.status != 'loaded') { + console.info('RECAP: Injecting content scripts'); + executeScripts(tabId, [ + { file: 'InjectManager.js' }, + { file: 'assets/js/jquery.js' }, + { file: 'assets/js/FileSaver.js' }, + { file: 'assets/js/moment.js' }, + { file: 'assets/js/livestamp.js' }, + { file: 'assets/js/bootstrap.bundle.js' }, + { file: 'action_button.js' }, + { file: 'pdf_upload.js' }, + { file: 'utils.js' }, + { file: 'notifier.js' }, + { file: 'pacer.js' }, + { file: 'recap.js' }, + { file: 'content_delegate.js' }, + { file: 'appellate/utils.js' }, + { file: 'appellate/appellate.js' }, + { file: 'appellate/acms_api.js' }, + { file: 'content.js' }, + ]); + } + } + ); + } +} + +export function showNotificationTab(details) { + // Show some kind of notification tab to the user after install/upgrade. + console.debug( + 'RECAP: showing install/upgrade notification if ' + 'version matches' + ); + let currentVersion = chrome.runtime.getManifest().version; + if (details.reason === 'update' && currentVersion === '1.2.3') { + // This version is when we pushed for donations. Show that page. + chrome.tabs.create({ + url: 'https://free.law/fundraisers/2017/recap/', + }); + } else if (details.reason === 'update' && currentVersion === '1.2.10') { + chrome.tabs.create({ + url: 'https://free.law/fundraisers/2018/recap/', + }); + } else if (details.reason === 'update' && currentVersion === '1.2.15') { + chrome.tabs.create({ + url: 'https://free.law/fundraisers/2019/recap/', + }); + } else if (details.reason === 'update' && currentVersion === '1.2.27') { + chrome.tabs.create({ + url: 'https://free.law/fundraiser/2021/recap', + }); + } else if (details.reason === 'update' && currentVersion === '1.2.31') { + chrome.tabs.create({ + url: 'https://free.law/fundraiser/2022/recap', + }); + } else if (details.reason === 'update' && currentVersion === '2.4.2') { + chrome.tabs.create({ + url: 'https://donate.free.law/forms/11', + }); + } +} diff --git a/src/utils/toolbar_button.js b/src/utils/toolbar_button.js new file mode 100644 index 00000000..1a556a4a --- /dev/null +++ b/src/utils/toolbar_button.js @@ -0,0 +1,174 @@ +// This file is part of RECAP for Chrome. +// Copyright 2013 Ka-Ping Yee +// +// RECAP for Chrome 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. RECAP for Chrome 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 +// RECAP for Chrome. If not, see: http://www.gnu.org/licenses/ + +// ------------------------------------------------------------------------- +// Toolbar button for RECAP (or "browser action" in Chrome parlance). +import { + areTransactionReceiptsDisabled, + getCourtFromUrl, +} from './url_and_cookie_helpers.js'; + +export function getTabById(tabId, cb) { + chrome.tabs.get(tabId, cb); +} + +export function updateToolbarButton(tab) { + // Updates the toolbar button for a tab to reflect the tab's login status. + let setTitleIcon = function (title, icon) { + chrome.action.setTitle({ title: `RECAP: ${title}` }); + chrome.action.setIcon({ path: icon }); + }; + + chrome.storage.local.get('options', function (items) { + if ( + 'dismiss_news_badge' in items['options'] && + items['options']['dismiss_news_badge'] + ) { + chrome.action.setBadgeText({ text: '' }); + } else { + if ( + /Safari/.test(navigator.userAgent) && + !/Chrome|Chromium/.test(navigator.userAgent) + ) { + // Detect Safari engine + chrome.action.setBadgeText({ text: '1' }); + } else { + chrome.action.setBadgeText({ text: '🔔' }); + chrome.action.setBadgeBackgroundColor({ color: '#404040' }); + } + } + + if (tab === null || tab === undefined) { + // There's code in Firefox that can be called before the defaults are set + // and before the tab is even established. Catch that, and handle it or + // else it can crash things. + setTitleIcon('RECAP is ready', { + 19: 'assets/images/grey-19.png', + 38: 'assets/images/grey-38.png', + }); + return; + } + if (!Object.keys(items).length) { + // Firefox 56 bug. The default settings didn't get created properly when + // upgrading from the legacy extension. This can be removed when everybody + // is safely beyond 56 (and the ESR) + setDefaultOptions({}); + } + + if (items && items['options'] && !items['options']['recap_enabled']) { + setTitleIcon('RECAP is temporarily disabled', { + 19: 'assets/images/disabled-19.png', + 38: 'assets/images/disabled-38.png', + }); + } else { + // Is it a PACER URL? + let court = getCourtFromUrl(tab.url); + if (!court) { + // Not a PACER URL. Show gray. + setTitleIcon('Not at a PACER site', { + 19: 'assets/images/grey-19.png', + 38: 'assets/images/grey-38.png', + }); + } else { + // It's a valid PACER URL. Therefore either show the nice blue icon or + // show the blue icon with a warning, if receipts are disabled. + chrome.cookies.get( + { + url: tab.url, + name: 'PacerPref', + }, + function (pref_cookie) { + if (areTransactionReceiptsDisabled(pref_cookie)) { + // Receipts are disabled. Show the warning. + setTitleIcon('Receipts are disabled in your PACER settings', { + 19: 'assets/images/warning-19.png', + 38: 'assets/images/warning-38.png', + }); + } else { + // At PACER, and things look good! + setTitleIcon('Logged in to PACER. RECAP is active.', { + 19: 'assets/images/icon-19.png', + 38: 'assets/images/icon-38.png', + }); + } + } + ); + } + } + }); +} + +export function setDefaultOptions(details) { + // Set options to their default values. + console.debug('RECAP: Setting default options after install/upgrade.'); + chrome.storage.local.get('options', function (items) { + console.debug( + 'RECAP: Attempted to get options key from local ' + + `storage. Got: ${items}` + ); + let defaults = { + external_pdf: false, + recap_enabled: true, + recap_link_popups: true, + show_notifications: true, + + // Radio button + ia_style_filenames: false, + lawyer_style_filenames: true, + }; + if (!Object.keys(items).length) { + console.debug('RECAP: New install. Attempting to set defaults.'); + saveOptionsAndUpdateToolbar(defaults); + console.debug('RECAP: Set the defaults on new install successfully.'); + } else { + console.debug( + 'RECAP: Existing install. Attempting to set new ' + 'defaults, if any' + ); + + // it's weird that we have a `recap_disabled` option + // when it should be `recap_enabled`. + // + // In order to flip the polarity, we'll read out the + // `recap_disabled` option (which has previously been set, + // so everyone should have it) + let optionToUpgrade = 'recap_disabled'; + // if the option is a Boolean (as it should be) + if (typeof items.options[optionToUpgrade] === 'boolean') { + // set the inverse option `recap_enabled` to + // the inverse of `recap_disabled` + items.options.recap_enabled = !items.options[optionToUpgrade]; + } else { + // if for some reason it's _not_ a boolean, let's default to uploading. + items.options.recap_enabled = true; + } + + // okay now set the rest of the defaults that are missing. + for (let key in defaults) { + if (!(key in items.options)) { + items.options[key] = defaults[key]; + } + } + console.debug('RECAP: Persisting new settings object.'); + saveOptionsAndUpdateToolbar(items.options); + } + }); +} + +export function saveOptionsAndUpdateToolbar(options) { + chrome.storage.local.set({ options: options }, function () { + chrome.tabs.query({ active: true, currentWindow: true }, function (tabs) { + updateToolbarButton(tabs[0]); + }); + }); +} diff --git a/src/utils/url_and_cookie_helpers.js b/src/utils/url_and_cookie_helpers.js new file mode 100644 index 00000000..8ee1ea5b --- /dev/null +++ b/src/utils/url_and_cookie_helpers.js @@ -0,0 +1,32 @@ +// Returns true if the receipts are disabled globally +export function areTransactionReceiptsDisabled(cookie) { + return cookie && cookie.value.match(/receipt=N/); +} + +export function getCourtFromUrl(url) { + // This function is used as a security check to ensure that no components of + // RECAP are being used outside of PACER/ECF/ACMS. Be sure tests pass + // appropriately before tweaking these regexes. + if (!url) { + return null; + } + + let match; + // CM/ECF and PACER + match = url + .toLowerCase() + .match(/^\w+:\/\/(ecf|pacer)\.(\w+)(?:\.audio)?\.uscourts\.gov(?:\/.*)?$/); + if (match) { + return match[2]; + } + + // ACMS + match = url + .toLowerCase() + .match(/^\w+:\/\/(\w+)-showdoc\.azurewebsites\.us(?:\/.*)?$/); + if (match) { + return match[1]; + } + + return null; +}