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:
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
// isn't valid in HTML
.replace(/ /g, " ")
// 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