Unify attachment opening code and support EPUB/snapshot note links (#3705)

This commit is contained in:
Abe Jellinek 2024-02-23 06:38:11 -05:00 committed by GitHub
parent b1333d0e9d
commit a7d59a90d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 696 additions and 465 deletions

View file

@ -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";

View file

@ -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;
};

View file

@ -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;

View file

@ -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 <http://www.gnu.org/licenses/>.
***** 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,
}
}
});
}
};

View file

@ -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 <http://www.gnu.org/licenses/>.
***** 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);
}
}

View file

@ -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;
}

View file

@ -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(),
);
}
}
};

View file

@ -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++) {

View file

@ -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;
}

View file

@ -113,7 +113,7 @@ const xpcomFilesLocal = [
'locateManager',
'mime',
'notifier',
'openPDF',
'fileHandlers',
'plugins',
'reader',
'progressQueue',

View file

@ -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();
});
});
});