www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

commit 755ead21194f57d11f00e80754d501ac0158bfb6
parent c5ee3651fee4d0c4f4e618b12829a1beafbb9c76
Author: Dan Stillman <dstillman@zotero.org>
Date:   Mon,  8 Sep 2014 16:51:05 -0400

Update zotero:// extensions (report, timeline, etc.) for async DB, and more

- Protocol handler extensions can now handle promises and can also make
  data available as it's ready instead of all at once (e.g., reports now
  output one entry at a time)
- zotero:// URL syntaxes are now more consistent and closer to the web
  API (old URLs should work, but some may currently be broken)

Also:

- Code to generate server API, currently available for testing via
  zotero://data URLs but eventually moving to HTTP -- zotero://data URLs match
  web API URLs, with a different prefix for the personal library (/library vs.
  /users/12345)
- Miscellaneous fixes to data objects

Under the hood:

- Extensions now return an AsyncChannel, which is an nsIChannel implementation
  that takes a promise-yielding generator that returns a string,
  nsIAsyncInputStream, or file that will be used for the channel's data
- New function Zotero.Utilities.Internal.getAsyncInputStream() takes a
  generator that yields either promises or strings and returns an async input
  stream filled with the yielded strings
- Zotero.Router parsers URLs and extract parameters
- Zotero.Item.toResponseJSON()

Diffstat:
Mchrome/content/zotero/advancedSearch.js | 7++++++-
Mchrome/content/zotero/bindings/zoterosearch.xml | 5+++--
Mchrome/content/zotero/reportInterface.js | 64++++++++++++++++++++++++++--------------------------------------
Mchrome/content/zotero/timelineInterface.js | 28+++++++++++++---------------
Achrome/content/zotero/xpcom/api.js | 191+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/content/zotero/xpcom/data/collection.js | 3+--
Mchrome/content/zotero/xpcom/data/collections.js | 2+-
Mchrome/content/zotero/xpcom/data/dataObjectUtilities.js | 6++++++
Mchrome/content/zotero/xpcom/data/dataObjects.js | 28+++++++++++++++++++++++++---
Mchrome/content/zotero/xpcom/data/item.js | 64++++++++++++++++++++++++++++++++++++++++------------------------
Mchrome/content/zotero/xpcom/data/items.js | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mchrome/content/zotero/xpcom/data/tags.js | 2+-
Mchrome/content/zotero/xpcom/date.js | 23+++++++++++++++++++++++
Mchrome/content/zotero/xpcom/file.js | 4++--
Mchrome/content/zotero/xpcom/report.js | 277+++++++++++++++++++++++++++++++++++++++++--------------------------------------
Achrome/content/zotero/xpcom/router.js | 25+++++++++++++++++++++++++
Mchrome/content/zotero/xpcom/search.js | 77++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------
Mchrome/content/zotero/xpcom/timeline.js | 30+++++++++++++-----------------
Achrome/content/zotero/xpcom/users.js | 101+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/content/zotero/xpcom/utilities_internal.js | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/content/zotero/zoteroPane.js | 20++++++++++++--------
Mchrome/skin/default/zotero/timeline/timeline.html | 2+-
Mchrome/skin/default/zotero/timeline/timelineControls.js | 25+++++++++----------------
Mcomponents/zotero-protocol-handler.js | 1344+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Mcomponents/zotero-service.js | 2++
Aresource/pathparser.js | 120+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
26 files changed, 1731 insertions(+), 900 deletions(-)

