www

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

commit 8f38b01712d8c3c04e2889ca6a3f4f77d07c81d6
parent 0769a84a0031db82de968a7fbf3db6ea590f1db9
Author: Dan Stillman <dstillman@zotero.org>
Date:   Tue,  4 Oct 2016 23:44:07 -0400

Move search code into separate files in xpcom/data

Diffstat:
Achrome/content/zotero/xpcom/data/search.js | 1594+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Achrome/content/zotero/xpcom/data/searchConditions.js | 690+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Achrome/content/zotero/xpcom/data/searches.js | 172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dchrome/content/zotero/xpcom/search.js | 2410-------------------------------------------------------------------------------
Mcomponents/zotero-service.js | 4+++-
5 files changed, 2459 insertions(+), 2411 deletions(-)

diff --git a/chrome/content/zotero/xpcom/data/search.js b/chrome/content/zotero/xpcom/data/search.js @@ -0,0 +1,1594 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2009 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.Search = function(params = {}) { + Zotero.Search._super.apply(this); + + this._name = null; + + this._scope = null; + this._scopeIncludeChildren = null; + this._sql = null; + this._sqlParams = false; + this._maxSearchConditionID = -1; + this._conditions = {}; + this._hasPrimaryConditions = false; + + Zotero.Utilities.assignProps(this, params, ['name', 'libraryID']); +} + +Zotero.extendClass(Zotero.DataObject, Zotero.Search); + +Zotero.Search.prototype._objectType = 'search'; +Zotero.Search.prototype._dataTypes = Zotero.Search._super.prototype._dataTypes.concat([ + 'conditions' +]); + +Zotero.Search.prototype.getID = function(){ + Zotero.debug('Zotero.Search.getName() is deprecated -- use Search.id'); + return this._id; +} + +Zotero.Search.prototype.getName = function() { + Zotero.debug('Zotero.Search.getName() is deprecated -- use Search.name'); + return this.name; +} + +Zotero.Search.prototype.setName = function(val) { + Zotero.debug('Zotero.Search.setName() is deprecated -- use Search.name'); + this.name = val; +} + +Zotero.defineProperty(Zotero.Search.prototype, 'id', { + get: function() this._get('id'), + set: function(val) this._set('id', val) +}); +Zotero.defineProperty(Zotero.Search.prototype, 'libraryID', { + get: function() this._get('libraryID'), + set: function(val) this._set('libraryID', val) +}); +Zotero.defineProperty(Zotero.Search.prototype, 'key', { + get: function() this._get('key'), + set: function(val) this._set('key', val) +}); +Zotero.defineProperty(Zotero.Search.prototype, 'name', { + get: function() this._get('name'), + set: function(val) this._set('name', val) +}); +Zotero.defineProperty(Zotero.Search.prototype, 'version', { + get: function() this._get('version'), + set: function(val) this._set('version', val) +}); +Zotero.defineProperty(Zotero.Search.prototype, 'synced', { + get: function() this._get('synced'), + set: function(val) this._set('synced', val) +}); +Zotero.defineProperty(Zotero.Search.prototype, 'conditions', { + get: function() this.getConditions() +}); +Zotero.defineProperty(Zotero.Search.prototype, '_canHaveParent', { + value: false +}); + + +Zotero.Search.prototype.loadFromRow = function (row) { + var primaryFields = this._ObjectsClass.primaryFields; + for (let i=0; i<primaryFields.length; i++) { + let col = primaryFields[i]; + try { + var val = row[col]; + } + catch (e) { + Zotero.debug('Skipping missing ' + this._objectType + ' field ' + col); + continue; + } + + switch (col) { + case this._ObjectsClass.idColumn: + col = 'id'; + break; + + // Integer + case 'libraryID': + val = parseInt(val); + break; + + // Integer or 0 + case 'version': + val = val ? parseInt(val) : 0; + break; + + // Boolean + case 'synced': + val = !!val; + break; + + default: + val = val || ''; + } + + this['_' + col] = val; + } + + this._loaded.primaryData = true; + this._clearChanged('primaryData'); + this._identified = true; +} + +Zotero.Search.prototype._initSave = Zotero.Promise.coroutine(function* (env) { + if (!this.name) { + throw new Error('Name not provided for saved search'); + } + return Zotero.Search._super.prototype._initSave.apply(this, arguments); +}); + +Zotero.Search.prototype._saveData = Zotero.Promise.coroutine(function* (env) { + var isNew = env.isNew; + var options = env.options; + + var searchID = this._id = this.id ? this.id : Zotero.ID.get('savedSearches'); + + env.sqlColumns.push( + 'savedSearchName' + ); + env.sqlValues.push( + { string: this.name } + ); + + if (isNew) { + env.sqlColumns.unshift('savedSearchID'); + env.sqlValues.unshift(searchID ? { int: searchID } : null); + + let placeholders = env.sqlColumns.map(function () '?').join(); + let sql = "INSERT INTO savedSearches (" + env.sqlColumns.join(', ') + ") " + + "VALUES (" + placeholders + ")"; + yield Zotero.DB.queryAsync(sql, env.sqlValues); + } + else { + let sql = 'UPDATE savedSearches SET ' + + env.sqlColumns.map(function (x) x + '=?').join(', ') + ' WHERE savedSearchID=?'; + env.sqlValues.push(searchID ? { int: searchID } : null); + yield Zotero.DB.queryAsync(sql, env.sqlValues); + } + + if (this._changed.conditions) { + if (!isNew) { + var sql = "DELETE FROM savedSearchConditions WHERE savedSearchID=?"; + yield Zotero.DB.queryAsync(sql, this.id); + } + + var i = 0; + var sql = "INSERT INTO savedSearchConditions " + + "(savedSearchID, searchConditionID, condition, operator, value, required) " + + "VALUES (?,?,?,?,?,?)"; + for (let id in this._conditions) { + let condition = this._conditions[id]; + + // Convert condition and mode to "condition[/mode]" + let conditionString = condition.mode ? + condition.condition + '/' + condition.mode : + condition.condition + + var sqlParams = [ + searchID, + i, + conditionString, + condition.operator ? condition.operator : null, + condition.value ? condition.value : null, + condition.required ? 1 : null + ]; + yield Zotero.DB.queryAsync(sql, sqlParams); + i++; + } + } +}); + +Zotero.Search.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) { + if (env.isNew) { + // Update library searches status + yield Zotero.Libraries.get(this.libraryID).updateSearches(); + + Zotero.Notifier.queue('add', 'search', this.id, env.notifierData, env.options.notifierQueue); + } + else if (!env.options.skipNotifier) { + Zotero.Notifier.queue('modify', 'search', this.id, env.notifierData, env.options.notifierQueue); + } + + if (env.isNew && Zotero.Libraries.isGroupLibrary(this.libraryID)) { + var groupID = Zotero.Groups.getGroupIDFromLibraryID(this.libraryID); + var group = yield Zotero.Groups.get(groupID); + group.clearSearchCache(); + } + + if (!env.skipCache) { + yield this.reload(); + // If new, there's no other data we don't have, so we can mark everything as loaded + if (env.isNew) { + this._markAllDataTypeLoadStates(true); + } + this._clearChanged(); + } + + return env.isNew ? this.id : true; +}); + + +Zotero.Search.prototype.clone = function (libraryID) { + var s = new Zotero.Search(); + s.libraryID = libraryID === undefined ? this.libraryID : libraryID; + + var conditions = this.getConditions(); + + for each(var condition in conditions) { + var name = condition.mode ? + condition.condition + '/' + condition.mode : + condition.condition + + s.addCondition(name, condition.operator, condition.value, + condition.required); + } + + return s; +}; + + +Zotero.Search.prototype._eraseData = Zotero.Promise.coroutine(function* (env) { + Zotero.DB.requireTransaction(); + + var sql = "DELETE FROM savedSearchConditions WHERE savedSearchID=?"; + yield Zotero.DB.queryAsync(sql, this.id); + + var sql = "DELETE FROM savedSearches WHERE savedSearchID=?"; + yield Zotero.DB.queryAsync(sql, this.id); +}); + +Zotero.Search.prototype._finalizeErase = Zotero.Promise.coroutine(function* (env) { + yield Zotero.Search._super.prototype._finalizeErase.call(this, env); + + // Update library searches status + yield Zotero.Libraries.get(this.libraryID).updateSearches(); +}); + + +Zotero.Search.prototype.addCondition = function (condition, operator, value, required) { + this._requireData('conditions'); + + if (!Zotero.SearchConditions.hasOperator(condition, operator)){ + let e = new Error("Invalid operator '" + operator + "' for condition " + condition); + e.name = "ZoteroUnknownFieldError"; + throw e; + } + + // Shortcut to add a condition on every table -- does not return an id + if (condition.match(/^quicksearch/)) { + var parts = Zotero.SearchConditions.parseSearchString(value); + + for each(var part in parts) { + this.addCondition('blockStart'); + + // If search string is 8 characters, see if this is a item key + if (operator == 'contains' && part.text.length == 8) { + this.addCondition('key', 'is', part.text, false); + } + + if (condition == 'quicksearch-titleCreatorYear') { + this.addCondition('title', operator, part.text, false); + this.addCondition('publicationTitle', operator, part.text, false); + this.addCondition('shortTitle', operator, part.text, false); + this.addCondition('court', operator, part.text, false); + this.addCondition('year', operator, part.text, false); + } + else { + this.addCondition('field', operator, part.text, false); + this.addCondition('tag', operator, part.text, false); + this.addCondition('note', operator, part.text, false); + } + this.addCondition('creator', operator, part.text, false); + + if (condition == 'quicksearch-everything') { + this.addCondition('annotation', operator, part.text, false); + + if (part.inQuotes) { + this.addCondition('fulltextContent', operator, part.text, false); + } + else { + var splits = Zotero.Fulltext.semanticSplitter(part.text); + for each(var split in splits) { + this.addCondition('fulltextWord', operator, split, false); + } + } + } + + this.addCondition('blockEnd'); + } + + if (condition == 'quicksearch-titleCreatorYear') { + this.addCondition('noChildren', 'true'); + } + + return false; + } + // Shortcut to add a collection (which must be loaded first) + else if (condition == 'collectionID') { + let {libraryID, key} = Zotero.Collections.getLibraryAndKeyFromID(value); + if (!key) { + let msg = "Collection " + value + " not found"; + Zotero.debug(msg, 2); + Components.utils.reportError(msg); + return; + } + if (this.libraryID && libraryID != this.libraryID) { + Zotero.logError(new Error("Collection " + value + " is in different library")); + return; + } + return this.addCondition('collection', operator, key, required); + } + // Shortcut to add a saved search (which must be loaded first) + else if (condition == 'savedSearchID') { + let {libraryID, key} = Zotero.Searches.getLibraryAndKeyFromID(value); + if (!key) { + let msg = "Saved search " + value + " not found"; + Zotero.debug(msg, 2); + Components.utils.reportError(msg); + return; + } + if (this.libraryID && libraryID != this.libraryID) { + Zotero.logError(new Error("Collection " + value + " is in different library")); + return; + } + return this.addCondition('savedSearch', operator, key, required); + } + + var searchConditionID = ++this._maxSearchConditionID; + + let mode; + [condition, mode] = Zotero.SearchConditions.parseCondition(condition); + + if (typeof value == 'string') value = value.normalize(); + + this._conditions[searchConditionID] = { + id: searchConditionID, + condition: condition, + mode: mode, + operator: operator, + value: value, + required: !!required + }; + + this._sql = null; + this._sqlParams = false; + this._markFieldChange('conditions', this._conditions); + this._changed.conditions = true; + + return searchConditionID; +} + + +/* + * Sets scope of search to the results of the passed Search object + */ +Zotero.Search.prototype.setScope = function (searchObj, includeChildren) { + this._scope = searchObj; + this._scopeIncludeChildren = includeChildren; +} + + +/** + * @param {Number} searchConditionID + * @param {String} condition + * @param {String} operator + * @param {String} value + * @param {Boolean} [required] + * @return {Promise} + */ +Zotero.Search.prototype.updateCondition = function (searchConditionID, condition, operator, value, required) { + this._requireData('conditions'); + + if (typeof this._conditions[searchConditionID] == 'undefined'){ + throw new Error('Invalid searchConditionID ' + searchConditionID); + } + + if (!Zotero.SearchConditions.hasOperator(condition, operator)){ + let e = new Error("Invalid operator '" + operator + "' for condition " + condition); + e.name = "ZoteroUnknownFieldError"; + throw e; + } + + var [condition, mode] = Zotero.SearchConditions.parseCondition(condition); + + if (typeof value == 'string') value = value.normalize(); + + this._conditions[searchConditionID] = { + id: parseInt(searchConditionID), + condition: condition, + mode: mode, + operator: operator, + value: value, + required: !!required + }; + + this._sql = null; + this._sqlParams = false; + this._markFieldChange('conditions', this._conditions); + this._changed.conditions = true; +} + + +Zotero.Search.prototype.removeCondition = function (searchConditionID) { + this._requireData('conditions'); + + if (typeof this._conditions[searchConditionID] == 'undefined'){ + throw new Error('Invalid searchConditionID ' + searchConditionID + ' in removeCondition()'); + } + + delete this._conditions[searchConditionID]; + this._maxSearchConditionID--; + this._markFieldChange('conditions', this._conditions); + this._changed.conditions = true; +} + + +/* + * Returns an array with 'condition', 'operator', 'value', 'required' + * for the given searchConditionID + */ +Zotero.Search.prototype.getCondition = function(searchConditionID){ + this._requireData('conditions'); + return this._conditions[searchConditionID]; +} + + +/* + * Returns an object of conditions/operator/value sets used in the search, + * indexed by searchConditionID + */ +Zotero.Search.prototype.getConditions = function(){ + this._requireData('conditions'); + var conditions = {}; + for (let id in this._conditions) { + let condition = this._conditions[id]; + conditions[id] = { + id: id, + condition: condition.condition, + mode: condition.mode, + operator: condition.operator, + value: condition.value, + required: condition.required + }; + } + return conditions; +} + + +Zotero.Search.prototype.hasPostSearchFilter = function() { + this._requireData('conditions'); + for each(var i in this._conditions){ + if (i.condition == 'fulltextContent'){ + return true; + } + } + return false; +} + + +/** + * Run the search and return an array of item ids for results + * + * @param {Boolean} [asTempTable=false] + * @return {Promise} + */ +Zotero.Search.prototype.search = Zotero.Promise.coroutine(function* (asTempTable) { + var tmpTable; + + // Mark conditions as loaded + // TODO: Necessary? + if (!this._identified) { + this._requireData('conditions'); + } + try { + if (!this._sql){ + yield this._buildQuery(); + } + + // Default to 'all' mode + var joinMode = 'all'; + + // Set some variables for conditions to avoid further lookups + for each(var condition in this._conditions) { + switch (condition.condition) { + case 'joinMode': + if (condition.operator == 'any') { + joinMode = 'any'; + } + break; + + case 'fulltextContent': + var fulltextContent = true; + break; + + case 'includeParentsAndChildren': + if (condition.operator == 'true') { + var includeParentsAndChildren = true; + } + break; + + case 'includeParents': + if (condition.operator == 'true') { + var includeParents = true; + } + break; + + case 'includeChildren': + if (condition.operator == 'true') { + var includeChildren = true; + } + break; + + case 'blockStart': + var hasQuicksearch = true; + break; + } + } + + // Run a subsearch to define the superset of possible results + if (this._scope) { + // If subsearch has post-search filter, run and insert ids into temp table + if (this._scope.hasPostSearchFilter()) { + var ids = yield this._scope.search(); + if (!ids) { + return []; + } + + Zotero.debug('g'); + Zotero.debug(ids); + tmpTable = yield Zotero.Search.idsToTempTable(ids); + } + // Otherwise, just copy to temp table directly + else { + tmpTable = "tmpSearchResults_" + Zotero.randomString(8); + var sql = "CREATE TEMPORARY TABLE " + tmpTable + " AS " + + (yield this._scope.getSQL()); + yield Zotero.DB.queryAsync(sql, yield this._scope.getSQLParams()); + var sql = "CREATE INDEX " + tmpTable + "_itemID ON " + tmpTable + "(itemID)"; + yield Zotero.DB.queryAsync(sql); + } + + // Search ids in temp table + var sql = "SELECT GROUP_CONCAT(itemID) FROM items WHERE itemID IN (" + this._sql + ") " + + "AND (" + + "itemID IN (SELECT itemID FROM " + tmpTable + ")"; + + if (this._scopeIncludeChildren) { + sql += " OR itemID IN (SELECT itemID FROM itemAttachments" + + " WHERE parentItemID IN (SELECT itemID FROM " + tmpTable + ")) OR " + + "itemID IN (SELECT itemID FROM itemNotes" + + " WHERE parentItemID IN (SELECT itemID FROM " + tmpTable + "))"; + } + sql += ")"; + + var res = yield Zotero.DB.valueQueryAsync(sql, this._sqlParams); + var ids = res ? res.split(",") : []; + /* + // DEBUG: Should this be here? + // + if (!ids) { + Zotero.DB.query("DROP TABLE " + tmpTable); + Zotero.DB.commitTransaction(); + return false; + } + */ + } + // Or just run main search + else { + var ids = yield Zotero.DB.columnQueryAsync(this._sql, this._sqlParams); + } + + //Zotero.debug('IDs from main search or subsearch: '); + //Zotero.debug(ids); + + //Zotero.debug('Join mode: ' + joinMode); + + // Filter results with fulltext search + // + // If join mode ALL, return the (intersection of main and fulltext word search) + // filtered by fulltext content + // + // If join mode ANY or there's a quicksearch (which we assume + // fulltextContent is part of), return the union of the main search and + // (a separate fulltext word search filtered by fulltext content) + for each(var condition in this._conditions){ + if (condition['condition']=='fulltextContent'){ + var fulltextWordIntersectionFilter = function (val, index, array) !!hash[val]; + var fulltextWordIntersectionConditionFilter = function(val, index, array) { + return hash[val] ? + (condition.operator == 'contains') : + (condition.operator == 'doesNotContain'); + }; + + // Regexp mode -- don't use fulltext word index + if (condition.mode && condition.mode.indexOf('regexp') == 0) { + // In an ANY search, only bother scanning items that + // haven't already been found by the main search + if (joinMode == 'any') { + if (!tmpTable) { + tmpTable = yield Zotero.Search.idsToTempTable(ids); + } + + var sql = "SELECT GROUP_CONCAT(itemID) FROM items WHERE " + + "itemID NOT IN (SELECT itemID FROM " + tmpTable + ")"; + var res = yield Zotero.DB.valueQueryAsync(sql); + var scopeIDs = res ? res.split(",") : []; + } + // If an ALL search, scan only items from the main search + else { + var scopeIDs = ids; + } + } + // If not regexp mode, run a new search against the fulltext word + // index for words in this phrase + else { + Zotero.debug('Running subsearch against fulltext word index'); + var s = new Zotero.Search(); + + // Add any necessary conditions to the fulltext word search -- + // those that are required in an ANY search and any outside the + // quicksearch in an ALL search + for each(var c in this._conditions) { + if (c.condition == 'blockStart') { + var inQS = true; + continue; + } + else if (c.condition == 'blockEnd') { + inQS = false; + continue; + } + else if (c.condition == 'fulltextContent' || inQS) { + continue; + } + else if (joinMode == 'any' && !c.required) { + continue; + } + s.addCondition(c.condition, c.operator, c.value); + } + + var splits = Zotero.Fulltext.semanticSplitter(condition.value); + for each(var split in splits){ + s.addCondition('fulltextWord', condition.operator, split); + } + var fulltextWordIDs = yield s.search(); + + //Zotero.debug("Fulltext word IDs"); + //Zotero.debug(fulltextWordIDs); + + // If ALL mode, set intersection of main search and fulltext word index + // as the scope for the fulltext content search + if (joinMode == 'all' && !hasQuicksearch) { + var hash = {}; + for (let i=0; i<fulltextWordIDs.length; i++) { + hash[fulltextWordIDs[i].id] = true; + } + + if (ids) { + var scopeIDs = ids.filter(fulltextWordIntersectionFilter); + } + else { + var scopeIDs = []; + } + } + // If ANY mode, just use fulltext word index hits for content search, + // since the main results will be added in below + else { + var scopeIDs = fulltextWordIDs; + } + } + + if (scopeIDs && scopeIDs.length) { + var fulltextIDs = yield Zotero.Fulltext.findTextInItems(scopeIDs, + condition['value'], condition['mode']); + + var hash = {}; + for (let i=0; i<fulltextIDs.length; i++) { + hash[fulltextIDs[i].id] = true; + } + + filteredIDs = scopeIDs.filter(fulltextWordIntersectionConditionFilter); + } + else { + var filteredIDs = []; + } + + //Zotero.debug("Filtered IDs:") + //Zotero.debug(filteredIDs); + + // If join mode ANY, add any new items from the fulltext content + // search to the main search results + // + // We only do this if there are primary conditions that alter the + // main search, since otherwise all items will match + if (this._hasPrimaryConditions && + (joinMode == 'any' || hasQuicksearch) && ids) { + //Zotero.debug("Adding filtered IDs to main set"); + for (let i=0; i<filteredIDs.length; i++) { + let id = filteredIDs[i]; + if (ids.indexOf(id) == -1) { + ids.push(id); + } + } + } + else { + //Zotero.debug("Replacing main set with filtered IDs"); + ids = filteredIDs; + } + } + } + + if (this.hasPostSearchFilter() && + (includeParentsAndChildren || includeParents || includeChildren)) { + Zotero.debug('b'); + Zotero.debug(ids); + var tmpTable = yield Zotero.Search.idsToTempTable(ids); + + if (includeParentsAndChildren || includeParents) { + //Zotero.debug("Adding parent items to result set"); + var sql = "SELECT parentItemID FROM itemAttachments " + + "WHERE itemID IN (SELECT itemID FROM " + tmpTable + ") " + + " AND parentItemID IS NOT NULL " + + "UNION SELECT parentItemID FROM itemNotes " + + "WHERE itemID IN (SELECT itemID FROM " + tmpTable + ")" + + " AND parentItemID IS NOT NULL"; + } + + if (includeParentsAndChildren || includeChildren) { + //Zotero.debug("Adding child items to result set"); + var childrenSQL = "SELECT itemID FROM itemAttachments WHERE " + + "parentItemID IN (SELECT itemID FROM " + tmpTable + ") UNION " + + "SELECT itemID FROM itemNotes WHERE parentItemID IN " + + "(SELECT itemID FROM " + tmpTable + ")"; + + if (includeParentsAndChildren || includeParents) { + sql += " UNION " + childrenSQL; + } + else { + sql = childrenSQL; + } + } + + sql = "SELECT GROUP_CONCAT(itemID) FROM items WHERE itemID IN (" + sql + ")"; + var res = yield Zotero.DB.valueQueryAsync(sql); + var parentChildIDs = res ? res.split(",") : []; + + // Add parents and children to main ids + if (parentChildIDs) { + for (var i=0; i<parentChildIDs.length; i++) { + var id = parentChildIDs[i]; + if (ids.indexOf(id) == -1) { + ids.push(id); + } + } + } + } + } + finally { + if (tmpTable && !asTempTable) { + yield Zotero.DB.queryAsync("DROP TABLE IF EXISTS " + tmpTable); + } + } + + //Zotero.debug('Final result set'); + //Zotero.debug(ids); + + if (!ids || !ids.length) { + return []; + } + + if (asTempTable) { + Zotero.debug('c'); + Zotero.debug(ids); + return Zotero.Search.idsToTempTable(ids); + } + return ids; +}); + + +/** + * Populate the object's data from an API JSON data object + * + * If this object is identified (has an id or library/key), loadAll() must have been called. + */ +Zotero.Search.prototype.fromJSON = function (json) { + if (!json.name) { + throw new Error("'name' property not provided for search"); + } + this.name = json.name; + + Object.keys(this.getConditions()).forEach(id => this.removeCondition(id)); + for (let i = 0; i < json.conditions.length; i++) { + let condition = json.conditions[i]; + this.addCondition( + condition.condition, + condition.operator, + condition.value + ); + } +} + + +Zotero.Search.prototype.toJSON = function (options = {}) { + var env = this._preToJSON(options); + var mode = env.mode; + + var obj = env.obj = {}; + obj.key = this.key; + obj.version = this.version; + obj.name = this.name; + var conditions = this.getConditions(); + obj.conditions = Object.keys(conditions) + .map(x => ({ + condition: conditions[x].condition + + (conditions[x].mode !== false ? "/" + conditions[x].mode : ""), + operator: conditions[x].operator, + // TODO: Change joinMode to use 'is' + 'any' instead of operator 'any'? + value: conditions[x].value ? conditions[x].value : "" + })); + return this._postToJSON(env); +} + + +/* + * Get the SQL string for the search + */ +Zotero.Search.prototype.getSQL = Zotero.Promise.coroutine(function* () { + if (!this._sql) { + yield this._buildQuery(); + } + return this._sql; +}); + + +Zotero.Search.prototype.getSQLParams = Zotero.Promise.coroutine(function* () { + if (!this._sql) { + yield this._buildQuery(); + } + return this._sqlParams; +}); + + +/* + * Batch insert + */ +Zotero.Search.idsToTempTable = function (ids) { + const N_COMBINED_INSERTS = 1000; + + var tmpTable = "tmpSearchResults_" + Zotero.randomString(8); + + return Zotero.DB.executeTransaction(function* () { + var sql = "CREATE TEMPORARY TABLE " + tmpTable + " (itemID INTEGER PRIMARY KEY)"; + yield Zotero.DB.queryAsync(sql); + + var ids2 = ids ? ids.concat() : []; + while (ids2.length) { + let chunk = ids2.splice(0, N_COMBINED_INSERTS); + let sql = 'INSERT INTO ' + tmpTable + ' VALUES ' + + chunk.map((x) => "(" + parseInt(x) + ")").join(", "); + yield Zotero.DB.queryAsync(sql, false, { debug: false }); + } + + return tmpTable; + }); +} + + +/* + * 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 + var anySQL = ''; + var anySQLParams = []; + + var conditions = []; + + for (var i in this._conditions){ + var data = Zotero.SearchConditions.get(this._conditions[i]['condition']); + + // Has a table (or 'savedSearch', which doesn't have a table but isn't special) + if (data.table || data.name == 'savedSearch' || data.name == 'tempTable') { + conditions.push({ + name: data['name'], + alias: data['name']!=this._conditions[i]['condition'] + ? this._conditions[i]['condition'] : false, + table: data['table'], + field: data['field'], + operator: this._conditions[i]['operator'], + value: this._conditions[i]['value'], + flags: data['flags'], + required: this._conditions[i]['required'] + }); + + this._hasPrimaryConditions = true; + } + + // Handle special conditions + else { + switch (data['name']){ + case 'deleted': + var deleted = this._conditions[i].operator == 'true'; + continue; + + case 'noChildren': + var noChildren = this._conditions[i]['operator']=='true'; + continue; + + case 'includeParentsAndChildren': + var includeParentsAndChildren = this._conditions[i]['operator'] == 'true'; + continue; + + case 'includeParents': + var includeParents = this._conditions[i]['operator'] == 'true'; + continue; + + case 'includeChildren': + var includeChildren = this._conditions[i]['operator'] == 'true'; + continue; + + case 'unfiled': + var unfiled = this._conditions[i]['operator'] == 'true'; + continue; + + // Search subcollections + case 'recursive': + var recursive = this._conditions[i]['operator']=='true'; + continue; + + // Join mode ('any' or 'all') + case 'joinMode': + var joinMode = this._conditions[i]['operator'].toUpperCase(); + continue; + + case 'fulltextContent': + // Handled in Search.search() + continue; + + // For quicksearch block markers + case 'blockStart': + conditions.push({name:'blockStart'}); + continue; + case 'blockEnd': + conditions.push({name:'blockEnd'}); + continue; + } + + throw ('Unhandled special condition ' + this._conditions[i]['condition']); + } + } + + // Exclude deleted items (and their child items) by default + let not = deleted ? "" : "NOT "; + let op = deleted ? "OR" : "AND"; + sql += " WHERE (" + + `itemID ${not} IN (SELECT itemID FROM deletedItems) ` + + `${op} itemID ${not}IN (SELECT itemID FROM itemNotes ` + + "WHERE parentItemID IS NOT NULL AND " + + "parentItemID IN (SELECT itemID FROM deletedItems)) " + + `${op} itemID ${not}IN (SELECT itemID FROM itemAttachments ` + + "WHERE parentItemID IS NOT NULL AND " + + "parentItemID IN (SELECT itemID FROM deletedItems))" + + ")"; + + if (noChildren){ + sql += " AND (itemID NOT IN (SELECT itemID FROM itemNotes " + + "WHERE parentItemID IS NOT NULL) AND itemID NOT IN " + + "(SELECT itemID FROM itemAttachments " + + "WHERE parentItemID IS NOT NULL))"; + } + + if (unfiled) { + sql += " AND (itemID NOT IN (SELECT itemID FROM collectionItems) " + // Exclude children + + "AND itemID NOT IN " + + "(SELECT itemID FROM itemAttachments WHERE parentItemID IS NOT NULL " + + "UNION SELECT itemID FROM itemNotes WHERE parentItemID IS NOT NULL)" + + ")"; + } + + // Limit to library search belongs to + // + // This is equivalent to adding libraryID as a search condition, + // but it works with ANY + if (this.libraryID !== null) { + sql += " AND (itemID IN (SELECT itemID FROM items WHERE libraryID=?))"; + sqlParams.push(this.libraryID); + } + + if (this._hasPrimaryConditions) { + sql += " AND "; + + for each(var condition in conditions){ + var skipOperators = false; + var openParens = 0; + var condSQL = ''; + var selectOpenParens = 0; + var condSelectSQL = ''; + var condSQLParams = []; + + // + // Special table handling + // + if (condition['table']){ + switch (condition['table']){ + default: + condSelectSQL += 'itemID ' + switch (condition['operator']){ + case 'isNot': + case 'doesNotContain': + condSelectSQL += 'NOT '; + break; + } + condSelectSQL += 'IN ('; + selectOpenParens = 1; + condSQL += 'SELECT itemID FROM ' + + condition['table'] + ' WHERE ('; + openParens = 1; + } + } + + // + // Special condition handling + // + switch (condition['name']){ + case 'field': + case 'datefield': + case 'numberfield': + if (condition['alias']) { + // Add base field + condSQLParams.push( + Zotero.ItemFields.getID(condition['alias']) + ); + var typeFields = Zotero.ItemFields.getTypeFieldsFromBase(condition['alias']); + if (typeFields) { + condSQL += 'fieldID IN (?,'; + // Add type-specific fields + for each(var fieldID in typeFields) { + condSQL += '?,'; + condSQLParams.push(fieldID); + } + condSQL = condSQL.substr(0, condSQL.length - 1); + condSQL += ') AND '; + } + else { + condSQL += 'fieldID=? AND '; + } + } + + condSQL += "valueID IN (SELECT valueID FROM " + + "itemDataValues WHERE "; + + openParens++; + break; + + case 'year': + condSQLParams.push(Zotero.ItemFields.getID('date')); + //Add base field + var dateFields = Zotero.ItemFields.getTypeFieldsFromBase('date'); + if (dateFields) { + condSQL += 'fieldID IN (?,'; + // Add type-specific date fields (dateEnacted, dateDecided, issueDate) + for each(var fieldID in dateFields) { + condSQL += '?,'; + condSQLParams.push(fieldID); + } + condSQL = condSQL.substr(0, condSQL.length - 1); + condSQL += ') AND '; + } + + condSQL += "valueID IN (SELECT valueID FROM " + + "itemDataValues WHERE "; + + openParens++; + break; + + case 'collection': + case 'savedSearch': + let obj; + let objLibraryID; + let objKey = condition.value; + let objectType = condition.name == 'collection' ? 'collection' : 'search'; + let objectTypeClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType); + + // Old-style library-key hash + if (objKey.indexOf('_') != -1) { + [objLibraryID, objKey] = objKey.split('_'); + } + // libraryID assigned on search + else if (this.libraryID !== null) { + objLibraryID = this.libraryID; + } + + // If search doesn't have a libraryID, check all possible libraries + // for the collection/search + if (objLibraryID === undefined) { + let foundLibraryID = false; + for each (let c in this._conditions) { + if (c.condition == 'libraryID' && c.operator == 'is') { + foundLibraryID = true; + obj = yield objectTypeClass.getByLibraryAndKeyAsync( + c.value, objKey + ); + if (obj) { + break; + } + } + } + if (!foundLibraryID) { + Zotero.debug("WARNING: libraryID condition not found for " + + objectType + " in search", 2); + } + } + else { + obj = yield objectTypeClass.getByLibraryAndKeyAsync( + objLibraryID, objKey + ); + } + if (!obj) { + var msg = objectType.charAt(0).toUpperCase() + objectType.substr(1) + + " " + objKey + " specified in search not found"; + Zotero.debug(msg, 2); + Zotero.log(msg, 'warning', 'chrome://zotero/content/xpcom/search.js'); + if (objectType == 'search') { + continue; + } + obj = { + id: 0 + }; + } + + if (objectType == 'collection') { + var q = ['?']; + var p = [obj.id]; + + // Search descendent collections if recursive search + if (recursive){ + var descendents = obj.getDescendents(false, 'collection'); + for (let d of descendents) { + q.push('?'); + p.push(d.id); + } + } + + condSQL += "collectionID IN (" + q.join() + ")"; + condSQLParams = condSQLParams.concat(p); + } + else { + // Check if there are any post-search filters + var hasFilter = search.hasPostSearchFilter(); + + // This is an ugly and inefficient way of doing a + // subsearch, but it's necessary if there are any + // post-search filters (e.g. fulltext scanning) in the + // subsearch + // + // DEBUG: it's possible there's a query length limit here + // or that this slows things down with large libraries + // -- should probably use a temporary table instead + if (hasFilter){ + let subids = yield search.search(); + condSQL += subids.join(); + } + // Otherwise just put the SQL in a subquery + else { + condSQL += yield search.getSQL(); + let subpar = yield search.getSQLParams(); + for (let k in subpar){ + condSQLParams.push(subpar[k]); + } + } + condSQL += ")"; + } + + skipOperators = true; + break; + + case 'itemType': + condSQL += "itemTypeID IN (SELECT itemTypeID FROM itemTypesCombined WHERE "; + openParens++; + break; + + case 'fileTypeID': + var ftSQL = 'SELECT mimeType FROM fileTypeMimeTypes ' + + 'WHERE fileTypeID IN (' + + 'SELECT fileTypeID FROM fileTypes WHERE ' + + 'fileTypeID=?)'; + var patterns = yield Zotero.DB.columnQueryAsync(ftSQL, { int: condition.value }); + if (patterns) { + for each(str in patterns) { + condSQL += 'contentType LIKE ? OR '; + condSQLParams.push(str + '%'); + } + condSQL = condSQL.substring(0, condSQL.length - 4); + } + else { + throw ("Invalid fileTypeID '" + condition.value + "' specified in search.js") + } + skipOperators = true; + break; + + case 'tag': + condSQL += "tagID IN (SELECT tagID FROM tags WHERE "; + openParens++; + break; + + case 'creator': + case 'lastName': + condSQL += "creatorID IN (SELECT creatorID FROM creators WHERE "; + openParens++; + break; + + case 'childNote': + condSQL += "itemID IN (SELECT parentItemID FROM " + + "itemNotes WHERE "; + openParens++; + break; + + case 'fulltextWord': + condSQL += "wordID IN (SELECT wordID FROM fulltextWords " + + "WHERE "; + openParens++; + break; + + case 'tempTable': + if (!condition.value.match(/^[a-zA-Z0-9]+$/)) { + throw ("Invalid temp table '" + condition.value + "'"); + } + condSQL += "itemID IN (SELECT id FROM " + condition.value + ")"; + skipOperators = true; + break; + + // For quicksearch blocks + case 'blockStart': + case 'blockEnd': + skipOperators = true; + break; + } + + if (!skipOperators){ + // Special handling for date fields + // + // Note: We assume full datetimes are already UTC and don't + // need to be handled specially + if ((condition['name']=='dateAdded' || + condition['name']=='dateModified' || + condition['name']=='datefield') && + !Zotero.Date.isSQLDateTime(condition['value'])){ + + // TODO: document these flags + var parseDate = null; + var alt = null; + var useFreeform = null; + + switch (condition['operator']){ + case 'is': + case 'isNot': + var parseDate = true; + var alt = '__'; + var useFreeform = true; + break; + + case 'isBefore': + var parseDate = true; + var alt = '00'; + var useFreeform = false; + break; + + case 'isAfter': + var parseDate = true; + // '__' used here just so the > string comparison + // doesn't match dates in the specified year + var alt = '__'; + var useFreeform = false; + break; + + case 'isInTheLast': + var parseDate = false; + break; + + default: + throw ('Invalid date field operator in search'); + } + + // Convert stored UTC dates to localtime + // + // It'd be nice not to deal with time zones here at all, + // but otherwise searching for the date part of a field + // stored as UTC that wraps midnight would be unsuccessful + if (condition['name']=='dateAdded' || + condition['name']=='dateModified' || + condition['alias']=='accessDate'){ + condSQL += "DATE(" + condition['field'] + ", 'localtime')"; + } + // Only use first (SQL) part of multipart dates + else { + condSQL += "SUBSTR(" + condition['field'] + ", 1, 10)"; + } + + if (parseDate){ + var go = false; + var dateparts = Zotero.Date.strToDate(condition.value); + + // Search on SQL date -- underscore is + // single-character wildcard + // + // If isBefore or isAfter, month and day fall back + // to '00' so that a search for just a year works + // (and no year will just not find anything) + var sqldate = dateparts.year ? + Zotero.Utilities.lpad(dateparts.year, '0', 4) : '____'; + sqldate += '-' + sqldate += dateparts.month || dateparts.month === 0 ? + Zotero.Utilities.lpad(dateparts.month + 1, '0', 2) : alt; + sqldate += '-'; + sqldate += dateparts.day ? + Zotero.Utilities.lpad(dateparts.day, '0', 2) : alt; + + if (sqldate!='____-__-__'){ + go = true; + + switch (condition['operator']){ + case 'is': + case 'isNot': + condSQL += ' LIKE ?'; + break; + + case 'isBefore': + condSQL += '<?'; + condSQL += ' AND ' + condition['field'] + + ">'0000-00-00'"; + break; + + case 'isAfter': + condSQL += '>?'; + break; + } + + condSQLParams.push({string:sqldate}); + } + + // Search for any remaining parts individually + if (useFreeform && dateparts['part']){ + go = true; + var parts = dateparts['part'].split(' '); + for each (var part in parts){ + condSQL += " AND SUBSTR(" + condition['field'] + ", 12, 100)"; + condSQL += " LIKE ?"; + condSQLParams.push('%' + part + '%'); + } + } + + // If neither part used, invalidate clause + if (!go){ + condSQL += '=0'; + } + } + + else { + switch (condition['operator']){ + case 'isInTheLast': + condSQL += ">DATE('NOW', 'localtime', ?)"; // e.g. ('NOW', '-10 DAYS') + condSQLParams.push({string: '-' + condition['value']}); + break; + } + } + } + + // Non-date fields + else { + switch (condition.operator) { + // Cast strings as integers for < and > comparisons, + // at least until + case 'isLessThan': + case 'isGreaterThan': + condSQL += "CAST(" + condition['field'] + " AS INT)"; + // Make sure either field is an integer or + // converting to an integer and back to a string + // yields the same result (i.e. it's numeric) + var opAppend = " AND (TYPEOF(" + + condition['field'] + ") = 'integer' OR " + + "CAST(" + + "CAST(" + condition['field'] + " AS INT)" + + " AS STRING) = " + condition['field'] + ")" + break; + + default: + condSQL += condition['field']; + } + + switch (condition['operator']){ + case 'contains': + case 'doesNotContain': // excluded with NOT IN above + condSQL += ' LIKE ?'; + // For fields with 'leftbound' flag, perform a + // leftbound search even for 'contains' condition + if (condition['flags'] && + condition['flags']['leftbound'] && + Zotero.Prefs.get('search.useLeftBound')) { + condSQLParams.push(condition['value'] + '%'); + } + else { + condSQLParams.push('%' + condition['value'] + '%'); + } + break; + + case 'is': + case 'isNot': // excluded with NOT IN above + // Automatically cast values which might + // have been stored as integers + if (condition.value && typeof condition.value == 'string' + && condition.value.match(/^[1-9]+[0-9]*$/)) { + condSQL += ' LIKE ?'; + } + else if (condition.value === null) { + condSQL += ' IS NULL'; + break; + } + else { + condSQL += '=?'; + } + condSQLParams.push(condition['value']); + break; + + case 'beginsWith': + condSQL += ' LIKE ?'; + condSQLParams.push(condition['value'] + '%'); + break; + + case 'isLessThan': + condSQL += '<?'; + condSQLParams.push({int:condition['value']}); + condSQL += opAppend; + break; + + case 'isGreaterThan': + condSQL += '>?'; + condSQLParams.push({int:condition['value']}); + condSQL += opAppend; + break; + + // Next two only used with full datetimes + case 'isBefore': + condSQL += '<?'; + condSQLParams.push({string:condition['value']}); + break; + + case 'isAfter': + condSQL += '>?'; + condSQLParams.push({string:condition['value']}); + break; + } + } + } + + // Close open parentheses + for (var k=openParens; k>0; k--){ + condSQL += ')'; + } + + if (includeParentsAndChildren || includeParents) { + var parentSQL = "SELECT itemID FROM items WHERE " + + "itemID IN (SELECT parentItemID FROM itemAttachments " + + "WHERE itemID IN (" + condSQL + ")) " + + "OR itemID IN (SELECT parentItemID FROM itemNotes " + + "WHERE itemID IN (" + condSQL + ")) "; + var parentSQLParams = condSQLParams.concat(condSQLParams); + } + + if (includeParentsAndChildren || includeChildren) { + var childrenSQL = "SELECT itemID FROM itemAttachments WHERE " + + "parentItemID IN (" + condSQL + ") UNION " + + "SELECT itemID FROM itemNotes " + + "WHERE parentItemID IN (" + condSQL + ")"; + var childSQLParams = condSQLParams.concat(condSQLParams); + } + + if (includeParentsAndChildren || includeParents) { + condSQL += " UNION " + parentSQL; + condSQLParams = condSQLParams.concat(parentSQLParams); + } + + if (includeParentsAndChildren || includeChildren) { + condSQL += " UNION " + childrenSQL; + condSQLParams = condSQLParams.concat(childSQLParams); + } + + condSQL = condSelectSQL + condSQL; + + // Close open parentheses + for (var k=selectOpenParens; k>0; k--) { + condSQL += ')'; + } + + // Little hack to support multiple quicksearch words + if (condition['name'] == 'blockStart') { + var inQS = true; + var qsSQL = ''; + var qsParams = []; + continue; + } + else if (condition['name'] == 'blockEnd') { + inQS = false; + // Strip ' OR ' from last condition + qsSQL = qsSQL.substring(0, qsSQL.length-4); + + // Add to existing quicksearch words + if (!quicksearchSQLSet) { + var quicksearchSQLSet = []; + var quicksearchParamsSet = []; + } + quicksearchSQLSet.push(qsSQL); + quicksearchParamsSet.push(qsParams); + } + else if (inQS) { + qsSQL += condSQL + ' OR '; + qsParams = qsParams.concat(condSQLParams); + } + // Keep non-required conditions separate if in ANY mode + else if (!condition['required'] && joinMode == 'ANY') { + anySQL += condSQL + ' OR '; + anySQLParams = anySQLParams.concat(condSQLParams); + } + else { + condSQL += ' AND '; + sql += condSQL; + sqlParams = sqlParams.concat(condSQLParams); + } + } + + // Add on ANY conditions + if (anySQL){ + sql += '(' + anySQL; + sqlParams = sqlParams.concat(anySQLParams); + sql = sql.substring(0, sql.length-4); // remove last ' OR ' + sql += ')'; + } + else { + sql = sql.substring(0, sql.length-5); // remove last ' AND ' + } + + // Add on quicksearch conditions + if (quicksearchSQLSet) { + sql = "SELECT itemID FROM items WHERE itemID IN (" + sql + ") " + + "AND ((" + quicksearchSQLSet.join(') AND (') + "))"; + + for (var k=0; k<quicksearchParamsSet.length; k++) { + sqlParams = sqlParams.concat(quicksearchParamsSet[k]); + } + } + } + + this._sql = sql; + this._sqlParams = sqlParams.length ? sqlParams : false; +}); diff --git a/chrome/content/zotero/xpcom/data/searchConditions.js b/chrome/content/zotero/xpcom/data/searchConditions.js @@ -0,0 +1,690 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2006-2016 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + https://zotero.org + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see <http://www.gnu.org/licenses/>. + + ***** END LICENSE BLOCK ***** +*/ + +Zotero.SearchConditions = new function(){ + this.get = get; + this.getStandardConditions = getStandardConditions; + this.hasOperator = hasOperator; + this.getLocalizedName = getLocalizedName; + this.parseSearchString = parseSearchString; + this.parseCondition = parseCondition; + + var _initialized = false; + var _conditions; + var _standardConditions; + + var self = this; + + /* + * Define the advanced search operators + */ + var _operators = { + // Standard -- these need to match those in zoterosearch.xml + is: true, + isNot: true, + beginsWith: true, + contains: true, + doesNotContain: true, + isLessThan: true, + isGreaterThan: true, + isBefore: true, + isAfter: true, + isInTheLast: true, + + // Special + any: true, + all: true, + true: true, + false: true + }; + + + /* + * Define and set up the available advanced search conditions + * + * Flags: + * - special (don't show in search window menu) + * - template (special handling) + * - noLoad (can't load from saved search) + */ + this.init = Zotero.Promise.coroutine(function* () { + var conditions = [ + // + // Special conditions + // + { + name: 'deleted', + operators: { + true: true, + false: true + } + }, + + // Don't include child items + { + name: 'noChildren', + operators: { + true: true, + false: true + } + }, + + { + name: 'unfiled', + operators: { + true: true, + false: true + } + }, + + { + name: 'includeParentsAndChildren', + operators: { + true: true, + false: true + } + }, + + { + name: 'includeParents', + operators: { + true: true, + false: true + } + }, + + { + name: 'includeChildren', + operators: { + true: true, + false: true + } + }, + + // Search recursively within collections + { + name: 'recursive', + operators: { + true: true, + false: true + } + }, + + // Join mode + { + name: 'joinMode', + operators: { + any: true, + all: true + } + }, + + { + name: 'quicksearch-titleCreatorYear', + operators: { + is: true, + isNot: true, + contains: true, + doesNotContain: true + }, + noLoad: true + }, + + { + name: 'quicksearch-fields', + operators: { + is: true, + isNot: true, + contains: true, + doesNotContain: true + }, + noLoad: true + }, + + { + name: 'quicksearch-everything', + operators: { + is: true, + isNot: true, + contains: true, + doesNotContain: true + }, + noLoad: true + }, + + // Deprecated + { + name: 'quicksearch', + operators: { + is: true, + isNot: true, + contains: true, + doesNotContain: true + }, + noLoad: true + }, + + // Quicksearch block markers + { + name: 'blockStart', + noLoad: true + }, + + { + name: 'blockEnd', + noLoad: true + }, + + // Shortcuts for adding collections and searches by id + { + name: 'collectionID', + operators: { + is: true, + isNot: true + }, + noLoad: true + }, + + { + name: 'savedSearchID', + operators: { + is: true, + isNot: true + }, + noLoad: true + }, + + + // + // Standard conditions + // + + // Collection id to search within + { + name: 'collection', + operators: { + is: true, + isNot: true + }, + table: 'collectionItems', + field: 'collectionID' + }, + + // Saved search to search within + { + name: 'savedSearch', + operators: { + is: true, + isNot: true + }, + special: false + }, + + { + name: 'dateAdded', + operators: { + is: true, + isNot: true, + isBefore: true, + isAfter: true, + isInTheLast: true + }, + table: 'items', + field: 'dateAdded' + }, + + { + name: 'dateModified', + operators: { + is: true, + isNot: true, + isBefore: true, + isAfter: true, + isInTheLast: true + }, + table: 'items', + field: 'dateModified' + }, + + // Deprecated + { + name: 'itemTypeID', + operators: { + is: true, + isNot: true + }, + table: 'items', + field: 'itemTypeID', + special: true + }, + + { + name: 'itemType', + operators: { + is: true, + isNot: true + }, + table: 'items', + field: 'typeName' + }, + + { + name: 'fileTypeID', + operators: { + is: true, + isNot: true + }, + table: 'itemAttachments', + field: 'fileTypeID' + }, + + { + name: 'tagID', + operators: { + is: true, + isNot: true + }, + table: 'itemTags', + field: 'tagID', + special: true + }, + + { + name: 'tag', + operators: { + is: true, + isNot: true, + contains: true, + doesNotContain: true + }, + table: 'itemTags', + field: 'name' + }, + + { + name: 'note', + operators: { + contains: true, + doesNotContain: true + }, + table: 'itemNotes', + field: 'note' + }, + + { + name: 'childNote', + operators: { + contains: true, + doesNotContain: true + }, + table: 'items', + field: 'note' + }, + + { + name: 'creator', + operators: { + is: true, + isNot: true, + contains: true, + doesNotContain: true + }, + table: 'itemCreators', + field: "TRIM(firstName || ' ' || lastName)" + }, + + { + name: 'lastName', + operators: { + is: true, + isNot: true, + contains: true, + doesNotContain: true + }, + table: 'itemCreators', + field: 'lastName', + special: true + }, + + { + name: 'field', + operators: { + is: true, + isNot: true, + contains: true, + doesNotContain: true + }, + table: 'itemData', + field: 'value', + aliases: yield Zotero.DB.columnQueryAsync("SELECT fieldName FROM fieldsCombined " + + "WHERE fieldName NOT IN ('accessDate', 'date', 'pages', " + + "'section','seriesNumber','issue')"), + template: true // mark for special handling + }, + + { + name: 'datefield', + operators: { + is: true, + isNot: true, + isBefore: true, + isAfter: true, + isInTheLast: true + }, + table: 'itemData', + field: 'value', + aliases: ['accessDate', 'date', 'dateDue', 'accepted'], // TEMP - NSF + template: true // mark for special handling + }, + + { + name: 'year', + operators: { + is: true, + isNot: true, + contains: true, + doesNotContain: true + }, + table: 'itemData', + field: 'SUBSTR(value, 1, 4)', + special: true + }, + + { + name: 'numberfield', + operators: { + is: true, + isNot: true, + contains: true, + doesNotContain: true, + isLessThan: true, + isGreaterThan: true + }, + table: 'itemData', + field: 'value', + aliases: ['pages', 'numPages', 'numberOfVolumes', 'section', 'seriesNumber','issue'], + template: true // mark for special handling + }, + + { + name: 'libraryID', + operators: { + is: true, + isNot: true + }, + table: 'items', + field: 'libraryID', + special: true, + noLoad: true + }, + + { + name: 'key', + operators: { + is: true, + isNot: true, + beginsWith: true + }, + table: 'items', + field: 'key', + special: true, + noLoad: true + }, + + { + name: 'itemID', + operators: { + is: true, + isNot: true + }, + table: 'items', + field: 'itemID', + special: true, + noLoad: true + }, + + { + name: 'annotation', + operators: { + contains: true, + doesNotContain: true + }, + table: 'annotations', + field: 'text' + }, + + { + name: 'fulltextWord', + operators: { + contains: true, + doesNotContain: true + }, + table: 'fulltextItemWords', + field: 'word', + flags: { + leftbound: true + }, + special: true + }, + + { + name: 'fulltextContent', + operators: { + contains: true, + doesNotContain: true + }, + special: false + }, + + { + name: 'tempTable', + operators: { + is: true + } + } + ]; + + // Index conditions by name and aliases + _conditions = {}; + for (var i in conditions) { + _conditions[conditions[i]['name']] = conditions[i]; + if (conditions[i]['aliases']) { + for (var j in conditions[i]['aliases']) { + // TEMP - NSF + switch (conditions[i]['aliases'][j]) { + case 'dateDue': + case 'accepted': + if (!Zotero.ItemTypes.getID('nsfReviewer')) { + continue; + } + } + _conditions[conditions[i]['aliases'][j]] = conditions[i]; + } + } + _conditions[conditions[i]['name']] = conditions[i]; + } + + _standardConditions = []; + + var baseMappedFields = Zotero.ItemFields.getBaseMappedFields(); + var locale = Zotero.locale; + + // Separate standard conditions for menu display + for (var i in _conditions){ + var fieldID = false; + if (['field', 'datefield', 'numberfield'].indexOf(_conditions[i]['name']) != -1) { + fieldID = Zotero.ItemFields.getID(i); + } + + // If explicitly special... + if (_conditions[i]['special'] || + // or a template master (e.g. 'field')... + (_conditions[i]['template'] && i==_conditions[i]['name']) || + // or no table and not explicitly unspecial... + (!_conditions[i]['table'] && + typeof _conditions[i]['special'] == 'undefined') || + // or field is a type-specific version of a base field... + (fieldID && baseMappedFields.indexOf(fieldID) != -1)) { + // ...then skip + continue; + } + + let localized = self.getLocalizedName(i); + // Hack to use a different name for "issue" in French locale, + // where 'number' and 'issue' are translated the same + // https://forums.zotero.org/discussion/14942/ + if (fieldID == 5 && locale.substr(0, 2).toLowerCase() == 'fr') { + localized = "Num\u00E9ro (p\u00E9riodique)"; + } + + _standardConditions.push({ + name: i, + localized: localized, + operators: _conditions[i]['operators'], + flags: _conditions[i]['flags'] + }); + } + + var collation = Zotero.getLocaleCollation(); + _standardConditions.sort(function(a, b) { + return collation.compareString(1, a.localized, b.localized); + }); + }); + + + /* + * Get condition data + */ + function get(condition){ + return _conditions[condition]; + } + + + /* + * Returns array of possible conditions + * + * Does not include special conditions, only ones that would show in a drop-down list + */ + function getStandardConditions(){ + // TODO: return copy instead + return _standardConditions; + } + + + /* + * Check if an operator is valid for a given condition + */ + 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]){ + let e = new Error("Invalid condition '" + condition + "' in hasOperator()"); + e.name = "ZoteroUnknownFieldError"; + throw e; + } + + if (!operator && typeof _conditions[condition]['operators'] == 'undefined'){ + return true; + } + + return !!_conditions[condition]['operators'][operator]; + } + + + function getLocalizedName(str) { + // TEMP + if (str == 'itemType') { + str = 'itemTypeID'; + } + + try { + return Zotero.getString('searchConditions.' + str) + } + catch (e) { + return Zotero.ItemFields.getLocalizedString(null, str); + } + } + + + /** + * Compare two API JSON condition objects + */ + this.equals = function (data1, data2) { + return data1.condition === data2.condition + && data1.operator === data2.operator + && data1.value === data2.value; + } + + + /* + * Parses a search into words and "double-quoted phrases" + * + * Also strips unpaired quotes at the beginning and end of words + * + * Returns array of objects containing 'text' and 'inQuotes' + */ + function parseSearchString(str) { + var parts = str.split(/\s*("[^"]*")\s*|"\s|\s"|^"|"$|'\s|\s'|^'|'$|\s/m); + var parsed = []; + + for (var i in parts) { + var part = parts[i]; + if (!part || !part.length) { + continue; + } + + if (part.charAt(0)=='"' && part.charAt(part.length-1)=='"') { + parsed.push({ + text: part.substring(1, part.length-1), + inQuotes: true + }); + } + else { + parsed.push({ + text: part, + inQuotes: false + }); + } + } + + return parsed; + } + + + function parseCondition(condition){ + var mode = false; + var pos = condition.indexOf('/'); + if (pos != -1){ + mode = condition.substr(pos+1); + condition = condition.substr(0, pos); + } + + return [condition, mode]; + } +} diff --git a/chrome/content/zotero/xpcom/data/searches.js b/chrome/content/zotero/xpcom/data/searches.js @@ -0,0 +1,172 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright © 2006-2016 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + https://zotero.org + + This file is part of Zotero. + + Zotero is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Zotero is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with Zotero. If not, see <http://www.gnu.org/licenses/>. + + ***** END LICENSE BLOCK ***** +*/ + +Zotero.Searches = function() { + this.constructor = null; + + this._ZDO_object = 'search'; + this._ZDO_id = 'savedSearchID'; + this._ZDO_table = 'savedSearches'; + + this._primaryDataSQLParts = { + savedSearchID: "O.savedSearchID", + name: "O.savedSearchName AS name", + libraryID: "O.libraryID", + key: "O.key", + version: "O.version", + synced: "O.synced" + } + + this._primaryDataSQLFrom = "FROM savedSearches O"; + + this.init = Zotero.Promise.coroutine(function* () { + yield Zotero.DataObjects.prototype.init.apply(this); + yield Zotero.SearchConditions.init(); + }); + + + /** + * Returns an array of Zotero.Search objects, ordered by name + * + * @param {Integer} [libraryID] + */ + this.getAll = Zotero.Promise.coroutine(function* (libraryID) { + var sql = "SELECT savedSearchID FROM savedSearches WHERE libraryID=?"; + var ids = yield Zotero.DB.columnQueryAsync(sql, libraryID); + if (!ids.length) { + return [] + } + + var searches = this.get(ids); + // Do proper collation sort + var collation = Zotero.getLocaleCollation(); + searches.sort(function (a, b) { + return collation.compareString(1, a.name, b.name); + }); + return searches; + }); + + + this.getPrimaryDataSQL = function () { + // This should be the same as the query in Zotero.Search.loadPrimaryData(), + // just without a specific savedSearchID + return "SELECT " + + Object.keys(this._primaryDataSQLParts).map(key => this._primaryDataSQLParts[key]).join(", ") + " " + + "FROM savedSearches O WHERE 1"; + } + + + this._loadConditions = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) { + var sql = "SELECT savedSearchID, searchConditionID, condition, operator, value, required " + + "FROM savedSearches LEFT JOIN savedSearchConditions USING (savedSearchID) " + + "WHERE libraryID=?" + idSQL + + "ORDER BY savedSearchID, searchConditionID"; + var params = [libraryID]; + var lastID = null; + var rows = []; + var setRows = function (searchID, rows) { + var search = this._objectCache[searchID]; + if (!search) { + throw new Error("Search " + searchID + " not found"); + } + + search._conditions = {}; + + if (rows.length) { + search._maxSearchConditionID = rows[rows.length - 1].searchConditionID; + } + + // Reindex conditions, in case they're not contiguous in the DB + for (let i = 0; i < rows.length; i++) { + let condition = rows[i]; + + // Parse "condition[/mode]" + let [conditionName, mode] = Zotero.SearchConditions.parseCondition(condition.condition); + + let cond = Zotero.SearchConditions.get(conditionName); + if (!cond || cond.noLoad) { + Zotero.debug("Invalid saved search condition '" + conditionName + "' -- skipping", 2); + continue; + } + + // Convert itemTypeID to itemType + // + // TEMP: This can be removed at some point + if (conditionName == 'itemTypeID') { + conditionName = 'itemType'; + condition.value = Zotero.ItemTypes.getName(condition.value); + } + + search._conditions[i] = { + id: i, + condition: conditionName, + mode: mode, + operator: condition.operator, + value: condition.value, + required: !!condition.required + }; + } + search._loaded.conditions = true; + search._clearChanged('conditions'); + }.bind(this); + + yield Zotero.DB.queryAsync( + sql, + params, + { + noCache: ids.length != 1, + onRow: function (row) { + let searchID = row.getResultByIndex(0); + + if (lastID && searchID != lastID) { + setRows(lastID, rows); + rows = []; + } + + lastID = searchID; + let searchConditionID = row.getResultByIndex(1); + // No conditions + if (searchConditionID === null) { + return; + } + rows.push({ + searchConditionID, + condition: row.getResultByIndex(2), + operator: row.getResultByIndex(3), + value: row.getResultByIndex(4), + required: row.getResultByIndex(5) + }); + }.bind(this) + } + ); + if (lastID) { + setRows(lastID, rows); + } + }); + + Zotero.DataObjects.call(this); + + return this; +}.bind(Object.create(Zotero.DataObjects.prototype))(); diff --git a/chrome/content/zotero/xpcom/search.js b/chrome/content/zotero/xpcom/search.js @@ -1,2410 +0,0 @@ -/* - ***** BEGIN LICENSE BLOCK ***** - - Copyright © 2009 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.Search = function(params = {}) { - Zotero.Search._super.apply(this); - - this._name = null; - - this._scope = null; - this._scopeIncludeChildren = null; - this._sql = null; - this._sqlParams = false; - this._maxSearchConditionID = -1; - this._conditions = {}; - this._hasPrimaryConditions = false; - - Zotero.Utilities.assignProps(this, params, ['name', 'libraryID']); -} - -Zotero.extendClass(Zotero.DataObject, Zotero.Search); - -Zotero.Search.prototype._objectType = 'search'; -Zotero.Search.prototype._dataTypes = Zotero.Search._super.prototype._dataTypes.concat([ - 'conditions' -]); - -Zotero.Search.prototype.getID = function(){ - Zotero.debug('Zotero.Search.getName() is deprecated -- use Search.id'); - return this._id; -} - -Zotero.Search.prototype.getName = function() { - Zotero.debug('Zotero.Search.getName() is deprecated -- use Search.name'); - return this.name; -} - -Zotero.Search.prototype.setName = function(val) { - Zotero.debug('Zotero.Search.setName() is deprecated -- use Search.name'); - this.name = val; -} - -Zotero.defineProperty(Zotero.Search.prototype, 'id', { - get: function() this._get('id'), - set: function(val) this._set('id', val) -}); -Zotero.defineProperty(Zotero.Search.prototype, 'libraryID', { - get: function() this._get('libraryID'), - set: function(val) this._set('libraryID', val) -}); -Zotero.defineProperty(Zotero.Search.prototype, 'key', { - get: function() this._get('key'), - set: function(val) this._set('key', val) -}); -Zotero.defineProperty(Zotero.Search.prototype, 'name', { - get: function() this._get('name'), - set: function(val) this._set('name', val) -}); -Zotero.defineProperty(Zotero.Search.prototype, 'version', { - get: function() this._get('version'), - set: function(val) this._set('version', val) -}); -Zotero.defineProperty(Zotero.Search.prototype, 'synced', { - get: function() this._get('synced'), - set: function(val) this._set('synced', val) -}); -Zotero.defineProperty(Zotero.Search.prototype, 'conditions', { - get: function() this.getConditions() -}); -Zotero.defineProperty(Zotero.Search.prototype, '_canHaveParent', { - value: false -}); - - -Zotero.Search.prototype.loadFromRow = function (row) { - var primaryFields = this._ObjectsClass.primaryFields; - for (let i=0; i<primaryFields.length; i++) { - let col = primaryFields[i]; - try { - var val = row[col]; - } - catch (e) { - Zotero.debug('Skipping missing ' + this._objectType + ' field ' + col); - continue; - } - - switch (col) { - case this._ObjectsClass.idColumn: - col = 'id'; - break; - - // Integer - case 'libraryID': - val = parseInt(val); - break; - - // Integer or 0 - case 'version': - val = val ? parseInt(val) : 0; - break; - - // Boolean - case 'synced': - val = !!val; - break; - - default: - val = val || ''; - } - - this['_' + col] = val; - } - - this._loaded.primaryData = true; - this._clearChanged('primaryData'); - this._identified = true; -} - -Zotero.Search.prototype._initSave = Zotero.Promise.coroutine(function* (env) { - if (!this.name) { - throw new Error('Name not provided for saved search'); - } - return Zotero.Search._super.prototype._initSave.apply(this, arguments); -}); - -Zotero.Search.prototype._saveData = Zotero.Promise.coroutine(function* (env) { - var isNew = env.isNew; - var options = env.options; - - var searchID = this._id = this.id ? this.id : Zotero.ID.get('savedSearches'); - - env.sqlColumns.push( - 'savedSearchName' - ); - env.sqlValues.push( - { string: this.name } - ); - - if (isNew) { - env.sqlColumns.unshift('savedSearchID'); - env.sqlValues.unshift(searchID ? { int: searchID } : null); - - let placeholders = env.sqlColumns.map(function () '?').join(); - let sql = "INSERT INTO savedSearches (" + env.sqlColumns.join(', ') + ") " - + "VALUES (" + placeholders + ")"; - yield Zotero.DB.queryAsync(sql, env.sqlValues); - } - else { - let sql = 'UPDATE savedSearches SET ' - + env.sqlColumns.map(function (x) x + '=?').join(', ') + ' WHERE savedSearchID=?'; - env.sqlValues.push(searchID ? { int: searchID } : null); - yield Zotero.DB.queryAsync(sql, env.sqlValues); - } - - if (this._changed.conditions) { - if (!isNew) { - var sql = "DELETE FROM savedSearchConditions WHERE savedSearchID=?"; - yield Zotero.DB.queryAsync(sql, this.id); - } - - var i = 0; - var sql = "INSERT INTO savedSearchConditions " - + "(savedSearchID, searchConditionID, condition, operator, value, required) " - + "VALUES (?,?,?,?,?,?)"; - for (let id in this._conditions) { - let condition = this._conditions[id]; - - // Convert condition and mode to "condition[/mode]" - let conditionString = condition.mode ? - condition.condition + '/' + condition.mode : - condition.condition - - var sqlParams = [ - searchID, - i, - conditionString, - condition.operator ? condition.operator : null, - condition.value ? condition.value : null, - condition.required ? 1 : null - ]; - yield Zotero.DB.queryAsync(sql, sqlParams); - i++; - } - } -}); - -Zotero.Search.prototype._finalizeSave = Zotero.Promise.coroutine(function* (env) { - if (env.isNew) { - // Update library searches status - yield Zotero.Libraries.get(this.libraryID).updateSearches(); - - Zotero.Notifier.queue('add', 'search', this.id, env.notifierData, env.options.notifierQueue); - } - else if (!env.options.skipNotifier) { - Zotero.Notifier.queue('modify', 'search', this.id, env.notifierData, env.options.notifierQueue); - } - - if (env.isNew && Zotero.Libraries.isGroupLibrary(this.libraryID)) { - var groupID = Zotero.Groups.getGroupIDFromLibraryID(this.libraryID); - var group = yield Zotero.Groups.get(groupID); - group.clearSearchCache(); - } - - if (!env.skipCache) { - yield this.reload(); - // If new, there's no other data we don't have, so we can mark everything as loaded - if (env.isNew) { - this._markAllDataTypeLoadStates(true); - } - this._clearChanged(); - } - - return env.isNew ? this.id : true; -}); - - -Zotero.Search.prototype.clone = function (libraryID) { - var s = new Zotero.Search(); - s.libraryID = libraryID === undefined ? this.libraryID : libraryID; - - var conditions = this.getConditions(); - - for each(var condition in conditions) { - var name = condition.mode ? - condition.condition + '/' + condition.mode : - condition.condition - - s.addCondition(name, condition.operator, condition.value, - condition.required); - } - - return s; -}; - - -Zotero.Search.prototype._eraseData = Zotero.Promise.coroutine(function* (env) { - Zotero.DB.requireTransaction(); - - var sql = "DELETE FROM savedSearchConditions WHERE savedSearchID=?"; - yield Zotero.DB.queryAsync(sql, this.id); - - var sql = "DELETE FROM savedSearches WHERE savedSearchID=?"; - yield Zotero.DB.queryAsync(sql, this.id); -}); - -Zotero.Search.prototype._finalizeErase = Zotero.Promise.coroutine(function* (env) { - yield Zotero.Search._super.prototype._finalizeErase.call(this, env); - - // Update library searches status - yield Zotero.Libraries.get(this.libraryID).updateSearches(); -}); - - -Zotero.Search.prototype.addCondition = function (condition, operator, value, required) { - this._requireData('conditions'); - - if (!Zotero.SearchConditions.hasOperator(condition, operator)){ - let e = new Error("Invalid operator '" + operator + "' for condition " + condition); - e.name = "ZoteroUnknownFieldError"; - throw e; - } - - // Shortcut to add a condition on every table -- does not return an id - if (condition.match(/^quicksearch/)) { - var parts = Zotero.SearchConditions.parseSearchString(value); - - for each(var part in parts) { - this.addCondition('blockStart'); - - // If search string is 8 characters, see if this is a item key - if (operator == 'contains' && part.text.length == 8) { - this.addCondition('key', 'is', part.text, false); - } - - if (condition == 'quicksearch-titleCreatorYear') { - this.addCondition('title', operator, part.text, false); - this.addCondition('publicationTitle', operator, part.text, false); - this.addCondition('shortTitle', operator, part.text, false); - this.addCondition('court', operator, part.text, false); - this.addCondition('year', operator, part.text, false); - } - else { - this.addCondition('field', operator, part.text, false); - this.addCondition('tag', operator, part.text, false); - this.addCondition('note', operator, part.text, false); - } - this.addCondition('creator', operator, part.text, false); - - if (condition == 'quicksearch-everything') { - this.addCondition('annotation', operator, part.text, false); - - if (part.inQuotes) { - this.addCondition('fulltextContent', operator, part.text, false); - } - else { - var splits = Zotero.Fulltext.semanticSplitter(part.text); - for each(var split in splits) { - this.addCondition('fulltextWord', operator, split, false); - } - } - } - - this.addCondition('blockEnd'); - } - - if (condition == 'quicksearch-titleCreatorYear') { - this.addCondition('noChildren', 'true'); - } - - return false; - } - // Shortcut to add a collection (which must be loaded first) - else if (condition == 'collectionID') { - let {libraryID, key} = Zotero.Collections.getLibraryAndKeyFromID(value); - if (!key) { - let msg = "Collection " + value + " not found"; - Zotero.debug(msg, 2); - Components.utils.reportError(msg); - return; - } - if (this.libraryID && libraryID != this.libraryID) { - Zotero.logError(new Error("Collection " + value + " is in different library")); - return; - } - return this.addCondition('collection', operator, key, required); - } - // Shortcut to add a saved search (which must be loaded first) - else if (condition == 'savedSearchID') { - let {libraryID, key} = Zotero.Searches.getLibraryAndKeyFromID(value); - if (!key) { - let msg = "Saved search " + value + " not found"; - Zotero.debug(msg, 2); - Components.utils.reportError(msg); - return; - } - if (this.libraryID && libraryID != this.libraryID) { - Zotero.logError(new Error("Collection " + value + " is in different library")); - return; - } - return this.addCondition('savedSearch', operator, key, required); - } - - var searchConditionID = ++this._maxSearchConditionID; - - let mode; - [condition, mode] = Zotero.SearchConditions.parseCondition(condition); - - if (typeof value == 'string') value = value.normalize(); - - this._conditions[searchConditionID] = { - id: searchConditionID, - condition: condition, - mode: mode, - operator: operator, - value: value, - required: !!required - }; - - this._sql = null; - this._sqlParams = false; - this._markFieldChange('conditions', this._conditions); - this._changed.conditions = true; - - return searchConditionID; -} - - -/* - * Sets scope of search to the results of the passed Search object - */ -Zotero.Search.prototype.setScope = function (searchObj, includeChildren) { - this._scope = searchObj; - this._scopeIncludeChildren = includeChildren; -} - - -/** - * @param {Number} searchConditionID - * @param {String} condition - * @param {String} operator - * @param {String} value - * @param {Boolean} [required] - * @return {Promise} - */ -Zotero.Search.prototype.updateCondition = function (searchConditionID, condition, operator, value, required) { - this._requireData('conditions'); - - if (typeof this._conditions[searchConditionID] == 'undefined'){ - throw new Error('Invalid searchConditionID ' + searchConditionID); - } - - if (!Zotero.SearchConditions.hasOperator(condition, operator)){ - let e = new Error("Invalid operator '" + operator + "' for condition " + condition); - e.name = "ZoteroUnknownFieldError"; - throw e; - } - - var [condition, mode] = Zotero.SearchConditions.parseCondition(condition); - - if (typeof value == 'string') value = value.normalize(); - - this._conditions[searchConditionID] = { - id: parseInt(searchConditionID), - condition: condition, - mode: mode, - operator: operator, - value: value, - required: !!required - }; - - this._sql = null; - this._sqlParams = false; - this._markFieldChange('conditions', this._conditions); - this._changed.conditions = true; -} - - -Zotero.Search.prototype.removeCondition = function (searchConditionID) { - this._requireData('conditions'); - - if (typeof this._conditions[searchConditionID] == 'undefined'){ - throw new Error('Invalid searchConditionID ' + searchConditionID + ' in removeCondition()'); - } - - delete this._conditions[searchConditionID]; - this._maxSearchConditionID--; - this._markFieldChange('conditions', this._conditions); - this._changed.conditions = true; -} - - -/* - * Returns an array with 'condition', 'operator', 'value', 'required' - * for the given searchConditionID - */ -Zotero.Search.prototype.getCondition = function(searchConditionID){ - this._requireData('conditions'); - return this._conditions[searchConditionID]; -} - - -/* - * Returns an object of conditions/operator/value sets used in the search, - * indexed by searchConditionID - */ -Zotero.Search.prototype.getConditions = function(){ - this._requireData('conditions'); - var conditions = {}; - for (let id in this._conditions) { - let condition = this._conditions[id]; - conditions[id] = { - id: id, - condition: condition.condition, - mode: condition.mode, - operator: condition.operator, - value: condition.value, - required: condition.required - }; - } - return conditions; -} - - -Zotero.Search.prototype.hasPostSearchFilter = function() { - this._requireData('conditions'); - for each(var i in this._conditions){ - if (i.condition == 'fulltextContent'){ - return true; - } - } - return false; -} - - -/** - * Run the search and return an array of item ids for results - * - * @param {Boolean} [asTempTable=false] - * @return {Promise} - */ -Zotero.Search.prototype.search = Zotero.Promise.coroutine(function* (asTempTable) { - var tmpTable; - - // Mark conditions as loaded - // TODO: Necessary? - if (!this._identified) { - this._requireData('conditions'); - } - try { - if (!this._sql){ - yield this._buildQuery(); - } - - // Default to 'all' mode - var joinMode = 'all'; - - // Set some variables for conditions to avoid further lookups - for each(var condition in this._conditions) { - switch (condition.condition) { - case 'joinMode': - if (condition.operator == 'any') { - joinMode = 'any'; - } - break; - - case 'fulltextContent': - var fulltextContent = true; - break; - - case 'includeParentsAndChildren': - if (condition.operator == 'true') { - var includeParentsAndChildren = true; - } - break; - - case 'includeParents': - if (condition.operator == 'true') { - var includeParents = true; - } - break; - - case 'includeChildren': - if (condition.operator == 'true') { - var includeChildren = true; - } - break; - - case 'blockStart': - var hasQuicksearch = true; - break; - } - } - - // Run a subsearch to define the superset of possible results - if (this._scope) { - // If subsearch has post-search filter, run and insert ids into temp table - if (this._scope.hasPostSearchFilter()) { - var ids = yield this._scope.search(); - if (!ids) { - return []; - } - - Zotero.debug('g'); - Zotero.debug(ids); - tmpTable = yield Zotero.Search.idsToTempTable(ids); - } - // Otherwise, just copy to temp table directly - else { - tmpTable = "tmpSearchResults_" + Zotero.randomString(8); - var sql = "CREATE TEMPORARY TABLE " + tmpTable + " AS " - + (yield this._scope.getSQL()); - yield Zotero.DB.queryAsync(sql, yield this._scope.getSQLParams()); - var sql = "CREATE INDEX " + tmpTable + "_itemID ON " + tmpTable + "(itemID)"; - yield Zotero.DB.queryAsync(sql); - } - - // Search ids in temp table - var sql = "SELECT GROUP_CONCAT(itemID) FROM items WHERE itemID IN (" + this._sql + ") " - + "AND (" - + "itemID IN (SELECT itemID FROM " + tmpTable + ")"; - - if (this._scopeIncludeChildren) { - sql += " OR itemID IN (SELECT itemID FROM itemAttachments" - + " WHERE parentItemID IN (SELECT itemID FROM " + tmpTable + ")) OR " - + "itemID IN (SELECT itemID FROM itemNotes" - + " WHERE parentItemID IN (SELECT itemID FROM " + tmpTable + "))"; - } - sql += ")"; - - var res = yield Zotero.DB.valueQueryAsync(sql, this._sqlParams); - var ids = res ? res.split(",") : []; - /* - // DEBUG: Should this be here? - // - if (!ids) { - Zotero.DB.query("DROP TABLE " + tmpTable); - Zotero.DB.commitTransaction(); - return false; - } - */ - } - // Or just run main search - else { - var ids = yield Zotero.DB.columnQueryAsync(this._sql, this._sqlParams); - } - - //Zotero.debug('IDs from main search or subsearch: '); - //Zotero.debug(ids); - - //Zotero.debug('Join mode: ' + joinMode); - - // Filter results with fulltext search - // - // If join mode ALL, return the (intersection of main and fulltext word search) - // filtered by fulltext content - // - // If join mode ANY or there's a quicksearch (which we assume - // fulltextContent is part of), return the union of the main search and - // (a separate fulltext word search filtered by fulltext content) - for each(var condition in this._conditions){ - if (condition['condition']=='fulltextContent'){ - var fulltextWordIntersectionFilter = function (val, index, array) !!hash[val]; - var fulltextWordIntersectionConditionFilter = function(val, index, array) { - return hash[val] ? - (condition.operator == 'contains') : - (condition.operator == 'doesNotContain'); - }; - - // Regexp mode -- don't use fulltext word index - if (condition.mode && condition.mode.indexOf('regexp') == 0) { - // In an ANY search, only bother scanning items that - // haven't already been found by the main search - if (joinMode == 'any') { - if (!tmpTable) { - tmpTable = yield Zotero.Search.idsToTempTable(ids); - } - - var sql = "SELECT GROUP_CONCAT(itemID) FROM items WHERE " - + "itemID NOT IN (SELECT itemID FROM " + tmpTable + ")"; - var res = yield Zotero.DB.valueQueryAsync(sql); - var scopeIDs = res ? res.split(",") : []; - } - // If an ALL search, scan only items from the main search - else { - var scopeIDs = ids; - } - } - // If not regexp mode, run a new search against the fulltext word - // index for words in this phrase - else { - Zotero.debug('Running subsearch against fulltext word index'); - var s = new Zotero.Search(); - - // Add any necessary conditions to the fulltext word search -- - // those that are required in an ANY search and any outside the - // quicksearch in an ALL search - for each(var c in this._conditions) { - if (c.condition == 'blockStart') { - var inQS = true; - continue; - } - else if (c.condition == 'blockEnd') { - inQS = false; - continue; - } - else if (c.condition == 'fulltextContent' || inQS) { - continue; - } - else if (joinMode == 'any' && !c.required) { - continue; - } - s.addCondition(c.condition, c.operator, c.value); - } - - var splits = Zotero.Fulltext.semanticSplitter(condition.value); - for each(var split in splits){ - s.addCondition('fulltextWord', condition.operator, split); - } - var fulltextWordIDs = yield s.search(); - - //Zotero.debug("Fulltext word IDs"); - //Zotero.debug(fulltextWordIDs); - - // If ALL mode, set intersection of main search and fulltext word index - // as the scope for the fulltext content search - if (joinMode == 'all' && !hasQuicksearch) { - var hash = {}; - for (let i=0; i<fulltextWordIDs.length; i++) { - hash[fulltextWordIDs[i].id] = true; - } - - if (ids) { - var scopeIDs = ids.filter(fulltextWordIntersectionFilter); - } - else { - var scopeIDs = []; - } - } - // If ANY mode, just use fulltext word index hits for content search, - // since the main results will be added in below - else { - var scopeIDs = fulltextWordIDs; - } - } - - if (scopeIDs && scopeIDs.length) { - var fulltextIDs = yield Zotero.Fulltext.findTextInItems(scopeIDs, - condition['value'], condition['mode']); - - var hash = {}; - for (let i=0; i<fulltextIDs.length; i++) { - hash[fulltextIDs[i].id] = true; - } - - filteredIDs = scopeIDs.filter(fulltextWordIntersectionConditionFilter); - } - else { - var filteredIDs = []; - } - - //Zotero.debug("Filtered IDs:") - //Zotero.debug(filteredIDs); - - // If join mode ANY, add any new items from the fulltext content - // search to the main search results - // - // We only do this if there are primary conditions that alter the - // main search, since otherwise all items will match - if (this._hasPrimaryConditions && - (joinMode == 'any' || hasQuicksearch) && ids) { - //Zotero.debug("Adding filtered IDs to main set"); - for (let i=0; i<filteredIDs.length; i++) { - let id = filteredIDs[i]; - if (ids.indexOf(id) == -1) { - ids.push(id); - } - } - } - else { - //Zotero.debug("Replacing main set with filtered IDs"); - ids = filteredIDs; - } - } - } - - if (this.hasPostSearchFilter() && - (includeParentsAndChildren || includeParents || includeChildren)) { - Zotero.debug('b'); - Zotero.debug(ids); - var tmpTable = yield Zotero.Search.idsToTempTable(ids); - - if (includeParentsAndChildren || includeParents) { - //Zotero.debug("Adding parent items to result set"); - var sql = "SELECT parentItemID FROM itemAttachments " - + "WHERE itemID IN (SELECT itemID FROM " + tmpTable + ") " - + " AND parentItemID IS NOT NULL " - + "UNION SELECT parentItemID FROM itemNotes " - + "WHERE itemID IN (SELECT itemID FROM " + tmpTable + ")" - + " AND parentItemID IS NOT NULL"; - } - - if (includeParentsAndChildren || includeChildren) { - //Zotero.debug("Adding child items to result set"); - var childrenSQL = "SELECT itemID FROM itemAttachments WHERE " - + "parentItemID IN (SELECT itemID FROM " + tmpTable + ") UNION " - + "SELECT itemID FROM itemNotes WHERE parentItemID IN " - + "(SELECT itemID FROM " + tmpTable + ")"; - - if (includeParentsAndChildren || includeParents) { - sql += " UNION " + childrenSQL; - } - else { - sql = childrenSQL; - } - } - - sql = "SELECT GROUP_CONCAT(itemID) FROM items WHERE itemID IN (" + sql + ")"; - var res = yield Zotero.DB.valueQueryAsync(sql); - var parentChildIDs = res ? res.split(",") : []; - - // Add parents and children to main ids - if (parentChildIDs) { - for (var i=0; i<parentChildIDs.length; i++) { - var id = parentChildIDs[i]; - if (ids.indexOf(id) == -1) { - ids.push(id); - } - } - } - } - } - finally { - if (tmpTable && !asTempTable) { - yield Zotero.DB.queryAsync("DROP TABLE IF EXISTS " + tmpTable); - } - } - - //Zotero.debug('Final result set'); - //Zotero.debug(ids); - - if (!ids || !ids.length) { - return []; - } - - if (asTempTable) { - Zotero.debug('c'); - Zotero.debug(ids); - return Zotero.Search.idsToTempTable(ids); - } - return ids; -}); - - -/** - * Populate the object's data from an API JSON data object - * - * If this object is identified (has an id or library/key), loadAll() must have been called. - */ -Zotero.Search.prototype.fromJSON = function (json) { - if (!json.name) { - throw new Error("'name' property not provided for search"); - } - this.name = json.name; - - Object.keys(this.getConditions()).forEach(id => this.removeCondition(id)); - for (let i = 0; i < json.conditions.length; i++) { - let condition = json.conditions[i]; - this.addCondition( - condition.condition, - condition.operator, - condition.value - ); - } -} - - -Zotero.Search.prototype.toJSON = function (options = {}) { - var env = this._preToJSON(options); - var mode = env.mode; - - var obj = env.obj = {}; - obj.key = this.key; - obj.version = this.version; - obj.name = this.name; - var conditions = this.getConditions(); - obj.conditions = Object.keys(conditions) - .map(x => ({ - condition: conditions[x].condition - + (conditions[x].mode !== false ? "/" + conditions[x].mode : ""), - operator: conditions[x].operator, - // TODO: Change joinMode to use 'is' + 'any' instead of operator 'any'? - value: conditions[x].value ? conditions[x].value : "" - })); - return this._postToJSON(env); -} - - -/* - * Get the SQL string for the search - */ -Zotero.Search.prototype.getSQL = Zotero.Promise.coroutine(function* () { - if (!this._sql) { - yield this._buildQuery(); - } - return this._sql; -}); - - -Zotero.Search.prototype.getSQLParams = Zotero.Promise.coroutine(function* () { - if (!this._sql) { - yield this._buildQuery(); - } - return this._sqlParams; -}); - - -/* - * Batch insert - */ -Zotero.Search.idsToTempTable = function (ids) { - const N_COMBINED_INSERTS = 1000; - - var tmpTable = "tmpSearchResults_" + Zotero.randomString(8); - - return Zotero.DB.executeTransaction(function* () { - var sql = "CREATE TEMPORARY TABLE " + tmpTable + " (itemID INTEGER PRIMARY KEY)"; - yield Zotero.DB.queryAsync(sql); - - var ids2 = ids ? ids.concat() : []; - while (ids2.length) { - let chunk = ids2.splice(0, N_COMBINED_INSERTS); - let sql = 'INSERT INTO ' + tmpTable + ' VALUES ' - + chunk.map((x) => "(" + parseInt(x) + ")").join(", "); - yield Zotero.DB.queryAsync(sql, false, { debug: false }); - } - - return tmpTable; - }); -} - - -/* - * 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 - var anySQL = ''; - var anySQLParams = []; - - var conditions = []; - - for (var i in this._conditions){ - var data = Zotero.SearchConditions.get(this._conditions[i]['condition']); - - // Has a table (or 'savedSearch', which doesn't have a table but isn't special) - if (data.table || data.name == 'savedSearch' || data.name == 'tempTable') { - conditions.push({ - name: data['name'], - alias: data['name']!=this._conditions[i]['condition'] - ? this._conditions[i]['condition'] : false, - table: data['table'], - field: data['field'], - operator: this._conditions[i]['operator'], - value: this._conditions[i]['value'], - flags: data['flags'], - required: this._conditions[i]['required'] - }); - - this._hasPrimaryConditions = true; - } - - // Handle special conditions - else { - switch (data['name']){ - case 'deleted': - var deleted = this._conditions[i].operator == 'true'; - continue; - - case 'noChildren': - var noChildren = this._conditions[i]['operator']=='true'; - continue; - - case 'includeParentsAndChildren': - var includeParentsAndChildren = this._conditions[i]['operator'] == 'true'; - continue; - - case 'includeParents': - var includeParents = this._conditions[i]['operator'] == 'true'; - continue; - - case 'includeChildren': - var includeChildren = this._conditions[i]['operator'] == 'true'; - continue; - - case 'unfiled': - var unfiled = this._conditions[i]['operator'] == 'true'; - continue; - - // Search subcollections - case 'recursive': - var recursive = this._conditions[i]['operator']=='true'; - continue; - - // Join mode ('any' or 'all') - case 'joinMode': - var joinMode = this._conditions[i]['operator'].toUpperCase(); - continue; - - case 'fulltextContent': - // Handled in Search.search() - continue; - - // For quicksearch block markers - case 'blockStart': - conditions.push({name:'blockStart'}); - continue; - case 'blockEnd': - conditions.push({name:'blockEnd'}); - continue; - } - - throw ('Unhandled special condition ' + this._conditions[i]['condition']); - } - } - - // Exclude deleted items (and their child items) by default - let not = deleted ? "" : "NOT "; - let op = deleted ? "OR" : "AND"; - sql += " WHERE (" - + `itemID ${not} IN (SELECT itemID FROM deletedItems) ` - + `${op} itemID ${not}IN (SELECT itemID FROM itemNotes ` - + "WHERE parentItemID IS NOT NULL AND " - + "parentItemID IN (SELECT itemID FROM deletedItems)) " - + `${op} itemID ${not}IN (SELECT itemID FROM itemAttachments ` - + "WHERE parentItemID IS NOT NULL AND " - + "parentItemID IN (SELECT itemID FROM deletedItems))" - + ")"; - - if (noChildren){ - sql += " AND (itemID NOT IN (SELECT itemID FROM itemNotes " - + "WHERE parentItemID IS NOT NULL) AND itemID NOT IN " - + "(SELECT itemID FROM itemAttachments " - + "WHERE parentItemID IS NOT NULL))"; - } - - if (unfiled) { - sql += " AND (itemID NOT IN (SELECT itemID FROM collectionItems) " - // Exclude children - + "AND itemID NOT IN " - + "(SELECT itemID FROM itemAttachments WHERE parentItemID IS NOT NULL " - + "UNION SELECT itemID FROM itemNotes WHERE parentItemID IS NOT NULL)" - + ")"; - } - - // Limit to library search belongs to - // - // This is equivalent to adding libraryID as a search condition, - // but it works with ANY - if (this.libraryID !== null) { - sql += " AND (itemID IN (SELECT itemID FROM items WHERE libraryID=?))"; - sqlParams.push(this.libraryID); - } - - if (this._hasPrimaryConditions) { - sql += " AND "; - - for each(var condition in conditions){ - var skipOperators = false; - var openParens = 0; - var condSQL = ''; - var selectOpenParens = 0; - var condSelectSQL = ''; - var condSQLParams = []; - - // - // Special table handling - // - if (condition['table']){ - switch (condition['table']){ - default: - condSelectSQL += 'itemID ' - switch (condition['operator']){ - case 'isNot': - case 'doesNotContain': - condSelectSQL += 'NOT '; - break; - } - condSelectSQL += 'IN ('; - selectOpenParens = 1; - condSQL += 'SELECT itemID FROM ' + - condition['table'] + ' WHERE ('; - openParens = 1; - } - } - - // - // Special condition handling - // - switch (condition['name']){ - case 'field': - case 'datefield': - case 'numberfield': - if (condition['alias']) { - // Add base field - condSQLParams.push( - Zotero.ItemFields.getID(condition['alias']) - ); - var typeFields = Zotero.ItemFields.getTypeFieldsFromBase(condition['alias']); - if (typeFields) { - condSQL += 'fieldID IN (?,'; - // Add type-specific fields - for each(var fieldID in typeFields) { - condSQL += '?,'; - condSQLParams.push(fieldID); - } - condSQL = condSQL.substr(0, condSQL.length - 1); - condSQL += ') AND '; - } - else { - condSQL += 'fieldID=? AND '; - } - } - - condSQL += "valueID IN (SELECT valueID FROM " - + "itemDataValues WHERE "; - - openParens++; - break; - - case 'year': - condSQLParams.push(Zotero.ItemFields.getID('date')); - //Add base field - var dateFields = Zotero.ItemFields.getTypeFieldsFromBase('date'); - if (dateFields) { - condSQL += 'fieldID IN (?,'; - // Add type-specific date fields (dateEnacted, dateDecided, issueDate) - for each(var fieldID in dateFields) { - condSQL += '?,'; - condSQLParams.push(fieldID); - } - condSQL = condSQL.substr(0, condSQL.length - 1); - condSQL += ') AND '; - } - - condSQL += "valueID IN (SELECT valueID FROM " - + "itemDataValues WHERE "; - - openParens++; - break; - - case 'collection': - case 'savedSearch': - let obj; - let objLibraryID; - let objKey = condition.value; - let objectType = condition.name == 'collection' ? 'collection' : 'search'; - let objectTypeClass = Zotero.DataObjectUtilities.getObjectsClassForObjectType(objectType); - - // Old-style library-key hash - if (objKey.indexOf('_') != -1) { - [objLibraryID, objKey] = objKey.split('_'); - } - // libraryID assigned on search - else if (this.libraryID !== null) { - objLibraryID = this.libraryID; - } - - // If search doesn't have a libraryID, check all possible libraries - // for the collection/search - if (objLibraryID === undefined) { - let foundLibraryID = false; - for each (let c in this._conditions) { - if (c.condition == 'libraryID' && c.operator == 'is') { - foundLibraryID = true; - obj = yield objectTypeClass.getByLibraryAndKeyAsync( - c.value, objKey - ); - if (obj) { - break; - } - } - } - if (!foundLibraryID) { - Zotero.debug("WARNING: libraryID condition not found for " - + objectType + " in search", 2); - } - } - else { - obj = yield objectTypeClass.getByLibraryAndKeyAsync( - objLibraryID, objKey - ); - } - if (!obj) { - var msg = objectType.charAt(0).toUpperCase() + objectType.substr(1) - + " " + objKey + " specified in search not found"; - Zotero.debug(msg, 2); - Zotero.log(msg, 'warning', 'chrome://zotero/content/xpcom/search.js'); - if (objectType == 'search') { - continue; - } - obj = { - id: 0 - }; - } - - if (objectType == 'collection') { - var q = ['?']; - var p = [obj.id]; - - // Search descendent collections if recursive search - if (recursive){ - var descendents = obj.getDescendents(false, 'collection'); - for (let d of descendents) { - q.push('?'); - p.push(d.id); - } - } - - condSQL += "collectionID IN (" + q.join() + ")"; - condSQLParams = condSQLParams.concat(p); - } - else { - // Check if there are any post-search filters - var hasFilter = search.hasPostSearchFilter(); - - // This is an ugly and inefficient way of doing a - // subsearch, but it's necessary if there are any - // post-search filters (e.g. fulltext scanning) in the - // subsearch - // - // DEBUG: it's possible there's a query length limit here - // or that this slows things down with large libraries - // -- should probably use a temporary table instead - if (hasFilter){ - let subids = yield search.search(); - condSQL += subids.join(); - } - // Otherwise just put the SQL in a subquery - else { - condSQL += yield search.getSQL(); - let subpar = yield search.getSQLParams(); - for (let k in subpar){ - condSQLParams.push(subpar[k]); - } - } - condSQL += ")"; - } - - skipOperators = true; - break; - - case 'itemType': - condSQL += "itemTypeID IN (SELECT itemTypeID FROM itemTypesCombined WHERE "; - openParens++; - break; - - case 'fileTypeID': - var ftSQL = 'SELECT mimeType FROM fileTypeMimeTypes ' - + 'WHERE fileTypeID IN (' - + 'SELECT fileTypeID FROM fileTypes WHERE ' - + 'fileTypeID=?)'; - var patterns = yield Zotero.DB.columnQueryAsync(ftSQL, { int: condition.value }); - if (patterns) { - for each(str in patterns) { - condSQL += 'contentType LIKE ? OR '; - condSQLParams.push(str + '%'); - } - condSQL = condSQL.substring(0, condSQL.length - 4); - } - else { - throw ("Invalid fileTypeID '" + condition.value + "' specified in search.js") - } - skipOperators = true; - break; - - case 'tag': - condSQL += "tagID IN (SELECT tagID FROM tags WHERE "; - openParens++; - break; - - case 'creator': - case 'lastName': - condSQL += "creatorID IN (SELECT creatorID FROM creators WHERE "; - openParens++; - break; - - case 'childNote': - condSQL += "itemID IN (SELECT parentItemID FROM " - + "itemNotes WHERE "; - openParens++; - break; - - case 'fulltextWord': - condSQL += "wordID IN (SELECT wordID FROM fulltextWords " - + "WHERE "; - openParens++; - break; - - case 'tempTable': - if (!condition.value.match(/^[a-zA-Z0-9]+$/)) { - throw ("Invalid temp table '" + condition.value + "'"); - } - condSQL += "itemID IN (SELECT id FROM " + condition.value + ")"; - skipOperators = true; - break; - - // For quicksearch blocks - case 'blockStart': - case 'blockEnd': - skipOperators = true; - break; - } - - if (!skipOperators){ - // Special handling for date fields - // - // Note: We assume full datetimes are already UTC and don't - // need to be handled specially - if ((condition['name']=='dateAdded' || - condition['name']=='dateModified' || - condition['name']=='datefield') && - !Zotero.Date.isSQLDateTime(condition['value'])){ - - // TODO: document these flags - var parseDate = null; - var alt = null; - var useFreeform = null; - - switch (condition['operator']){ - case 'is': - case 'isNot': - var parseDate = true; - var alt = '__'; - var useFreeform = true; - break; - - case 'isBefore': - var parseDate = true; - var alt = '00'; - var useFreeform = false; - break; - - case 'isAfter': - var parseDate = true; - // '__' used here just so the > string comparison - // doesn't match dates in the specified year - var alt = '__'; - var useFreeform = false; - break; - - case 'isInTheLast': - var parseDate = false; - break; - - default: - throw ('Invalid date field operator in search'); - } - - // Convert stored UTC dates to localtime - // - // It'd be nice not to deal with time zones here at all, - // but otherwise searching for the date part of a field - // stored as UTC that wraps midnight would be unsuccessful - if (condition['name']=='dateAdded' || - condition['name']=='dateModified' || - condition['alias']=='accessDate'){ - condSQL += "DATE(" + condition['field'] + ", 'localtime')"; - } - // Only use first (SQL) part of multipart dates - else { - condSQL += "SUBSTR(" + condition['field'] + ", 1, 10)"; - } - - if (parseDate){ - var go = false; - var dateparts = Zotero.Date.strToDate(condition.value); - - // Search on SQL date -- underscore is - // single-character wildcard - // - // If isBefore or isAfter, month and day fall back - // to '00' so that a search for just a year works - // (and no year will just not find anything) - var sqldate = dateparts.year ? - Zotero.Utilities.lpad(dateparts.year, '0', 4) : '____'; - sqldate += '-' - sqldate += dateparts.month || dateparts.month === 0 ? - Zotero.Utilities.lpad(dateparts.month + 1, '0', 2) : alt; - sqldate += '-'; - sqldate += dateparts.day ? - Zotero.Utilities.lpad(dateparts.day, '0', 2) : alt; - - if (sqldate!='____-__-__'){ - go = true; - - switch (condition['operator']){ - case 'is': - case 'isNot': - condSQL += ' LIKE ?'; - break; - - case 'isBefore': - condSQL += '<?'; - condSQL += ' AND ' + condition['field'] + - ">'0000-00-00'"; - break; - - case 'isAfter': - condSQL += '>?'; - break; - } - - condSQLParams.push({string:sqldate}); - } - - // Search for any remaining parts individually - if (useFreeform && dateparts['part']){ - go = true; - var parts = dateparts['part'].split(' '); - for each (var part in parts){ - condSQL += " AND SUBSTR(" + condition['field'] + ", 12, 100)"; - condSQL += " LIKE ?"; - condSQLParams.push('%' + part + '%'); - } - } - - // If neither part used, invalidate clause - if (!go){ - condSQL += '=0'; - } - } - - else { - switch (condition['operator']){ - case 'isInTheLast': - condSQL += ">DATE('NOW', 'localtime', ?)"; // e.g. ('NOW', '-10 DAYS') - condSQLParams.push({string: '-' + condition['value']}); - break; - } - } - } - - // Non-date fields - else { - switch (condition.operator) { - // Cast strings as integers for < and > comparisons, - // at least until - case 'isLessThan': - case 'isGreaterThan': - condSQL += "CAST(" + condition['field'] + " AS INT)"; - // Make sure either field is an integer or - // converting to an integer and back to a string - // yields the same result (i.e. it's numeric) - var opAppend = " AND (TYPEOF(" - + condition['field'] + ") = 'integer' OR " - + "CAST(" - + "CAST(" + condition['field'] + " AS INT)" - + " AS STRING) = " + condition['field'] + ")" - break; - - default: - condSQL += condition['field']; - } - - switch (condition['operator']){ - case 'contains': - case 'doesNotContain': // excluded with NOT IN above - condSQL += ' LIKE ?'; - // For fields with 'leftbound' flag, perform a - // leftbound search even for 'contains' condition - if (condition['flags'] && - condition['flags']['leftbound'] && - Zotero.Prefs.get('search.useLeftBound')) { - condSQLParams.push(condition['value'] + '%'); - } - else { - condSQLParams.push('%' + condition['value'] + '%'); - } - break; - - case 'is': - case 'isNot': // excluded with NOT IN above - // Automatically cast values which might - // have been stored as integers - if (condition.value && typeof condition.value == 'string' - && condition.value.match(/^[1-9]+[0-9]*$/)) { - condSQL += ' LIKE ?'; - } - else if (condition.value === null) { - condSQL += ' IS NULL'; - break; - } - else { - condSQL += '=?'; - } - condSQLParams.push(condition['value']); - break; - - case 'beginsWith': - condSQL += ' LIKE ?'; - condSQLParams.push(condition['value'] + '%'); - break; - - case 'isLessThan': - condSQL += '<?'; - condSQLParams.push({int:condition['value']}); - condSQL += opAppend; - break; - - case 'isGreaterThan': - condSQL += '>?'; - condSQLParams.push({int:condition['value']}); - condSQL += opAppend; - break; - - // Next two only used with full datetimes - case 'isBefore': - condSQL += '<?'; - condSQLParams.push({string:condition['value']}); - break; - - case 'isAfter': - condSQL += '>?'; - condSQLParams.push({string:condition['value']}); - break; - } - } - } - - // Close open parentheses - for (var k=openParens; k>0; k--){ - condSQL += ')'; - } - - if (includeParentsAndChildren || includeParents) { - var parentSQL = "SELECT itemID FROM items WHERE " - + "itemID IN (SELECT parentItemID FROM itemAttachments " - + "WHERE itemID IN (" + condSQL + ")) " - + "OR itemID IN (SELECT parentItemID FROM itemNotes " - + "WHERE itemID IN (" + condSQL + ")) "; - var parentSQLParams = condSQLParams.concat(condSQLParams); - } - - if (includeParentsAndChildren || includeChildren) { - var childrenSQL = "SELECT itemID FROM itemAttachments WHERE " - + "parentItemID IN (" + condSQL + ") UNION " - + "SELECT itemID FROM itemNotes " - + "WHERE parentItemID IN (" + condSQL + ")"; - var childSQLParams = condSQLParams.concat(condSQLParams); - } - - if (includeParentsAndChildren || includeParents) { - condSQL += " UNION " + parentSQL; - condSQLParams = condSQLParams.concat(parentSQLParams); - } - - if (includeParentsAndChildren || includeChildren) { - condSQL += " UNION " + childrenSQL; - condSQLParams = condSQLParams.concat(childSQLParams); - } - - condSQL = condSelectSQL + condSQL; - - // Close open parentheses - for (var k=selectOpenParens; k>0; k--) { - condSQL += ')'; - } - - // Little hack to support multiple quicksearch words - if (condition['name'] == 'blockStart') { - var inQS = true; - var qsSQL = ''; - var qsParams = []; - continue; - } - else if (condition['name'] == 'blockEnd') { - inQS = false; - // Strip ' OR ' from last condition - qsSQL = qsSQL.substring(0, qsSQL.length-4); - - // Add to existing quicksearch words - if (!quicksearchSQLSet) { - var quicksearchSQLSet = []; - var quicksearchParamsSet = []; - } - quicksearchSQLSet.push(qsSQL); - quicksearchParamsSet.push(qsParams); - } - else if (inQS) { - qsSQL += condSQL + ' OR '; - qsParams = qsParams.concat(condSQLParams); - } - // Keep non-required conditions separate if in ANY mode - else if (!condition['required'] && joinMode == 'ANY') { - anySQL += condSQL + ' OR '; - anySQLParams = anySQLParams.concat(condSQLParams); - } - else { - condSQL += ' AND '; - sql += condSQL; - sqlParams = sqlParams.concat(condSQLParams); - } - } - - // Add on ANY conditions - if (anySQL){ - sql += '(' + anySQL; - sqlParams = sqlParams.concat(anySQLParams); - sql = sql.substring(0, sql.length-4); // remove last ' OR ' - sql += ')'; - } - else { - sql = sql.substring(0, sql.length-5); // remove last ' AND ' - } - - // Add on quicksearch conditions - if (quicksearchSQLSet) { - sql = "SELECT itemID FROM items WHERE itemID IN (" + sql + ") " - + "AND ((" + quicksearchSQLSet.join(') AND (') + "))"; - - for (var k=0; k<quicksearchParamsSet.length; k++) { - sqlParams = sqlParams.concat(quicksearchParamsSet[k]); - } - } - } - - this._sql = sql; - this._sqlParams = sqlParams.length ? sqlParams : false; -}); - -Zotero.Searches = function() { - this.constructor = null; - - this._ZDO_object = 'search'; - this._ZDO_id = 'savedSearchID'; - this._ZDO_table = 'savedSearches'; - - this._primaryDataSQLParts = { - savedSearchID: "O.savedSearchID", - name: "O.savedSearchName AS name", - libraryID: "O.libraryID", - key: "O.key", - version: "O.version", - synced: "O.synced" - } - - this._primaryDataSQLFrom = "FROM savedSearches O"; - - this.init = Zotero.Promise.coroutine(function* () { - yield Zotero.DataObjects.prototype.init.apply(this); - yield Zotero.SearchConditions.init(); - }); - - - /** - * Returns an array of Zotero.Search objects, ordered by name - * - * @param {Integer} [libraryID] - */ - this.getAll = Zotero.Promise.coroutine(function* (libraryID) { - var sql = "SELECT savedSearchID FROM savedSearches WHERE libraryID=?"; - var ids = yield Zotero.DB.columnQueryAsync(sql, libraryID); - if (!ids.length) { - return [] - } - - var searches = this.get(ids); - // Do proper collation sort - var collation = Zotero.getLocaleCollation(); - searches.sort(function (a, b) { - return collation.compareString(1, a.name, b.name); - }); - return searches; - }); - - - this.getPrimaryDataSQL = function () { - // This should be the same as the query in Zotero.Search.loadPrimaryData(), - // just without a specific savedSearchID - return "SELECT " - + Object.keys(this._primaryDataSQLParts).map(key => this._primaryDataSQLParts[key]).join(", ") + " " - + "FROM savedSearches O WHERE 1"; - } - - - this._loadConditions = Zotero.Promise.coroutine(function* (libraryID, ids, idSQL) { - var sql = "SELECT savedSearchID, searchConditionID, condition, operator, value, required " - + "FROM savedSearches LEFT JOIN savedSearchConditions USING (savedSearchID) " - + "WHERE libraryID=?" + idSQL - + "ORDER BY savedSearchID, searchConditionID"; - var params = [libraryID]; - var lastID = null; - var rows = []; - var setRows = function (searchID, rows) { - var search = this._objectCache[searchID]; - if (!search) { - throw new Error("Search " + searchID + " not found"); - } - - search._conditions = {}; - - if (rows.length) { - search._maxSearchConditionID = rows[rows.length - 1].searchConditionID; - } - - // Reindex conditions, in case they're not contiguous in the DB - for (let i = 0; i < rows.length; i++) { - let condition = rows[i]; - - // Parse "condition[/mode]" - let [conditionName, mode] = Zotero.SearchConditions.parseCondition(condition.condition); - - let cond = Zotero.SearchConditions.get(conditionName); - if (!cond || cond.noLoad) { - Zotero.debug("Invalid saved search condition '" + conditionName + "' -- skipping", 2); - continue; - } - - // Convert itemTypeID to itemType - // - // TEMP: This can be removed at some point - if (conditionName == 'itemTypeID') { - conditionName = 'itemType'; - condition.value = Zotero.ItemTypes.getName(condition.value); - } - - search._conditions[i] = { - id: i, - condition: conditionName, - mode: mode, - operator: condition.operator, - value: condition.value, - required: !!condition.required - }; - } - search._loaded.conditions = true; - search._clearChanged('conditions'); - }.bind(this); - - yield Zotero.DB.queryAsync( - sql, - params, - { - noCache: ids.length != 1, - onRow: function (row) { - let searchID = row.getResultByIndex(0); - - if (lastID && searchID != lastID) { - setRows(lastID, rows); - rows = []; - } - - lastID = searchID; - let searchConditionID = row.getResultByIndex(1); - // No conditions - if (searchConditionID === null) { - return; - } - rows.push({ - searchConditionID, - condition: row.getResultByIndex(2), - operator: row.getResultByIndex(3), - value: row.getResultByIndex(4), - required: row.getResultByIndex(5) - }); - }.bind(this) - } - ); - if (lastID) { - setRows(lastID, rows); - } - }); - - Zotero.DataObjects.call(this); - - return this; -}.bind(Object.create(Zotero.DataObjects.prototype))(); - - - -Zotero.SearchConditions = new function(){ - this.get = get; - this.getStandardConditions = getStandardConditions; - this.hasOperator = hasOperator; - this.getLocalizedName = getLocalizedName; - this.parseSearchString = parseSearchString; - this.parseCondition = parseCondition; - - var _initialized = false; - var _conditions; - var _standardConditions; - - var self = this; - - /* - * Define the advanced search operators - */ - var _operators = { - // Standard -- these need to match those in zoterosearch.xml - is: true, - isNot: true, - beginsWith: true, - contains: true, - doesNotContain: true, - isLessThan: true, - isGreaterThan: true, - isBefore: true, - isAfter: true, - isInTheLast: true, - - // Special - any: true, - all: true, - true: true, - false: true - }; - - - /* - * Define and set up the available advanced search conditions - * - * Flags: - * - special (don't show in search window menu) - * - template (special handling) - * - noLoad (can't load from saved search) - */ - this.init = Zotero.Promise.coroutine(function* () { - var conditions = [ - // - // Special conditions - // - { - name: 'deleted', - operators: { - true: true, - false: true - } - }, - - // Don't include child items - { - name: 'noChildren', - operators: { - true: true, - false: true - } - }, - - { - name: 'unfiled', - operators: { - true: true, - false: true - } - }, - - { - name: 'includeParentsAndChildren', - operators: { - true: true, - false: true - } - }, - - { - name: 'includeParents', - operators: { - true: true, - false: true - } - }, - - { - name: 'includeChildren', - operators: { - true: true, - false: true - } - }, - - // Search recursively within collections - { - name: 'recursive', - operators: { - true: true, - false: true - } - }, - - // Join mode - { - name: 'joinMode', - operators: { - any: true, - all: true - } - }, - - { - name: 'quicksearch-titleCreatorYear', - operators: { - is: true, - isNot: true, - contains: true, - doesNotContain: true - }, - noLoad: true - }, - - { - name: 'quicksearch-fields', - operators: { - is: true, - isNot: true, - contains: true, - doesNotContain: true - }, - noLoad: true - }, - - { - name: 'quicksearch-everything', - operators: { - is: true, - isNot: true, - contains: true, - doesNotContain: true - }, - noLoad: true - }, - - // Deprecated - { - name: 'quicksearch', - operators: { - is: true, - isNot: true, - contains: true, - doesNotContain: true - }, - noLoad: true - }, - - // Quicksearch block markers - { - name: 'blockStart', - noLoad: true - }, - - { - name: 'blockEnd', - noLoad: true - }, - - // Shortcuts for adding collections and searches by id - { - name: 'collectionID', - operators: { - is: true, - isNot: true - }, - noLoad: true - }, - - { - name: 'savedSearchID', - operators: { - is: true, - isNot: true - }, - noLoad: true - }, - - - // - // Standard conditions - // - - // Collection id to search within - { - name: 'collection', - operators: { - is: true, - isNot: true - }, - table: 'collectionItems', - field: 'collectionID' - }, - - // Saved search to search within - { - name: 'savedSearch', - operators: { - is: true, - isNot: true - }, - special: false - }, - - { - name: 'dateAdded', - operators: { - is: true, - isNot: true, - isBefore: true, - isAfter: true, - isInTheLast: true - }, - table: 'items', - field: 'dateAdded' - }, - - { - name: 'dateModified', - operators: { - is: true, - isNot: true, - isBefore: true, - isAfter: true, - isInTheLast: true - }, - table: 'items', - field: 'dateModified' - }, - - // Deprecated - { - name: 'itemTypeID', - operators: { - is: true, - isNot: true - }, - table: 'items', - field: 'itemTypeID', - special: true - }, - - { - name: 'itemType', - operators: { - is: true, - isNot: true - }, - table: 'items', - field: 'typeName' - }, - - { - name: 'fileTypeID', - operators: { - is: true, - isNot: true - }, - table: 'itemAttachments', - field: 'fileTypeID' - }, - - { - name: 'tagID', - operators: { - is: true, - isNot: true - }, - table: 'itemTags', - field: 'tagID', - special: true - }, - - { - name: 'tag', - operators: { - is: true, - isNot: true, - contains: true, - doesNotContain: true - }, - table: 'itemTags', - field: 'name' - }, - - { - name: 'note', - operators: { - contains: true, - doesNotContain: true - }, - table: 'itemNotes', - field: 'note' - }, - - { - name: 'childNote', - operators: { - contains: true, - doesNotContain: true - }, - table: 'items', - field: 'note' - }, - - { - name: 'creator', - operators: { - is: true, - isNot: true, - contains: true, - doesNotContain: true - }, - table: 'itemCreators', - field: "TRIM(firstName || ' ' || lastName)" - }, - - { - name: 'lastName', - operators: { - is: true, - isNot: true, - contains: true, - doesNotContain: true - }, - table: 'itemCreators', - field: 'lastName', - special: true - }, - - { - name: 'field', - operators: { - is: true, - isNot: true, - contains: true, - doesNotContain: true - }, - table: 'itemData', - field: 'value', - aliases: yield Zotero.DB.columnQueryAsync("SELECT fieldName FROM fieldsCombined " - + "WHERE fieldName NOT IN ('accessDate', 'date', 'pages', " - + "'section','seriesNumber','issue')"), - template: true // mark for special handling - }, - - { - name: 'datefield', - operators: { - is: true, - isNot: true, - isBefore: true, - isAfter: true, - isInTheLast: true - }, - table: 'itemData', - field: 'value', - aliases: ['accessDate', 'date', 'dateDue', 'accepted'], // TEMP - NSF - template: true // mark for special handling - }, - - { - name: 'year', - operators: { - is: true, - isNot: true, - contains: true, - doesNotContain: true - }, - table: 'itemData', - field: 'SUBSTR(value, 1, 4)', - special: true - }, - - { - name: 'numberfield', - operators: { - is: true, - isNot: true, - contains: true, - doesNotContain: true, - isLessThan: true, - isGreaterThan: true - }, - table: 'itemData', - field: 'value', - aliases: ['pages', 'numPages', 'numberOfVolumes', 'section', 'seriesNumber','issue'], - template: true // mark for special handling - }, - - { - name: 'libraryID', - operators: { - is: true, - isNot: true - }, - table: 'items', - field: 'libraryID', - special: true, - noLoad: true - }, - - { - name: 'key', - operators: { - is: true, - isNot: true, - beginsWith: true - }, - table: 'items', - field: 'key', - special: true, - noLoad: true - }, - - { - name: 'itemID', - operators: { - is: true, - isNot: true - }, - table: 'items', - field: 'itemID', - special: true, - noLoad: true - }, - - { - name: 'annotation', - operators: { - contains: true, - doesNotContain: true - }, - table: 'annotations', - field: 'text' - }, - - { - name: 'fulltextWord', - operators: { - contains: true, - doesNotContain: true - }, - table: 'fulltextItemWords', - field: 'word', - flags: { - leftbound: true - }, - special: true - }, - - { - name: 'fulltextContent', - operators: { - contains: true, - doesNotContain: true - }, - special: false - }, - - { - name: 'tempTable', - operators: { - is: true - } - } - ]; - - // Index conditions by name and aliases - _conditions = {}; - for (var i in conditions) { - _conditions[conditions[i]['name']] = conditions[i]; - if (conditions[i]['aliases']) { - for (var j in conditions[i]['aliases']) { - // TEMP - NSF - switch (conditions[i]['aliases'][j]) { - case 'dateDue': - case 'accepted': - if (!Zotero.ItemTypes.getID('nsfReviewer')) { - continue; - } - } - _conditions[conditions[i]['aliases'][j]] = conditions[i]; - } - } - _conditions[conditions[i]['name']] = conditions[i]; - } - - _standardConditions = []; - - var baseMappedFields = Zotero.ItemFields.getBaseMappedFields(); - var locale = Zotero.locale; - - // Separate standard conditions for menu display - for (var i in _conditions){ - var fieldID = false; - if (['field', 'datefield', 'numberfield'].indexOf(_conditions[i]['name']) != -1) { - fieldID = Zotero.ItemFields.getID(i); - } - - // If explicitly special... - if (_conditions[i]['special'] || - // or a template master (e.g. 'field')... - (_conditions[i]['template'] && i==_conditions[i]['name']) || - // or no table and not explicitly unspecial... - (!_conditions[i]['table'] && - typeof _conditions[i]['special'] == 'undefined') || - // or field is a type-specific version of a base field... - (fieldID && baseMappedFields.indexOf(fieldID) != -1)) { - // ...then skip - continue; - } - - let localized = self.getLocalizedName(i); - // Hack to use a different name for "issue" in French locale, - // where 'number' and 'issue' are translated the same - // https://forums.zotero.org/discussion/14942/ - if (fieldID == 5 && locale.substr(0, 2).toLowerCase() == 'fr') { - localized = "Num\u00E9ro (p\u00E9riodique)"; - } - - _standardConditions.push({ - name: i, - localized: localized, - operators: _conditions[i]['operators'], - flags: _conditions[i]['flags'] - }); - } - - var collation = Zotero.getLocaleCollation(); - _standardConditions.sort(function(a, b) { - return collation.compareString(1, a.localized, b.localized); - }); - }); - - - /* - * Get condition data - */ - function get(condition){ - return _conditions[condition]; - } - - - /* - * Returns array of possible conditions - * - * Does not include special conditions, only ones that would show in a drop-down list - */ - function getStandardConditions(){ - // TODO: return copy instead - return _standardConditions; - } - - - /* - * Check if an operator is valid for a given condition - */ - 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]){ - let e = new Error("Invalid condition '" + condition + "' in hasOperator()"); - e.name = "ZoteroUnknownFieldError"; - throw e; - } - - if (!operator && typeof _conditions[condition]['operators'] == 'undefined'){ - return true; - } - - return !!_conditions[condition]['operators'][operator]; - } - - - function getLocalizedName(str) { - // TEMP - if (str == 'itemType') { - str = 'itemTypeID'; - } - - try { - return Zotero.getString('searchConditions.' + str) - } - catch (e) { - return Zotero.ItemFields.getLocalizedString(null, str); - } - } - - - /** - * Compare two API JSON condition objects - */ - this.equals = function (data1, data2) { - return data1.condition === data2.condition - && data1.operator === data2.operator - && data1.value === data2.value; - } - - - /* - * Parses a search into words and "double-quoted phrases" - * - * Also strips unpaired quotes at the beginning and end of words - * - * Returns array of objects containing 'text' and 'inQuotes' - */ - function parseSearchString(str) { - var parts = str.split(/\s*("[^"]*")\s*|"\s|\s"|^"|"$|'\s|\s'|^'|'$|\s/m); - var parsed = []; - - for (var i in parts) { - var part = parts[i]; - if (!part || !part.length) { - continue; - } - - if (part.charAt(0)=='"' && part.charAt(part.length-1)=='"') { - parsed.push({ - text: part.substring(1, part.length-1), - inQuotes: true - }); - } - else { - parsed.push({ - text: part, - inQuotes: false - }); - } - } - - return parsed; - } - - - function parseCondition(condition){ - var mode = false; - var pos = condition.indexOf('/'); - if (pos != -1){ - mode = condition.substr(pos+1); - condition = condition.substr(0, pos); - } - - return [condition, mode]; - } -} diff --git a/components/zotero-service.js b/components/zotero-service.js @@ -82,6 +82,9 @@ const xpcomFilesLocal = [ 'data/groups', 'data/itemFields', 'data/relations', + 'data/search', + 'data/searchConditions', + 'data/searches', 'data/tags', 'db', 'duplicates', @@ -98,7 +101,6 @@ const xpcomFilesLocal = [ 'report', 'router', 'schema', - 'search', 'server', 'style', 'sync',