diff --git a/src/background/broken-page-report.js b/src/background/broken-page-report.js new file mode 100644 index 000000000..f286c56f0 --- /dev/null +++ b/src/background/broken-page-report.js @@ -0,0 +1,107 @@ +/** + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2017-present Ghostery GmbH. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { store } from 'hybrids'; + +import Options, { SYNC_OPTIONS } from '/store/options.js'; + +import getBrowserInfo from '/utils/browser-info.js'; +import { SUPPORT_PAGE_URL } from '/utils/urls.js'; + +import { tabStats } from './stats.js'; + +async function getMetadata(tab) { + let result = '\n------\n\n'; + + const { version } = chrome.runtime.getManifest(); + result += `Extension version: ${version}`; + + // Send only not-private options + const options = Object.fromEntries( + Object.entries(await store.resolve(Options)).filter(([key]) => + SYNC_OPTIONS.includes(key), + ), + ); + + result += `\nOptions: ${JSON.stringify(options)}`; + + const trackers = tabStats.get(tab.id)?.trackers.map((t) => t.id); + if (trackers) { + result += `\nTrackers(${trackers.length}): ${trackers.join(', ')}`; + } + + return result; +} + +chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + if (msg.action === 'report-broken-page') { + (async () => { + try { + const formData = new FormData(); + const browserInfo = await getBrowserInfo(); + + formData.append('support_ticket[user_name]', msg.email); + formData.append('support_ticket[user_email]', msg.email); + formData.append( + 'support_ticket[subject]', + `[GBE] Broken page report: ${msg.url}`, + ); + + formData.append('support_ticket[selected_browser]', browserInfo.name); + formData.append('support_ticket[browser_version]', browserInfo.version); + + if (browserInfo.osVersion !== 'other') { + formData.append('support_ticket[selected_os]', browserInfo.osVersion); + formData.append('support_ticket[os_version]', ''); + } + + let description = msg.description.trim() + (await getMetadata(msg.tab)); + + if (description.length > 5000) { + description = description.slice(0, 4997) + '...'; + } + + formData.append('support_ticket[message]', description); + + if (msg.screenshot) { + const screenshot = await chrome.tabs.captureVisibleTab(null, { + format: 'jpeg', + quality: 100, + }); + formData.append( + 'support_ticket[screenshot]', + await fetch(screenshot).then((res) => res.blob()), + 'screenshot.jpeg', + ); + } + + await fetch(SUPPORT_PAGE_URL, { + method: 'POST', + body: formData, + }).then((res) => { + if (!res.ok || res.status > 204) { + throw new Error( + `Sending report has failed with status: ${res.status}`, + ); + } + }); + + sendResponse(); + } catch (e) { + sendResponse(e.message); + } + })(); + + return true; + } + + return false; +}); diff --git a/src/background/index.js b/src/background/index.js index b9529cf73..f2e6334b1 100644 --- a/src/background/index.js +++ b/src/background/index.js @@ -22,6 +22,7 @@ import './session.js'; import './stats.js'; import './notifications.js'; import './serp.js'; +import './broken-page-report.js'; import './helpers.js'; import './external.js'; diff --git a/src/background/session.js b/src/background/session.js index f879852ef..890fc2291 100644 --- a/src/background/session.js +++ b/src/background/session.js @@ -10,7 +10,8 @@ */ import { store } from 'hybrids'; import Session, { UPDATE_SESSION_ACTION_NAME } from '/store/session.js'; -import { HOME_PAGE_URL, ACCOUNT_PAGE_URL, COOKIE_DOMAIN } from '/utils/api.js'; +import { HOME_PAGE_URL, ACCOUNT_PAGE_URL } from '/utils/urls.js'; +import { COOKIE_DOMAIN } from '/utils/api.js'; function refreshSession() { chrome.runtime diff --git a/src/background/sync.js b/src/background/sync.js index 36e1051e5..e12d041ea 100644 --- a/src/background/sync.js +++ b/src/background/sync.js @@ -15,7 +15,7 @@ import Session from '/store/session.js'; import { getUserOptions, setUserOptions } from '/utils/api.js'; import * as OptionsObserver from '/utils/options-observer.js'; -import { HOME_PAGE_URL, ACCOUNT_PAGE_URL } from '/utils/api.js'; +import { HOME_PAGE_URL, ACCOUNT_PAGE_URL } from '/utils/urls.js'; import debounce from '/utils/debounce.js'; const syncOptions = debounce( diff --git a/src/manifest.chromium.json b/src/manifest.chromium.json index c1ad8589c..0d673b904 100644 --- a/src/manifest.chromium.json +++ b/src/manifest.chromium.json @@ -14,6 +14,7 @@ "storage", "scripting", "tabs", + "activeTab", "webRequest", "offscreen" ], diff --git a/src/manifest.firefox.json b/src/manifest.firefox.json index cbdcc5066..513ee3eaa 100644 --- a/src/manifest.firefox.json +++ b/src/manifest.firefox.json @@ -11,6 +11,7 @@ "storage", "scripting", "tabs", + "activeTab", "webNavigation", "webRequest", "webRequestBlocking", diff --git a/src/manifest.safari.json b/src/manifest.safari.json index 834d85c07..5de06d278 100644 --- a/src/manifest.safari.json +++ b/src/manifest.safari.json @@ -13,7 +13,8 @@ "webNavigation", "storage", "scripting", - "tabs" + "tabs", + "activeTab" ], "host_permissions": [ "http://*/*", diff --git a/src/pages/panel/assets/contribution.svg b/src/pages/panel/assets/contribution.svg new file mode 100644 index 000000000..2c725ac8a --- /dev/null +++ b/src/pages/panel/assets/contribution.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/pages/panel/components/pause.js b/src/pages/panel/components/pause.js index 3555f1188..ead59f1cf 100644 --- a/src/pages/panel/components/pause.js +++ b/src/pages/panel/components/pause.js @@ -89,7 +89,6 @@ export default { html` `} -
+ ${pauseList && html`
+ ${paused?.revokeAt && + html` +
+ + + Something wrong? + + Report a broken page + + + + +
+ `} ` : html` diff --git a/src/pages/panel/views/report-confirm.js b/src/pages/panel/views/report-confirm.js new file mode 100644 index 000000000..da69850c1 --- /dev/null +++ b/src/pages/panel/views/report-confirm.js @@ -0,0 +1,49 @@ +/** + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2017-present Ghostery GmbH. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { html, router } from 'hybrids'; + +import contributionImage from '../assets/contribution.svg'; + +export default { + render: () => html` + + `, +}; diff --git a/src/pages/panel/views/report-form.js b/src/pages/panel/views/report-form.js new file mode 100644 index 000000000..c4f510970 --- /dev/null +++ b/src/pages/panel/views/report-form.js @@ -0,0 +1,166 @@ +/** + * Ghostery Browser Extension + * https://www.ghostery.com/ + * + * Copyright 2017-present Ghostery GmbH. All rights reserved. + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0 + */ + +import { html, router, store, msg } from 'hybrids'; + +import Session from '/store/session.js'; +import { getCurrentTab, openTabWithUrl } from '/utils/tabs.js'; +import { SUPPORT_PAGE_URL } from '/utils/urls.js'; + +import ReportConfirm from './report-confirm.js'; + +const Form = { + url: '', + email: '', + description: '', + screenshot: false, + [store.connect]: { + async get() { + const [currentTab, session] = await Promise.all([ + getCurrentTab(), + store.resolve(Session), + ]); + + const url = currentTab && new URL(currentTab.url); + + return { + url: url ? `${url.origin}${url.pathname}` : '', + email: session.email, + }; + }, + async set(_, values) { + const error = await chrome.runtime.sendMessage({ + action: 'report-broken-page', + tab: await getCurrentTab(), + ...values, + }); + + if (error) throw new Error(error); + + return values; + }, + }, +}; + +function submit(host, event) { + try { + router.resolve( + event, + store.submit(host.form).then(() => store.clear(Form)), + ); + } catch { + event.preventDefault(); + } +} + +export default { + form: store(Form, { draft: true }), + render: ({ form }) => html` + + `, +}; diff --git a/src/pages/settings/components/custom-filters.js b/src/pages/settings/components/custom-filters.js index e97f4c804..5514801e4 100644 --- a/src/pages/settings/components/custom-filters.js +++ b/src/pages/settings/components/custom-filters.js @@ -38,7 +38,7 @@ export default { render: ({ input, result, disabled }) => html`