Mercurial > addons > firefox-addons > feed-preview
view js/background.js @ 54:ede87e1004f9
Fix issues with feed detection
Query the feed probe content script for available feeds from the background
script instead of making the content script message the background script.
This solves a race condition between the message from the content script
sending any feeds associated with the current document and the tab's status
"complete" event signaling that a new document has been loaded and hiding
the page action. Sometimes that event would be triggered after the message
from the content script and thus hide the page action again. In addition,
navigating back to a previously visited page might not cause a reload which
means that the content script would not send a message if there were feeds
associated with the current document.
author | Guido Berhoerster <guido+feed-preview@berhoerster.name> |
---|---|
date | Thu, 26 Sep 2019 23:11:18 +0200 |
parents | 586eebf8efb7 |
children | fc5fca9af05f |
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/#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; } });