From 755ead21194f57d11f00e80754d501ac0158bfb6 Mon Sep 17 00:00:00 2001 From: Dan Stillman Date: Mon, 8 Sep 2014 16:51:05 -0400 Subject: [PATCH] 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() --- chrome/content/zotero/advancedSearch.js | 7 +- .../content/zotero/bindings/zoterosearch.xml | 5 +- chrome/content/zotero/reportInterface.js | 64 +- chrome/content/zotero/timelineInterface.js | 28 +- chrome/content/zotero/xpcom/api.js | 191 +++ .../content/zotero/xpcom/data/collection.js | 3 +- .../content/zotero/xpcom/data/collections.js | 2 +- .../zotero/xpcom/data/dataObjectUtilities.js | 6 + .../content/zotero/xpcom/data/dataObjects.js | 28 +- chrome/content/zotero/xpcom/data/item.js | 64 +- chrome/content/zotero/xpcom/data/items.js | 64 +- chrome/content/zotero/xpcom/data/tags.js | 2 +- chrome/content/zotero/xpcom/date.js | 23 + chrome/content/zotero/xpcom/file.js | 4 +- chrome/content/zotero/xpcom/report.js | 277 ++-- chrome/content/zotero/xpcom/router.js | 25 + chrome/content/zotero/xpcom/search.js | 77 +- chrome/content/zotero/xpcom/timeline.js | 28 +- chrome/content/zotero/xpcom/users.js | 101 ++ .../zotero/xpcom/utilities_internal.js | 117 ++ chrome/content/zotero/zoteroPane.js | 20 +- .../default/zotero/timeline/timeline.html | 2 +- .../zotero/timeline/timelineControls.js | 25 +- components/zotero-protocol-handler.js | 1347 +++++++++-------- components/zotero-service.js | 2 + resource/pathparser.js | 119 ++ 26 files changed, 1731 insertions(+), 900 deletions(-) create mode 100644 chrome/content/zotero/xpcom/api.js create mode 100644 chrome/content/zotero/xpcom/router.js create mode 100644 chrome/content/zotero/xpcom/users.js create mode 100644 resource/pathparser.js diff --git a/chrome/content/zotero/advancedSearch.js b/chrome/content/zotero/advancedSearch.js index e614f771ab..00ae023c26 100644 --- 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 index 224996d61c..84069bca26 100644 --- 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; + } ]]> diff --git a/chrome/content/zotero/reportInterface.js b/chrome/content/zotero/reportInterface.js index 1bcb3f245a..dc5b834553 100644 --- 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'); + + var url = 'zotero://report/'; + + var source = ZoteroPane_Local.getSelectedCollection(); + if (!source) { + source = ZoteroPane_Local.getSelectedSavedSearch(); + } + if (!source) { + throw new Error('No collection currently selected'); } - if (col) { - ZoteroPane_Local.loadURI('zotero://report/collection/' - + Zotero.Collections.getLibraryKeyHash(col) - + '/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; } - var s = ZoteroPane_Local.getSelectedSavedSearch(); - if (s) { - ZoteroPane_Local.loadURI('zotero://report/search/' - + Zotero.Searches.getLibraryKeyHash(s) - + '/html/report.html' + queryString, event); - return; - } + url += '/items/report.html' + queryString; - throw ('No collection currently selected'); + 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 index 94b113f75f..d2a6151e4d 100644 --- 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(); - if (col) { - ZoteroPane_Local.loadURI(uri + 'collection/' + Zotero.Collections.getLibraryKeyHash(col)); - return; + uri += Zotero.API.getLibraryPrefix(col.libraryID) + '/collections/' + col.key; } - - var s = ZoteroPane_Local.getSelectedSavedSearch(); - if (s) { - ZoteroPane_Local.loadURI(uri + 'search/' + Zotero.Searches.getLibraryKeyHash(s)); - 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); + } + } } - - var l = ZoteroPane_Local.getSelectedLibraryID(); - if (l) { - ZoteroPane_Local.loadURI(uri + 'library/' + l); - return; - } - ZoteroPane_Local.loadURI(uri); } } diff --git a/chrome/content/zotero/xpcom/api.js b/chrome/content/zotero/xpcom/api.js new file mode 100644 index 0000000000..c3225dc7b2 --- /dev/null +++ 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 . + + ***** 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}. + */ + 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 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 index 61b0d14aa7..394a979251 100644 --- 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 index c92721c9c4..c41b5eec50 100644 --- 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 index 3605d715fb..38ab4f856c 100644 --- 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 index 57a7df2c93..e6e2797625 100644 --- 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 = '\n'; - content += '\n'; - content += '\n'; - content += '' + Zotero.getString('report.title.default') + '\n'; - content += '\n'; - content += '\n'; - content += '\n'; - content += '\n\n\n'; +Zotero.Report = {}; + +Zotero.Report.HTML = new function () { + this.listGenerator = function* (items, combineChildItems) { + yield '\n' + + '\n' + + ' \n' + + ' \n' + + ' ' + Zotero.getString('report.title.default') + '\n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + ' \n' + + '
    '; - content += '
      \n'; - for each(var arr in items) { - content += '\n
    • \n'; + for (let i=0; i\n'; + + if (obj.title) { // Top-level item matched search, so display title - if (arr.reportSearchMatch) { - content += '

      ' + escapeXML(arr.title) + '

      \n'; + if (obj.reportSearchMatch) { + content += '\t\t\t

      ' + escapeXML(obj.title) + '

      \n'; } // Non-matching parent, so display "Parent Item: [Title]" else { - content += '

      ' + escapeXML(Zotero.getString('report.parentItem')) - + ' ' + escapeXML(arr.title) + '

      '; + content += '\t\t\t

      ' + escapeXML(Zotero.getString('report.parentItem')) + + ' ' + escapeXML(obj.title) + '

      \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('
      ' - + 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() { + '
      ', "application/xml"); if (doc.documentElement.tagName == 'parsererror') { Zotero.debug(doc.documentElement.textContent, 2); - content += '

      ' + escapeXML(arr.note) + '

      \n'; + content += '

      ' + escapeXML(obj.note) + '

      \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 += '

      ' + escapeXML(Zotero.getString('report.notes')) + '

      \n'; + if (obj.reportSearchMatch) { + content += '\t\t\t\t

      ' + escapeXML(Zotero.getString('report.notes')) + '

      \n'; } - content += '
        \n'; - for each(var note in arr.reportChildren.notes) { - content += '
      • \n'; + content += '\t\t\t\t
          \n'; + for each(var note in obj.reportChildren.notes) { + content += '\t\t\t\t\t
        • \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 += '
        • \n'; + content += '\t\t\t\t\t\n'; } - content += '
        \n'; + content += '\t\t\t\t
      \n'; } // Chid attachments - content += _generateAttachmentsList(arr.reportChildren); + content += _generateAttachmentsList(obj.reportChildren); } - // Related - if (arr.reportSearchMatch && arr.related && arr.related.length) { - content += '\n'; - content += '\n'; } - content += '
    • \n\n'; + content += '\t\t\t\n\n'; + + yield content; } - content += '
    \n'; - content += '\n'; - return content; - } + yield '\t\t
\n\t\n'; + }; - function generateHTMLList(items) { - - } - - - function _generateMetadataTable(arr) { + function _generateMetadataTable(obj) { var table = false; - var content = '\n'; + var content = '\t\t\t\t
\n'; // Item type - content += '\n'; - content += '\n'; + content += '\t\t\t\t\t\t\n'; - content += '\n'; - content += '\n'; + content += '\t\t\t\t\t\t\n'; + content += '\t\t\t\t\t\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 += '\n'; - content += '\n'; + content += '\t\t\t\t\t\t\n'; - content += '\n'; - content += '\n'; + content += '\t\t\t\t\t\t\n'; + content += '\t\t\t\t\t\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 = '' - + escapeXML(arr[i]) + ''; + if (i == 'url' && obj[i].match(/^https?:\/\//)) { + fieldText = '' + escapeXML(obj[i]) + ''; } // Hyperlink DOI else if (i == 'DOI') { - fieldText = '' - + escapeXML(arr[i]) + ''; + fieldText = '' + + escapeXML(obj[i]) + ''; } // 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 += '\n\n\n\n'; + content += '\t\t\t\t\t\n\t\t\t\t\t\n\t\t\t\t\t\t\n\t\t\t\t\t\n'; } - content += '
' + content += '\t\t\t\t\t
' + escapeXML(Zotero.getString('itemFields.itemType')) + '' + escapeXML(Zotero.ItemTypes.getLocalizedString(arr.itemType)) + '
' + escapeXML(Zotero.ItemTypes.getLocalizedString(obj.itemType)) + '
' + content += '\t\t\t\t\t
' + escapeXML(Zotero.getString('creatorTypes.' + creator.creatorType)) + '' + escapeXML(displayText) + '
' + escapeXML(displayText) + '
' + escapeXML(localizedFieldName) - + '' + fieldText + '
' + escapeXML(localizedFieldName) + + '' + fieldText + '
'; + content += '\t\t\t\t\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 += '

' + escapeXML(str) + '

\n'; - content += '
    \n'; - for each(var tag in arr.tags) { - content += '
  • ' + escapeXML(tag.fields.name) + '
  • \n'; + content += '\t\t\t\t

    ' + escapeXML(str) + '

    \n'; + content += '\t\t\t\t
      \n'; + for (let i=0; i\n'; } - content += '
    \n'; + content += '\t\t\t\t
\n'; } return content; } - function _generateAttachmentsList(arr) { + function _generateAttachmentsList(obj) { var content = ''; - if (arr.attachments && arr.attachments.length) { - content += '

' + escapeXML(Zotero.getString('itemFields.attachments')) + '

\n'; - content += '
    \n'; - for each(var attachment in arr.attachments) { - content += '
  • '; - content += escapeXML(attachment.title); + if (obj.attachments && obj.attachments.length) { + content += '\t\t\t\t

    ' + escapeXML(Zotero.getString('itemFields.attachments')) + '

    \n'; + content += '\t\t\t\t
      \n'; + for (let i=0; i'; + if (attachment.title !== undefined) { + content += escapeXML(attachment.title); + } // Attachment tags content += _generateTagsList(attachment); // Attachment note if (attachment.note) { - content += '
      '; + content += '\t\t\t\t\t\t
      '; if (attachment.note.substr(0, 1024).match(/]*>/)) { content += attachment.note + '\n'; } @@ -321,13 +324,19 @@ Zotero.Report = new function() { else { content += '

      ' + escapeXML(attachment.note) + '

      \n'; } - content += '
      '; + content += '\t\t\t\t\t
      '; } - content += '\n'; + content += '\t\t\t\t\t\n'; } - content += '
    \n'; + content += '\t\t\t\t
\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 new file mode 100644 index 0000000000..d24c2b84c2 --- /dev/null +++ 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 index c48093ded2..08c267519e 100644 --- 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 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 index 093b7ab537..ed473c9a00 100644 --- 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 = '\n'; - for each(var item in items) { + yield '\n'; + for (let i=0; i'; } - - function generateXMLList(items) { - } -} \ 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 new file mode 100644 index 0000000000..cfd3e74ab1 --- /dev/null +++ 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 . + + ***** 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 index adb31fc593..887874c362 100644 --- 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 index b85a512e4f..f75d11f597 100644 --- 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 index 30223fc4b5..20cd96234f 100644 --- 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 index d0cd1f18b9..9fde290085 100644 --- 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 index ce347c5f80..319fdadb4e 100644 --- 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; -function ChromeExtensionHandler() { +var ioService = Components.classes["@mozilla.org/network/io-service;1"] + .getService(Components.interfaces.nsIIOService); + +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; iError generating report'; + } + ); } - } - 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 parsed = router.run(path); + if (!parsed) { + this.contentType = 'text/html'; + return "URL could not be parsed"; + } + if (params.groupID) { + params.libraryID = Zotero.Groups.getLibraryIDFromGroupID(params.groupID); } - var pathParts = path.split('/'); - if (pathParts[0] != 'data') { - var [type, id] = pathParts; - } - else { - var [, type, id] = pathParts; - } + 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 = ''; - 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