Remove synchronous database methods

This required doing additional caching at startup (e.g., item types and fields)
so that various methods can remain synchronous.

This lets us switch back to using the current Sqlite.jsm. Previously we were
bundling the Fx24 version, which avoided freezes with locking_mode=EXCLUSIVE
with both sync and async queries.

Known broken things:

  - Autocomplete
  - Database backup
  - UDFs (e.g., REGEXP function used in Zotero.DB.getNextName())
This commit is contained in:
Dan Stillman 2014-08-09 18:01:28 -04:00
parent 86bc20c4e9
commit f5896dbb8d
29 changed files with 733 additions and 2966 deletions

View file

@ -28,7 +28,6 @@ var ZoteroAdvancedSearch = new function() {
this.onLoad = onLoad;
this.search = search;
this.clear = clear;
this.save = save;
this.onDblClick = onDblClick;
this.onUnload = onUnload;
@ -104,13 +103,13 @@ var ZoteroAdvancedSearch = new function() {
}
function save() {
this.save = Zotero.Promise.coroutine(function* () {
_searchBox.updateSearch();
var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Components.interfaces.nsIPromptService);
var untitled = Zotero.DB.getNextName(
var untitled = yield Zotero.DB.getNextName(
_searchBox.search.libraryID,
'savedSearches',
'savedSearchName',
@ -132,15 +131,12 @@ var ZoteroAdvancedSearch = new function() {
name.value = untitled;
}
return _searchBox.search.clone()
.then(function (s) {
s.name = name.value;
return s.save();
})
.then(function () {
window.close()
});
}
var s = yield _searchBox.search.clone();
s.name = name.value;
yield s.save();
window.close()
});
this.onLibraryChange = function (libraryID) {

View file

@ -159,7 +159,7 @@ var Zotero_Browser = new function() {
var tab = _getTabObject(Zotero_Browser.tabbrowser.selectedBrowser);
if(tab.page.translators && tab.page.translators.length) {
tab.page.translate.setTranslator(translator || tab.page.translators[0]);
Zotero_Browser.performTranslation(tab.page.translate);
Zotero_Browser.performTranslation(tab.page.translate); // TODO: async
}
}
@ -493,7 +493,7 @@ var Zotero_Browser = new function() {
* have been called
* @param {Zotero.Translate} translate
*/
this.performTranslation = function(translate, libraryID, collection) {
this.performTranslation = Zotero.Promise.coroutine(function* (translate, libraryID, collection) {
if (Zotero.locked) {
Zotero_Browser.progress.changeHeadline(Zotero.getString("ingester.scrapeError"));
var desc = Zotero.localeJoin([
@ -506,15 +506,7 @@ var Zotero_Browser = new function() {
return;
}
if (!Zotero.stateCheck()) {
Zotero_Browser.progress.changeHeadline(Zotero.getString("ingester.scrapeError"));
var desc = Zotero.getString("ingester.scrapeErrorDescription.previousError")
+ ' ' + Zotero.getString("general.restartFirefoxAndTryAgain", Zotero.appName);
Zotero_Browser.progress.addDescription(desc);
Zotero_Browser.progress.show();
Zotero_Browser.progress.startCloseTimer(8000);
return;
}
yield Zotero.DB.waitForTransaction();
Zotero_Browser.progress.show();
Zotero_Browser.isScraping = true;
@ -615,7 +607,7 @@ var Zotero_Browser = new function() {
});
translate.translate(libraryID);
}
});
//////////////////////////////////////////////////////////////////////////////

View file

@ -47,11 +47,11 @@ Zotero_Preferences.Advanced = {
},
runIntegrityCheck: function () {
runIntegrityCheck: Zotero.Promise.coroutine(function* () {
var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Components.interfaces.nsIPromptService);
var ok = Zotero.DB.integrityCheck();
var ok = yield Zotero.DB.integrityCheck();
if (ok) {
ok = Zotero.Schema.integrityCheck();
if (!ok) {
@ -110,7 +110,7 @@ Zotero_Preferences.Advanced = {
Zotero.getString('general.' + str),
Zotero.getString('db.integrityCheck.' + str)
+ (!ok ? "\n\n" + Zotero.getString('db.integrityCheck.dbRepairTool') : ''));
},
}),
resetTranslatorsAndStyles: function () {

View file

@ -122,7 +122,7 @@ var Zotero_ProxyEditor = new function() {
window, Zotero.getString("proxies.error"),
Zotero.getString("proxies.error." + error, hasErrors)
);
if(window.arguments && window.arguments[0]) proxy.revert();
if(window.arguments && window.arguments[0]) proxy.revert(); // async
return false;
}
proxy.save(true);

View file

@ -82,6 +82,7 @@ Zotero.Attachments = new function(){
yield _postProcessFile(itemID, newFile, contentType);
}.bind(this))
.catch(function (e) {
Zotero.debug(e, 1);
var msg = "Failed importing file " + file.path;
Components.utils.reportError(msg);
Zotero.debug(msg, 1);

View file

@ -43,8 +43,8 @@
*
*/
Zotero.CachedTypes = function() {
var _types = [];
var _typesLoaded;
this._types = null;
this._typesArray = null;
var self = this;
// Override these variables in child classes
@ -57,11 +57,38 @@ Zotero.CachedTypes = function() {
this.getName = getName;
this.getID = getID;
this.getTypes = getTypes;
this.init = Zotero.Promise.coroutine(function* () {
this._types = {};
this._typesArray = [];
var types = yield this._getTypesFromDB();
for (let i=0; i<types.length; i++) {
let type = types[i];
// Store as both id and name for access by either
var typeData = {
id: types[i].id,
name: types[i].name,
custom: this._hasCustom ? !!types[i].custom : false
}
this._types['_' + types[i].id] = typeData;
if (this._ignoreCase) {
this._types['_' + types[i].name.toLowerCase()] = this._types['_' + types[i].id];
}
else {
this._types['_' + types[i].name] = this._types['_' + types[i].id];
}
this._typesArray.push(typeData);
}
});
function getName(idOrName) {
if (!_typesLoaded) {
_load();
if (!this._types) {
throw new Zotero.Exception.UnloadedDataException(
this._typeDesc[0].toUpperCase() + this._typeDesc.substr(1) + " data not yet loaded"
);
}
if (this._ignoreCase) {
@ -69,19 +96,21 @@ Zotero.CachedTypes = function() {
idOrName = idOrName.toLowerCase();
}
if (!_types['_' + idOrName]) {
if (!this._types['_' + idOrName]) {
Zotero.debug('Invalid ' + this._typeDesc + ' ' + idOrName, 1);
Zotero.debug((new Error()).stack, 1);
return '';
}
return _types['_' + idOrName]['name'];
return this._types['_' + idOrName]['name'];
}
function getID(idOrName) {
if (!_typesLoaded) {
_load();
if (!this._types) {
throw new Zotero.Exception.UnloadedDataException(
this._typeDesc[0].toUpperCase() + this._typeDesc.substr(1) + " data not yet loaded"
);
}
if (this._ignoreCase) {
@ -89,18 +118,37 @@ Zotero.CachedTypes = function() {
idOrName = idOrName.toLowerCase();
}
if (!_types['_' + idOrName]) {
if (!this._types['_' + idOrName]) {
Zotero.debug('Invalid ' + this._typeDesc + ' ' + idOrName, 1);
Zotero.debug((new Error()).stack, 1);
return false;
}
return _types['_' + idOrName]['id'];
return this._types['_' + idOrName]['id'];
}
function getTypes(where, params) {
return Zotero.DB.query(
this.getTypes = function () {
if (!this._typesArray) {
throw new Zotero.Exception.UnloadedDataException(
this._typeDesc[0].toUpperCase() + this._typeDesc.substr(1) + " data not yet loaded"
);
}
return this._typesArray;
}
// Currently used only for item types
this.isCustom = function (idOrName) {
return this._types['_' + idOrName] && this._types['_' + idOrName].custom ? this._types['_' + idOrName].custom : false;
}
/**
* @return {Promise}
*/
this._getTypesFromDB = function (where, params) {
return Zotero.DB.queryAsync(
'SELECT ' + this._idCol + ' AS id, '
+ this._nameCol + ' AS name'
+ (this._hasCustom ? ', custom' : '')
@ -108,46 +156,8 @@ Zotero.CachedTypes = function() {
+ (where ? ' ' + where : ''),
params ? params : false
);
}
// Currently used only for item types
this.isCustom = function (idOrName) {
if (!_typesLoaded) {
_load();
}
return _types['_' + idOrName] && _types['_' + idOrName].custom ? _types['_' + idOrName].custom : false;
}
this.reload = function () {
_typesLoaded = false;
}
function _load() {
_types = [];
var types = self.getTypes();
for (var i in types) {
// Store as both id and name for access by either
var typeData = {
id: types[i]['id'],
name: types[i]['name'],
custom: types[i].custom ? types[i].custom : false
}
_types['_' + types[i]['id']] = typeData;
if (self._ignoreCase) {
_types['_' + types[i]['name'].toLowerCase()] = _types['_' + types[i]['id']];
}
else {
_types['_' + types[i]['name']] = _types['_' + types[i]['id']];
}
}
_typesLoaded = true;
}
};
}
@ -169,22 +179,35 @@ Zotero.CreatorTypes = new function() {
var _creatorTypesByItemType = {};
var _isValidForItemType = {};
function getTypesForItemType(itemTypeID) {
if (_creatorTypesByItemType[itemTypeID]) {
return _creatorTypesByItemType[itemTypeID];
}
this.init = Zotero.Promise.coroutine(function* () {
yield this.constructor.prototype.init.apply(this);
var sql = "SELECT creatorTypeID AS id, creatorType AS name "
var sql = "SELECT itemTypeID, creatorTypeID AS id, creatorType AS name "
+ "FROM itemTypeCreatorTypes NATURAL JOIN creatorTypes "
// DEBUG: sort needs to be on localized strings in itemPane.js
// (though still put primary field at top)
+ "WHERE itemTypeID=? ORDER BY primaryField=1 DESC, name";
var types = Zotero.DB.query(sql, itemTypeID);
if (!types) {
types = [];
+ "ORDER BY primaryField=1 DESC, name";
var rows = yield Zotero.DB.queryAsync(sql);
for (let i=0; i<rows.length; i++) {
let row = rows[i];
let itemTypeID = row.itemTypeID;
if (!_creatorTypesByItemType[itemTypeID]) {
_creatorTypesByItemType[itemTypeID] = [];
}
_creatorTypesByItemType[itemTypeID].push({
id: row.id,
name: row.name
});
}
});
function getTypesForItemType(itemTypeID) {
if (!_creatorTypesByItemType[itemTypeID]) {
throw new Error("Creator types not loaded for itemTypeID " + itemTypeID);
}
_creatorTypesByItemType[itemTypeID] = types;
return _creatorTypesByItemType[itemTypeID];
}
@ -245,10 +268,6 @@ Zotero.ItemTypes = new function() {
Zotero.CachedTypes.apply(this, arguments);
this.constructor.prototype = new Zotero.CachedTypes();
this.getPrimaryTypes = getPrimaryTypes;
this.getSecondaryTypes = getSecondaryTypes;
this.getHiddenTypes = getHiddenTypes;
this.getLocalizedString = getLocalizedString;
this.getImageSrc = getImageSrc;
this.customIDOffset = 10000;
@ -259,10 +278,18 @@ Zotero.ItemTypes = new function() {
this._table = 'itemTypesCombined';
this._hasCustom = true;
var _primaryTypes;
var _secondaryTypes;
var _hiddenTypes;
var _customImages = {};
var _customLabels = {};
function getPrimaryTypes() {
this.init = Zotero.Promise.coroutine(function* () {
yield this.constructor.prototype.init.apply(this);
// Primary types
var limit = 5;
// TODO: get rid of ' AND itemTypeID!=5' and just remove display=2
@ -294,31 +321,57 @@ Zotero.ItemTypes = new function() {
params = false;
}
sql += 'LIMIT ' + limit;
_primaryTypes = yield this._getTypesFromDB(sql, params);
return this.getTypes(sql, params);
// Secondary types
_secondaryTypes = yield this._getTypesFromDB('WHERE display IN (1,2)');
// Hidden types
_hiddenTypes = yield this._getTypesFromDB('WHERE display=0')
// Custom labels and icons
var sql = "SELECT customItemTypeID AS id, label, icon FROM customItemTypes";
var rows = yield Zotero.DB.queryAsync(sql);
for (let i=0; i<rows.length; i++) {
let row = rows[i];
let id = row.id;
_customLabels[id] = row.label;
_customImages[id] = row.icon;
}
});
this.getPrimaryTypes = function () {
if (!_primaryTypes) {
throw new Zotero.Exception.UnloadedDataException("Primary item type data not yet loaded");
}
return _primaryTypes;
}
function getSecondaryTypes() {
return this.getTypes('WHERE display IN (1,2)');
this.getSecondaryTypes = function () {
if (!_secondaryTypes) {
throw new Zotero.Exception.UnloadedDataException("Secondary item type data not yet loaded");
}
return _secondaryTypes;
}
function getHiddenTypes() {
return this.getTypes('WHERE display=0');
this.getHiddenTypes = function () {
if (!_hiddenTypes) {
throw new Zotero.Exception.UnloadedDataException("Hidden item type data not yet loaded");
}
return _hiddenTypes;
}
function getLocalizedString(idOrName) {
this.getLocalizedString = function (idOrName) {
var typeName = this.getName(idOrName);
// For custom types, use provided label
if (this.isCustom(idOrName)) {
var id = this.getID(idOrName) - this.customIDOffset;
if (_customLabels[id]) {
return _customLabels[id];
if (!_customLabels[id]) {
throw new Error("Label not available for custom field " + idOrName);
}
var sql = "SELECT label FROM customItemTypes WHERE customItemTypeID=?";
var label = Zotero.DB.valueQuery(sql, id);
_customLabels[id] = label;
return label;
return _customLabels[id];
}
return Zotero.getString("itemTypes." + typeName);
@ -327,15 +380,10 @@ Zotero.ItemTypes = new function() {
function getImageSrc(itemType) {
if (this.isCustom(itemType)) {
var id = this.getID(itemType) - this.customIDOffset;
if (_customImages[id]) {
return _customImages[id];
}
var sql = "SELECT icon FROM customItemTypes WHERE customItemTypeID=?";
var src = Zotero.DB.valueQuery(sql, id);
if (src) {
_customImages[id] = src;
return src;
if (!_customImages[id]) {
throw new Error("Image not available for custom field " + itemType);
}
return _customImages[id];
}
switch (itemType) {
@ -396,14 +444,14 @@ Zotero.FileTypes = new function() {
this._nameCol = 'fileType';
this._table = 'fileTypes';
this.getIDFromMIMEType = getIDFromMIMEType;
function getIDFromMIMEType(mimeType) {
/**
* @return {Promise<Integer>} fileTypeID
*/
this.getIDFromMIMEType = function (mimeType) {
var sql = "SELECT fileTypeID FROM fileTypeMIMETypes "
+ "WHERE ? LIKE mimeType || '%'";
return Zotero.DB.valueQuery(sql, [mimeType]);
}
return Zotero.DB.valueQueryAsync(sql, [mimeType]);
};
}

View file

@ -329,7 +329,7 @@ Zotero.DataObject.prototype._requireData = function (dataType) {
this._loaded[dataType] = true;
}
else if (!this._loaded[dataType]) {
throw new Zotero.DataObjects.UnloadedDataException(
throw new Zotero.Exception.UnloadedDataException(
"'" + dataType + "' not loaded for " + this._objectType + " ("
+ this._id + "/" + this._libraryID + "/" + this._key + ")",
this._objectType + dataType[0].toUpperCase() + dataType.substr(1)

View file

@ -61,7 +61,7 @@ Zotero.DataObjects = function (object, objectPlural, id, table) {
this.init = function () {
this._loadIDsAndKeys();
return this._loadIDsAndKeys();
}
@ -105,7 +105,7 @@ Zotero.DataObjects = function (object, objectPlural, id, table) {
let id = ids[i];
// Check if already loaded
if (!this._objectCache[id]) {
throw new this.UnloadedDataException(this._ZDO_Object + " " + id + " not yet loaded");
throw new Zotero.Exception.UnloadedDataException(this._ZDO_Object + " " + id + " not yet loaded");
}
toReturn.push(this._objectCache[id]);
}
@ -598,27 +598,16 @@ Zotero.DataObjects = function (object, objectPlural, id, table) {
});
this._loadIDsAndKeys = function () {
this._loadIDsAndKeys = Zotero.Promise.coroutine(function* () {
var sql = "SELECT ROWID AS id, libraryID, key FROM " + this._ZDO_table;
return Zotero.DB.queryAsync(sql)
.then(function (rows) {
for (let i=0; i<rows.length; i++) {
let row = rows[i];
this._objectKeys[row.id] = [row.libraryID, row.key];
if (!this._objectIDs[row.libraryID]) {
this._objectIDs[row.libraryID] = {};
}
this._objectIDs[row.libraryID][row.key] = row.id;
var rows = yield Zotero.DB.queryAsync(sql);
for (let i=0; i<rows.length; i++) {
let row = rows[i];
this._objectKeys[row.id] = [row.libraryID, row.key];
if (!this._objectIDs[row.libraryID]) {
this._objectIDs[row.libraryID] = {};
}
}.bind(this));
}
this._objectIDs[row.libraryID][row.key] = row.id;
}
});
}
Zotero.DataObjects.UnloadedDataException = function (msg, dataType) {
this.message = msg;
this.dataType = dataType;
this.stack = (new Error).stack;
}
Zotero.DataObjects.UnloadedDataException.prototype = Object.create(Error.prototype);
Zotero.DataObjects.UnloadedDataException.prototype.name = "UnloadedDataException"

View file

@ -242,7 +242,7 @@ Zotero.Item.prototype.getField = function(field, unformatted, includeBaseMapped)
// or this field has to be populated (e.g., by Zotero.Items.cacheFields())
// before getField() is called.
if (value === null) {
throw new Zotero.DataObjects.UnloadedDataException(
throw new Zotero.Exception.UnloadedDataException(
"Item data not loaded and field '" + field + "' not set", "itemData"
);
}
@ -2484,7 +2484,7 @@ Zotero.Item.prototype._updateAttachmentStates = function (exists) {
var item = Zotero.Items.getByLibraryAndKey(this.libraryID, parentKey);
}
catch (e) {
if (e instanceof Zotero.Items.UnloadedDataException) {
if (e instanceof Zotero.Exception.UnloadedDataException) {
Zotero.debug("Attachment parent not yet loaded in Zotero.Item.updateAttachmentStates()", 2);
return;
}

View file

@ -29,14 +29,14 @@ Zotero.ItemFields = new function() {
var _fields = {};
var _fieldsFormats = [];
var _fieldsLoaded;
var _itemTypeFieldsLoaded;
var _fieldFormats = [];
var _itemTypeFields = [];
var _baseTypeFields = [];
var _baseMappedFields = [];
var _typeFieldIDsByBase = {};
var _typeFieldNamesByBase = {};
var self = this;
// Privileged methods
this.getName = getName;
this.getID = getID;
@ -46,18 +46,60 @@ Zotero.ItemFields = new function() {
this.getItemTypeFields = getItemTypeFields;
this.isBaseField = isBaseField;
this.isFieldOfBase = isFieldOfBase;
this.getBaseMappedFields = getBaseMappedFields;
this.getFieldIDFromTypeAndBase = getFieldIDFromTypeAndBase;
this.getBaseIDFromTypeAndField = getBaseIDFromTypeAndField;
this.getTypeFieldsFromBase = getTypeFieldsFromBase;
/*
* Load all fields into an internal hash array
*/
this.init = Zotero.Promise.coroutine(function* () {
_fields = {};
_fieldsFormats = [];
var result = yield Zotero.DB.queryAsync('SELECT * FROM fieldFormats');
for (var i=0; i<result.length; i++) {
_fieldFormats[result[i]['fieldFormatID']] = {
regex: result[i]['regex'],
isInteger: result[i]['isInteger']
};
}
var fields = yield Zotero.DB.queryAsync('SELECT * FROM fieldsCombined');
var fieldItemTypes = yield _getFieldItemTypes();
var sql = "SELECT DISTINCT baseFieldID FROM baseFieldMappingsCombined";
var baseFields = yield Zotero.DB.columnQueryAsync(sql);
for each(var field in fields) {
_fields[field['fieldID']] = {
id: field['fieldID'],
name: field.fieldName,
label: field.label,
custom: !!field.custom,
isBaseField: (baseFields.indexOf(field['fieldID']) != -1),
formatID: field['fieldFormatID'],
itemTypes: fieldItemTypes[field['fieldID']]
};
// Store by name as well as id
_fields[field['fieldName']] = _fields[field['fieldID']];
}
_fieldsLoaded = true;
yield _loadBaseTypeFields();
yield _loadItemTypeFields();
});
/*
* Return the fieldID for a passed fieldID or fieldName
*/
function getID(field) {
if (!_fieldsLoaded) {
_loadFields();
throw new Zotero.Exception.UnloadedDataException("Item field data not yet loaded");
}
if (typeof field == 'number') {
@ -73,7 +115,7 @@ Zotero.ItemFields = new function() {
*/
function getName(field) {
if (!_fieldsLoaded) {
_loadFields();
throw new Zotero.Exception.UnloadedDataException("Item field data not yet loaded");
}
return _fields[field] ? _fields[field]['name'] : false;
@ -167,24 +209,18 @@ Zotero.ItemFields = new function() {
* Returns an array of fieldIDs for a given item type
*/
function getItemTypeFields(itemTypeID) {
if (!_fieldsLoaded) {
_loadFields();
}
if (_itemTypeFields[itemTypeID]) {
return _itemTypeFields[itemTypeID];
}
if (!itemTypeID) {
throw("Invalid item type id '" + itemTypeID
+ "' provided to getItemTypeFields()");
throw new Error("Invalid item type id '" + itemTypeID + "'");
}
var sql = 'SELECT fieldID FROM itemTypeFieldsCombined '
+ 'WHERE itemTypeID=' + itemTypeID + ' ORDER BY orderIndex';
var fields = Zotero.DB.columnQuery(sql);
if (!_itemTypeFieldsLoaded) {
throw new Zotero.Exception.UnloadedDataException("Item field data not yet loaded");
}
if (!_itemTypeFields[itemTypeID]) {
throw new Error("Item type field data not found for itemTypeID " + itemTypeID);
}
_itemTypeFields[itemTypeID] = fields ? fields : [];
return _itemTypeFields[itemTypeID];
}
@ -213,8 +249,8 @@ Zotero.ItemFields = new function() {
}
function getBaseMappedFields() {
return Zotero.DB.columnQuery("SELECT DISTINCT fieldID FROM baseFieldMappingsCombined");
this.getBaseMappedFields = function () {
return _baseMappedFields.concat();
}
@ -367,17 +403,12 @@ Zotero.ItemFields = new function() {
}
this.reload = function () {
_fieldsLoaded = false;
}
/**
* Check whether a field is valid, throwing an exception if not
* (since it should never actually happen)
**/
function _fieldCheck(field, func) {
var fieldID = self.getID(field);
var fieldID = Zotero.ItemFields.getID(field);
if (!fieldID) {
Zotero.debug((new Error).stack, 1);
throw new Error("Invalid field '" + field + (func ? "' in ItemFields." + func + "()" : "'"));
@ -389,29 +420,28 @@ Zotero.ItemFields = new function() {
/*
* Returns hash array of itemTypeIDs for which a given field is valid
*/
function _getFieldItemTypes() {
var _getFieldItemTypes = Zotero.Promise.coroutine(function* () {
var sql = 'SELECT fieldID, itemTypeID FROM itemTypeFieldsCombined';
var results = Zotero.DB.query(sql);
var results = yield Zotero.DB.queryAsync(sql);
if (!results) {
throw ('No fields in itemTypeFields!');
}
var fields = new Array();
for (var i=0; i<results.length; i++) {
if (!fields[results[i]['fieldID']]) {
fields[results[i]['fieldID']] = new Array();
var fields = [];
for (let i=0; i<results.length; i++) {
if (!fields[results[i].fieldID]) {
fields[results[i].fieldID] = [];
}
fields[results[i]['fieldID']][results[i]['itemTypeID']] = true;
fields[results[i].fieldID][results[i].itemTypeID] = true;
}
return fields;
}
});
/*
* Build a lookup table for base field mappings
*/
function _loadBaseTypeFields() {
var _loadBaseTypeFields = Zotero.Promise.coroutine(function* () {
_typeFieldIDsByBase = {};
_typeFieldNamesByBase = {};
@ -420,10 +450,10 @@ Zotero.ItemFields = new function() {
+ "FROM itemTypesCombined IT LEFT JOIN fieldsCombined F "
+ "LEFT JOIN baseFieldMappingsCombined BFM"
+ " ON (IT.itemTypeID=BFM.itemTypeID AND F.fieldID=BFM.baseFieldID)";
var rows = Zotero.DB.query(sql);
var rows = yield Zotero.DB.queryAsync(sql);
var sql = "SELECT DISTINCT baseFieldID FROM baseFieldMappingsCombined";
var baseFields = Zotero.DB.columnQuery(sql);
var baseFields = yield Zotero.DB.columnQueryAsync(sql);
var fields = [];
for each(var row in rows) {
@ -444,13 +474,11 @@ Zotero.ItemFields = new function() {
fields[row.itemTypeID][row.baseFieldID] = false;
}
}
_baseTypeFields = fields;
var sql = "SELECT baseFieldID, fieldID, fieldName "
+ "FROM baseFieldMappingsCombined JOIN fieldsCombined USING (fieldID)";
var rows = Zotero.DB.query(sql);
var rows = yield Zotero.DB.queryAsync(sql);
for each(var row in rows) {
if (!_typeFieldIDsByBase[row['baseFieldID']]) {
_typeFieldIDsByBase[row['baseFieldID']] = [];
@ -459,50 +487,31 @@ Zotero.ItemFields = new function() {
_typeFieldIDsByBase[row['baseFieldID']].push(row['fieldID']);
_typeFieldNamesByBase[row['baseFieldID']].push(row['fieldName']);
}
}
// Get all fields mapped to base types
sql = "SELECT DISTINCT fieldID FROM baseFieldMappingsCombined";
_baseMappedFields = yield Zotero.DB.columnQueryAsync(sql);
_baseTypeFieldsLoaded = true;
});
/*
* Load all fields into an internal hash array
*/
function _loadFields() {
_fields = {};
_fieldsFormats = [];
_itemTypeFields = [];
var _loadItemTypeFields = Zotero.Promise.coroutine(function* () {
var sql = 'SELECT itemTypeID, fieldID FROM itemTypeFieldsCombined ORDER BY orderIndex';
var rows = yield Zotero.DB.queryAsync(sql);
var result = Zotero.DB.query('SELECT * FROM fieldFormats');
_itemTypeFields = {};
_itemTypeFields[1] = []; // notes have no fields
for (var i=0; i<result.length; i++) {
_fieldFormats[result[i]['fieldFormatID']] = {
regex: result[i]['regex'],
isInteger: result[i]['isInteger']
};
for (let i=0; i<rows.length; i++) {
let row = rows[i];
let itemTypeID = row.itemTypeID;
if (!_itemTypeFields[itemTypeID]) {
_itemTypeFields[itemTypeID] = [];
}
_itemTypeFields[itemTypeID].push(row.fieldID);
}
var fields = Zotero.DB.query('SELECT * FROM fieldsCombined');
var fieldItemTypes = _getFieldItemTypes();
var sql = "SELECT DISTINCT baseFieldID FROM baseFieldMappingsCombined";
var baseFields = Zotero.DB.columnQuery(sql);
for each(var field in fields) {
_fields[field['fieldID']] = {
id: field['fieldID'],
name: field.fieldName,
label: field.label,
custom: !!field.custom,
isBaseField: (baseFields.indexOf(field['fieldID']) != -1),
formatID: field['fieldFormatID'],
itemTypes: fieldItemTypes[field['fieldID']]
};
// Store by name as well as id
_fields[field['fieldName']] = _fields[field['fieldID']];
}
_fieldsLoaded = true;
_loadBaseTypeFields();
}
_itemTypeFieldsLoaded = true;
});
}

View file

@ -24,6 +24,23 @@
*/
Zotero.Libraries = new function () {
var _libraryData = {};
this.init = Zotero.Promise.coroutine(function* () {
// Library data
var sql = "SELECT * FROM libraries";
var rows = yield Zotero.DB.queryAsync(sql);
for (let i=0; i<rows.length; i++) {
let row = rows[i];
_libraryData[row.libraryID] = {
type: row.libraryType,
version: row.version
};
}
// Current library
});
this.exists = function (libraryID) {
// Until there are other library types, this can just check groups,
// which already preload ids at startup
@ -54,7 +71,7 @@ Zotero.Libraries = new function () {
this.dbLibraryID = function (libraryID) {
return (libraryID == Zotero.libraryID) ? 0 : libraryID;
return (libraryID == Zotero.Users.getCurrentLibraryID()) ? 0 : libraryID;
}
@ -77,24 +94,24 @@ Zotero.Libraries = new function () {
this.getType = function (libraryID) {
if (libraryID === 0 || !Zotero.libraryID || libraryID == Zotero.libraryID) {
if (libraryID === 0) {
return 'user';
}
var sql = "SELECT libraryType FROM libraries WHERE libraryID=?";
var libraryType = Zotero.DB.valueQuery(sql, libraryID);
if (!libraryType) {
throw new Error("Library " + libraryID + " does not exist in Zotero.Libraries.getType()");
if (!_libraryTypes[libraryID]) {
throw new Error("Library data not loaded for library " + libraryID);
}
return libraryType;
return _libraryTypes[libraryID].type;
}
/**
* @param {Integer} libraryID
* @return {Promise:Integer}
* @return {Integer}
*/
this.getVersion = function (libraryID) {
var sql = "SELECT version FROM libraries WHERE libraryID=?";
return Zotero.DB.valueQueryAsync(sql, libraryID);
if (!_libraryTypes[libraryID]) {
throw new Error("Library data not loaded for library " + libraryID);
}
return _libraryTypes[libraryID].version;
}
@ -103,10 +120,12 @@ Zotero.Libraries = new function () {
* @param {Integer} version
* @return {Promise}
*/
this.setVersion = function (libraryID, version) {
this.setVersion = Zotero.Promise.coroutine(function* (libraryID, version) {
version = parseInt(version);
var sql = "UPDATE libraries SET version=? WHERE libraryID=?";
return Zotero.DB.queryAsync(sql, [version, libraryID]);
}
yield Zotero.DB.queryAsync(sql, [version, libraryID]);
_libraryTypes[libraryID] = version;
});
this.isEditable = function (libraryID) {

View file

@ -142,13 +142,13 @@ Zotero.Relations = new function () {
var relation = new Zotero.Relation;
if (!libraryID) {
libraryID = Zotero.libraryID;
libraryID = Zotero.Users.getCurrentLibraryID();
}
if (libraryID) {
relation.libraryID = parseInt(libraryID);
}
else {
relation.libraryID = "local/" + Zotero.getLocalUserKey(true);
relation.libraryID = "local/" + Zotero.Users.getLocalUserKey();
}
relation.subject = subject;
relation.predicate = predicate;
@ -253,9 +253,9 @@ Zotero.Relations = new function () {
relation.libraryID = parseInt(libraryID);
}
else {
libraryID = Zotero.libraryID;
libraryID = Zotero.Users.getCurrentLibraryID();
if (!libraryID) {
libraryID = Zotero.getLocalUserKey(true);
libraryID = Zotero.Users.getLocalUserKey();
}
relation.libraryID = parseInt(libraryID);
}

View file

@ -854,7 +854,7 @@ Zotero.Tags = new function() {
function _requireLoad(libraryID) {
if (!_loaded[libraryID]) {
throw new Zotero.DataObjects.UnloadedDataException(
throw new Zotero.Exception.UnloadedDataException(
"Tag data has not been loaded for library " + libraryID,
"tags"
);

View file

@ -36,12 +36,7 @@ Zotero.DBConnection = function(dbName) {
this.MAX_BOUND_PARAMETERS = 999;
Components.utils.import("resource://gre/modules/Task.jsm", this);
// Use the Fx24 Sqlite.jsm, because the Sqlite.jsm in Firefox 25 breaks
// locking_mode=EXCLUSIVE with async DB access. In the Fx24 version,
// the main-thread DB connection is still used for async access.
//Components.utils.import("resource://gre/modules/Sqlite.jsm", this);
Components.utils.import("resource://zotero/Sqlite.jsm", this);
Components.utils.import("resource://gre/modules/Sqlite.jsm", this);
this.skipBackup = false;
this.transactionVacuum = false;
@ -94,6 +89,12 @@ Zotero.DBConnection = function(dbName) {
};
this._dbIsCorrupt = null
this._self = this;
this._transactionPromise = null;
// Get GeneratorFunction, so we can test for an ES6 generator
var g = function* () { yield 1; };
this._generatorFunction = Object.getPrototypeOf(g).constructor;
}
/////////////////////////////////////////////////////////////////
@ -108,233 +109,9 @@ Zotero.DBConnection = function(dbName) {
* @return void
*/
Zotero.DBConnection.prototype.test = function () {
this._getDBConnection();
return this._getConnectionAsync().return();
}
/*
* Run an SQL query
*
* Optional _params_ is an array of bind parameters in the form
* [1,"hello",3] or [{'int':2},{'string':'foobar'}]
*
* Returns:
* - Associative array (similar to mysql_fetch_assoc) for SELECT's
* - lastInsertId for INSERT's
* - TRUE for other successful queries
* - FALSE on error
*/
Zotero.DBConnection.prototype.query = function (sql,params) {
Zotero.debug("WARNING: Zotero.DBConnection.prototype.query() is deprecated "
+ "-- use queryAsync() instead [QUERY: " + sql + "]", 2);
var db = this._getDBConnection();
try {
// Parse out the SQL command being used
var op = sql.match(/^[^a-z]*[^ ]+/i);
if (op) {
op = op.toString().toLowerCase();
}
// If SELECT statement, return result
if (op == 'select' || op == 'pragma') {
// Until the native dataset methods work (or at least exist),
// we build a multi-dimensional associative array manually
var statement = this.getStatement(sql, params, {
checkParams: true
});
// Get column names
var columns = [];
var numCols = statement.columnCount;
for (var i=0; i<numCols; i++) {
let colName = statement.getColumnName(i);
columns.push(colName);
}
var dataset = [];
while (statement.executeStep()) {
var row = [];
for(var i=0; i<numCols; i++) {
row[columns[i]] = this._getTypedValue(statement, i);
}
dataset.push(row);
}
statement.finalize();
return dataset.length ? dataset : false;
}
else {
if (params) {
var statement = this.getStatement(sql, params, {
checkParams: true
});
statement.execute();
}
else {
let sql2;
[sql2, ] = this.parseQueryAndParams(sql, params);
this._debug(sql2, 5);
db.executeSimpleSQL(sql2);
}
if (op == 'insert' || op == 'replace') {
return db.lastInsertRowID;
}
// DEBUG: Can't get affected rows for UPDATE or DELETE?
else {
return true;
}
}
}
catch (e) {
this.checkException(e);
try {
[sql, params] = this.parseQueryAndParams(sql, params);
}
catch (e2) {}
var dberr = (db.lastErrorString!='not an error')
? ' [ERROR: ' + db.lastErrorString + ']' : '';
throw new Error(e + ' [QUERY: ' + sql + ']' + dberr);
}
}
/*
* Query a single value and return it
*/
Zotero.DBConnection.prototype.valueQuery = function (sql,params) {
var statement = this.getStatement(sql, params, {
checkParams: true
});
// No rows
if (!statement.executeStep()) {
statement.finalize();
return false;
}
var value = this._getTypedValue(statement, 0);
statement.finalize();
return value;
}
/*
* Run a query and return the first row
*/
Zotero.DBConnection.prototype.rowQuery = function (sql,params) {
var result = this.query(sql,params);
if (result) {
return result[0];
}
}
/*
* Run a query and return the first column as a numerically-indexed array
*/
Zotero.DBConnection.prototype.columnQuery = function (sql,params) {
var statement = this.getStatement(sql, params, {
checkParams: true
});
if (statement) {
var column = new Array();
while (statement.executeStep()) {
column.push(this._getTypedValue(statement, 0));
}
statement.finalize();
return column.length ? column : false;
}
return false;
}
/*
/*
* Get a raw mozStorage statement from the DB for manual processing
*
* This should only be used externally for manual parameter binding for
* large repeated queries
*
* Optional _params_ is an array of bind parameters in the form
* [1,"hello",3] or [{'int':2},{'string':'foobar'}]
*/
Zotero.DBConnection.prototype.getStatement = function (sql, params, options) {
var db = this._getDBConnection();
// TODO: limit to Zotero.DB, not all Zotero.DBConnections?
if (db.transactionInProgress && Zotero.waiting > this._transactionWaitLevel) {
throw ("Cannot access database layer from a higher wait level if a transaction is open");
}
[sql, params] = this.parseQueryAndParams(sql, params, options);
try {
this._debug(sql,5);
var statement = db.createStatement(sql);
}
catch (e) {
var dberr = (db.lastErrorString!='not an error')
? ' [ERROR: ' + db.lastErrorString + ']' : '';
throw new Error(e + ' [QUERY: ' + sql + ']' + dberr);
}
var numParams = statement.parameterCount;
if (params.length) {
for (var i=0; i<params.length; i++) {
var value = params[i];
// Bind the parameter as the correct type
switch (typeof value) {
case 'number':
// Store as 32-bit signed integer
if (value <= 2147483647) {
this._debug('Binding parameter ' + (i+1)
+ ' of type int: ' + value, 5);
statement.bindInt32Parameter(i, value);
}
// Store as 64-bit signed integer
//
// Note: 9007199254740992 (2^53) is JS's upper bound for decimal integers
else {
this._debug('Binding parameter ' + (i + 1) + ' of type int64: ' + value, 5);
statement.bindInt64Parameter(i, value);
}
break;
case 'string':
this._debug('Binding parameter ' + (i+1)
+ ' of type string: "' + value + '"', 5);
statement.bindUTF8StringParameter(i, value);
break;
case 'object':
if (value !== null) {
let msg = 'Invalid bound parameter ' + value
+ ' in ' + Zotero.Utilities.varDump(params)
+ ' [QUERY: ' + sql + ']';
Zotero.debug(msg);
throw new Error(msg);
}
this._debug('Binding parameter ' + (i+1) + ' of type NULL', 5);
statement.bindNullParameter(i);
break;
}
}
}
return statement;
}
Zotero.DBConnection.prototype.getAsyncStatement = Zotero.Promise.coroutine(function* (sql) {
var conn = yield this._getConnectionAsync();
conn = conn._connection;
@ -506,14 +283,6 @@ Zotero.DBConnection.prototype.executeAsyncStatement = function (statement) {
return deferred.promise;
}
/*
* Only for use externally with this.getStatement()
*/
Zotero.DBConnection.prototype.getLastInsertID = function () {
var db = this._getDBConnection();
return db.lastInsertRowID;
}
/*
* Only for use externally with this.getStatement()
@ -524,131 +293,6 @@ Zotero.DBConnection.prototype.getLastErrorString = function () {
}
Zotero.DBConnection.prototype.beginTransaction = function () {
var db = this._getDBConnection();
if (db.transactionInProgress) {
// TODO: limit to Zotero.DB, not all Zotero.DBConnections?
if (Zotero.waiting != this._transactionWaitLevel) {
var msg = "Cannot start a DB transaction from a different wait level";
Zotero.debug(msg, 2);
throw (msg);
}
this._transactionNestingLevel++;
this._debug('Transaction in progress -- increasing level to '
+ this._transactionNestingLevel, 5);
}
else {
this._transactionWaitLevel = Zotero.waiting;
this._debug('Beginning DB transaction', 5);
db.beginTransaction();
// Set a timestamp for this transaction
this._transactionDate = new Date(Math.floor(new Date / 1000) * 1000);
// If transaction time hasn't changed since last used transaction time,
// add a second -- this is a hack to get around a sync problem when
// multiple sync sessions run within the same second
if (this._lastTransactionDate &&
this._transactionDate.getTime() <= this._lastTransactionDate.getTime()) {
this._transactionDate = new Date(this._lastTransactionDate.getTime() + 1000)
}
// Run callbacks
for (var i=0; i<this._callbacks.begin.length; i++) {
if (this._callbacks.begin[i]) {
this._callbacks.begin[i]();
}
}
}
}
Zotero.DBConnection.prototype.commitTransaction = function () {
var db = this._getDBConnection();
if (this._transactionNestingLevel) {
this._transactionNestingLevel--;
this._debug('Decreasing transaction level to ' + this._transactionNestingLevel, 5);
}
else if (this._transactionRollback) {
this._debug('Rolling back previously flagged transaction', 5);
this.rollbackTransaction();
}
else {
this._debug('Committing transaction',5);
// Clear transaction time
if (this._transactionDate) {
this._transactionDate = null;
}
try {
if (!db.transactionInProgress) {
throw new Error("No transaction in progress");
}
db.commitTransaction();
if (this.transactionVacuum) {
Zotero.debug('Vacuuming database');
db.executeSimpleSQL('VACUUM');
this.transactionVacuum = false;
}
// Run callbacks
for (var i=0; i<this._callbacks.commit.length; i++) {
if (this._callbacks.commit[i]) {
this._callbacks.commit[i]();
}
}
}
catch(e) {
var dberr = (db.lastErrorString!='not an error')
? ' [ERROR: ' + db.lastErrorString + ']' : '';
throw(e + dberr);
}
}
}
Zotero.DBConnection.prototype.rollbackTransaction = function () {
var db = this._getDBConnection();
if (!db.transactionInProgress) {
this._debug("Transaction is not in progress in rollbackTransaction()", 2);
return;
}
if (this._transactionNestingLevel) {
this._transactionNestingLevel--;
this._transactionRollback = true;
this._debug('Flagging nested transaction for rollback', 5);
}
else {
this._debug('Rolling back transaction', 5);
this._transactionRollback = false;
try {
db.rollbackTransaction();
// Run callbacks
for (var i=0; i<this._callbacks.rollback.length; i++) {
if (this._callbacks.rollback[i]) {
this._callbacks.rollback[i]();
}
}
}
catch(e) {
var dberr = (db.lastErrorString!='not an error')
? ' [ERROR: ' + db.lastErrorString + ']' : '';
throw(e + dberr);
}
}
}
Zotero.DBConnection.prototype.addCallback = function (type, cb) {
switch (type) {
case 'begin':
@ -724,24 +368,7 @@ Zotero.DBConnection.prototype.rollbackAllTransactions = function () {
}
Zotero.DBConnection.prototype.tableExists = function (table) {
return this._getDBConnection().tableExists(table);
}
Zotero.DBConnection.prototype.getColumns = function (table) {
try {
var rows = this.query("PRAGMA table_info(" + table + ")");
return [row.name for each (row in rows)];
}
catch (e) {
this._debug(e,1);
return false;
}
}
Zotero.DBConnection.prototype.getColumnsAsync = function (table) {
return Zotero.DB.queryAsync("PRAGMA table_info(" + table + ")")
.then(function (rows) {
return [row.name for each (row in rows)];
@ -753,46 +380,6 @@ Zotero.DBConnection.prototype.getColumnsAsync = function (table) {
}
Zotero.DBConnection.prototype.getColumnHash = function (table) {
var cols = this.getColumns(table);
var hash = {};
if (cols.length) {
for (var i=0; i<cols.length; i++) {
hash[cols[i]] = true;
}
}
return hash;
}
/**
* Find the lowest unused integer >0 in a table column
*
* Note: This retrieves all the rows of the column, so it's not really
* meant for particularly large tables.
**/
Zotero.DBConnection.prototype.getNextID = function (table, column) {
var sql = 'SELECT ' + column + ' FROM ' + table + ' ORDER BY ' + column;
var vals = this.columnQuery(sql);
if (!vals) {
return 1;
}
if (vals[0] === '0') {
vals.shift();
}
for (var i=0, len=vals.length; i<len; i++) {
if (vals[i] != i+1) {
break;
}
}
return i+1;
}
/**
* Find the next lowest numeric suffix for a value in table column
*
@ -803,7 +390,7 @@ Zotero.DBConnection.prototype.getNextID = function (table, column) {
*
* If _name_ alone is available, returns that
**/
Zotero.DBConnection.prototype.getNextName = function (libraryID, table, field, name)
Zotero.DBConnection.prototype.getNextName = Zotero.Promise.coroutine(function* (libraryID, table, field, name)
{
if (typeof name == 'undefined') {
Zotero.debug("WARNING: The parameters of Zotero.DB.getNextName() have changed -- update your code", 2);
@ -816,7 +403,7 @@ Zotero.DBConnection.prototype.getNextName = function (libraryID, table, field, n
+ "AND libraryID=?"
+ " ORDER BY " + field;
var params = [libraryID];
var suffixes = this.columnQuery(sql, params);
var suffixes = yield this.columnQueryAsync(sql, params);
// If none found or first one has a suffix, use default name
if (!suffixes || suffixes[0]) {
return name;
@ -839,7 +426,7 @@ Zotero.DBConnection.prototype.getNextName = function (libraryID, table, field, n
num++;
}
return name + ' ' + num;
}
});
//
@ -890,9 +477,7 @@ Zotero.DBConnection.prototype.getNextName = function (libraryID, table, field, n
* generally from queryAsync() and similar
* @return {Promise} - Promise for result of generator function
*/
Zotero.DBConnection.prototype.executeTransaction = function (func, options) {
var self = this;
Zotero.DBConnection.prototype.executeTransaction = Zotero.Promise.coroutine(function* (func, options) {
// Set temporary options for this transaction that will be reset at the end
var origOptions = {};
if (options) {
@ -902,62 +487,74 @@ Zotero.DBConnection.prototype.executeTransaction = function (func, options) {
}
}
return this._getConnectionAsync()
.then(function (conn) {
var conn = yield this._getConnectionAsync();
try {
if (conn.transactionInProgress) {
Zotero.debug("Async DB transaction in progress -- increasing level to "
+ ++self._asyncTransactionNestingLevel, 5);
return self.Task.spawn(func)
.then(
function (result) {
if (options) {
if (options.onCommit) {
self._callbacks.current.commit.push(options.onCommit);
}
if (options.onRollback) {
self._callbacks.current.rollback.push(options.onRollback);
}
}
Zotero.debug("Decreasing async DB transaction level to "
+ --self._asyncTransactionNestingLevel, 5);
return result;
},
function (e) {
Zotero.debug("Rolled back nested async DB transaction", 5);
self._asyncTransactionNestingLevel = 0;
throw e;
+ ++this._asyncTransactionNestingLevel, 5);
try {
// Check for ES5 generators, which don't work properly
if (func.isGenerator() && !(func instanceof this._generatorFunction)) {
Zotero.debug(func);
throw new Error("func must be an ES6 generator");
}
);
var result = yield Zotero.Promise.coroutine(func)();
}
catch (e) {
Zotero.debug("Rolled back nested async DB transaction", 5);
this._asyncTransactionNestingLevel = 0;
throw e;
}
if (options) {
if (options.onCommit) {
this._callbacks.current.commit.push(options.onCommit);
}
if (options.onRollback) {
this._callbacks.current.rollback.push(options.onRollback);
}
}
Zotero.debug("Decreasing async DB transaction level to "
+ --this._asyncTransactionNestingLevel, 5);
return result;
}
else {
Zotero.debug("Beginning async DB transaction", 5);
var resolve;
var reject;
this._transactionPromise = new Zotero.Promise(function () {
resolve = arguments[0];
reject = arguments[1];
});
// Set a timestamp for this transaction
self._transactionDate = new Date(Math.floor(new Date / 1000) * 1000);
this._transactionDate = new Date(Math.floor(new Date / 1000) * 1000);
// Run begin callbacks
for (var i=0; i<self._callbacks.begin.length; i++) {
if (self._callbacks.begin[i]) {
self._callbacks.begin[i]();
for (var i=0; i<this._callbacks.begin.length; i++) {
if (this._callbacks.begin[i]) {
this._callbacks.begin[i]();
}
}
return conn.executeTransaction(func)
.then(Zotero.Promise.coroutine(function* (result) {
var result = yield conn.executeTransaction(func);
try {
Zotero.debug("Committed async DB transaction", 5);
// Clear transaction time
if (self._transactionDate) {
self._transactionDate = null;
if (this._transactionDate) {
this._transactionDate = null;
}
if (options) {
// Function to run once transaction has been committed but before any
// permanent callbacks
if (options.onCommit) {
self._callbacks.current.commit.push(options.onCommit);
this._callbacks.current.commit.push(options.onCommit);
}
self._callbacks.current.rollback = [];
this._callbacks.current.rollback = [];
if (options.vacuumOnCommit) {
Zotero.debug('Vacuuming database');
@ -967,56 +564,70 @@ Zotero.DBConnection.prototype.executeTransaction = function (func, options) {
// Run temporary commit callbacks
var f;
while (f = self._callbacks.current.commit.shift()) {
while (f = this._callbacks.current.commit.shift()) {
yield Zotero.Promise.resolve(f());
}
// Run commit callbacks
for (var i=0; i<self._callbacks.commit.length; i++) {
if (self._callbacks.commit[i]) {
yield self._callbacks.commit[i]();
for (var i=0; i<this._callbacks.commit.length; i++) {
if (this._callbacks.commit[i]) {
yield this._callbacks.commit[i]();
}
}
setTimeout(resolve, 0);
return result;
}))
.catch(Zotero.Promise.coroutine(function* (e) {
}
catch (e) {
Zotero.debug("Rolled back async DB transaction", 5);
Zotero.debug(e, 1);
if (options) {
// Function to run once transaction has been committed but before any
// permanent callbacks
if (options.onRollback) {
self._callbacks.current.rollback.push(options.onRollback);
try {
if (options) {
// Function to run once transaction has been committed but before any
// permanent callbacks
if (options.onRollback) {
this._callbacks.current.rollback.push(options.onRollback);
}
}
// Run temporary commit callbacks
var f;
while (f = this._callbacks.current.rollback.shift()) {
yield Zotero.Promise.resolve(f());
}
// Run rollback callbacks
for (var i=0; i<this._callbacks.rollback.length; i++) {
if (this._callbacks.rollback[i]) {
yield Zotero.Promise.resolve(this._callbacks.rollback[i]());
}
}
}
// Run temporary commit callbacks
var f;
while (f = self._callbacks.current.rollback.shift()) {
yield Zotero.Promise.resolve(f());
}
// Run rollback callbacks
for (var i=0; i<self._callbacks.rollback.length; i++) {
if (self._callbacks.rollback[i]) {
yield Zotero.Promise.resolve(self._callbacks.rollback[i]());
}
finally {
setTimeout(reject, 0);
}
throw e;
}));
}
}
})
.finally(function () {
}
finally {
// Reset options back to their previous values
if (options) {
for (let option in options) {
self[option] = origOptions[option];
this[option] = origOptions[option];
}
}
}
});
Zotero.DBConnection.prototype.waitForTransaction = function () {
Zotero.debug("Waiting for transaction to finish");
return this._transactionPromise.then(function () {
Zotero.debug("Done waiting for transaction");
});
};
@ -1084,7 +695,8 @@ Zotero.DBConnection.prototype.queryAsync = function (sql, params, options) {
return target.getResultByName(name);
}
catch (e) {
Zotero.debug("DB column '" + name + "' not found");
Zotero.debug((new Error).stack, 2);
Zotero.debug("DB column '" + name + "' not found", 2);
return undefined;
}
}
@ -1212,7 +824,7 @@ Zotero.DBConnection.prototype.columnQueryAsync = function (sql, params) {
};
Zotero.DBConnection.prototype.tableExistsAsync = function (table) {
Zotero.DBConnection.prototype.tableExists = function (table) {
return this._getConnectionAsync()
.then(function () {
var sql = "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND tbl_name=?";
@ -1256,19 +868,6 @@ Zotero.DBConnection.prototype.executeSQLFile = function (sql) {
}
/**
* Generator functions can't return values, but Task.js-style generators,
* as used by executeTransaction(), can throw a special exception in order
* to do so. This function throws such an exception for passed value and
* can be used at the end of executeTransaction() to return a value to the
* next promise handler.
*/
Zotero.DBConnection.prototype.asyncResult = function (val) {
throw new this.Task.Result(val);
};
/*
* Implements nsIObserver
*/
@ -1281,10 +880,10 @@ Zotero.DBConnection.prototype.observe = function(subject, topic, data) {
}
Zotero.DBConnection.prototype.integrityCheck = function () {
var ok = this.valueQuery("PRAGMA integrity_check");
Zotero.DBConnection.prototype.integrityCheck = Zotero.Promise.coroutine(function* () {
var ok = yield this.valueQueryAsync("PRAGMA integrity_check");
return ok == 'ok';
}
});
Zotero.DBConnection.prototype.checkException = function (e) {
@ -1328,25 +927,15 @@ Zotero.DBConnection.prototype.checkException = function (e) {
* @param {Boolean} [permanent] If true, throw an error instead of
* allowing code to re-open the database again
*/
Zotero.DBConnection.prototype.closeDatabase = function (permanent) {
if (this._connection || this._connectionAsync) {
var deferred = Zotero.Promise.defer();
Zotero.Promise.all([this._connection.asyncClose, this._connectionAsync.asyncClose])
.then(function () {
this._connection = undefined;
this._connection = permanent ? false : null;
this._connectionAsync = undefined;
this._connectionAsync = permanent ? false : null;
}.bind(this))
.then(function () {
deferred.resolve();
});
return deferred.promise;
Zotero.DBConnection.prototype.closeDatabase = Zotero.Promise.coroutine(function* (permanent) {
if (this._connectionAsync) {
Zotero.debug("Closing database");
yield this._connectionAsync.close();
this._connectionAsync = undefined;
this._connectionAsync = permanent ? false : null;
Zotero.debug("Database closed");
}
return Zotero.Promise.resolve();
}
});
Zotero.DBConnection.prototype.backupDatabase = function (suffix, force) {
@ -1540,199 +1129,6 @@ Zotero.DBConnection.prototype.getSQLDataType = function(value) {
//
/////////////////////////////////////////////////////////////////
/*
* Retrieve a link to the data store
*/
Zotero.DBConnection.prototype._getDBConnection = function () {
if (this._connection) {
return this._connection;
} else if (this._connection === false) {
throw new Error("Database permanently closed; not re-opening");
}
this._debug("Opening database '" + this._dbName + "'");
// Get the storage service
var store = Components.classes["@mozilla.org/storage/service;1"].
getService(Components.interfaces.mozIStorageService);
var file = Zotero.getZoteroDatabase(this._dbName);
var backupFile = Zotero.getZoteroDatabase(this._dbName, 'bak');
var fileName = this._dbName + '.sqlite';
var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Components.interfaces.nsIPromptService);
catchBlock: try {
var corruptMarker = Zotero.getZoteroDatabase(this._dbName, 'is.corrupt');
if (corruptMarker.exists()) {
throw({ name: 'NS_ERROR_FILE_CORRUPTED' })
}
this._connection = store.openDatabase(file);
}
catch (e) {
if (e.name=='NS_ERROR_FILE_CORRUPTED') {
this._debug("Database file '" + file.leafName + "' is marked as corrupted", 1);
// No backup file! Eek!
if (!backupFile.exists()) {
this._debug("No backup file for DB '" + this._dbName + "' exists", 1);
// Save damaged file if it exists
if (file.exists()) {
this._debug('Saving damaged DB file with .damaged extension', 1);
var damagedFile = Zotero.getZoteroDatabase(this._dbName, 'damaged');
Zotero.moveToUnique(file, damagedFile);
}
else {
this._debug(file.leafName + " does not exist -- creating new database");
}
// Create new main database
var file = Zotero.getZoteroDatabase(this._dbName);
this._connection = store.openDatabase(file);
if (corruptMarker.exists()) {
corruptMarker.remove(null);
}
// FIXME: If damaged file didn't exist, it won't be saved, as the message claims
let msg = Zotero.getString('db.dbCorruptedNoBackup', fileName);
Zotero.debug(msg, 1);
ps.alert(null, Zotero.getString('general.warning'), msg);
break catchBlock;
}
// Save damaged file if it exists
if (file.exists()) {
this._debug('Saving damaged DB file with .damaged extension', 1);
var damagedFile = Zotero.getZoteroDatabase(this._dbName, 'damaged');
Zotero.moveToUnique(file, damagedFile);
}
else {
this._debug(file.leafName + " does not exist");
}
// Test the backup file
try {
this._connection = store.openDatabase(backupFile);
}
// Can't open backup either
catch (e) {
// Create new main database
var file = Zotero.getZoteroDatabase(this._dbName);
this._connection = store.openDatabase(file);
let msg = Zotero.getString('db.dbRestoreFailed', fileName);
Zotero.debug(msg, 1);
ps.alert(null, Zotero.getString('general.warning'), msg);
if (corruptMarker.exists()) {
corruptMarker.remove(null);
}
break catchBlock;
}
this._connection = undefined;
// Copy backup file to main DB file
this._debug("Restoring database '" + this._dbName + "' from backup file", 1);
try {
backupFile.copyTo(backupFile.parent, fileName);
}
catch (e) {
// TODO: deal with low disk space
throw (e);
}
// Open restored database
var file = Zotero.getZoteroDirectory();
file.append(fileName);
this._connection = store.openDatabase(file);
this._debug('Database restored', 1);
// FIXME: If damaged file didn't exist, it won't be saved, as the message claims
var msg = Zotero.getString('db.dbRestored', [
fileName,
Zotero.Date.getFileDateString(backupFile),
Zotero.Date.getFileTimeString(backupFile)
]);
Zotero.debug(msg, 1);
ps.alert(
null,
Zotero.getString('general.warning'),
msg
);
if (corruptMarker.exists()) {
corruptMarker.remove(null);
}
break catchBlock;
}
// Some other error that we don't yet know how to deal with
throw (e);
}
if (DB_LOCK_EXCLUSIVE) {
Zotero.DB.query("PRAGMA locking_mode=EXCLUSIVE");
}
else {
Zotero.DB.query("PRAGMA locking_mode=NORMAL");
}
// Set page cache size to 8MB
var pageSize = Zotero.DB.valueQuery("PRAGMA page_size");
var cacheSize = 8192000 / pageSize;
Zotero.DB.query("PRAGMA cache_size=" + cacheSize);
// Enable foreign key checks
Zotero.DB.query("PRAGMA foreign_keys=1");
// Register idle and shutdown handlers to call this.observe() for DB backup
var idleService = Components.classes["@mozilla.org/widget/idleservice;1"]
.getService(Components.interfaces.nsIIdleService);
idleService.addIdleObserver(this, 60);
idleService = null;
// User-defined functions
// TODO: move somewhere else?
// Levenshtein distance UDF
var lev = {
onFunctionCall: function (arg) {
var a = arg.getUTF8String(0);
var b = arg.getUTF8String(1);
return Zotero.Utilities.levenshtein(a, b);
}
};
this._connection.createFunction('levenshtein', 2, lev);
// Regexp UDF
var rx = {
onFunctionCall: function (arg) {
var re = new RegExp(arg.getUTF8String(0));
var str = arg.getUTF8String(1);
return re.test(str);
}
};
this._connection.createFunction('regexp', 2, rx);
// text2html UDF
var rx = {
onFunctionCall: function (arg) {
var str = arg.getUTF8String(0);
return Zotero.Utilities.text2html(str, true);
}
};
this._connection.createFunction('text2html', 1, rx);
return this._connection;
};
/*
* Retrieve a link to the data store asynchronously
*/

View file

@ -144,4 +144,13 @@ Zotero.Exception.UserCancelled = function(whatCancelled) {
Zotero.Exception.UserCancelled.prototype = {
"name":"UserCancelledException",
"toString":function() { return "User cancelled "+this.whatCancelled+"."; }
};
};
Zotero.Exception.UnloadedDataException = function (msg, dataType) {
this.message = msg;
this.dataType = dataType;
this.stack = (new Error).stack;
}
Zotero.Exception.UnloadedDataException.prototype = Object.create(Error.prototype);
Zotero.Exception.UnloadedDataException.prototype.name = "UnloadedDataException"

View file

@ -672,7 +672,7 @@ Zotero.Fulltext = new function(){
let itemID = itemIDs[i];
let item = yield Zotero.Items.getAsync(itemID);
let libraryID = item.libraryID
libraryID = libraryID ? libraryID : Zotero.libraryID;
libraryID = libraryID ? libraryID : Zotero.Users.getCurrentLibraryID();
if (!undownloaded[libraryID]) {
undownloaded[libraryID] = [];
}

View file

@ -117,10 +117,7 @@ Zotero.ItemTreeView.prototype.setTree = Zotero.Promise.coroutine(function* (tree
return;
}
// If a DB transaction is open, display error message and bail
if (!Zotero.stateCheck()) {
throw new Error();
}
yield Zotero.DB.waitForTransaction();
yield this.refresh();
@ -1557,9 +1554,6 @@ Zotero.ItemTreeView.prototype.sort = Zotero.Promise.coroutine(function* (itemID)
* Select an item
*/
Zotero.ItemTreeView.prototype.selectItem = Zotero.Promise.coroutine(function* (id, expand, noRecurse) {
var selected = this.getSelectedItems(true);
var alreadySelected = selected.length == 1 && selected[0] == id;
// Don't change selection if UI updates are disabled (e.g., during sync)
if (Zotero.suppressUIUpdates) {
Zotero.debug("Sync is running; not selecting item");
@ -1619,30 +1613,30 @@ Zotero.ItemTreeView.prototype.selectItem = Zotero.Promise.coroutine(function* (i
}
if (!alreadySelected) {
// This function calls nsITreeSelection.select(), which triggers the <tree>'s 'onselect'
// attribute, which calls ZoteroPane.itemSelected(), which calls ZoteroItemPane.viewItem(),
// which refreshes the itembox. But since the 'onselect' doesn't handle promises,
// itemSelected() isn't waited for and 'yield selectItem(itemID)' continues before the
// itembox has been refreshed. To get around this, we make a promise resolver that's
// triggered by itemSelected() when it's done.
// This function calls nsITreeSelection.select(), which triggers the <tree>'s 'onselect'
// attribute, which calls ZoteroPane.itemSelected(), which calls ZoteroItemPane.viewItem(),
// which refreshes the itembox. But since the 'onselect' doesn't handle promises,
// itemSelected() isn't waited for and 'yield selectItem(itemID)' continues before the
// itembox has been refreshed. To get around this, we make a promise resolver that's
// triggered by itemSelected() when it's done.
if (!this.selection.selectEventsSuppressed) {
var itemSelectedPromise = new Zotero.Promise(function () {
this._itemSelectedPromiseResolver = {
resolve: arguments[0],
reject: arguments[1]
};
}.bind(this));
this.selection.select(row);
}
this.selection.select(row);
// If |expand|, open row if container
if (expand && this.isContainer(row) && !this.isContainerOpen(row)) {
yield this.toggleOpenState(row);
}
this.selection.select(row);
if (!alreadySelected && !this.selection.selectEventsSuppressed) {
if (!this.selection.selectEventsSuppressed) {
yield itemSelectedPromise;
}

View file

@ -153,7 +153,7 @@ Zotero.MIMETypeHandler = new function () {
// translate using first available
translation.setTranslator(translators[0]);
frontWindow.Zotero_Browser.performTranslation(translation);
return frontWindow.Zotero_Browser.performTranslation(translation);
});
}

View file

@ -45,13 +45,13 @@ Zotero.Proxies = new function() {
/**
* Initializes http-on-examine-response observer to intercept page loads and gets preferences
*/
this.init = function() {
this.init = Zotero.Promise.coroutine(function* () {
if(!this.proxies) {
var me = this;
Zotero.MIMETypeHandler.addObserver(function(ch) { me.observe(ch) });
var rows = Zotero.DB.query("SELECT * FROM proxies");
Zotero.Proxies.proxies = [new Zotero.Proxy(row) for each(row in rows)];
var rows = yield Zotero.DB.queryAsync("SELECT * FROM proxies");
Zotero.Proxies.proxies = [(yield this.newProxyFromRow(row)) for each(row in rows)];
for each(var proxy in Zotero.Proxies.proxies) {
for each(var host in proxy.hosts) {
@ -69,7 +69,19 @@ Zotero.Proxies = new function() {
Zotero.Proxies.lastIPCheck = 0;
Zotero.Proxies.lastIPs = "";
Zotero.Proxies.disabledByDomain = false;
}
});
/**
* @param {Object} row - Database row with proxy data
* @return {Promise<Zotero.Proxy>}
*/
this.newProxyFromRow = Zotero.Promise.coroutine(function* (row) {
var proxy = new Zotero.Proxy;
yield proxy._loadFromRow(row);
return proxy;
});
/**
* Observe method to capture page loads and determine if they're going through an EZProxy.
@ -457,13 +469,9 @@ Zotero.Proxies = new function() {
* @constructor
* @class Represents an individual proxy server
*/
Zotero.Proxy = function(row) {
if(row) {
this._loadFromRow(row);
} else {
this.hosts = [];
this.multiHost = false;
}
Zotero.Proxy = function () {
this.hosts = [];
this.multiHost = false;
}
/**
@ -622,10 +630,11 @@ Zotero.Proxy.prototype.save = function(transparent) {
/**
* Reverts to the previously saved version of this proxy
*/
Zotero.Proxy.prototype.revert = function() {
if(!this.proxyID) throw "Cannot revert an unsaved proxy";
this._loadFromRow(Zotero.DB.rowQuery("SELECT * FROM proxies WHERE proxyID = ?", [this.proxyID]));
}
Zotero.Proxy.prototype.revert = Zotero.Promise.coroutine(function* () {
if (!this.proxyID) throw new Error("Cannot revert an unsaved proxy");
var row = yield Zotero.DB.rowQueryAsync("SELECT * FROM proxies WHERE proxyID = ?", [this.proxyID]);
yield this._loadFromRow(row);
});
/**
* Deletes this proxy
@ -703,14 +712,14 @@ Zotero.Proxy.prototype.toProxy = function(uri) {
* Loads a proxy object from a DB row
* @private
*/
Zotero.Proxy.prototype._loadFromRow = function(row) {
Zotero.Proxy.prototype._loadFromRow = Zotero.Promise.coroutine(function* (row) {
this.proxyID = row.proxyID;
this.multiHost = !!row.multiHost;
this.autoAssociate = !!row.autoAssociate;
this.scheme = row.scheme;
this.hosts = Zotero.DB.columnQuery("SELECT hostname FROM proxyHosts WHERE proxyID = ? ORDER BY hostname", row.proxyID);
this.hosts = Zotero.DB.columnQueryAsync("SELECT hostname FROM proxyHosts WHERE proxyID = ? ORDER BY hostname", row.proxyID);
this.compileRegexp();
}
});
/**
* Detectors for various proxy systems

View file

@ -54,7 +54,7 @@ Zotero.Schema = new function(){
return dbVersion;
})
.catch(function (e) {
return Zotero.DB.tableExistsAsync('version')
return Zotero.DB.tableExists('version')
.then(function (exists) {
if (exists) {
throw e;
@ -101,7 +101,7 @@ Zotero.Schema = new function(){
}
})
.then(function () {
return Zotero.DB.executeTransaction(function (conn) {
return Zotero.DB.executeTransaction(function* (conn) {
var updated = yield _updateSchema('system');
// Update custom tables if they exist so that changes are in
@ -112,7 +112,7 @@ Zotero.Schema = new function(){
updated = yield _migrateUserDataSchema(userdata);
yield _updateSchema('triggers');
Zotero.DB.asyncResult(updated);
return updated;
})
.then(function (updated) {
// Populate combined tables for custom types and fields
@ -307,29 +307,26 @@ Zotero.Schema = new function(){
}
});
function _reloadSchema() {
Zotero.Schema.updateCustomTables()
.then(function () {
Zotero.ItemTypes.reload();
Zotero.ItemFields.reload();
Zotero.SearchConditions.reload();
// Update item type menus in every open window
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var enumerator = wm.getEnumerator("navigator:browser");
while (enumerator.hasMoreElements()) {
var win = enumerator.getNext();
win.ZoteroPane.buildItemTypeSubMenu();
win.document.getElementById('zotero-editpane-item-box').buildItemTypeMenu();
}
})
.done();
}
var _reloadSchema = Zotero.Promise.coroutine(function* () {
yield Zotero.Schema.updateCustomTables();
yield Zotero.ItemTypes.load();
yield Zotero.ItemFields.load();
yield Zotero.SearchConditions.init();
// Update item type menus in every open window
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var enumerator = wm.getEnumerator("navigator:browser");
while (enumerator.hasMoreElements()) {
var win = enumerator.getNext();
win.ZoteroPane.buildItemTypeSubMenu();
win.document.getElementById('zotero-editpane-item-box').buildItemTypeMenu();
}
});
this.updateCustomTables = function (skipDelete, skipSystem) {
return Zotero.DB.executeTransaction(function (conn) {
return Zotero.DB.executeTransaction(function* (conn) {
Zotero.debug("Updating custom tables");
if (!skipDelete) {
@ -926,10 +923,8 @@ Zotero.Schema = new function(){
* @param {Boolean} force Force a repository query regardless of how
* long it's been since the last check
*/
this.updateFromRepository = function (force) {
return Zotero.Promise.try(function () {
if (force) return true;
this.updateFromRepository = Zotero.Promise.coroutine(function* (force) {
if (!force) {
if (_remoteUpdateInProgress) {
Zotero.debug("A remote update is already in progress -- not checking repository");
return false;
@ -942,108 +937,99 @@ Zotero.Schema = new function(){
}
// Determine the earliest local time that we'd query the repository again
return self.getDBVersion('lastcheck')
.then(function (lastCheck) {
var nextCheck = new Date();
nextCheck.setTime((lastCheck + ZOTERO_CONFIG.REPOSITORY_CHECK_INTERVAL) * 1000);
var now = new Date();
// If enough time hasn't passed, don't update
if (now < nextCheck) {
Zotero.debug('Not enough time since last update -- not checking repository', 4);
// Set the repository timer to the remaining time
_setRepositoryTimer(Math.round((nextCheck.getTime() - now.getTime()) / 1000));
return false;
let lastCheck = yield this.getDBVersion('lastcheck');
let nextCheck = new Date();
nextCheck.setTime((lastCheck + ZOTERO_CONFIG.REPOSITORY_CHECK_INTERVAL) * 1000);
// If enough time hasn't passed, don't update
var now = new Date();
if (now < nextCheck) {
Zotero.debug('Not enough time since last update -- not checking repository', 4);
// Set the repository timer to the remaining time
_setRepositoryTimer(Math.round((nextCheck.getTime() - now.getTime()) / 1000));
return false;
}
}
if (_localUpdateInProgress) {
Zotero.debug('A local update is already in progress -- delaying repository check', 4);
_setRepositoryTimer(600);
return;
}
if (Zotero.locked) {
Zotero.debug('Zotero is locked -- delaying repository check', 4);
_setRepositoryTimer(600);
return;
}
// If transaction already in progress, delay by ten minutes
yield Zotero.DB.waitForTransaction();
// Get the last timestamp we got from the server
var lastUpdated = yield this.getDBVersion('repository');
try {
var url = ZOTERO_CONFIG.REPOSITORY_URL + '/updated?'
+ (lastUpdated ? 'last=' + lastUpdated + '&' : '')
+ 'version=' + Zotero.version;
Zotero.debug('Checking repository for updates');
_remoteUpdateInProgress = true;
if (force) {
if (force == 2) {
url += '&m=2';
}
else {
url += '&m=1';
}
return true;
});
})
.then(function (update) {
if (!update) return;
if (_localUpdateInProgress) {
Zotero.debug('A local update is already in progress -- delaying repository check', 4);
_setRepositoryTimer(600);
return;
}
if (Zotero.locked) {
Zotero.debug('Zotero is locked -- delaying repository check', 4);
_setRepositoryTimer(600);
return;
// Send list of installed styles
var styles = Zotero.Styles.getAll();
var styleTimestamps = [];
for (var id in styles) {
var updated = Zotero.Date.sqlToDate(styles[id].updated);
updated = updated ? updated.getTime() / 1000 : 0;
var selfLink = styles[id].url;
var data = {
id: id,
updated: updated
};
if (selfLink) {
data.url = selfLink;
}
styleTimestamps.push(data);
}
var body = 'styles=' + encodeURIComponent(JSON.stringify(styleTimestamps));
// If transaction already in progress, delay by ten minutes
if (Zotero.DB.transactionInProgress()) {
Zotero.debug('Transaction in progress -- delaying repository check', 4)
_setRepositoryTimer(600);
return;
try {
let xmlhttp = Zotero.HTTP.promise("POST", url, { body: body });
return _updateFromRepositoryCallback(xmlhttp, !!force);
}
// Get the last timestamp we got from the server
return self.getDBVersion('repository')
.then(function (lastUpdated) {
var url = ZOTERO_CONFIG['REPOSITORY_URL'] + '/updated?'
+ (lastUpdated ? 'last=' + lastUpdated + '&' : '')
+ 'version=' + Zotero.version;
Zotero.debug('Checking repository for updates');
_remoteUpdateInProgress = true;
if (force) {
if (force == 2) {
url += '&m=2';
catch (e) {
if (e instanceof Zotero.HTTP.BrowserOfflineException || e.xmlhttp) {
let msg = " -- retrying in " + ZOTERO_CONFIG.REPOSITORY_RETRY_INTERVAL
if (e instanceof Zotero.HTTP.BrowserOfflineException) {
Zotero.debug("Browser is offline" + msg, 2);
}
else {
url += '&m=1';
Components.utils.reportError(e);
Zotero.debug("Error updating from repository " + msg, 1);
}
// TODO: instead, add an observer to start and stop timer on online state change
_setRepositoryTimer(ZOTERO_CONFIG.REPOSITORY_RETRY_INTERVAL);
return;
}
// Send list of installed styles
var styles = Zotero.Styles.getAll();
var styleTimestamps = [];
for (var id in styles) {
var updated = Zotero.Date.sqlToDate(styles[id].updated);
updated = updated ? updated.getTime() / 1000 : 0;
var selfLink = styles[id].url;
var data = {
id: id,
updated: updated
};
if (selfLink) {
data.url = selfLink;
}
styleTimestamps.push(data);
}
var body = 'styles=' + encodeURIComponent(JSON.stringify(styleTimestamps));
return Zotero.HTTP.promise("POST", url, { body: body })
.then(function (xmlhttp) {
return _updateFromRepositoryCallback(xmlhttp, !!force);
})
.catch(function (e) {
if (e instanceof Zotero.HTTP.BrowserOfflineException || e.xmlhttp) {
var msg = " -- retrying in " + ZOTERO_CONFIG.REPOSITORY_RETRY_INTERVAL
if (e instanceof Zotero.HTTP.BrowserOfflineException) {
Zotero.debug("Browser is offline" + msg);
}
else {
Components.utils.reportError(e);
Zotero.debug("Error updating from repository " + msg);
}
// TODO: instead, add an observer to start and stop timer on online state change
_setRepositoryTimer(ZOTERO_CONFIG.REPOSITORY_RETRY_INTERVAL);
return;
}
throw e;
});
})
.finally(function () _remoteUpdateInProgress = false);
});
}
throw e;
};
}
finally {
_remoteUpdateInProgress = false;
}
});
this.stopRepositoryTimer = function () {
@ -1394,7 +1380,7 @@ Zotero.Schema = new function(){
* Create new DB schema
*/
function _initializeSchema(){
return Zotero.DB.executeTransaction(function (conn) {
return Zotero.DB.executeTransaction(function* (conn) {
// Enable auto-vacuuming
yield Zotero.DB.queryAsync("PRAGMA page_size = 4096");
yield Zotero.DB.queryAsync("PRAGMA encoding = 'UTF-8'");
@ -1536,7 +1522,7 @@ Zotero.Schema = new function(){
var styleUpdates = xmlhttp.responseXML.getElementsByTagName('style');
if (!translatorUpdates.length && !styleUpdates.length){
return Zotero.DB.executeTransaction(function (conn) {
return Zotero.DB.executeTransaction(function* (conn) {
// Store the timestamp provided by the server
yield _updateDBVersion('repository', currentTime);
@ -1579,7 +1565,7 @@ Zotero.Schema = new function(){
.then(function (update) {
if (!update) return false;
return Zotero.DB.executeTransaction(function (conn) {
return Zotero.DB.executeTransaction(function* (conn) {
// Store the timestamp provided by the server
yield _updateDBVersion('repository', currentTime);
@ -1766,7 +1752,7 @@ Zotero.Schema = new function(){
Zotero.debug('Updating user data tables from version ' + fromVersion + ' to ' + toVersion);
return Zotero.DB.executeTransaction(function (conn) {
return Zotero.DB.executeTransaction(function* (conn) {
// Step through version changes until we reach the current version
//
// Each block performs the changes necessary to move from the
@ -2020,7 +2006,7 @@ Zotero.Schema = new function(){
yield Zotero.DB.queryAsync("CREATE TABLE groupItems (\n itemID INTEGER PRIMARY KEY,\n createdByUserID INT,\n lastModifiedByUserID INT,\n FOREIGN KEY (itemID) REFERENCES items(itemID) ON DELETE CASCADE,\n FOREIGN KEY (createdByUserID) REFERENCES users(userID) ON DELETE SET NULL,\n FOREIGN KEY (lastModifiedByUserID) REFERENCES users(userID) ON DELETE SET NULL\n)");
yield Zotero.DB.queryAsync("INSERT OR IGNORE INTO groupItems SELECT * FROM groupItemsOld");
let cols = yield Zotero.DB.getColumnsAsync('fulltextItems');
let cols = yield Zotero.DB.getColumns('fulltextItems');
if (cols.indexOf("synced") == -1) {
Zotero.DB.queryAsync("ALTER TABLE fulltextItems ADD COLUMN synced INT DEFAULT 0");
Zotero.DB.queryAsync("REPLACE INTO settings (setting, key, value) VALUES ('fulltext', 'downloadAll', 1)");

View file

@ -1653,6 +1653,13 @@ Zotero.Searches = new function(){
Zotero.DataObjects.apply(this, ['search', 'searches', 'savedSearch', 'savedSearches']);
this.constructor.prototype = new Zotero.DataObjects();
this.init = Zotero.Promise.coroutine(function* () {
yield this.constructor.prototype.init.apply(this);
yield Zotero.SearchConditions.init();
});
/**
* Returns an array of Zotero.Search objects, ordered by name
*
@ -1765,7 +1772,7 @@ Zotero.SearchConditions = new function(){
* - template (special handling)
* - noLoad (can't load from saved search)
*/
function _init(){
this.init = Zotero.Promise.coroutine(function* () {
var conditions = [
//
// Special conditions
@ -2076,9 +2083,9 @@ Zotero.SearchConditions = new function(){
},
table: 'itemData',
field: 'value',
aliases: Zotero.DB.columnQuery("SELECT fieldName FROM fieldsCombined " +
"WHERE fieldName NOT IN ('accessDate', 'date', 'pages', " +
"'section','seriesNumber','issue')"),
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
},
@ -2256,19 +2263,13 @@ Zotero.SearchConditions = new function(){
_standardConditions.sort(function(a, b) {
return collation.compareString(1, a.localized, b.localized);
});
_initialized = true;
}
});
/*
* Get condition data
*/
function get(condition){
if (!_initialized){
_init();
}
return _conditions[condition];
}
@ -2279,10 +2280,6 @@ Zotero.SearchConditions = new function(){
* Does not include special conditions, only ones that would show in a drop-down list
*/
function getStandardConditions(){
if (!_initialized){
_init();
}
// TODO: return copy instead
return _standardConditions;
}
@ -2292,10 +2289,6 @@ Zotero.SearchConditions = new function(){
* Check if an operator is valid for a given condition
*/
function hasOperator(condition, operator){
if (!_initialized){
_init();
}
var [condition, mode] = this.parseCondition(condition);
if (!_conditions[condition]){
@ -2370,11 +2363,4 @@ Zotero.SearchConditions = new function(){
return [condition, mode];
}
this.reload = function () {
_initialized = false;
_conditions = {};
_standardConditions = [];
}
}

View file

@ -731,7 +731,7 @@ Zotero.Sync.Storage.ZFS = (function () {
_rootURI = uri;
uri = uri.clone();
uri.spec += 'users/' + Zotero.userID + '/';
uri.spec += 'users/' + Zotero.Users.getCurrentUserID() + '/';
_userURI = uri;
};
@ -1132,7 +1132,7 @@ Zotero.Sync.Storage.ZFS = (function () {
}.bind(this))
then(function () {
// If we don't have a user id we've never synced and don't need to bother
if (!Zotero.userID) {
if (!Zotero.Users.getCurrentUserID()) {
return false;
}

View file

@ -71,6 +71,9 @@ Zotero.Sync = new function() {
this.init = function () {
Zotero.debug("Syncing is disabled", 1);
return;
Zotero.DB.beginTransaction();
var sql = "SELECT version FROM version WHERE schema='syncdeletelog'";
@ -2373,7 +2376,7 @@ Zotero.Sync.Server = new function () {
if (lastUserID != userID || lastLibraryID != libraryID) {
if (!lastLibraryID) {
var repl = "local/" + Zotero.getLocalUserKey();
var repl = "local/" + Zotero.Users.getLocalUserKey();
}
Zotero.userID = userID;

View file

@ -37,12 +37,11 @@ Zotero.URI = new function () {
* @return {String|False} e.g., 'http://zotero.org/users/v3aG8nQf'
*/
this.getLocalUserURI = function () {
var key = Zotero.getLocalUserKey();
var key = Zotero.Users.getLocalUserKey();
if (!key) {
return false;
}
return _baseURI + "users/local/" + Zotero.getLocalUserKey();
return _baseURI + "users/local/" + key;
}
@ -52,7 +51,7 @@ Zotero.URI = new function () {
* @return {String}
*/
this.getCurrentUserURI = function (noLocal) {
var userID = Zotero.userID;
var userID = Zotero.Users.getCurrentUserID();
if (!userID && noLocal) {
throw new Exception("Local userID not available and noLocal set in Zotero.URI.getCurrentUserURI()");
}
@ -60,12 +59,12 @@ Zotero.URI = new function () {
return _baseURI + "users/" + userID;
}
return _baseURI + "users/local/" + Zotero.getLocalUserKey(true);
return _baseURI + "users/local/" + Zotero.Users.getLocalUserKey();
}
this.getCurrentUserLibraryURI = function () {
var userID = Zotero.userID;
var userID = Zotero.Users.getCurrentUserID();
if (!userID) {
return false;
}
@ -87,7 +86,7 @@ Zotero.URI = new function () {
switch (libraryType) {
case 'user':
var id = Zotero.userID;
var id = Zotero.Users.getCurrentUserID();
if (!id) {
throw new Exception("User id not available in Zotero.URI.getLibraryPath()");
}
@ -269,7 +268,7 @@ Zotero.URI = new function () {
}
}
} else {
if(id == Zotero.userID) {
if(id == Zotero.Users.getCurrentUserID()) {
return null;
}
}

View file

@ -51,7 +51,6 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
(function(){
// Privileged (public) methods
this.init = init;
this.stateCheck = stateCheck;
this.getProfileDirectory = getProfileDirectory;
this.getZoteroDirectory = getZoteroDirectory;
this.getStorageDirectory = getStorageDirectory;
@ -86,66 +85,10 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
Components.utils.import("resource://zotero/bluebird.js", this);
this.__defineGetter__('userID', function () {
if (_userID !== undefined) return _userID;
var sql = "SELECT value FROM settings WHERE "
+ "setting='account' AND key='userID'";
return _userID = Zotero.DB.valueQuery(sql);
});
this.__defineSetter__('userID', function (val) {
var sql = "REPLACE INTO settings VALUES ('account', 'userID', ?)";
Zotero.DB.query(sql, parseInt(val));
_userID = val;
});
this.__defineGetter__('libraryID', function () {
if (_libraryID !== undefined) return _libraryID;
var sql = "SELECT value FROM settings WHERE "
+ "setting='account' AND key='libraryID'";
return _libraryID = Zotero.DB.valueQuery(sql);
});
this.__defineSetter__('libraryID', function (val) {
var sql = "REPLACE INTO settings VALUES ('account', 'libraryID', ?)";
Zotero.DB.query(sql, parseInt(val));
_libraryID = val;
});
this.__defineGetter__('username', function () {
var sql = "SELECT value FROM settings WHERE "
+ "setting='account' AND key='username'";
return Zotero.DB.valueQuery(sql);
});
this.__defineSetter__('username', function (val) {
var sql = "REPLACE INTO settings VALUES ('account', 'username', ?)";
Zotero.DB.query(sql, val);
});
this.getActiveZoteroPane = function() {
return Services.wm.getMostRecentWindow("navigator:browser").ZoteroPane;
};
this.getLocalUserKey = function (generate) {
if (_localUserKey) {
return _localUserKey;
}
var sql = "SELECT value FROM settings WHERE "
+ "setting='account' AND key='localUserKey'";
var key = Zotero.DB.valueQuery(sql);
// Generate a local user key if we don't have a global library id
if (!key && generate) {
key = Zotero.randomString(8);
var sql = "INSERT INTO settings VALUES ('account', 'localUserKey', ?)";
Zotero.DB.query(sql, key);
}
_localUserKey = key;
return key;
};
/**
* @property {Boolean} waiting Whether Zotero is waiting for other
* main thread events to be processed
@ -568,7 +511,7 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
}
}
if(!_initDB()) return false;
if(!(yield _initDB())) return false;
Zotero.HTTP.triggerProxyAuth();
@ -640,6 +583,15 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
Zotero.locked = false;
yield Zotero.Users.init();
yield Zotero.Libraries.init();
yield Zotero.ItemTypes.init();
yield Zotero.ItemFields.init();
yield Zotero.CreatorTypes.init();
yield Zotero.CharacterSets.init();
yield Zotero.FileTypes.init();
// Initialize various services
Zotero.Styles.preinit();
Zotero.Integration.init();
@ -656,7 +608,7 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
Zotero.Sync.Runner.init();
Zotero.MIMETypeHandler.init();
Zotero.Proxies.init();
yield Zotero.Proxies.init();
// Initialize keyboard shortcuts
Zotero.Keys.init();
@ -762,10 +714,10 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
/**
* Initializes the DB connection
*/
function _initDB(haveReleasedLock) {
var _initDB = Zotero.Promise.coroutine(function* (haveReleasedLock) {
try {
// Test read access
Zotero.DB.test();
yield Zotero.DB.test();
var dbfile = Zotero.getZoteroDatabase();
@ -843,7 +795,7 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
}
return true;
}
});
/**
* Called when the DB has been released by another Zotero process to perform necessary
@ -859,21 +811,6 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
}
}
/*
* Check if a DB transaction is open and, if so, disable Zotero
*/
function stateCheck() {
if(!Zotero.isConnector && Zotero.DB.transactionInProgress()) {
Zotero.logError("State check failed due to transaction in progress");
this.initialized = false;
this.skipLoading = true;
return false;
}
return true;
}
this.shutdown = function() {
Zotero.debug("Shutting down Zotero");
@ -893,14 +830,7 @@ Components.utils.import("resource://gre/modules/osfile.jsm");
// remove temp directory
Zotero.removeTempDirectory();
if(Zotero.DB && Zotero.DB._connection) {
Zotero.debug("Closing database");
// run GC to finalize open statements
// TODO remove this and finalize statements created with
// Zotero.DBConnection.getStatement() explicitly
Components.utils.forceGC();
if (Zotero.DB && Zotero.DB._connectionAsync) {
// close DB
return Zotero.DB.closeDatabase(true).then(function() {
// broadcast that DB lock has been released

View file

@ -729,10 +729,7 @@ var ZoteroPane = new function()
*/
this.newItem = Zotero.Promise.coroutine(function* (typeID, data, row, manual)
{
if (!Zotero.stateCheck()) {
this.displayErrorMessage(true);
return false;
}
yield Zotero.DB.waitForTransaction();
if ((row === undefined || row === null) && this.collectionsView.selection) {
row = this.collectionsView.selection.currentIndex;
@ -754,7 +751,7 @@ var ZoteroPane = new function()
}
let itemID;
yield Zotero.DB.executeTransaction(function () {
yield Zotero.DB.executeTransaction(function* () {
var item = new Zotero.Item(typeID);
item.libraryID = libraryID;
for (var i in data) {
@ -763,7 +760,7 @@ var ZoteroPane = new function()
itemID = yield item.save();
if (collectionTreeRow && collectionTreeRow.isCollection()) {
collectionTreeRow.ref.addItem(itemID);
yield collectionTreeRow.ref.addItem(itemID);
}
});
@ -800,11 +797,8 @@ var ZoteroPane = new function()
}
this.newCollection = Zotero.Promise.method(function (parentKey) {
if (!Zotero.stateCheck()) {
this.displayErrorMessage(true);
return false;
}
this.newCollection = Zotero.Promise.coroutine(function* (parentKey) {
yield Zotero.DB.waitForTransaction();
if (!this.canEditLibrary()) {
this.displayCannotEditLibraryMessage();
@ -815,7 +809,7 @@ var ZoteroPane = new function()
var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Components.interfaces.nsIPromptService);
var untitled = Zotero.DB.getNextName(
var untitled = yield Zotero.DB.getNextName(
libraryID,
'collections',
'collectionName',
@ -851,17 +845,14 @@ var ZoteroPane = new function()
this.newSearch = Zotero.Promise.coroutine(function* () {
if (!Zotero.stateCheck()) {
this.displayErrorMessage(true);
return false;
}
yield Zotero.DB.waitForTransaction();
var s = new Zotero.Search();
s.libraryID = this.getSelectedLibraryID();
yield s.addCondition('title', 'contains', '');
var untitled = Zotero.getString('pane.collections.untitled');
untitled = Zotero.DB.getNextName('savedSearches', 'savedSearchName',
untitled = yield Zotero.DB.getNextName('savedSearches', 'savedSearchName',
Zotero.getString('pane.collections.untitled'));
var io = {dataIn: {search: s, name: untitled}, dataOut: null};
window.openDialog('chrome://zotero/content/searchDialog.xul','','chrome,modal',io);
@ -939,11 +930,8 @@ var ZoteroPane = new function()
}
this.openLookupWindow = function () {
if (!Zotero.stateCheck()) {
this.displayErrorMessage(true);
return false;
}
this.openLookupWindow = Zotero.Promise.coroutine(function* () {
yield Zotero.DB.waitForTransaction();
if (!this.canEdit()) {
this.displayCannotEditLibraryMessage();
@ -951,7 +939,7 @@ var ZoteroPane = new function()
}
window.openDialog('chrome://zotero/content/lookup.xul', 'zotero-lookup', 'chrome,modal');
}
});
this.openAdvancedSearchWindow = Zotero.Promise.coroutine(function* () {
@ -1220,10 +1208,7 @@ var ZoteroPane = new function()
*/
this.itemSelected = function (event) {
return Zotero.spawn(function* () {
if (!Zotero.stateCheck()) {
this.displayErrorMessage();
return;
}
yield Zotero.DB.waitForTransaction();
// Display restore button if items selected in Trash
if (this.itemsView.selection.count) {
@ -2933,10 +2918,7 @@ var ZoteroPane = new function()
* @return {Promise}
*/
this.newNote = Zotero.Promise.coroutine(function* (popup, parentKey, text, citeURI) {
if (!Zotero.stateCheck()) {
this.displayErrorMessage(true);
return;
}
yield Zotero.DB.waitForTransaction();
if (!this.canEdit()) {
this.displayCannotEditLibraryMessage();
@ -3230,10 +3212,7 @@ var ZoteroPane = new function()
//
// Duplicate newItem() checks here
//
if (!Zotero.stateCheck()) {
this.displayErrorMessage(true);
return false;
}
yield Zotero.DB.waitForTransaction();
// Currently selected row
if (row === undefined && this.collectionsView && this.collectionsView.selection) {
@ -3328,126 +3307,118 @@ var ZoteroPane = new function()
url = Zotero.Utilities.resolveIntermediateURL(url);
return Zotero.MIME.getMIMETypeFromURL(url)
.spread(function (mimeType, hasNativeHandler) {
var self = this;
let [mimeType, hasNativeHandler] = yield Zotero.MIME.getMIMETypeFromURL(url);
// If native type, save using a hidden browser
if (hasNativeHandler) {
var deferred = Zotero.Promise.defer();
// If native type, save using a hidden browser
if (hasNativeHandler) {
var deferred = Zotero.Promise.defer();
var processor = function (doc) {
ZoteroPane_Local.addItemFromDocument(doc, itemType, saveSnapshot, row)
.then(function () {
deferred.resolve()
})
};
// TODO: processDocuments should wait for the processor promise to be resolved
var done = function () {}
var exception = function (e) {
Zotero.debug(e, 1);
deferred.reject(e);
}
Zotero.HTTP.processDocuments([url], processor, done, exception);
return deferred.promise;
var processor = function (doc) {
ZoteroPane_Local.addItemFromDocument(doc, itemType, saveSnapshot, row)
.then(function () {
deferred.resolve()
})
};
// TODO: processDocuments should wait for the processor promise to be resolved
var done = function () {}
var exception = function (e) {
Zotero.debug(e, 1);
deferred.reject(e);
}
// Otherwise create placeholder item, attach attachment, and update from that
else {
// TODO: this, needless to say, is a temporary hack
if (itemType == 'temporaryPDFHack') {
itemType = null;
Zotero.HTTP.processDocuments([url], processor, done, exception);
return deferred.promise;
}
// Otherwise create placeholder item, attach attachment, and update from that
else {
// TODO: this, needless to say, is a temporary hack
if (itemType == 'temporaryPDFHack') {
itemType = null;
if (mimeType == 'application/pdf') {
//
// Duplicate newItem() checks here
//
yield Zotero.DB.waitForTransaction();
if (mimeType == 'application/pdf') {
//
// Duplicate newItem() checks here
//
if (!Zotero.stateCheck()) {
ZoteroPane_Local.displayErrorMessage(true);
return false;
}
// Currently selected row
if (row === undefined) {
row = ZoteroPane_Local.collectionsView.selection.currentIndex;
}
if (!ZoteroPane_Local.canEdit(row)) {
ZoteroPane_Local.displayCannotEditLibraryMessage();
return;
}
if (row !== undefined) {
var collectionTreeRow = ZoteroPane_Local.collectionsView.getRow(row);
var libraryID = collectionTreeRow.ref.libraryID;
}
else {
var libraryID = 0;
var collectionTreeRow = null;
}
//
//
//
if (!ZoteroPane_Local.canEditFiles(row)) {
ZoteroPane_Local.displayCannotEditLibraryFilesMessage();
return;
}
if (collectionTreeRow && collectionTreeRow.isCollection()) {
var collectionID = collectionTreeRow.ref.id;
}
else {
var collectionID = false;
}
// TODO: Update for async DB
var attachmentItem = Zotero.Attachments.importFromURL(url, false,
false, false, collectionID, mimeType, libraryID,
function(attachmentItem) {
self.selectItem(attachmentItem.id);
});
// Currently selected row
if (row === undefined) {
row = ZoteroPane_Local.collectionsView.selection.currentIndex;
}
if (!ZoteroPane_Local.canEdit(row)) {
ZoteroPane_Local.displayCannotEditLibraryMessage();
return;
}
}
if (!itemType) {
itemType = 'webpage';
}
return Zotero.Promise.coroutine(function* () {
var item = yield ZoteroPane_Local.newItem(itemType, {}, row)
if (item.libraryID) {
var group = Zotero.Groups.getByLibraryID(item.libraryID);
filesEditable = group.filesEditable;
if (row !== undefined) {
var collectionTreeRow = ZoteroPane_Local.collectionsView.getRow(row);
var libraryID = collectionTreeRow.ref.libraryID;
}
else {
filesEditable = true;
var libraryID = 0;
var collectionTreeRow = null;
}
//
//
//
if (!ZoteroPane_Local.canEditFiles(row)) {
ZoteroPane_Local.displayCannotEditLibraryFilesMessage();
return;
}
// Save snapshot if explicitly enabled or automatically pref is set and not explicitly disabled
if (saveSnapshot || (saveSnapshot !== false && Zotero.Prefs.get('automaticSnapshots'))) {
var link = false;
if (link) {
//Zotero.Attachments.linkFromURL(doc, item.id);
}
else if (filesEditable) {
var attachmentItem = Zotero.Attachments.importFromURL(url, item.id, false, false, false, mimeType);
if (attachmentItem) {
item.setField('title', attachmentItem.getField('title'));
item.setField('url', attachmentItem.getField('url'));
item.setField('accessDate', attachmentItem.getField('accessDate'));
yield item.save();
}
}
if (collectionTreeRow && collectionTreeRow.isCollection()) {
var collectionID = collectionTreeRow.ref.id;
}
else {
var collectionID = false;
}
return item.id;
})();
// TODO: Update for async DB
var attachmentItem = Zotero.Attachments.importFromURL(url, false,
false, false, collectionID, mimeType, libraryID,
function(attachmentItem) {
self.selectItem(attachmentItem.id);
});
return;
}
}
});
if (!itemType) {
itemType = 'webpage';
}
var item = yield ZoteroPane_Local.newItem(itemType, {}, row)
if (item.libraryID) {
var group = Zotero.Groups.getByLibraryID(item.libraryID);
filesEditable = group.filesEditable;
}
else {
filesEditable = true;
}
// Save snapshot if explicitly enabled or automatically pref is set and not explicitly disabled
if (saveSnapshot || (saveSnapshot !== false && Zotero.Prefs.get('automaticSnapshots'))) {
var link = false;
if (link) {
//Zotero.Attachments.linkFromURL(doc, item.id);
}
else if (filesEditable) {
var attachmentItem = Zotero.Attachments.importFromURL(url, item.id, false, false, false, mimeType);
if (attachmentItem) {
item.setField('title', attachmentItem.getField('title'));
item.setField('url', attachmentItem.getField('url'));
item.setField('accessDate', attachmentItem.getField('accessDate'));
yield item.save();
}
}
}
return item.id;
}
});
@ -3457,11 +3428,8 @@ var ZoteroPane = new function()
* |itemID| -- itemID of parent item
* |link| -- create web link instead of snapshot
*/
this.addAttachmentFromPage = Zotero.Promise.method(function (link, itemID) {
if (!Zotero.stateCheck()) {
this.displayErrorMessage(true);
return;
}
this.addAttachmentFromPage = Zotero.Promise.coroutine(function* (link, itemID) {
yield Zotero.DB.waitForTransaction();
if (typeof itemID != 'number') {
throw new Error("itemID must be an integer");

View file

@ -109,6 +109,7 @@ const xpcomFilesLocal = [
'syncedSettings',
'timeline',
'uri',
'users',
'translation/translate_item',
'translation/translators',
'server_connector'

View file

@ -1,868 +0,0 @@
// This is Mozilla code from Firefox 24.
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = [
"Sqlite",
];
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
Cu.import("resource://gre/modules/osfile.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://zotero/log4moz.js");
XPCOMUtils.defineLazyModuleGetter(this, "CommonUtils",
"resource://services-common/utils.js");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
// Counts the number of created connections per database basename(). This is
// used for logging to distinguish connection instances.
let connectionCounters = {};
/**
* Opens a connection to a SQLite database.
*
* The following parameters can control the connection:
*
* path -- (string) The filesystem path of the database file to open. If the
* file does not exist, a new database will be created.
*
* sharedMemoryCache -- (bool) Whether multiple connections to the database
* share the same memory cache. Sharing the memory cache likely results
* in less memory utilization. However, sharing also requires connections
* to obtain a lock, possibly making database access slower. Defaults to
* true.
*
* shrinkMemoryOnConnectionIdleMS -- (integer) If defined, the connection
* will attempt to minimize its memory usage after this many
* milliseconds of connection idle. The connection is idle when no
* statements are executing. There is no default value which means no
* automatic memory minimization will occur. Please note that this is
* *not* a timer on the idle service and this could fire while the
* application is active.
*
* FUTURE options to control:
*
* special named databases
* pragma TEMP STORE = MEMORY
* TRUNCATE JOURNAL
* SYNCHRONOUS = full
*
* @param options
* (Object) Parameters to control connection and open options.
*
* @return Promise<OpenedConnection>
*/
function openConnection(options) {
let log = Log4Moz.repository.getLogger("Sqlite.ConnectionOpener");
if (!options.path) {
throw new Error("path not specified in connection options.");
}
// Retains absolute paths and normalizes relative as relative to profile.
let path = OS.Path.join(OS.Constants.Path.profileDir, options.path);
let sharedMemoryCache = "sharedMemoryCache" in options ?
options.sharedMemoryCache : true;
let openedOptions = {};
if ("shrinkMemoryOnConnectionIdleMS" in options) {
if (!Number.isInteger(options.shrinkMemoryOnConnectionIdleMS)) {
throw new Error("shrinkMemoryOnConnectionIdleMS must be an integer. " +
"Got: " + options.shrinkMemoryOnConnectionIdleMS);
}
openedOptions.shrinkMemoryOnConnectionIdleMS =
options.shrinkMemoryOnConnectionIdleMS;
}
let file = FileUtils.File(path);
let openDatabaseFn = sharedMemoryCache ?
Services.storage.openDatabase :
Services.storage.openUnsharedDatabase;
let basename = OS.Path.basename(path);
if (!connectionCounters[basename]) {
connectionCounters[basename] = 1;
}
let number = connectionCounters[basename]++;
let identifier = basename + "#" + number;
log.info("Opening database: " + path + " (" + identifier + ")");
try {
let connection = openDatabaseFn(file);
if (!connection.connectionReady) {
log.warn("Connection is not ready.");
return Promise.reject(new Error("Connection is not ready."));
}
return Promise.resolve(new OpenedConnection(connection, basename, number,
openedOptions));
} catch (ex) {
log.warn("Could not open database: " + CommonUtils.exceptionStr(ex));
return Promise.reject(ex);
}
}
/**
* Handle on an opened SQLite database.
*
* This is essentially a glorified wrapper around mozIStorageConnection.
* However, it offers some compelling advantages.
*
* The main functions on this type are `execute` and `executeCached`. These are
* ultimately how all SQL statements are executed. It's worth explaining their
* differences.
*
* `execute` is used to execute one-shot SQL statements. These are SQL
* statements that are executed one time and then thrown away. They are useful
* for dynamically generated SQL statements and clients who don't care about
* performance (either their own or wasting resources in the overall
* application). Because of the performance considerations, it is recommended
* to avoid `execute` unless the statement you are executing will only be
* executed once or seldomly.
*
* `executeCached` is used to execute a statement that will presumably be
* executed multiple times. The statement is parsed once and stuffed away
* inside the connection instance. Subsequent calls to `executeCached` will not
* incur the overhead of creating a new statement object. This should be used
* in preference to `execute` when a specific SQL statement will be executed
* multiple times.
*
* Instances of this type are not meant to be created outside of this file.
* Instead, first open an instance of `UnopenedSqliteConnection` and obtain
* an instance of this type by calling `open`.
*
* FUTURE IMPROVEMENTS
*
* Ability to enqueue operations. Currently there can be race conditions,
* especially as far as transactions are concerned. It would be nice to have
* an enqueueOperation(func) API that serially executes passed functions.
*
* Support for SAVEPOINT (named/nested transactions) might be useful.
*
* @param connection
* (mozIStorageConnection) Underlying SQLite connection.
* @param basename
* (string) The basename of this database name. Used for logging.
* @param number
* (Number) The connection number to this database.
* @param options
* (object) Options to control behavior of connection. See
* `openConnection`.
*/
function OpenedConnection(connection, basename, number, options) {
let log = Log4Moz.repository.getLogger("Sqlite.Connection." + basename);
// getLogger() returns a shared object. We can't modify the functions on this
// object since they would have effect on all instances and last write would
// win. So, we create a "proxy" object with our custom functions. Everything
// else is proxied back to the shared logger instance via prototype
// inheritance.
let logProxy = {__proto__: log};
// Automatically prefix all log messages with the identifier.
for (let level in Log4Moz.Level) {
if (level == "Desc") {
continue;
}
let lc = level.toLowerCase();
logProxy[lc] = function (msg) {
return log[lc].call(log, "Conn #" + number + ": " + msg);
};
}
this._log = logProxy;
this._log.info("Opened");
this._connection = connection;
this._open = true;
this._cachedStatements = new Map();
this._anonymousStatements = new Map();
this._anonymousCounter = 0;
// A map from statement index to mozIStoragePendingStatement, to allow for
// canceling prior to finalizing the mozIStorageStatements.
this._pendingStatements = new Map();
// Increments for each executed statement for the life of the connection.
this._statementCounter = 0;
this._inProgressTransaction = null;
this._idleShrinkMS = options.shrinkMemoryOnConnectionIdleMS;
if (this._idleShrinkMS) {
this._idleShrinkTimer = Cc["@mozilla.org/timer;1"]
.createInstance(Ci.nsITimer);
// We wait for the first statement execute to start the timer because
// shrinking now would not do anything.
}
}
OpenedConnection.prototype = Object.freeze({
TRANSACTION_DEFERRED: "DEFERRED",
TRANSACTION_IMMEDIATE: "IMMEDIATE",
TRANSACTION_EXCLUSIVE: "EXCLUSIVE",
TRANSACTION_TYPES: ["DEFERRED", "IMMEDIATE", "EXCLUSIVE"],
get connectionReady() {
return this._open && this._connection.connectionReady;
},
/**
* The row ID from the last INSERT operation.
*
* Because all statements are executed asynchronously, this could
* return unexpected results if multiple statements are performed in
* parallel. It is the caller's responsibility to schedule
* appropriately.
*
* It is recommended to only use this within transactions (which are
* handled as sequential statements via Tasks).
*/
get lastInsertRowID() {
this._ensureOpen();
return this._connection.lastInsertRowID;
},
/**
* The number of rows that were changed, inserted, or deleted by the
* last operation.
*
* The same caveats regarding asynchronous execution for
* `lastInsertRowID` also apply here.
*/
get affectedRows() {
this._ensureOpen();
return this._connection.affectedRows;
},
/**
* The integer schema version of the database.
*
* This is 0 if not schema version has been set.
*/
get schemaVersion() {
this._ensureOpen();
return this._connection.schemaVersion;
},
set schemaVersion(value) {
this._ensureOpen();
this._connection.schemaVersion = value;
},
/**
* Close the database connection.
*
* This must be performed when you are finished with the database.
*
* Closing the database connection has the side effect of forcefully
* cancelling all active statements. Therefore, callers should ensure that
* all active statements have completed before closing the connection, if
* possible.
*
* The returned promise will be resolved once the connection is closed.
*
* IMPROVEMENT: Resolve the promise to a closed connection which can be
* reopened.
*
* @return Promise<>
*/
close: function () {
if (!this._connection) {
return Promise.resolve();
}
this._log.debug("Request to close connection.");
this._clearIdleShrinkTimer();
let deferred = Promise.defer();
// We need to take extra care with transactions during shutdown.
//
// If we don't have a transaction in progress, we can proceed with shutdown
// immediately.
if (!this._inProgressTransaction) {
this._finalize(deferred);
return deferred.promise;
}
// Else if we do have a transaction in progress, we forcefully roll it
// back. This is an async task, so we wait on it to finish before
// performing finalization.
this._log.warn("Transaction in progress at time of close. Rolling back.");
let onRollback = this._finalize.bind(this, deferred);
this.execute("ROLLBACK TRANSACTION").then(onRollback, onRollback);
this._inProgressTransaction.reject(new Error("Connection being closed."));
this._inProgressTransaction = null;
return deferred.promise;
},
_finalize: function (deferred) {
this._log.debug("Finalizing connection.");
// Cancel any pending statements.
for (let [k, statement] of this._pendingStatements) {
statement.cancel();
}
this._pendingStatements.clear();
// We no longer need to track these.
this._statementCounter = 0;
// Next we finalize all active statements.
for (let [k, statement] of this._anonymousStatements) {
statement.finalize();
}
this._anonymousStatements.clear();
for (let [k, statement] of this._cachedStatements) {
statement.finalize();
}
this._cachedStatements.clear();
// This guards against operations performed between the call to this
// function and asyncClose() finishing. See also bug 726990.
this._open = false;
this._log.debug("Calling asyncClose().");
this._connection.asyncClose({
complete: function () {
this._log.info("Closed");
this._connection = null;
deferred.resolve();
}.bind(this),
});
},
/**
* Execute a SQL statement and cache the underlying statement object.
*
* This function executes a SQL statement and also caches the underlying
* derived statement object so subsequent executions are faster and use
* less resources.
*
* This function optionally binds parameters to the statement as well as
* optionally invokes a callback for every row retrieved.
*
* By default, no parameters are bound and no callback will be invoked for
* every row.
*
* Bound parameters can be defined as an Array of positional arguments or
* an object mapping named parameters to their values. If there are no bound
* parameters, the caller can pass nothing or null for this argument.
*
* Callers are encouraged to pass objects rather than Arrays for bound
* parameters because they prevent foot guns. With positional arguments, it
* is simple to modify the parameter count or positions without fixing all
* users of the statement. Objects/named parameters are a little safer
* because changes in order alone won't result in bad things happening.
*
* When `onRow` is not specified, all returned rows are buffered before the
* returned promise is resolved. For INSERT or UPDATE statements, this has
* no effect because no rows are returned from these. However, it has
* implications for SELECT statements.
*
* If your SELECT statement could return many rows or rows with large amounts
* of data, for performance reasons it is recommended to pass an `onRow`
* handler. Otherwise, the buffering may consume unacceptable amounts of
* resources.
*
* If a `StopIteration` is thrown during execution of an `onRow` handler,
* the execution of the statement is immediately cancelled. Subsequent
* rows will not be processed and no more `onRow` invocations will be made.
* The promise is resolved immediately.
*
* If a non-`StopIteration` exception is thrown by the `onRow` handler, the
* exception is logged and processing of subsequent rows occurs as if nothing
* happened. The promise is still resolved (not rejected).
*
* The return value is a promise that will be resolved when the statement
* has completed fully.
*
* The promise will be rejected with an `Error` instance if the statement
* did not finish execution fully. The `Error` may have an `errors` property.
* If defined, it will be an Array of objects describing individual errors.
* Each object has the properties `result` and `message`. `result` is a
* numeric error code and `message` is a string description of the problem.
*
* @param name
* (string) The name of the registered statement to execute.
* @param params optional
* (Array or object) Parameters to bind.
* @param onRow optional
* (function) Callback to receive each row from result.
*/
executeCached: function (sql, params=null, onRow=null) {
this._ensureOpen();
if (!sql) {
throw new Error("sql argument is empty.");
}
let statement = this._cachedStatements.get(sql);
if (!statement) {
statement = this._connection.createAsyncStatement(sql);
this._cachedStatements.set(sql, statement);
}
this._clearIdleShrinkTimer();
let deferred = Promise.defer();
try {
this._executeStatement(sql, statement, params, onRow).then(
function onResult(result) {
this._startIdleShrinkTimer();
deferred.resolve(result);
}.bind(this),
function onError(error) {
this._startIdleShrinkTimer();
deferred.reject(error);
}.bind(this)
);
} catch (ex) {
this._startIdleShrinkTimer();
throw ex;
}
return deferred.promise;
},
/**
* Execute a one-shot SQL statement.
*
* If you find yourself feeding the same SQL string in this function, you
* should *not* use this function and instead use `executeCached`.
*
* See `executeCached` for the meaning of the arguments and extended usage info.
*
* @param sql
* (string) SQL to execute.
* @param params optional
* (Array or Object) Parameters to bind to the statement.
* @param onRow optional
* (function) Callback to receive result of a single row.
*/
execute: function (sql, params=null, onRow=null) {
if (typeof(sql) != "string") {
throw new Error("Must define SQL to execute as a string: " + sql);
}
this._ensureOpen();
let statement = this._connection.createAsyncStatement(sql);
let index = this._anonymousCounter++;
this._anonymousStatements.set(index, statement);
this._clearIdleShrinkTimer();
let onFinished = function () {
this._anonymousStatements.delete(index);
statement.finalize();
this._startIdleShrinkTimer();
}.bind(this);
let deferred = Promise.defer();
try {
this._executeStatement(sql, statement, params, onRow).then(
function onResult(rows) {
onFinished();
deferred.resolve(rows);
}.bind(this),
function onError(error) {
onFinished();
deferred.reject(error);
}.bind(this)
);
} catch (ex) {
onFinished();
throw ex;
}
return deferred.promise;
},
/**
* Whether a transaction is currently in progress.
*/
get transactionInProgress() {
return this._open && !!this._inProgressTransaction;
},
/**
* Perform a transaction.
*
* A transaction is specified by a user-supplied function that is a
* generator function which can be used by Task.jsm's Task.spawn(). The
* function receives this connection instance as its argument.
*
* The supplied function is expected to yield promises. These are often
* promises created by calling `execute` and `executeCached`. If the
* generator is exhausted without any errors being thrown, the
* transaction is committed. If an error occurs, the transaction is
* rolled back.
*
* The returned value from this function is a promise that will be resolved
* once the transaction has been committed or rolled back. The promise will
* be resolved to whatever value the supplied function resolves to. If
* the transaction is rolled back, the promise is rejected.
*
* @param func
* (function) What to perform as part of the transaction.
* @param type optional
* One of the TRANSACTION_* constants attached to this type.
*/
executeTransaction: function (func, type=this.TRANSACTION_DEFERRED) {
if (this.TRANSACTION_TYPES.indexOf(type) == -1) {
throw new Error("Unknown transaction type: " + type);
}
this._ensureOpen();
if (this._inProgressTransaction) {
throw new Error("A transaction is already active. Only one transaction " +
"can be active at a time.");
}
this._log.debug("Beginning transaction");
let deferred = Promise.defer();
this._inProgressTransaction = deferred;
Task.spawn(function doTransaction() {
// It's tempting to not yield here and rely on the implicit serial
// execution of issued statements. However, the yield serves an important
// purpose: catching errors in statement execution.
yield this.execute("BEGIN " + type + " TRANSACTION");
let result;
try {
result = yield Task.spawn(func(this));
} catch (ex) {
// It's possible that a request to close the connection caused the
// error.
// Assertion: close() will unset this._inProgressTransaction when
// called.
if (!this._inProgressTransaction) {
this._log.warn("Connection was closed while performing transaction. " +
"Received error should be due to closed connection: " +
CommonUtils.exceptionStr(ex));
throw ex;
}
this._log.warn("Error during transaction. Rolling back: " +
CommonUtils.exceptionStr(ex));
try {
yield this.execute("ROLLBACK TRANSACTION");
} catch (inner) {
this._log.warn("Could not roll back transaction. This is weird: " +
CommonUtils.exceptionStr(inner));
}
throw ex;
}
// See comment above about connection being closed during transaction.
if (!this._inProgressTransaction) {
this._log.warn("Connection was closed while performing transaction. " +
"Unable to commit.");
throw new Error("Connection closed before transaction committed.");
}
try {
yield this.execute("COMMIT TRANSACTION");
} catch (ex) {
this._log.warn("Error committing transaction: " +
CommonUtils.exceptionStr(ex));
throw ex;
}
throw new Task.Result(result);
}.bind(this)).then(
function onSuccess(result) {
this._inProgressTransaction = null;
deferred.resolve(result);
}.bind(this),
function onError(error) {
this._inProgressTransaction = null;
deferred.reject(error);
}.bind(this)
);
return deferred.promise;
},
/**
* Whether a table exists in the database.
*
* IMPROVEMENT: Look for temporary tables.
*
* @param name
* (string) Name of the table.
*
* @return Promise<bool>
*/
tableExists: function (name) {
return this.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name=?",
[name])
.then(function onResult(rows) {
return Promise.resolve(rows.length > 0);
}
);
},
/**
* Whether a named index exists.
*
* IMPROVEMENT: Look for indexes in temporary tables.
*
* @param name
* (string) Name of the index.
*
* @return Promise<bool>
*/
indexExists: function (name) {
return this.execute(
"SELECT name FROM sqlite_master WHERE type='index' AND name=?",
[name])
.then(function onResult(rows) {
return Promise.resolve(rows.length > 0);
}
);
},
/**
* Free up as much memory from the underlying database connection as possible.
*
* @return Promise<>
*/
shrinkMemory: function () {
this._log.info("Shrinking memory usage.");
let onShrunk = this._clearIdleShrinkTimer.bind(this);
return this.execute("PRAGMA shrink_memory").then(onShrunk, onShrunk);
},
/**
* Discard all cached statements.
*
* Note that this relies on us being non-interruptible between
* the insertion or retrieval of a statement in the cache and its
* execution: we finalize all statements, which is only safe if
* they will not be executed again.
*
* @return (integer) the number of statements discarded.
*/
discardCachedStatements: function () {
let count = 0;
for (let [k, statement] of this._cachedStatements) {
++count;
statement.finalize();
}
this._cachedStatements.clear();
this._log.debug("Discarded " + count + " cached statements.");
return count;
},
/**
* Helper method to bind parameters of various kinds through
* reflection.
*/
_bindParameters: function (statement, params) {
if (!params) {
return;
}
if (Array.isArray(params)) {
// It's an array of separate params.
if (params.length && (typeof(params[0]) == "object")) {
let paramsArray = statement.newBindingParamsArray();
for (let p of params) {
let bindings = paramsArray.newBindingParams();
for (let [key, value] of Iterator(p)) {
bindings.bindByName(key, value);
}
paramsArray.addParams(bindings);
}
statement.bindParameters(paramsArray);
return;
}
// Indexed params.
for (let i = 0; i < params.length; i++) {
statement.bindByIndex(i, params[i]);
}
return;
}
// Named params.
if (params && typeof(params) == "object") {
for (let k in params) {
statement.bindByName(k, params[k]);
}
return;
}
throw new Error("Invalid type for bound parameters. Expected Array or " +
"object. Got: " + params);
},
_executeStatement: function (sql, statement, params, onRow) {
if (statement.state != statement.MOZ_STORAGE_STATEMENT_READY) {
throw new Error("Statement is not ready for execution.");
}
if (onRow && typeof(onRow) != "function") {
throw new Error("onRow must be a function. Got: " + onRow);
}
this._bindParameters(statement, params);
let index = this._statementCounter++;
let deferred = Promise.defer();
let userCancelled = false;
let errors = [];
let rows = [];
// Don't incur overhead for serializing params unless the messages go
// somewhere.
if (this._log.level <= Log4Moz.Level.Trace) {
let msg = "Stmt #" + index + " " + sql;
if (params) {
msg += " - " + JSON.stringify(params);
}
this._log.trace(msg);
} else {
this._log.debug("Stmt #" + index + " starting");
}
let self = this;
let pending = statement.executeAsync({
handleResult: function (resultSet) {
// .cancel() may not be immediate and handleResult() could be called
// after a .cancel().
for (let row = resultSet.getNextRow(); row && !userCancelled; row = resultSet.getNextRow()) {
if (!onRow) {
rows.push(row);
continue;
}
try {
onRow(row);
} catch (e if e instanceof StopIteration) {
userCancelled = true;
pending.cancel();
break;
} catch (ex) {
self._log.warn("Exception when calling onRow callback: " +
CommonUtils.exceptionStr(ex));
}
}
},
handleError: function (error) {
self._log.info("Error when executing SQL (" + error.result + "): " +
error.message);
errors.push(error);
},
handleCompletion: function (reason) {
self._log.debug("Stmt #" + index + " finished.");
self._pendingStatements.delete(index);
switch (reason) {
case Ci.mozIStorageStatementCallback.REASON_FINISHED:
// If there is an onRow handler, we always resolve to null.
let result = onRow ? null : rows;
deferred.resolve(result);
break;
case Ci.mozIStorageStatementCallback.REASON_CANCELLED:
// It is not an error if the user explicitly requested cancel via
// the onRow handler.
if (userCancelled) {
let result = onRow ? null : rows;
deferred.resolve(result);
} else {
deferred.reject(new Error("Statement was cancelled."));
}
break;
case Ci.mozIStorageStatementCallback.REASON_ERROR:
let error = new Error("Error(s) encountered during statement execution.");
error.errors = errors;
deferred.reject(error);
break;
default:
deferred.reject(new Error("Unknown completion reason code: " +
reason));
break;
}
},
});
this._pendingStatements.set(index, pending);
return deferred.promise;
},
_ensureOpen: function () {
if (!this._open) {
throw new Error("Connection is not open.");
}
},
_clearIdleShrinkTimer: function () {
if (!this._idleShrinkTimer) {
return;
}
this._idleShrinkTimer.cancel();
},
_startIdleShrinkTimer: function () {
if (!this._idleShrinkTimer) {
return;
}
this._idleShrinkTimer.initWithCallback(this.shrinkMemory.bind(this),
this._idleShrinkMS,
this._idleShrinkTimer.TYPE_ONE_SHOT);
},
});
this.Sqlite = {
openConnection: openConnection,
};

View file

@ -1,700 +0,0 @@
// This is Mozilla code from Firefox 24.
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ['Log4Moz'];
const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
const ONE_BYTE = 1;
const ONE_KILOBYTE = 1024 * ONE_BYTE;
const ONE_MEGABYTE = 1024 * ONE_KILOBYTE;
const STREAM_SEGMENT_SIZE = 4096;
const PR_UINT32_MAX = 0xffffffff;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
this.Log4Moz = {
Level: {
Fatal: 70,
Error: 60,
Warn: 50,
Info: 40,
Config: 30,
Debug: 20,
Trace: 10,
All: 0,
Desc: {
70: "FATAL",
60: "ERROR",
50: "WARN",
40: "INFO",
30: "CONFIG",
20: "DEBUG",
10: "TRACE",
0: "ALL"
},
Numbers: {
"FATAL": 70,
"ERROR": 60,
"WARN": 50,
"INFO": 40,
"CONFIG": 30,
"DEBUG": 20,
"TRACE": 10,
"ALL": 0,
}
},
get repository() {
delete Log4Moz.repository;
Log4Moz.repository = new LoggerRepository();
return Log4Moz.repository;
},
set repository(value) {
delete Log4Moz.repository;
Log4Moz.repository = value;
},
LogMessage: LogMessage,
Logger: Logger,
LoggerRepository: LoggerRepository,
Formatter: Formatter,
BasicFormatter: BasicFormatter,
StructuredFormatter: StructuredFormatter,
Appender: Appender,
DumpAppender: DumpAppender,
ConsoleAppender: ConsoleAppender,
StorageStreamAppender: StorageStreamAppender,
FileAppender: FileAppender,
BoundedFileAppender: BoundedFileAppender,
// Logging helper:
// let logger = Log4Moz.repository.getLogger("foo");
// logger.info(Log4Moz.enumerateInterfaces(someObject).join(","));
enumerateInterfaces: function Log4Moz_enumerateInterfaces(aObject) {
let interfaces = [];
for (i in Ci) {
try {
aObject.QueryInterface(Ci[i]);
interfaces.push(i);
}
catch(ex) {}
}
return interfaces;
},
// Logging helper:
// let logger = Log4Moz.repository.getLogger("foo");
// logger.info(Log4Moz.enumerateProperties(someObject).join(","));
enumerateProperties: function Log4Moz_enumerateProps(aObject,
aExcludeComplexTypes) {
let properties = [];
for (p in aObject) {
try {
if (aExcludeComplexTypes &&
(typeof aObject[p] == "object" || typeof aObject[p] == "function"))
continue;
properties.push(p + " = " + aObject[p]);
}
catch(ex) {
properties.push(p + " = " + ex);
}
}
return properties;
}
};
/*
* LogMessage
* Encapsulates a single log event's data
*/
function LogMessage(loggerName, level, message, params) {
this.loggerName = loggerName;
this.level = level;
this.message = message;
this.params = params;
// The _structured field will correspond to whether this message is to
// be interpreted as a structured message.
this._structured = this.params && this.params.action;
this.time = Date.now();
}
LogMessage.prototype = {
get levelDesc() {
if (this.level in Log4Moz.Level.Desc)
return Log4Moz.Level.Desc[this.level];
return "UNKNOWN";
},
toString: function LogMsg_toString(){
let msg = "LogMessage [" + this.time + " " + this.level + " " +
this.message;
if (this.params) {
msg += " " + JSON.stringify(this.params);
}
return msg + "]"
}
};
/*
* Logger
* Hierarchical version. Logs to all appenders, assigned or inherited
*/
function Logger(name, repository) {
if (!repository)
repository = Log4Moz.repository;
this._name = name;
this.children = [];
this.ownAppenders = [];
this.appenders = [];
this._repository = repository;
}
Logger.prototype = {
get name() {
return this._name;
},
_level: null,
get level() {
if (this._level != null)
return this._level;
if (this.parent)
return this.parent.level;
dump("log4moz warning: root logger configuration error: no level defined\n");
return Log4Moz.Level.All;
},
set level(level) {
this._level = level;
},
_parent: null,
get parent() this._parent,
set parent(parent) {
if (this._parent == parent) {
return;
}
// Remove ourselves from parent's children
if (this._parent) {
let index = this._parent.children.indexOf(this);
if (index != -1) {
this._parent.children.splice(index, 1);
}
}
this._parent = parent;
parent.children.push(this);
this.updateAppenders();
},
updateAppenders: function updateAppenders() {
if (this._parent) {
let notOwnAppenders = this._parent.appenders.filter(function(appender) {
return this.ownAppenders.indexOf(appender) == -1;
}, this);
this.appenders = notOwnAppenders.concat(this.ownAppenders);
} else {
this.appenders = this.ownAppenders.slice();
}
// Update children's appenders.
for (let i = 0; i < this.children.length; i++) {
this.children[i].updateAppenders();
}
},
addAppender: function Logger_addAppender(appender) {
if (this.ownAppenders.indexOf(appender) != -1) {
return;
}
this.ownAppenders.push(appender);
this.updateAppenders();
},
removeAppender: function Logger_removeAppender(appender) {
let index = this.ownAppenders.indexOf(appender);
if (index == -1) {
return;
}
this.ownAppenders.splice(index, 1);
this.updateAppenders();
},
/**
* Logs a structured message object.
*
* @param action
* (string) A message action, one of a set of actions known to the
* log consumer.
* @param params
* (object) Parameters to be included in the message.
* If _level is included as a key and the corresponding value
* is a number or known level name, the message will be logged
* at the indicated level.
*/
logStructured: function (action, params) {
if (!action) {
throw "An action is required when logging a structured message.";
}
if (!params) {
return this.log(this.level, undefined, {"action": action});
}
if (typeof params != "object") {
throw "The params argument is required to be an object.";
}
let level = params._level || this.level;
if ((typeof level == "string") && level in Log4Moz.Level.Numbers) {
level = Log4Moz.Level.Numbers[level];
}
params.action = action;
this.log(level, params._message, params);
},
log: function (level, string, params) {
if (this.level > level)
return;
// Hold off on creating the message object until we actually have
// an appender that's responsible.
let message;
let appenders = this.appenders;
for (let appender of appenders) {
if (appender.level > level) {
continue;
}
if (!message) {
message = new LogMessage(this._name, level, string, params);
}
appender.append(message);
}
},
fatal: function (string, params) {
this.log(Log4Moz.Level.Fatal, string, params);
},
error: function (string, params) {
this.log(Log4Moz.Level.Error, string, params);
},
warn: function (string, params) {
this.log(Log4Moz.Level.Warn, string, params);
},
info: function (string, params) {
this.log(Log4Moz.Level.Info, string, params);
},
config: function (string, params) {
this.log(Log4Moz.Level.Config, string, params);
},
debug: function (string, params) {
this.log(Log4Moz.Level.Debug, string, params);
},
trace: function (string, params) {
this.log(Log4Moz.Level.Trace, string, params);
}
};
/*
* LoggerRepository
* Implements a hierarchy of Loggers
*/
function LoggerRepository() {}
LoggerRepository.prototype = {
_loggers: {},
_rootLogger: null,
get rootLogger() {
if (!this._rootLogger) {
this._rootLogger = new Logger("root", this);
this._rootLogger.level = Log4Moz.Level.All;
}
return this._rootLogger;
},
set rootLogger(logger) {
throw "Cannot change the root logger";
},
_updateParents: function LogRep__updateParents(name) {
let pieces = name.split('.');
let cur, parent;
// find the closest parent
// don't test for the logger name itself, as there's a chance it's already
// there in this._loggers
for (let i = 0; i < pieces.length - 1; i++) {
if (cur)
cur += '.' + pieces[i];
else
cur = pieces[i];
if (cur in this._loggers)
parent = cur;
}
// if we didn't assign a parent above, there is no parent
if (!parent)
this._loggers[name].parent = this.rootLogger;
else
this._loggers[name].parent = this._loggers[parent];
// trigger updates for any possible descendants of this logger
for (let logger in this._loggers) {
if (logger != name && logger.indexOf(name) == 0)
this._updateParents(logger);
}
},
getLogger: function LogRep_getLogger(name) {
if (name in this._loggers)
return this._loggers[name];
this._loggers[name] = new Logger(name, this);
this._updateParents(name);
return this._loggers[name];
}
};
/*
* Formatters
* These massage a LogMessage into whatever output is desired.
* BasicFormatter and StructuredFormatter are implemented here.
*/
// Abstract formatter
function Formatter() {}
Formatter.prototype = {
format: function Formatter_format(message) {}
};
// Basic formatter that doesn't do anything fancy.
function BasicFormatter(dateFormat) {
if (dateFormat)
this.dateFormat = dateFormat;
}
BasicFormatter.prototype = {
__proto__: Formatter.prototype,
format: function BF_format(message) {
return message.time + "\t" +
message.loggerName + "\t" +
message.levelDesc + "\t" +
message.message + "\n";
}
};
// Structured formatter that outputs JSON based on message data.
// This formatter will format unstructured messages by supplying
// default values.
function StructuredFormatter() { }
StructuredFormatter.prototype = {
__proto__: Formatter.prototype,
format: function (logMessage) {
let output = {
_time: logMessage.time,
_namespace: logMessage.loggerName,
_level: logMessage.levelDesc
};
for (let key in logMessage.params) {
output[key] = logMessage.params[key];
}
if (!output.action) {
output.action = "UNKNOWN";
}
if (!output._message && logMessage.message) {
output._message = logMessage.message;
}
return JSON.stringify(output);
}
}
/*
* Appenders
* These can be attached to Loggers to log to different places
* Simply subclass and override doAppend to implement a new one
*/
function Appender(formatter) {
this._name = "Appender";
this._formatter = formatter? formatter : new BasicFormatter();
}
Appender.prototype = {
level: Log4Moz.Level.All,
append: function App_append(message) {
if (message) {
this.doAppend(this._formatter.format(message));
}
},
toString: function App_toString() {
return this._name + " [level=" + this._level +
", formatter=" + this._formatter + "]";
},
doAppend: function App_doAppend(message) {}
};
/*
* DumpAppender
* Logs to standard out
*/
function DumpAppender(formatter) {
this._name = "DumpAppender";
Appender.call(this, formatter);
}
DumpAppender.prototype = {
__proto__: Appender.prototype,
doAppend: function DApp_doAppend(message) {
dump(message);
}
};
/*
* ConsoleAppender
* Logs to the javascript console
*/
function ConsoleAppender(formatter) {
this._name = "ConsoleAppender";
Appender.call(this, formatter);
}
ConsoleAppender.prototype = {
__proto__: Appender.prototype,
doAppend: function CApp_doAppend(message) {
if (message.level > Log4Moz.Level.Warn) {
Cu.reportError(message);
return;
}
Cc["@mozilla.org/consoleservice;1"].
getService(Ci.nsIConsoleService).logStringMessage(message);
}
};
/**
* Append to an nsIStorageStream
*
* This writes logging output to an in-memory stream which can later be read
* back as an nsIInputStream. It can be used to avoid expensive I/O operations
* during logging. Instead, one can periodically consume the input stream and
* e.g. write it to disk asynchronously.
*/
function StorageStreamAppender(formatter) {
this._name = "StorageStreamAppender";
Appender.call(this, formatter);
}
StorageStreamAppender.prototype = {
__proto__: Appender.prototype,
_converterStream: null, // holds the nsIConverterOutputStream
_outputStream: null, // holds the underlying nsIOutputStream
_ss: null,
get outputStream() {
if (!this._outputStream) {
// First create a raw stream. We can bail out early if that fails.
this._outputStream = this.newOutputStream();
if (!this._outputStream) {
return null;
}
// Wrap the raw stream in an nsIConverterOutputStream. We can reuse
// the instance if we already have one.
if (!this._converterStream) {
this._converterStream = Cc["@mozilla.org/intl/converter-output-stream;1"]
.createInstance(Ci.nsIConverterOutputStream);
}
this._converterStream.init(
this._outputStream, "UTF-8", STREAM_SEGMENT_SIZE,
Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
}
return this._converterStream;
},
newOutputStream: function newOutputStream() {
let ss = this._ss = Cc["@mozilla.org/storagestream;1"]
.createInstance(Ci.nsIStorageStream);
ss.init(STREAM_SEGMENT_SIZE, PR_UINT32_MAX, null);
return ss.getOutputStream(0);
},
getInputStream: function getInputStream() {
if (!this._ss) {
return null;
}
return this._ss.newInputStream(0);
},
reset: function reset() {
if (!this._outputStream) {
return;
}
this.outputStream.close();
this._outputStream = null;
this._ss = null;
},
doAppend: function (message) {
if (!message) {
return;
}
try {
this.outputStream.writeString(message);
} catch(ex) {
if (ex.result == Cr.NS_BASE_STREAM_CLOSED) {
// The underlying output stream is closed, so let's open a new one
// and try again.
this._outputStream = null;
} try {
this.outputStream.writeString(message);
} catch (ex) {
// Ah well, we tried, but something seems to be hosed permanently.
}
}
}
};
/**
* File appender
*
* Writes output to file using OS.File.
*/
function FileAppender(path, formatter) {
this._name = "FileAppender";
this._encoder = new TextEncoder();
this._path = path;
this._file = null;
this._fileReadyPromise = null;
// This is a promise exposed for testing/debugging the logger itself.
this._lastWritePromise = null;
Appender.call(this, formatter);
}
FileAppender.prototype = {
__proto__: Appender.prototype,
_openFile: function () {
return Task.spawn(function _openFile() {
try {
this._file = yield OS.File.open(this._path,
{truncate: true});
} catch (err) {
if (err instanceof OS.File.Error) {
this._file = null;
} else {
throw err;
}
}
}.bind(this));
},
_getFile: function() {
if (!this._fileReadyPromise) {
this._fileReadyPromise = this._openFile();
return this._fileReadyPromise;
}
return this._fileReadyPromise.then(_ => {
if (!this._file) {
return this._openFile();
}
});
},
doAppend: function (message) {
let array = this._encoder.encode(message);
if (this._file) {
this._lastWritePromise = this._file.write(array);
} else {
this._lastWritePromise = this._getFile().then(_ => {
this._fileReadyPromise = null;
if (this._file) {
return this._file.write(array);
}
});
}
},
reset: function () {
let fileClosePromise = this._file.close();
return fileClosePromise.then(_ => {
this._file = null;
return OS.File.remove(this._path);
});
}
};
/**
* Bounded File appender
*
* Writes output to file using OS.File. After the total message size
* (as defined by message.length) exceeds maxSize, existing messages
* will be discarded, and subsequent writes will be appended to a new log file.
*/
function BoundedFileAppender(path, formatter, maxSize=2*ONE_MEGABYTE) {
this._name = "BoundedFileAppender";
this._size = 0;
this._maxSize = maxSize;
this._closeFilePromise = null;
FileAppender.call(this, path, formatter);
}
BoundedFileAppender.prototype = {
__proto__: FileAppender.prototype,
doAppend: function (message) {
if (!this._removeFilePromise) {
if (this._size < this._maxSize) {
this._size += message.length;
return FileAppender.prototype.doAppend.call(this, message);
}
this._removeFilePromise = this.reset();
}
this._removeFilePromise.then(_ => {
this._removeFilePromise = null;
this.doAppend(message);
});
},
reset: function () {
let fileClosePromise;
if (this._fileReadyPromise) {
// An attempt to open the file may still be in progress.
fileClosePromise = this._fileReadyPromise.then(_ => {
return this._file.close();
});
} else {
fileClosePromise = this._file.close();
}
return fileClosePromise.then(_ => {
this._size = 0;
this._file = null;
return OS.File.remove(this._path);
});
}
};