Permalink
Comparing changes
Open a pull request
- 2 commits
- 4 files changed
- 0 commit comments
- 1 contributor
Unified
Split
Showing
with
134 additions
and 23 deletions.
- +1 −0 h/browser/chrome/lib/hypothesis-chrome-extension.js
- +58 −10 h/browser/chrome/lib/sidebar-injector.js
- +44 −0 h/browser/chrome/lib/util.js
- +31 −13 h/browser/chrome/test/sidebar-injector-test.js
| @@ -204,6 +204,7 @@ function HypothesisChromeExtension(dependencies) { | ||
| }); | ||
| return sidebar.injectIntoTab(tab) | ||
| .catch(function (err) { | ||
| console.error('Failed to inject Hypothesis Sidebar:', err); | ||
| tabErrors.setTabError(tab.id, err); | ||
| state.errorTab(tab.id); | ||
| }); | ||
| @@ -3,6 +3,29 @@ | ||
| var blocklist = require('../../../static/scripts/blocklist'); | ||
| var errors = require('./errors'); | ||
| var settings = require('./settings'); | ||
| var util = require('./util'); | ||
| var CONTENT_TYPE_HTML = 'HTML'; | ||
| var CONTENT_TYPE_PDF = 'PDF'; | ||
| // a function which is executed as a content script | ||
| // to determine the type of content being displayed in a tab | ||
| function detectContentType() { | ||
| // check if this is the Chrome PDF viewer | ||
| if (document.querySelector('embed[type="application/pdf"]')) { | ||
| return { | ||
| type: 'PDF', | ||
| }; | ||
| } else { | ||
| return { | ||
| type: 'HTML', | ||
| }; | ||
| } | ||
| } | ||
| function toIIFEString(fn) { | ||
| return '(' + fn.toString() + ')()'; | ||
| } | ||
| /* The SidebarInjector is used to deploy and remove the Hypothesis sidebar | ||
| * from tabs. It also deals with loading PDF documents into the PDF.js viewer | ||
| @@ -22,6 +45,8 @@ function SidebarInjector(chromeTabs, dependencies) { | ||
| var isAllowedFileSchemeAccess = dependencies.isAllowedFileSchemeAccess; | ||
| var extensionURL = dependencies.extensionURL; | ||
| var executeScriptFn = util.promisify(chromeTabs.executeScript); | ||
| if (typeof extensionURL !== 'function') { | ||
| throw new TypeError('extensionURL must be a function'); | ||
| } | ||
| @@ -72,8 +97,20 @@ function SidebarInjector(chromeTabs, dependencies) { | ||
| return PDF_VIEWER_URL + '?file=' + encodeURIComponent(url); | ||
| } | ||
| function isPDFURL(url) { | ||
| return url.toLowerCase().indexOf('.pdf') > 0; | ||
| function detectTabContentType(tab) { | ||
| if (isPDFViewerURL(tab.url)) { | ||
| return Promise.resolve(CONTENT_TYPE_PDF); | ||
| } | ||
| if (!isSupportedURL(tab.url)) { | ||
| return Promise.resolve(CONTENT_TYPE_HTML); | ||
| } | ||
| return executeScriptFn(tab.id, { | ||
| code: toIIFEString(detectContentType) | ||
| }).then(function (frameResults) { | ||
| return frameResults[0].type; | ||
| }); | ||
| } | ||
| function isPDFViewerURL(url) { | ||
| @@ -85,22 +122,33 @@ function SidebarInjector(chromeTabs, dependencies) { | ||
| } | ||
| function isSupportedURL(url) { | ||
| var SUPPORTED_PROTOCOLS = ['http:', 'https:', 'ftp:']; | ||
| // Injection of content scripts is limited to a small number of protocols, | ||
| // see https://developer.chrome.com/extensions/match_patterns | ||
| var parsedURL = new URL(url); | ||
| var SUPPORTED_PROTOCOLS = ['http:', 'https:', 'ftp:', 'file:']; | ||
| return SUPPORTED_PROTOCOLS.some(function (protocol) { | ||
| return url.indexOf(protocol) === 0; | ||
| return parsedURL.protocol === protocol; | ||
| }); | ||
| } | ||
| function injectIntoLocalDocument(tab) { | ||
| if (isPDFURL(tab.url)) { | ||
| return injectIntoLocalPDF(tab); | ||
| } else { | ||
| return Promise.reject(new errors.LocalFileError('Local non-PDF files are not supported')); | ||
| } | ||
| return detectTabContentType(tab).then(function (type) { | ||
| if (type === CONTENT_TYPE_PDF) { | ||
| return injectIntoLocalPDF(tab); | ||
| } else { | ||
| return Promise.reject(new errors.LocalFileError('Local non-PDF files are not supported')); | ||
| } | ||
| }); | ||
| } | ||
| function injectIntoRemoteDocument(tab) { | ||
| return isPDFURL(tab.url) ? injectIntoPDF(tab) : injectIntoHTML(tab); | ||
| return detectTabContentType(tab).then(function (type) { | ||
| if (type === CONTENT_TYPE_PDF) { | ||
| return injectIntoPDF(tab); | ||
| } else { | ||
| return injectIntoHTML(tab); | ||
| } | ||
| }); | ||
| } | ||
| function injectIntoPDF(tab) { | ||
| @@ -0,0 +1,44 @@ | ||
| function getLastError() { | ||
| if (typeof chrome !== 'undefined' && chrome.extension) { | ||
| return chrome.extension.lastError; | ||
| } else { | ||
| return undefined; | ||
| } | ||
| } | ||
| /** | ||
| * Converts an async Chrome API into a function | ||
| * which returns a promise. | ||
| * | ||
| * Usage: | ||
| * var apiFn = promisify(chrome.someModule.aFunction); | ||
| * apiFn(arg1, arg2) | ||
| * .then(function (result) { ...handle success }) | ||
| * .catch(function (err) { ...handle error }) | ||
| * | ||
| * | ||
| * @param fn A Chrome API function whose last argument is a callback | ||
| * which is invoked with the result of the query. When this callback | ||
| * is invoked, the promise is rejected if chrome.extension.lastError | ||
| * is set or resolved with the first argument to the callback otherwise. | ||
| */ | ||
| function promisify(fn) { | ||
| return function () { | ||
| var args = [].slice.call(arguments); | ||
| var result = new Promise(function (resolve, reject) { | ||
| fn.apply(this, args.concat(function (result) { | ||
| var lastError = getLastError(); | ||
| if (lastError) { | ||
| reject(lastError); | ||
| } else { | ||
| resolve(result); | ||
| } | ||
| })); | ||
| }); | ||
| return result; | ||
| }; | ||
| } | ||
| module.exports = { | ||
| promisify: promisify, | ||
| }; |
| @@ -7,10 +7,30 @@ describe('SidebarInjector', function () { | ||
| var fakeChromeTabs; | ||
| var fakeFileAccess; | ||
| // the content type that the detection script injected into | ||
| // the page should report ('HTML' or 'PDF') | ||
| var contentType; | ||
| // the return value from the content script which checks whether | ||
| // the sidebar has already been injected into the page | ||
| var isAlreadyInjected; | ||
| beforeEach(function () { | ||
| contentType = 'HTML'; | ||
| isAlreadyInjected = false; | ||
| var executeScriptSpy = sinon.spy(function (tabId, details, callback) { | ||
| if (details.code.match(/window.annotator/)) { | ||
| callback([isAlreadyInjected]); | ||
| } else if (details.code.match(/detectContentType/)) { | ||
| callback([{type: contentType}]); | ||
| } else { | ||
| callback([false]); | ||
| } | ||
| }); | ||
| fakeChromeTabs = { | ||
| update: sinon.stub(), | ||
| executeScript: sinon.stub() | ||
| executeScript: executeScriptSpy, | ||
| }; | ||
| fakeFileAccess = sinon.stub().yields(true); | ||
| @@ -29,13 +49,7 @@ describe('SidebarInjector', function () { | ||
| } | ||
| describe('.injectIntoTab', function () { | ||
| beforeEach(function () { | ||
| // Handle loading the config. | ||
| fakeChromeTabs.executeScript.withArgs(1, {code: 'window.annotator'}).yields([false]); | ||
| fakeChromeTabs.executeScript.yields([]); | ||
| }); | ||
| var protocols = ['chrome:', 'chrome-devtools:', 'chrome-extension']; | ||
| var protocols = ['chrome:', 'chrome-devtools:', 'chrome-extension:']; | ||
| protocols.forEach(function (protocol) { | ||
| it('bails early when trying to load an unsupported ' + protocol + ' url', function () { | ||
| var spy = fakeChromeTabs.executeScript; | ||
| @@ -52,6 +66,7 @@ describe('SidebarInjector', function () { | ||
| describe('when viewing a remote PDF', function () { | ||
| it('injects hypothesis into the page', function () { | ||
| contentType = 'PDF'; | ||
| var spy = fakeChromeTabs.update.yields({tab: 1}); | ||
| var url = 'http://example.com/foo.pdf'; | ||
| @@ -69,7 +84,6 @@ describe('SidebarInjector', function () { | ||
| var url = 'http://example.com/foo.html'; | ||
| return injector.injectIntoTab({id: 1, url: url}).then(function() { | ||
| assert.callCount(spy, 2); | ||
| assert.calledWith(spy, 1, { | ||
| code: sinon.match('/public/config.js') | ||
| }); | ||
| @@ -85,6 +99,7 @@ describe('SidebarInjector', function () { | ||
| it('loads the PDFjs viewer', function () { | ||
| var spy = fakeChromeTabs.update.yields([]); | ||
| var url = 'file://foo.pdf'; | ||
| contentType = 'PDF'; | ||
| return injector.injectIntoTab({id: 1, url: url}).then( | ||
| function () { | ||
| @@ -100,6 +115,7 @@ describe('SidebarInjector', function () { | ||
| describe('when file access is disabled', function () { | ||
| beforeEach(function () { | ||
| fakeFileAccess.yields(false); | ||
| contentType = 'PDF'; | ||
| }); | ||
| it('returns an error', function () { | ||
| @@ -125,7 +141,9 @@ describe('SidebarInjector', function () { | ||
| var url = 'file://foo.html'; | ||
| var promise = injector.injectIntoTab({id: 1, url: url}); | ||
| return promise.then(assertReject, function (err) { | ||
| assert.notCalled(fakeChromeTabs.executeScript); | ||
| assert.isFalse(fakeChromeTabs.executeScript.calledWith(1, { | ||
| code: sinon.match(/config\.js/), | ||
| })); | ||
| }); | ||
| }); | ||
| }); | ||
| @@ -239,7 +257,7 @@ describe('SidebarInjector', function () { | ||
| }); | ||
| }); | ||
| var protocols = ['chrome:', 'chrome-devtools:', 'chrome-extension']; | ||
| var protocols = ['chrome:', 'chrome-devtools:', 'chrome-extension:']; | ||
| protocols.forEach(function (protocol) { | ||
| it('bails early when trying to unload an unsupported ' + protocol + ' url', function () { | ||
| var spy = fakeChromeTabs.executeScript; | ||
| @@ -266,9 +284,9 @@ describe('SidebarInjector', function () { | ||
| describe('when viewing an HTML page', function () { | ||
| it('injects a destroy script into the page', function () { | ||
| var stub = fakeChromeTabs.executeScript.yields([true]); | ||
| isAlreadyInjected = true; | ||
| return injector.removeFromTab({id: 1, url: 'http://example.com/foo.html'}).then(function () { | ||
| assert.calledWith(stub, 1, { | ||
| assert.calledWith(fakeChromeTabs.executeScript, 1, { | ||
| code: sinon.match('/public/destroy.js') | ||
| }); | ||
| }); | ||