Adds WebDAV file sync

- Still experimental and incomplete, with no lock support and not much error handling

Also:

- New expiry date for sync functions
- Attachment character set was being dropped during syncing
- Possibly improves sizing issues with preferences window
- Fixes problems with attachment filenames with extended characters
- Fixes some problem with tags that I don't remember
- Makes XMLHTTPRequest calls are now background requests (no auth windows or other prompts)
- Z.U.HTTP.doOptions() now takes an nsIURI instead of a URL spec
- New methods:
  - Zotero.Utilities.rand(min, max)
  - Zotero.Utilities.probability(x)
  - Zotero.Utilities.Base64.encode(str) and decode(str)
  - Zotero.getTempDirectory()
  - Zotero.Date.dateToISO(date) - convert JS Date object to ISO 8601 UTC date/time
  - Zotero.Date.isoToDate(isoDate) - convert an ISO 8601 UTC date/time to a JS Date object
This commit is contained in:
Dan Stillman 2008-08-31 23:36:01 +00:00
parent 227b4cbfcd
commit a8bb8dae40
19 changed files with 3516 additions and 246 deletions

View file

@ -133,6 +133,10 @@
<menuitem label="Clear Server Data" oncommand="Zotero.Sync.Server.clear()"/>
<menuitem label="Reset Server Lock" oncommand="Zotero.Sync.Server.resetServer()"/>
<menuitem label="Reset Client" oncommand="Zotero.Sync.Server.resetClient()"/>
<menuseparator id="zotero-tb-actions-storage-separator"/>
<menuitem label="Reset Storage History" oncommand="Zotero.Sync.Storage.resetAllSyncStates()"/>
<menuitem label="Purge Deleted Storage Files" oncommand="Zotero.Sync.Storage.purgeDeletedStorageFiles(function(results) { Zotero.debug(results); })"/>
<menuitem label="Purge Orphaned Storage Files" oncommand="Zotero.Sync.Storage.purgeOrphanedStorageFiles(function(results) { Zotero.debug(results); })"/>
<menuseparator id="zotero-tb-actions-separator"/>
<menuitem id="zotero-tb-actions-prefs" label="&zotero.toolbar.preferences.label;"
oncommand="window.openDialog('chrome://zotero/content/preferences/preferences.xul', 'zotero-prefs', 'chrome,titlebar,toolbar,' + Zotero.Prefs.get('browser.preferences.instantApply', true) ? 'dialog=no' : 'modal')"/>
@ -305,16 +309,49 @@
<splitter id="zotero-view-splitter" resizebefore="closest" resizeafter="closest"/>
<vbox id="zotero-item-pane" persist="width">
<toolbar align="right">
<toolbar align="center" pack="end">
<progressmeter id="zotero-tb-syncProgress" mode="determined"
value="0" tooltip="zotero-tb-syncProgress-tooltip"
hidden="true">
</progressmeter>
<tooltip id="zotero-tb-syncProgress-tooltip" noautohide="true">
<grid>
<columns>
<column/>
<column/>
</columns>
<rows>
<row>
<label value="&zotero.sync.storage.progress;"/>
<label id="zotero-tb-syncProgress-tooltip-progress"/>
</row>
<row>
<label value="&zotero.sync.storage.downloads;"/>
<label
id="zotero-tb-syncProgress-tooltip-downloads"/>
</row>
<row>
<label value="&zotero.sync.storage.uploads;"/>
<label
id="zotero-tb-syncProgress-tooltip-uploads"/>
</row>
</rows>
</grid>
</tooltip>
<toolbarbutton id="zotero-tb-sync" tooltip="_child"
oncommand="Zotero.Sync.Server.sync()">
oncommand="Zotero.Sync.Runner.sync()">
<tooltip
onpopupshowing="if (Zotero.Sync.Server.lastSyncError) { this.firstChild.nextSibling.value = 'Last error: ' + Zotero.Sync.Server.lastSyncError; return; } this.firstChild.nextSibling.value = 'Last sync: ' + (Zotero.Sync.Server.lastLocalSyncTime ? new Date(Zotero.Sync.Server.lastLocalSyncTime * 1000).toLocaleString() : 'Not yet synced')"
noautohide="true"><!-- localize -->
noautohide="true"><!-- TODO: localize -->
<label value="Sync with Zotero Server"/>
<label id="zotero-last-sync-time"/>
</tooltip>
</toolbarbutton>
<!--
<toolbarbutton id="zotero-tb-storage-sync"
tooltiptext="Sync with Storage Server"
oncommand="Zotero.Sync.Storage.sync()"/>
-->
<toolbarseparator/>
<toolbarbutton id="zotero-tb-fullscreen" tooltiptext="&zotero.toolbar.fullscreen.tooltip;" oncommand="ZoteroPane.fullScreen();"/>
<toolbarbutton class="tabs-closebutton" oncommand="ZoteroPane.toggleDisplay()"/>

View file

@ -137,6 +137,170 @@ function populateOpenURLResolvers() {
}
//
// Sync
//
function unverifyStorageServer() {
Zotero.debug("Clearing storage settings");
Zotero.Sync.Storage.clearSettingsCache();
Zotero.Prefs.set('sync.storage.verified', false);
}
function verifyStorageServer() {
Zotero.debug("Verifying storage");
var verifyButton = document.getElementById("storage-verify");
var abortButton = document.getElementById("storage-abort");
var progressMeter = document.getElementById("storage-progress");
var callback = function (uri, status, authRequired) {
verifyButton.hidden = false;
abortButton.hidden = true;
progressMeter.hidden = true;
var promptService =
Components.classes["@mozilla.org/network/default-prompt;1"].
createInstance(Components.interfaces.nsIPrompt);
if (uri) {
var spec = uri.scheme + '://' + uri.hostPort + uri.path;
}
switch (status) {
case Zotero.Sync.Storage.SUCCESS:
promptService.alert(
"Server configuration verified",
"File storage is successfully set up."
);
Zotero.Prefs.set("sync.storage.verified", true);
return true;
case Zotero.Sync.Storage.ERROR_NO_URL:
var errorMessage = "Please enter a URL.";
setTimeout(function () {
document.getElementById("storage-url").focus();
}, 1);
break;
case Zotero.Sync.Storage.ERROR_NO_USERNAME:
var errorMessage = "Please enter a username.";
setTimeout(function () {
document.getElementById("storage-username").focus();
}, 1);
break;
case Zotero.Sync.Storage.ERROR_NO_PASSWORD:
var errorMessage = "Please enter a password.";
setTimeout(function () {
document.getElementById("storage-password").focus();
}, 1);
break;
case Zotero.Sync.Storage.ERROR_UNREACHABLE:
var errorMessage = "The server " + uri.host + " could not be reached.";
break;
case Zotero.Sync.Storage.ERROR_NOT_DAV:
var errorMessage = spec + " is not a valid WebDAV URL.";
break;
case Zotero.Sync.Storage.ERROR_AUTH_FAILED:
var errorTitle = "Permission denied";
var errorMessage = "The server did not accept the username and "
+ "password you entered." + " "
+ "Please check your server settings "
+ "or contact your server administrator.";
break;
case Zotero.Sync.Storage.ERROR_FORBIDDEN:
var errorTitle = "Permission denied";
var errorMessage = "You don't have permission to access "
+ uri.path + " on this server." + " "
+ "Please check your server settings "
+ "or contact your server administrator.";
break;
case Zotero.Sync.Storage.ERROR_PARENT_DIR_NOT_FOUND:
var errorTitle = "Directory not found";
var parentSpec = spec.replace(/\/zotero\/$/, "");
var errorMessage = parentSpec + " does not exist.";
break;
case Zotero.Sync.Storage.ERROR_ZOTERO_DIR_NOT_FOUND:
var create = promptService.confirmEx(
// TODO: localize
"Directory not found",
spec + " does not exist.\n\nDo you want to create it now?",
promptService.BUTTON_POS_0
* promptService.BUTTON_TITLE_IS_STRING
+ promptService.BUTTON_POS_1
* promptService.BUTTON_TITLE_CANCEL,
"Create",
null, null, null, {}
);
if (create != 0) {
return;
}
Zotero.Sync.Storage.createServerDirectory(function (uri, status) {
switch (status) {
case Zotero.Sync.Storage.SUCCESS:
promptService.alert(
"Server configuration verified",
"File storage is successfully set up."
);
Zotero.Prefs.set("sync.storage.verified", true);
return true;
case Zotero.Sync.Storage.ERROR_FORBIDDEN:
var errorTitle = "Permission denied";
var errorMessage = "You do not have "
+ "permission to create a Zotero directory "
+ "at the following address:" + "\n\n" + spec;
errorMessage += "\n\n"
+ "Please check your server settings or "
+ "contact your server administrator.";
break;
}
// TEMP
if (!errorMessage) {
var errorMessage = status;
}
promptService.alert(errorTitle, errorMessage);
});
return false;
}
if (!errorTitle) {
var errorTitle = Zotero.getString("general.error");
}
// TEMP
if (!errorMessage) {
var errorMessage = status;
}
promptService.alert(errorTitle, errorMessage);
return false;
}
verifyButton.hidden = true;
abortButton.hidden = false;
progressMeter.hidden = false;
var requestHolder = Zotero.Sync.Storage.checkServer(callback);
abortButton.onclick = function () {
if (requestHolder.request) {
requestHolder.request.onreadystatechange = undefined;
requestHolder.request.abort();
verifyButton.hidden = false;
abortButton.hidden = true;
progressMeter.hidden = true;
}
}
}
/*
* Builds the main Quick Copy drop-down from the current global pref
*/