diff --git a/chrome/content/zotero/advancedSearch.js b/chrome/content/zotero/advancedSearch.js @@ -44,7 +44,11 @@ var ZoteroAdvancedSearch = new function() { _searchBox.onLibraryChange = this.onLibraryChange; var io = window.arguments[0]; - _searchBox.search = io.dataIn.search; + + io.dataIn.search.loadPrimaryData() + .then(function () { + _searchBox.search = io.dataIn.search; + }); } @@ -62,6 +66,7 @@ var ZoteroAdvancedSearch = new function() { // Hack to create a condition for the search's library -- // this logic should really go in the search itself instead of here // and in collectionTreeView.js + yield search.loadPrimaryData(); var conditions = search.getSearchConditions(); if (!conditions.some(function (condition) condition.condition == 'libraryID')) { yield search.addCondition('libraryID', 'is', _searchBox.search.libraryID); diff --git a/chrome/content/zotero/bindings/zoterosearch.xml b/chrome/content/zotero/bindings/zoterosearch.xml @@ -176,8 +176,9 @@ if (this.onLibraryChange) { this.onLibraryChange(libraryID); } - - this.searchRef.libraryID = libraryID; + if (!this.searchRef.id) { + this.searchRef.libraryID = libraryID; + } ]]></body> </method> diff --git a/chrome/content/zotero/reportInterface.js b/chrome/content/zotero/reportInterface.js @@ -25,47 +25,45 @@ var Zotero_Report_Interface = new function() { - this.loadCollectionReport = loadCollectionReport; - this.loadItemReport = loadItemReport; - this.loadItemReportByIds = loadItemReportByIds; - - /* * Load a report for the currently selected collection */ - function loadCollectionReport(event) { - var queryString = ''; - - var col = ZoteroPane_Local.getSelectedCollection(); + this.loadCollectionReport = function (event) { var sortColumn = ZoteroPane_Local.getSortField(); var sortDirection = ZoteroPane_Local.getSortDirection(); - if (sortColumn != 'title' || sortDirection != 'ascending') { - queryString = '?sort=' + sortColumn + (sortDirection == 'ascending' ? '' : '/d'); - } + var queryString = '?sort=' + sortColumn + + '&direction=' + (sortDirection == 'ascending' ? 'asc' : 'desc'); - if (col) { - ZoteroPane_Local.loadURI('zotero://report/collection/' - + Zotero.Collections.getLibraryKeyHash(col) - + '/html/report.html' + queryString, event); - return; + var url = 'zotero://report/'; + + var source = ZoteroPane_Local.getSelectedCollection(); + if (!source) { + source = ZoteroPane_Local.getSelectedSavedSearch(); + } + if (!source) { + throw new Error('No collection currently selected'); } - var s = ZoteroPane_Local.getSelectedSavedSearch(); - if (s) { - ZoteroPane_Local.loadURI('zotero://report/search/' - + Zotero.Searches.getLibraryKeyHash(s) - + '/html/report.html' + queryString, event); - return; + url += Zotero.API.getLibraryPrefix(source.libraryID) + '/'; + + if (source instanceof Zotero.Collection) { + url += 'collections/' + source.key; + } + else { + url += 'searches/' + source.key; } - throw ('No collection currently selected'); + url += '/items/report.html' + queryString; + + ZoteroPane_Local.loadURI(url, event); } /* * Load a report for the currently selected items */ - function loadItemReport(event) { + this.loadItemReport = function (event) { + var libraryID = ZoteroPane_Local.getSelectedLibraryID(); var items = ZoteroPane_Local.getSelectedItems(); if (!items || !items.length) { @@ -77,18 +75,8 @@ var Zotero_Report_Interface = new function() { keyHashes.push(Zotero.Items.getLibraryKeyHash(item)); } - ZoteroPane_Local.loadURI('zotero://report/items/' + keyHashes.join('-') + '/html/report.html', event); - } - - - /* - * Load a report for the specified items - */ - function loadItemReportByIds(ids) { - if (!ids || !ids.length) { - throw ('No itemIDs provided to loadItemReportByIds()'); - } - - ZoteroPane_Local.loadURI('zotero://report/items/' + ids.join('-') + '/html/report.html'); + var url = 'zotero://report/' + Zotero.API.getLibraryPrefix(libraryID) + '/items/report.html' + + '?itemKey=' + items.map(item => item.key).join(','); + ZoteroPane_Local.loadURI(url, event); } } diff --git a/chrome/content/zotero/timelineInterface.js b/chrome/content/zotero/timelineInterface.js @@ -30,25 +30,23 @@ var Zotero_Timeline_Interface = new function() { */ this.loadTimeline = function () { var uri = 'zotero://timeline/'; - var col = ZoteroPane_Local.getSelectedCollection(); + var col = ZoteroPane_Local.getSelectedCollection(); if (col) { - ZoteroPane_Local.loadURI(uri + 'collection/' + Zotero.Collections.getLibraryKeyHash(col)); - return; - } - - var s = ZoteroPane_Local.getSelectedSavedSearch(); - if (s) { - ZoteroPane_Local.loadURI(uri + 'search/' + Zotero.Searches.getLibraryKeyHash(s)); - return; + uri += Zotero.API.getLibraryPrefix(col.libraryID) + '/collections/' + col.key; } - - var l = ZoteroPane_Local.getSelectedLibraryID(); - if (l) { - ZoteroPane_Local.loadURI(uri + 'library/' + l); - return; + else { + var s = ZoteroPane_Local.getSelectedSavedSearch(); + if (s) { + uri += Zotero.API.getLibraryPrefix(s.libraryID) + '/searches/' + s.key; + } + else { + let libraryID = ZoteroPane_Local.getSelectedLibraryID(); + if (libraryID) { + uri += Zotero.API.getLibraryPrefix(libraryID); + } + } } - ZoteroPane_Local.loadURI(uri); } } diff --git a/chrome/content/zotero/xpcom/api.js b/chrome/content/zotero/xpcom/api.js @@ -0,0 +1,191 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2014 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://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 ***** +*/ + +Zotero.API = { + parseParams: function (params) { + if (params.groupID) { + params.libraryID = Zotero.Groups.getLibraryIDFromGroupID(params.groupID); + } + + if (typeof params.itemKey == 'string') { + params.itemKey = params.itemKey.split(','); + } + }, + + + getResultsFromParams: Zotero.Promise.coroutine(function* (params) { + var results; + switch (params.scopeObject) { + case 'collections': + if (params.scopeObjectKey) { + var col = yield Zotero.Collections.getByLibraryAndKeyAsync( + params.libraryID, params.scopeObjectKey + ); + } + else { + var col = yield Zotero.Collections.getAsync(params.scopeObjectID); + } + if (!col) { + throw new Error('Invalid collection ID or key'); + } + yield col.loadChildItems(); + results = col.getChildItems(); + 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) { + throw new Error('Invalid search ID or key'); + } + + // FIXME: Hack to exclude group libraries for now + var s2 = new Zotero.Search(); + s2.setScope(s); + var groups = Zotero.Groups.getAll(); + for each(var group in groups) { + yield s2.addCondition('libraryID', 'isNot', group.libraryID); + } + var ids = yield s2.search(); + break; + + default: + if (params.scopeObject) { + throw new Error("Invalid scope object '" + params.scopeObject + "'"); + } + + if (params.itemKey) { + var s = new Zotero.Search; + yield s.addCondition('libraryID', 'is', params.libraryID); + yield s.addCondition('blockStart'); + for (let i=0; i<params.itemKey.length; i++) { + let itemKey = params.itemKey[i]; + yield s.addCondition('key', 'is', itemKey); + } + yield s.addCondition('blockEnd'); + var ids = yield s.search(); + } + else { + // Display all items + var s = new Zotero.Search(); + yield s.addCondition('libraryID', 'is', params.libraryID); + yield s.addCondition('noChildren', 'true'); + var ids = yield s.search(); + } + } + + if (results) { + // Filter results by item key + if (params.itemKey) { + results = results.filter(function (result) { + return params.itemKey.indexOf(result.key) !== -1; + }); + } + } + else if (ids) { + // Filter results by item key + if (params.itemKey) { + ids = ids.filter(function (id) { + var [libraryID, key] = Zotero.Items.getLibraryAndKeyFromID(id); + return params.itemKey.indexOf(key) !== -1; + }); + } + results = yield Zotero.Items.getAsync(ids); + } + + return results; + }), + + + getLibraryPrefix: function (libraryID) { + return libraryID + ? 'groups/' + Zotero.Groups.getGroupIDFromLibraryID(libraryID) + : 'library'; + } +}; + +Zotero.API.Data = { + /** + * Parse a relative URI path and return parameters for the request + */ + parsePath: function (path) { + var params = {}; + var router = new Zotero.Router(params); + + // Top-level objects + router.add('library/:controller/top', function () { + params.libraryID = 0; + params.subset = 'top'; + }); + router.add('groups/:groupID/:controller/top', function () { + params.subset = 'top'; + }); + + router.add('library/:scopeObject/:scopeObjectKey/items/:objectKey/:subset', function () { + params.libraryID = 0; + params.controller = 'items'; + }); + router.add('groups/:groupID/:scopeObject/:scopeObjectKey/items/:objectKey/:subset', function () { + params.controller = 'items'; + }); + + // All objects + router.add('library/:controller', function () { + params.libraryID = 0; + }); + router.add('groups/:groupID/:controller', function () {}); + + var parsed = router.run(path); + if (!parsed || !params.controller) { + throw new Zotero.Router.InvalidPathException(path); + } + + if (params.groupID) { + params.libraryID = Zotero.Groups.getLibraryIDFromGroupID(params.groupID); + } + Zotero.Router.Utilities.convertControllerToObjectType(params); + + return params; + }, + + + getGenerator: function (path) { + var params = this.parsePath(path); + //Zotero.debug(params); + + return Zotero.DataObjectUtilities.getClassForObjectType(params.objectType) + .apiDataGenerator(params); + } +}; + + + + diff --git a/chrome/content/zotero/xpcom/data/collection.js b/chrome/content/zotero/xpcom/data/collection.js @@ -121,8 +121,7 @@ Zotero.Collection.prototype.loadPrimaryData = Zotero.Promise.coroutine(function* var key = this._key; var libraryID = this._libraryID; - // Should be same as query in Zotero.Collections, just with collectionID - var sql = Zotero.Collections._getPrimaryDataSQL(); + var sql = Zotero.Collections.getPrimaryDataSQL(); if (id) { sql += " AND O.collectionID=?"; var params = id; diff --git a/chrome/content/zotero/xpcom/data/collections.js b/chrome/content/zotero/xpcom/data/collections.js @@ -204,7 +204,7 @@ Zotero.Collections = new function() { } - this._getPrimaryDataSQL = function () { + this.getPrimaryDataSQL = function () { // This should be the same as the query in Zotero.Collection.load(), // just without a specific collectionID return "SELECT " diff --git a/chrome/content/zotero/xpcom/data/dataObjectUtilities.js b/chrome/content/zotero/xpcom/data/dataObjectUtilities.js @@ -53,6 +53,12 @@ Zotero.DataObjectUtilities = { return key; }, + + getObjectTypeSingular: function (objectTypePlural) { + return objectTypePlural.replace(/(s|es)$/, ''); + }, + + "getObjectTypePlural": function getObjectTypePlural(objectType) { return objectType == 'search' ? 'searches' : objectType + 's'; }, diff --git a/chrome/content/zotero/xpcom/data/dataObjects.js b/chrome/content/zotero/xpcom/data/dataObjects.js @@ -186,17 +186,38 @@ Zotero.DataObjects = function (object, objectPlural, id, table) { }); + /** + * @deprecated - use .libraryKey + */ this.makeLibraryKeyHash = function (libraryID, key) { + Zotero.debug("WARNING: Zotero.DataObjects.makeLibraryKeyHash() is deprecated -- use obj.libraryKey instead"); return libraryID + '_' + key; } + /** + * @deprecated - use .libraryKey + */ this.getLibraryKeyHash = function (obj) { + Zotero.debug("WARNING: Zotero.DataObjects.getLibraryKeyHash() is deprecated -- use obj.libraryKey instead"); return this.makeLibraryKeyHash(obj.libraryID, obj.key); } + this.parseLibraryKey = function (libraryKey) { + var [libraryID, key] = libraryKey.split('/'); + return { + libraryID: parseInt(libraryID), + key: key + }; + } + + + /** + * @deprecated - Use Zotero.DataObjects.parseLibraryKey() + */ this.parseLibraryKeyHash = function (libraryKey) { + Zotero.debug("WARNING: Zotero.DataObjects.parseLibraryKeyHash() is deprecated -- use .parseLibraryKey() instead"); var [libraryID, key] = libraryKey.split('_'); if (!key) { return false; @@ -254,7 +275,8 @@ Zotero.DataObjects = function (object, objectPlural, id, table) { this.getIDFromLibraryAndKey = function (libraryID, key) { - return this._objectIDs[libraryID][key] ? this._objectIDs[libraryID][key] : false; + return (this._objectIDs[libraryID] && this._objectIDs[libraryID][key]) + ? this._objectIDs[libraryID][key] : false; } @@ -531,8 +553,8 @@ Zotero.DataObjects = function (object, objectPlural, id, table) { return loaded; } - // _getPrimaryDataSQL() should use "O" for the primary table alias - var sql = this._getPrimaryDataSQL(); + // getPrimaryDataSQL() should use "O" for the primary table alias + var sql = this.getPrimaryDataSQL(); var params = []; if (libraryID !== false) { sql += ' AND O.libraryID=?'; diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js @@ -293,7 +293,7 @@ Zotero.Item.prototype.loadPrimaryData = Zotero.Promise.coroutine(function* (relo if (!columns.length) { return; } - // This should match Zotero.Items._getPrimaryDataSQL(), but without + // This should match Zotero.Items.getPrimaryDataSQL(), but without // necessarily including all columns var sql = "SELECT " + columns.join(", ") + Zotero.Items.primaryDataSQLFrom; if (id) { @@ -923,9 +923,9 @@ Zotero.Item.prototype.getCreator = function (pos) { * @param {Integer} pos * @return {Object|Boolean} The API JSON creator data at the given position, or FALSE if none */ -Zotero.Item.prototype.getCreatorsJSON = function (pos) { +Zotero.Item.prototype.getCreatorJSON = function (pos) { this._requireData('creators'); - return this._creators[pos] ? Zotero.Creators.internalToAPIJSON(this._creators[pos]) : false; + return this._creators[pos] ? Zotero.Creators.internalToJSON(this._creators[pos]) : false; } @@ -954,7 +954,7 @@ Zotero.Item.prototype.getCreators = function () { */ Zotero.Item.prototype.getCreatorsAPIData = function () { this._requireData('creators'); - return this._creators.map(function (data) Zotero.Creators.internalToAPIJSON(data)); + return this._creators.map(function (data) Zotero.Creators.internalToJSON(data)); } @@ -2075,7 +2075,7 @@ Zotero.Item.prototype.numAttachments = function(includeTrashed) { /** * Get an nsILocalFile for the attachment, or false for invalid paths * - * This no longer checks whether a file exists + * Note: This no longer checks whether a file exists * * @return {nsILocalFile|false} An nsIFile, or false for invalid paths */ @@ -4150,12 +4150,8 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options, patc } var obj = {}; - if (options && options.includeKey) { - obj.itemKey = this.key; - } - if (options && options.includeVersion) { - obj.itemVersion = this.version; - } + obj.itemKey = this.key; + obj.itemVersion = this.version; obj.itemType = Zotero.ItemTypes.getName(this.itemTypeID); // Fields @@ -4170,7 +4166,7 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options, patc // Creators if (this.isRegularItem()) { yield this.loadCreators() - obj.creators = this.getCreators(); + obj.creators = this.getCreatorsAPIData(); } else { var parent = this.parentKey; @@ -4200,14 +4196,8 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options, patc obj.tags = []; yield this.loadTags() var tags = yield this.getTags(); - for each (let tag in tags) { - let tagObj = {}; - tagObj.tag = tag.name; - let type = tag.type; - if (type != 0 || mode == 'full') { - tagObj.type = tag.type; - } - obj.tags.push(tagObj); + for (let i=0; i<tags.length; i++) { + obj.tags.push(tags[i]); } // Collections @@ -4248,10 +4238,8 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options, patc obj.deleted = deleted; } - if (options && options.includeDate) { - obj.dateAdded = this.dateAdded; - obj.dateModified = this.dateModified; - } + obj.dateAdded = Zotero.Date.sqlToISO8601(this.dateAdded); + obj.dateModified = Zotero.Date.sqlToISO8601(this.dateModified); if (mode == 'patch') { for (let i in patchBase) { @@ -4277,6 +4265,34 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options, patc }); +Zotero.Item.prototype.toResponseJSON = Zotero.Promise.coroutine(function* (options, patchBase) { + var json = { + key: this.key, + version: this.version, + meta: {}, + data: yield this.toJSON(options, patchBase) + }; + + // TODO: library block? + + // creatorSummary + var firstCreator = this.getField('firstCreator'); + if (firstCreator) { + json.meta.creatorSummary = firstCreator; + } + // parsedDate + var parsedDate = Zotero.Date.multipartToSQL(this.getField('date', true, true)); + if (parsedDate) { + // 0000? + json.meta.parsedDate = parsedDate; + } + // numChildren + if (this.isRegularItem()) { + json.meta.numChildren = this.numChildren(); + } + return json; +}) + ////////////////////////////////////////////////////////////////////////////// // diff --git a/chrome/content/zotero/xpcom/data/items.js b/chrome/content/zotero/xpcom/data/items.js @@ -154,6 +154,67 @@ Zotero.Items = new function() { }); + /** + * Return item data in web API format + * + * var data = Zotero.Items.getAPIData(0, 'collections/NF3GJ38A/items'); + * + * @param {Number} libraryID + * @param {String} [apiPath='items'] - Web API style + * @return {Promise<String>}. + */ + this.getAPIData = Zotero.Promise.coroutine(function* (libraryID, apiPath) { + var gen = this.getAPIDataGenerator(...arguments); + var data = ""; + while (true) { + var result = gen.next(); + if (result.done) { + break; + } + var val = yield result.value; + if (typeof val == 'string') { + data += val; + } + else if (val === undefined) { + continue; + } + else { + throw new Error("Invalid return value from generator"); + } + } + return data; + }); + + + /** + * Zotero.Utilities.Internal.getAsyncInputStream-compatible generator that yields item data + * in web API format as strings + * + * @param {Object} params - Request parameters from Zotero.API.parsePath() + */ + this.apiDataGenerator = function* (params) { + Zotero.debug(params); + var s = new Zotero.Search; + yield s.addCondition('libraryID', 'is', params.libraryID); + if (params.scopeObject == 'collections') { + yield s.addCondition('collection', 'is', params.libraryID + '/' + params.scopeObjectKey); + } + yield s.addCondition('title', 'contains', 'test'); + var ids = yield s.search(); + + yield '[\n'; + + for (let i=0; i<ids.length; i++) { + let prefix = i > 0 ? ',\n' : ''; + let item = yield this.getAsync(ids[i], { noCache: true }); + var json = yield item.toResponseJSON(); + yield prefix + JSON.stringify(json, null, 4); + } + + yield '\n]'; + }; + + /* * Create a new item with optional metadata and pass back the primary reference * @@ -621,8 +682,7 @@ Zotero.Items = new function() { }); - this._getPrimaryDataSQL = function () { - // This should match Zotero.Item.loadPrimaryData, but with all possible columns + this.getPrimaryDataSQL = function () { return "SELECT " + Object.keys(this._primaryDataSQLParts).map((val) => this._primaryDataSQLParts[val]).join(', ') + this.primaryDataSQLFrom; diff --git a/chrome/content/zotero/xpcom/data/tags.js b/chrome/content/zotero/xpcom/data/tags.js @@ -845,7 +845,7 @@ Zotero.Tags = new function() { } - this._getPrimaryDataSQL = function () { + this.getPrimaryDataSQL = function () { // This should be the same as the query in Zotero.Tag.load(), // just without a specific tagID return "SELECT * FROM tags O WHERE 1"; diff --git a/chrome/content/zotero/xpcom/date.js b/chrome/content/zotero/xpcom/date.js @@ -544,6 +544,29 @@ Zotero.Date = new function(){ return false; } + + this.sqlToISO8601 = function (sqlDate) { + var date = sqlDate.substr(0, 10); + var matches = date.match(/^([0-9]{4})\-([0-9]{2})\-([0-9]{2})/); + if (!matches) { + return false; + } + date = matches[1]; + // Drop parts for reduced precision + if (matches[2] !== "00") { + date += "-" + matches[2]; + if (matches[3] !== "00") { + date += "-" + matches[3]; + } + } + var time = sqlDate.substr(11); + // TODO: validate times + if (time) { + date += "T" + time + "Z"; + } + return date; + } + function strToMultipart(str){ if (!str){ return ''; diff --git a/chrome/content/zotero/xpcom/file.js b/chrome/content/zotero/xpcom/file.js @@ -189,7 +189,7 @@ Zotero.File = new function(){ * @param {nsIURI|nsIFile|string spec|string path|nsIChannel|nsIInputStream} source The source to read * @param {String} [charset] The character set; defaults to UTF-8 * @param {Integer} [maxLength] Maximum length to fetch, in bytes - * @return {Promise} A Q promise that is resolved with the contents of the file + * @return {Promise} A promise that is resolved with the contents of the file */ this.getContentsAsync = function (source, charset, maxLength) { Zotero.debug("Getting contents of " + source); @@ -243,7 +243,7 @@ Zotero.File = new function(){ * * @param {nsIURI|nsIFile|string spec|nsIChannel|nsIInputStream} source The source to read * @param {Integer} [maxLength] Maximum length to fetch, in bytes (unimplemented) - * @return {Promise} A Q promise that is resolved with the contents of the source + * @return {Promise} A promise that is resolved with the contents of the source */ this.getBinaryContentsAsync = function (source, maxLength) { var deferred = Zotero.Promise.defer(); diff --git a/chrome/content/zotero/xpcom/report.js b/chrome/content/zotero/xpcom/report.js @@ -24,57 +24,54 @@ */ -Zotero.Report = new function() { - this.generateHTMLDetails = generateHTMLDetails; - this.generateHTMLList = generateHTMLList; - - var escapeXML = function (str) { - str = str.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\ud800-\udfff\ufffe\uffff]/g, '\u2B1A'); - return Zotero.Utilities.htmlSpecialChars(str); - } - - - function generateHTMLDetails(items, combineChildItems) { - var content = '<!DOCTYPE html>\n'; - content += '<html>\n'; - content += '<head><meta http-equiv="Content-Type" content="text/html; charset=utf-8" />\n'; - content += '<title>' + Zotero.getString('report.title.default') + '</title>\n'; - content += '<link rel="stylesheet" type="text/css" href="zotero://report/detail.css"/>\n'; - content += '<link rel="stylesheet" type="text/css" media="screen,projection" href="zotero://report/detail_screen.css"/>\n'; - content += '<link rel="stylesheet" type="text/css" media="print" href="zotero://report/detail_print.css"/>\n'; - content += '</head>\n\n<body>\n'; +Zotero.Report = {}; + +Zotero.Report.HTML = new function () { + this.listGenerator = function* (items, combineChildItems) { + yield '<!DOCTYPE html>\n' + + '<html>\n' + + ' <head>\n' + + ' <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />\n' + + ' <title>' + Zotero.getString('report.title.default') + '</title>\n' + + ' <link rel="stylesheet" type="text/css" href="zotero://report/detail.css"/>\n' + + ' <link rel="stylesheet" type="text/css" media="screen,projection" href="zotero://report/detail_screen.css"/>\n' + + ' <link rel="stylesheet" type="text/css" media="print" href="zotero://report/detail_print.css"/>\n' + + ' </head>\n' + + ' <body>\n' + + ' <ul class="report' + (combineChildItems ? ' combineChildItems' : '') + '">'; - content += '<ul class="report' + (combineChildItems ? ' combineChildItems' : '') + '">\n'; - for each(var arr in items) { - content += '\n<li id="i' + arr.itemID + '" class="item ' + arr.itemType + '">\n'; + for (let i=0; i<items.length; i++) { + let obj = items[i]; - if (arr.title) { + let content = '\n\t\t\t<li id="item_' + obj.itemKey + '" class="item ' + obj.itemType + '">\n'; + + if (obj.title) { // Top-level item matched search, so display title - if (arr.reportSearchMatch) { - content += '<h2>' + escapeXML(arr.title) + '</h2>\n'; + if (obj.reportSearchMatch) { + content += '\t\t\t<h2>' + escapeXML(obj.title) + '</h2>\n'; } // Non-matching parent, so display "Parent Item: [Title]" else { - content += '<h2 class="parentItem">' + escapeXML(Zotero.getString('report.parentItem')) - + ' <span class="title">' + escapeXML(arr.title) + '</span></h2>'; + content += '\t\t\t<h2 class="parentItem">' + escapeXML(Zotero.getString('report.parentItem')) + + ' <span class="title">' + escapeXML(obj.title) + '</span></h2>\n'; } } // If parent matches search, display parent item metadata table and tags - if (arr.reportSearchMatch) { - content += _generateMetadataTable(arr); + if (obj.reportSearchMatch) { + content += _generateMetadataTable(obj); - content += _generateTagsList(arr); + content += _generateTagsList(obj); // Independent note - if (arr['note']) { - content += '\n'; + if (obj['note']) { + content += '\n\t\t\t'; // If not valid XML, display notes with entities encoded var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"] .createInstance(Components.interfaces.nsIDOMParser); var doc = parser.parseFromString('<div>' - + arr.note + + obj.note // &nbsp; isn't valid in HTML .replace(/&nbsp;/g, "&#160;") // Strip control characters (for notes that were @@ -83,26 +80,26 @@ Zotero.Report = new function() { + '</div>', "application/xml"); if (doc.documentElement.tagName == 'parsererror') { Zotero.debug(doc.documentElement.textContent, 2); - content += '<p class="plaintext">' + escapeXML(arr.note) + '</p>\n'; + content += '<p class="plaintext">' + escapeXML(obj.note) + '</p>\n'; } // Otherwise render markup normally else { - content += arr.note + '\n'; + content += obj.note + '\n'; } } } // Children - if (arr.reportChildren) { + if (obj.reportChildren) { // Child notes - if (arr.reportChildren.notes.length) { + if (obj.reportChildren.notes.length) { // Only display "Notes:" header if parent matches search - if (arr.reportSearchMatch) { - content += '<h3 class="notes">' + escapeXML(Zotero.getString('report.notes')) + '</h3>\n'; + if (obj.reportSearchMatch) { + content += '\t\t\t\t<h3 class="notes">' + escapeXML(Zotero.getString('report.notes')) + '</h3>\n'; } - content += '<ul class="notes">\n'; - for each(var note in arr.reportChildren.notes) { - content += '<li id="i' + note.itemID + '">\n'; + content += '\t\t\t\t<ul class="notes">\n'; + for each(var note in obj.reportChildren.notes) { + content += '\t\t\t\t\t<li id="item_' + note.itemKey + '">\n'; // If not valid XML, display notes with entities encoded var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"] @@ -126,114 +123,117 @@ Zotero.Report = new function() { // Child note tags content += _generateTagsList(note); - content += '</li>\n'; + content += '\t\t\t\t\t</li>\n'; } - content += '</ul>\n'; + content += '\t\t\t\t</ul>\n'; } // Chid attachments - content += _generateAttachmentsList(arr.reportChildren); + content += _generateAttachmentsList(obj.reportChildren); } - // Related - if (arr.reportSearchMatch && arr.related && arr.related.length) { - content += '<h3 class="related">' + escapeXML(Zotero.getString('itemFields.related')) + '</h3>\n'; - content += '<ul class="related">\n'; - var relateds = Zotero.Items.get(arr.related); - for each(var related in relateds) { - content += '<li id="i' + related.getID() + '">'; - content += escapeXML(related.getDisplayTitle()); - content += '</li>\n'; + // Related items + if (obj.reportSearchMatch && Zotero.Relations.relatedItemPredicate in obj.relations) { + content += '\t\t\t\t<h3 class="related">' + escapeXML(Zotero.getString('itemFields.related')) + '</h3>\n'; + content += '\t\t\t\t<ul class="related">\n'; + var rels = obj.relations[Zotero.Relations.relatedItemPredicate]; + // TEMP + if (!Array.isArray(rels)) { + rels = [rels]; + } + for (let i=0; i<rels.length; i++) { + let rel = rels[i]; + let relItem = Zotero.URI.getURIItem(rel); + if (relItem) { + content += '\t\t\t\t\t<li id="item_' + relItem.key + '">'; + content += escapeXML(relItem.getDisplayTitle()); + content += '</li>\n'; + } } - content += '</ul>\n'; + content += '\t\t\t\t</ul>\n'; } - content += '</li>\n\n'; + content += '\t\t\t</li>\n\n'; + + yield content; } - content += '</ul>\n'; - content += '</body>\n</html>'; - - return content; - } - - - function generateHTMLList(items) { - } + yield '\t\t</ul>\n\t</body>\n</html>'; + }; - function _generateMetadataTable(arr) { + function _generateMetadataTable(obj) { var table = false; - var content = '<table>\n'; + var content = '\t\t\t\t<table>\n'; // Item type - content += '<tr>\n'; - content += '<th>' + content += '\t\t\t\t\t<tr>\n'; + content += '\t\t\t\t\t\t<th>' + escapeXML(Zotero.getString('itemFields.itemType')) + '</th>\n'; - content += '<td>' + escapeXML(Zotero.ItemTypes.getLocalizedString(arr.itemType)) + '</td>\n'; - content += '</tr>\n'; + content += '\t\t\t\t\t\t<td>' + escapeXML(Zotero.ItemTypes.getLocalizedString(obj.itemType)) + '</td>\n'; + content += '\t\t\t\t\t</tr>\n'; // Creators - if (arr['creators']) { + if (obj['creators']) { table = true; var displayText; - for each(var creator in arr['creators']) { - // Two fields - if (creator['fieldMode']==0) { - displayText = creator['firstName'] + ' ' + creator['lastName']; - } - // Single field - else if (creator['fieldMode']==1) { - displayText = creator['lastName']; + for each(var creator in obj['creators']) { + // One field + if (creator.name !== undefined) { + displayText = creator.name; } + // Two field else { - // TODO + displayText = (creator.firstName + ' ' + creator.lastName).trim(); } - content += '<tr>\n'; - content += '<th class="' + creator.creatorType + '">' + content += '\t\t\t\t\t<tr>\n'; + content += '\t\t\t\t\t\t<th class="' + creator.creatorType + '">' + escapeXML(Zotero.getString('creatorTypes.' + creator.creatorType)) + '</th>\n'; - content += '<td>' + escapeXML(displayText) + '</td>\n'; - content += '</tr>\n'; + content += '\t\t\t\t\t\t<td>' + escapeXML(displayText) + '</td>\n'; + content += '\t\t\t\t\t</tr>\n'; } } - // Move dateAdded and dateModified to the end of the array - var da = arr['dateAdded']; - var dm = arr['dateModified']; - delete arr['dateAdded']; - delete arr['dateModified']; - arr['dateAdded'] = da; - arr['dateModified'] = dm; + // Move dateAdded and dateModified to the end of the objay + var da = obj['dateAdded']; + var dm = obj['dateModified']; + delete obj['dateAdded']; + delete obj['dateModified']; + obj['dateAdded'] = da; + obj['dateModified'] = dm; - for (var i in arr) { + for (var i in obj) { // Skip certain fields switch (i) { case 'reportSearchMatch': case 'reportChildren': - case 'libraryID': - case 'key': + case 'itemKey': + case 'itemVersion': case 'itemType': - case 'itemID': - case 'parentItemID': case 'title': - case 'firstCreator': case 'creators': - case 'tags': - case 'related': - case 'notes': case 'note': - case 'attachments': + case 'collections': + case 'relations': + case 'tags': + case 'deleted': + case 'parentItem': + + case 'charset': + case 'contentType': + case 'linkMode': + case 'path': continue; } try { - var localizedFieldName = Zotero.ItemFields.getLocalizedString(arr.itemType, i); + var localizedFieldName = Zotero.ItemFields.getLocalizedString(obj.itemType, i); } // Skip fields we don't have a localized string for catch (e) { @@ -241,79 +241,82 @@ Zotero.Report = new function() { continue; } - arr[i] = Zotero.Utilities.trim(arr[i] + ''); + obj[i] = (obj[i] + '').trim(); // Skip empty fields - if (!arr[i]) { + if (!obj[i]) { continue; } table = true; var fieldText; - if (i == 'url' && arr[i].match(/^https?:\/\//)) { - fieldText = '<a href="' + escapeXML(arr[i]) + '">' - + escapeXML(arr[i]) + '</a>'; + if (i == 'url' && obj[i].match(/^https?:\/\//)) { + fieldText = '<a href="' + escapeXML(obj[i]) + '">' + escapeXML(obj[i]) + '</a>'; } // Hyperlink DOI else if (i == 'DOI') { - fieldText = '<a href="' + escapeXML('http://doi.org/' + arr[i]) + '">' - + escapeXML(arr[i]) + '</a>'; + fieldText = '<a href="' + escapeXML('http://doi.org/' + obj[i]) + '">' + + escapeXML(obj[i]) + '</a>'; } // Remove SQL date from multipart dates // (e.g. '2006-00-00 Summer 2006' becomes 'Summer 2006') else if (i=='date') { - fieldText = escapeXML(Zotero.Date.multipartToStr(arr[i])); + fieldText = escapeXML(Zotero.Date.multipartToStr(obj[i])); } // Convert dates to local format else if (i=='accessDate' || i=='dateAdded' || i=='dateModified') { - var date = Zotero.Date.sqlToDate(arr[i], true) + var date = Zotero.Date.isoToDate(obj[i], true) fieldText = escapeXML(date.toLocaleString()); } else { - fieldText = escapeXML(arr[i]); + fieldText = escapeXML(obj[i]); } - content += '<tr>\n<th>' + escapeXML(localizedFieldName) - + '</th>\n<td>' + fieldText + '</td>\n</tr>\n'; + content += '\t\t\t\t\t<tr>\n\t\t\t\t\t<th>' + escapeXML(localizedFieldName) + + '</th>\n\t\t\t\t\t\t<td>' + fieldText + '</td>\n\t\t\t\t\t</tr>\n'; } - content += '</table>'; + content += '\t\t\t\t</table>\n'; return table ? content : ''; } - function _generateTagsList(arr) { + function _generateTagsList(obj) { var content = ''; - if (arr['tags'] && arr['tags'].length) { + if (obj.tags && obj.tags.length) { var str = Zotero.getString('report.tags'); - content += '<h3 class="tags">' + escapeXML(str) + '</h3>\n'; - content += '<ul class="tags">\n'; - for each(var tag in arr.tags) { - content += '<li>' + escapeXML(tag.fields.name) + '</li>\n'; + content += '\t\t\t\t<h3 class="tags">' + escapeXML(str) + '</h3>\n'; + content += '\t\t\t\t<ul class="tags">\n'; + for (let i=0; i<obj.tags.length; i++) { + content += '\t\t\t\t\t<li>' + escapeXML(obj.tags[i].tag) + '</li>\n'; } - content += '</ul>\n'; + content += '\t\t\t\t</ul>\n'; } return content; } - function _generateAttachmentsList(arr) { + function _generateAttachmentsList(obj) { var content = ''; - if (arr.attachments && arr.attachments.length) { - content += '<h3 class="attachments">' + escapeXML(Zotero.getString('itemFields.attachments')) + '</h3>\n'; - content += '<ul class="attachments">\n'; - for each(var attachment in arr.attachments) { - content += '<li id="i' + attachment.itemID + '">'; - content += escapeXML(attachment.title); + if (obj.attachments && obj.attachments.length) { + content += '\t\t\t\t<h3 class="attachments">' + escapeXML(Zotero.getString('itemFields.attachments')) + '</h3>\n'; + content += '\t\t\t\t<ul class="attachments">\n'; + for (let i=0; i<obj.attachments.length; i++) { + let attachment = obj.attachments[i]; + + content += '\t\t\t\t\t<li id="item_' + attachment.itemKey + '">'; + if (attachment.title !== undefined) { + content += escapeXML(attachment.title); + } // Attachment tags content += _generateTagsList(attachment); // Attachment note if (attachment.note) { - content += '<div class="note">'; + content += '\t\t\t\t\t\t<div class="note">'; if (attachment.note.substr(0, 1024).match(/<p[^>]*>/)) { content += attachment.note + '\n'; } @@ -321,13 +324,19 @@ Zotero.Report = new function() { else { content += '<p class="plaintext">' + escapeXML(attachment.note) + '</p>\n'; } - content += '</div>'; + content += '\t\t\t\t\t</div>'; } - content += '</li>\n'; + content += '\t\t\t\t\t</li>\n'; } - content += '</ul>\n'; + content += '\t\t\t\t</ul>\n'; } return content; } + + + var escapeXML = function (str) { + str = str.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\ud800-\udfff\ufffe\uffff]/g, '\u2B1A'); + return Zotero.Utilities.htmlSpecialChars(str); + } } diff --git a/chrome/content/zotero/xpcom/router.js b/chrome/content/zotero/xpcom/router.js @@ -0,0 +1,25 @@ +Components.utils.import("resource://zotero/pathparser.js", Zotero); +Zotero.Router = Zotero.PathParser; +delete Zotero.PathParser; + +Zotero.Router.Utilities = { + convertControllerToObjectType: function (params) { + if (params.controller !== undefined) { + params.objectType = Zotero.DataObjectUtilities.getObjectTypeSingular(params.controller); + delete params.controller; + } + } +}; + + +Zotero.Router.InvalidPathException = function (path) { + this.path = path; +} + + +Zotero.Router.InvalidPathException.prototype = { + name: "InvalidPathException", + toString: function () { + return "Path '" + this.path + "' could not be parsed"; + } +}; diff --git a/chrome/content/zotero/xpcom/search.js b/chrome/content/zotero/xpcom/search.js @@ -119,13 +119,17 @@ Zotero.Search.prototype.loadPrimaryData = Zotero.Promise.coroutine(function* (re var libraryID = this._libraryID; var desc = id ? id : libraryID + "/" + key; - var sql = "SELECT * FROM savedSearches WHERE "; + if (!id && !key) { + throw new Error('ID or key not set'); + } + + var sql = Zotero.Searches.getPrimaryDataSQL() if (id) { - sql += "savedSearchID=?"; + sql += " AND savedSearchID=?"; var params = id; } else { - sql += "key=? AND libraryID=?"; + sql += " AND key=? AND libraryID=?"; var params = [key, libraryID]; } var data = yield Zotero.DB.rowQueryAsync(sql, params); @@ -323,7 +327,7 @@ Zotero.Search.prototype.clone = Zotero.Promise.coroutine(function* (libraryID) { Zotero.Search.prototype.addCondition = Zotero.Promise.coroutine(function* (condition, operator, value, required) { - yield this.loadPrimaryData(); + this._requireData('conditions'); if (!Zotero.SearchConditions.hasOperator(condition, operator)){ throw ("Invalid operator '" + operator + "' for condition " + condition); @@ -483,7 +487,7 @@ Zotero.Search.prototype.removeCondition = Zotero.Promise.coroutine(function* (se * for the given searchConditionID */ Zotero.Search.prototype.getSearchCondition = function(searchConditionID){ - this._requireData('primaryData'); + this._requireData('conditions'); return this._conditions[searchConditionID]; } @@ -493,8 +497,7 @@ Zotero.Search.prototype.getSearchCondition = function(searchConditionID){ * used in the search, indexed by searchConditionID */ Zotero.Search.prototype.getSearchConditions = function(){ - this._requireData('primaryData'); - + this._requireData('conditions'); var conditions = []; for (var id in this._conditions) { var condition = this._conditions[id]; @@ -512,7 +515,7 @@ Zotero.Search.prototype.getSearchConditions = function(){ Zotero.Search.prototype.hasPostSearchFilter = function() { - this._requireData('primaryData'); + this._requireData('conditions'); for each(var i in this._conditions){ if (i.condition == 'fulltextContent'){ return true; @@ -531,9 +534,15 @@ Zotero.Search.prototype.hasPostSearchFilter = function() { Zotero.Search.prototype.search = Zotero.Promise.coroutine(function* (asTempTable) { var tmpTable; + if (this._identified) { + yield this.loadConditions(); + } + // Mark conditions as loaded + else { + this._requireData('conditions'); + } + try { - yield this.loadPrimaryData(); - if (!this._sql){ yield this._buildQuery(); } @@ -580,6 +589,11 @@ Zotero.Search.prototype.search = Zotero.Promise.coroutine(function* (asTempTable // Run a subsearch to define the superset of possible results if (this._scope) { + if (this._scope._identified) { + yield this._scope.loadPrimaryData(); + yield this._scope.loadConditions(); + } + // If subsearch has post-search filter, run and insert ids into temp table if (this._scope.hasPostSearchFilter()) { var ids = yield this._scope.search(); @@ -863,6 +877,7 @@ Zotero.Search.prototype.serialize = function() { */ Zotero.Search.prototype.getSQL = Zotero.Promise.coroutine(function* () { if (!this._sql) { + yield this.loadConditions(); yield this._buildQuery(); } return this._sql; @@ -871,6 +886,7 @@ Zotero.Search.prototype.getSQL = Zotero.Promise.coroutine(function* () { Zotero.Search.prototype.getSQLParams = Zotero.Promise.coroutine(function* () { if (!this._sql) { + yield this.loadConditions(); yield this._buildQuery(); } return this._sqlParams; @@ -966,6 +982,8 @@ Zotero.Search.idsToTempTable = function (ids) { * Build the SQL query for the search */ Zotero.Search.prototype._buildQuery = Zotero.Promise.coroutine(function* () { + this._requireData('conditions'); + var sql = 'SELECT itemID FROM items'; var sqlParams = []; // Separate ANY conditions for 'required' condition support @@ -1171,14 +1189,16 @@ Zotero.Search.prototype._buildQuery = Zotero.Promise.coroutine(function* () { if (condition.value) { var lkh = Zotero.Collections.parseLibraryKeyHash(condition.value); if (lkh) { - col = Zotero.Collections.getByLibraryAndKey(lkh.libraryID, lkh.key); + col = yield Zotero.Collections.getByLibraryAndKeyAsync(lkh.libraryID, lkh.key); } } if (!col) { var msg = "Collection " + condition.value + " specified in saved search doesn't exist"; Zotero.debug(msg, 2); Zotero.log(msg, 'warning', 'chrome://zotero/content/xpcom/search.js'); - continue; + col = { + id: 0 + }; } var q = ['?']; @@ -1633,6 +1653,22 @@ Zotero.Searches = new function(){ Zotero.DataObjects.apply(this, ['search', 'searches', 'savedSearch', 'savedSearches']); this.constructor.prototype = new Zotero.DataObjects(); + Object.defineProperty(this, "_primaryDataSQLParts", { + get: function () { + return _primaryDataSQLParts ? _primaryDataSQLParts : (_primaryDataSQLParts = { + savedSearchID: "O.savedSearchID", + name: "O.savedSearchName", + libraryID: "O.libraryID", + key: "O.key", + version: "O.version", + synced: "O.synced" + }); + } + }); + + + var _primaryDataSQLParts; + this.init = Zotero.Promise.coroutine(function* () { yield this.constructor.prototype.init.apply(this); @@ -1661,7 +1697,7 @@ Zotero.Searches = new function(){ }); var searches = []; - for (i=0; i<rows.length; i++) { + for (var i=0; i<rows.length; i++) { let search = new Zotero.Search; search.id = rows[i].id; yield search.loadPrimaryData(); @@ -1697,10 +1733,12 @@ Zotero.Searches = new function(){ }); - this._getPrimaryDataSQL = function () { + this.getPrimaryDataSQL = function () { // This should be the same as the query in Zotero.Search.loadPrimaryData(), // just without a specific savedSearchID - return "SELECT O.* FROM savedSearches O WHERE 1"; + return "SELECT " + + Object.keys(this._primaryDataSQLParts).map(key => this._primaryDataSQLParts[key]).join(", ") + " " + + "FROM savedSearches O WHERE 1"; } } @@ -1715,8 +1753,8 @@ Zotero.SearchConditions = new function(){ this.parseCondition = parseCondition; var _initialized = false; - var _conditions = {}; - var _standardConditions = []; + var _conditions; + var _standardConditions; var self = this; @@ -2180,6 +2218,7 @@ Zotero.SearchConditions = new function(){ ]; // Index conditions by name and aliases + _conditions = {}; for (var i in conditions) { _conditions[conditions[i]['name']] = conditions[i]; if (conditions[i]['aliases']) { @@ -2271,6 +2310,10 @@ Zotero.SearchConditions = new function(){ function hasOperator(condition, operator){ var [condition, mode] = this.parseCondition(condition); + if (!_conditions) { + throw new Zotero.Exception.UnloadedDataException("Search conditions not yet loaded"); + } + if (!_conditions[condition]){ throw ("Invalid condition '" + condition + "' in hasOperator()"); } diff --git a/chrome/content/zotero/xpcom/timeline.js b/chrome/content/zotero/xpcom/timeline.js @@ -24,32 +24,28 @@ */ -Zotero.Timeline = new function () { - this.generateXMLDetails = generateXMLDetails; - this.generateXMLList = generateXMLList; - - function generateXMLDetails(items, dateType) { +Zotero.Timeline = { + generateXMLDetails: function* (items, dateType) { var escapeXML = Zotero.Utilities.htmlSpecialChars; - var content = '<data>\n'; - for each(var item in items) { + yield '<data>\n'; + for (let i=0; i<items.length; i++) { + let item = items[i]; + yield item.loadItemData(); var date = item.getField(dateType, true, true); if (date) { - var sqlDate = (dateType == 'date') ? Zotero.Date.multipartToSQL(date) : date; + let sqlDate = (dateType == 'date') ? Zotero.Date.multipartToSQL(date) : date; sqlDate = sqlDate.replace("00-00", "01-01"); - content += '<event start="' + Zotero.Date.sqlToDate(sqlDate) + '" '; - var title = item.getField('title'); + let content = '<event start="' + Zotero.Date.sqlToDate(sqlDate) + '" '; + let title = item.getField('title'); content += 'title=" ' + (title ? escapeXML(title) : '') + '" '; content += 'icon="' + item.getImageSrc() + '" '; content += 'color="black">'; content += item.id; content += '</event>\n'; + yield content; } } - content += '</data>'; - return content; - } - - function generateXMLList(items) { + yield '</data>'; } -} -\ No newline at end of file +}; +\ No newline at end of file diff --git a/chrome/content/zotero/xpcom/users.js b/chrome/content/zotero/xpcom/users.js @@ -0,0 +1,101 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2014 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://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 ***** +*/ + +Zotero.Users = new function () { + var _userID; + var _libraryID; + var _username; + var _localUserKey; + + this.init = Zotero.Promise.coroutine(function* () { + var sql = "SELECT value FROM settings WHERE setting='account' AND key='userID'"; + _userID = yield Zotero.DB.valueQueryAsync(sql); + + sql = "SELECT value FROM settings WHERE setting='account' AND key='libraryID'"; + _libraryID = yield Zotero.DB.valueQueryAsync(sql); + + sql = "SELECT value FROM settings WHERE setting='account' AND key='username'"; + _username = yield Zotero.DB.valueQueryAsync(sql); + + // If we don't have a global user id, generate a local user key + if (!_userID) { + sql = "SELECT value FROM settings WHERE setting='account' AND key='localUserKey'"; + let key = yield Zotero.DB.valueQueryAsync(sql); + // Generate a local user key if we don't have one + if (!key) { + key = Zotero.randomString(8); + sql = "INSERT INTO settings VALUES ('account', 'localUserKey', ?)"; + yield Zotero.DB.queryAsync(sql, key); + } + _localUserKey = key; + } + }); + + + this.getCurrentUserID = function () { + return _userID; + }; + + + this.setCurrentUserID = Zotero.Promise.coroutine(function* (val) { + val = parseInt(val); + var sql = "REPLACE INTO settings VALUES ('account', 'userID', ?)"; + Zotero.DB.queryAsync(sql, val); + _userID = val; + }); + + + this.getCurrentLibraryID = function () { + return _libraryID; + }; + + + this.setCurrentLibraryID = Zotero.Promise.coroutine(function* (val) { + val = parseInt(val); + var sql = "REPLACE INTO settings VALUES ('account', 'libraryID', ?)"; + Zotero.DB.queryAsync(sql, val); + _userID = val; + }); + + + this.getCurrentUsername = function () { + return _username; + }; + + + this.setCurrentUsername = Zotero.Promise.coroutine(function* (val) { + var sql = "REPLACE INTO settings VALUES ('account', 'username', ?)"; + Zotero.DB.queryAsync(sql, val); + _userID = val; + }); + + + this.getLocalUserKey = function () { + if (!_localUserKey) { + throw new Error("Local user key not available"); + } + return _localUserKey; + }; +}; diff --git a/chrome/content/zotero/xpcom/utilities_internal.js b/chrome/content/zotero/xpcom/utilities_internal.js @@ -368,6 +368,123 @@ Zotero.Utilities.Internal = { } }, + + /** + * Return an input stream that will be filled asynchronously with strings yielded from a + * generator. If the generator yields a promise, the promise is waited for, but its value + * is not added to the input stream. + * + * @param {GeneratorFunction|Generator} gen - Promise-returning generator function or + * generator + * @return {nsIAsyncInputStream} + */ + getAsyncInputStream: function (gen, onError) { + const funcName = 'getAsyncInputStream'; + const maxOutOfSequenceSeconds = 10; + const outOfSequenceDelay = 50; + + // Initialize generator if necessary + var g = gen.next ? gen : gen(); + var seq = 0; + + var pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe); + pipe.init(true, true, 0, 0, null); + + var os = Components.classes["@mozilla.org/intl/converter-output-stream;1"] + .createInstance(Components.interfaces.nsIConverterOutputStream); + os.init(pipe.outputStream, 'utf-8', 0, 0x0000); + + pipe.outputStream.asyncWait({ + onOutputStreamReady: function (aos) { + Zotero.debug("Output stream is ready"); + + let currentSeq = seq++; + + Zotero.spawn(function* () { + var lastVal; + var error = false; + + while (true) { + var data; + + try { + let result = g.next(lastVal); + + if (result.done) { + Zotero.debug("No more data to write"); + aos.close(); + return; + } + // If a promise is yielded, wait for it and pass on its value + if (result.value.then) { + lastVal = yield result.value; + continue; + } + // Otherwise use the return value + data = result.value; + break; + } + catch (e) { + Zotero.debug(e, 1); + + if (onError) { + error = e; + data = onError(); + break; + } + + Zotero.debug("Closing input stream"); + aos.close(); + throw e; + } + } + + if (typeof data != 'string') { + throw new Error("Yielded value is not a string or promise in " + funcName + + " ('" + data + "')"); + } + + // Make sure that we're writing to the stream in order, in case + // onOutputStreamReady is called again before the last promise completes. + // If not in order, wait a bit and try again. + var maxTries = Math.floor(maxOutOfSequenceSeconds * 1000 / outOfSequenceDelay); + while (currentSeq != seq - 1) { + if (maxTries <= 0) { + throw new Error("Next promise took too long to finish in " + funcName); + } + Zotero.debug("Promise finished out of sequence in " + funcName + + "-- waiting " + outOfSequenceDelay + " ms"); + yield Zotero.Promise.delay(outOfSequenceDelay); + maxTries--; + } + + // Write to stream + Zotero.debug("Writing " + data.length + " characters"); + os.writeString(data); + + if (error) { + Zotero.debug("Closing input stream"); + aos.close(); + throw error; + } + + Zotero.debug("Waiting to write more"); + + // Wait until stream is ready for more + aos.asyncWait(this, 0, 0, null); + }, this) + .catch(function (e) { + Zotero.debug("Error getting data for async stream", 1); + Components.utils.reportError(e); + Zotero.debug(e, 1); + os.close(); + }); + } + }, 0, 0, null); + + return pipe.inputStream; + }, + /** * Defines property on the object's prototype. * More compact way to do Object.defineProperty diff --git a/chrome/content/zotero/zoteroPane.js b/chrome/content/zotero/zoteroPane.js @@ -43,7 +43,6 @@ var ZoteroPane = new function() this.handleKeyUp = handleKeyUp; this.setHighlightedRowsCallback = setHighlightedRowsCallback; this.handleKeyPress = handleKeyPress; - this.editSelectedCollection = editSelectedCollection; this.handleSearchKeypress = handleSearchKeypress; this.handleSearchInput = handleSearchInput; this.getSelectedCollection = getSelectedCollection; @@ -1781,8 +1780,7 @@ var ZoteroPane = new function() }); - function editSelectedCollection() - { + this.editSelectedCollection = function () { if (!this.canEdit()) { this.displayCannotEditLibraryMessage(); return; @@ -1807,11 +1805,17 @@ var ZoteroPane = new function() else { var s = new Zotero.Search(); s.id = row.ref.id; - var io = {dataIn: {search: s, name: row.getName()}, dataOut: null}; - window.openDialog('chrome://zotero/content/searchDialog.xul','','chrome,modal',io); - if (io.dataOut) { - this.onCollectionSelected(); //reload itemsView - } + s.loadPrimaryData() + .then(function () { + return s.loadConditions(); + }) + .then(function () { + var io = {dataIn: {search: s, name: row.getName()}, dataOut: null}; + window.openDialog('chrome://zotero/content/searchDialog.xul','','chrome,modal',io); + if (io.dataOut) { + this.onCollectionSelected(); //reload itemsView + } + }.bind(this)); } } } diff --git a/chrome/skin/default/zotero/timeline/timeline.html b/chrome/skin/default/zotero/timeline/timeline.html @@ -69,7 +69,7 @@ Timeline.loadXML("zotero://timeline/data/", function(xml, url) { eventSource.loadXML(xml, url); }); setupFilterHighlightControls(document.getElementById("my-timeline-controls"), tl, [0,1,2], theme); - setupOtherControls(document.getElementById("my-other-controls"), tl, document.URL); + setupOtherControls(document.getElementById("my-other-controls"), tl, document.location.search); } function onResize() { diff --git a/chrome/skin/default/zotero/timeline/timelineControls.js b/chrome/skin/default/zotero/timeline/timelineControls.js @@ -145,7 +145,7 @@ function checkDate(date) { } } } -function changeBand(path, queryString, band, intervals, selectedIndex) { +function changeBand(queryString, band, intervals, selectedIndex) { var values = new Array('d', 'm', 'y', 'e', 'c', 'i'); var newIntervals = ''; @@ -158,7 +158,7 @@ function changeBand(path, queryString, band, intervals, selectedIndex) { } } - window.location = path + queryString + 'i=' + newIntervals; + window.location.search = queryString + 'i=' + newIntervals; } function createOption(t, selected) { @@ -192,7 +192,7 @@ function getFull(a) { } function createQueryString(theQueryValue, except, timeline) { - var temp = '?'; + var temp = ''; for(var i in theQueryValue) { if(except != i) { temp += i + '=' + theQueryValue[i] + '&'; @@ -208,16 +208,9 @@ function createQueryString(theQueryValue, except, timeline) { return temp; } -function setupOtherControls(div, timeline, url) { +function setupOtherControls(div, timeline, queryString) { var table = document.createElement("table"); - var [path, queryString] = url.split('?'); - if(path == 'zotero://timeline') { - path += '/'; - } - if(path =='zotero://timeline/') { - path += 'library'; - } var defaultQueryValue = new Object(); defaultQueryValue['i'] = 'mye'; defaultQueryValue['t'] = 'd'; @@ -289,7 +282,7 @@ function setupOtherControls(div, timeline, url) { select1.appendChild(createOption(options[i],(options[i] == selected))); } select1.onchange = function () { - changeBand(path, createQueryString(theQueryValue, 'i', timeline), 0, intervals, table.rows[1].cells[1].firstChild.selectedIndex); + changeBand(createQueryString(theQueryValue, 'i', timeline), 0, intervals, table.rows[1].cells[1].firstChild.selectedIndex); }; td.appendChild(select1); @@ -301,7 +294,7 @@ function setupOtherControls(div, timeline, url) { select2.appendChild(createOption(options[i],(options[i] == selected))); } select2.onchange = function () { - changeBand(path, createQueryString(theQueryValue, 'i', timeline), 1, intervals, table.rows[1].cells[2].firstChild.selectedIndex); + changeBand(createQueryString(theQueryValue, 'i', timeline), 1, intervals, table.rows[1].cells[2].firstChild.selectedIndex); }; td.appendChild(select2); @@ -313,7 +306,7 @@ function setupOtherControls(div, timeline, url) { select3.appendChild(createOption(options[i],(options[i] == selected))); } select3.onchange = function () { - changeBand(path, createQueryString(theQueryValue, 'i', timeline), 2, intervals, table.rows[1].cells[3].firstChild.selectedIndex); + changeBand(createQueryString(theQueryValue, 'i', timeline), 2, intervals, table.rows[1].cells[3].firstChild.selectedIndex); }; td.appendChild(select3); @@ -327,7 +320,7 @@ function setupOtherControls(div, timeline, url) { select4.appendChild(createOption(options[i],(values[i] == dateType))); } select4.onchange = function () { - window.location = path + createQueryString(theQueryValue, 't', timeline) + 't=' + values[table.rows[1].cells[4].firstChild.selectedIndex]; + window.location.search = createQueryString(theQueryValue, 't', timeline) + 't=' + values[table.rows[1].cells[4].firstChild.selectedIndex]; }; td.appendChild(select4); @@ -335,7 +328,7 @@ function setupOtherControls(div, timeline, url) { var fitToScreen = document.createElement("button"); fitToScreen.innerHTML = getString("general.fitToScreen"); Timeline.DOM.registerEvent(fitToScreen, "click", function () { - window.location = path + createQueryString(theQueryValue, false, timeline); + window.location.search = createQueryString(theQueryValue, false, timeline); }); td.appendChild(fitToScreen); diff --git a/components/zotero-protocol-handler.js b/components/zotero-protocol-handler.js @@ -32,6 +32,10 @@ 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"; +const Cc = Components.classes; +const Ci = Components.interfaces; +const Cr = Components.results; + Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); // Dummy chrome URL used to obtain a valid chrome channel @@ -39,170 +43,148 @@ Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); // for any other well known chrome URL in the browser installation const DUMMY_CHROME_URL = "chrome://mozapps/content/xpinstall/xpinstallConfirm.xul"; +var Zotero = Components.classes["@zotero.org/Zotero;1"] + .getService(Components.interfaces.nsISupports) + .wrappedJSObject; + +var ioService = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); -function ChromeExtensionHandler() { +function ZoteroProtocolHandler() { this.wrappedJSObject = this; - this._systemPrincipal = null; + this._principal = null; this._extensions = {}; + /** + * 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"; + } + }); + } + }; + + /* * Report generation extension for Zotero protocol - * - * Example URLs: - * - * zotero://report/ -- library - * zotero://report/collection/0_ABCD1234 - * zotero://report/search/0_ABCD1234 - * zotero://report/items/0_ABCD1234-0_BCDE2345-0_CDEF3456 - * zotero://report/item/0_ABCD1234 - * - * Optional format can be specified after hashes - * - * - 'html', 'rtf', 'csv' ['rtf' and 'csv' not yet supported] - * - defaults to 'html' if not specified - * - * e.g. zotero://report/collection/0_ABCD1234/rtf - * - * - * Sorting: - * - * - 'sort' query string variable - * - format is field[/order] [, field[/order], ...] - * - order can be 'asc', 'a', 'desc' or 'd'; defaults to ascending order - * - * zotero://report/collection/0_ABCD1234?sort=itemType/d,title - * - * - * Also supports ids (e.g., zotero://report/collection/1234), but ids are not - * guaranteed to be consistent across synced machines */ - var ReportExtension = new function(){ - this.newChannel = newChannel; + var ReportExtension = { + loadAsChrome: false, - this.__defineGetter__('loadAsChrome', function () { return false; }); - - function newChannel(uri){ - var ioService = Components.classes["@mozilla.org/network/io-service;1"] - .getService(Components.interfaces.nsIIOService); - - var Zotero = Components.classes["@zotero.org/Zotero;1"] - .getService(Components.interfaces.nsISupports) - .wrappedJSObject; - - generateContent:try { - var mimeType, content = ''; + newChannel: function (uri) { + return new AsyncChannel(uri, function* () { + var path = uri.path; + if (!path) { + return 'Invalid URL'; + } + // Strip leading '/' + path = path.substr(1); - var [path, queryString] = uri.path.substr(1).split('?'); - var [type, ids, format] = path.split('/'); + // Proxy CSS files + if (path.endsWith('.css')) { + var chromeURL = 'chrome://zotero/skin/report/' + path; + Zotero.debug(chromeURL); + var ios = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + 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); + } - // Get query string variables - if (queryString) { - var queryVars = queryString.split('&'); - for (var i in queryVars) { - var [key, val] = queryVars[i].split('='); - switch (key) { - case 'sort': - var sortBy = val; - break; - } + var params = { + format: 'html', + sort: 'title' + }; + var router = new Zotero.Router(params); + + // Items within a collection or search + router.add('library/:scopeObject/:scopeObjectKey/items/report.html', function () { + params.libraryID = 0; + }); + router.add('groups/:groupID/:scopeObject/:scopeObjectKey/items/report.html'); + + // All items + router.add('library/items/report.html', function () { + params.libraryID = 0; + }); + router.add('groups/:groupID/items/report.html'); + + // 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; + params.scopeObjectKey = lkh.key; + } + else { + params.scopeObjectID = params.id; } + 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; + 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]; + params.itemKey = ids.map(x => x.split('_')[1]); + delete params.ids; + }); + + var parsed = router.run(path); + if (!parsed) { + return "URL could not be parsed"; } - switch (type){ - case 'collection': - var lkh = Zotero.Collections.parseLibraryKeyHash(ids); - if (lkh) { - var col = Zotero.Collections.getByLibraryAndKey(lkh.libraryID, lkh.key); - } - else { - var col = Zotero.Collections.get(ids); - } - if (!col) { - mimeType = 'text/html'; - content = 'Invalid collection ID or key'; - break generateContent; - } - var results = col.getChildItems(); - break; - - case 'search': - var lkh = Zotero.Searches.parseLibraryKeyHash(ids); - if (lkh) { - var s = Zotero.Searches.getByLibraryAndKey(lkh.libraryID, lkh.key); - } - else { - var s = Zotero.Searches.get(ids); - } - if (!s) { - mimeType = 'text/html'; - content = 'Invalid search ID or key'; - break generateContent; - } - - // FIXME: Hack to exclude group libraries for now - var s2 = new Zotero.Search(); - s2.setScope(s); - var groups = Zotero.Groups.getAll(); - for each(var group in groups) { - s2.addCondition('libraryID', 'isNot', group.libraryID); - } - var ids = s2.search(); - - var results = Zotero.Items.get(ids); - break; - - case 'items': - case 'item': - ids = ids.split('-'); - - // Keys - if (Zotero.Items.parseLibraryKeyHash(ids[0])) { - var results = []; - for each(var lkh in ids) { - var lkh = Zotero.Items.parseLibraryKeyHash(lkh); - var item = Zotero.Items.getByLibraryAndKey(lkh.libraryID, lkh.key); - if (item) { - results.push(item); - } - } - } - // IDs - else { - var results = Zotero.Items.get(ids); - } - - if (!results.length) { - mimeType = 'text/html'; - content = 'Invalid ID'; - break generateContent; - } - break; - - default: - // Proxy CSS files - if (type.match(/^detail.*\.css$/)) { - var chromeURL = 'chrome://zotero/skin/report/' + type; - var ios = Components.classes["@mozilla.org/network/io-service;1"] - .getService(Components.interfaces.nsIIOService); - var uri = ios.newURI(chromeURL, null, null); - var chromeReg = Components.classes["@mozilla.org/chrome/chrome-registry;1"] - .getService(Components.interfaces.nsIChromeRegistry); - var fileURI = chromeReg.convertChromeURL(uri); - var ph = Components.classes["@mozilla.org/network/protocol;1?name=file"] - .createInstance(Components.interfaces.nsIFileProtocolHandler); - var channel = ioService.newChannelFromURI(fileURI); - return channel; - } - - // Display all items - var type = 'library'; - var s = new Zotero.Search(); - s.addCondition('noChildren', 'true'); - var ids = s.search(); - var results = Zotero.Items.get(ids); + // TODO: support old URLs + // collection + // search + // items + // item + if (params.sort.contains('/')) { + 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(); } + var mimeType, content = ''; var items = []; var itemsHash = {}; // key = itemID, val = position in |items| var searchItemIDs = {}; // hash of all selected items @@ -216,10 +198,10 @@ function ChromeExtensionHandler() { 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].getSource(); + var parentItemID = results[i].parentItemID; if (parentItemID) { searchParentIDs[parentItemID] = true; - searchChildIDs[results[i].getID()] = true; + searchChildIDs[results[i].id] = true; // Don't include all child items if any child // items were selected @@ -228,15 +210,15 @@ function ChromeExtensionHandler() { // If combining children or standalone note/attachment, add matching parents else if (combineChildItems || !results[i].isRegularItem() || results[i].numChildren() == 0) { - itemsHash[results[i].getID()] = [items.length]; - items.push(results[i].toArray(2)); + itemsHash[results[i].id] = [items.length]; + items.push(yield results[i].toJSON({ mode: 'full' })); // Flag item as a search match items[items.length - 1].reportSearchMatch = true; } else { unhandledParents[i] = true; } - searchItemIDs[results[i].getID()] = true; + searchItemIDs[results[i].id] = true; } // If including all child items, add children of all matched @@ -245,7 +227,7 @@ function ChromeExtensionHandler() { for (var id in searchItemIDs) { if (!searchChildIDs[id]) { var children = []; - var item = Zotero.Items.get(id); + var item = yield Zotero.Items.getAsync(id); if (!item.isRegularItem()) { continue; } @@ -256,6 +238,7 @@ function ChromeExtensionHandler() { } } }; + yield item.loadChildItems(); func(item.getNotes()); func(item.getAttachments()); } @@ -266,7 +249,7 @@ function ChromeExtensionHandler() { else { for (var i in unhandledParents) { itemsHash[results[i].id] = [items.length]; - items.push(results[i].toArray(2)); + items.push(yield results[i].toJSON({ mode: 'full' })); // Flag item as a search match items[items.length - 1].reportSearchMatch = true; } @@ -276,27 +259,27 @@ function ChromeExtensionHandler() { // Add parents of matches if parents aren't matches themselves for (var id in searchParentIDs) { if (!searchItemIDs[id] && !itemsHash[id]) { - var item = Zotero.Items.get(id); + var item = yield Zotero.Items.getAsync(id); itemsHash[id] = items.length; - items.push(item.toArray(2)); + items.push(yield item.toJSON({ mode: 'full' })); } } // Add children to reportChildren property of parents for (var id in searchChildIDs) { - var item = Zotero.Items.get(id); - var parentItemID = item.getSource(); - if (!items[itemsHash[parentItemID]].reportChildren) { - items[itemsHash[parentItemID]].reportChildren = { + var item = yield Zotero.Items.getAsync(id); + var parentID = item.parentID; + if (!items[itemsHash[parentID]].reportChildren) { + items[itemsHash[parentID]].reportChildren = { notes: [], attachments: [] }; } if (item.isNote()) { - items[itemsHash[parentItemID]].reportChildren.notes.push(item.toArray()); + items[itemsHash[parentID]].reportChildren.notes.push(yield item.toJSON({ mode: 'full' })); } if (item.isAttachment()) { - items[itemsHash[parentItemID]].reportChildren.attachments.push(item.toArray()); + items[itemsHash[parentID]].reportChildren.attachments.push(yield item.toJSON({ mode: 'full' })); } } } @@ -304,8 +287,8 @@ function ChromeExtensionHandler() { // for each matching child else { for (var id in searchChildIDs) { - var item = Zotero.Items.get(id); - var parentID = item.getSource(); + var item = yield Zotero.Items.getAsync(id); + var parentID = item.parentID; var parentItem = Zotero.Items.get(parentID); if (!itemsHash[parentID]) { @@ -313,7 +296,7 @@ function ChromeExtensionHandler() { // add on its own if (searchItemIDs[parentID]) { itemsHash[parentID] = [items.length]; - items.push(parentItem.toArray(2)); + items.push(yield parentItem.toJSON({ mode: 'full' })); items[items.length - 1].reportSearchMatch = true; } else { @@ -323,50 +306,28 @@ function ChromeExtensionHandler() { // Now add parent and child itemsHash[parentID].push(items.length); - items.push(parentItem.toArray(2)); + items.push(parentItem.toJSON({ mode: 'full' })); if (item.isNote()) { items[items.length - 1].reportChildren = { - notes: [item.toArray()], + notes: [yield item.toJSON({ mode: 'full' })], attachments: [] }; } else if (item.isAttachment()) { items[items.length - 1].reportChildren = { notes: [], - attachments: [item.toArray()] + attachments: [yield item.toJSON({ mode: 'full' })] }; } } } - // Sort items - if (!sortBy) { - sortBy = 'title'; - } - - var sorts = sortBy.split(','); - for (var i=0; i<sorts.length; i++) { - var [field, order] = sorts[i].split('/'); - // Year field is really date field - if (field == 'year') { - field = 'date'; - } - switch (order) { - case 'd': - case 'desc': - order = -1; - break; - - default: - order = 1; - } - - sorts[i] = { - field: field, - order: order - }; - } + // TODO: restore multiple sort fields + var sorts = [{ + field: params.sort, + order: params.direction != 'desc' ? 1 : -1 + }]; var collation = Zotero.getLocaleCollation(); @@ -468,279 +429,288 @@ function ChromeExtensionHandler() { } // Pass off to the appropriate handler - switch (format){ + switch (params.format) { case 'rtf': - mimeType = 'text/rtf'; - break; + this.contentType = 'text/rtf'; + return ''; case 'csv': - mimeType = 'text/plain'; - break; + this.contentType = 'text/plain'; + return ''; default: - var content = Zotero.Report.generateHTMLDetails(items, combineChildItems); - mimeType = 'text/html'; + this.contentType = 'text/html'; + return Zotero.Utilities.Internal.getAsyncInputStream( + Zotero.Report.HTML.listGenerator(items, combineChildItems), + function () { + return '<span style="color: red; font-weight: bold">Error generating report</span>'; + } + ); } - } - catch (e){ - Zotero.debug(e); - throw (e); - } - - var uri_str = 'data:' + (mimeType ? mimeType + ',' : '') + encodeURIComponent(content); - var ext_uri = ioService.newURI(uri_str, null, null); - var extChannel = ioService.newChannelFromURI(ext_uri); - - return extChannel; + }); } }; - - var TimelineExtension = new function(){ - this.newChannel = newChannel; - - this.__defineGetter__('loadAsChrome', function () { return true; }); - - /* - queryString 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.) - - - - zotero://timeline -----> creates HTML for timeline - (defaults: type = library | intervals = month, year, decade | timelineDate = today's date | dateType = date) - - - Example URLs: - - zotero://timeline/library?i=yec - zotero://timeline/collection/12345?t=da&d=Jul.24.2008 - zotero://timeline/search/54321?d=Dec.1.-500&i=dmy&t=d - - - - zotero://timeline/data ----->creates XML file - (defaults: type = library | dateType = date) - - - Example URLs: - - zotero://timeline/data/library?t=da - zotero://timeline/data/collection/12345 - zotero://timeline/data/search/54321?t=dm - - */ - function newChannel(uri) { - var ioService = Components.classes["@mozilla.org/network/io-service;1"] - .getService(Components.interfaces.nsIIOService); - - var Zotero = Components.classes["@zotero.org/Zotero;1"] - .getService(Components.interfaces.nsISupports) - .wrappedJSObject; - - generateContent:try { - var mimeType, content = ''; - var [path, queryString] = uri.path.substr(1).split('?'); - var [intervals, timelineDate, dateType] = ['','','']; + /** + * 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* () { + path = uri.spec.match(/zotero:\/\/[^/]+(.*)/)[1]; + if (!path) { + this.contentType = 'text/html'; + return 'Invalid URL'; + } - if (queryString) { - var queryVars = queryString.split('&'); - for (var i in queryVars) { - var [key, val] = queryVars[i].split('='); - if(val) { - switch (key) { - case 'i': - intervals = val; - break; - case 'd': - timelineDate = val; - break; - case 't': - dateType = val; - break; - } - } + var params = {}; + var router = new Zotero.Router(params); + + // HTML + router.add('library/:scopeObject/:scopeObjectKey', function () { + params.libraryID = 0; + params.controller = 'html'; + }); + router.add('groups/:groupID/:scopeObject/:scopeObjectKey', function () { + params.controller = 'html'; + }); + router.add('library', function () { + params.libraryID = 0; + params.controller = 'html'; + }); + router.add('groups/:groupID', function () { + params.controller = 'html'; + }); + + // Data + router.add('data/library/:scopeObject/:scopeObjectKey', function () { + params.libraryID = 0; + params.controller = 'data'; + }); + router.add('data/groups/:groupID/:scopeObject/:scopeObjectKey', function () { + params.controller = 'data'; + }); + router.add('data/library', function () { + params.libraryID = 0; + 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; + params.scopeObjectKey = lkh.key; } - } + 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; + params.scopeObjectKey = lkh.key; + } + else { + params.scopeObjectID = params.id; + } + delete params.id; + }); + router.add('/', function () { + params.controller = 'html'; + params.libraryID = 0; + }); - var pathParts = path.split('/'); - if (pathParts[0] != 'data') { - var [type, id] = pathParts; + var parsed = router.run(path); + if (!parsed) { + this.contentType = 'text/html'; + return "URL could not be parsed"; } - else { - var [, type, id] = pathParts; + 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 (type) { - case 'collection': - var lkh = Zotero.Collections.parseLibraryKeyHash(id); - if (lkh) { - collection = Zotero.Collections.getByLibraryAndKey(lkh.libraryID, lkh.key); + switch (params.scopeObject) { + case 'collections': + if (params.scopeObjectKey) { + collection = yield Zotero.Collections.getByLibraryAndKeyAsync( + params.libraryID, params.scopeObjectKey + ); } else { - collection = Zotero.Collections.get(id); + collection = yield Zotero.Collections.getAsync(params.scopeObjectID); } if (!collection) { - mimeType = 'text/html'; - content = 'Invalid collection ID or key'; - break generateContent; + this.contentType = 'text/html'; + return 'Invalid collection ID or key'; } break; - case 'search': - var lkh = Zotero.Searches.parseLibraryKeyHash(id); - if (lkh) { - var s = Zotero.Searches.getByLibraryAndKey(lkh.libraryID, lkh.key); + case 'searches': + if (params.scopeObjectKey) { + var s = yield Zotero.Searches.getByLibraryAndKeyAsync( + params.libraryID, params.scopeObjectKey + ); } else { - var s = Zotero.Searches.get(id); + var s = yield Zotero.Searches.getAsync(params.scopeObjectID); } if (!s) { - mimeType = 'text/html'; - content = 'Invalid search ID or key'; - break generateContent; + 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(); + var groups = yield Zotero.Groups.getAll(); for each(var group in groups) { - search.addCondition('libraryID', 'isNot', group.libraryID); + yield search.addCondition('libraryID', 'isNot', group.libraryID); } break; } - if (pathParts[0] != 'data') { - //creates HTML file - content = Zotero.File.getContentsFromURL('chrome://zotero/skin/timeline/timeline.html'); - mimeType = 'text/html'; - - var [type, id] = pathParts; - - if(!timelineDate){ - timelineDate=Date(); - var dateParts=timelineDate.toString().split(' '); - timelineDate=dateParts[1]+'.'+dateParts[2]+'.'+dateParts[3]; - } - if (intervals.length < 3) { - intervals += "mye".substr(intervals.length); - } - - var theIntervals = new Object(); - theIntervals['d'] = 'Timeline.DateTime.DAY'; - theIntervals['m'] = 'Timeline.DateTime.MONTH'; - theIntervals['y'] = 'Timeline.DateTime.YEAR'; - theIntervals['e'] = 'Timeline.DateTime.DECADE'; - theIntervals['c'] = 'Timeline.DateTime.CENTURY'; - theIntervals['i'] = 'Timeline.DateTime.MILLENNIUM'; - - //sets the intervals of the timeline bands - var theTemp = '<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(theTemp, theTemp + a + ',' + b + ',' + c + ',\'' + timelineDate + '\''); - - theTemp = 'document.write("<title>'; - if(type == 'collection') { - content = content.replace(theTemp, theTemp + collection.name + ' - '); - } - else if(type == 'search') { - content = content.replace(theTemp, theTemp + search.name + ' - '); - } - else { - content = content.replace(theTemp, theTemp + Zotero.getString('pane.collections.library') + ' - '); - } - - theTemp = 'Timeline.loadXML("zotero://timeline/data/'; - var d = ''; - //passes information (type,ids, dateType) for when the XML is created - if(!type || (type != 'collection' && type != 'search')) { - d += 'library' + (id ? "/" + id : ""); - } - else { - d += type + '/' + id; - } - - if(dateType) { - d += '?t=' + dateType; - } - - content = content.replace(theTemp, theTemp + d); - - - var uri_str = 'data:' + (mimeType ? mimeType + ',' : '') + encodeURIComponent(content); - var ext_uri = ioService.newURI(uri_str, null, null); - var extChannel = ioService.newChannelFromURI(ext_uri); - - return extChannel; - } + // // Create XML file - else { - switch (type) { - case 'collection': + // + if (params.controller == 'data') { + switch (params.scopeObject) { + case 'collections': + yield collection.loadChildItems(); var results = collection.getChildItems(); break; - case 'search': - var ids = search.search(); - var results = Zotero.Items.get(ids); + case 'searches': + var ids = yield search.search(); + var results = yield Zotero.Items.getAsync(ids); break; default: - type = 'library'; - var s = new Zotero.Search(); - s.addCondition('libraryID', 'is', id ? id : 0); - s.addCondition('noChildren', 'true'); - var ids = s.search(); - var results = Zotero.Items.get(ids); + if (params.scopeObject) { + return "Invalid scope object '" + params.scopeObject + "'"; + } + + let s = new Zotero.Search(); + yield s.addCondition('libraryID', 'is', params.libraryID); + yield s.addCondition('noChildren', 'true'); + var ids = yield s.search(); + var results = yield Zotero.Items.getAsync(ids); } - + var items = []; // Only include parent items - for (var i = 0; i < results.length; i++) { - if (!results[i].getSource()) { + for (let i=0; i<results.length; i++) { + if (!results[i].parentItemID) { items.push(results[i]); } } - if (!items) { - mimeType = 'text/html'; - content = 'Invalid ID'; - break generateContent; - } - - mimeType = 'application/xml'; - - var theDateTypes = new Object(); - theDateTypes['d'] = 'date'; - theDateTypes['da'] = 'dateAdded'; - theDateTypes['dm'] = 'dateModified'; + var dateTypes = { + d: 'date', + da: 'dateAdded', + dm: 'dateModified' + }; //default dateType = date - if (!dateType || !theDateTypes[dateType]) { + if (!dateType || !dateTypes[dateType]) { dateType = 'd'; } - content = Zotero.Timeline.generateXMLDetails(items, theDateTypes[dateType]); + this.contentType = 'application/xml'; + return Zotero.Utilities.Internal.getAsyncInputStream( + Zotero.Timeline.generateXMLDetails(items, dateTypes[dateType]) + ); } - var uri_str = 'data:' + (mimeType ? mimeType + ',' : '') + encodeURIComponent(content); - var ext_uri = ioService.newURI(uri_str, null, null); - var extChannel = ioService.newChannelFromURI(ext_uri); + // + // Generate main HTML page + // + content = Zotero.File.getContentsFromURL('chrome://zotero/skin/timeline/timeline.html'); + this.contentType = 'text/html'; - return extChannel; - } - catch (e){ - Zotero.debug(e); - throw (e); - } + if(!timelineDate){ + timelineDate=Date(); + var dateParts=timelineDate.toString().split(' '); + timelineDate=dateParts[1]+'.'+dateParts[2]+'.'+dateParts[3]; + } + Zotero.debug('='); + Zotero.debug(params.i); + Zotero.debug(intervals); + 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); + }); } }; @@ -748,84 +718,70 @@ function ChromeExtensionHandler() { /* zotero://attachment/[id]/ */ - var AttachmentExtension = new function() { - this.newChannel = newChannel; - - this.__defineGetter__('loadAsChrome', function () { return false; }); + var AttachmentExtension = { + loadAsChrome: false, - function newChannel(uri) { - var ioService = Components.classes["@mozilla.org/network/io-service;1"] - .getService(Components.interfaces.nsIIOService); - - var Zotero = Components.classes["@zotero.org/Zotero;1"] - .getService(Components.interfaces.nsISupports) - .wrappedJSObject; + newChannel: function (uri) { + var self = this; - try { - var errorMsg; - var [id, fileName] = uri.path.substr(1).split('/'); - - if (parseInt(id) != id) { - // Proxy annotation icons - if (id.match(/^annotation.*\.(png|html|css|gif)$/)) { - var chromeURL = 'chrome://zotero/skin/' + id; - var ios = Components.classes["@mozilla.org/network/io-service;1"]. - getService(Components.interfaces.nsIIOService); - var uri = ios.newURI(chromeURL, null, null); - var chromeReg = Components.classes["@mozilla.org/chrome/chrome-registry;1"] - .getService(Components.interfaces.nsIChromeRegistry); - var fileURI = chromeReg.convertChromeURL(uri); - } - else { - return _errorChannel("Attachment id not an integer"); - } - } - - if (!fileURI) { - var item = Zotero.Items.get(id); - if (!item) { - return _errorChannel("Item not found"); - } - var file = item.getFile(); - if (!file) { - return _errorChannel("File not found"); + return new AsyncChannel(uri, function* () { + try { + var errorMsg; + var [id, fileName] = uri.path.substr(1).split('/'); + + if (parseInt(id) != id) { + // Proxy annotation icons + if (id.match(/^annotation.*\.(png|html|css|gif)$/)) { + var chromeURL = 'chrome://zotero/skin/' + id; + var ios = Components.classes["@mozilla.org/network/io-service;1"]. + getService(Components.interfaces.nsIIOService); + let uri = ios.newURI(chromeURL, null, null); + var chromeReg = Components.classes["@mozilla.org/chrome/chrome-registry;1"] + .getService(Components.interfaces.nsIChromeRegistry); + var fileURI = chromeReg.convertChromeURL(uri); + } + else { + return self._errorChannel("Attachment id not an integer"); + } } - if (fileName) { - file = file.parent; - file.append(fileName); - if (!file.exists()) { - return _errorChannel("File not found"); + + if (!fileURI) { + var item = yield Zotero.Items.getAsync(id); + if (!item) { + return self._errorChannel("Item not found"); + } + var file = item.getFile(); + if (!file) { + return self._errorChannel("File not found"); + } + if (fileName) { + file = file.parent; + file.append(fileName); + if (!file.exists()) { + return self._errorChannel("File 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 is not allowed + this.originalURI = uri; + + return file; } - - var ph = Components.classes["@mozilla.org/network/protocol;1?name=file"]. - createInstance(Components.interfaces.nsIFileProtocolHandler); - if (!fileURI) { - var fileURI = ph.newFileURI(file); + catch (e) { + Zotero.debug(e); + throw (e); } - var channel = ioService.newChannelFromURI(fileURI); - //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 is not allowed - channel.originalURI = uri; - - return channel; - } - catch (e) { - Zotero.debug(e); - throw (e); - } - } + }); + }, - function _errorChannel(msg) { - var ioService = Components.classes["@mozilla.org/network/io-service;1"] - .getService(Components.interfaces.nsIIOService); - var uriStr = 'data:text/plain,' + encodeURIComponent(msg); - var dataURI = ioService.newURI(uriStr, null, null); - var channel = ioService.newChannelFromURI(dataURI); - return channel; + _errorChannel: function (msg) { + this.status = Components.results.NS_ERROR_FAILURE; + this.contentType = 'text/plain'; + return msg; } }; @@ -834,84 +790,73 @@ function ChromeExtensionHandler() { * zotero://select/[type]/0_ABCD1234 * zotero://select/[type]/1234 (not consistent across synced machines) */ - var SelectExtension = new function(){ - this.newChannel = newChannel; - - function newChannel(uri) { - var ioService = Components.classes["@mozilla.org/network/io-service;1"] - .getService(Components.interfaces.nsIIOService); - - var Zotero = Components.classes["@zotero.org/Zotero;1"] - .getService(Components.interfaces.nsISupports) - .wrappedJSObject; - - generateContent:try { - var mimeType, content = ''; - - var [path, queryString] = uri.path.substr(1).split('?'); - var [type, id] = path.split('/'); - - // currently only able to select one item - var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] - .getService(Components.interfaces.nsIWindowMediator); - var win = wm.getMostRecentWindow("navigator:browser"); - - // restore window if it's in the dock - if(win.windowState == Components.interfaces.nsIDOMChromeWindow.STATE_MINIMIZED) { - win.restore(); - } - - // open Zotero pane - win.ZoteroPane.show(); - - if(!id) return; - - var lkh = Zotero.Items.parseLibraryKeyHash(id); - if (lkh) { - var item = Zotero.Items.getByLibraryAndKey(lkh.libraryID, lkh.key); - } - else { - var item = Zotero.Items.get(id); + var SelectExtension = { + newChannel: function (uri) { + return new AsyncChannel(uri, function* () { + generateContent:try { + var mimeType, content = ''; + + var [path, queryString] = uri.path.substr(1).split('?'); + var [type, id] = path.split('/'); + + // currently only able to select one item + var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator); + var win = wm.getMostRecentWindow("navigator:browser"); + + // restore window if it's in the dock + if(win.windowState == Components.interfaces.nsIDOMChromeWindow.STATE_MINIMIZED) { + win.restore(); + } + + // open Zotero pane + win.ZoteroPane.show(); + + if(!id) return; + + var lkh = Zotero.Items.parseLibraryKeyHash(id); + if (lkh) { + var item = Zotero.Items.getByLibraryAndKey(lkh.libraryID, lkh.key); + } + else { + var item = Zotero.Items.get(id); + } + if (!item) { + var msg = "Item " + id + " not found in zotero://select"; + Zotero.debug(msg, 2); + Components.utils.reportError(msg); + return; + } + + win.ZoteroPane.selectItem(item.id); } - if (!item) { - var msg = "Item " + id + " not found in zotero://select"; - Zotero.debug(msg, 2); - Components.utils.reportError(msg); - return; + catch (e){ + Zotero.debug(e); + throw (e); } - - win.ZoteroPane.selectItem(item.id); - } - catch (e){ - Zotero.debug(e); - throw (e); - } + }); } }; /* zotero://fullscreen */ - var FullscreenExtension = new function() { - this.newChannel = newChannel; - - this.__defineGetter__('loadAsChrome', function () { return false; }); + var FullscreenExtension = { + loadAsChrome: false, - function newChannel(uri) { - var Zotero = Components.classes["@zotero.org/Zotero;1"] - .getService(Components.interfaces.nsISupports) - .wrappedJSObject; - - generateContent: try { - var window = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] - .getService(Components.interfaces.nsIWindowWatcher) - .openWindow(null, 'chrome://zotero/content/standalone/standalone.xul', '', - 'chrome,centerscreen,resizable', null); - } - catch (e) { - Zotero.debug(e); - throw (e); - } + newChannel: function (uri) { + return new AsyncChannel(uri, function* () { + try { + var window = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] + .getService(Components.interfaces.nsIWindowWatcher) + .openWindow(null, 'chrome://zotero/content/standalone/standalone.xul', '', + 'chrome,centerscreen,resizable', null); + } + catch (e) { + Zotero.debug(e, 1); + throw e; + } + }); } }; @@ -919,30 +864,22 @@ function ChromeExtensionHandler() { /* zotero://debug/ */ - var DebugExtension = new function() { - this.newChannel = newChannel; + var DebugExtension = { + loadAsChrome: false, - this.__defineGetter__('loadAsChrome', function () { return false; }); - - function newChannel(uri) { - var ioService = Components.classes["@mozilla.org/network/io-service;1"] - .getService(Components.interfaces.nsIIOService); - - var Zotero = Components.classes["@zotero.org/Zotero;1"] - .getService(Components.interfaces.nsISupports) - .wrappedJSObject; - - try { - var output = Zotero.Debug.get(); + newChannel: function () { + return new AsyncChannel(uri, function* () { + var ioService = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); - var uriStr = 'data:text/plain,' + encodeURIComponent(output); - var extURI = ioService.newURI(uriStr, null, null); - return ioService.newChannelFromURI(extURI); - } - catch (e) { - Zotero.debug(e); - throw (e); - } + try { + return Zotero.Debug.get(); + } + catch (e) { + Zotero.debug(e, 1); + throw e; + } + }); } }; @@ -1042,33 +979,21 @@ function ChromeExtensionHandler() { } }; - var ReportExtensionSpec = ZOTERO_SCHEME + "://report" - this._extensions[ReportExtensionSpec] = ReportExtension; - - var TimelineExtensionSpec = ZOTERO_SCHEME + "://timeline" - this._extensions[TimelineExtensionSpec] = TimelineExtension; - - var AttachmentExtensionSpec = ZOTERO_SCHEME + "://attachment" - this._extensions[AttachmentExtensionSpec] = AttachmentExtension; - - var SelectExtensionSpec = ZOTERO_SCHEME + "://select" - this._extensions[SelectExtensionSpec] = SelectExtension; - - var FullscreenExtensionSpec = ZOTERO_SCHEME + "://fullscreen" - this._extensions[FullscreenExtensionSpec] = FullscreenExtension; - - var DebugExtensionSpec = ZOTERO_SCHEME + "://debug" - this._extensions[DebugExtensionSpec] = DebugExtension; - - var ConnectorExtensionSpec = ZOTERO_SCHEME + "://connector" - this._extensions[ConnectorExtensionSpec] = ConnectorExtension; + this._extensions[ZOTERO_SCHEME + "://data"] = DataExtension; + this._extensions[ZOTERO_SCHEME + "://report"] = ReportExtension; + this._extensions[ZOTERO_SCHEME + "://timeline"] = TimelineExtension; + this._extensions[ZOTERO_SCHEME + "://attachment"] = AttachmentExtension; + this._extensions[ZOTERO_SCHEME + "://select"] = SelectExtension; + this._extensions[ZOTERO_SCHEME + "://fullscreen"] = FullscreenExtension; + this._extensions[ZOTERO_SCHEME + "://debug"] = DebugExtension; + this._extensions[ZOTERO_SCHEME + "://connector"] = ConnectorExtension; } /* * Implements nsIProtocolHandler */ -ChromeExtensionHandler.prototype = { +ZoteroProtocolHandler.prototype = { scheme: ZOTERO_SCHEME, defaultPort : -1, @@ -1110,16 +1035,18 @@ ChromeExtensionHandler.prototype = { var ext = this._extensions[extSpec]; if (uriString.indexOf(extSpec) == 0) { - if (ext.loadAsChrome && this._systemPrincipal == null) { - var chromeURI = chromeService.newURI(DUMMY_CHROME_URL, null, null); - var chromeChannel = chromeService.newChannel(chromeURI); - - // Cache System Principal from chrome request - // so proxied pages load with chrome privileges - this._systemPrincipal = chromeChannel.owner; - - var chromeRequest = chromeChannel.QueryInterface(Components.interfaces.nsIRequest); - chromeRequest.cancel(0x804b0002); // BINDING_ABORTED + if (!this._principal) { + if (ext.loadAsChrome) { + var chromeURI = chromeService.newURI(DUMMY_CHROME_URL, null, null); + var chromeChannel = chromeService.newChannel(chromeURI); + + // Cache System Principal from chrome request + // so proxied pages load with chrome privileges + this._principal = chromeChannel.owner; + + var chromeRequest = chromeChannel.QueryInterface(Components.interfaces.nsIRequest); + chromeRequest.cancel(0x804b0002); // BINDING_ABORTED + } } var extChannel = ext.newChannel(uri); @@ -1131,9 +1058,9 @@ ChromeExtensionHandler.prototype = { chromeRequest.cancel(0x804b0002); // BINDING_ABORTED } - // Apply cached system principal to extension channel - if (ext.loadAsChrome) { - extChannel.owner = this._systemPrincipal; + // Apply cached principal to extension channel + if (this._principal) { + extChannel.owner = this._principal; } if(!extChannel.originalURI) extChannel.originalURI = uri; @@ -1142,7 +1069,7 @@ ChromeExtensionHandler.prototype = { } } - // pass request through to ChromeProtocolHandler::newChannel + // pass request through to ZoteroProtocolHandler::newChannel if (uriString.indexOf("chrome") != 0) { uriString = uri.spec; uriString = "chrome" + uriString.substring(uriString.indexOf(":")); @@ -1152,6 +1079,8 @@ ChromeExtensionHandler.prototype = { newChannel = chromeService.newChannel(uri); } catch (e) { + Components.utils.reportError(e); + Zotero.debug(e, 1); throw Components.results.NS_ERROR_FAILURE; } @@ -1165,16 +1094,200 @@ ChromeExtensionHandler.prototype = { Components.interfaces.nsIProtocolHandler]) }; -// -// XPCOM goop -// /** -* XPCOMUtils.generateNSGetFactory was introduced in Mozilla 2 (Firefox 4). -* XPCOMUtils.generateNSGetModule is for Mozilla 1.9.2 (Firefox 3.6). -*/ -if (XPCOMUtils.generateNSGetFactory) { - var NSGetFactory = XPCOMUtils.generateNSGetFactory([ChromeExtensionHandler]); -} else { - var NSGetModule = XPCOMUtils.generateNSGetModule([ChromeExtensionHandler]); -} -\ No newline at end of file + * 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: 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; + + let channel = this; + + // Proxy requests to other zotero:// URIs + let uri2 = this.URI.clone(); + if (uri2.path.startsWith('/proxy/')) { + let re = new RegExp(uri2.scheme + '://' + uri2.host + '/proxy/([^/]+)(.*)'); + let matches = uri2.spec.match(re); + uri2.spec = uri2.scheme + '://' + matches[1] + '/' + (matches[2] ? matches[2] : ''); + var data = Zotero.File.getContentsFromURL(uri2.spec); + } + Zotero.Promise.try(function () { + return data ? data : Zotero.spawn(channel._generator, channel); + }) + .then(function (data) { + 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, data.length); + + listenerWrapper.onStopRequest(this, context, this.status); + return promise; + } + // 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); + pump.init(data, -1, -1, 0, 0, true); + pump.asyncRead(listenerWrapper, context); + return promise; + } + else if (data instanceof Ci.nsIFile || data instanceof Ci.nsIURI) { + if (data instanceof Ci.nsIFile) { + Zotero.debug("AsyncChannel: Got file from generator"); + data = ioService.newFileURI(data); + } + else { + Zotero.debug("AsyncChannel: Got URI from generator"); + } + + let uri = data; + uri.QueryInterface(Ci.nsIURL); + this.contentType = Zotero.MIME.getMIMETypeFromExtension(uri.fileExtension); + + Components.utils.import("resource://gre/modules/NetUtil.jsm"); + NetUtil.asyncFetch(data, 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); + }); + return promise; + } + else { + throw new Error("Invalid return type (" + typeof data + ") from generator passed to AsyncChannel"); + } + }.bind(this)) + .then(function () { + Zotero.debug("AsyncChannel request succeeded in " + (new Date - t) + " ms"); + channel._isPending = false; + }) + .catch(function (e) { + Zotero.debug(e, 1); + if (channel._isPending) { + streamListener.onStopRequest(channel, context, Components.results.NS_ERROR_FAILURE); + channel._isPending = false; + } + throw e; + }) + .finally(function () { + 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]); diff --git a/components/zotero-service.js b/components/zotero-service.js @@ -56,6 +56,7 @@ const xpcomFilesLocal = [ 'libraryTreeView', 'collectionTreeView', 'annotate', + 'api', 'attachments', 'cite', 'cookieSandbox', @@ -89,6 +90,7 @@ const xpcomFilesLocal = [ 'proxy', 'quickCopy', 'report', + 'router', 'schema', 'search', 'server', diff --git a/resource/pathparser.js b/resource/pathparser.js @@ -0,0 +1,119 @@ +/** + * pathparser.js - tiny URL parser/router + * + * Copyright (c) 2014 Dan Stillman + * License: MIT + * https://github.com/dstillman/pathparser.js + */ +(function (factory) { + // AMD/RequireJS + if (typeof define === 'function' && define.amd) { + define(factory); + // CommonJS/Node + } else if (typeof exports === 'object') { + module.exports = factory(); + // Mozilla JSM + } else if (~String(this).indexOf('BackstagePass')) { + EXPORTED_SYMBOLS = ["PathParser"]; + PathParser = factory(); + // Browser global + } else { + PathParser = factory(); + } +}(function () { + "use strict"; + + var PathParser = function (params) { + this.rules = []; + this.params = params; + } + + PathParser.prototype = (function () { + function getParamsFromRule(rule, pathParts, queryParts) { + var params = {}; + var missingParams = {}; + + // Parse path components + for (var i = 0; i < rule.parts.length; i++) { + var rulePart = rule.parts[i]; + var part = pathParts[i]; + + if (part !== undefined) { + if (rulePart.charAt(0) == ':') { + params[rulePart.substr(1)] = part; + continue; + } + else if (rulePart !== part) { + return false; + } + } + else if (rulePart.charAt(0) != ':') { + return false; + } + else { + missingParams[rulePart.substr(1)] = true; + } + } + + // Parse query strings + for (var i = 0; i < queryParts.length; ++i) { + var nameValue = queryParts[i].split('=', 2); + var key = nameValue[0]; + // But ignore empty parameters and don't override named parameters + if (nameValue.length == 2 && !params[key] && !missingParams[key]) { + params[key] = nameValue[1]; + } + } + + return params; + } + + return { + add: function (route, handler, autoPopulateOnMatch) { + this.rules.push({ + parts: route.replace(/^\//, '').split('/'), + handler: handler, + autoPopulateOnMatch: autoPopulateOnMatch === undefined || autoPopulateOnMatch + }); + }, + + run: function (url) { + if (url && url.length) { + url = url + // Remove redundant slashes + .replace(/\/+/g, '/') + // Strip leading and trailing '/' (at end or before query string) + .replace(/^\/|\/($|\?)/, '') + // Strip fragment identifiers + .replace(/#.*$/, ''); + } + + var urlSplit = url.split('?', 2); + var pathParts = urlSplit[0].split('/', 50); + var queryParts = urlSplit[1] ? urlSplit[1].split('&', 50) : []; + + for (var i=0; i < this.rules.length; i++) { + var rule = this.rules[i]; + var params = getParamsFromRule(rule, pathParts, queryParts); + if (params) { + params.url = url; + // Automatic parameter assignment + if (rule.autoPopulateOnMatch && this.params) { + for (var param in params) { + this.params[param] = params[param]; + } + } + // Call handler with 'this' bound to parameter object + if (rule.handler) { + rule.handler.call(params); + } + return true; + } + } + return false; + } + }; + })(); + + return PathParser; +})); +\ No newline at end of file