Mercurial > addons > firefox-addons > feed-preview
view js/background.js @ 70:e405ff21ab31 version-14
Release version 14
author | Guido Berhoerster <guido+feed-preview@berhoerster.name> |
---|---|
date | Sun, 03 Mar 2024 18:12:27 +0100 |
parents | fc5fca9af05f |
children |
line wrap: on
line source
/* * Copyright (C) 2018 Guido Berhoerster <guido+feed-preview@berhoerster.name> * * 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/. */ 'use strict'; import * as feedParser from './feed-parser.js'; import {renderFeedPreview} from './feed-preview.js'; const FEED_READERS_PRESET = [ { title: 'Feedly', urlTemplate: 'https://feedly.com/i/subscription/feed/%s' }, { title: 'FlowReader', urlTemplate: 'https://www.flowreader.com/subscribe?url=%s' }, { title: 'InoReader', urlTemplate: 'https://www.inoreader.com/feed/%s' }, { title: 'Kouio', urlTemplate: 'https://kouio.com/subscribe?url=%s' }, { title: 'My Yahoo', urlTemplate: 'https://add.my.yahoo.com/rss?url=%s' }, { title: 'Netvibes', urlTemplate: 'https://www.netvibes.com/subscribe.php?url=%s' }, { title: 'NewsBlur', urlTemplate: 'https://www.newsblur.com/?url=%s' }, { title: 'The Old Reader', urlTemplate: 'https://theoldreader.com/feeds/subscribe?url=%s' } ]; const FEED_MAGIC = [ '<rss', '<feed', feedParser.XMLNS.ATOM03, feedParser.XMLNS.ATOM10, feedParser.XMLNS.RSS09, feedParser.XMLNS.RSS10 ]; var tabsFeeds = new Map(); var tabsFeedPreviews = new Map(); var fetchingFeedPreview = fetch('web_resources/feed-preview.xhtml') .then(response => response.text()); var feedPreviewOptions = { expandEntries: false }; function parseContentType(header) { let contentType = { mediaType: '', charset: 'utf-8' }; let parts = header.toLowerCase().split(';'); contentType.mediaType = parts.shift().trim(); for (let parameter of parts) { let [, name, value, ] = parameter.trim().split(/([^=]+)="?([^"]*)"?/); if (name.toLowerCase() === 'charset') { contentType.charset = value.toLowerCase(); break; } } return contentType; } async function handleFeed(inputText, tabId, url) { // fast-path: eliminate XML documents which cannot be Atom nor RSS feeds let inputTextStart = inputText.substring(0, 512); if (!FEED_MAGIC.some(element => inputTextStart.includes(element))) { return inputText; } let feed; try { feed = (new feedParser.FeedParser).parseFromString(inputText, url); } catch (e) { if (e instanceof feedParser.ParserError || e instanceof feedParser.UnsupportedFeedTypeError) { // let the browser deal with non-well formed XML or XML documents // which are not supported Atom or RSS feeds return inputText; } throw e; } console.log(`parsed feed ${url}:\n`, feed); // mark this feed preview for content script injection tabsFeedPreviews.set(tabId, url); // render the preview document let feedPreviewDocument = new DOMParser() .parseFromString(await fetchingFeedPreview, 'text/html'); renderFeedPreview(feedPreviewDocument, feed, feedPreviewOptions.expandEntries); return new XMLSerializer().serializeToString(feedPreviewDocument); } browser.webRequest.onHeadersReceived.addListener(details => { if (details.statusCode !== 200) { return {}; } let contentTypeIndex = details.responseHeaders.findIndex(header => header.name.toLowerCase() === 'content-type' && typeof header.value !== 'undefined'); if (contentTypeIndex < 0) { // no Content-Type header found return {}; } let headerValue = details.responseHeaders[contentTypeIndex].value let contentType = parseContentType(headerValue); // until content handlers become available to webextensions // (https://bugzilla.mozilla.org/show_bug.cgi?id=1457500) intercept all // responses and change the content type from application/atom+xml or // application/rss+xml to application/xml which will then be probed for // Atom or RSS content switch (contentType.mediaType) { case 'application/atom+xml': case 'application/rss+xml': case 'application/rdf+xml': case 'application/xml': case 'text/xml': break; default: // non-XML media type return {}; } console.log(`response is an XML document\n`, `media type: ${contentType.mediaType}\n`, `charset: ${contentType.charset}`); let decoder; try { decoder = new TextDecoder(contentType.charset); } catch (e) { if (e instanceof RangeError) { // unsupported charset return {}; } else { throw e; } } let encoder = new TextEncoder(); let inputText = ''; let filter = browser.webRequest.filterResponseData(details.requestId); filter.addEventListener('data', ev => { inputText += decoder.decode(ev.data, {stream: true}); }); filter.addEventListener('stop', async ev => { let result = await handleFeed(inputText, details.tabId, details.url); filter.write(encoder.encode(result)); filter.close(); }); details.responseHeaders[contentTypeIndex] = { name: 'Content-Type', value: 'application/xml;charset=utf-8' }; return {responseHeaders: details.responseHeaders}; }, {urls: ['http://*/*', 'https://*/*'], types: ['main_frame']}, ['blocking', 'responseHeaders']); browser.runtime.onMessage.addListener((request, sender, sendResponse) => { // popup querying feeds sendResponse(tabsFeeds.get(request)); }); browser.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { if (changeInfo.status !== 'complete') { // hide the page action when the URL changes since it is no longer // valid, it will be shown again if the content script detects a feed browser.pageAction.hide(tabId); return; } if (tabsFeedPreviews.get(tabId) === tab.url) { // inject content script once if the requested URL is a feed preview browser.tabs.executeScript(tabId, { file: 'content_scripts/feed-readers.js' }); tabsFeedPreviews.delete(tabId); } else { // query available feeds let feeds = await browser.tabs.sendMessage(tabId, {}); if (feeds.length > 0) { tabsFeeds.set(tabId, feeds); browser.pageAction.show(tabId); console.log(`detected feeds in tab ${tabId} for ${tab.url}:\n`, feeds); } else { console.log(`no feeds detected in tab ${tabId} for ${tab.url}\n`); } } }, {properties: ["status"]}); browser.tabs.onRemoved.addListener((tabId, removeInfo) => { tabsFeeds.delete(tabId); tabsFeedPreviews.delete(tabId); }); browser.runtime.onInstalled.addListener(async details => { if (details.reason === 'install' || (details.reason === 'update' && details.previousVersion < 2)) { let {feedReaders = []} = await browser.storage.sync.get('feedReaders'); let feedReadersSet = new Set(feedReaders.map(feedReader => feedReader.urlTemplate)); for (let feedReader of FEED_READERS_PRESET) { if (!feedReadersSet.has(feedReader.urlTemplate)) { feedReaders.push(feedReader); } } console.log('set feedReaders to', feedReaders); browser.storage.sync.set({feedReaders}); } }); browser.storage.sync.get('feedPreview').then(({feedPreview}) => { if (typeof feedPreview !== 'undefined' && feedPreview === Object(feedPreview)) { feedPreviewOptions.expandEntries = !!feedPreview.expandEntries; } }); browser.storage.onChanged.addListener((changes, areaName) => { if (areaName !== 'sync' || typeof changes.feedPreview === 'undefined') { return; } let newValue = changes.feedPreview.newValue; if (typeof newValue !== 'undefined' && newValue === Object(newValue)) { feedPreviewOptions.expandEntries = !!newValue.expandEntries; } });