View file

@ -156,16 +156,23 @@ To add a new preference:
<!-- localize -->
<prefpane id="zotero-prefpane-sync"
label="Sync"
onpaneload="document.getElementById('sync-password').value = Zotero.Sync.Server.password;"
onpaneload="document.getElementById('sync-password').value = Zotero.Sync.Server.password; document.getElementById('storage-password').value = Zotero.Sync.Storage.password;"
image="chrome://zotero/skin/prefs-sync.png">
<preferences>
<preference id="pref-sync-username" name="extensions.zotero.sync.server.username" type="string"/>
<preference id="pref-sync-autosync" name="extensions.zotero.sync.autoSync" type="bool"/>
<preference id="pref-sync-username" name="extensions.zotero.sync.server.username" type="string" instantApply="true"/>
<preference id="pref-storage-enabled" name="extensions.zotero.sync.storage.enabled" type="bool"/>
<preference id="pref-storage-url" name="extensions.zotero.sync.storage.url" type="string" instantApply="true"/>
<preference id="pref-storage-username" name="extensions.zotero.sync.storage.username" type="string" instantApply="true"/>
</preferences>
<groupbox>
<caption label="Zotero Sync Server"/>
<grid>
<columns>
<columns/>
<columns/>
<column/>
<column/>
</columns>
<rows>
@ -179,8 +186,86 @@ To add a new preference:
<textbox id="sync-password" type="password"
onchange="Zotero.Sync.Server.password = this.value"/>
</row>
<!--
<row>
<box/>
<hbox>
<button label="Verify login"
oncommand="alert('Unimplemented')"/>
</hbox>
</row>
-->
</rows>
</grid>
<separator class="thin"/>
<hbox>
<checkbox label="Sync automatically" preference="pref-sync-autosync"/>
</hbox>
</groupbox>
<groupbox>
<caption label="Storage Server"/>
<hbox>
<checkbox label="Enable file syncing" preference="pref-storage-enabled"/>
</hbox>
<separator class="thin"/>
<grid id="storage-settings">
<columns>
<column/>
<column flex="1"/>
</columns>
<rows>
<row>
<label value="URL:"/>
<hbox>
<label value="https://"/>
<textbox id="storage-url" flex="1"
preference="pref-storage-url"
onkeypress="if (Zotero.isMac &amp;&amp; event.keyCode == 13) { this.blur(); verifyStorageServer(); }"
onsynctopreference="unverifyStorageServer();"
onchange="this.value = this.value.replace(/(^https?:\/\/|\/zotero\/?$|\/$)/g, '')"/>
<label value="/zotero/"/>
</hbox>
</row>
<row>
<label value="Username:"/>
<hbox>
<textbox id="storage-username"
preference="pref-storage-username"
onkeypress="if (Zotero.isMac &amp;&amp; event.keyCode == 13) { this.blur(); setTimeout('verifyStorageServer();', 1); }"
onsynctopreference="unverifyStorageServer();"
onchange="var pass = document.getElementById('storage-password'); if (pass.value) { Zotero.Sync.Storage.password = pass.value; }"/>
</hbox>
</row>
<row>
<label value="Password:"/>
<hbox>
<textbox id="storage-password" flex="0" type="password"
onkeypress="if (Zotero.isMac &amp;&amp; event.keyCode == 13) { this.blur(); setTimeout('verifyStorageServer();', 1); }"
oninput="unverifyStorageServer()"
onchange="Zotero.Sync.Storage.password = this.value"/>
</hbox>
</row>
<row>
<box/>
<hbox>
<button id="storage-verify" label="Verify Server"
oncommand="verifyStorageServer()"/>
<button id="storage-abort" label="Stop" hidden="true"/>
<progressmeter id="storage-progress" hidden="true"
mode="undetermined"/>
</hbox>
</row>
</rows>
</grid>
</groupbox>
</prefpane>
@ -282,8 +367,12 @@ To add a new preference:
<groupbox>
<caption label="&zotero.preferences.citationOptions.caption;"/>
<checkbox label="&zotero.preferences.export.citePaperJournalArticleURL;" preference="pref-export-citePaperJournalArticleURL"/>
<label id="export-citePaperJournalArticleURL">&zotero.preferences.export.citePaperJournalArticleURL.description;</label>
<!-- This doesn't wrap without an explicit wrap, for some reason -->
<label id="export-citePaperJournalArticleURL" width="45em">
&zotero.preferences.export.citePaperJournalArticleURL.description;
</label>
</groupbox>
<groupbox>
@ -511,8 +600,7 @@ To add a new preference:
</groupbox>
</prefpane>
<!-- These mess up the prefwindow if they come before the prefpanes
<!-- These mess up the prefwindow (more) if they come before the prefpanes
https://bugzilla.mozilla.org/show_bug.cgi?id=296418 -->
<script src="chrome://zotero/content/include.js"/>
<script src="preferences.js"/>

View file

