diff --git a/chrome/content/zotero/elements/attachmentPreview.js b/chrome/content/zotero/elements/attachmentPreview.js
index c932b26838..ed0df9d7ee 100644
--- a/chrome/content/zotero/elements/attachmentPreview.js
+++ b/chrome/content/zotero/elements/attachmentPreview.js
@@ -27,9 +27,6 @@
// eslint-disable-next-line no-undef
class AttachmentPreview extends XULElementBase {
static fileTypeMap = {
- 'application/pdf': 'pdf',
- 'application/epub+zip': 'epub',
- 'text/html': 'snapshot',
// TODO: support video and audio
// 'video/mp4': 'video',
// 'video/webm': 'video',
@@ -111,6 +108,9 @@
}
get previewType() {
+ if (this._item?.attachmentReaderType) {
+ return this._item.attachmentReaderType;
+ }
let contentType = this._item?.attachmentContentType;
if (!contentType) {
return "file";
diff --git a/chrome/content/zotero/locateMenu.js b/chrome/content/zotero/locateMenu.js
index 54e93cfab2..d02b0a82cd 100644
--- a/chrome/content/zotero/locateMenu.js
+++ b/chrome/content/zotero/locateMenu.js
@@ -370,11 +370,6 @@ var Zotero_LocateMenu = new function() {
snapshot: "zotero-menuitem-attachments-snapshot",
multiple: "zotero-menuitem-new-tab",
};
- const attachmentTypes = {
- "application/pdf": "pdf",
- "application/epub+zip": "epub",
- "text/html": "snapshot",
- };
this._attachmentType = "multiple";
Object.defineProperty(this, "className", {
get: () => (alternateWindowBehavior ? "zotero-menuitem-new-window" : classNames[this._attachmentType]),
@@ -404,7 +399,7 @@ var Zotero_LocateMenu = new function() {
this._attachmentType = "multiple";
}
else if (attachment) {
- this._attachmentType = attachmentTypes[attachment.attachmentContentType];
+ this._attachmentType = attachment.attachmentReaderType;
}
return !!attachment;
};
diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js
index fef04456da..40b414949a 100644
--- a/chrome/content/zotero/xpcom/data/item.js
+++ b/chrome/content/zotero/xpcom/data/item.js
@@ -1914,7 +1914,7 @@ Zotero.Item.prototype._saveData = Zotero.Promise.coroutine(function* (env) {
if (!parentItem.isFileAttachment()) {
throw new Error("Annotation parent must be a file attachment");
}
- if (!['application/pdf', 'application/epub+zip', 'text/html'].includes(parentItem.attachmentContentType)) {
+ if (!parentItem.attachmentReaderType) {
throw new Error("Annotation parent must be a PDF, EPUB, or HTML snapshot");
}
let type = this._getLatestField('annotationType');
@@ -3103,6 +3103,25 @@ Zotero.defineProperty(Zotero.Item.prototype, 'attachmentContentType', {
});
+Zotero.defineProperty(Zotero.Item.prototype, 'attachmentReaderType', {
+ get() {
+ if (!this.isFileAttachment()) {
+ return undefined;
+ }
+ switch (this.attachmentContentType) {
+ case 'application/pdf':
+ return 'pdf';
+ case 'application/epub+zip':
+ return 'epub';
+ case 'text/html':
+ return 'snapshot';
+ default:
+ return undefined;
+ }
+ }
+});
+
+
Zotero.Item.prototype.getAttachmentCharset = function() {
Zotero.debug("getAttachmentCharset() deprecated -- use .attachmentCharset");
return this.attachmentCharset;
diff --git a/chrome/content/zotero/xpcom/fileHandlers.js b/chrome/content/zotero/xpcom/fileHandlers.js
new file mode 100644
index 0000000000..1acdfa2035
--- /dev/null
+++ b/chrome/content/zotero/xpcom/fileHandlers.js
@@ -0,0 +1,501 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2018 Center for History and New Media
+ George Mason University, Fairfax, Virginia, USA
+ https://zotero.org
+
+ This file is part of Zotero.
+
+ Zotero is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Zotero 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 Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with Zotero. If not, see .
+
+ ***** END LICENSE BLOCK *****
+*/
+
+/* eslint-disable array-element-newline */
+
+Zotero.FileHandlers = {
+ async open(item, params) {
+ let { location, openInWindow = false } = params || {};
+
+ let path = await item.getFilePathAsync();
+ if (!path) {
+ Zotero.warn(`File not found: ${item.attachmentPath}`);
+ return false;
+ }
+
+ Zotero.debug('Opening ' + path);
+
+ let readerType = item.attachmentReaderType;
+
+ // Not a file that we/external readers handle with page number support -
+ // just open it with the system handler
+ if (!readerType) {
+ Zotero.debug('No associated reader type -- launching default application');
+ Zotero.launchFile(path);
+ return true;
+ }
+
+ let handler = Zotero.Prefs.get(`fileHandler.${readerType}`);
+ if (!handler) {
+ Zotero.debug('No external handler for ' + readerType + ' -- opening in Zotero');
+ await Zotero.Reader.open(item.id, location, {
+ openInWindow,
+ allowDuplicate: openInWindow
+ });
+ return true;
+ }
+
+ let systemHandler = this._getSystemHandler(item.attachmentContentType);
+
+ if (handler === 'system') {
+ handler = systemHandler;
+ Zotero.debug(`System handler is ${handler}`);
+ }
+ else {
+ Zotero.debug(`Custom handler is ${handler}`);
+ }
+
+ let handlers;
+ if (this._mockHandlers) {
+ handlers = this._mockHandlers[readerType];
+ }
+ else if (Zotero.isMac) {
+ handlers = this._handlersMac[readerType];
+ }
+ else if (Zotero.isWin) {
+ handlers = this._handlersWin[readerType];
+ }
+ else if (Zotero.isLinux) {
+ handlers = this._handlersLinux[readerType];
+ }
+
+ let page = location?.position?.pageIndex ?? undefined;
+ // Add 1 to page index for external readers
+ if (page !== undefined && parseInt(page) == page) {
+ page = parseInt(page) + 1;
+ }
+
+ // If there are handlers for this platform and this reader type...
+ if (handlers) {
+ // First try to open with the custom handler
+ try {
+ for (let [i, { name, open }] of handlers.entries()) {
+ if (name.test(handler)) {
+ Zotero.debug('Opening with handler ' + i);
+ await open(handler, { filePath: path, location, page });
+ return true;
+ }
+ }
+ }
+ catch (e) {
+ Zotero.logError(e);
+ }
+
+ // If we get here, we don't have special handling for the custom
+ // handler that the user has set. If we have a location, we really
+ // want to open with something we know how to pass a page number to,
+ // so we'll see if we know how to do that for the system handler.
+ if (location) {
+ try {
+ if (systemHandler && handler !== systemHandler) {
+ Zotero.debug(`Custom handler did not match -- falling back to system handler ${systemHandler}`);
+ handler = systemHandler;
+ for (let [i, { name, open }] of handlers.entries()) {
+ if (name.test(handler)) {
+ Zotero.debug('Opening with handler ' + i);
+ await open(handler, { filePath: path, location, page });
+ return true;
+ }
+ }
+ }
+ }
+ catch (e) {
+ Zotero.logError(e);
+ }
+
+ // And lastly, the fallback handler for this platform/reader type,
+ // if we have one
+ let fallback = handlers.find(h => h.fallback);
+ if (fallback) {
+ try {
+ Zotero.debug('Opening with fallback');
+ await fallback.open(null, { filePath: path, location, page });
+ return true;
+ }
+ catch (e) {
+ // Don't log error if fallback fails
+ // Just move on and try system handler
+ }
+ }
+ }
+ }
+
+ Zotero.debug("Opening handler without page number");
+
+ handler = handler || systemHandler;
+ if (handler) {
+ if (Zotero.isMac) {
+ try {
+ await Zotero.Utilities.Internal.exec('/usr/bin/open', ['-a', handler, path]);
+ return true;
+ }
+ catch (e) {
+ Zotero.logError(e);
+ }
+ }
+
+ try {
+ if (await OS.File.exists(handler)) {
+ Zotero.debug(`Opening with handler ${handler}`);
+ Zotero.launchFileWithApplication(path, handler);
+ return true;
+ }
+ }
+ catch (e) {
+ Zotero.logError(e);
+ }
+ Zotero.logError(`${handler} not found`);
+ }
+
+ Zotero.debug('Launching file normally');
+ Zotero.launchFile(path);
+ return true;
+ },
+
+ _handlersMac: {
+ pdf: [
+ {
+ name: /Preview/,
+ fallback: true,
+ async open(appPath, { filePath, page }) {
+ await Zotero.Utilities.Internal.exec('/usr/bin/open', ['-a', "Preview", filePath]);
+ if (page !== undefined) {
+ // Go to page using AppleScript
+ let args = [
+ '-e', 'tell app "Preview" to activate',
+ '-e', 'tell app "System Events" to keystroke "g" using {option down, command down}',
+ '-e', `tell app "System Events" to keystroke "${page}"`,
+ '-e', 'tell app "System Events" to keystroke return'
+ ];
+ await Zotero.Utilities.Internal.exec('/usr/bin/osascript', args);
+ }
+ },
+ },
+ {
+ name: /Adobe Acrobat/,
+ async open(appPath, { page }) {
+ if (page !== undefined) {
+ // Go to page using AppleScript
+ let args = [
+ '-e', `tell app "${appPath}" to activate`,
+ '-e', 'tell app "System Events" to keystroke "n" using {command down, shift down}',
+ '-e', `tell app "System Events" to keystroke "${page}"`,
+ '-e', 'tell app "System Events" to keystroke return'
+ ];
+ await Zotero.Utilities.Internal.exec('/usr/bin/osascript', args);
+ }
+ }
+ },
+ {
+ name: /Skim/,
+ async open(appPath, { filePath, page }) {
+ // Escape double-quotes in path
+ var quoteRE = /"/g;
+ filePath = filePath.replace(quoteRE, '\\"');
+ let args = [
+ '-e', `tell app "${appPath}" to activate`,
+ '-e', `tell app "${appPath}" to open "${filePath}"`
+ ];
+ if (page !== undefined) {
+ let filename = OS.Path.basename(filePath)
+ .replace(quoteRE, '\\"');
+ args.push('-e', `tell document "${filename}" of application "${appPath}" to go to page ${page}`);
+ }
+ await Zotero.Utilities.Internal.exec('/usr/bin/osascript', args);
+ }
+ },
+ {
+ name: /PDF Expert/,
+ async open(appPath, { page }) {
+ // Go to page using AppleScript (same as Preview)
+ let args = [
+ '-e', `tell app "${appPath}" to activate`
+ ];
+ if (page !== undefined) {
+ args.push(
+ '-e', 'tell app "System Events" to keystroke "g" using {option down, command down}',
+ '-e', `tell app "System Events" to keystroke "${page}"`,
+ '-e', 'tell app "System Events" to keystroke return'
+ );
+ }
+ await Zotero.Utilities.Internal.exec('/usr/bin/osascript', args);
+ }
+ },
+ ],
+ epub: [
+ {
+ name: /Calibre/i,
+ async open(appPath, { filePath, location }) {
+ if (!appPath.endsWith('ebook-viewer.app')) {
+ appPath += '/Contents/ebook-viewer.app';
+ }
+ let args = ['-a', appPath, filePath];
+ if (location?.position?.value) {
+ args.push('--args', '--open-at=' + location.position.value);
+ }
+ await Zotero.Utilities.Internal.exec('/usr/bin/open', args);
+ }
+ },
+ ]
+ },
+
+ _handlersWin: {
+ pdf: [
+ {
+ name: new RegExp(''), // Match any handler
+ async open(appPath, { filePath, page }) {
+ let args = [filePath];
+ if (page !== undefined) {
+ // Include flags to open the PDF on a given page in various apps
+ //
+ // Adobe Acrobat: http://partners.adobe.com/public/developer/en/acrobat/PDFOpenParameters.pdf
+ // PDF-XChange: http://help.tracker-software.com/eu/default.aspx?pageid=PDFXView25:command_line_options
+ args.unshift('/A', 'page=' + page);
+ }
+ await Zotero.Utilities.Internal.exec(appPath, args);
+ }
+ }
+ ],
+ epub: [
+ {
+ name: /Calibre/i,
+ async open(appPath, { filePath, location }) {
+ if (appPath.toLowerCase().endsWith('calibre.exe')) {
+ appPath = appPath.slice(0, -11) + 'ebook-viewer.exe';
+ }
+ let args = [filePath];
+ if (location?.position?.value) {
+ args.push('--open-at=' + location.position.value);
+ }
+ await Zotero.Utilities.Internal.exec(appPath, args);
+ }
+ }
+ ]
+ },
+
+ _handlersLinux: {
+ pdf: [
+ {
+ name: /evince|okular/i,
+ fallback: true,
+ async open(appPath, { filePath, page }) {
+ if (appPath) {
+ switch (appPath.toLowerCase()) {
+ case 'okular':
+ appPath = '/usr/bin/okular';
+
+ // It's "Document Viewer" on stock Ubuntu
+ case 'document viewer':
+ case 'evince':
+ appPath = '/usr/bin/evince';
+ }
+ }
+ else if (await OS.File.exists('/usr/bin/okular')) {
+ appPath = '/usr/bin/okular';
+ }
+ else if (await OS.File.exists('/usr/bin/evince')) {
+ appPath = '/usr/bin/evince';
+ }
+ else {
+ throw new Error('No PDF reader found');
+ }
+
+ // TODO: Try to get default from mimeapps.list, etc., in case system default is okular
+ // or evince somewhere other than /usr/bin
+
+ let args = [filePath];
+ if (page !== undefined) {
+ args.unshift('-p', page);
+ }
+ await Zotero.Utilities.Internal.exec(appPath, args);
+ }
+ }
+ ],
+ epub: [
+ {
+ name: /calibre/i,
+ async open(appPath, { filePath, location }) {
+ if (appPath.toLowerCase().endsWith('calibre')) {
+ appPath = appPath.slice(0, -7) + 'ebook-viewer';
+ }
+ let args = [filePath];
+ if (location?.position?.value) {
+ args.push('--open-at=' + location.position.value);
+ }
+ await Zotero.Utilities.Internal.exec(appPath, args);
+ }
+ }
+ ]
+ },
+
+ _getSystemHandler(mimeType) {
+ if (Zotero.isWin) {
+ return this._getSystemHandlerWin(mimeType);
+ }
+ else {
+ return this._getSystemHandlerPOSIX(mimeType);
+ }
+ },
+
+ _getSystemHandlerWin(mimeType) {
+ // Based on getPDFReader() in ZotFile (GPL)
+ // https://github.com/jlegewie/zotfile/blob/a6c9e02e17b60cbc1f9bb4062486548d9ef583e3/chrome/content/zotfile/utils.js
+
+ var wrk = Components.classes["@mozilla.org/windows-registry-key;1"]
+ .createInstance(Components.interfaces.nsIWindowsRegKey);
+ // Get handler
+ var extension = Zotero.MIME.getPrimaryExtension(mimeType);
+ var tryKeys = [
+ {
+ root: wrk.ROOT_KEY_CURRENT_USER,
+ path: `Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\.${extension}\\UserChoice`,
+ value: 'Progid'
+ },
+ {
+ root: wrk.ROOT_KEY_CLASSES_ROOT,
+ path: `.${extension}`,
+ value: ''
+ }
+ ];
+ var progId;
+ for (let key of tryKeys) {
+ try {
+ wrk.open(key.root, key.path, wrk.ACCESS_READ);
+ progId = wrk.readStringValue(key.value);
+ if (progId) {
+ break;
+ }
+ }
+ catch (e) {}
+ }
+
+ if (!progId) {
+ wrk.close();
+ return false;
+ }
+
+ // Get version specific handler, if it exists
+ try {
+ wrk.open(
+ wrk.ROOT_KEY_CLASSES_ROOT,
+ progId + '\\CurVer',
+ wrk.ACCESS_READ
+ );
+ progId = wrk.readStringValue('') || progId;
+ }
+ catch (e) {}
+
+ // Get command
+ var success = false;
+ tryKeys = [
+ progId + '\\shell\\Read\\command',
+ progId + '\\shell\\Open\\command'
+ ];
+ for (let key of tryKeys) {
+ try {
+ wrk.open(
+ wrk.ROOT_KEY_CLASSES_ROOT,
+ key,
+ wrk.ACCESS_READ
+ );
+ success = true;
+ break;
+ }
+ catch (e) {}
+ }
+
+ if (!success) {
+ wrk.close();
+ return false;
+ }
+
+ try {
+ var command = wrk.readStringValue('').match(/^(?:".+?"|[^"]\S+)/);
+ }
+ catch (e) {}
+
+ wrk.close();
+
+ if (!command) return false;
+ return command[0].replace(/"/g, '');
+ },
+
+ _getSystemHandlerPOSIX(mimeType) {
+ var handlerService = Cc["@mozilla.org/uriloader/handler-service;1"]
+ .getService(Ci.nsIHandlerService);
+ var handlers = handlerService.enumerate();
+ var handler;
+ while (handlers.hasMoreElements()) {
+ let handlerInfo = handlers.getNext().QueryInterface(Ci.nsIHandlerInfo);
+ if (handlerInfo.type == mimeType) {
+ handler = handlerInfo;
+ break;
+ }
+ }
+ if (!handler) {
+ // We can't get the name of the system default handler unless we add an entry
+ Zotero.debug("Default handler not found -- adding default entry");
+ let mimeService = Components.classes["@mozilla.org/mime;1"]
+ .getService(Components.interfaces.nsIMIMEService);
+ let mimeInfo = mimeService.getFromTypeAndExtension(mimeType, "");
+ mimeInfo.preferredAction = 4;
+ mimeInfo.alwaysAskBeforeHandling = false;
+ handlerService.store(mimeInfo);
+
+ // And once we do that, we can get the name (but not the path, unfortunately)
+ let handlers = handlerService.enumerate();
+ while (handlers.hasMoreElements()) {
+ let handlerInfo = handlers.getNext().QueryInterface(Ci.nsIHandlerInfo);
+ if (handlerInfo.type == mimeType) {
+ handler = handlerInfo;
+ break;
+ }
+ }
+ }
+ if (handler) {
+ Zotero.debug(`Default handler is ${handler.defaultDescription}`);
+ return handler.defaultDescription;
+ }
+ return false;
+ }
+};
+
+Zotero.OpenPDF = {
+ openToPage: async function (pathOrItem, page, annotationKey) {
+ Zotero.warn('Zotero.OpenPDF.openToPage() is deprecated -- use Zotero.FileHandlers.open()');
+ if (typeof pathOrItem === 'string') {
+ throw new Error('Zotero.OpenPDF.openToPage() requires an item -- update your code!');
+ }
+
+ await Zotero.FileHandlers.open(pathOrItem, {
+ location: {
+ annotationID: annotationKey,
+ position: {
+ pageIndex: page,
+ }
+ }
+ });
+ }
+};
diff --git a/chrome/content/zotero/xpcom/openPDF.js b/chrome/content/zotero/xpcom/openPDF.js
deleted file mode 100644
index 1cdd419cfc..0000000000
--- a/chrome/content/zotero/xpcom/openPDF.js
+++ /dev/null
@@ -1,353 +0,0 @@
-/*
- ***** BEGIN LICENSE BLOCK *****
-
- Copyright © 2018 Center for History and New Media
- George Mason University, Fairfax, Virginia, USA
- https://zotero.org
-
- This file is part of Zotero.
-
- Zotero is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- Zotero 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 Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with Zotero. If not, see .
-
- ***** END LICENSE BLOCK *****
-*/
-
-/* eslint-disable array-element-newline */
-
-Zotero.OpenPDF = {
- openToPage: async function (pathOrItem, page, annotationKey) {
- var handler = Zotero.Prefs.get("fileHandler.pdf");
-
- var path;
- if (pathOrItem == 'string') {
- Zotero.logError("Zotero.OpenPDF.openToPage() now takes a Zotero.Item rather than a path "
- + "-- please update your code");
- path = pathOrItem;
- }
- else {
- let item = pathOrItem;
- let library = Zotero.Libraries.get(item.libraryID);
- // Zotero PDF reader
- if (!handler) {
- let location = {
- annotationID: annotationKey,
- pageIndex: page && page - 1
- };
- await Zotero.Reader.open(item.id, location);
- return true;
- }
-
- path = await item.getFilePathAsync();
- if (!path) {
- Zotero.warn(`${path} not found`);
- return false;
- }
- }
-
- var opened = false;
-
- if (handler != 'system') {
- Zotero.debug(`Custom handler is ${handler}`);
- }
-
- if (Zotero.isMac) {
- if (!this._openWithHandlerMac(handler, path, page)) {
- // Try to detect default app
- handler = this._getPDFHandlerName();
- if (!this._openWithHandlerMac(handler, path, page)) {
- // Fall back to Preview
- this._openWithPreview(path, page);
- }
- }
- opened = true;
- }
- else if (Zotero.isWin) {
- if (handler == 'system') {
- handler = this._getPDFHandlerWindows();
- if (handler) {
- Zotero.debug(`Default handler is ${handler}`);
- }
- }
- if (handler) {
- // Include flags to open the PDF on a given page in various apps
- //
- // Adobe Acrobat: http://partners.adobe.com/public/developer/en/acrobat/PDFOpenParameters.pdf
- // PDF-XChange: http://help.tracker-software.com/eu/default.aspx?pageid=PDFXView25:command_line_options
- let args = ['/A', 'page=' + page, path];
- Zotero.Utilities.Internal.exec(handler, args);
- opened = true;
- }
- else {
- Zotero.debug("No handler found");
- }
- }
- else if (Zotero.isLinux) {
- if (handler == 'system') {
- handler = await this._getPDFHandlerLinux();
- if (handler) {
- Zotero.debug(`Resolved handler is ${handler}`);
- }
- }
- if (handler && (handler.includes('evince') || handler.includes('okular'))) {
- this._openWithEvinceOrOkular(handler, path, page);
- opened = true;
- }
- // Fall back to okular and then evince if unknown handler
- else if (await OS.File.exists('/usr/bin/okular')) {
- this._openWithEvinceOrOkular('/usr/bin/okular', path, page);
- opened = true;
- }
- else if (await OS.File.exists('/usr/bin/evince')) {
- this._openWithEvinceOrOkular('/usr/bin/evince', path, page);
- opened = true;
- }
- else {
- Zotero.debug("No handler found");
- }
- }
- return opened;
- },
-
- _getPDFHandlerName: function () {
- var handlerService = Cc["@mozilla.org/uriloader/handler-service;1"]
- .getService(Ci.nsIHandlerService);
- var handlers = handlerService.enumerate();
- var handler;
- while (handlers.hasMoreElements()) {
- let handlerInfo = handlers.getNext().QueryInterface(Ci.nsIHandlerInfo);
- if (handlerInfo.type == 'application/pdf') {
- handler = handlerInfo;
- break;
- }
- }
- if (!handler) {
- // We can't get the name of the system default handler unless we add an entry
- Zotero.debug("Default handler not found -- adding default entry");
- let mimeService = Components.classes["@mozilla.org/mime;1"]
- .getService(Components.interfaces.nsIMIMEService);
- let mimeInfo = mimeService.getFromTypeAndExtension("application/pdf", "");
- mimeInfo.preferredAction = 4;
- mimeInfo.alwaysAskBeforeHandling = false;
- handlerService.store(mimeInfo);
-
- // And once we do that, we can get the name (but not the path, unfortunately)
- let handlers = handlerService.enumerate();
- while (handlers.hasMoreElements()) {
- let handlerInfo = handlers.getNext().QueryInterface(Ci.nsIHandlerInfo);
- if (handlerInfo.type == 'application/pdf') {
- handler = handlerInfo;
- break;
- }
- }
- }
- if (handler) {
- Zotero.debug(`Default handler is ${handler.defaultDescription}`);
- return handler.defaultDescription;
- }
- return false;
- },
-
- //
- // Mac
- //
- _openWithHandlerMac: function (handler, path, page) {
- if (!handler) {
- return false;
- }
- if (handler.includes('Preview')) {
- this._openWithPreview(path, page);
- return true;
- }
- if (handler.includes('Adobe Acrobat')) {
- this._openWithAcrobat(handler, path, page);
- return true;
- }
- if (handler.includes('Skim')) {
- this._openWithSkim(handler, path, page);
- return true;
- }
- if (handler.includes('PDF Expert')) {
- this._openWithPDFExpert(handler, path, page);
- return true;
- }
- return false;
- },
-
- _openWithPreview: async function (filePath, page) {
- await Zotero.Utilities.Internal.exec('/usr/bin/open', ['-a', "Preview", filePath]);
- // Go to page using AppleScript
- let args = [
- '-e', 'tell app "Preview" to activate',
- '-e', 'tell app "System Events" to keystroke "g" using {option down, command down}',
- '-e', `tell app "System Events" to keystroke "${page}"`,
- '-e', 'tell app "System Events" to keystroke return'
- ];
- await Zotero.Utilities.Internal.exec('/usr/bin/osascript', args);
- },
-
- _openWithAcrobat: async function (appPath, filePath, page) {
- await Zotero.Utilities.Internal.exec('/usr/bin/open', ['-a', appPath, filePath]);
- // Go to page using AppleScript
- let args = [
- '-e', `tell app "${appPath}" to activate`,
- '-e', 'tell app "System Events" to keystroke "n" using {command down, shift down}',
- '-e', `tell app "System Events" to keystroke "${page}"`,
- '-e', 'tell app "System Events" to keystroke return'
- ];
- await Zotero.Utilities.Internal.exec('/usr/bin/osascript', args);
- },
-
- _openWithSkim: async function (appPath, filePath, page) {
- // Escape double-quotes in path
- var quoteRE = /"/g;
- filePath = filePath.replace(quoteRE, '\\"');
- let filename = OS.Path.basename(filePath).replace(quoteRE, '\\"');
- let args = [
- '-e', `tell app "${appPath}" to activate`,
- '-e', `tell app "${appPath}" to open "${filePath}"`
- ];
- args.push('-e', `tell document "${filename}" of application "${appPath}" to go to page ${page}`);
- await Zotero.Utilities.Internal.exec('/usr/bin/osascript', args);
- },
-
- _openWithPDFExpert: async function (appPath, filePath, page) {
- await Zotero.Utilities.Internal.exec('/usr/bin/open', ['-a', appPath, filePath]);
- // Go to page using AppleScript (same as Preview)
- let args = [
- '-e', `tell app "${appPath}" to activate`,
- '-e', 'tell app "System Events" to keystroke "g" using {option down, command down}',
- '-e', `tell app "System Events" to keystroke "${page}"`,
- '-e', 'tell app "System Events" to keystroke return'
- ];
- await Zotero.Utilities.Internal.exec('/usr/bin/osascript', args);
- },
-
- //
- // Windows
- //
- /**
- * Get path to default pdf reader application on windows
- *
- * From getPDFReader() in ZotFile (GPL)
- * https://github.com/jlegewie/zotfile/blob/master/chrome/content/zotfile/utils.js
- *
- * @return {String|false} - Path to default pdf reader application, or false if none
- */
- _getPDFHandlerWindows: function () {
- var wrk = Components.classes["@mozilla.org/windows-registry-key;1"]
- .createInstance(Components.interfaces.nsIWindowsRegKey);
- // Get handler for PDFs
- var tryKeys = [
- {
- root: wrk.ROOT_KEY_CURRENT_USER,
- path: 'Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts\\.pdf\\UserChoice',
- value: 'Progid'
- },
- {
- root: wrk.ROOT_KEY_CLASSES_ROOT,
- path: '.pdf',
- value: ''
- }
- ];
- var progId;
- for (let i = 0; !progId && i < tryKeys.length; i++) {
- try {
- wrk.open(
- tryKeys[i].root,
- tryKeys[i].path,
- wrk.ACCESS_READ
- );
- progId = wrk.readStringValue(tryKeys[i].value);
- }
- catch (e) {}
- }
-
- if (!progId) {
- wrk.close();
- return false;
- }
-
- // Get version specific handler, if it exists
- try {
- wrk.open(
- wrk.ROOT_KEY_CLASSES_ROOT,
- progId + '\\CurVer',
- wrk.ACCESS_READ
- );
- progId = wrk.readStringValue('') || progId;
- }
- catch (e) {}
-
- // Get command
- var success = false;
- tryKeys = [
- progId + '\\shell\\Read\\command',
- progId + '\\shell\\Open\\command'
- ];
- for (let i = 0; !success && i < tryKeys.length; i++) {
- try {
- wrk.open(
- wrk.ROOT_KEY_CLASSES_ROOT,
- tryKeys[i],
- wrk.ACCESS_READ
- );
- success = true;
- }
- catch (e) {}
- }
-
- if (!success) {
- wrk.close();
- return false;
- }
-
- try {
- var command = wrk.readStringValue('').match(/^(?:".+?"|[^"]\S+)/);
- }
- catch (e) {}
-
- wrk.close();
-
- if (!command) return false;
- return command[0].replace(/"/g, '');
- },
-
- //
- // Linux
- //
- _getPDFHandlerLinux: async function () {
- var name = this._getPDFHandlerName();
- switch (name.toLowerCase()) {
- case 'okular':
- return `/usr/bin/${name}`;
-
- // It's "Document Viewer" on stock Ubuntu
- case 'document viewer':
- case 'evince':
- return `/usr/bin/evince`;
- }
-
- // TODO: Try to get default from mimeapps.list, etc., in case system default is okular
- // or evince somewhere other than /usr/bin
- var homeDir = OS.Constants.Path.homeDir;
-
- return false;
-
- },
-
- _openWithEvinceOrOkular: function (appPath, filePath, page) {
- var args = ['-p', page, filePath];
- Zotero.Utilities.Internal.exec(appPath, args);
- }
-}
diff --git a/chrome/content/zotero/xpcom/reader.js b/chrome/content/zotero/xpcom/reader.js
index acdecf7297..1f25dda8a3 100644
--- a/chrome/content/zotero/xpcom/reader.js
+++ b/chrome/content/zotero/xpcom/reader.js
@@ -48,11 +48,9 @@ class ReaderInstance {
this._pendingWriteStateTimeout = null;
this._pendingWriteStateFunction = null;
- switch (this._item.attachmentContentType) {
- case 'application/pdf': this._type = 'pdf'; break;
- case 'application/epub+zip': this._type = 'epub'; break;
- case 'text/html': this._type = 'snapshot'; break;
- default: throw new Error('Unsupported attachment type');
+ this._type = this._item.attachmentReaderType;
+ if (!this._type) {
+ throw new Error('Unsupported attachment type');
}
return new Proxy(this, {
@@ -93,7 +91,7 @@ class ReaderInstance {
}
getSecondViewState() {
- let state = this._iframeWindow.wrappedJSObject.getSecondViewState();
+ let state = this._iframeWindow?.wrappedJSObject?.getSecondViewState?.();
return state ? JSON.parse(JSON.stringify(state)) : undefined;
}
diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js
index 153cdd737a..da507aec77 100644
--- a/chrome/content/zotero/xpcom/zotero.js
+++ b/chrome/content/zotero/xpcom/zotero.js
@@ -998,6 +998,13 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
file.launch();
}
catch (e) {
+ // macOS only: if there's no associated application, launch() will throw, but
+ // the OS will show a dialog asking the user to choose an application. We don't
+ // want to show the Firefox dialog in that case.
+ if (Zotero.isMac && file.exists()) {
+ return;
+ }
+
Zotero.debug(e, 2);
Zotero.debug("launch() not supported -- trying fallback executable", 2);
@@ -1013,18 +1020,19 @@ Services.scriptloader.loadSubScript("resource://zotero/polyfill.js");
}
catch (e) {
Zotero.debug(e);
- Zotero.debug("Launching via executable failed -- passing to loadUrl()");
+ Zotero.debug("Launching via executable failed -- passing to loadURI()");
// If nsIFile.launch() isn't available and the fallback
// executable doesn't exist, we just let the Firefox external
// helper app window handle it
- var nsIFPH = Components.classes["@mozilla.org/network/protocol;1?name=file"]
- .getService(Components.interfaces.nsIFileProtocolHandler);
- var uri = nsIFPH.newFileURI(file);
+ var uri = Services.io.newFileURI(file);
var nsIEPS = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"].
getService(Components.interfaces.nsIExternalProtocolService);
- nsIEPS.loadUrl(uri);
+ nsIEPS.loadURI(
+ uri,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ );
}
}
};
diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js
index 517c198860..b76c63515b 100644
--- a/chrome/content/zotero/zoteroPane.js
+++ b/chrome/content/zotero/zoteroPane.js
@@ -4908,64 +4908,15 @@ var ZoteroPane = new function()
await item.saveTx();
}
- if (['application/pdf', 'application/epub+zip', 'text/html'].includes(contentType)) {
- let type;
- if (contentType === 'application/pdf') {
- type = 'pdf';
- }
- else if (contentType === 'application/epub+zip') {
- type = 'epub';
- }
- else {
- type = 'snapshot';
- }
- let handler = Zotero.Prefs.get('fileHandler.' + type);
-
- // Zotero PDF reader
- if (!handler) {
- let openInWindow = Zotero.Prefs.get('openReaderInNewWindow');
- let useAlternateWindowBehavior = event?.shiftKey || extraData?.forceAlternateWindowBehavior;
- if (useAlternateWindowBehavior) {
- openInWindow = !openInWindow;
- }
- await Zotero.Reader.open(
- item.id,
- extraData && extraData.location,
- {
- openInWindow,
- allowDuplicate: openInWindow
- }
- );
- return;
- }
- // Try to open external PDF reader to page number if specified
- // TODO: Implement for EPUBs if readers support it
- else if (type == 'pdf') {
- let pageIndex = extraData?.location?.position?.pageIndex;
- if (pageIndex !== undefined) {
- await Zotero.OpenPDF.openToPage(
- item,
- parseInt(pageIndex) + 1
- );
- return;
- }
- }
- // Custom PDF handler
- // TODO: Remove this and unify with Zotero.OpenPDF
- if (handler != 'system') {
- try {
- if (await OS.File.exists(handler)) {
- Zotero.launchFileWithApplication(path, handler);
- return;
- }
- }
- catch (e) {
- Zotero.logError(e);
- }
- Zotero.logError(`${handler} not found -- launching file normally`);
- }
+ let openInWindow = Zotero.Prefs.get('openReaderInNewWindow');
+ let useAlternateWindowBehavior = event?.shiftKey || extraData?.forceAlternateWindowBehavior;
+ if (useAlternateWindowBehavior) {
+ openInWindow = !openInWindow;
}
- Zotero.launchFile(path);
+ await Zotero.FileHandlers.open(item, {
+ location: extraData?.location,
+ openInWindow,
+ });
};
for (let i = 0; i < itemIDs.length; i++) {
diff --git a/components/zotero-protocol-handler.js b/components/zotero-protocol-handler.js
index 8b61c337a1..fa4cf717d4 100644
--- a/components/zotero-protocol-handler.js
+++ b/components/zotero-protocol-handler.js
@@ -1135,7 +1135,7 @@ function ZoteroProtocolHandler() {
* Also supports ZotFile format:
* zotero://open-pdf/[libraryID]_[key]/[page]
*/
- var OpenPDFExtension = {
+ var OpenExtension = {
noContent: true,
doAction: async function (uri) {
@@ -1145,8 +1145,7 @@ function ZoteroProtocolHandler() {
if (!uriPath) {
return 'Invalid URL';
}
- uriPath = uriPath.substr('//open-pdf/'.length);
- var mimeType, content = '';
+ uriPath = uriPath.replace(/^\/\/open(-pdf)?\//, '');
var params = {
objectType: 'item'
@@ -1174,11 +1173,7 @@ function ZoteroProtocolHandler() {
Zotero.API.parseParams(params);
var results = await Zotero.API.getResultsFromParams(params);
- var page = params.page;
- if (parseInt(page) != page) {
- page = null;
- }
- var annotation = params.annotation;
+ var { annotation, page, cfi, sel } = params;
if (!results.length) {
Zotero.warn(`No item found for ${uriPath}`);
@@ -1198,32 +1193,43 @@ function ZoteroProtocolHandler() {
return;
}
- if (!path.toLowerCase().endsWith('.pdf')
- && Zotero.MIME.sniffForMIMEType(await Zotero.File.getSample(path)) != 'application/pdf') {
- Zotero.warn(`${path} is not a PDF`);
- return;
+ try {
+ if (page) {
+ await Zotero.FileHandlers.open(item, {
+ location: {
+ position: {
+ pageIndex: page
+ },
+ annotationID: annotation
+ }
+ });
+ }
+ else if (cfi) {
+ await Zotero.FileHandlers.open(item, {
+ location: {
+ position: {
+ type: 'FragmentSelector',
+ conformsTo: 'http://www.idpf.org/epub/linking/cfi/epub-cfi.html',
+ value: cfi
+ }
+ }
+ });
+ }
+ else if (sel) {
+ await Zotero.FileHandlers.open(item, {
+ location: {
+ position: {
+ type: 'CssSelector',
+ value: sel
+ }
+ }
+ });
+ }
+ }
+ catch (e) {
+ Zotero.logError(e);
}
- var opened = false;
- if (page || annotation) {
- try {
- opened = await Zotero.OpenPDF.openToPage(item, page, annotation);
- }
- catch (e) {
- Zotero.logError(e);
- }
- }
-
- // If something went wrong, just open PDF without page
- if (!opened) {
- Zotero.debug("Launching PDF without page number");
- let zp = Zotero.getActiveZoteroPane();
- // TODO: Open pane if closed (macOS)
- if (zp) {
- zp.viewAttachment([item.id]);
- }
- return;
- }
Zotero.Notifier.trigger('open', 'file', item.id);
},
@@ -1241,7 +1247,8 @@ function ZoteroProtocolHandler() {
this._extensions[ZOTERO_SCHEME + "://debug"] = DebugExtension;
this._extensions[ZOTERO_SCHEME + "://connector"] = ConnectorExtension;
this._extensions[ZOTERO_SCHEME + "://pdf.js"] = PDFJSExtension;
- this._extensions[ZOTERO_SCHEME + "://open-pdf"] = OpenPDFExtension;
+ this._extensions[ZOTERO_SCHEME + "://open"] = OpenExtension;
+ this._extensions[ZOTERO_SCHEME + "://open-pdf"] = OpenExtension;
}
diff --git a/components/zotero-service.js b/components/zotero-service.js
index cf24c15591..f7b1f25aa2 100644
--- a/components/zotero-service.js
+++ b/components/zotero-service.js
@@ -113,7 +113,7 @@ const xpcomFilesLocal = [
'locateManager',
'mime',
'notifier',
- 'openPDF',
+ 'fileHandlers',
'plugins',
'reader',
'progressQueue',
diff --git a/test/tests/fileHandlersTest.js b/test/tests/fileHandlersTest.js
new file mode 100644
index 0000000000..0db8b8d056
--- /dev/null
+++ b/test/tests/fileHandlersTest.js
@@ -0,0 +1,105 @@
+describe("Zotero.FileHandlers", () => {
+ describe("open()", () => {
+ var win;
+
+ function clearPrefs() {
+ Zotero.Prefs.clear('fileHandler.pdf');
+ Zotero.Prefs.clear('fileHandler.epub');
+ Zotero.Prefs.clear('fileHandler.snapshot');
+ Zotero.Prefs.clear('openReaderInNewWindow');
+ }
+
+ before(async function () {
+ clearPrefs();
+ win = await loadZoteroPane();
+ });
+
+ afterEach(function () {
+ clearPrefs();
+ delete Zotero.FileHandlers._mockHandlers;
+ for (let reader of Zotero.Reader._readers) {
+ reader.close();
+ }
+ });
+
+ after(async function () {
+ win.close();
+ });
+
+ it("should open a PDF internally when no handler is set", async function () {
+ let pdf = await importFileAttachment('wonderland_short.pdf');
+ await Zotero.FileHandlers.open(pdf, {
+ location: { position: { pageIndex: 2 } }
+ });
+ assert.ok(Zotero.Reader.getByTabID(win.Zotero_Tabs.selectedID));
+ });
+
+ it("should open a PDF in a new window when no handler is set and openInWindow is passed", async function () {
+ let pdf = await importFileAttachment('wonderland_short.pdf');
+ await Zotero.FileHandlers.open(pdf, {
+ location: { position: { pageIndex: 2 } },
+ openInWindow: true
+ });
+ assert.notOk(Zotero.Reader.getByTabID(win.Zotero_Tabs.selectedID));
+ assert.isNotEmpty(Zotero.Reader.getWindowStates());
+ });
+
+ it("should use matching handler", async function () {
+ let pdf = await importFileAttachment('wonderland_short.pdf');
+ let wasRun = false;
+ let readerOpenSpy = sinon.spy(Zotero.Reader, 'open');
+ Zotero.FileHandlers._mockHandlers = {
+ pdf: [
+ {
+ name: /mock/,
+ async open() {
+ wasRun = true;
+ }
+ }
+ ]
+ };
+ Zotero.Prefs.set('fileHandler.pdf', 'mock');
+
+ await Zotero.FileHandlers.open(pdf);
+ assert.isTrue(wasRun);
+ assert.isFalse(readerOpenSpy.called);
+ assert.notOk(Zotero.Reader.getByTabID(win.Zotero_Tabs.selectedID));
+ assert.isEmpty(Zotero.Reader.getWindowStates());
+
+ readerOpenSpy.restore();
+ });
+
+ it("should fall back to fallback handler when location is passed", async function () {
+ let pdf = await importFileAttachment('wonderland_short.pdf');
+ let wasRun = false;
+ let readerOpenSpy = sinon.spy(Zotero.Reader, 'open');
+ Zotero.FileHandlers._mockHandlers = {
+ pdf: [
+ {
+ name: /mock/,
+ fallback: true,
+ async open(appPath) {
+ assert.notOk(appPath); // appPath won't be set when called as fallback
+ wasRun = true;
+ }
+ }
+ ]
+ };
+
+ // Set our custom handler to something nonexistent,
+ // and stub the system handler to something nonexistent as well
+ Zotero.Prefs.set('fileHandler.pdf', 'some nonexistent tool');
+ let getSystemHandlerStub = sinon.stub(Zotero.FileHandlers, '_getSystemHandler');
+ getSystemHandlerStub.returns('some other nonexistent tool');
+
+ await Zotero.FileHandlers.open(pdf, { location: {} });
+ assert.isTrue(wasRun);
+ assert.isFalse(readerOpenSpy.called);
+ assert.notOk(Zotero.Reader.getByTabID(win.Zotero_Tabs.selectedID));
+ assert.isEmpty(Zotero.Reader.getWindowStates());
+
+ readerOpenSpy.restore();
+ getSystemHandlerStub.restore();
+ });
+ });
+});