zotero/components/zotero-protocol-handler.js

1534 lines
44 KiB
JavaScript
Raw Normal View History

2007-10-23 07:11:59 +00:00
/*
***** BEGIN LICENSE BLOCK *****
2009-12-28 09:47:49 +00:00
Copyright © 2009 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
2007-10-23 07:11:59 +00:00
2009-12-28 09:47:49 +00:00
This file is part of Zotero.
2007-10-23 07:11:59 +00:00
2009-12-28 09:47:49 +00:00
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
2009-12-28 09:47:49 +00:00
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.
2009-12-28 09:47:49 +00:00
You should have received a copy of the GNU Affero General Public License
2009-12-28 09:47:49 +00:00
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
2007-10-23 07:11:59 +00:00
Based on nsChromeExtensionHandler example code by Ed Anuff at
http://kb.mozillazine.org/Dev_:_Extending_the_Chrome_Protocol
***** END LICENSE BLOCK *****
*/
const ZOTERO_SCHEME = "zotero";
const ZOTERO_PROTOCOL_CID = Components.ID("{9BC3D762-9038-486A-9D70-C997AF848A7C}");
const ZOTERO_PROTOCOL_CONTRACTID = "@mozilla.org/network/protocol;1?name=" + ZOTERO_SCHEME;
const ZOTERO_PROTOCOL_NAME = "Zotero Chrome Extension Protocol";
Components.utils.import("resource://gre/modules/Services.jsm");
Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/NetUtil.jsm");
Components.utils.import("resource://gre/modules/osfile.jsm")
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const ios = Services.io;
2007-10-23 07:11:59 +00:00
// Dummy chrome URL used to obtain a valid chrome channel
const DUMMY_CHROME_URL = "chrome://zotero/content/zoteroPane.xul";
2007-10-23 07:11:59 +00:00
var Zotero = Components.classes["@zotero.org/Zotero;1"]
.getService(Components.interfaces.nsISupports)
.wrappedJSObject;
function ZoteroProtocolHandler() {
2007-10-23 07:11:59 +00:00
this.wrappedJSObject = this;
this._principal = null;
2007-10-23 07:11:59 +00:00
this._extensions = {};
/**
* zotero://attachment/library/[itemKey]
* zotero://attachment/groups/[groupID]/[itemKey]
*/
var AttachmentExtension = {
loadAsChrome: false,
newChannel: function (uri) {
return new AsyncChannel(uri, function* () {
try {
var uriPath = uri.pathQueryRef;
if (!uriPath) {
return this._errorChannel('Invalid URL');
}
uriPath = uriPath.substr('//attachment/'.length);
var params = {};
var router = new Zotero.Router(params);
router.add('library/items/:itemKey', function () {
params.libraryID = Zotero.Libraries.userLibraryID;
});
router.add('groups/:groupID/items/:itemKey');
router.run(uriPath);
if (params.groupID) {
params.libraryID = Zotero.Groups.getLibraryIDFromGroupID(params.groupID);
}
if (!params.itemKey) {
return this._errorChannel("Item key not provided");
}
var item = yield Zotero.Items.getByLibraryAndKeyAsync(params.libraryID, params.itemKey);
if (!item) {
return this._errorChannel(`No item found for ${uriPath}`);
}
if (!item.isFileAttachment()) {
return this._errorChannel(`Item for ${uriPath} is not a file attachment`);
}
var path = yield item.getFilePathAsync();
if (!path) {
return this._errorChannel(`${path} not found`);
}
// Set originalURI so that it seems like we're serving from zotero:// protocol.
// This is necessary to allow url() links to work from within CSS files.
// Otherwise they try to link to files on the file:// protocol, which isn't allowed.
this.originalURI = uri;
return Zotero.File.pathToFile(path);
}
catch (e) {
return this._errorChannel(e.message);
}
}.bind(this));
},
_errorChannel: function (msg) {
Zotero.logError(msg);
this.status = Components.results.NS_ERROR_FAILURE;
this.contentType = 'text/plain';
return msg;
}
};
/**
* zotero://data/library/collection/ABCD1234/items?sort=itemType&direction=desc
* zotero://data/groups/12345/collection/ABCD1234/items?sort=title&direction=asc
*/
var DataExtension = {
loadAsChrome: false,
newChannel: function (uri) {
return new AsyncChannel(uri, function* () {
this.contentType = 'text/plain';
path = uri.spec.match(/zotero:\/\/[^/]+(.*)/)[1];
try {
return Zotero.Utilities.Internal.getAsyncInputStream(
Zotero.API.Data.getGenerator(path)
);
}
catch (e) {
if (e instanceof Zotero.Router.InvalidPathException) {
return "URL could not be parsed";
}
}
});
}
};
2007-10-23 07:11:59 +00:00
/*
* Report generation extension for Zotero protocol
*/
var ReportExtension = {
loadAsChrome: false,
2007-10-23 07:11:59 +00:00
newChannel: function (uri) {
return new AsyncChannel(uri, function* () {
var userLibraryID = Zotero.Libraries.userLibraryID;
var path = uri.pathQueryRef;
if (!path) {
return 'Invalid URL';
}
path = path.substr('//report/'.length);
2007-10-23 07:11:59 +00:00
// Proxy CSS files
if (path.endsWith('.css')) {
var chromeURL = 'chrome://zotero/skin/report/' + path;
Zotero.debug(chromeURL);
let uri = ios.newURI(chromeURL, null, null);
var chromeReg = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
.getService(Components.interfaces.nsIChromeRegistry);
return chromeReg.convertChromeURL(uri);
}
2007-10-23 07:11:59 +00:00
var params = {
objectType: 'item',
format: 'html',
sort: 'title'
};
var router = new Zotero.Router(params);
// Items within a collection or search
router.add('library/:scopeObject/:scopeObjectKey/items', function () {
params.libraryID = userLibraryID;
});
router.add('groups/:groupID/:scopeObject/:scopeObjectKey/items');
// All items
router.add('library/items/:objectKey', function () {
params.libraryID = userLibraryID;
});
router.add('groups/:groupID/items');
// Old-style URLs
router.add('collection/:id/html/report.html', function () {
params.scopeObject = 'collections';
var lkh = Zotero.Collections.parseLibraryKeyHash(params.id);
if (lkh) {
params.libraryID = lkh.libraryID || userLibraryID;
params.scopeObjectKey = lkh.key;
}
else {
params.scopeObjectID = params.id;
2007-10-23 07:11:59 +00:00
}
delete params.id;
});
router.add('search/:id/html/report.html', function () {
params.scopeObject = 'searches';
var lkh = Zotero.Searches.parseLibraryKeyHash(this.id);
if (lkh) {
params.libraryID = lkh.libraryID || userLibraryID;
params.scopeObjectKey = lkh.key;
}
else {
params.scopeObjectID = this.id;
}
delete params.id;
});
router.add('items/:ids/html/report.html', function () {
var ids = this.ids.split('-');
params.libraryID = ids[0].split('_')[0] || userLibraryID;
params.itemKey = ids.map(x => x.split('_')[1]);
delete params.ids;
});
var parsed = router.run(path);
if (!parsed) {
return "URL could not be parsed";
2007-10-23 07:11:59 +00:00
}
// TODO: support old URLs
// collection
// search
// items
// item
if (params.sort.indexOf('/') != -1) {
let parts = params.sort.split('/');
params.sort = parts[0];
params.direction = parts[1] == 'd' ? 'desc' : 'asc';
}
try {
Zotero.API.parseParams(params);
var results = yield Zotero.API.getResultsFromParams(params);
}
catch (e) {
Zotero.debug(e, 1);
return e.toString();
2007-10-23 07:11:59 +00:00
}
var mimeType, content = '';
2007-10-23 07:11:59 +00:00
var items = [];
var itemsHash = {}; // key = itemID, val = position in |items|
var searchItemIDs = new Set(); // All selected items
var searchParentIDs = new Set(); // Parents of selected child items
var searchChildIDs = new Set() // Selected chlid items
2007-10-23 07:11:59 +00:00
var includeAllChildItems = Zotero.Prefs.get('report.includeAllChildItems');
var combineChildItems = Zotero.Prefs.get('report.combineChildItems');
var unhandledParents = {};
2007-10-23 07:11:59 +00:00
for (var i=0; i<results.length; i++) {
// Don't add child items directly
// (instead mark their parents for inclusion below)
var parentItemID = results[i].parentItemID;
Async DB megacommit Promise-based rewrite of most of the codebase, with asynchronous database and file access -- see https://github.com/zotero/zotero/issues/518 for details. WARNING: This includes backwards-incompatible schema changes. An incomplete list of other changes: - Schema overhaul - Replace main tables with new versions with updated schema - Enable real foreign key support and remove previous triggers - Don't use NULLs for local libraryID, which broke the UNIQUE index preventing object key duplication. All code (Zotero and third-party) using NULL for the local library will need to be updated to use 0 instead (already done for Zotero code) - Add 'compatibility' DB version that can be incremented manually to break DB compatibility with previous versions. 'userdata' upgrades will no longer automatically break compatibility. - Demote creators and tags from first-class objects to item properties - New API syncing properties - 'synced'/'version' properties to data objects - 'etag' to groups - 'version' to libraries - Create Zotero.DataObject that other objects inherit from - Consolidate data object loading into Zotero.DataObjects - Change object reloading so that only the loaded and changed parts of objects are reloaded, instead of reloading all data from the database (with some exceptions, including item primary data) - Items and collections now have .parentItem and .parentKey properties, replacing item.getSource() and item.getSourceKey() - New function Zotero.serial(fn), to wrap an async function such that all calls are run serially - New function Zotero.Utilities.Internal.forEachChunkAsync(arr, chunkSize, func) - Add tag selector loading message - Various API and name changes, since everything was breaking anyway Known broken things: - Syncing (will be completely rewritten for API syncing) - Translation architecture (needs promise-based rewrite) - Duplicates view - DB integrity check (from schema changes) - Dragging (may be difficult to fix) Lots of other big and little things are certainly broken, particularly with the UI, which can be affected by async code in all sorts of subtle ways.
2014-08-06 21:38:05 +00:00
if (parentItemID) {
searchParentIDs.add(parentItemID);
searchChildIDs.add(results[i].id);
2007-10-23 07:11:59 +00:00
// Don't include all child items if any child
// items were selected
includeAllChildItems = false;
}
// If combining children or standalone note/attachment, add matching parents
else if (combineChildItems || !results[i].isRegularItem()
|| results[i].numChildren() == 0) {
itemsHash[results[i].id] = [items.length];
Deasyncification :back: :cry: While trying to get translation and citing working with asynchronously generated data, we realized that drag-and-drop support was going to be...problematic. Firefox only supports synchronous methods for providing drag data (unlike, it seems, the DataTransferItem interface supported by Chrome), which means that we'd need to preload all relevant data on item selection (bounded by export.quickCopy.dragLimit) and keep the translate/cite methods synchronous (or maintain two separate versions). What we're trying instead is doing what I said in #518 we weren't going to do: loading most object data on startup and leaving many more functions synchronous. Essentially, this takes the various load*() methods described in #518, moves them to startup, and makes them operate on entire libraries rather than individual objects. The obvious downside here (other than undoing much of the work of the last many months) is that it increases startup time, potentially quite a lot for larger libraries. On my laptop, with a 3,000-item library, this adds about 3 seconds to startup time. I haven't yet tested with larger libraries. But I'm hoping that we can optimize this further to reduce that delay. Among other things, this is loading data for all libraries, when it should be able to load data only for the library being viewed. But this is also fundamentally just doing some SELECT queries and storing the results, so it really shouldn't need to be that slow (though performance may be bounded a bit here by XPCOM overhead). If we can make this fast enough, it means that third-party plugins should be able to remain much closer to their current designs. (Some things, including saving, will still need to be made asynchronous.)
2016-03-07 21:05:51 +00:00
items.push(results[i].toJSON({ mode: 'full' }));
2007-10-23 07:11:59 +00:00
// Flag item as a search match
items[items.length - 1].reportSearchMatch = true;
}
else {
unhandledParents[i] = true;
}
searchItemIDs.add(results[i].id);
2007-10-23 07:11:59 +00:00
}
// If including all child items, add children of all matched
// parents to the child array
if (includeAllChildItems) {
for (let id of searchItemIDs) {
if (!searchChildIDs.has(id)) {
2007-10-23 07:11:59 +00:00
var children = [];
var item = yield Zotero.Items.getAsync(id);
2007-10-23 07:11:59 +00:00
if (!item.isRegularItem()) {
continue;
}
var func = function (ids) {
if (ids) {
for (var i=0; i<ids.length; i++) {
searchChildIDs.add(ids[i]);
2007-10-23 07:11:59 +00:00
}
}
};
func(item.getNotes());
func(item.getAttachments());
}
}
}
// If not including all children, add matching parents,
// in case they don't have any matching children below
else {
for (var i in unhandledParents) {
itemsHash[results[i].id] = [items.length];
Deasyncification :back: :cry: While trying to get translation and citing working with asynchronously generated data, we realized that drag-and-drop support was going to be...problematic. Firefox only supports synchronous methods for providing drag data (unlike, it seems, the DataTransferItem interface supported by Chrome), which means that we'd need to preload all relevant data on item selection (bounded by export.quickCopy.dragLimit) and keep the translate/cite methods synchronous (or maintain two separate versions). What we're trying instead is doing what I said in #518 we weren't going to do: loading most object data on startup and leaving many more functions synchronous. Essentially, this takes the various load*() methods described in #518, moves them to startup, and makes them operate on entire libraries rather than individual objects. The obvious downside here (other than undoing much of the work of the last many months) is that it increases startup time, potentially quite a lot for larger libraries. On my laptop, with a 3,000-item library, this adds about 3 seconds to startup time. I haven't yet tested with larger libraries. But I'm hoping that we can optimize this further to reduce that delay. Among other things, this is loading data for all libraries, when it should be able to load data only for the library being viewed. But this is also fundamentally just doing some SELECT queries and storing the results, so it really shouldn't need to be that slow (though performance may be bounded a bit here by XPCOM overhead). If we can make this fast enough, it means that third-party plugins should be able to remain much closer to their current designs. (Some things, including saving, will still need to be made asynchronous.)
2016-03-07 21:05:51 +00:00
items.push(results[i].toJSON({ mode: 'full' }));
// Flag item as a search match
items[items.length - 1].reportSearchMatch = true;
}
}
2007-10-23 07:11:59 +00:00
if (combineChildItems) {
// Add parents of matches if parents aren't matches themselves
for (let id of searchParentIDs) {
if (!searchItemIDs.has(id) && !itemsHash[id]) {
var item = yield Zotero.Items.getAsync(id);
2007-10-23 07:11:59 +00:00
itemsHash[id] = items.length;
Deasyncification :back: :cry: While trying to get translation and citing working with asynchronously generated data, we realized that drag-and-drop support was going to be...problematic. Firefox only supports synchronous methods for providing drag data (unlike, it seems, the DataTransferItem interface supported by Chrome), which means that we'd need to preload all relevant data on item selection (bounded by export.quickCopy.dragLimit) and keep the translate/cite methods synchronous (or maintain two separate versions). What we're trying instead is doing what I said in #518 we weren't going to do: loading most object data on startup and leaving many more functions synchronous. Essentially, this takes the various load*() methods described in #518, moves them to startup, and makes them operate on entire libraries rather than individual objects. The obvious downside here (other than undoing much of the work of the last many months) is that it increases startup time, potentially quite a lot for larger libraries. On my laptop, with a 3,000-item library, this adds about 3 seconds to startup time. I haven't yet tested with larger libraries. But I'm hoping that we can optimize this further to reduce that delay. Among other things, this is loading data for all libraries, when it should be able to load data only for the library being viewed. But this is also fundamentally just doing some SELECT queries and storing the results, so it really shouldn't need to be that slow (though performance may be bounded a bit here by XPCOM overhead). If we can make this fast enough, it means that third-party plugins should be able to remain much closer to their current designs. (Some things, including saving, will still need to be made asynchronous.)
2016-03-07 21:05:51 +00:00
items.push(item.toJSON({ mode: 'full' }));
2007-10-23 07:11:59 +00:00
}
}
// Add children to reportChildren property of parents
for (let id of searchChildIDs) {
let item = yield Zotero.Items.getAsync(id);
var parentID = item.parentID;
if (!items[itemsHash[parentID]].reportChildren) {
items[itemsHash[parentID]].reportChildren = {
2007-10-23 07:11:59 +00:00
notes: [],
attachments: []
};
}
if (item.isNote()) {
Deasyncification :back: :cry: While trying to get translation and citing working with asynchronously generated data, we realized that drag-and-drop support was going to be...problematic. Firefox only supports synchronous methods for providing drag data (unlike, it seems, the DataTransferItem interface supported by Chrome), which means that we'd need to preload all relevant data on item selection (bounded by export.quickCopy.dragLimit) and keep the translate/cite methods synchronous (or maintain two separate versions). What we're trying instead is doing what I said in #518 we weren't going to do: loading most object data on startup and leaving many more functions synchronous. Essentially, this takes the various load*() methods described in #518, moves them to startup, and makes them operate on entire libraries rather than individual objects. The obvious downside here (other than undoing much of the work of the last many months) is that it increases startup time, potentially quite a lot for larger libraries. On my laptop, with a 3,000-item library, this adds about 3 seconds to startup time. I haven't yet tested with larger libraries. But I'm hoping that we can optimize this further to reduce that delay. Among other things, this is loading data for all libraries, when it should be able to load data only for the library being viewed. But this is also fundamentally just doing some SELECT queries and storing the results, so it really shouldn't need to be that slow (though performance may be bounded a bit here by XPCOM overhead). If we can make this fast enough, it means that third-party plugins should be able to remain much closer to their current designs. (Some things, including saving, will still need to be made asynchronous.)
2016-03-07 21:05:51 +00:00
items[itemsHash[parentID]].reportChildren.notes.push(item.toJSON({ mode: 'full' }));
2007-10-23 07:11:59 +00:00
}
if (item.isAttachment()) {
Deasyncification :back: :cry: While trying to get translation and citing working with asynchronously generated data, we realized that drag-and-drop support was going to be...problematic. Firefox only supports synchronous methods for providing drag data (unlike, it seems, the DataTransferItem interface supported by Chrome), which means that we'd need to preload all relevant data on item selection (bounded by export.quickCopy.dragLimit) and keep the translate/cite methods synchronous (or maintain two separate versions). What we're trying instead is doing what I said in #518 we weren't going to do: loading most object data on startup and leaving many more functions synchronous. Essentially, this takes the various load*() methods described in #518, moves them to startup, and makes them operate on entire libraries rather than individual objects. The obvious downside here (other than undoing much of the work of the last many months) is that it increases startup time, potentially quite a lot for larger libraries. On my laptop, with a 3,000-item library, this adds about 3 seconds to startup time. I haven't yet tested with larger libraries. But I'm hoping that we can optimize this further to reduce that delay. Among other things, this is loading data for all libraries, when it should be able to load data only for the library being viewed. But this is also fundamentally just doing some SELECT queries and storing the results, so it really shouldn't need to be that slow (though performance may be bounded a bit here by XPCOM overhead). If we can make this fast enough, it means that third-party plugins should be able to remain much closer to their current designs. (Some things, including saving, will still need to be made asynchronous.)
2016-03-07 21:05:51 +00:00
items[itemsHash[parentID]].reportChildren.attachments.push(item.toJSON({ mode: 'full' }));
2007-10-23 07:11:59 +00:00
}
}
}
// If not combining children, add a parent/child pair
// for each matching child
else {
for (let id of searchChildIDs) {
var item = yield Zotero.Items.getAsync(id);
var parentID = item.parentID;
2007-10-23 07:11:59 +00:00
var parentItem = Zotero.Items.get(parentID);
if (!itemsHash[parentID]) {
// If parent is a search match and not yet added,
// add on its own
if (searchItemIDs.has(parentID)) {
2007-10-23 07:11:59 +00:00
itemsHash[parentID] = [items.length];
Deasyncification :back: :cry: While trying to get translation and citing working with asynchronously generated data, we realized that drag-and-drop support was going to be...problematic. Firefox only supports synchronous methods for providing drag data (unlike, it seems, the DataTransferItem interface supported by Chrome), which means that we'd need to preload all relevant data on item selection (bounded by export.quickCopy.dragLimit) and keep the translate/cite methods synchronous (or maintain two separate versions). What we're trying instead is doing what I said in #518 we weren't going to do: loading most object data on startup and leaving many more functions synchronous. Essentially, this takes the various load*() methods described in #518, moves them to startup, and makes them operate on entire libraries rather than individual objects. The obvious downside here (other than undoing much of the work of the last many months) is that it increases startup time, potentially quite a lot for larger libraries. On my laptop, with a 3,000-item library, this adds about 3 seconds to startup time. I haven't yet tested with larger libraries. But I'm hoping that we can optimize this further to reduce that delay. Among other things, this is loading data for all libraries, when it should be able to load data only for the library being viewed. But this is also fundamentally just doing some SELECT queries and storing the results, so it really shouldn't need to be that slow (though performance may be bounded a bit here by XPCOM overhead). If we can make this fast enough, it means that third-party plugins should be able to remain much closer to their current designs. (Some things, including saving, will still need to be made asynchronous.)
2016-03-07 21:05:51 +00:00
items.push(parentItem.toJSON({ mode: 'full' }));
2007-10-23 07:11:59 +00:00
items[items.length - 1].reportSearchMatch = true;
}
else {
itemsHash[parentID] = [];
}
}
// Now add parent and child
itemsHash[parentID].push(items.length);
items.push(parentItem.toJSON({ mode: 'full' }));
2007-10-23 07:11:59 +00:00
if (item.isNote()) {
items[items.length - 1].reportChildren = {
Deasyncification :back: :cry: While trying to get translation and citing working with asynchronously generated data, we realized that drag-and-drop support was going to be...problematic. Firefox only supports synchronous methods for providing drag data (unlike, it seems, the DataTransferItem interface supported by Chrome), which means that we'd need to preload all relevant data on item selection (bounded by export.quickCopy.dragLimit) and keep the translate/cite methods synchronous (or maintain two separate versions). What we're trying instead is doing what I said in #518 we weren't going to do: loading most object data on startup and leaving many more functions synchronous. Essentially, this takes the various load*() methods described in #518, moves them to startup, and makes them operate on entire libraries rather than individual objects. The obvious downside here (other than undoing much of the work of the last many months) is that it increases startup time, potentially quite a lot for larger libraries. On my laptop, with a 3,000-item library, this adds about 3 seconds to startup time. I haven't yet tested with larger libraries. But I'm hoping that we can optimize this further to reduce that delay. Among other things, this is loading data for all libraries, when it should be able to load data only for the library being viewed. But this is also fundamentally just doing some SELECT queries and storing the results, so it really shouldn't need to be that slow (though performance may be bounded a bit here by XPCOM overhead). If we can make this fast enough, it means that third-party plugins should be able to remain much closer to their current designs. (Some things, including saving, will still need to be made asynchronous.)
2016-03-07 21:05:51 +00:00
notes: [item.toJSON({ mode: 'full' })],
2007-10-23 07:11:59 +00:00
attachments: []
};
}
else if (item.isAttachment()) {
items[items.length - 1].reportChildren = {
notes: [],
Deasyncification :back: :cry: While trying to get translation and citing working with asynchronously generated data, we realized that drag-and-drop support was going to be...problematic. Firefox only supports synchronous methods for providing drag data (unlike, it seems, the DataTransferItem interface supported by Chrome), which means that we'd need to preload all relevant data on item selection (bounded by export.quickCopy.dragLimit) and keep the translate/cite methods synchronous (or maintain two separate versions). What we're trying instead is doing what I said in #518 we weren't going to do: loading most object data on startup and leaving many more functions synchronous. Essentially, this takes the various load*() methods described in #518, moves them to startup, and makes them operate on entire libraries rather than individual objects. The obvious downside here (other than undoing much of the work of the last many months) is that it increases startup time, potentially quite a lot for larger libraries. On my laptop, with a 3,000-item library, this adds about 3 seconds to startup time. I haven't yet tested with larger libraries. But I'm hoping that we can optimize this further to reduce that delay. Among other things, this is loading data for all libraries, when it should be able to load data only for the library being viewed. But this is also fundamentally just doing some SELECT queries and storing the results, so it really shouldn't need to be that slow (though performance may be bounded a bit here by XPCOM overhead). If we can make this fast enough, it means that third-party plugins should be able to remain much closer to their current designs. (Some things, including saving, will still need to be made asynchronous.)
2016-03-07 21:05:51 +00:00
attachments: [item.toJSON({ mode: 'full' })]
2007-10-23 07:11:59 +00:00
};
}
}
}
// Sort items
// TODO: restore multiple sort fields
var sorts = [{
field: params.sort,
order: params.direction != 'desc' ? 1 : -1
}];
2007-10-23 07:11:59 +00:00
var collation = Zotero.getLocaleCollation();
var compareFunction = function(a, b) {
var index = 0;
// Multidimensional sort
do {
// In combineChildItems, use note or attachment as item
if (!combineChildItems) {
if (a.reportChildren) {
if (a.reportChildren.notes.length) {
a = a.reportChildren.notes[0];
}
else {
a = a.reportChildren.attachments[0];
}
}
if (b.reportChildren) {
if (b.reportChildren.notes.length) {
b = b.reportChildren.notes[0];
}
else {
b = b.reportChildren.attachments[0];
}
}
}
var valA, valB;
if (sorts[index].field == 'title') {
// For notes, use content for 'title'
if (a.itemType == 'note') {
valA = a.note;
}
else {
valA = a.title;
}
if (b.itemType == 'note') {
valB = b.note;
}
else {
valB = b.title;
}
valA = Zotero.Items.getSortTitle(valA);
valB = Zotero.Items.getSortTitle(valB);
}
2011-04-12 14:47:44 +00:00
else if (sorts[index].field == 'date') {
var itemA = Zotero.Items.getByLibraryAndKey(params.libraryID, a.key);
var itemB = Zotero.Items.getByLibraryAndKey(params.libraryID, b.key);
valA = itemA.getField('date', true, true);
valB = itemB.getField('date', true, true);
2011-04-12 14:47:44 +00:00
}
// TEMP: This is an ugly hack to make creator sorting
// slightly less broken. To do this right, real creator
// sorting needs to be abstracted from itemTreeView.js.
else if (sorts[index].field == 'firstCreator') {
var itemA = Zotero.Items.getByLibraryAndKey(params.libraryID, a.key);
var itemB = Zotero.Items.getByLibraryAndKey(params.libraryID, b.key);
valA = itemA.getField('firstCreator');
valB = itemB.getField('firstCreator');
}
else {
2011-04-12 14:47:44 +00:00
valA = a[sorts[index].field];
valB = b[sorts[index].field];
}
2007-10-23 07:11:59 +00:00
// Put empty values last
if (!valA && valB) {
var cmp = 1;
}
else if (valA && !valB) {
var cmp = -1;
}
else {
var cmp = collation.compareString(0, valA, valB);
2007-10-23 07:11:59 +00:00
}
var result = 0;
if (cmp != 0) {
result = cmp * sorts[index].order;
}
2007-10-23 07:11:59 +00:00
index++;
}
while (result == 0 && sorts[index]);
return result;
};
items.sort(compareFunction);
for (var i in items) {
if (items[i].reportChildren) {
items[i].reportChildren.notes.sort(compareFunction);
items[i].reportChildren.attachments.sort(compareFunction);
}
}
// Pass off to the appropriate handler
switch (params.format) {
2007-10-23 07:11:59 +00:00
case 'rtf':
this.contentType = 'text/rtf';
return '';
2007-10-23 07:11:59 +00:00
case 'csv':
this.contentType = 'text/plain';
return '';
2007-10-23 07:11:59 +00:00
default:
this.contentType = 'text/html';
return Zotero.Utilities.Internal.getAsyncInputStream(
Zotero.Report.HTML.listGenerator(items, combineChildItems),
function () {
Zotero.logError(e);
return '<span style="color: red; font-weight: bold">Error generating report</span>';
}
);
2007-10-23 07:11:59 +00:00
}
});
2007-10-23 07:11:59 +00:00
}
};
/**
* Generate MIT SIMILE Timeline
*
* Query string key abbreviations: intervals = i
* dateType = t
* timelineDate = d
*
* interval abbreviations: day = d | month = m | year = y | decade = e | century = c | millennium = i
* dateType abbreviations: date = d | dateAdded = da | dateModified = dm
* timelineDate format: shortMonthName.day.year (year is positive for A.D. and negative for B.C.)
*
* Defaults: intervals = month, year, decade
* dateType = date
* timelineDate = today's date
*/
var TimelineExtension = {
loadAsChrome: true,
newChannel: function (uri) {
return new AsyncChannel(uri, function* () {
var userLibraryID = Zotero.Libraries.userLibraryID;
var path = uri.spec.match(/zotero:\/\/[^/]+(.*)/)[1];
if (!path) {
this.contentType = 'text/html';
return 'Invalid URL';
}
2007-10-23 07:11:59 +00:00
var params = {};
var router = new Zotero.Router(params);
// HTML
router.add('library/:scopeObject/:scopeObjectKey', function () {
params.libraryID = userLibraryID;
params.controller = 'html';
});
router.add('groups/:groupID/:scopeObject/:scopeObjectKey', function () {
params.controller = 'html';
});
router.add('library', function () {
params.libraryID = userLibraryID;
params.controller = 'html';
});
router.add('groups/:groupID', function () {
params.controller = 'html';
});
// Data
router.add('data/library/:scopeObject/:scopeObjectKey', function () {
params.libraryID = userLibraryID;
params.controller = 'data';
});
router.add('data/groups/:groupID/:scopeObject/:scopeObjectKey', function () {
params.controller = 'data';
});
router.add('data/library', function () {
params.libraryID = userLibraryID;
params.controller = 'data';
});
router.add('data/groups/:groupID', function () {
params.controller = 'data';
});
// Old-style HTML URLs
router.add('collection/:id', function () {
params.controller = 'html';
params.scopeObject = 'collections';
var lkh = Zotero.Collections.parseLibraryKeyHash(params.id);
if (lkh) {
params.libraryID = lkh.libraryID || userLibraryID;
params.scopeObjectKey = lkh.key;
2007-10-23 07:11:59 +00:00
}
else {
params.scopeObjectID = params.id;
}
delete params.id;
});
router.add('search/:id', function () {
params.controller = 'html';
params.scopeObject = 'searches';
var lkh = Zotero.Searches.parseLibraryKeyHash(params.id);
if (lkh) {
params.libraryID = lkh.libraryID || userLibraryID;
params.scopeObjectKey = lkh.key;
}
else {
params.scopeObjectID = params.id;
}
delete params.id;
});
router.add('/', function () {
params.controller = 'html';
params.libraryID = userLibraryID;
});
2007-10-23 07:11:59 +00:00
var parsed = router.run(path);
if (!parsed) {
this.contentType = 'text/html';
return "URL could not be parsed";
}
if (params.groupID) {
params.libraryID = Zotero.Groups.getLibraryIDFromGroupID(params.groupID);
}
var intervals = params.i ? params.i : '';
var timelineDate = params.d ? params.d : '';
var dateType = params.t ? params.t : '';
// Get the collection or search object
var collection, search;
switch (params.scopeObject) {
case 'collections':
if (params.scopeObjectKey) {
collection = yield Zotero.Collections.getByLibraryAndKeyAsync(
params.libraryID, params.scopeObjectKey
);
}
else {
collection = yield Zotero.Collections.getAsync(params.scopeObjectID);
}
if (!collection) {
this.contentType = 'text/html';
return 'Invalid collection ID or key';
}
break;
case 'searches':
if (params.scopeObjectKey) {
var s = yield Zotero.Searches.getByLibraryAndKeyAsync(
params.libraryID, params.scopeObjectKey
);
}
else {
var s = yield Zotero.Searches.getAsync(params.scopeObjectID);
}
if (!s) {
return 'Invalid search ID or key';
}
// FIXME: Hack to exclude group libraries for now
var search = new Zotero.Search();
search.setScope(s);
var groups = Zotero.Groups.getAll();
for (let group of groups) {
search.addCondition('libraryID', 'isNot', group.libraryID);
}
break;
}
2007-10-23 07:11:59 +00:00
//
// Create XML file
//
if (params.controller == 'data') {
switch (params.scopeObject) {
case 'collections':
var results = collection.getChildItems();
2007-10-23 07:11:59 +00:00
break;
case 'searches':
var ids = yield search.search();
var results = yield Zotero.Items.getAsync(ids);
2007-10-23 07:11:59 +00:00
break;
2007-10-23 07:11:59 +00:00
default:
if (params.scopeObject) {
return "Invalid scope object '" + params.scopeObject + "'";
}
let s = new Zotero.Search();
s.addCondition('libraryID', 'is', params.libraryID);
s.addCondition('noChildren', 'true');
var ids = yield s.search();
var results = yield Zotero.Items.getAsync(ids);
2007-10-23 07:11:59 +00:00
}
2007-10-23 07:11:59 +00:00
var items = [];
// Only include parent items
for (let i=0; i<results.length; i++) {
if (!results[i].parentItemID) {
2007-10-23 07:11:59 +00:00
items.push(results[i]);
}
}
var dateTypes = {
d: 'date',
da: 'dateAdded',
dm: 'dateModified'
};
2007-10-23 07:11:59 +00:00
//default dateType = date
if (!dateType || !dateTypes[dateType]) {
2007-10-23 07:11:59 +00:00
dateType = 'd';
}
this.contentType = 'application/xml';
return Zotero.Utilities.Internal.getAsyncInputStream(
Zotero.Timeline.generateXMLDetails(items, dateTypes[dateType])
);
2007-10-23 07:11:59 +00:00
}
//
// Generate main HTML page
//
var content = Zotero.File.getContentsFromURL('chrome://zotero/skin/timeline/timeline.html');
this.contentType = 'text/html';
if(!timelineDate){
timelineDate=Date();
var dateParts=timelineDate.toString().split(' ');
timelineDate=dateParts[1]+'.'+dateParts[2]+'.'+dateParts[3];
}
if (!intervals || intervals.length < 3) {
intervals += "mye".substr(intervals.length);
}
var theIntervals = {
d: 'Timeline.DateTime.DAY',
m: 'Timeline.DateTime.MONTH',
y: 'Timeline.DateTime.YEAR',
e: 'Timeline.DateTime.DECADE',
c: 'Timeline.DateTime.CENTURY',
i: 'Timeline.DateTime.MILLENNIUM'
};
//sets the intervals of the timeline bands
var tempStr = '<body onload="onLoad(';
var a = (theIntervals[intervals[0]]) ? theIntervals[intervals[0]] : 'Timeline.DateTime.MONTH';
var b = (theIntervals[intervals[1]]) ? theIntervals[intervals[1]] : 'Timeline.DateTime.YEAR';
var c = (theIntervals[intervals[2]]) ? theIntervals[intervals[2]] : 'Timeline.DateTime.DECADE';
content = content.replace(tempStr, tempStr + a + ',' + b + ',' + c + ',\'' + timelineDate + '\'');
tempStr = 'document.write("<title>';
if (params.scopeObject == 'collections') {
content = content.replace(tempStr, tempStr + collection.name + ' - ');
}
else if (params.scopeObject == 'searches') {
content = content.replace(tempStr, tempStr + search.name + ' - ');
}
else {
content = content.replace(tempStr, tempStr + Zotero.getString('pane.collections.library') + ' - ');
}
tempStr = 'Timeline.loadXML("zotero://timeline/data/';
var d = '';
if (params.groupID) {
d += 'groups/' + params.groupID + '/';
}
else {
d += 'library/';
}
if (params.scopeObject) {
d += params.scopeObject + "/" + params.scopeObjectKey;
}
if (dateType) {
d += '?t=' + dateType;
}
return content.replace(tempStr, tempStr + d);
});
2007-10-23 07:11:59 +00:00
}
};
/**
2020-06-11 03:55:51 +00:00
* Select an item
*
* zotero://select/library/items/[itemKey]
* zotero://select/groups/[groupID]/items/[itemKey]
*
* Deprecated:
*
* zotero://select/[type]/0_ABCD1234
* zotero://select/[type]/1234 (not consistent across synced machines)
*/
var SelectExtension = {
noContent: true,
doAction: Zotero.Promise.coroutine(function* (uri) {
var userLibraryID = Zotero.Libraries.userLibraryID;
var path = uri.pathQueryRef;
if (!path) {
return 'Invalid URL';
}
path = path.substr('//select/'.length);
var mimeType, content = '';
var params = {
objectType: 'item'
};
var router = new Zotero.Router(params);
// Item within a collection or search
router.add('library/:scopeObject/:scopeObjectKey/items/:objectKey', function () {
params.libraryID = userLibraryID;
});
router.add('groups/:groupID/:scopeObject/:scopeObjectKey/items/:objectKey');
// All items
router.add('library/items/:objectKey', function () {
params.libraryID = userLibraryID;
});
router.add('groups/:groupID/items/:objectKey');
// Old-style URLs
router.add('items/:id', function () {
var lkh = Zotero.Items.parseLibraryKeyHash(params.id);
if (lkh) {
params.libraryID = lkh.libraryID || userLibraryID;
params.objectKey = lkh.key;
}
else {
params.objectID = params.id;
}
delete params.id;
});
// Collection
router.add('library/collections/:objectKey', function () {
params.objectType = 'collection'
params.libraryID = userLibraryID;
});
router.add('groups/:groupID/collections/:objectKey', function () {
params.objectType = 'collection'
2019-08-02 02:11:03 +00:00
});
// Search
router.add('library/searches/:objectKey', function () {
params.objectType = 'search'
params.libraryID = userLibraryID;
});
2019-08-02 02:11:03 +00:00
router.add('groups/:groupID/searches/:objectKey', function () {
params.objectType = 'search'
});
router.run(path);
Zotero.API.parseParams(params);
if (!params.objectKey && !params.objectID && !params.itemKey) {
Zotero.debug("No objects specified");
return;
}
var results = yield Zotero.API.getResultsFromParams(params);
if (!results.length) {
var msg = "Objects not found";
Zotero.debug(msg, 2);
Components.utils.reportError(msg);
return;
}
var zp = Zotero.getActiveZoteroPane();
if (!zp) {
// TEMP
throw new Error("Pane not open");
}
if (params.objectType == 'collection') {
return zp.collectionsView.selectCollection(results[0].id);
}
2019-08-02 02:11:03 +00:00
else if (params.objectType == 'search') {
return zp.collectionsView.selectSearch(results[0].id);
}
else {
// Select collection first if specified
if (params.scopeObject == 'collections') {
let col;
if (params.scopeObjectKey) {
col = Zotero.Collections.getByLibraryAndKey(
params.libraryID, params.scopeObjectKey
);
}
else {
col = Zotero.Collections.get(params.scopeObjectID);
}
yield zp.collectionsView.selectCollection(col.id);
}
else if (params.scopeObject == 'searches') {
let s;
if (params.scopeObjectKey) {
s = Zotero.Searches.getByLibraryAndKey(
params.libraryID, params.scopeObjectKey
);
}
else {
s = Zotero.Searches.get(params.scopeObjectID);
}
yield zp.collectionsView.selectSearch(s.id);
}
// If collection not specified, select library root
else {
yield zp.collectionsView.selectLibrary(params.libraryID);
}
return zp.selectItems(results.map(x => x.id));
}
}),
newChannel: function (uri) {
this.doAction(uri);
2007-10-23 07:11:59 +00:00
}
};
/*
zotero://debug/
*/
var DebugExtension = {
loadAsChrome: false,
2016-01-04 20:48:56 +00:00
newChannel: function (uri) {
return new AsyncChannel(uri, function* () {
2016-01-04 20:48:56 +00:00
this.contentType = "text/plain";
try {
return Zotero.Debug.get();
}
catch (e) {
Zotero.debug(e, 1);
throw e;
}
});
}
};
2010-09-23 04:14:19 +00:00
var ConnectorChannel = function(uri, data) {
var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
.getService(Components.interfaces.nsIScriptSecurityManager);
this.name = uri;
this.URI = ios.newURI(uri, "UTF-8", null);
2012-11-11 22:14:05 +00:00
this.owner = (secMan.getCodebasePrincipal || secMan.getSimpleCodebasePrincipal)(this.URI);
2010-09-23 04:14:19 +00:00
this._isPending = true;
var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
this._stream = converter.convertToInputStream(data);
this.contentLength = this._stream.available();
}
ConnectorChannel.prototype.contentCharset = "UTF-8";
ConnectorChannel.prototype.contentType = "text/html";
ConnectorChannel.prototype.notificationCallbacks = null;
2010-09-23 04:14:19 +00:00
ConnectorChannel.prototype.securityInfo = null;
ConnectorChannel.prototype.status = 0;
ConnectorChannel.prototype.loadGroup = null;
ConnectorChannel.prototype.loadFlags = 393216;
ConnectorChannel.prototype.__defineGetter__("originalURI", function() { return this.URI });
ConnectorChannel.prototype.__defineSetter__("originalURI", function() { });
ConnectorChannel.prototype.asyncOpen = function(streamListener, context) {
if(this.loadGroup) this.loadGroup.addRequest(this, null);
2010-09-23 04:14:19 +00:00
streamListener.onStartRequest(this, context);
streamListener.onDataAvailable(this, context, this._stream, 0, this.contentLength);
streamListener.onStopRequest(this, context, this.status);
this._isPending = false;
if(this.loadGroup) this.loadGroup.removeRequest(this, null, 0);
2010-09-23 04:14:19 +00:00
}
ConnectorChannel.prototype.isPending = function() {
return this._isPending;
}
ConnectorChannel.prototype.cancel = function(status) {
this.status = status;
this._isPending = false;
if(this._stream) this._stream.close();
}
ConnectorChannel.prototype.suspend = function() {}
2010-09-23 04:14:19 +00:00
ConnectorChannel.prototype.resume = function() {}
2010-09-23 04:14:19 +00:00
ConnectorChannel.prototype.open = function() {
return this._stream;
}
ConnectorChannel.prototype.QueryInterface = function(iid) {
if (!iid.equals(Components.interfaces.nsIChannel) && !iid.equals(Components.interfaces.nsIRequest) &&
!iid.equals(Components.interfaces.nsISupports)) {
throw Components.results.NS_ERROR_NO_INTERFACE;
}
return this;
}
/**
* zotero://connector/
*
* URI spoofing for transferring page data across boundaries
*/
var ConnectorExtension = new function() {
this.loadAsChrome = false;
this.newChannel = function(uri) {
var secMan = Components.classes["@mozilla.org/scriptsecuritymanager;1"]
.getService(Components.interfaces.nsIScriptSecurityManager);
var Zotero = Components.classes["@zotero.org/Zotero;1"]
.getService(Components.interfaces.nsISupports)
.wrappedJSObject;
try {
var originalURI = uri.pathQueryRef.substr('zotero://connector/'.length);
originalURI = decodeURIComponent(originalURI);
if(!Zotero.Server.Connector.Data[originalURI]) {
2010-09-23 04:14:19 +00:00
return null;
} else {
return new ConnectorChannel(originalURI, Zotero.Server.Connector.Data[originalURI]);
2010-09-23 04:14:19 +00:00
}
} catch(e) {
Zotero.debug(e);
throw e;
}
}
};
/*
2019-03-28 18:52:22 +00:00
zotero://pdf.js/viewer.html
zotero://pdf.js/pdf/1/ABCD5678
*/
var PDFJSExtension = {
2019-03-28 18:52:22 +00:00
loadAsChrome: true,
newChannel: function (uri) {
return new AsyncChannel(uri, function* () {
try {
uri = uri.spec;
// Proxy PDF.js files
2019-03-28 18:52:22 +00:00
if (uri.startsWith('zotero://pdf.js/') && !uri.startsWith('zotero://pdf.js/pdf/')) {
uri = uri.replace(/zotero:\/\/pdf.js\//, 'resource://zotero/pdf.js/');
let newURI = Services.io.newURI(uri, null, null);
return this.getURIInputStream(newURI);
}
// Proxy attachment PDFs
var pdfPrefix = 'zotero://pdf.js/pdf/';
if (!uri.startsWith(pdfPrefix)) {
return this._errorChannel("File not found");
}
var [libraryID, key] = uri.substr(pdfPrefix.length).split('/');
libraryID = parseInt(libraryID);
var item = yield Zotero.Items.getByLibraryAndKeyAsync(libraryID, key);
if (!item) {
2019-03-28 18:52:22 +00:00
return this._errorChannel("Item not found");
}
var path = yield item.getFilePathAsync();
if (!path) {
return this._errorChannel("File not found");
}
return this.getURIInputStream(OS.Path.toFileURI(path));
}
catch (e) {
Zotero.debug(e, 1);
throw e;
}
}.bind(this));
},
getURIInputStream: function (uri) {
return new Zotero.Promise((resolve, reject) => {
NetUtil.asyncFetch(uri, function (inputStream, result) {
if (!Components.isSuccessCode(result)) {
// TODO: Handle error
return;
}
resolve(inputStream);
});
});
},
_errorChannel: function (msg) {
this.status = Components.results.NS_ERROR_FAILURE;
this.contentType = 'text/plain';
return msg;
}
};
/**
* Open a PDF at a given page (or try to)
*
* zotero://open-pdf/library/items/[itemKey]?page=[page]
* zotero://open-pdf/groups/[groupID]/items/[itemKey]?page=[page]
*
* Also supports ZotFile format:
* zotero://open-pdf/[libraryID]_[key]/[page]
*/
var OpenPDFExtension = {
noContent: true,
doAction: async function (uri) {
var userLibraryID = Zotero.Libraries.userLibraryID;
var uriPath = uri.pathQueryRef;
if (!uriPath) {
return 'Invalid URL';
}
uriPath = uriPath.substr('//open-pdf/'.length);
var mimeType, content = '';
var params = {
objectType: 'item'
};
var router = new Zotero.Router(params);
// All items
router.add('library/items/:objectKey', function () {
params.libraryID = userLibraryID;
});
router.add('groups/:groupID/items/:objectKey');
// ZotFile URLs
router.add(':id/:page', function () {
var lkh = Zotero.Items.parseLibraryKeyHash(params.id);
if (!lkh) {
Zotero.warn(`Invalid URL ${url}`);
return;
}
params.libraryID = lkh.libraryID || userLibraryID;
params.objectKey = lkh.key;
delete params.id;
});
router.run(uriPath);
Zotero.API.parseParams(params);
var results = await Zotero.API.getResultsFromParams(params);
var page = params.page;
if (parseInt(page) != page) {
page = null;
}
if (!results.length) {
Zotero.warn(`No item found for ${uriPath}`);
return;
}
var item = results[0];
if (!item.isFileAttachment()) {
Zotero.warn(`Item for ${uriPath} is not a file attachment`);
return;
}
var path = await item.getFilePathAsync();
if (!path) {
Zotero.warn(`${path} not found`);
return;
}
if (!path.toLowerCase().endsWith('.pdf')
&& Zotero.MIME.sniffForMIMEType(await Zotero.File.getSample(path)) != 'application/pdf') {
Zotero.warn(`${path} is not a PDF`);
return;
}
var opened = false;
if (page) {
try {
opened = await Zotero.OpenPDF.openToPage(path, page);
}
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);
},
newChannel: function (uri) {
this.doAction(uri);
}
};
this._extensions[ZOTERO_SCHEME + "://attachment"] = AttachmentExtension;
this._extensions[ZOTERO_SCHEME + "://data"] = DataExtension;
this._extensions[ZOTERO_SCHEME + "://report"] = ReportExtension;
this._extensions[ZOTERO_SCHEME + "://timeline"] = TimelineExtension;
this._extensions[ZOTERO_SCHEME + "://select"] = SelectExtension;
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;
2007-10-23 07:11:59 +00:00
}
/*
* Implements nsIProtocolHandler
*/
ZoteroProtocolHandler.prototype = {
2007-10-23 07:11:59 +00:00
scheme: ZOTERO_SCHEME,
defaultPort : -1,
protocolFlags :
Components.interfaces.nsIProtocolHandler.URI_NORELATIVE |
Components.interfaces.nsIProtocolHandler.URI_NOAUTH |
2008-11-30 20:18:48 +00:00
// DEBUG: This should be URI_IS_LOCAL_FILE, and MUST be if any
// extensions that modify data are added
// - https://www.zotero.org/trac/ticket/1156
//
2011-03-05 04:10:29 +00:00
Components.interfaces.nsIProtocolHandler.URI_IS_LOCAL_FILE,
//Components.interfaces.nsIProtocolHandler.URI_LOADABLE_BY_ANYONE,
2007-10-23 07:11:59 +00:00
allowPort : function(port, scheme) {
return false;
},
getExtension: function (uri) {
let uriString = uri;
if (uri instanceof Components.interfaces.nsIURI) {
uriString = uri.spec;
}
uriString = uriString.toLowerCase();
for (let extSpec in this._extensions) {
if (uriString.startsWith(extSpec)) {
return this._extensions[extSpec];
}
}
return false;
},
newURI: function (spec, charset, baseURI) {
2019-03-28 18:52:22 +00:00
// A temporary workaround because baseURI.resolve(spec) just returns spec
if (baseURI) {
if (!spec.includes('://') && baseURI.spec.includes('/pdf.js/')) {
let parts = baseURI.spec.split('/');
parts.pop();
parts.push(spec);
spec = parts.join('/');
}
}
return Components.classes["@mozilla.org/network/simple-uri-mutator;1"]
.createInstance(Components.interfaces.nsIURIMutator)
.setSpec(spec)
.finalize();
2007-10-23 07:11:59 +00:00
},
newChannel : function(uri) {
var chromeService = Components.classes["@mozilla.org/network/protocol;1?name=chrome"]
.getService(Components.interfaces.nsIProtocolHandler);
var newChannel = null;
try {
let ext = this.getExtension(uri);
2007-10-23 07:11:59 +00:00
if (!ext) {
// Return cancelled channel for unknown paths
//
// These can be in the form zotero://example.com/... -- maybe for "//example.com" URLs?
var chromeURI = chromeService.newURI(DUMMY_CHROME_URL, null, null);
var extChannel = chromeService.newChannel(chromeURI);
var chromeRequest = extChannel.QueryInterface(Components.interfaces.nsIRequest);
chromeRequest.cancel(0x804b0002); // BINDING_ABORTED
return extChannel;
}
if (!this._principal && ext.loadAsChrome) {
this._principal = Services.scriptSecurityManager.getSystemPrincipal();
2007-10-23 07:11:59 +00:00
}
var extChannel = ext.newChannel(uri);
// Extension returned null, so cancel request
if (!extChannel) {
var chromeURI = chromeService.newURI(DUMMY_CHROME_URL, null, null);
var extChannel = chromeService.newChannel(chromeURI);
var chromeRequest = extChannel.QueryInterface(Components.interfaces.nsIRequest);
chromeRequest.cancel(0x804b0002); // BINDING_ABORTED
}
// Apply cached principal to extension channel
if (this._principal) {
extChannel.owner = this._principal;
}
if(!extChannel.originalURI) extChannel.originalURI = uri;
return extChannel;
2007-10-23 07:11:59 +00:00
}
catch (e) {
Components.utils.reportError(e);
Zotero.debug(e, 1);
2007-10-23 07:11:59 +00:00
throw Components.results.NS_ERROR_FAILURE;
}
return newChannel;
},
contractID: ZOTERO_PROTOCOL_CONTRACTID,
classDescription: ZOTERO_PROTOCOL_NAME,
classID: ZOTERO_PROTOCOL_CID,
QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsISupports,
Components.interfaces.nsIProtocolHandler])
2007-10-23 07:11:59 +00:00
};
/**
* nsIChannel implementation that takes a promise-yielding generator that returns a
* string, nsIAsyncInputStream, or file
*/
function AsyncChannel(uri, gen) {
this._generator = gen;
this._isPending = true;
// nsIRequest
this.name = uri;
this.loadFlags = 0;
this.loadGroup = null;
this.status = 0;
// nsIChannel
this.contentLength = -1;
this.contentType = "text/html";
this.contentCharset = "utf-8";
this.URI = uri;
this.originalURI = uri;
this.owner = null;
this.notificationCallbacks = null;
this.securityInfo = null;
}
AsyncChannel.prototype = {
asyncOpen: Zotero.Promise.coroutine(function* (streamListener, context) {
if (this.loadGroup) this.loadGroup.addRequest(this, null);
var channel = this;
var resolve;
var reject;
var promise = new Zotero.Promise(function () {
resolve = arguments[0];
reject = arguments[1];
});
var listenerWrapper = {
onStartRequest: function (request, context) {
//Zotero.debug("Starting request");
streamListener.onStartRequest(channel, context);
},
onDataAvailable: function (request, context, inputStream, offset, count) {
//Zotero.debug("onDataAvailable");
streamListener.onDataAvailable(channel, context, inputStream, offset, count);
},
onStopRequest: function (request, context, status) {
//Zotero.debug("Stopping request");
streamListener.onStopRequest(channel, context, status);
channel._isPending = false;
if (status == 0) {
resolve();
}
else {
reject(new Error("AsyncChannel request failed with status " + status));
}
}
};
//Zotero.debug("AsyncChannel's asyncOpen called");
var t = new Date;
var data;
try {
if (!data) {
data = yield Zotero.spawn(channel._generator, channel)
}
if (typeof data == 'string') {
//Zotero.debug("AsyncChannel: Got string from generator");
listenerWrapper.onStartRequest(this, context);
let converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
let inputStream = converter.convertToInputStream(data);
listenerWrapper.onDataAvailable(this, context, inputStream, 0, inputStream.available());
listenerWrapper.onStopRequest(this, context, this.status);
}
// If an async input stream is given, pass the data asynchronously to the stream listener
else if (data instanceof Ci.nsIAsyncInputStream) {
//Zotero.debug("AsyncChannel: Got input stream from generator");
var pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance(Ci.nsIInputStreamPump);
try {
pump.init(data, 0, 0, true);
}
catch (e) {
pump.init(data, -1, -1, 0, 0, true);
}
pump.asyncRead(listenerWrapper, context);
}
else if (data instanceof Ci.nsIFile || data instanceof Ci.nsIURI) {
if (data instanceof Ci.nsIFile) {
//Zotero.debug("AsyncChannel: Got file from generator");
data = ios.newFileURI(data);
}
else {
//Zotero.debug("AsyncChannel: Got URI from generator");
}
let uri = data;
uri.QueryInterface(Ci.nsIURL);
this.contentType = Zotero.MIME.getMIMETypeFromExtension(uri.fileExtension);
if (!this.contentType) {
let sample = yield Zotero.File.getSample(data);
this.contentType = Zotero.MIME.getMIMETypeFromData(sample);
}
Components.utils.import("resource://gre/modules/NetUtil.jsm");
NetUtil.asyncFetch({ uri: data, loadUsingSystemPrincipal: true }, function (inputStream, status) {
if (!Components.isSuccessCode(status)) {
reject();
return;
}
listenerWrapper.onStartRequest(channel, context);
try {
listenerWrapper.onDataAvailable(channel, context, inputStream, 0, inputStream.available());
}
catch (e) {
reject(e);
}
listenerWrapper.onStopRequest(channel, context, status);
});
}
else if (data === undefined) {
this.cancel(0x804b0002); // BINDING_ABORTED
}
else {
throw new Error("Invalid return type (" + typeof data + ") from generator passed to AsyncChannel");
}
if (this._isPending) {
//Zotero.debug("AsyncChannel request succeeded in " + (new Date - t) + " ms");
channel._isPending = false;
}
return promise;
} catch (e) {
Zotero.debug(e, 1);
if (channel._isPending) {
streamListener.onStopRequest(channel, context, Components.results.NS_ERROR_FAILURE);
channel._isPending = false;
}
throw e;
} finally {
if (channel.loadGroup) channel.loadGroup.removeRequest(channel, null, 0);
}
}),
// nsIRequest
isPending: function () {
return this._isPending;
},
cancel: function (status) {
Zotero.debug("Cancelling");
this.status = status;
this._isPending = false;
},
resume: function () {
Zotero.debug("Resuming");
},
suspend: function () {
Zotero.debug("Suspending");
},
// nsIWritablePropertyBag
setProperty: function (prop, val) {
this[prop] = val;
},
deleteProperty: function (prop) {
delete this[prop];
},
QueryInterface: function (iid) {
if (iid.equals(Components.interfaces.nsISupports)
|| iid.equals(Components.interfaces.nsIRequest)
|| iid.equals(Components.interfaces.nsIChannel)
// pdf.js wants this
|| iid.equals(Components.interfaces.nsIWritablePropertyBag)) {
return this;
}
throw Components.results.NS_ERROR_NO_INTERFACE;
}
};
var NSGetFactory = XPCOMUtils.generateNSGetFactory([ZoteroProtocolHandler]);