diff --git a/chrome/content/zotero/xpcom/search.js b/chrome/content/zotero/xpcom/data/search.js
similarity index 73%
rename from chrome/content/zotero/xpcom/search.js
rename to chrome/content/zotero/xpcom/data/search.js
index 261ed8561..56162b225 100644
--- a/chrome/content/zotero/xpcom/search.js
+++ b/chrome/content/zotero/xpcom/data/search.js
@@ -1592,819 +1592,3 @@ Zotero.Search.prototype._buildQuery = Zotero.Promise.coroutine(function* () {
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/chrome/content/zotero/xpcom/data/searchConditions.js b/chrome/content/zotero/xpcom/data/searchConditions.js
new file mode 100644
index 000000000..bdf20a1a5
--- /dev/null
+++ 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 .
+
+ ***** 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
new file mode 100644
index 000000000..c996aab30
--- /dev/null
+++ 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 .
+
+ ***** 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/components/zotero-service.js b/components/zotero-service.js
index ca6642086..535ff705a 100644
--- 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',