@ -943,13 +943,62 @@ Zotero.Attachments = new function(){
function getPath(file, linkMode) {
if (linkMode == self.LINK_MODE_IMPORTED_URL ||
linkMode == self.LINK_MODE_IMPORTED_FILE) {
return 'storage:' + file.leafName;
file.QueryInterface(Components.interfaces.nsILocalFile);
var fileName = file.getRelativeDescriptor(file.parent);
return 'storage:' + fileName;
}
return file.persistentDescriptor;
}
/**
* @param {Zotero.Item} item
* @param {Boolean} [skipHidden=FALSE] Don't count hidden files
* @return {Integer} Total file size in bytes
*/
this.getTotalFileSize = function (item, skipHidden) {
var funcName = "Zotero.Attachments.getTotalFileSize()";
if (!item.isAttachment()) {
throw ("Item is not an attachment in " + funcName);
}
var linkMode = item.attachmentLinkMode;
switch (linkMode) {
case Zotero.Attachments.LINK_MODE_IMPORTED_URL:
case Zotero.Attachments.LINK_MODE_IMPORTED_FILE:
case Zotero.Attachments.LINK_MODE_LINKED_FILE:
break;
default:
throw ("Invalid attachment link mode in " + funcName);
}
var file = item.getFile();
if (!file) {
throw ("File not found in " + funcName);
}
if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
return item.fileSize;
}
var parentDir = file.parent;
var files = parentDir.directoryEntries;
var size = 0;
while (files.hasMoreElements()) {
file = files.getNext();
file.QueryInterface(Components.interfaces.nsIFile);
if (skipHidden && file.leafName.indexOf('.') == 0) {
continue;
}
size += file.fileSize;
}
return size;
}
function _getFileNameFromURL(url, mimeType){
var nsIURL = Components.classes["@mozilla.org/network/standard-url;1"]
.createInstance(Components.interfaces.nsIURL);

View file

@ -87,8 +87,9 @@ Zotero.Item.prototype._init = function () {
this._attachmentLinkMode = null;
this._attachmentMIMEType = null;
this._attachmentCharset = null;
this._attachmentCharset;
this._attachmentPath = null;
this._attachmentSyncState;
this._relatedItems = false;
}
@ -1254,22 +1255,13 @@ Zotero.Item.prototype.save = function() {
// Attachment
if (this.isAttachment()) {
var sql = "INSERT INTO itemAttachments (itemID, sourceItemID, linkMode, "
+ "mimeType, charsetID, path) VALUES (?,?,?,?,?,?)";
+ "mimeType, charsetID, path, syncState) VALUES (?,?,?,?,?,?,?)";
var parent = this.getSource();
var linkMode = this.attachmentLinkMode;
switch (linkMode) {
case Zotero.Attachments.LINK_MODE_IMPORTED_FILE:
case Zotero.Attachments.LINK_MODE_IMPORTED_URL:
case Zotero.Attachments.LINK_MODE_LINKED_FILE:
case Zotero.Attachments.LINK_MODE_LINKED_URL:
break;
default:
throw ("Invalid attachment link mode " + linkMode + " in Zotero.Item.save()");
}
var mimeType = this.attachmentMIMEType;
var charsetID = this.attachmentCharset;
var path = this.attachmentPath;
var syncState = this.attachmentSyncState;
var bindParams = [
itemID,
@ -1277,7 +1269,8 @@ Zotero.Item.prototype.save = function() {
{ int: linkMode },
mimeType ? { string: mimeType } : null,
charsetID ? { int: charsetID } : null,
path ? { string: path } : null
path ? { string: path } : null,
syncState ? { int: syncState } : 0
];
Zotero.DB.query(sql, bindParams);
}
@ -1596,21 +1589,24 @@ Zotero.Item.prototype.save = function() {
// Attachment
if (this._changedAttachmentData) {
var sql = "REPLACE INTO itemAttachments (itemID, sourceItemID, linkMode, "
+ "mimeType, charsetID, path) VALUES (?,?,?,?,?,?)";
var sql = "UPDATE itemAttachments SET sourceItemID=?, "
+ "linkMode=?, mimeType=?, charsetID=?, path=?, syncState=? "
+ "WHERE itemID=?";
var parent = this.getSource();
var linkMode = this.attachmentLinkMode;
var mimeType = this.attachmentMIMEType;
var charsetID = this.attachmentCharset;
var path = this.attachmentPath;
var syncState = this.attachmentSyncState;
var bindParams = [
this.id,
parent ? parent : null,
{ int: linkMode },
mimeType ? { string: mimeType } : null,
charsetID ? { int: charsetID } : null,
path ? { string: path } : null
path ? { string: path } : null,
syncState ? { int: syncState } : 0,
this.id
];
Zotero.DB.query(sql, bindParams);
}
@ -2109,7 +2105,7 @@ Zotero.Item.prototype.numAttachments = function() {
* Get an nsILocalFile for the attachment, or false if the associated file
* doesn't exist
*
* _row_ is optional itemAttachments row if available to skip query
* _row_ is optional itemAttachments row if available to skip queries
*
* Note: Always returns false for items with LINK_MODE_LINKED_URL,
* since they have no files -- use getField('url') instead
@ -2120,12 +2116,10 @@ Zotero.Item.prototype.getFile = function(row, skipExistsCheck) {
}
if (!row) {
var sql = "SELECT linkMode, path FROM itemAttachments WHERE itemID=?"
var row = Zotero.DB.rowQuery(sql, this.id);
}
if (!row) {
throw ('Attachment data not found for item ' + this.id + ' in getFile()');
var row = {
linkMode: this.attachmentLinkMode,
path: this.attachmentPath
};
}
// No associated files for linked URLs
@ -2144,7 +2138,7 @@ Zotero.Item.prototype.getFile = function(row, skipExistsCheck) {
var path = row.path.substr(8);
var file = Zotero.Attachments.getStorageDirectory(this.id);
file.QueryInterface(Components.interfaces.nsILocalFile);
file.append(path);
file.setRelativeDescriptor(file, path);
if (!file.exists()) {
Zotero.debug("Attachment file '" + path + "' not found");
throw ('File not found');
@ -2321,7 +2315,8 @@ Zotero.Item.prototype.__defineSetter__('attachmentLinkMode', function (val) {
break;
default:
throw ("Invalid attachment link mode '" + val + "' in Zotero.Item.attachmentLinkMode setter");
throw ("Invalid attachment link mode '" + val
+ "' in Zotero.Item.attachmentLinkMode setter");
}
if (val === this._attachmentLinkMode) {
@ -2402,18 +2397,18 @@ Zotero.Item.prototype.__defineGetter__('attachmentCharset', function () {
return undefined;
}
if (this._attachmentCharset !== null) {
if (this._attachmentCharset != undefined) {
return this._attachmentCharset;
}
if (!this.id) {
return '';
return null;
}
var sql = "SELECT charsetID FROM itemAttachments WHERE itemID=?";
var charset = Zotero.DB.valueQuery(sql, this.id);
if (!charset) {
charset = '';
charset = null;
}
this._attachmentCharset = charset;
return charset;
@ -2425,8 +2420,10 @@ Zotero.Item.prototype.__defineSetter__('attachmentCharset', function (val) {
throw (".attachmentCharset can only be set for attachment items");
}
val = Zotero.CharacterSets.getID(val);
if (!val) {
val = '';
val = null;
}
if (val == this._attachmentCharset) {
@ -2489,6 +2486,90 @@ Zotero.Item.prototype.__defineSetter__('attachmentPath', function (val) {
});
Zotero.Item.prototype.__defineGetter__('attachmentSyncState', function () {
if (!this.isAttachment()) {
return undefined;
}
if (this._attachmentSyncState != undefined) {
return this._attachmentSyncState;
}
if (!this.id) {
return undefined;
}
var sql = "SELECT syncState FROM itemAttachments WHERE itemID=?";
var syncState = Zotero.DB.valueQuery(sql, this.id);
this._attachmentSyncState = syncState;
return syncState;
});
Zotero.Item.prototype.__defineSetter__('attachmentSyncState', function (val) {
if (!this.isAttachment()) {
throw ("attachmentSyncState can only be set for attachment items");
}
switch (this.attachmentLinkMode) {
case Zotero.Attachments.LINK_MODE_IMPORTED_URL:
case Zotero.Attachments.LINK_MODE_IMPORTED_FILE:
break;
default:
throw ("attachmentSyncState can only be set for snapshots and "
+ "imported files");
}
switch (val) {
case Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD:
case Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD:
case Zotero.Sync.Storage.SYNC_STATE_IN_SYNC:
break;
default:
throw ("Invalid sync state '" + val
+ "' in Zotero.Item.attachmentSyncState setter");
}
if (val == this._attachmentSyncState) {
return;
}
if (!this._changedAttachmentData) {
this._changedAttachmentData = {};
}
this._changedAttachmentData.syncState = true;
this._attachmentSyncState = val;
});
/**
* Modification time of an attachment file
*
* Note: This is the mod time of the file itself, not the last-known mod time
* of the file on the storage server as stored in the database
*
* @return {Number} File modification time as UNIX timestamp
*/
Zotero.Item.prototype.__defineGetter__('attachmentModificationTime', function () {
if (!this.isAttachment()) {
return undefined;
}
if (!this.id) {
return undefined;
}
var file = this.getFile();
if (!file) {
return undefined;
}
return file.lastModifiedTime / 1000;
});
/**
* Returns an array of attachment itemIDs that have this item as a source,
* or FALSE if none
@ -2579,18 +2660,28 @@ Zotero.Item.prototype.addTag = function(name, type) {
Zotero.DB.beginTransaction();
var existingTypes = Zotero.Tags.getTypes(name);
if (existingTypes) {
// If existing automatic and adding identical user, remove automatic
if (type == 0 && existingTypes.indexOf(1) != -1) {
this.removeTag(Zotero.Tags.getID(name, 1));
var matchingTags = Zotero.Tags.getIDs(name);
if (matchingTags) {
var itemTags = this.getTags();
for each(var id in matchingTags) {
if (itemTags.indexOf(id) != -1) {
var tag = Zotero.Tags.get(id);
// If existing automatic and adding identical user,
// remove automatic
if (type == 0 && tag.type == 1) {
this.removeTag(id);
break;
}
else {
Zotero.debug('Identical tag already exists -- not adding tag');
// If existing user and adding automatic, skip
else if (type == 1 && tag.type == 0) {
Zotero.debug("Identical user tag '" + name
+ "' already exists -- skipping automatic tag");
Zotero.DB.commitTransaction();
return false;
}
}
}
}
var tagID = Zotero.Tags.getID(name, type);
if (!tagID) {
@ -2601,9 +2692,9 @@ Zotero.Item.prototype.addTag = function(name, type) {
}
try {
this.addTagByID(tagID);
var added = this.addTagByID(tagID);
Zotero.DB.commitTransaction();
return tagID;
return added ? tagID : false;
}
catch (e) {
Zotero.DB.rollbackTransaction();
@ -2641,8 +2732,12 @@ Zotero.Item.prototype.addTagByID = function(tagID) {
throw ('Cannot add invalid tag ' + tagID + ' in Zotero.Item.addTagByID()');
}
tag.addItem(this.id);
var added = tag.addItem(this.id);
if (!added) {
return false;
}
tag.save();
return true;
}
Zotero.Item.prototype.hasTag = function(tagID) {

View file

@ -401,6 +401,11 @@ Zotero.Items = new function() {
var sql = "DELETE FROM itemDataValues WHERE valueID NOT IN "
+ "(SELECT valueID FROM itemData)";
Zotero.DB.query(sql);
var ZU = new Zotero.Utilities;
if (Zotero.Sync.Storage.active && ZU.probability(10)) {
Zotero.Sync.Storage.purgeDeletedStorageFiles();
}
}

View file

@ -1671,6 +1671,14 @@ Zotero.Schema = new function(){
Zotero.DB.query("CREATE TABLE proxyHosts (\n hostID INTEGER PRIMARY KEY,\n proxyID INTEGER,\n hostname TEXT,\n FOREIGN KEY (proxyID) REFERENCES proxies(proxyID)\n)");
Zotero.DB.query("CREATE INDEX proxyHosts_proxyID ON proxyHosts(proxyID)");
}
if (i==40) {
Zotero.DB.query("ALTER TABLE itemAttachments ADD COLUMN syncState INT DEFAULT 0");
Zotero.DB.query("ALTER TABLE itemAttachments ADD COLUMN storageModTime INT");
Zotero.DB.query("CREATE INDEX itemAttachments_syncState ON itemAttachments(syncState)");
Zotero.DB.query("CREATE TABLE storageDeleteLog (\n key TEXT PRIMARY KEY,\n timestamp INT NOT NULL\n)");
Zotero.DB.query("CREATE INDEX storageDeleteLog_timestamp ON storageDeleteLog(timestamp)");
}
}
_updateDBVersion('userdata', toVersion);

File diff suppressed because it is too large Load diff

View file

@ -241,7 +241,7 @@ Zotero.Sync = new function() {
/**
* Notifier observer to add deleted objects to syncDeleteLog
* Notifier observer to add deleted objects to syncDeleteLog/storageDeleteLog
* plus related methods
*/
Zotero.Sync.EventListener = new function () {
@ -313,13 +313,22 @@ Zotero.Sync.EventListener = new function () {
return;
}
var isItem = Zotero.Sync.getObjectTypeName(objectTypeID) == 'item';
var ZU = new Zotero.Utilities;
Zotero.DB.beginTransaction();
if (event == 'delete') {
var sql = "INSERT INTO syncDeleteLog VALUES (?, ?, ?, ?)";
var statement = Zotero.DB.getStatement(sql);
var syncStatement = Zotero.DB.getStatement(sql);
if (isItem && Zotero.Sync.Storage.active) {
var storageEnabled = true;
var sql = "INSERT INTO storageDeleteLog VALUES (?, ?)";
var storageStatement = Zotero.DB.getStatement(sql);
}
var storageBound = false;
var ts = Zotero.Date.getUnixTimestamp();
@ -331,24 +340,51 @@ Zotero.Sync.EventListener = new function () {
continue;
}
var key = extraData[ids[i]].old.primary.key;
var oldItem = extraData[ids[i]].old;
var key = oldItem.primary.key;
statement.bindInt32Parameter(0, objectTypeID);
statement.bindInt32Parameter(1, ids[i]);
statement.bindStringParameter(2, key);
statement.bindInt32Parameter(3, ts);
if (!key) {
throw("Key not provided in notifier object in "
+ "Zotero.Sync.EventListener.notify()");
}
syncStatement.bindInt32Parameter(0, objectTypeID);
syncStatement.bindInt32Parameter(1, ids[i]);
syncStatement.bindStringParameter(2, key);
syncStatement.bindInt32Parameter(3, ts);
if (storageEnabled &&
oldItem.primary.itemType == 'attachment' &&
[
Zotero.Attachments.LINK_MODE_IMPORTED_FILE,
Zotero.Attachments.LINK_MODE_IMPORTED_URL
].indexOf(oldItem.attachment.linkMode) != -1) {
storageStatement.bindStringParameter(0, key);
storageStatement.bindInt32Parameter(1, ts);
storageBound = true;
}
try {
statement.execute();
syncStatement.execute();
if (storageBound) {
storageStatement.execute();
storageBound = false;
}
}
catch(e) {
statement.reset();
syncStatement.reset();
if (storageEnabled) {
storageStatement.reset();
}
Zotero.DB.rollbackTransaction();
throw(Zotero.DB.getLastErrorString());
}
}
statement.reset();
syncStatement.reset();
if (storageEnabled) {
storageStatement.reset();
}
}
Zotero.DB.commitTransaction();
@ -374,12 +410,154 @@ Zotero.Sync.EventListener = new function () {
}
Zotero.Sync.Runner = new function () {
this.__defineGetter__("lastSyncError", function () {
return _lastSyncError;
});
this.__defineSetter__("lastSyncError", function (val) {
_lastSyncError = val ? val : '';
});
var _lastSyncError;
var _autoSyncTimer;
var _queue;
var _running;
this.init = function () {
this.EventListener.init();
}
this.sync = function () {
if (_running) {
throw ("Sync already running in Zotero.Sync.Runner.sync()");
}
_queue = [
Zotero.Sync.Storage.sync,
Zotero.Sync.Server.sync,
Zotero.Sync.Storage.sync
];
_running = true;
this.clearSyncTimeout();
this.setSyncIcon('animate');
this.next();
}
this.next = function () {
if (!_queue.length) {
this.setSyncIcon();
_running = false;
return;
}
var func = _queue.shift();
func();
}
this.reset = function () {
_queue = [];
}
this.setSyncTimeout = function () {
// check if server/auto-sync are enabled
var autoSyncTimeout = 15;
Zotero.debug('Setting auto-sync timeout to ' + autoSyncTimeout + ' seconds');
if (_autoSyncTimer) {
_autoSyncTimer.cancel();
}
else {
_autoSyncTimer = Components.classes["@mozilla.org/timer;1"].
createInstance(Components.interfaces.nsITimer);
}
// {} implements nsITimerCallback
_autoSyncTimer.initWithCallback({ notify: function (event, type, ids) {
if (event == 'refresh') {
return;
}
if (Zotero.Sync.Storage.syncInProgress) {
Zotero.debug('Storage sync already in progress -- skipping auto-sync', 4);
return;
}
if (Zotero.Sync.Server.syncInProgress) {
Zotero.debug('Sync already in progress -- skipping auto-sync', 4);
return;
}
Zotero.Sync.Runner.sync();
}}, autoSyncTimeout * 1000, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
}
this.clearSyncTimeout = function () {
if (_autoSyncTimer) {
_autoSyncTimer.cancel();
}
}
this.setSyncIcon = function (status) {
status = status ? status : '';
switch (status) {
case '':
case 'animate':
case 'error':
break;
default:
throw ("Invalid sync icon status '" + status
+ "' in Zotero.Sync.Runner.setSyncIcon()");
}
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var win = wm.getMostRecentWindow('navigator:browser');
var icon = win.document.getElementById('zotero-tb-sync');
icon.setAttribute('status', status);
switch (status) {
case 'animate':
icon.setAttribute('disabled', true);
break;
default:
icon.setAttribute('disabled', false);
}
}
}
Zotero.Sync.Runner.EventListener = {
init: function () {
Zotero.Notifier.registerObserver(this);
},
notify: function (event, type, ids, extraData) {
// TODO: skip others
if (type == 'refresh') {
return;
}
if (Zotero.Prefs.get('sync.autoSync') && Zotero.Sync.Server.enabled
&& !Zotero.Sync.Server.syncInProgress
&& !Zotero.Sync.Storage.syncInProgress) {
Zotero.Sync.Runner.setSyncTimeout();
}
}
}
/**
* Methods for syncing with the Zotero Server
*/
Zotero.Sync.Server = new function () {
this.init = init;
this.login = login;
this.sync = sync;
this.lock = lock;
@ -388,14 +566,12 @@ Zotero.Sync.Server = new function () {
this.resetServer = resetServer;
this.resetClient = resetClient;
this.logout = logout;
this.setSyncTimeout = setSyncTimeout;
this.clearSyncTimeout = clearSyncTimeout;
this.setSyncIcon = setSyncIcon;
this.__defineGetter__('enabled', function () {
// Set auto-sync expiry
var expiry = new Date("September 1, 2008 00:00:00");
var expiry = new Date("November 1, 2008 00:00:00");
if (new Date() > expiry) {
Components.utils.reportError("Build has expired -- syncing disabled");
return false;
}
@ -469,6 +645,7 @@ Zotero.Sync.Server = new function () {
}
});
this.__defineGetter__("syncInProgress", function () _syncInProgress);
this.__defineGetter__("sessionIDComponent", function () {
return 'sessionid=' + _sessionID;
});
@ -484,12 +661,6 @@ Zotero.Sync.Server = new function () {
this.__defineSetter__("lastLocalSyncTime", function (val) {
Zotero.DB.query("REPLACE INTO version VALUES ('lastlocalsync', ?)", { int: val });
});
this.__defineGetter__("lastSyncError", function () {
return _lastSyncError;
});
this.__defineSetter__("lastSyncError", function (val) {
_lastSyncError = val ? val : '';
});
this.nextLocalSyncDate = false;
this.apiVersion = 2;
@ -508,13 +679,6 @@ Zotero.Sync.Server = new function () {
var _syncInProgress;
var _sessionID;
var _sessionLock;
var _lastSyncError;
var _autoSyncTimer;
function init() {
this.EventListener.init();
}
function login(callback) {
@ -572,8 +736,7 @@ Zotero.Sync.Server = new function () {
function sync() {
Zotero.Sync.Server.clearSyncTimeout();
Zotero.Sync.Server.setSyncIcon('animate');
Zotero.Sync.Runner.setSyncIcon('animate');
if (_attempts < 0) {
_error('Too many attempts in Zotero.Sync.Server.sync()');
@ -594,6 +757,7 @@ Zotero.Sync.Server = new function () {
_error("Sync operation already in progress");
}
Zotero.debug("Beginning server sync");
_syncInProgress = true;
// Get updated data
@ -682,8 +846,10 @@ Zotero.Sync.Server = new function () {
Zotero.Sync.Server.lastLocalSyncTime = nextLocalSyncTime;
Zotero.Sync.Server.nextLocalSyncDate = false;
Zotero.DB.commitTransaction();
Zotero.Sync.Server.unlock();
Zotero.Sync.Server.unlock(function () {
_syncInProgress = false;
Zotero.Sync.Runner.next();
});
return;
}
@ -722,8 +888,10 @@ Zotero.Sync.Server = new function () {
//throw('break2');
Zotero.DB.commitTransaction();
Zotero.Sync.Server.unlock();
Zotero.Sync.Server.unlock(function () {
_syncInProgress = false;
Zotero.Sync.Runner.next();
});
}
var compress = Zotero.Prefs.get('sync.server.compressData');
@ -894,12 +1062,6 @@ Zotero.Sync.Server = new function () {
if (callback) {
callback();
}
// Reset sync icon and last error
if (syncInProgress) {
Zotero.Sync.Server.lastSyncError = '';
Zotero.Sync.Server.setSyncIcon();
}
});
}
@ -1001,6 +1163,7 @@ Zotero.Sync.Server = new function () {
Zotero.DB.query(sql);
Zotero.DB.query("DELETE FROM syncDeleteLog");
Zotero.DB.query("DELETE FROM storageDeleteLog");
sql = "INSERT INTO version VALUES ('syncdeletelog', ?)";
Zotero.DB.query(sql, Zotero.Date.getUnixTimestamp());
@ -1037,61 +1200,6 @@ Zotero.Sync.Server = new function () {
}
function setSyncTimeout() {
// check if server/auto-sync are enabled
var autoSyncTimeout = 15;
Zotero.debug('Setting auto-sync timeout to ' + autoSyncTimeout + ' seconds');
if (_autoSyncTimer) {
_autoSyncTimer.cancel();
}
else {
_autoSyncTimer = Components.classes["@mozilla.org/timer;1"].
createInstance(Components.interfaces.nsITimer);
}
// {} implements nsITimerCallback
_autoSyncTimer.initWithCallback({ notify: function (event, type, ids) {
if (event == 'refresh') {
return;
}
if (_syncInProgress) {
Zotero.debug('Sync already in progress -- skipping auto-sync');
return;
}
Zotero.Sync.Server.sync();
}}, autoSyncTimeout * 1000, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
}
function clearSyncTimeout() {
if (_autoSyncTimer) {
_autoSyncTimer.cancel();
}
}
function setSyncIcon(status) {
status = status ? status : '';
switch (status) {
case '':
case 'animate':
case 'error':
break;
default:
throw ("Invalid sync icon status '" + status + "' in Zotero.Sync.Server.setSyncIcon()");
}
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var win = wm.getMostRecentWindow('navigator:browser');
win.document.getElementById('zotero-tb-sync').setAttribute('status', status);
}
function _checkResponse(xmlhttp) {
if (!xmlhttp.responseXML ||
!xmlhttp.responseXML.childNodes[0] ||
@ -1130,14 +1238,15 @@ Zotero.Sync.Server = new function () {
Zotero.Sync.Server.unlock()
}
Zotero.Sync.Server.setSyncIcon('error');
Zotero.Sync.Runner.setSyncIcon('error');
if (e.name) {
Zotero.Sync.Server.lastSyncError = e.name;
Zotero.Sync.Runner.lastSyncError = e.name;
}
else {
Zotero.Sync.Server.lastSyncError = e;
Zotero.Sync.Runner.lastSyncError = e;
}
Zotero.debug(e, 1);
Zotero.Sync.Runner.reset();
throw(e);
}
}
@ -1182,26 +1291,6 @@ Zotero.BufferedInputListener.prototype = {
}
// TODO: use prototype
Zotero.Sync.Server.EventListener = {
init: function () {
Zotero.Notifier.registerObserver(this);
},
notify: function (event, type, ids, extraData) {
// TODO: skip others
if (type == 'refresh') {
return;
}
if (Zotero.Prefs.get('sync.server.autoSync') && Zotero.Sync.Server.enabled) {
Zotero.Sync.Server.setSyncTimeout();
}
}
}
Zotero.Sync.Server.Data = new function() {
this.processUpdatedXML = processUpdatedXML;
this.buildUploadXML = buildUploadXML;
@ -1299,6 +1388,7 @@ Zotero.Sync.Server.Data = new function() {
var remoteCreatorStore = {};
var relatedItemsStore = {};
var itemStorageModTimes = {};
Zotero.DB.beginTransaction();
@ -1527,10 +1617,10 @@ Zotero.Sync.Server.Data = new function() {
// Create or overwrite locally
obj = Zotero.Sync.Server.Data['xmlTo' + Type](xmlNode, obj);
if (isNewObject && type == 'tag') {
// If a local tag matches the name of a different remote tag,
// delete the local tag and add items linked to it to the
// matching remote tag
if (isNewObject && type == 'tag') {
var tagName = xmlNode.@name.toString();
var tagType = xmlNode.@type.toString()
? parseInt(xmlNode.@type) : 0;
@ -1562,6 +1652,25 @@ Zotero.Sync.Server.Data = new function() {
// Don't use assigned-but-unsaved ids for new ids
Zotero.ID.skip(types, obj.id);
if (type == 'item' && obj.isAttachment() &&
(obj.attachmentLinkMode ==
Zotero.Attachments.LINK_MODE_IMPORTED_FILE ||
obj.attachmentLinkMode ==
Zotero.Attachments.LINK_MODE_IMPORTED_URL)) {
// Mark new attachments for download
if (isNewObject) {
obj.attachmentSyncState =
Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD;
}
// Set existing attachments mtime update check
else {
var mtime = xmlNode.@storageModTime.toString();
if (mtime) {
itemStorageModTimes[obj.id] = parseInt(mtime);
}
}
}
}
@ -1738,6 +1847,19 @@ Zotero.Sync.Server.Data = new function() {
Zotero[Types].erase(toDeleteParents);
Zotero.Sync.EventListener.unignoreDeletions(type, toDeleteParents);
}
// Check mod times of updated items against stored time to see
// if they've been updated elsewhere and mark for download if so
if (type == 'item') {
var ids = [];
for (var id in itemStorageModTimes) {
ids.push(id);
}
if (ids.length > 0) {
Zotero.Sync.Storage.checkForUpdatedFiles(ids, itemStorageModTimes);
}
}
}
var xmlstr = Zotero.Sync.Server.Data.buildUploadXML(uploadIDs);
@ -1888,10 +2010,18 @@ Zotero.Sync.Server.Data = new function() {
if (item.primary.itemType == 'attachment') {
xml.@linkMode = item.attachment.linkMode;
xml.@mimeType = item.attachment.mimeType;
xml.@charset = item.attachment.charset;
var charset = item.attachment.charset;
if (charset) {
xml.@charset = charset;
}
// Don't include paths for links
// Include storage sync time and paths for non-links
if (item.attachment.linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) {
var mtime = Zotero.Sync.Storage.getSyncedModificationTime(item.primary.itemID);
if (mtime) {
xml.@storageModTime = mtime;
}
var path = <path>{item.attachment.path}</path>;
xml.path += path;
}
@ -2018,8 +2148,8 @@ Zotero.Sync.Server.Data = new function() {
// Attachment metadata
if (item.isAttachment()) {
item.attachmentLinkMode = parseInt(xmlItem.@linkMode);
item.attachmentMIMEType = xmlItem.@mimeType;
item.attachmentCharset = parseInt(xmlItem.@charsetID);
item.attachmentMIMEType = xmlItem.@mimeType.toString();
item.attachmentCharset = xmlItem.@charset.toString();
if (item.attachmentLinkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) {
item.attachmentPath = xmlItem.path.toString();
}

View file

@ -296,6 +296,31 @@ Zotero.Utilities.prototype.isInt = function(x) {
}
/**
* Generate a random integer between min and max inclusive
*
* @param {Integer} min
* @param {Integer} max
* @return {Integer}
*/
Zotero.Utilities.prototype.rand = function (min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* Return true according to a given probability
*
* @param {Integer} x Will return true every x times on average
* @return {Boolean} On average, TRUE every x times
* the function is called
*/
Zotero.Utilities.prototype.probability = function (x) {
return this.rand(1, x) == this.rand(1, x);
}
/**
* Determine the necessary data type for SQLite parameter binding
*
@ -643,9 +668,9 @@ Zotero.Utilities.HTTP = new function() {
this.doGet = doGet;
this.doPost = doPost;
this.doHead = doHead;
this.doOptions = doOptions;
this.browserIsOffline = browserIsOffline;
this.WebDAV = {};
/**
* Send an HTTP GET request via XMLHTTPRequest
@ -665,8 +690,9 @@ Zotero.Utilities.HTTP = new function() {
var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance();
var test = xmlhttp.open('GET', url, true);
// Prevent certificate/authentication dialogs from popping up
xmlhttp.mozBackgroundRequest = true;
xmlhttp.open('GET', url, true);
xmlhttp.onreadystatechange = function(){
_stateChange(xmlhttp, onDone, responseCharset);
@ -716,7 +742,8 @@ Zotero.Utilities.HTTP = new function() {
var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance();
// Prevent certificate/authentication dialogs from popping up
xmlhttp.mozBackgroundRequest = true;
xmlhttp.open('POST', url, true);
xmlhttp.setRequestHeader("Content-Type", (requestContentType ? requestContentType : "application/x-www-form-urlencoded" ));
@ -750,8 +777,9 @@ Zotero.Utilities.HTTP = new function() {
var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance();
var test = xmlhttp.open('HEAD', url, true);
// Prevent certificate/authentication dialogs from popping up
xmlhttp.mozBackgroundRequest = true;
xmlhttp.open('HEAD', url, true);
xmlhttp.onreadystatechange = function(){
_stateChange(xmlhttp, onDone);
@ -778,24 +806,28 @@ Zotero.Utilities.HTTP = new function() {
/**
* Send an HTTP OPTIONS request via XMLHTTPRequest
*
* doOptions can be called as:
* Zotero.Utilities.HTTP.doOptions(url, body, onDone)
*
* Returns the XMLHTTPRequest object
**/
function doOptions(url, body, onDone) {
Zotero.debug("HTTP OPTIONS "+url);
if (this.browserIsOffline()){
* @param {nsIURI} url
* @param {Function} onDone
* @return {XMLHTTPRequest}
*/
this.doOptions = function (uri, callback) {
// Don't display password in console
var disp = uri.clone();
disp.password = "********";
Zotero.debug("HTTP OPTIONS to " + disp.spec);
if (Zotero.Utilities.HTTP.browserIsOffline()){
return false;
}
var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance();
xmlhttp.open('OPTIONS', url, true);
// Prevent certificate/authentication dialogs from popping up
xmlhttp.mozBackgroundRequest = true;
xmlhttp.open('OPTIONS', uri.spec, true);
xmlhttp.onreadystatechange = function() {
_stateChange(xmlhttp, onDone);
_stateChange(xmlhttp, callback);
};
// Temporarily set cookieBehavior to 0 for Firefox 3
@ -806,7 +838,7 @@ Zotero.Utilities.HTTP = new function() {
var cookieBehavior = prefService.getIntPref("network.cookie.cookieBehavior");
prefService.setIntPref("network.cookie.cookieBehavior", 0);
xmlhttp.send(body);
xmlhttp.send(null);
}
finally {
prefService.setIntPref("network.cookie.cookieBehavior", cookieBehavior);
@ -816,36 +848,231 @@ Zotero.Utilities.HTTP = new function() {
}
//
// WebDAV methods
//
/**
* Send a WebDAV PROP* request via XMLHTTPRequest
*
* Returns false if browser is offline
*
* @param {String} method PROPFIND or PROPPATCH
* @param {nsIURI} uri
* @param {String} body XML string
* @param {Function} callback
* @param {Object} requestHeaders e.g. { Depth: 0 }
*/
this.WebDAV.doProp = function (method, uri, body, callback, requestHeaders) {
switch (method) {
case 'PROPFIND':
case 'PROPPATCH':
break;
default:
throw ("Invalid method '" + method
+ "' in Zotero.Utilities.HTTP.doProp");
}
if (requestHeaders && requestHeaders.depth != undefined) {
var depth = requestHeaders.depth;
}
// Don't display password in console
var disp = uri.clone();
disp.password = "********";
var bodyStart = body.substr(0, 1024);
Zotero.debug("HTTP " + method + " "
+ (depth != undefined ? "(depth " + depth + ") " : "")
+ (body.length > 1024 ?
bodyStart + "... (" + body.length + " chars)" : bodyStart)
+ " to " + disp.spec);
if (Zotero.Utilities.HTTP.browserIsOffline()) {
Zotero.debug("Browser is offline", 2);
return false;
}
var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance();
// Prevent certificate/authentication dialogs from popping up
xmlhttp.mozBackgroundRequest = true;
xmlhttp.open(method, uri.spec, true);
if (requestHeaders) {
for (var header in requestHeaders) {
xmlhttp.setRequestHeader(header, requestHeaders[header]);
}
}
xmlhttp.setRequestHeader("Content-Type", 'text/xml; charset="utf-8"');
xmlhttp.onreadystatechange = function() {
_stateChange(xmlhttp, callback);
};
xmlhttp.send(body);
return xmlhttp;
}
/**
* Send a WebDAV MKCOL request via XMLHTTPRequest
*
* @param {nsIURI} url
* @param {Function} onDone
* @return {XMLHTTPRequest}
*/
this.WebDAV.doMkCol = function (uri, callback) {
// Don't display password in console
var disp = uri.clone();
disp.password = "********";
Zotero.debug("HTTP MKCOL to " + disp.spec);
if (Zotero.Utilities.HTTP.browserIsOffline()) {
return false;
}
var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance();
// Prevent certificate/authentication dialogs from popping up
xmlhttp.mozBackgroundRequest = true;
xmlhttp.open('MKCOL', uri.spec, true);
xmlhttp.onreadystatechange = function() {
_stateChange(xmlhttp, callback);
};
xmlhttp.send(null);
return xmlhttp;
}
/**
* Send a WebDAV PUT request via XMLHTTPRequest
*
* @param {nsIURI} url
* @param {String} body String body to PUT
* @param {Function} onDone
* @return {XMLHTTPRequest}
*/
this.WebDAV.doPut = function (uri, body, callback) {
// Don't display password in console
var disp = uri.clone();
disp.password = "********";
var bodyStart = "'" + body.substr(0, 1024) + "'";
Zotero.debug("HTTP PUT "
+ (body.length > 1024 ?
bodyStart + "... (" + body.length + " chars)" : bodyStart)
+ " to " + disp.spec);
if (Zotero.Utilities.HTTP.browserIsOffline()) {
return false;
}
var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance();
// Prevent certificate/authentication dialogs from popping up
xmlhttp.mozBackgroundRequest = true;
xmlhttp.open("PUT", uri.spec, true);
xmlhttp.onreadystatechange = function() {
_stateChange(xmlhttp, callback);
};
xmlhttp.send(body);
return xmlhttp;
}
/**
* Send a WebDAV PUT request via XMLHTTPRequest
*
* @param {nsIURI} url
* @param {Function} onDone
* @return {XMLHTTPRequest}
*/
this.WebDAV.doDelete = function (uri, callback) {
// Don't display password in console
var disp = uri.clone();
disp.password = "********";
Zotero.debug("WebDAV DELETE to " + disp.spec);
if (Zotero.Utilities.HTTP.browserIsOffline()) {
return false;
}
var xmlhttp = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance();
// Prevent certificate/authentication dialogs from popping up
xmlhttp.mozBackgroundRequest = true;
xmlhttp.open("DELETE", uri.spec, true);
xmlhttp.onreadystatechange = function() {
_stateChange(xmlhttp, callback);
};
xmlhttp.send(null);
return xmlhttp;
}
/**
* Get the Authorization header used by a channel
*
* As of Firefox 3.0.1 subsequent requests to higher-level directories
* seem not to authenticate properly and just return 401s, so this
* can be used to manually include the Authorization header in a request
*
* It can also be used to check whether a request was forced to
* use authentication
*
* @param {nsIChannel} channel
* @return {String|FALSE} Authorization header, or FALSE if none
*/
this.getChannelAuthorization = function (channel) {
try {
channel.QueryInterface(Components.interfaces.nsIHttpChannel);
var authHeader = channel.getRequestHeader("Authorization");
return authHeader;
}
catch (e) {
Zotero.debug(e);
return false;
}
}
function browserIsOffline() {
return Components.classes["@mozilla.org/network/io-service;1"]
.getService(Components.interfaces.nsIIOService).offline;
}
function _stateChange(xmlhttp, onDone, responseCharset){
function _stateChange(xmlhttp, callback, responseCharset, data) {
switch (xmlhttp.readyState){
// Request not yet made
case 1:
break;
// Called multiple while downloading in progress
case 2:
break;
// Called multiple times while downloading in progress
case 3:
break;
// Download complete
case 4:
if(onDone){
if (callback) {
// Override the content charset
if (responseCharset) {
xmlhttp.channel.contentCharset = responseCharset;
}
onDone(xmlhttp);
callback(xmlhttp, data);
}
break;
}
}
}
// Downloads and processes documents with processor()
@ -953,3 +1180,142 @@ Zotero.Utilities.AutoComplete = new function(){
return false;
}
}
/**
* Base64 encode / decode
* From http://www.webtoolkit.info/
*/
Zotero.Utilities.Base64 = {
// private property
_keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
// public method for encoding
encode : function (input) {
var output = "";
var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
var i = 0;
input = this._utf8_encode(input);
while (i < input.length) {
chr1 = input.charCodeAt(i++);
chr2 = input.charCodeAt(i++);
chr3 = input.charCodeAt(i++);
enc1 = chr1 >> 2;
enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
enc4 = chr3 & 63;
if (isNaN(chr2)) {
enc3 = enc4 = 64;
} else if (isNaN(chr3)) {
enc4 = 64;
}
output = output +
this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);
}
return output;
},
// public method for decoding
decode : function (input) {
var output = "";
var chr1, chr2, chr3;
var enc1, enc2, enc3, enc4;
var i = 0;
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
while (i < input.length) {
enc1 = this._keyStr.indexOf(input.charAt(i++));
enc2 = this._keyStr.indexOf(input.charAt(i++));
enc3 = this._keyStr.indexOf(input.charAt(i++));
enc4 = this._keyStr.indexOf(input.charAt(i++));
chr1 = (enc1 << 2) | (enc2 >> 4);
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
chr3 = ((enc3 & 3) << 6) | enc4;
output = output + String.fromCharCode(chr1);
if (enc3 != 64) {
output = output + String.fromCharCode(chr2);
}
if (enc4 != 64) {
output = output + String.fromCharCode(chr3);
}
}
output = this._utf8_decode(output);
return output;
},
// private method for UTF-8 encoding
_utf8_encode : function (string) {
string = string.replace(/\r\n/g,"\n");
var utftext = "";
for (var n = 0; n < string.length; n++) {
var c = string.charCodeAt(n);
if (c < 128) {
utftext += String.fromCharCode(c);
}
else if((c > 127) && (c < 2048)) {
utftext += String.fromCharCode((c >> 6) | 192);
utftext += String.fromCharCode((c & 63) | 128);
}
else {
utftext += String.fromCharCode((c >> 12) | 224);
utftext += String.fromCharCode(((c >> 6) & 63) | 128);
utftext += String.fromCharCode((c & 63) | 128);
}
}
return utftext;
},
// private method for UTF-8 decoding
_utf8_decode : function (utftext) {
var string = "";
var i = 0;
var c = c1 = c2 = 0;
while ( i < utftext.length ) {
c = utftext.charCodeAt(i);
if (c < 128) {
string += String.fromCharCode(c);
i++;
}
else if((c > 191) && (c < 224)) {
c2 = utftext.charCodeAt(i+1);
string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
i += 2;
}
else {
c2 = utftext.charCodeAt(i+1);
c3 = utftext.charCodeAt(i+2);
string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
i += 3;
}
}
return string;
}
}

View file

@ -41,6 +41,7 @@ var Zotero = new function(){
this.getZoteroDirectory = getZoteroDirectory;
this.getStorageDirectory = getStorageDirectory;
this.getZoteroDatabase = getZoteroDatabase;
this.getTempDirectory = getTempDirectory;
this.chooseZoteroDirectory = chooseZoteroDirectory;
this.debug = debug;
this.log = log;
@ -249,6 +250,9 @@ var Zotero = new function(){
if (typeof e == 'string' && e.match('newer than SQL file')) {
_startupError = e;
}
else {
_startupError = "Database upgrade error";
}
Components.utils.reportError(_startupError);
return false;
}
@ -265,7 +269,8 @@ var Zotero = new function(){
Zotero.Zeroconf.init();
Zotero.Sync.init();
Zotero.Sync.Server.init();
Zotero.Sync.Runner.init();
Zotero.Sync.Storage.init();
this.initialized = true;
@ -357,6 +362,22 @@ var Zotero = new function(){
}
/**
* @return {nsIFile}
*/
function getTempDirectory() {
var tmp = this.getZoteroDirectory();
tmp.append('tmp');
if (!tmp.exists() || !tmp.isDirectory()) {
if (tmp.exists() && !tmp.isDirectory()) {
tmp.remove(null);
}
tmp.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0755);
}
return tmp;
}
function chooseZoteroDirectory(forceRestartNow, useProfileDir) {
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
@ -1284,6 +1305,66 @@ Zotero.Date = new function(){
}
/**
* Convert a JS Date object to an ISO 8601 UTC date/time
*
* @param {Date} date JS Date object
* @return {String} ISO 8601 UTC date/time
* e.g. 2008-08-15T20:00:00Z
*/
this.dateToISO = function (date) {
var year = date.getUTCFullYear();
var month = date.getUTCMonth();
var day = date.getUTCDate();
var hours = date.getUTCHours();
var minutes = date.getUTCMinutes();
var seconds = date.getUTCSeconds();
var utils = new Zotero.Utilities();
year = utils.lpad(year, '0', 4);
month = utils.lpad(month + 1, '0', 2);
day = utils.lpad(day, '0', 2);
hours = utils.lpad(hours, '0', 2);
minutes = utils.lpad(minutes, '0', 2);
seconds = utils.lpad(seconds, '0', 2);
return year + '-' + month + '-' + day + 'T'
+ hours + ':' + minutes + ':' + seconds + 'Z';
}
/**
* Convert an ISO 8601formatted UTC date/time to a JS Date
*
* Adapted from http://delete.me.uk/2005/03/iso8601.html (AFL-licensed)
*
* @param {String} isoDate ISO 8601 date
* @return {Date} JS Date
*/
this.isoToDate = function (isoDate) {
var re8601 = /([0-9]{4})(-([0-9]{2})(-([0-9]{2})(T([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2}):([0-9]{2})))?)?)?)?/;
var d = isoDate.match(re8601);
var offset = 0;
var date = new Date(d[1], 0, 1);
if (d[3]) { date.setMonth(d[3] - 1); }
if (d[5]) { date.setDate(d[5]); }
if (d[7]) { date.setHours(d[7]); }
if (d[8]) { date.setMinutes(d[8]); }
if (d[10]) { date.setSeconds(d[10]); }
if (d[12]) { date.setMilliseconds(Number("0." + d[12]) * 1000); }
if (d[14]) {
offset = (Number(d[16]) * 60) + Number(d[17]);
offset *= ((d[15] == '-') ? 1 : -1);
}
offset -= date.getTimezoneOffset();
var time = (Number(date) + (offset * 60 * 1000));
return new Date(time);
}
/*
* converts a string to an object containing:
* day: integer form of the day

View file

@ -156,6 +156,10 @@
<!ENTITY zotero.integration.references.label "References in Bibliography">
<!ENTITY zotero.sync.storage.progress "Progress:">
<!ENTITY zotero.sync.storage.downloads "Downloads:">
<!ENTITY zotero.sync.storage.uploads "Uploads:">
<!ENTITY zotero.proxy.recognized.title "Proxy Recognized">
<!ENTITY zotero.proxy.recognized.warning "Only add proxies linked from your library, school, or corporate website">
<!ENTITY zotero.proxy.recognized.warning.secondary "Adding other proxies allows malicious sites to masquerade as sites you trust.">

View file

@ -501,6 +501,9 @@ styles.installed = The style "%S" was installed successfully.
styles.installError = %S does not appear to be a valid CSL file.
styles.deleteStyle = Are you sure you want to delete the style "%1$S"?
sync.storage.kbRemaining = %SKB remaining
sync.storage.none = None
proxies.multiSite = Multi-Site
proxies.error = Information Validation Error
proxies.error.scheme.noHTTP = Valid proxy schemes must start with "http://" or "https://"

Binary file not shown.

After

Width:  |  Height:  |  Size: 585 B

View file

@ -191,11 +191,28 @@
list-style-image: url('chrome://zotero/skin/toolbar-advanced-search.png');
}
#zotero-tb-syncProgress
{
min-width: 50px;
width: 50px;
height: 10px;
}
#zotero-tb-syncProgress-tooltip row label:first-child
{
text-align: right;
font-weight: bold;
}
#zotero-tb-storage-sync
{
list-style-image: url(chrome://zotero/skin/drive_network.png);
}
#zotero-tb-sync {
margin-top: -2px;
list-style-image: url(chrome://zotero/skin/arrow_rotate_static.png);
margin-left: -2px;
margin-right: -2px;
list-style-image: url(chrome://zotero/skin/arrow_rotate_static.png);
}
#zotero-tb-sync[status=animate] {

View file

@ -3,14 +3,8 @@ prefwindow .chromeclass-toolbar
display: -moz-box !important; /* Ignore toolbar collapse button on OS X */
}
/* Prevent bugs in automatic prefpane sizing in Firefox 2.0
From http://forums.mozillazine.org/viewtopic.php?p=2883233&sid=e1285f81ea9c824363802ea5ca96c9b2
*/
prefwindow {
width: 45em;
}
prefwindow > prefpane > vbox.content-box {
height: 42em;
min-width: 600px;
}
radio[pane]
@ -75,6 +69,50 @@ grid row hbox:first-child
}
/*
* Sync pane
*/
#zotero-prefpane-sync row, #zotero-prefpane-sync row hbox
{
-moz-box-align: center;
}
#zotero-prefpane-sync row label:first-child
{
text-align: right;
}
#zotero-prefpane-sync row hbox
{
margin-left: 4px;
}
#zotero-prefpane-sync row hbox label:first-child
{
margin-left: 0;
margin-right: 0;
}
#zotero-prefpane-sync row hbox textbox
{
margin-left: 3px;
margin-right: 3px;
}
#zotero-prefpane-sync row hbox label:last-child
{
margin-left: 0;
margin-right: 10px;
}
#storage-settings
{
margin-left: 10px;
margin-right: 5px;
}
#storage-verify, #storage-abort, #storage-clean
{
margin-left: 0;
min-width: 8em;
}
/*
* Search pane
*/

View file

@ -51,6 +51,7 @@ var xpcomFiles = [
'schema',
'search',
'sync',
'storage',
'timeline',
'translate',
'utilities',

View file

@ -82,10 +82,17 @@ pref("extensions.zotero.zeroconf.server.enabled", false);
// Annotation settings
pref("extensions.zotero.annotations.warnOnClose", true);
// Server
pref("extensions.zotero.sync.server.autoSync", true);
// Sync
pref("extensions.zotero.sync.autoSync", true);
pref("extensions.zotero.sync.server.username", '');
pref("extensions.zotero.sync.server.compressData", true);
pref("extensions.zotero.sync.storage.enabled", false);
pref("extensions.zotero.sync.storage.verified", false);
pref("extensions.zotero.sync.storage.url", '');
pref("extensions.zotero.sync.storage.username", '');
pref("extensions.zotero.sync.storage.maxDownloads", 4);
pref("extensions.zotero.sync.storage.maxUploads", 4);
pref("extensions.zotero.sync.storage.deleteDelayDays", 30);
// Proxy
pref("extensions.zotero.proxies.autoRecognize", true);

View file

@ -1,4 +1,4 @@
-- 39
-- 40
-- This file creates tables containing user-specific data -- any changes made
-- here must be mirrored in transition steps in schema.js::_migrateSchema()
@ -62,11 +62,14 @@ CREATE TABLE itemAttachments (
charsetID INT,
path TEXT,
originalPath TEXT,
syncState INT DEFAULT 0,
storageModTime INT,
FOREIGN KEY (itemID) REFERENCES items(itemID),
FOREIGN KEY (sourceItemID) REFERENCES items(sourceItemID)
);
CREATE INDEX itemAttachments_sourceItemID ON itemAttachments(sourceItemID);
CREATE INDEX itemAttachments_mimeType ON itemAttachments(mimeType);
CREATE INDEX itemAttachments_syncState ON itemAttachments(syncState);
-- Individual entries for each tag
CREATE TABLE tags (
@ -202,6 +205,12 @@ CREATE TABLE syncDeleteLog (
);
CREATE INDEX syncDeleteLog_timestamp ON syncDeleteLog(timestamp);
CREATE TABLE storageDeleteLog (
key TEXT PRIMARY KEY,
timestamp INT NOT NULL
);
CREATE INDEX storageDeleteLog_timestamp ON storageDeleteLog(timestamp);
CREATE TABLE translators (
translatorID TEXT PRIMARY KEY,
minVersion TEXT,