commit 884e5474fec96324026e5e8409cb4ce664587004
parent a611706f869cf1458aa21486cf266cb4f28d0816
Author: Dan Stillman <dstillman@zotero.org>
Date: Sun, 13 Sep 2009 07:23:29 +0000
Zotero File Storage megacommit
- Group file sync via Zotero File Storage
- Split file syncing into separate modules for ZFS and WebDAV
- Dragging items between libraries copies child notes, snapshots/files, and links based on checkboxes for each (enabled by default) in the Zotero preferences
- Sync errors now trigger an exclamation/error icon separate from the sync icon, with a popup window displaying the error and an option to report it
- Various errors that could cause perpetual sync icon spinning now stop the sync properly
- Zotero.Utilities.md5(str) is now md5(strOrFile, base64)
- doPost(), doHead(), and retrieveSource() now takes a headers parameter instead of requestContentType
- doHead() can now accept an nsIURI (with login credentials), is a background request, and isn't cached
- When library access or file writing access is denied during sync, display a warning and then reset local group to server version
- Perform additional steps (e.g., removing local groups) when switching sync users to prevent errors
- Compare hash as well as mod time when checking for modified local files
- Don't trigger notifications when removing groups from the client
- Clear relation links to items in removed groups
- Zotero.Item.attachmentHash property to get file MD5
- importFromFile() now takes libraryID as a third parameter
- Zotero.Attachments.getNumFiles() returns the number of files in the attachment directory
- Zotero.Attachments.copyAttachmentToLibrary() copies an attachment item, including files, to another library
- Removed Zotero.File.getFileHash() in favor of updated Zotero.Utilities.md5()
- Zotero.File.copyDirectory(dir, newDir) copies all files from dir into newDir
- Preferences shuffling: OpenURL to Advanced, import/export character set options to Export, "Include URLs of paper articles in references" to Styles
- Other stuff I don't remember
Suffice it to say, this could use testing.
Diffstat:
31 files changed, 4260 insertions(+), 1996 deletions(-)
diff --git a/chrome/content/zotero/overlay.js b/chrome/content/zotero/overlay.js
@@ -177,11 +177,25 @@ var ZoteroPane = new function()
);
if (index == 0) {
- Zotero.Sync.Server.sync(function () {
- pr.alert(
- "Restore Completed",
- "The local Zotero database has been successfully restored."
- );
+ Zotero.Sync.Server.sync({
+ onSuccess: function () {
+ Zotero.Sync.Runner.setSyncIcon();
+
+ pr.alert(
+ "Restore Completed",
+ "The local Zotero database has been successfully restored."
+ );
+ },
+
+ onError: function (msg) {
+ pr.alert(
+ "Restore Failed",
+ "An error occurred while restoring from the server:\n\n"
+ + msg
+ );
+
+ Zotero.Sync.Runner.error(msg);
+ }
});
}
}, 1000);
@@ -898,7 +912,7 @@ var ZoteroPane = new function()
if (!tagSelector.getAttribute('collapsed') ||
tagSelector.getAttribute('collapsed') == 'false') {
Zotero.debug('Updating tag selector with current tags');
- if (itemGroup.isEditable()) {
+ if (itemGroup.editable) {
tagSelector.mode = 'edit';
}
else {
@@ -2281,7 +2295,7 @@ var ZoteroPane = new function()
var disabled = Zotero.locked;
if (!disabled && self.collectionsView.selection && self.collectionsView.selection.count) {
var itemGroup = self.collectionsView._getItemAtRow(self.collectionsView.selection.currentIndex);
- disabled = !itemGroup.isEditable()
+ disabled = !itemGroup.editable;
}
for each(var menuitem in menu.firstChild.childNodes) {
menuitem.disabled = disabled;
@@ -2837,7 +2851,26 @@ var ZoteroPane = new function()
}
var itemGroup = this.collectionsView._getItemAtRow(row);
- return itemGroup.isEditable();
+ return itemGroup.editable;
+ }
+
+
+ /**
+ * Test if the user can edit the currently selected library/collection,
+ * and display an error if not
+ *
+ * @param {Integer} [row]
+ *
+ * @return {Boolean} TRUE if user can edit, FALSE if not
+ */
+ this.canEditFiles = function (row) {
+ // Currently selected row
+ if (row === undefined) {
+ row = this.collectionsView.selection.currentIndex;
+ }
+
+ var itemGroup = this.collectionsView._getItemAtRow(row);
+ return itemGroup.filesEditable;
}
@@ -2999,12 +3032,6 @@ var ZoteroPane = new function()
this.setLastSyncStatus = function (tooltip) {
var label = tooltip.firstChild.nextSibling;
- var msg = Zotero.Sync.Runner.lastSyncError;
- if (msg) {
- label.value = 'Last error: ' + msg; // TODO: localize
- return;
- }
-
var lastSyncTime = Zotero.Sync.Server.lastLocalSyncTime;
// TODO: localize
msg = 'Last sync: ';
diff --git a/chrome/content/zotero/overlay.xul b/chrome/content/zotero/overlay.xul
@@ -164,8 +164,8 @@
<menuitem hidden="true" label=" Reset Client" oncommand="Zotero.Sync.Server.resetClient()"/>
<menuitem label="Storage Debugging" disabled="true"/>
<menuitem hidden="true" 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); })"/>
+ <menuitem label=" Purge Deleted Storage Files" oncommand="Zotero.Sync.Storage.purgeDeletedStorageFiles('webdav', function(results) { Zotero.debug(results); })"/>
+ <menuitem label=" Purge Orphaned Storage Files" oncommand="Zotero.Sync.Storage.purgeOrphanedStorageFiles('webdav', function(results) { Zotero.debug(results); })"/>
<menuseparator id="zotero-tb-actions-separator"/>
<menuitem id="zotero-tb-actions-prefs" label="&zotero.toolbar.preferences.label;"
oncommand="ZoteroPane.openPreferences()"/>
@@ -375,6 +375,7 @@
</grid>
</tooltip>
</hbox>
+ <toolbarbutton id="zotero-tb-sync-warning" hidden="true"/>
<toolbarbutton id="zotero-tb-sync" class="zotero-tb-button" tooltip="_child"
oncommand="Zotero.Sync.Server.canAutoResetClient = true; Zotero.Sync.Runner.sync()">
<tooltip
diff --git a/chrome/content/zotero/preferences/preferences.js b/chrome/content/zotero/preferences/preferences.js
@@ -42,7 +42,7 @@ function init()
rows[i].firstChild.nextSibling.value = Zotero.isMac ? 'Cmd+Shift+' : 'Ctrl+Alt+';
}
- updateStorageSettings();
+ updateStorageSettings(null, null, true);
refreshStylesList();
refreshProxyList();
populateQuickCopyList();
@@ -171,43 +171,85 @@ function populateOpenURLResolvers() {
//
// Sync
//
-/*
-function updateSyncStatus() {
- var disabled = !Zotero.Sync.Server.enabled;
+function updateStorageSettings(enabled, protocol, skipWarnings) {
+ if (enabled === null) {
+ enabled = document.getElementById('pref-storage-enabled').value;
+ }
- var radioGroup = document.getElementById('zotero-reset').firstChild;
- radioGroup.disabled = disabled;
- var labels = radioGroup.getElementsByTagName('label');
- for each(var label in labels) {
- label.disabled = disabled;
+ var oldProtocol = document.getElementById('pref-storage-protocol').value;
+ if (protocol === null) {
+ protocol = oldProtocol;
}
- var labels = radioGroup.getElementsByTagName('description');
- for each(var label in labels) {
- label.disabled = disabled;
+
+ var protocolMenu = document.getElementById('storage-protocol');
+ var settings = document.getElementById('storage-webdav-settings');
+ var sep = document.getElementById('storage-separator');
+
+ if (!enabled || protocol == 'zotero') {
+ settings.hidden = true;
+ sep.hidden = false;
}
- document.getElementById('zotero-reset-button').disabled = disabled;
-}
-*/
-
-function updateStorageSettings(value) {
- if (!value) {
- value = document.getElementById('pref-storage-protocol').value;
+ else {
+ settings.hidden = false;
+ sep.hidden = true;
}
- var prefix = document.getElementById('storage-url-prefix');
- switch (value) {
- case 'webdav':
- prefix.value = 'http://';
- break;
-
- case 'webdavs':
- prefix.value = 'https://';
- break;
+
+ protocolMenu.disabled = !enabled;
+
+ if (!skipWarnings) {
+ // WARN if going between
+ }
+
+ if (oldProtocol == 'zotero' && protocol == 'webdav') {
+ var sql = "SELECT COUNT(*) FROM version WHERE schema='storage_zfs'";
+ if (Zotero.DB.valueQuery(sql)) {
+ var pr = Components.classes["@mozilla.org/network/default-prompt;1"]
+ .getService(Components.interfaces.nsIPrompt);
+ var buttonFlags = (pr.BUTTON_POS_0) * (pr.BUTTON_TITLE_IS_STRING)
+ + (pr.BUTTON_POS_1) * (pr.BUTTON_TITLE_IS_STRING)
+ + pr.BUTTON_DELAY_ENABLE;
+ var account = Zotero.Sync.Server.username;
+ var index = pr.confirmEx(
+ // TODO: localize
+ "Purge Attachment Files on Zotero Servers?",
+
+ "If you plan to use WebDAV for file syncing and you previously synced attachment files in My Library "
+ + "to the Zotero servers, you can purge those files from the Zotero servers to give you more "
+ + "storage space for groups.\n\n"
+ + "You can purge files at any time from your account settings on zotero.org.",
+ buttonFlags,
+ "Purge Files Now",
+ "Do Not Purge", null, null, {}
+ );
+
+ if (index == 0) {
+ var sql = "INSERT OR IGNORE INTO settings VALUES (?,?,?)";
+ Zotero.DB.query(sql, ['storage', 'zfsPurge', 'user']);
+
+ Zotero.Sync.Storage.purgeDeletedStorageFiles('zfs', function (success) {
+ if (success) {
+ pr.alert(
+ Zotero.getString("general.success"),
+ "Attachment files from your personal library have been removed from the Zotero servers."
+ );
+ }
+ else {
+ pr.alert(
+ Zotero.getString("general.error"),
+ "An error occurred. Please try again later."
+ );
+ }
+ });
+ }
+ }
}
}
+
+
function unverifyStorageServer() {
- Zotero.Sync.Storage.clearSettingsCache();
Zotero.Prefs.set('sync.storage.verified', false);
+ Zotero.Sync.Storage.resetAllSyncStates(null, true, false);
}
function verifyStorageServer() {
@@ -220,7 +262,7 @@ function verifyStorageServer() {
var usernameField = document.getElementById("storage-username");
var passwordField = document.getElementById("storage-password");
- var callback = function (uri, status, authRequired) {
+ var callback = function (uri, status) {
verifyButton.hidden = false;
abortButton.hidden = true;
progressMeter.hidden = true;
@@ -245,13 +287,13 @@ function verifyStorageServer() {
break;
}
- Zotero.Sync.Storage.checkServerCallback(uri, status, authRequired, window);
+ Zotero.Sync.Storage.checkServerCallback(uri, status, window);
}
verifyButton.hidden = true;
abortButton.hidden = false;
progressMeter.hidden = false;
- var requestHolder = Zotero.Sync.Storage.checkServer(callback);
+ var requestHolder = Zotero.Sync.Storage.checkServer('webdav', callback);
abortButton.onclick = function () {
if (requestHolder.request) {
requestHolder.request.onreadystatechange = undefined;
diff --git a/chrome/content/zotero/preferences/preferences.xul b/chrome/content/zotero/preferences/preferences.xul
@@ -38,7 +38,8 @@ To add a new preference:
-->
<prefwindow id="zotero-prefs" title="&zotero.preferences.title;" onload="moveToAlertPosition(); init()" onunload="Zotero_Preferences.onUnload()"
- windowtype="zotero:pref" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ windowtype="zotero:pref" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ style="min-height: 600px">
<prefpane id="zotero-prefpane-general"
label="&zotero.preferences.prefpane.general;"
@@ -54,8 +55,10 @@ To add a new preference:
<preference id="pref-automaticSnapshots" name="extensions.zotero.automaticSnapshots" type="bool"/>
<preference id="pref-downloadAssociatedFiles" name="extensions.zotero.downloadAssociatedFiles" type="bool"/>
<preference id="pref-automaticTags" name="extensions.zotero.automaticTags" type="bool"/>
- <preference id="pref-openURL-resolver" name="extensions.zotero.openURL.resolver" type="string"/>
- <preference id="pref-openURL-version" name="extensions.zotero.openURL.version" type="string"/>
+
+ <preference id="pref-groups-copyChildNotes" name="extensions.zotero.groups.copyChildNotes" type="bool"/>
+ <preference id="pref-groups-copyChildFileAttachments" name="extensions.zotero.groups.copyChildFileAttachments" type="bool"/>
+ <preference id="pref-groups-copyChildLinks" name="extensions.zotero.groups.copyChildLinks" type="bool"/>
</preferences>
<groupbox>
@@ -138,31 +141,18 @@ To add a new preference:
</groupbox>
<groupbox>
- <caption label="&zotero.preferences.openurl.caption;"/>
+ <caption label="Groups"/>
- <button id="openURLSearchButton" label="&zotero.preferences.openurl.search;" oncommand="populateOpenURLResolvers()"/>
- <menulist id="openURLMenu" oncommand="onOpenURLSelected();">
- <menupopup>
- <menuseparator/>
- <menuitem label="&zotero.preferences.openurl.custom;" value="custom" selected="true"/>
- </menupopup>
- </menulist>
-
- <hbox align="center">
- <label value="&zotero.preferences.openurl.server;"/>
- <textbox id="openURLServerField" flex="1" oninput="onOpenURLCustomized();" preference="pref-openURL-resolver"/>
- </hbox>
-
- <hbox align="center">
- <label value="&zotero.preferences.openurl.version;" control="openURLVersionMenu"/>
- <menulist id="openURLVersionMenu" oncommand="onOpenURLCustomized();" preference="pref-openURL-version">
- <menupopup>
- <menuitem label="0.1" value="0.1"/>
- <menuitem label="1.0" value="1.0"/>
- </menupopup>
- </menulist>
- </hbox>
+ <!-- TODO: localize -->
+ <label value="When copying items between libraries, include:"/>
+ <vbox style="margin-left: 2em">
+ <checkbox label="child notes" preference="pref-groups-copyChildNotes"/>
+ <checkbox label="child snapshots and imported files" preference="pref-groups-copyChildFileAttachments"/>
+ <checkbox label="child links" preference="pref-groups-copyChildLinks"/>
+ </vbox>
</groupbox>
+
+ <separator/>
</prefpane>
@@ -174,10 +164,12 @@ To add a new preference:
<preferences>
<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-protocol" name="extensions.zotero.sync.storage.protocol" type="string"/>
<preference id="pref-storage-enabled" name="extensions.zotero.sync.storage.enabled" type="bool"/>
+ <preference id="pref-storage-protocol" name="extensions.zotero.sync.storage.protocol" type="string" onchange="unverifyStorageServer()"/>
+ <preference id="pref-storage-scheme" name="extensions.zotero.sync.storage.scheme" type="string"/>
<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"/>
+ <preference id="pref-group-storage-enabled" name="extensions.zotero.sync.storage.groups.enabled" type="bool"/>
</preferences>
<!-- This doesn't wrap without an explicit width, for some reason -->
@@ -250,26 +242,26 @@ To add a new preference:
</groupbox>
+ <!-- TODO: localize -->
<groupbox>
- <caption label="Storage Server"/>
+ <caption label="File Syncing"/>
- <hbox>
- <checkbox label="Enable file syncing" preference="pref-storage-enabled"/>
- </hbox>
-
- <separator class="thin"/>
- <hbox align="center">
- <label value="A" style="margin-right: 0"/>
- <label value="WebDAV server" class="text-link" href="http://zotero.org/support/sync#file_syncing" style="margin-left: .25em; margin-right: .25em"/>
- <label value="is currently required to sync attachment files." style="margin-left: 0"/>
+ <!-- My Library -->
+ <hbox style="margin: 0">
+ <checkbox label="Sync attachment files in My Library using" preference="pref-storage-enabled" oncommand="updateStorageSettings(this.checked, null)"/>
+ <menulist id="storage-protocol" style="margin-left: .5em" preference="pref-storage-protocol" oncommand="updateStorageSettings(null, this.value)">
+ <menupopup>
+ <menuitem label="Zotero" value="zotero"/>
+ <menuitem label="WebDAV" value="webdav"/>
+ </menupopup>
+ </menulist>
</hbox>
- <separator class="thin"/>
- <label value="Syncing of attachment files in group libraries is not currently supported."/>
-
- <separator/>
+ <stack id="storage-webdav-settings" style="margin-top: .5em; margin-bottom: .8em; border: 1px gray solid; -moz-border-radius: 3px">
+ <!-- Background shading -->
+ <box style="background: black; opacity:.03"/>
- <grid id="storage-settings">
+ <grid style="padding: .7em .4em .7em 0">
<columns>
<column/>
<column flex="1"/>
@@ -277,23 +269,17 @@ To add a new preference:
<rows>
<row>
- <label value="Protocol:"/>
+ <label value="URL:"/>
<hbox>
- <menulist id="storage-url-protocol"
- preference="pref-storage-protocol"
- onsynctopreference="updateStorageSettings(this.value); unverifyStorageServer();">
+ <menulist id="storage-url-prefix"
+ preference="pref-storage-scheme"
+ onsynctopreference="unverifyStorageServer()" style="padding: 0; width: 7em">
<menupopup>
- <menuitem label="WebDAV" value="webdav"/>
- <!-- TODO: localize -->
- <menuitem label="WebDAV (Secure)" value="webdavs"/>
+ <menuitem label="http" value="http" style="padding: 0"/>
+ <menuitem label="https" value="https" style="padding: 0"/>
</menupopup>
</menulist>
- </hbox>
- </row>
- <row>
- <label value="URL:"/>
- <hbox>
- <label id="storage-url-prefix"/>
+ <label value="://"/>
<textbox id="storage-url" flex="1"
preference="pref-storage-url"
onkeypress="if (Zotero.isMac && event.keyCode == 13) { this.blur(); verifyStorageServer(); }"
@@ -309,7 +295,7 @@ To add a new preference:
preference="pref-storage-username"
onkeypress="if (Zotero.isMac && 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; }"/>
+ onchange="var pass = document.getElementById('storage-password'); if (pass.value) { Zotero.Sync.Storage.Session.WebDAV.prototype.password = pass.value; }"/>
</hbox>
</row>
<row>
@@ -318,7 +304,7 @@ To add a new preference:
<textbox id="storage-password" flex="0" type="password"
onkeypress="if (Zotero.isMac && event.keyCode == 13) { this.blur(); setTimeout('verifyStorageServer();', 1); }"
oninput="unverifyStorageServer()"
- onchange="Zotero.Sync.Storage.password = this.value"/>
+ onchange="Zotero.Sync.Storage.Session.WebDAV.prototype.password = this.value"/>
</hbox>
</row>
<row>
@@ -333,6 +319,20 @@ To add a new preference:
</row>
</rows>
</grid>
+
+ </stack>
+
+ <separator id="storage-separator" class="thin"/>
+
+ <!-- Group Libraries -->
+ <checkbox label="Sync attachment files in group libraries using Zotero File Storage"
+ preference="pref-group-storage-enabled"/>
+
+ <separator class="thin"/>
+
+ <hbox style="margin-top:.3em">
+ <label class="text-link" style="margin-left: 0" value="About File Syncing" href="http://zotero.org/support/file_sync"/>
+ </hbox>
</groupbox>
</tabpanel>
@@ -377,7 +377,7 @@ To add a new preference:
</groupbox>
<groupbox>
- <caption label="Storage Server"/>
+ <caption label="File Syncing"/>
<grid>
<columns>
@@ -404,6 +404,8 @@ To add a new preference:
</tabpanel>
</tabpanels>
</tabbox>
+
+ <separator/>
</prefpane>
@@ -492,6 +494,8 @@ To add a new preference:
</rows>
</grid>
</groupbox>
+
+ <separator/>
</prefpane>
@@ -499,22 +503,11 @@ To add a new preference:
label="&zotero.preferences.prefpane.export;"
image="chrome://zotero/skin/prefs-export.png">
<preferences>
- <preference id="pref-export-citePaperJournalArticleURL" name="extensions.zotero.export.citePaperJournalArticleURL" type="bool"/>
<preference id="pref-quickCopy-setting" name="extensions.zotero.export.quickCopy.setting" type="string"/>
<preference id="pref-quickCopy-dragLimit" name="extensions.zotero.export.quickCopy.dragLimit" type="int"/>
</preferences>
<groupbox>
- <caption label="&zotero.preferences.citationOptions.caption;"/>
-
- <checkbox label="&zotero.preferences.export.citePaperJournalArticleURL;" preference="pref-export-citePaperJournalArticleURL"/>
- <!-- This doesn't wrap without an explicit width, for some reason -->
- <label id="export-citePaperJournalArticleURL" width="45em">
- &zotero.preferences.export.citePaperJournalArticleURL.description;
- </label>
- </groupbox>
-
- <groupbox>
<caption label="&zotero.preferences.quickCopy.caption;"/>
<label id="quickCopy-instructions"/>
@@ -548,23 +541,38 @@ To add a new preference:
<button label="+" onclick="showQuickCopySiteEditor()"/>
</hbox>
- <separator/>
-
<!-- TODO: localize -->
<hbox align="center">
<label value="Disable Quick Copy when dragging more than"/>
<textbox preference="pref-quickCopy-dragLimit" size="3"/>
<label value="items" flex="1"/>
</hbox>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&zotero.preferences.charset;"/>
- <separator/>
+ <checkbox id="zotero-export-displayCharsetOption" label="&zotero.preferences.charset.displayExportOption;"
+ preference="pref-export-displayCharsetOption"/>
+
+ <hbox align="center">
+ <label value="&zotero.preferences.charset.importCharset;:" control="zotero-import-charsetMenu"/>
+ <menulist id="zotero-import-charsetMenu" preference="pref-import-charset"/>
+ </hbox>
</groupbox>
+
+ <separator/>
</prefpane>
<prefpane id="zotero-prefpane-styles"
label="&zotero.preferences.prefpane.styles;"
image="chrome://zotero/skin/prefs-styles.png">
+
+ <preferences>
+ <preference id="pref-styles-citePaperJournalArticleURL" name="extensions.zotero.export.citePaperJournalArticleURL" type="bool"/>
+ </preferences>
+
<groupbox flex="1">
<caption label="&zotero.preferences.styles.styleManager;"/>
@@ -579,13 +587,24 @@ To add a new preference:
<treechildren id="styleManager-rows"/>
</tree>
<separator class="thin"/>
- <hbox pack="end">
+ <hbox align="center" flex="1">
+ <label class="text-link" href="http://www.zotero.org/styles/" value="&zotero.preferences.export.getAdditionalStyles;" flex="1"/>
<button disabled="true" id="styleManager-delete" label="-" onclick="deleteStyle()"/>
<button label="+" onclick="addStyle()"/>
</hbox>
- <separator/>
- <label class="text-link" href="http://www.zotero.org/styles/" value="&zotero.preferences.export.getAdditionalStyles;"/>
</groupbox>
+
+ <groupbox>
+ <caption label="&zotero.preferences.citationOptions.caption;"/>
+
+ <checkbox label="&zotero.preferences.export.citePaperJournalArticleURL;" preference="pref-styles-citePaperJournalArticleURL"/>
+ <!-- This doesn't wrap without an explicit width, for some reason -->
+ <label id="export-citePaperJournalArticleURL" width="45em">
+ &zotero.preferences.export.citePaperJournalArticleURL.description;
+ </label>
+ </groupbox>
+
+ <separator/>
</prefpane>
@@ -621,6 +640,8 @@ To add a new preference:
<button label="+" onclick="showProxyEditor()"/>
</hbox>
</groupbox>
+
+ <separator/>
</prefpane>
@@ -714,6 +735,8 @@ To add a new preference:
<checkbox label="&zotero.preferences.keys.overrideGlobal;" preference="pref-keys-overrideGlobal"/>
<label class="statusLine" value="&zotero.preferences.keys.changesTakeEffect;"/>
+
+ <separator/>
</prefpane>
@@ -726,6 +749,8 @@ To add a new preference:
<preference id="pref-export-displayCharsetOption" name="extensions.zotero.export.displayCharsetOption" type="bool"/>
<preference id="pref-debug-output-enableAfterRestart" name="extensions.zotero.debug.store" type="bool"/>
<preference id="pref-import-charset" name="extensions.zotero.import.charset" type="string"/>
+ <preference id="pref-openURL-resolver" name="extensions.zotero.openURL.resolver" type="string"/>
+ <preference id="pref-openURL-version" name="extensions.zotero.openURL.version" type="string"/>
</preferences>
<groupbox>
@@ -750,9 +775,6 @@ To add a new preference:
<hbox>
<button label="&zotero.preferences.dbMaintenance.integrityCheck;" oncommand="runIntegrityCheck()"/>
- </hbox>
-
- <hbox>
<button label="&zotero.preferences.dbMaintenance.resetTranslatorsAndStyles;" oncommand="resetTranslatorsAndStyles()"/>
</hbox>
</groupbox>
@@ -782,16 +804,38 @@ To add a new preference:
</groupbox>
<groupbox>
- <caption label="&zotero.preferences.charset;"/>
+ <caption label="&zotero.preferences.openurl.caption;"/>
- <checkbox id="zotero-export-displayCharsetOption" label="&zotero.preferences.charset.displayExportOption;"
- preference="pref-export-displayCharsetOption"/>
+ <hbox align="center">
+ <!-- vbox prevents some weird vertical stretching of the menulist -->
+ <vbox flex="1">
+ <menulist id="openURLMenu" oncommand="onOpenURLSelected();">
+ <menupopup>
+ <menuseparator/>
+ <menuitem label="&zotero.preferences.openurl.custom;" value="custom" selected="true"/>
+ </menupopup>
+ </menulist>
+ </vbox>
+ <button id="openURLSearchButton" label="&zotero.preferences.openurl.search;" oncommand="populateOpenURLResolvers()"/>
+ </hbox>
<hbox align="center">
- <label value="&zotero.preferences.charset.importCharset;:" control="zotero-import-charsetMenu"/>
- <menulist id="zotero-import-charsetMenu" preference="pref-import-charset"/>
+ <label value="&zotero.preferences.openurl.server;"/>
+ <textbox id="openURLServerField" flex="1" oninput="onOpenURLCustomized();" preference="pref-openURL-resolver"/>
+ </hbox>
+
+ <hbox align="center">
+ <label value="&zotero.preferences.openurl.version;" control="openURLVersionMenu"/>
+ <menulist id="openURLVersionMenu" oncommand="onOpenURLCustomized();" preference="pref-openURL-version">
+ <menupopup>
+ <menuitem label="0.1" value="0.1"/>
+ <menuitem label="1.0" value="1.0"/>
+ </menupopup>
+ </menulist>
</hbox>
</groupbox>
+
+ <separator/>
</prefpane>
<!-- These mess up the prefwindow (more) if they come before the prefpanes
diff --git a/chrome/content/zotero/xpcom/attachments.js b/chrome/content/zotero/xpcom/attachments.js
@@ -43,7 +43,7 @@ Zotero.Attachments = new function(){
var self = this;
- function importFromFile(file, sourceItemID){
+ function importFromFile(file, sourceItemID, libraryID) {
Zotero.debug('Importing attachment from file');
var title = file.leafName;
@@ -61,6 +61,9 @@ Zotero.Attachments = new function(){
var parentItem = Zotero.Items.get(sourceItemID);
attachmentItem.libraryID = parentItem.libraryID;
}
+ else if (libraryID) {
+ attachmentItem.libraryID = libraryID;
+ }
attachmentItem.setField('title', title);
attachmentItem.setSource(sourceItemID);
attachmentItem.attachmentLinkMode = this.LINK_MODE_IMPORTED_FILE;
@@ -980,6 +983,54 @@ Zotero.Attachments = new function(){
/**
+ * Returns the number of files in the attachment directory
+ *
+ * Only counts if MIME type is text/html
+ *
+ * @param {Zotero.Item} item Attachment item
+ */
+ this.getNumFiles = function (item) {
+ var funcName = "Zotero.Attachments.getNumFiles()";
+
+ 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:
+ break;
+
+ default:
+ throw ("Invalid attachment link mode in " + funcName);
+ }
+
+ if (item.attachmentMIMEType != 'text/html') {
+ return 1;
+ }
+
+ var file = item.getFile();
+ if (!file) {
+ throw ("File not found in " + funcName);
+ }
+
+ var numFiles = 0;
+ var parentDir = file.parent;
+ var files = parentDir.directoryEntries;
+ while (files.hasMoreElements()) {
+ file = files.getNext();
+ file.QueryInterface(Components.interfaces.nsIFile);
+ if (file.leafName.indexOf('.') == 0) {
+ continue;
+ }
+ numFiles++;
+ }
+ return numFiles;
+ }
+
+
+ /**
* @param {Zotero.Item} item
* @param {Boolean} [skipHidden=FALSE] Don't count hidden files
* @return {Integer} Total file size in bytes
@@ -1026,6 +1077,45 @@ Zotero.Attachments = new function(){
}
+ /**
+ * Copy attachment item, including files, to another library
+ */
+ this.copyAttachmentToLibrary = function (attachment, libraryID, sourceItemID) {
+ var linkMode = attachment.attachmentLinkMode;
+
+ if (attachment.libraryID == libraryID) {
+ throw ("Attachment is already in library " + libraryID);
+ }
+
+ var newAttachment = new Zotero.Item('attachment');
+ newAttachment.libraryID = libraryID;
+ // Link mode needs to be set when saving new attachment
+ newAttachment.attachmentLinkMode = linkMode;
+ if (attachment.isImportedAttachment()) {
+ // Attachment path isn't copied over by clone() if libraryID is different
+ newAttachment.attachmentPath = attachment.attachmentPath;
+ }
+ // DEBUG: save here because clone() doesn't currently work on unsaved tagged items
+ var id = newAttachment.save();
+ newAttachment = Zotero.Items.get(id);
+ attachment.clone(false, newAttachment);
+ if (sourceItemID) {
+ newAttachment.setSource(sourceItemID);
+ }
+ newAttachment.save();
+
+ // Copy over files
+ if (newAttachment.isImportedAttachment()) {
+ var dir = Zotero.Attachments.getStorageDirectory(attachment.id);
+ var newDir = Zotero.Attachments.createDirectoryForItem(newAttachment.id);
+ Zotero.File.copyDirectory(dir, newDir);
+ }
+
+ attachment.addLinkedItem(newAttachment);
+ return newAttachment.id;
+ }
+
+
function _getFileNameFromURL(url, mimeType){
var nsIURL = Components.classes["@mozilla.org/network/standard-url;1"]
.createInstance(Components.interfaces.nsIURL);
diff --git a/chrome/content/zotero/xpcom/collectionTreeView.js b/chrome/content/zotero/xpcom/collectionTreeView.js
@@ -326,9 +326,14 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids)
break;
case 'group':
- if (this._groupRowMap[ids[i]] != null) {
- rows.push(this._groupRowMap[ids[i]]);
- }
+ //if (this._groupRowMap[ids[i]] != null) {
+ // rows.push(this._groupRowMap[ids[i]]);
+ //}
+
+ // For now, just reload if a group is removed, since otherwise
+ // we'd have to remove collections too
+ this.reload();
+ this.rememberSelection(savedSelection);
break;
}
}
@@ -635,7 +640,7 @@ Zotero.CollectionTreeView.prototype.isSelectable = function (row, col) {
Zotero.CollectionTreeView.prototype.__defineGetter__('editable', function () {
- return this._getItemAtRow(this.selection.currentIndex).isEditable();
+ return this._getItemAtRow(this.selection.currentIndex).editable;
});
@@ -1029,16 +1034,15 @@ Zotero.CollectionTreeView.prototype.canDrop = function(row, orient, dragData)
{
var itemGroup = this._getItemAtRow(row); //the collection we are dragging over
- if (!itemGroup.isEditable()) {
+ if (!itemGroup.editable) {
return false;
}
if (dataType == 'zotero/item') {
-
if(itemGroup.isBucket()) {
return true;
}
-
+
var ids = data;
var items = Zotero.Items.get(ids);
var skip = true;
@@ -1048,10 +1052,13 @@ Zotero.CollectionTreeView.prototype.canDrop = function(row, orient, dragData)
return false
}
- // TODO: for now, only allow regular items to be dragged to groups
- if (itemGroup.isWithinGroup() && itemGroup.ref.libraryID != item.libraryID
- && !item.isRegularItem()) {
- return false;
+ if (itemGroup.isWithinGroup() && item.isAttachment()) {
+ // Linked files can't be added to groups
+ if (item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
+ return false;
+ }
+ skip = false;
+ continue;
}
// TODO: for now, skip items that are already linked
@@ -1070,7 +1077,7 @@ Zotero.CollectionTreeView.prototype.canDrop = function(row, orient, dragData)
continue;
}
- // Allow drag of group items to library
+ // Allow drag of group items to personal library
if (item.libraryID && (itemGroup.isLibrary()
|| itemGroup.isCollection() && !itemGroup.isWithinGroup())) {
// TODO: for now, skip items that are already linked
@@ -1106,10 +1113,17 @@ Zotero.CollectionTreeView.prototype.canDrop = function(row, orient, dragData)
if (itemGroup.isSearch()) {
return false;
}
- // Don't allow folder drag
- if (dataType == 'application/x-moz-file' && data[0].isDirectory()) {
- return false;
+ if (dataType == 'application/x-moz-file') {
+ // Don't allow folder drag
+ if (data[0].isDirectory()) {
+ return false;
+ }
+ // Don't allow drop if no permissions
+ if (!itemGroup.filesEditable) {
+ return false;
+ }
}
+
return true;
}
else if (dataType == 'zotero/collection') {
@@ -1179,7 +1193,7 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient)
else {
var targetLibraryID = null;
}
-
+
if(itemGroup.isBucket()) {
itemGroup.ref.uploadItems(ids);
return;
@@ -1263,12 +1277,19 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient)
*/
}
+ // Standalone attachment
+ if (item.isAttachment()) {
+ var id = Zotero.Attachments.copyAttachmentToLibrary(item, targetLibraryID);
+ newIDs.push(id);
+ continue;
+ }
+
// Create new unsaved clone item in target library
var newItem = new Zotero.Item(item.itemTypeID);
newItem.libraryID = targetLibraryID;
// DEBUG: save here because clone() doesn't currently work on unsaved tagged items
var id = newItem.save();
- var newItem = Zotero.Items.get(id);
+ newItem = Zotero.Items.get(id);
item.clone(false, newItem);
newItem.save();
//var id = newItem.save();
@@ -1277,6 +1298,62 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient)
// Record link
item.addLinkedItem(newItem);
newIDs.push(id);
+
+ if (item.isNote()) {
+ continue;
+ }
+
+ // For regular items, add child items if prefs and permissions allow
+
+ // Child notes
+ if (Zotero.Prefs.get('groups.copyChildNotes')) {
+ var noteIDs = item.getNotes();
+ var notes = Zotero.Items.get(noteIDs);
+ for each(var note in notes) {
+ var newNote = new Zotero.Item('note');
+ newNote.libraryID = targetLibraryID;
+ // DEBUG: save here because clone() doesn't currently work on unsaved tagged items
+ var id = newNote.save();
+ newNote = Zotero.Items.get(id);
+ note.clone(false, newNote);
+ newNote.setSource(newItem.id);
+ newNote.save();
+
+ note.addLinkedItem(newNote);
+ }
+ }
+
+ // Child attachments
+ var copyChildLinks = Zotero.Prefs.get('groups.copyChildLinks');
+ var copyChildFileAttachments = Zotero.Prefs.get('groups.copyChildFileAttachments');
+ if (copyChildLinks || copyChildFileAttachments) {
+ var attachmentIDs = item.getAttachments();
+ var attachments = Zotero.Items.get(attachmentIDs);
+ for each(var attachment in attachments) {
+ var linkMode = attachment.attachmentLinkMode;
+
+ // Skip linked files
+ if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
+ continue;
+ }
+
+ // Skip imported files if we don't have pref and permissions
+ if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
+ if (!copyChildLinks) {
+ Zotero.debug("Skipping child link attachment on drag");
+ continue;
+ }
+ }
+ else {
+ if (!copyChildFileAttachments || !itemGroup.filesEditable) {
+ Zotero.debug("Skipping child file attachment on drag");
+ continue;
+ }
+ }
+
+ var id = Zotero.Attachments.copyAttachmentToLibrary(attachment, targetLibraryID, newItem.id);
+ }
+ }
}
if (toReconcile.length) {
@@ -1342,12 +1419,11 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient)
return;
}
else if (dataType == 'text/x-moz-url' || dataType == 'application/x-moz-file') {
- // FIXME: temporarily disable dragging in of files
- if (dataType == 'application/x-moz-file' && itemGroup.isWithinGroup()) {
- var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
- .getService(Components.interfaces.nsIPromptService);
- ps.alert(null, "", "Files cannot currently be added to group libraries.");
- return;
+ if (itemGroup.isWithinGroup()) {
+ var targetLibraryID = itemGroup.ref.libraryID;
+ }
+ else {
+ var targetLibraryID = null;
}
if (itemGroup.isCollection()) {
@@ -1398,7 +1474,7 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient)
try {
Zotero.DB.beginTransaction();
- var itemID = Zotero.Attachments.importFromFile(file, false);
+ var itemID = Zotero.Attachments.importFromFile(file, false, targetLibraryID);
if (parentCollectionID) {
var col = Zotero.Collections.get(parentCollectionID);
if (col) {
@@ -1545,11 +1621,10 @@ Zotero.ItemGroup.prototype.isWithinGroup = function () {
return this.ref && !!this.ref.libraryID;
}
-Zotero.ItemGroup.prototype.isEditable = function () {
+Zotero.ItemGroup.prototype.__defineGetter__('editable', function () {
if (this.isTrash() || this.isShare()) {
return false;
}
-
if (!this.isWithinGroup()) {
return true;
}
@@ -1564,12 +1639,33 @@ Zotero.ItemGroup.prototype.isEditable = function () {
var group = Zotero.Groups.get(groupID);
return group.editable;
}
- else {
- throw ("Unknown library type '" + type + "' in Zotero.ItemGroup.isEditable()");
- }
+ throw ("Unknown library type '" + type + "' in Zotero.ItemGroup.editable");
}
-}
+ return false;
+});
+Zotero.ItemGroup.prototype.__defineGetter__('filesEditable', function () {
+ if (this.isTrash() || this.isShare()) {
+ return false;
+ }
+ if (!this.isWithinGroup()) {
+ return true;
+ }
+ var libraryID = this.ref.libraryID;
+ if (this.isGroup()) {
+ return this.ref.filesEditable;
+ }
+ if (this.isCollection()) {
+ var type = Zotero.Libraries.getType(libraryID);
+ if (type == 'group') {
+ var groupID = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
+ var group = Zotero.Groups.get(groupID);
+ return group.filesEditable;
+ }
+ throw ("Unknown library type '" + type + "' in Zotero.ItemGroup.filesEditable");
+ }
+ return false;
+});
Zotero.ItemGroup.prototype.getName = function()
{
diff --git a/chrome/content/zotero/xpcom/data/dataObjects.js b/chrome/content/zotero/xpcom/data/dataObjects.js
@@ -327,7 +327,7 @@ Zotero.DataObjects = function (object, objectPlural, id, table) {
this.editCheck = function (obj) {
- if (!Zotero.Sync.Server.syncInProgress && !this.isEditable(obj)) {
+ if (!Zotero.Sync.Server.syncInProgress && !Zotero.Sync.Storage.syncInProgress && !this.isEditable(obj)) {
throw ("Cannot edit " + this._ZDO_object + " in read-only Zotero library");
}
}
diff --git a/chrome/content/zotero/xpcom/data/group.js b/chrome/content/zotero/xpcom/data/group.js
@@ -286,6 +286,10 @@ Zotero.Group.prototype.save = function () {
* Deletes group and all descendant objects
**/
Zotero.Group.prototype.erase = function() {
+ // Don't send notifications for items and other groups objects that are deleted,
+ // since we're really only removing the group from the client
+ var notifierDisabled = Zotero.Notifier.disable();
+
Zotero.DB.beginTransaction();
var sql, ids, obj;
@@ -331,6 +335,9 @@ Zotero.Group.prototype.erase = function() {
sql = "DELETE FROM syncDeleteLog WHERE libraryID=?";
Zotero.DB.query(sql, this.libraryID);
+ var prefix = "groups/" + this.id;
+ Zotero.Relations.eraseByPathPrefix(prefix);
+
// Delete group
sql = "DELETE FROM groups WHERE groupID=?";
ids = Zotero.DB.query(sql, this.id)
@@ -342,6 +349,10 @@ Zotero.Group.prototype.erase = function() {
Zotero.DB.commitTransaction();
+ if (notifierDisabled) {
+ Zotero.Notifier.enable();
+ }
+
Zotero.Notifier.trigger('delete', 'group', this.id, notifierData);
}
diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js
@@ -2540,28 +2540,16 @@ Zotero.Item.prototype.getFile = function(row, skipExistsCheck) {
/**
* _row_ is optional itemAttachments row if available to skip queries
*/
-Zotero.Item.prototype.getFilename = function (row) {
+Zotero.Item.prototype.getFilename = function () {
if (!this.isAttachment()) {
throw ("getFileName() can only be called on attachment items in Zotero.Item.getFilename()");
}
- if (!row) {
- var row = {
- linkMode: this.attachmentLinkMode,
- path: this.attachmentPath
- };
- }
-
- if (row.linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
+ if (this.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
throw ("getFilename() cannot be called on link attachments in Zotero.Item.getFilename()");
}
- if (this.isImportedAttachment()) {
- var matches = row.path.match("^storage:(.+)$");
- return matches[1];
- }
-
- var file = this.getFile();
+ var file = this.getFile(null, true);
if (!file) {
return false;
}
@@ -2602,10 +2590,12 @@ Zotero.Item.prototype.renameAttachmentFile = function(newName, overwrite) {
}
file.moveTo(null, newName);
- // Update mod time so the file syncs
+ // Update mod time and clear hash so the file syncs
dest.lastModifiedTime = new Date();
this.relinkAttachmentFile(dest);
+ Zotero.Sync.Storage.setSyncedHash(this.id, null, false);
+
return true;
}
catch (e) {
@@ -2948,6 +2938,32 @@ Zotero.Item.prototype.__defineGetter__('attachmentModificationTime', function ()
/**
+ * MD5 hash of an attachment file
+ *
+ * Note: This is the hash of the file itself, not the last-known hash
+ * of the file on the storage server as stored in the database
+ *
+ * @return {String} MD5 hash of file as hex string
+ */
+Zotero.Item.prototype.__defineGetter__('attachmentHash', function () {
+ if (!this.isAttachment()) {
+ return undefined;
+ }
+
+ if (!this.id) {
+ return undefined;
+ }
+
+ var file = this.getFile();
+ if (!file) {
+ return undefined;
+ }
+
+ return Zotero.Utilities.prototype.md5(file);
+});
+
+
+/**
* Return plain text of attachment content
*
* - Currently works on HTML, PDF and plaintext attachments
diff --git a/chrome/content/zotero/xpcom/data/items.js b/chrome/content/zotero/xpcom/data/items.js
@@ -421,7 +421,6 @@ Zotero.Items = new function() {
var item = this.get(id);
if (!item) {
Zotero.debug('Item ' + id + ' does not exist in Items.erase()!', 1);
- Zotero.Notifier.trigger('delete', 'item', id);
continue;
}
item.erase(eraseChildren); // calls unload()
diff --git a/chrome/content/zotero/xpcom/data/relations.js b/chrome/content/zotero/xpcom/data/relations.js
@@ -6,6 +6,7 @@ Zotero.Relations = new function () {
owl: 'http://www.w3.org/2002/07/owl#'
};
+ var _prefix = "http://zotero.org/";
this.get = function (id) {
if (typeof id != 'number') {
@@ -76,6 +77,37 @@ Zotero.Relations = new function () {
}
+ this.updateUser = function (fromUserID, fromLibraryID, toUserID, toLibraryID) {
+ if (!fromUserID) {
+ throw ("Invalid source userID " + fromUserID + " in Zotero.Relations.updateUserID");
+ }
+ if (!fromLibraryID) {
+ throw ("Invalid source libraryID " + fromLibraryID + " in Zotero.Relations.updateUserID");
+ }
+ if (!toUserID) {
+ throw ("Invalid target userID " + toUserID + " in Zotero.Relations.updateUserID");
+ }
+ if (!toLibraryID) {
+ throw ("Invalid target libraryID " + toLibraryID + " in Zotero.Relations.updateUserID");
+ }
+
+ Zotero.DB.beginTransaction();
+
+ var sql = "UPDATE relations SET libraryID=? WHERE libraryID=?";
+ Zotero.DB.query(sql, [fromLibraryID, toLibraryID]);
+
+ sql = "UPDATE relations SET "
+ + "subject=REPLACE(subject, 'zotero.org/users/" + fromUserID + "', "
+ + "'zotero.org/users/" + toUserID + "'), "
+ + "object=REPLACE(object, 'zotero.org/users/" + fromUserID + "', "
+ + "'zotero.org/users/" + toUserID + "') "
+ + "WHERE predicate='owl:sameAs'";
+ Zotero.DB.query(sql);
+
+ Zotero.DB.commitTransaction();
+ }
+
+
this.add = function (libraryID, subject, predicate, object) {
predicate = _getPrefixAndValue(predicate).join(':');
@@ -109,6 +141,13 @@ Zotero.Relations = new function () {
}
+ this.eraseByPathPrefix = function (prefix) {
+ prefix = _prefix + prefix + '%';
+ sql = "DELETE FROM relations WHERE subject LIKE ? OR object LIKE ?";
+ Zotero.DB.query(sql, [prefix, prefix]);
+ }
+
+
this.xmlToRelation = function (xml) {
var relation = new Zotero.Relation;
var libraryID = xml.@libraryID.toString();
diff --git a/chrome/content/zotero/xpcom/db.js b/chrome/content/zotero/xpcom/db.js
@@ -421,7 +421,9 @@ Zotero.DBConnection.prototype.getLastErrorString = function () {
Zotero.DBConnection.prototype.beginTransaction = function () {
// TODO: limit to Zotero.DB, not all Zotero.DBConnections?
if (Zotero.waiting) {
- throw ("Cannot access database layer during active Zotero.wait()");
+ var msg = "Cannot access database layer during active Zotero.wait()";
+ Zotero.debug(msg, 2);
+ throw (msg);
}
var db = this._getDBConnection();
diff --git a/chrome/content/zotero/xpcom/error.js b/chrome/content/zotero/xpcom/error.js
@@ -1,6 +1,7 @@
-Zotero.Error = function (message, error) {
+Zotero.Error = function (message, error, data) {
this.name = "ZOTERO_ERROR";
this.message = message;
+ this.data = data;
if (parseInt(error) == error) {
this.error = error;
}
@@ -14,6 +15,11 @@ Zotero.Error.ERROR_MISSING_OBJECT = 1;
Zotero.Error.ERROR_FULL_SYNC_REQUIRED = 2;
Zotero.Error.ERROR_SYNC_USERNAME_NOT_SET = 3;
Zotero.Error.ERROR_INVALID_SYNC_LOGIN = 4;
+Zotero.Error.ERROR_ZFS_OVER_QUOTA = 5;
+Zotero.Error.ERROR_ZFS_UPLOAD_QUEUE_LIMIT = 6;
+Zotero.Error.ERROR_ZFS_FILE_EDITING_DENIED = 7;
+//Zotero.Error.ERROR_SYNC_EMPTY_RESPONSE_FROM_SERVER = 6;
+//Zotero.Error.ERROR_SYNC_INVALID_RESPONSE_FROM_SERVER = 7;
Zotero.Error.prototype.toString = function () {
return this.message;
diff --git a/chrome/content/zotero/xpcom/file.js b/chrome/content/zotero/xpcom/file.js
@@ -148,25 +148,6 @@ Zotero.File = new function(){
}
- /**
- * @param {nsIFile} file
- * @return {String} Base-64 representation of MD5 hash
- */
- this.getFileHash = function (file) {
- var fis = Components.classes["@mozilla.org/network/file-input-stream;1"]
- .createInstance(Components.interfaces.nsIFileInputStream);
- fis.init(file, -1, -1, false);
-
- var hash = Components.classes["@mozilla.org/security/hash;1"].
- createInstance(Components.interfaces.nsICryptoHash);
- hash.init(Components.interfaces.nsICryptoHash.MD5);
- hash.updateFromStream(fis, 4294967295); // PR_UINT32_MAX
- hash = hash.finish(true);
- fis.close();
- return hash;
- }
-
-
/*
* Write string to a file, overwriting existing file if necessary
*/
@@ -199,6 +180,19 @@ Zotero.File = new function(){
}
+ /**
+ * Copies all files from dir into newDir
+ */
+ this.copyDirectory = function (dir, newDir) {
+ var otherFiles = dir.directoryEntries;
+ while (otherFiles.hasMoreElements()) {
+ var file = otherFiles.getNext();
+ file.QueryInterface(Components.interfaces.nsIFile);
+ file.copyTo(newDir, null);
+ }
+ }
+
+
this.createDirectoryIfMissing = function (dir) {
if (!dir.exists() || !dir.isDirectory()) {
if (dir.exists() && !dir.isDirectory()) {
diff --git a/chrome/content/zotero/xpcom/itemTreeView.js b/chrome/content/zotero/xpcom/itemTreeView.js
@@ -2218,16 +2218,8 @@ Zotero.ItemTreeView.prototype.drop = function(row, orient)
}
}
else if (dataType == 'text/x-moz-url' || dataType == 'application/x-moz-file') {
- // FIXME: temporarily disable dragging in of files
- if (dataType == 'application/x-moz-file' && itemGroup.isWithinGroup()) {
- var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
- .getService(Components.interfaces.nsIPromptService);
- ps.alert(null, "", "Files cannot currently be added to group libraries.");
- return;
- }
-
// Disallow drop into read-only libraries
- if (!itemGroup.isEditable()) {
+ if (!itemGroup.editable) {
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var win = wm.getMostRecentWindow("navigator:browser");
@@ -2235,6 +2227,13 @@ Zotero.ItemTreeView.prototype.drop = function(row, orient)
return;
}
+ if (itemGroup.isWithinGroup()) {
+ var targetLibraryID = itemGroup.ref.libraryID;
+ }
+ else {
+ var targetLibraryID = null;
+ }
+
var sourceItemID = false;
var parentCollectionID = false;
@@ -2245,9 +2244,6 @@ Zotero.ItemTreeView.prototype.drop = function(row, orient)
else if (itemGroup.isCollection()) {
var parentCollectionID = itemGroup.ref.id;
}
- else if (itemGroup.isLibrary(true)) {
- var libraryID = itemGroup.ref.libraryID;
- }
var unlock = Zotero.Notifier.begin(true);
try {
@@ -2278,15 +2274,10 @@ Zotero.ItemTreeView.prototype.drop = function(row, orient)
// Still string, so remote URL
if (typeof file == 'string') {
- if (sourceItemID) {
- Zotero.Attachments.importFromURL(url, sourceItemID);
- }
- else {
- var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
- .getService(Components.interfaces.nsIWindowMediator);
- var win = wm.getMostRecentWindow("navigator:browser");
- win.ZoteroPane.addItemFromURL(url, 'temporaryPDFHack'); // TODO: don't do this
- }
+ var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Components.interfaces.nsIWindowMediator);
+ var win = wm.getMostRecentWindow("navigator:browser");
+ win.ZoteroPane.addItemFromURL(url, 'temporaryPDFHack', row); // TODO: don't do this
continue;
}
@@ -2295,7 +2286,7 @@ Zotero.ItemTreeView.prototype.drop = function(row, orient)
try {
Zotero.DB.beginTransaction();
- var itemID = Zotero.Attachments.importFromFile(file, sourceItemID);
+ var itemID = Zotero.Attachments.importFromFile(file, sourceItemID, targetLibraryID);
if (parentCollectionID) {
var col = Zotero.Collections.get(parentCollectionID);
if (col) {
diff --git a/chrome/content/zotero/xpcom/schema.js b/chrome/content/zotero/xpcom/schema.js
@@ -2451,6 +2451,21 @@ Zotero.Schema = new function(){
Zotero.DB.query("CREATE INDEX IF NOT EXISTS itemData_fieldID ON itemData(fieldID)");
}
+ if (i==63) {
+ Zotero.DB.query("ALTER TABLE itemAttachments ADD COLUMN storageHash TEXT");
+
+ var protocol = Zotero.Prefs.get('sync.storage.protocol');
+ if (protocol == 'webdav') {
+ Zotero.Prefs.set('sync.storage.scheme', 'http');
+ }
+ else {
+ Zotero.Prefs.set('sync.storage.protocol', 'webdav');
+ Zotero.Prefs.set('sync.storage.scheme', 'https');
+ }
+
+ Zotero.DB.query("UPDATE version SET schema='storage_webdav' WHERE schema='storage'");
+ }
+
Zotero.wait();
}
diff --git a/chrome/content/zotero/xpcom/storage.js b/chrome/content/zotero/xpcom/storage.js
@@ -28,155 +28,6 @@ Zotero.Sync.Storage = new function () {
// Public properties
//
- /**
- * URI of Zotero directory on storage server
- *
- * @return {nsIURI} nsIURI of data directory, with spec ending in '/'
- */
- this.__defineGetter__('rootURI', function () {
- if (_rootURI) {
- return _rootURI.clone()
- }
-
- var spec = Zotero.Prefs.get('sync.storage.url');
- if (!spec) {
- var msg = "Zotero storage URL not provided";
- Zotero.debug(msg);
- throw ({
- message: msg,
- name: "Z_ERROR_NO_URL",
- filename: "storage.js",
- toString: function () { return this.message; }
- });
- }
- var username = Zotero.Sync.Storage.username;
- var password = Zotero.Sync.Storage.password;
- if (username && !password) {
- var msg = "Zotero storage password not provided";
- Zotero.debug(msg);
- throw ({
- message: msg,
- name: "Z_ERROR_NO_PASSWORD",
- filename: "storage.js",
- toString: function () { return this.message; }
- });
- }
-
- var protocol = Zotero.Prefs.get('sync.storage.protocol');
- switch (protocol) {
- case 'webdav':
- var scheme = "http";
- break;
-
- case 'webdavs':
- var scheme = "https";
- break;
-
- default:
- throw ("Invalid storage protocol '" + protocol
- + "' in Zotero.Sync.Storage.rootURI");
- }
-
- spec = scheme + '://' + spec + '/zotero/';
-
- var ios = Components.classes["@mozilla.org/network/io-service;1"].
- getService(Components.interfaces.nsIIOService);
- try {
- var uri = ios.newURI(spec, null, null);
- if (username) {
- uri.username = username;
- uri.password = password;
- }
- }
- catch (e) {
- Zotero.debug(e);
- Components.utils.reportError(e);
- return false;
- }
- _rootURI = uri;
- return _rootURI.clone();
-
-
- return ;
- });
-
- this.__defineGetter__('username', function () {
- return Zotero.Prefs.get('sync.storage.username');
- });
-
- this.__defineGetter__('password', function () {
- var username = this.username;
-
- if (!username) {
- Zotero.debug('Username not set before setting Zotero.Sync.Storage.password');
- return '';
- }
-
- if (_cachedCredentials.username && _cachedCredentials.username == username) {
- return _cachedCredentials.password;
- }
-
- Zotero.debug('Getting Zotero storage password');
- var loginManager = Components.classes["@mozilla.org/login-manager;1"]
- .getService(Components.interfaces.nsILoginManager);
- var logins = loginManager.findLogins({}, _loginManagerHost, _loginManagerURL, null);
-
- // Find user from returned array of nsILoginInfo objects
- for (var i = 0; i < logins.length; i++) {
- if (logins[i].username == username) {
- _cachedCredentials.username = username;
- _cachedCredentials.password = logins[i].password;
- return logins[i].password;
- }
- }
-
- return '';
- });
-
- this.__defineSetter__('password', function (password) {
- _rootURI = false;
-
- var username = this.username;
- if (!username) {
- Zotero.debug('Username not set before setting Zotero.Sync.Server.password');
- return;
- }
-
- _cachedCredentials = {};
-
- var loginManager = Components.classes["@mozilla.org/login-manager;1"]
- .getService(Components.interfaces.nsILoginManager);
- var logins = loginManager.findLogins({}, _loginManagerHost, _loginManagerURL, null);
-
- for (var i = 0; i < logins.length; i++) {
- Zotero.debug('Clearing Zotero storage passwords');
- loginManager.removeLogin(logins[i]);
- break;
- }
-
- if (password) {
- Zotero.debug('Setting Zotero storage password');
- var nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
- Components.interfaces.nsILoginInfo, "init");
- var loginInfo = new nsLoginInfo(_loginManagerHost, _loginManagerURL,
- null, username, password, "", "");
- loginManager.addLogin(loginInfo);
- _cachedCredentials.username = username;
- _cachedCredentials.password = password;
- }
- });
-
- this.__defineGetter__('enabled', function () {
- return Zotero.Prefs.get("sync.storage.enabled");
- });
-
- this.__defineGetter__('verified', function () {
- return Zotero.Prefs.get("sync.storage.verified");
- });
-
- this.__defineGetter__('active', function () {
- return this.enabled && this.verified;
- });
this.__defineGetter__("syncInProgress", function () _syncInProgress);
@@ -194,67 +45,87 @@ Zotero.Sync.Storage = new function () {
//
// Private properties
//
- var _loginManagerHost = 'chrome://zotero';
- var _loginManagerURL = 'Zotero Storage Server';
- var _cachedCredentials = { username: null, password: null, authHeader: null };
- var _rootURI;
var _syncInProgress;
var _changesMade;
- var _finishCallback;
+ var _session;
+
+ var _callbacks = {
+ onSuccess: function () {},
+ onSkip: function () {},
+ onStop: function () {},
+ onError: function () {},
+ onWarning: function () {}
+ };
//
// Public methods
//
- this.sync = function () {
- if (!Zotero.Sync.Storage.enabled) {
- Zotero.debug("Storage sync is not enabled");
- Zotero.Sync.Runner.reset();
- Zotero.Sync.Runner.next();
+ this.sync = function (module, callbacks) {
+ for (var func in callbacks) {
+ _callbacks[func] = callbacks[func];
+ }
+
+ _session = new Zotero.Sync.Storage.Session(module, {
+ onChangesMade: function () {
+ _changesMade = true;
+ },
+ onError: _error
+ });
+
+ if (!_session.enabled) {
+ Zotero.debug(_session.name + " file sync is not enabled");
+ _callbacks.onSkip();
return;
}
+ if (!_session.initFromPrefs()) {
+ _error("Module not initialized");
+ }
- if (!Zotero.Sync.Storage.active) {
- Zotero.debug("Storage sync is not active");
+ if (!_session.active) {
+ Zotero.debug(_session.name + " file sync is not active");
- var callback = function (uri, status, authRequired) {
+ var callback = function (uri, status) {
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var lastWin = wm.getMostRecentWindow("navigator:browser");
- var success = Zotero.Sync.Storage.checkServerCallback(uri, status, authRequired, lastWin, true);
+ var success = _session.checkServerCallback(uri, status, lastWin, true);
if (success) {
- Zotero.debug("Storage sync is successfully set up");
- Zotero.Sync.Storage.sync();
+ Zotero.debug(_session.name + " file sync is successfully set up");
+ Zotero.Sync.Storage.sync(module, callbacks);
}
else {
- Zotero.debug("Storage sync verification failed");
- Zotero.Sync.Runner.reset();
- Zotero.Sync.Runner.next();
+ Zotero.debug(_session.name + " verification failed");
+ _callbacks.onSkip();
}
}
- Zotero.Sync.Storage.checkServer(callback);
+ _session.checkServer(callback);
return;
}
if (_syncInProgress) {
- _error("Storage sync operation already in progress");
+ _error("File sync operation already in progress");
}
- Zotero.debug("Beginning storage sync");
+ Zotero.debug("Beginning " + _session.name + " file sync");
_syncInProgress = true;
_changesMade = false;
- Zotero.Sync.Storage.checkForUpdatedFiles();
+ Zotero.Sync.Storage.checkForUpdatedFiles(null, null, _session.includeUserFiles, _session.includeGroupFiles);
- var successFileCheckCallback = function (lastSyncTime) {
+ var lastSyncCheckCallback = function (lastSyncTime) {
var downloadFiles = true;
- if (lastSyncTime) {
- var sql = "SELECT version FROM version WHERE schema='storage'";
+
+ var sql = "SELECT COUNT(*) FROM itemAttachments WHERE syncState=?";
+ var force = !!Zotero.DB.valueQuery(sql, Zotero.Sync.Storage.SYNC_STATE_FORCE_DOWNLOAD);
+
+ if (!force && lastSyncTime) {
+ var sql = "SELECT version FROM version WHERE schema='storage_" + module + "'";
var version = Zotero.DB.valueQuery(sql);
if (version == lastSyncTime) {
- Zotero.debug("Last storage sync time hasn't changed -- skipping file download step");
+ Zotero.debug("Last " + _session.name + " sync time hasn't changed -- skipping file download step");
downloadFiles = false;
}
}
@@ -263,27 +134,11 @@ Zotero.Sync.Storage = new function () {
var activeUp = Zotero.Sync.Storage.uploadFiles();
if (!activeDown && !activeUp) {
_syncInProgress = false;
- Zotero.Sync.Runner.reset();
- Zotero.Sync.Runner.next();
+ _callbacks.onSkip();
}
};
- // If authorization header isn't cached, cache it before proceeding,
- // since during testing Firefox 3.0.1 was being a bit amnesic with auth
- // info for subsequent requests -- surely a better way to fix this
- if (!_cachedCredentials.authHeader) {
- Zotero.Utilities.HTTP.doOptions(Zotero.Sync.Storage.rootURI, function (req) {
- var authHeader = Zotero.Utilities.HTTP.getChannelAuthorization(req.channel);
- if (authHeader) {
- _cachedCredentials.authHeader = authHeader;
- }
-
- _getSuccessFileTimestamp(successFileCheckCallback);
- });
- return;
- }
-
- _getSuccessFileTimestamp(successFileCheckCallback);
+ _session.getLastSyncTime(lastSyncCheckCallback);
}
@@ -368,69 +223,44 @@ Zotero.Sync.Storage = new function () {
/**
- * Get mod time of file on storage server
- *
- * @param {Zotero.Item} item
- * @param {Function} callback Callback f(item, mdate)
+ * @param {Integer} itemID
+ * @return {String|NULL} File hash, or NULL if never synced
*/
- this.getStorageModificationTime = function (item, callback) {
- var uri = _getItemPropertyURI(item);
- var headers = _cachedCredentials.authHeader ?
- { Authorization: _cachedCredentials.authHeader } : null;
-
- Zotero.Utilities.HTTP.doGet(uri, function (req) {
- var funcName = "Zotero.Sync.Storage.getStorageModificationTime()";
-
- // mod_speling can return 300s for 404s with base name matches
- if (req.status == 404 || req.status == 300) {
- callback(item, false);
- return;
- }
- else if (req.status != 200) {
- Zotero.debug(req.responseText);
- _error("Unexpected status code " + req.status + " in " + funcName);
- }
-
- Zotero.debug(req.responseText);
-
- var mtime = req.responseText;
- // No modification time set
- if (!mtime) {
- callback(item, false);
- return;
- }
-
- var mdate = new Date(mtime * 1000);
- callback(item, mdate);
- }, headers);
+ this.getSyncedHash = function (itemID) {
+ var sql = "SELECT storageHash FROM itemAttachments WHERE itemID=?";
+ var hash = Zotero.DB.valueQuery(sql, itemID);
+ if (hash === false) {
+ _error("Item " + itemID
+ + " not found in Zotero.Sync.Storage.getSyncedHash()");
+ }
+ return hash;
}
/**
- * Set mod time of file on storage server
- *
- * @param {Zotero.Item} item
- * @param {Function} callback Callback f(item, mtime)
+ * @param {Integer} itemID
+ * @param {String} hash File hash
+ * @param {Boolean} [updateItem=FALSE] Update dateModified field of
+ * attachment item
*/
- this.setStorageModificationTime = function (item, callback) {
- var uri = _getItemPropertyURI(item);
- var headers = _cachedCredentials.authHeader ?
- { Authorization: _cachedCredentials.authHeader } : null;
-
- Zotero.Utilities.HTTP.WebDAV.doPut(uri, item.attachmentModificationTime + '', function (req) {
- switch (req.status) {
- case 200:
- case 201:
- case 204:
- break;
-
- default:
- Zotero.debug(req.responseText);
- throw ("Unexpected status code " + req.status + " in "
- + "Zotero.Sync.Storage.setStorageModificationTime()");
- }
- callback(item, item.attachmentModificationTime);
- }, headers);
+ this.setSyncedHash = function (itemID, hash, updateItem) {
+ if (hash !== null && hash.length != 32) {
+ throw ("Invalid file hash '" + hash + "' in Zotero.Storage.setSyncedHash()");
+ }
+
+ Zotero.DB.beginTransaction();
+
+ var sql = "UPDATE itemAttachments SET storageHash=? WHERE itemID=?";
+ Zotero.DB.valueQuery(sql, [hash, itemID]);
+
+ if (updateItem) {
+ // Update item date modified so the new hash will be synced
+ var item = Zotero.Items.get(itemID);
+ item.setField('dateModified', Zotero.DB.transactionDateTime);
+ item.save();
+ }
+
+ Zotero.DB.commitTransaction();
}
@@ -443,7 +273,8 @@ Zotero.Sync.Storage = new function () {
*/
this.isFileModified = function (itemID) {
var item = Zotero.Items.get(itemID);
- if (!item.getFile()) {
+ var file = item.getFile();
+ if (!file) {
return false;
}
@@ -454,6 +285,17 @@ Zotero.Sync.Storage = new function () {
var syncModTime = Zotero.Sync.Storage.getSyncedModificationTime(itemID);
if (fileModTime != syncModTime) {
+ var syncHash = Zotero.Sync.Storage.getSyncedHash(itemID);
+ if (syncHash) {
+ var fileHash = item.attachmentHash;
+ Zotero.debug('================');
+ Zotero.debug(fileHash);
+ Zotero.debug(syncHash);
+ if (fileHash && fileHash == syncHash) {
+ Zotero.debug("Mod time didn't match but hash did for " + file.leafName + " -- ignoring");
+ return false;
+ }
+ }
return true;
}
@@ -467,24 +309,38 @@ Zotero.Sync.Storage = new function () {
*
* Also marks missing files for downloading
*
- * @param {Integer[]} itemIDs An optional set of item ids to check
- * @param {Object} itemModTimes Item mod times indexed by item ids
+ * @param {Integer[]} [itemIDs] An optional set of item ids to check
+ * @param {Object} [itemModTimes] Item mod times indexed by item ids
* appearing in itemIDs; if set,
* items with stored mod times
* that differ from the provided
* time but file mod times
* matching the stored time will
* be marked for download
+ * @param {Boolean} [includePersonalItems=false]
+ * @param {Boolean} [includeGroupItems=false]
* @return {Boolean} TRUE if any items changed state,
* FALSE otherwise
*/
- this.checkForUpdatedFiles = function (itemIDs, itemModTimes) {
+ this.checkForUpdatedFiles = function (itemIDs, itemModTimes, includeUserFiles, includeGroupFiles) {
+ var funcName = "Zotero.Sync.Storage.checkForUpdatedFiles()";
+
Zotero.debug("Checking for locally changed attachment files");
// check for current ops?
+ if (itemIDs) {
+ if (includeUserFiles || includeGroupFiles) {
+ _error("includeUserFiles and includeGroupFiles are not allowed when itemIDs is set in " + funcName);
+ }
+ }
+ else {
+ if (!includeUserFiles && !includeGroupFiles) {
+ _error("At least one of includeUserFiles or includeGroupFiles must be set in " + funcName);
+ }
+ }
+
if (itemModTimes && !itemIDs) {
- _error("itemModTimes can only be set if itemIDs is an array "
- + "in Zotero.Sync.Storage.checkForUpdatedFiles()");
+ _error("itemModTimes can only be set if itemIDs is an array in " + funcName);
}
var changed = false;
@@ -503,9 +359,15 @@ Zotero.Sync.Storage = new function () {
do {
var chunk = itemIDs.splice(0, maxIDs);
- var sql = "SELECT itemID, linkMode, path, storageModTime, syncState "
+ var sql = "SELECT itemID, linkMode, path, storageModTime, storageHash, syncState "
+ "FROM itemAttachments JOIN items USING (itemID) "
- + "WHERE linkMode IN (?,?) AND syncState IN (?,?) AND libraryID IS NULL";
+ + "WHERE linkMode IN (?,?) AND syncState IN (?,?)";
+ if (includeUserFiles && !includeGroupFiles) {
+ sql += " AND libraryID IS NULL";
+ }
+ else if (!includeUserFiles && includeGroupFiles) {
+ sql += " AND libraryID IS NOT NULL";
+ }
var params = [
Zotero.Attachments.LINK_MODE_IMPORTED_FILE,
Zotero.Attachments.LINK_MODE_IMPORTED_URL,
@@ -530,25 +392,17 @@ Zotero.Sync.Storage = new function () {
return changed;
}
- // Index mtimes by item id
+ // Index data by item id
var itemIDs = [];
var attachmentData = {};
for each(var row in rows) {
var id = row.itemID;
-
- // In download-marking mode, ignore attachments whose
- // storage mod times haven't changed
- if (itemModTimes &&
- row.storageModTime == itemModTimes[id]) {
- Zotero.debug("Storage mod time (" + row.storageModTime
- + ") hasn't changed for attachment " + id);
- continue;
- }
itemIDs.push(id);
attachmentData[id] = {
linkMode: row.linkMode,
path: row.path,
mtime: row.storageModTime,
+ hash: row.storageHash,
state: row.syncState
};
}
@@ -565,8 +419,7 @@ Zotero.Sync.Storage = new function () {
var file = item.getFile(attachmentData[item.id]);
if (!file) {
Zotero.debug("Marking attachment " + item.id + " as missing");
- updatedStates[item.id] =
- Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD;
+ updatedStates[item.id] = Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD;
continue;
}
@@ -575,25 +428,43 @@ Zotero.Sync.Storage = new function () {
//Zotero.debug("Stored mtime is " + attachmentData[item.id].mtime);
//Zotero.debug("File mtime is " + fileModTime);
+ // Download-marking mode
if (itemModTimes) {
Zotero.debug("Item mod time is " + itemModTimes[item.id]);
- }
-
- if (attachmentData[item.id].mtime != fileModTime) {
- if (attachmentData[item.id].state ==
- Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD) {
+
+ // Ignore attachments whose storage mod times haven't changed
+ if (row.storageModTime == itemModTimes[id]) {
+ Zotero.debug("Storage mod time (" + row.storageModTime + ") "
+ + "hasn't changed for attachment " + id);
continue;
}
- Zotero.debug("Marking attachment " + item.id + " as changed ("
- + attachmentData[item.id].mtime + " != " + fileModTime + ")");
- updatedStates[item.id] =
- Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD;
- }
- else if (itemModTimes) {
+
Zotero.debug("Marking attachment " + item.id + " for download");
- updatedStates[item.id] =
- Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD;
+ updatedStates[item.id] = Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD;
+
+ continue;
}
+
+ // If stored time matches file, it hasn't changed
+ if (attachmentData[item.id].mtime == fileModTime) {
+ continue;
+ }
+
+ // If file is already marked for upload, skip
+ if (attachmentData[item.id].state == Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD) {
+ continue;
+ }
+
+ // If file hash matches stored hash, only the mod time changed, so skip
+ var fileHash = item.attachmentHash;
+ if (attachmentData[item.id].hash && attachmentData[item.id].hash == fileHash) {
+ Zotero.debug("Mod time didn't match but hash did for " + file.leafName + " -- ignoring");
+ continue;
+ }
+
+ Zotero.debug("Marking attachment " + item.id + " as changed ("
+ + attachmentData[item.id].mtime + " != " + fileModTime + ")");
+ updatedStates[item.id] = Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD;
}
for (var itemID in updatedStates) {
@@ -629,7 +500,7 @@ Zotero.Sync.Storage = new function () {
_syncInProgress = true;
}
- var downloadFileIDs = _getFilesToDownload();
+ var downloadFileIDs = _getFilesToDownload(_session.includeUserFiles, _session.includeGroupFiles);
if (!downloadFileIDs) {
Zotero.debug("No files to download");
return false;
@@ -654,7 +525,7 @@ Zotero.Sync.Storage = new function () {
}
var request = new Zotero.Sync.Storage.Request(
- item.key, Zotero.Sync.Storage.downloadFile
+ item.libraryID + '/' + item.key, function (request) { _session.downloadFile(request); }
);
queue.addRequest(request);
}
@@ -666,93 +537,72 @@ Zotero.Sync.Storage = new function () {
/**
- * Begin download process for individual file
+ * Extract a downloaded file and update the database metadata
*
- * @param {Zotero.Sync.Storage.Request} [request]
+ * This is called from Zotero.Sync.Server.StreamListener.onStopRequest()
+ *
+ * @return {Object} data Properties 'request', 'item', 'compressed', 'syncModTime', 'syncHash'
*/
- this.downloadFile = function (request) {
- var key = request.name;
- var item = Zotero.Items.getByLibraryAndKey(null, key);
- if (!item) {
- _error("Item '" + key
- + "' not found in Zotero.Sync.Storage.downloadFile()");
- }
-
- // Retrieve modification time from server to store locally afterwards
- Zotero.Sync.Storage.getStorageModificationTime(item, function (item, mdate) {
- if (!request.isRunning()) {
- Zotero.debug("Download request '" + request.name
- + "' is no longer running after getting mod time");
- return;
- }
-
- if (!mdate) {
- Zotero.debug("Remote file not found for item " + item.key);
- request.finish();
- return;
- }
-
- try {
- var syncModTime = Zotero.Date.toUnixTimestamp(mdate);
-
- // Skip download if local file exists and matches mod time
- var file = item.getFile();
- if (file && file.exists()
- && syncModTime == Math.round(file.lastModifiedTime / 1000)) {
- Zotero.debug("File mod time matches remote file -- skipping download");
-
- Zotero.DB.beginTransaction();
- var syncState = Zotero.Sync.Storage.getSyncState(item.id);
- var updateItem = syncState != 1;
- Zotero.Sync.Storage.setSyncedModificationTime(item.id, syncModTime, updateItem);
- Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
- Zotero.DB.commitTransaction();
- _changesMade = true;
- request.finish();
- return;
- }
-
- var uri = _getItemURI(item);
- var destFile = Zotero.getTempDirectory();
- destFile.append(item.key + '.zip.tmp');
- if (destFile.exists()) {
- destFile.remove(false);
- }
-
- var listener = new Zotero.Sync.Storage.StreamListener(
- {
- onProgress: function (a, b, c) {
- request.onProgress(a, b, c)
- },
- onStop: _processDownload,
- request: request,
- item: item,
- syncModTime: syncModTime
- }
- );
-
- Zotero.debug('Saving with saveURI()');
- const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
- var wbp = Components
- .classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
- .createInstance(nsIWBP);
- wbp.persistFlags = nsIWBP.PERSIST_FLAGS_BYPASS_CACHE;
-
- wbp.progressListener = listener;
- wbp.saveURI(uri, null, null, null, null, destFile);
- }
- catch (e) {
- request.error(e);
- }
- });
+ this.processDownload = function (data) {
+ var funcName = "Zotero.Sync.Storage.processDownload()";
+
+ if (!data) {
+ _error("|data| not set in " + funcName);
+ }
+
+ if (!data.item) {
+ _error("|data.item| not set in " + funcName);
+ }
+
+ if (!data.syncModTime) {
+ _error("|data.syncModTime| not set in " + funcName);
+ }
+
+ if (!data.compressed && !data.syncHash) {
+ _error("|data.storageHash| is required if |data.compressed| is false in " + funcName);
+ }
+
+ var item = data.item;
+ var syncModTime = data.syncModTime;
+ var syncHash = data.syncHash;
+
+ // TODO: Test file hash
+
+ if (data.compressed) {
+ _processZipDownload(item);
+ }
+ else {
+ _processDownload(item);
+ }
+
+ var file = item.getFile();
+ if (!file) {
+ throw ("File not found for item " + item.id + " after processing download in " + funcName);
+ }
+ file.lastModifiedTime = syncModTime * 1000;
+
+ Zotero.DB.beginTransaction();
+ var syncState = Zotero.Sync.Storage.getSyncState(item.id);
+
+
+ var updateItem = syncState != 1;
+ var updateItem = false;
+
+
+ // Only save hash if file isn't compressed
+ if (!data.compressed) {
+ Zotero.Sync.Storage.setSyncedHash(item.id, syncHash, false);
+ }
+ Zotero.Sync.Storage.setSyncedModificationTime(item.id, syncModTime, updateItem);
+ Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
+ Zotero.DB.commitTransaction();
+ _changesMade = true;
}
/**
* Start upload of all attachments marked for upload
*
- * If mod time on server doesn't match file, display conflict window
- *
* @return {Boolean}
*/
this.uploadFiles = function () {
@@ -760,7 +610,7 @@ Zotero.Sync.Storage = new function () {
_syncInProgress = true;
}
- var uploadFileIDs = _getFilesToUpload();
+ var uploadFileIDs = _getFilesToUpload(_session.includeUserFiles, _session.includeGroupFiles);
if (!uploadFileIDs) {
Zotero.debug("No files to upload");
return false;
@@ -780,7 +630,7 @@ Zotero.Sync.Storage = new function () {
var item = Zotero.Items.get(itemID);
var request = new Zotero.Sync.Storage.Request(
- item.key, Zotero.Sync.Storage.uploadFile
+ item.libraryID + '/' + item.key, function (request) { _session.uploadFile(request); }
);
request.progressMax = Zotero.Attachments.getTotalFileSize(item, true);
queue.addRequest(request);
@@ -792,212 +642,35 @@ Zotero.Sync.Storage = new function () {
}
- this.uploadFile = function (request) {
- _createUploadFile(request);
- }
-
-
- /**
- * Remove files on storage server that were deleted locally more than
- * sync.storage.deleteDelayDays days ago
- *
- * @param {Function} callback Passed number of files deleted
- */
- this.purgeDeletedStorageFiles = function (callback) {
- Zotero.debug("Purging deleted storage files");
- var files = _getDeletedFiles();
- if (!files) {
- Zotero.debug("No files to delete remotely");
- return;
+ this.checkServer = function (module, callback) {
+ _session = new Zotero.Sync.Storage.Session(module, { onError: _error });
+ if (!_session.initFromPrefs()) {
+ _error("Module not initialized");
}
-
- // Add .zip extension
- var files = files.map(function (file) file + ".zip");
-
- _deleteStorageFiles(files, function (results) {
- // Remove deleted and nonexistent files from storage delete log
- var toPurge = results.deleted.concat(results.missing);
- if (toPurge.length > 0) {
- var done = 0;
- var maxFiles = 999;
- var numFiles = toPurge.length;
-
- Zotero.DB.beginTransaction();
-
- do {
- var chunk = toPurge.splice(0, maxFiles);
- var sql = "DELETE FROM storageDeleteLog WHERE key IN ("
- + chunk.map(function () '?').join() + ")";
- Zotero.DB.query(sql, chunk);
- done += chunk.length;
- }
- while (done < numFiles);
-
- Zotero.DB.commitTransaction();
- }
-
- if (callback) {
- callback(results.deleted.length);
- }
- });
+ _session.checkServer(callback);
}
- /**
- * Delete orphaned storage files older than a day before last sync time
- *
- * @param {Function} callback
- */
- this.purgeOrphanedStorageFiles = function (callback) {
- const daysBeforeSyncTime = 1;
-
- Zotero.debug("Purging orphaned storage files");
- var uri = Zotero.Sync.Storage.rootURI;
- var path = uri.path;
-
- var prolog = '<?xml version="1.0" encoding="utf-8" ?>\n';
- var D = new Namespace("D", "DAV:");
- var nsDeclarations = 'xmlns:' + D.prefix + '=' + '"' + D.uri + '"';
-
- var requestXML = new XML('<D:propfind ' + nsDeclarations + '/>');
- requestXML.D::prop = '';
- requestXML.D::prop.D::getlastmodified = '';
-
- var xmlstr = prolog + requestXML.toXMLString();
-
- var lastSyncDate = new Date(Zotero.Sync.Server.lastLocalSyncTime * 1000);
-
- Zotero.Utilities.HTTP.WebDAV.doProp("PROPFIND", uri, xmlstr, function (req) {
- Zotero.debug(req.responseText);
-
- var funcName = "Zotero.Sync.Storage.purgeOrphanedStorageFiles()";
-
- // Strip XML declaration and convert to E4X
- var xml = new XML(req.responseText.replace(/<\?xml.*\?>/, ''));
-
- var deleteFiles = [];
- var trailingSlash = !!path.match(/\/$/);
- for each(var response in xml.D::response) {
- var href = response.D::href.toString();
-
- // Strip trailing slash if there isn't one on the root path
- if (!trailingSlash) {
- href = href.replace(/\/$/, "")
- }
-
- // Absolute
- if (href.match(/^https?:\/\//)) {
- var ios = Components.classes["@mozilla.org/network/io-service;1"].
- getService(Components.interfaces.nsIIOService);
- var href = ios.newURI(href, null, null);
- href = href.path;
- }
-
- if (href.indexOf(path) == -1
- // Try URL-encoded as well, in case there's a '~' or similar
- // character in the URL and the server (e.g., Sakai) is
- // encoding the value
- && decodeURIComponent(href).indexOf(path) == -1) {
- _error("DAV:href '" + href
- + "' does not begin with path '" + path + "' in " + funcName);
- }
-
- // Skip root URI
- if (href == path
- // Try URL-encoded as well, as above
- || decodeURIComponent(href) == path) {
- continue;
- }
-
- var matches = href.match(/[^\/]+$/);
- if (!matches) {
- _error("Unexpected href '" + href + "' in " + funcName)
- }
- var file = matches[0];
-
- if (file.indexOf('.') == 0) {
- Zotero.debug("Skipping hidden file " + file);
- continue;
- }
- if (!file.match(/\.zip$/) && !file.match(/\.prop$/)) {
- Zotero.debug("Skipping file " + file);
- continue;
- }
-
- var key = file.replace(/\.(zip|prop)$/, '');
- var item = Zotero.Items.getByLibraryAndKey(null, key);
- if (item) {
- Zotero.debug("Skipping existing file " + file);
- continue;
- }
-
- Zotero.debug("Checking orphaned file " + file);
-
- // TODO: Parse HTTP date properly
- var lastModified = response..*::getlastmodified.toString();
- lastModified = Zotero.Date.strToISO(lastModified);
- lastModified = Zotero.Date.sqlToDate(lastModified);
-
- // Delete files older than a day before last sync time
- var days = (lastSyncDate - lastModified) / 1000 / 60 / 60 / 24;
-
- // DEBUG!!!!!!!!!!!!
- //
- // For now, delete all orphaned files immediately
- if (true) {
- deleteFiles.push(file);
- } else
-
- if (days > daysBeforeSyncTime) {
- deleteFiles.push(file);
- }
- }
-
- _deleteStorageFiles(deleteFiles, callback);
- },
- { Depth: 1 });
+ this.checkServerCallback = function (uri, status, window, skipSuccessMessage) {
+ return _session.checkServerCallback(uri, status, window, skipSuccessMessage);
}
- /**
- * Create a Zotero directory on the storage server
- */
- this.createServerDirectory = function (callback) {
- var uri = this.rootURI;
- Zotero.Utilities.HTTP.WebDAV.doMkCol(uri, function (req) {
- Zotero.debug(req.responseText);
- Zotero.debug(req.status);
-
- switch (req.status) {
- case 201:
- callback(uri, Zotero.Sync.Storage.SUCCESS);
- break;
-
- case 401:
- callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
- return;
-
- case 403:
- callback(uri, Zotero.Sync.Storage.ERROR_FORBIDDEN);
- return;
-
- case 405:
- callback(uri, Zotero.Sync.Storage.ERROR_NOT_ALLOWED);
- return;
-
- case 500:
- callback(uri, Zotero.Sync.Storage.ERROR_SERVER_ERROR);
- return;
-
- default:
- callback(uri, Zotero.Sync.Storage.ERROR_UNKNOWN);
- return;
- }
- });
+ this.purgeDeletedStorageFiles = function (module, callback) {
+ _session = new Zotero.Sync.Storage.Session(module, { onError: _error });
+ if (!_session.initFromPrefs()) {
+ _error("Module not initialized");
+ }
+ _session.purgeDeletedStorageFiles(callback);
}
- this.resetAllSyncStates = function (syncState) {
+ this.resetAllSyncStates = function (syncState, includeUserFiles, includeGroupFiles) {
+ if (!includeUserFiles && !includeGroupFiles) {
+ includeUserFiles = true;
+ includeGroupFiles = true;
+ }
+
if (!syncState) {
syncState = this.SYNC_STATE_TO_UPLOAD;
}
@@ -1013,172 +686,185 @@ Zotero.Sync.Storage = new function () {
+ "Zotero.Sync.Storage.resetAllSyncStates()");
}
- var sql = "UPDATE itemAttachments SET syncState=? WHERE itemID IN "
- + "(SELECT itemID FROM items WHERE libraryID IS NULL)";
+ //var sql = "UPDATE itemAttachments SET syncState=?, storageModTime=NULL, storageHash=NULL";
+ var sql = "UPDATE itemAttachments SET syncState=?";
+ if (includeUserFiles && !includeGroupFiles) {
+ sql += " WHERE itemID IN (SELECT itemID FROM items WHERE libraryID IS NULL)";
+ }
+ else if (!includeUserFiles && includeGroupFiles) {
+ sql += " WHERE itemID IN (SELECT itemID FROM items WHERE libraryID IS NOT NULL)";
+ }
Zotero.DB.query(sql, [syncState]);
- var sql = "DELETE FROM version WHERE schema='storage'";
+ var sql = "DELETE FROM version WHERE schema LIKE 'storage_%'";
Zotero.DB.query(sql);
}
- this.clearSettingsCache = function () {
- _rootURI = undefined;
+ this.getItemFromRequestName = function (name) {
+ var [libraryID, key] = name.split('/');
+ if (libraryID == "null") {
+ libraryID = null;
+ }
+ return Zotero.Items.getByLibraryAndKey(libraryID, key);
}
//
// Private methods
//
+ function _processDownload(item) {
+ var funcName = "Zotero.Sync.Storage._processDownload()";
+
+ var tempFile = Zotero.getTempDirectory();
+ tempFile.append(item.key + '.tmp');
+
+ if (!tempFile.exists()) {
+ Zotero.debug(tempFile.path);
+ throw ("Downloaded file not found in " + funcName);
+ }
+
+ var parentDir = Zotero.Attachments.getStorageDirectory(item.id);
+ if (!parentDir.exists()) {
+ Zotero.Attachments.createDirectoryForItem(item.id);
+ }
+
+ _deleteExistingAttachmentFiles(item);
+
+ var file = item.getFile(null, true);
+ if (!file) {
+ throw ("Empty path for item " + item.key + " in " + funcName);
+ }
+ var newName = file.leafName;
+
+ Zotero.debug("Moving download file " + tempFile.leafName + " into attachment directory");
+ tempFile.moveTo(parentDir, newName);
+ }
- /**
- * Extract a downloaded ZIP file and update the database metadata
- *
- * This is called from Zotero.Sync.Server.StreamListener.onStopRequest()
- *
- * @param {nsIRequest} request
- * @param {Integer} status Status code from download listener
- * @param {String} response
- * @return {Object} data Properties 'request', 'item', 'syncModTime'
- */
- function _processDownload(request, status, response, data) {
+ function _processZipDownload(item) {
+ var funcName = "Zotero.Sync.Storage._processDownloadedZip()";
+
+ var zipFile = Zotero.getTempDirectory();
+ zipFile.append(item.key + '.zip.tmp');
+
+ if (!zipFile.exists()) {
+ throw ("Downloaded ZIP file not found in " + funcName);
+ }
+
+ var zipReader = Components.classes["@mozilla.org/libjar/zip-reader;1"].
+ createInstance(Components.interfaces.nsIZipReader);
try {
- var funcName = "Zotero.Sync.Storage._processDownload()";
+ zipReader.open(zipFile);
+ zipReader.test(null);
- var request = data.request;
- var item = data.item;
- var syncModTime = data.syncModTime;
- var zipFile = Zotero.getTempDirectory();
- zipFile.append(item.key + '.zip.tmp');
-
- Zotero.debug("Finished download of " + zipFile.path + " with status " + status);
-
- var zipReader = Components.classes["@mozilla.org/libjar/zip-reader;1"].
- createInstance(Components.interfaces.nsIZipReader);
- try {
- zipReader.open(zipFile);
- zipReader.test(null);
-
- Zotero.debug("ZIP file is OK");
+ Zotero.debug("ZIP file is OK");
+ }
+ catch (e) {
+ Zotero.debug(zipFile.leafName + " is not a valid ZIP file", 2);
+ if (zipFile.exists()) {
+ zipFile.remove(false);
}
- catch (e) {
- Zotero.debug(zipFile.leafName + " is not a valid ZIP file", 2);
- if (zipFile.exists()) {
- zipFile.remove(false);
- }
- return;
+ return;
+ }
+
+ var parentDir = Zotero.Attachments.getStorageDirectory(item.id);
+ if (!parentDir.exists()) {
+ Zotero.Attachments.createDirectoryForItem(item.id);
+ }
+
+ _deleteExistingAttachmentFiles(item);
+
+ var entries = zipReader.findEntries(null);
+ while (entries.hasMore()) {
+ var entryName = entries.getNext();
+ var b64re = /%ZB64$/;
+ if (entryName.match(b64re)) {
+ var fileName = Zotero.Utilities.Base64.decode(
+ entryName.replace(b64re, '')
+ );
}
-
- var parentDir = Zotero.Attachments.getStorageDirectory(item.id);
- if (!parentDir.exists()) {
- Zotero.Attachments.createDirectoryForItem(item.id);
+ else {
+ var fileName = entryName;
}
- // Delete existing files
- var otherFiles = parentDir.directoryEntries;
- while (otherFiles.hasMoreElements()) {
- var file = otherFiles.getNext();
- file.QueryInterface(Components.interfaces.nsIFile);
- if (file.leafName[0] == '.' || file.equals(zipFile)) {
- continue;
- }
-
- // Firefox (as of 3.0.1) can't detect symlinks (at least on OS X),
- // so use pre/post-normalized path to check
- var origPath = file.path;
- var origFileName = file.leafName;
- file.normalize();
- if (origPath != file.path) {
- var msg = "Not deleting symlink '" + origFileName + "'";
- Zotero.debug(msg, 2);
- Components.utils.reportError(msg + " in " + funcName);
- continue;
- }
- // This should be redundant with above check, but let's do it anyway
- if (!parentDir.contains(file, false)) {
- var msg = "Storage directory doesn't contain '" + file.leafName + "'";
- Zotero.debug(msg, 2);
- Components.utils.reportError(msg + " in " + funcName);
- continue;
- }
-
- if (file.isFile()) {
- Zotero.debug("Deleting existing file " + file.leafName);
- file.remove(false);
- }
- else if (file.isDirectory()) {
- Zotero.debug("Deleting existing directory " + file.leafName);
- file.remove(true);
- }
+ if (fileName.indexOf('.') == 0) {
+ Zotero.debug("Skipping " + fileName);
+ continue;
}
- var entries = zipReader.findEntries(null);
- while (entries.hasMore()) {
- var entryName = entries.getNext();
- var b64re = /%ZB64$/;
- if (entryName.match(b64re)) {
- var fileName = Zotero.Utilities.Base64.decode(
- entryName.replace(b64re, '')
- );
- }
- else {
- var fileName = entryName;
- }
-
- if (fileName.indexOf('.') == 0) {
- Zotero.debug("Skipping " + fileName);
- continue;
- }
-
- Zotero.debug("Extracting " + fileName);
- var destFile = parentDir.clone();
- destFile.QueryInterface(Components.interfaces.nsILocalFile);
- destFile.setRelativeDescriptor(parentDir, fileName);
- if (destFile.exists()) {
- var msg = "ZIP entry '" + fileName + "' "
- + " already exists";
- Zotero.debug(msg, 2);
- Components.utils.reportError(msg + " in " + funcName);
- continue;
- }
- destFile.create(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0644);
- zipReader.extract(entryName, destFile);
-
- var origPath = destFile.path;
- var origFileName = destFile.leafName;
- destFile.normalize();
- if (origPath != destFile.path) {
- var msg = "ZIP file " + zipFile.leafName + " contained symlink '"
- + origFileName + "'";
- Zotero.debug(msg, 1);
- Components.utils.reportError(msg + " in " + funcName);
- continue;
- }
- destFile.permissions = 0644;
+ Zotero.debug("Extracting " + fileName);
+ var destFile = parentDir.clone();
+ destFile.QueryInterface(Components.interfaces.nsILocalFile);
+ destFile.setRelativeDescriptor(parentDir, fileName);
+ if (destFile.exists()) {
+ var msg = "ZIP entry '" + fileName + "' "
+ + " already exists";
+ Zotero.debug(msg, 2);
+ Components.utils.reportError(msg + " in " + funcName);
+ continue;
}
- zipReader.close();
- zipFile.remove(false);
+ destFile.create(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0644);
+ zipReader.extract(entryName, destFile);
- var file = item.getFile();
- if (!file) {
- var msg = "File not found for item " + item.id + " after extracting ZIP";
+ var origPath = destFile.path;
+ var origFileName = destFile.leafName;
+ destFile.normalize();
+ if (origPath != destFile.path) {
+ var msg = "ZIP file " + zipFile.leafName + " contained symlink '"
+ + origFileName + "'";
Zotero.debug(msg, 1);
Components.utils.reportError(msg + " in " + funcName);
- return;
+ continue;
}
- file.lastModifiedTime = syncModTime * 1000;
-
- Zotero.DB.beginTransaction();
- var syncState = Zotero.Sync.Storage.getSyncState(item.id);
- var updateItem = syncState != 1;
- Zotero.Sync.Storage.setSyncedModificationTime(item.id, syncModTime, updateItem);
- Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
- Zotero.DB.commitTransaction();
- _changesMade = true;
+ destFile.permissions = 0644;
}
- finally {
- request.finish();
+ zipReader.close();
+ zipFile.remove(false);
+ }
+
+
+ function _deleteExistingAttachmentFiles(item) {
+ var funcName = "Zotero.Sync.Storage._deleteExistingAttachmentFiles()";
+
+ var parentDir = Zotero.Attachments.getStorageDirectory(item.id);
+
+ // Delete existing files
+ var otherFiles = parentDir.directoryEntries;
+ while (otherFiles.hasMoreElements()) {
+ var file = otherFiles.getNext();
+ file.QueryInterface(Components.interfaces.nsIFile);
+ if (file.leafName[0] == '.') {
+ continue;
+ }
+
+ // Firefox (as of 3.0.1) can't detect symlinks (at least on OS X),
+ // so use pre/post-normalized path to check
+ var origPath = file.path;
+ var origFileName = file.leafName;
+ file.normalize();
+ if (origPath != file.path) {
+ var msg = "Not deleting symlink '" + origFileName + "'";
+ Zotero.debug(msg, 2);
+ Components.utils.reportError(msg + " in " + funcName);
+ continue;
+ }
+ // This should be redundant with above check, but let's do it anyway
+ if (!parentDir.contains(file, false)) {
+ var msg = "Storage directory doesn't contain '" + file.leafName + "'";
+ Zotero.debug(msg, 2);
+ Components.utils.reportError(msg + " in " + funcName);
+ continue;
+ }
+
+ if (file.isFile()) {
+ Zotero.debug("Deleting existing file " + file.leafName);
+ file.remove(false);
+ }
+ else if (file.isDirectory()) {
+ Zotero.debug("Deleting existing directory " + file.leafName);
+ file.remove(true);
+ }
}
}
@@ -1187,13 +873,13 @@ Zotero.Sync.Storage = new function () {
* Create zip file of attachment directory
*
* @param {Zotero.Sync.Storage.Request} request
+ * @param {Function} callback
* @return {Boolean} TRUE if zip process started,
* FALSE if storage was empty
*/
- function _createUploadFile(request) {
- var key = request.name;
- var item = Zotero.Items.getByLibraryAndKey(null, key);
- Zotero.debug("Creating zip file for item " + item.key);
+ this.createUploadFile = function (request, callback) {
+ var item = Zotero.Sync.Storage.getItemFromRequestName(request.name);
+ Zotero.debug("Creating zip file for item " + item.libraryID + "/" + item.key);
try {
switch (item.attachmentLinkMode) {
@@ -1205,7 +891,7 @@ Zotero.Sync.Storage = new function () {
));
}
- var dir = Zotero.Attachments.getStorageDirectoryByKey(key);
+ var dir = Zotero.Attachments.getStorageDirectoryByKey(item.key);
var tmpFile = Zotero.getTempDirectory();
tmpFile.append(item.key + '.zip');
@@ -1224,7 +910,7 @@ Zotero.Sync.Storage = new function () {
Zotero.debug('Creating ' + tmpFile.leafName + ' with ' + fileList.length + ' file(s)');
var observer = new Zotero.Sync.Storage.ZipWriterObserver(
- zw, _processUploadFile, { request: request, files: fileList }
+ zw, callback, { request: request, files: fileList }
);
zw.processQueue(observer, null);
return true;
@@ -1244,229 +930,50 @@ Zotero.Sync.Storage = new function () {
if (file.isDirectory()) {
//Zotero.debug("Recursing into directory " + file.leafName);
fileList.concat(_zipDirectory(rootDir, file, zipWriter));
- continue;
- }
- var fileName = file.getRelativeDescriptor(rootDir);
- if (fileName.indexOf('.') == 0) {
- Zotero.debug('Skipping file ' + fileName);
- continue;
- }
-
- //Zotero.debug("Adding file " + fileName);
-
- fileName = Zotero.Utilities.Base64.encode(fileName) + "%ZB64";
- zipWriter.addEntryFile(
- fileName,
- Components.interfaces.nsIZipWriter.COMPRESSION_DEFAULT,
- file,
- true
- );
- fileList.push(fileName);
- }
- return fileList;
- }
-
-
- /**
- * Upload the generated ZIP file to the server
- *
- * @param {Object} Object with 'request' property
- * @return {void}
- */
- function _processUploadFile(data) {
- /*
- _updateSizeMultiplier(
- (100 - Zotero.Sync.Storage.compressionTracker.ratio) / 100
- );
- */
-
- var request = data.request;
- var item = Zotero.Items.getByLibraryAndKey(null, request.name);
-
- Zotero.Sync.Storage.getStorageModificationTime(item, function (item, mdate) {
- if (!request.isRunning()) {
- Zotero.debug("Upload request '" + request.name
- + "' is no longer running after getting mod time");
- return;
- }
-
- try {
- // Check for conflict
- if (Zotero.Sync.Storage.getSyncState(item.id)
- != Zotero.Sync.Storage.SYNC_STATE_FORCE_UPLOAD) {
- if (mdate) {
- // Remote prop time
- var mtime = Zotero.Date.toUnixTimestamp(mdate);
- // Local file time
- var fmtime = item.attachmentModificationTime;
-
- if (fmtime == mtime) {
- Zotero.debug("File mod time matches remote file -- skipping upload");
-
- Zotero.DB.beginTransaction();
- var syncState = Zotero.Sync.Storage.getSyncState(item.id);
- Zotero.Sync.Storage.setSyncedModificationTime(item.id, fmtime, true);
- Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
- Zotero.DB.commitTransaction();
- _changesMade = true;
- request.finish();
- return;
- }
-
- var smtime = Zotero.Sync.Storage.getSyncedModificationTime(item.id);
- if (smtime != mtime) {
- var localData = { modTime: fmtime };
- var remoteData = { modTime: mtime };
- Zotero.Sync.Storage.QueueManager.addConflict(
- request.name, localData, remoteData
- );
- Zotero.debug("Conflict -- last synced file mod time "
- + "does not match time on storage server"
- + " (" + smtime + " != " + mtime + ")");
- request.finish();
- return;
- }
- }
- else {
- Zotero.debug("Remote file not found for item " + item.id);
- }
- }
-
- var file = Zotero.getTempDirectory();
- file.append(item.key + '.zip');
-
- var fis = Components.classes["@mozilla.org/network/file-input-stream;1"]
- .createInstance(Components.interfaces.nsIFileInputStream);
- fis.init(file, 0x01, 0, 0);
-
- var bis = Components.classes["@mozilla.org/network/buffered-input-stream;1"]
- .createInstance(Components.interfaces.nsIBufferedInputStream)
- bis.init(fis, 64 * 1024);
-
- var uri = _getItemURI(item);
-
- var ios = Components.classes["@mozilla.org/network/io-service;1"].
- getService(Components.interfaces.nsIIOService);
- var channel = ios.newChannelFromURI(uri);
- channel.QueryInterface(Components.interfaces.nsIUploadChannel);
- channel.setUploadStream(bis, 'application/octet-stream', -1);
- channel.QueryInterface(Components.interfaces.nsIHttpChannel);
- channel.requestMethod = 'PUT';
- channel.allowPipelining = false;
- if (_cachedCredentials.authHeader) {
- channel.setRequestHeader(
- 'Authorization', _cachedCredentials.authHeader, false
- );
- }
- channel.setRequestHeader('Keep-Alive', '', false);
- channel.setRequestHeader('Connection', '', false);
-
- var listener = new Zotero.Sync.Storage.StreamListener(
- {
- onProgress: function (a, b, c) {
- request.onProgress(a, b, c);
- },
- onStop: _onUploadComplete,
- onCancel: _onUploadCancel,
- request: request,
- item: item,
- streams: [fis, bis]
- }
- );
- channel.notificationCallbacks = listener;
-
- var dispURI = uri.clone();
- if (dispURI.password) {
- dispURI.password = '********';
- }
- Zotero.debug("HTTP PUT of " + file.leafName + " to " + dispURI.spec);
-
- channel.asyncOpen(listener, null);
- }
- catch (e) {
- request.error(e);
- }
- });
- }
-
-
- function _onUploadComplete(httpRequest, status, response, data) {
- var request = data.request;
- var item = data.item;
- var url = httpRequest.name;
-
- Zotero.debug("Upload of attachment " + item.key
- + " finished with status code " + status);
-
- switch (status) {
- case 200:
- case 201:
- case 204:
- break;
-
- default:
- _error("Unexpected file upload status " + status
- + " in Zotero.Sync.Storage._onUploadComplete()");
- }
-
- Zotero.Sync.Storage.setStorageModificationTime(item, function (item, mtime) {
- if (!request.isRunning()) {
- Zotero.debug("Upload request '" + request.name
- + "' is no longer running after getting mod time");
- return;
- }
-
- Zotero.DB.beginTransaction();
-
- Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
- Zotero.Sync.Storage.setSyncedModificationTime(item.id, mtime, true);
-
- Zotero.DB.commitTransaction();
-
- try {
- var file = Zotero.getTempDirectory();
- file.append(item.key + '.zip');
- file.remove(false);
+ continue;
}
- catch (e) {
- Components.utils.reportError(e);
+ var fileName = file.getRelativeDescriptor(rootDir);
+ if (fileName.indexOf('.') == 0) {
+ Zotero.debug('Skipping file ' + fileName);
+ continue;
}
- _changesMade = true;
- request.finish();
- });
- }
-
-
- function _onUploadCancel(httpRequest, status, data) {
- var request = data.request;
- var item = data.item;
-
- Zotero.debug("Upload of attachment " + item.key
- + " cancelled with status code " + status);
-
- try {
- var file = Zotero.getTempDirectory();
- file.append(item.key + '.zip');
- file.remove(false);
- }
- catch (e) {
- Components.utils.reportError(e);
+ //Zotero.debug("Adding file " + fileName);
+
+ fileName = Zotero.Utilities.Base64.encode(fileName) + "%ZB64";
+ zipWriter.addEntryFile(
+ fileName,
+ Components.interfaces.nsIZipWriter.COMPRESSION_DEFAULT,
+ file,
+ true
+ );
+ fileList.push(fileName);
}
-
- request.finish();
+ return fileList;
}
+
/**
* Get files marked as ready to upload
*
* @inner
* @return {Number[]} Array of attachment itemIDs
*/
- function _getFilesToDownload() {
+ function _getFilesToDownload(includeUserFiles, includeGroupFiles) {
+ if (!includeUserFiles && !includeGroupFiles) {
+ _error("At least one of includeUserFiles or includeGroupFiles must be set "
+ + "in Zotero.Sync.Storage._getFilesToDownload()");
+ }
+
var sql = "SELECT itemID FROM itemAttachments JOIN items USING (itemID) "
- + "WHERE syncState IN (?,?) AND libraryID IS NULL";
+ + "WHERE syncState IN (?,?)";
+ if (includeUserFiles && !includeGroupFiles) {
+ sql += " AND libraryID IS NULL";
+ }
+ else if (!includeUserFiles && includeGroupFiles) {
+ sql += " AND libraryID IS NOT NULL";
+ }
return Zotero.DB.columnQuery(sql,
[
Zotero.Sync.Storage.SYNC_STATE_TO_DOWNLOAD,
@@ -1482,9 +989,20 @@ Zotero.Sync.Storage = new function () {
* @inner
* @return {Number[]} Array of attachment itemIDs
*/
- function _getFilesToUpload() {
+ function _getFilesToUpload(includeUserFiles, includeGroupFiles) {
+ if (!includeUserFiles && !includeGroupFiles) {
+ _error("At least one of includeUserFiles or includeGroupFiles must be set "
+ + "in Zotero.Sync.Storage._getFilesToUpload()");
+ }
+
var sql = "SELECT itemID FROM itemAttachments JOIN items USING (itemID) "
- + "WHERE syncState IN (?,?) AND linkMode IN (?,?) AND libraryID IS NULL";
+ + "WHERE syncState IN (?,?) AND linkMode IN (?,?)";
+ if (includeUserFiles && !includeGroupFiles) {
+ sql += " AND libraryID IS NULL";
+ }
+ else if (!includeUserFiles && includeGroupFiles) {
+ sql += " AND libraryID IS NOT NULL";
+ }
return Zotero.DB.columnQuery(sql,
[
Zotero.Sync.Storage.SYNC_STATE_TO_UPLOAD,
@@ -1502,7 +1020,7 @@ Zotero.Sync.Storage = new function () {
* Number of days old files have to be
* @return {String[]|FALSE} Array of keys, or FALSE if none
*/
- function _getDeletedFiles(days) {
+ this.getDeletedFiles = function (days) {
if (!days) {
days = Zotero.Prefs.get("sync.storage.deleteDelayDays");
}
@@ -1515,673 +1033,113 @@ Zotero.Sync.Storage = new function () {
}
- /**
- * @inner
- * @param {String[]} files Remote filenames to delete (e.g., ZIPs)
- * @param {Function} callback Passed object containing three arrays:
- * 'deleted', 'missing', and 'error',
- * each containing filenames
- */
- function _deleteStorageFiles(files, callback) {
- var results = {
- deleted: [],
- missing: [],
- error: []
- };
-
- if (files.length == 0) {
- if (callback) {
- callback(results);
- }
- return;
- }
-
- for (var i=0; i<files.length; i++) {
- let last = (i == files.length - 1);
- let fileName = files[i];
-
- let deleteURI = Zotero.Sync.Storage.rootURI;
- // This should never happen, but let's be safe
- if (!deleteURI.spec.match(/\/$/)) {
- callback(deleted);
- _error("Root URI does not end in slash in "
- + "Zotero.Sync.Storage._deleteStorageFiles()");
- }
- deleteURI.QueryInterface(Components.interfaces.nsIURL);
- deleteURI.fileName = files[i];
- deleteURI.QueryInterface(Components.interfaces.nsIURI);
- Zotero.Utilities.HTTP.WebDAV.doDelete(deleteURI, function (req) {
- switch (req.status) {
- case 204:
- // IIS 5.1 and Sakai return 200
- case 200:
- var fileDeleted = true;
- break;
-
- case 404:
- var fileDeleted = false;
- break;
-
- default:
- if (last && callback) {
- callback(results);
- }
-
- results.error.push(fileName);
- var msg = "An error occurred attempting to delete "
- + "'" + fileName
- + "' (" + req.status + " " + req.statusText + ").";
- _error(msg);
- return;
- }
-
- // If an item file URI, get the property URI
- var deletePropURI = _getPropertyURIFromItemURI(deleteURI);
- if (!deletePropURI) {
- if (fileDeleted) {
- results.deleted.push(fileName);
- }
- else {
- results.missing.push(fileName);
- }
- if (last && callback) {
- callback(results);
- }
- return;
- }
-
- // If property file appears separately in delete queue,
- // remove it, since we're taking care of it here
- var propIndex = files.indexOf(deletePropURI.fileName);
- if (propIndex > i) {
- delete files[propIndex];
- i--;
- last = (i == files.length - 1);
- }
-
- // Delete property file
- Zotero.Utilities.HTTP.WebDAV.doDelete(deletePropURI, function (req) {
- switch (req.status) {
- case 204:
- // IIS 5.1 and Sakai return 200
- case 200:
- results.deleted.push(fileName);
- break;
-
- case 404:
- if (fileDeleted) {
- results.deleted.push(fileName);
- }
- else {
- results.missing.push(fileName);
- }
- break;
-
- default:
- var error = true;
- }
-
- if (last && callback) {
- callback(results);
- }
-
- if (error) {
- results.error.push(fileName);
- var msg = "An error occurred attempting to delete "
- + "'" + fileName
- + "' (" + req.status + " " + req.statusText + ").";
- _error(msg);
- }
- });
- });
- }
- }
-
-
- /**
- * @param {Function} callback Function to pass URI and result value to
- * @param {Object} errorCallbacks
- */
- this.checkServer = function (callback) {
- try {
- var uri = this.rootURI;
- }
- catch (e) {
- switch (e.name) {
- case 'Z_ERROR_NO_URL':
- callback(null, Zotero.Sync.Storage.ERROR_NO_URL);
- return;
-
- case 'Z_ERROR_NO_PASSWORD':
- callback(null, Zotero.Sync.Storage.ERROR_NO_PASSWORD);
- return;
-
- default:
- Zotero.debug(e);
- Components.utils.reportError(e);
- callback(null, Zotero.Sync.Storage.ERROR_UNKNOWN);
- return;
- }
- }
-
- var requestHolder = { request: null };
-
- var prolog = '<?xml version="1.0" encoding="utf-8" ?>\n';
- var D = new Namespace("D", "DAV:");
- var nsDeclarations = 'xmlns:' + D.prefix + '=' + '"' + D.uri + '"';
-
- var requestXML = new XML('<D:propfind ' + nsDeclarations + '/>');
- requestXML.D::prop = '';
- // IIS 5.1 requires at least one property in PROPFIND
- requestXML.D::prop.D::getcontentlength = '';
-
- var xmlstr = prolog + requestXML.toXMLString();
-
- // Test whether URL is WebDAV-enabled
- var request = Zotero.Utilities.HTTP.doOptions(uri, function (req) {
- Zotero.debug(req.status);
-
- // Timeout
- if (req.status == 0) {
- callback(uri, Zotero.Sync.Storage.ERROR_UNREACHABLE);
- return;
- }
-
- Zotero.debug(req.getAllResponseHeaders());
- Zotero.debug(req.responseText);
- Zotero.debug(req.status);
-
- switch (req.status) {
- case 400:
- callback(uri, Zotero.Sync.Storage.ERROR_BAD_REQUEST);
- return;
-
- case 401:
- callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
- return;
-
- case 403:
- callback(uri, Zotero.Sync.Storage.ERROR_FORBIDDEN);
- return;
-
- case 500:
- callback(uri, Zotero.Sync.Storage.ERROR_SERVER_ERROR);
- return;
- }
-
- var dav = req.getResponseHeader("DAV");
- if (dav == null) {
- callback(uri, Zotero.Sync.Storage.ERROR_NOT_DAV);
- return;
- }
-
- var headers = { Depth: 0 };
-
- var authHeader = Zotero.Utilities.HTTP.getChannelAuthorization(req.channel);
- if (authHeader) {
- _cachedCredentials.authHeader = authHeader;
- headers.Authorization = authHeader;
- // Create a version without Depth
- var authHeaders = { Authorization: authHeader };
- var authRequired = true;
- }
- else {
- var authRequired = false;
- }
-
- // Test whether Zotero directory exists
- Zotero.Utilities.HTTP.WebDAV.doProp("PROPFIND", uri, xmlstr, function (req) {
- Zotero.debug(req.responseText);
- Zotero.debug(req.status);
-
- switch (req.status) {
- case 207:
- // Test if Zotero directory is writable
- var testFileURI = uri.clone();
- testFileURI.spec += "zotero-test-file";
- Zotero.Utilities.HTTP.WebDAV.doPut(testFileURI, "", function (req) {
- Zotero.debug(req.responseText);
- Zotero.debug(req.status);
-
- switch (req.status) {
- case 200:
- case 201:
- case 204:
- // Delete test file
- Zotero.Utilities.HTTP.WebDAV.doDelete(
- testFileURI,
- function (req) {
- Zotero.debug(req.responseText);
- Zotero.debug(req.status);
-
- switch (req.status) {
- case 200: // IIS 5.1 and Sakai return 200
- case 204:
- callback(
- uri,
- Zotero.Sync.Storage.SUCCESS,
- !authRequired
- );
- return;
-
- case 401:
- callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
- return;
-
- case 403:
- callback(uri, Zotero.Sync.Storage.ERROR_FORBIDDEN);
- return;
-
- default:
- callback(uri, Zotero.Sync.Storage.ERROR_UNKNOWN);
- return;
- }
- }
- );
- return;
-
- case 401:
- callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
- return;
-
- case 403:
- callback(uri, Zotero.Sync.Storage.ERROR_FORBIDDEN);
- return;
-
- case 500:
- callback(uri, Zotero.Sync.Storage.ERROR_SERVER_ERROR);
- return;
-
- default:
- callback(uri, Zotero.Sync.Storage.ERROR_UNKNOWN);
- return;
- }
- });
- return;
-
- case 400:
- callback(uri, Zotero.Sync.Storage.ERROR_BAD_REQUEST);
- return;
-
- case 401:
- callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
- return;
-
- case 403:
- callback(uri, Zotero.Sync.Storage.ERROR_FORBIDDEN);
- return;
-
- case 404:
- var parentURI = uri.clone();
- parentURI.spec = parentURI.spec.replace(/zotero\/$/, '');
-
- // Zotero directory wasn't found, so see if at least
- // the parent directory exists
- Zotero.Utilities.HTTP.WebDAV.doProp("PROPFIND", parentURI, xmlstr,
- function (req) {
- Zotero.debug(req.responseText);
- Zotero.debug(req.status);
-
- switch (req.status) {
- // Parent directory existed
- case 207:
- callback(uri, Zotero.Sync.Storage.ERROR_ZOTERO_DIR_NOT_FOUND);
- return;
-
- case 400:
- callback(uri, Zotero.Sync.Storage.ERROR_BAD_REQUEST);
- return;
-
- case 401:
- callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
- return;
-
- // Parent directory wasn't found either
- case 404:
- callback(uri, Zotero.Sync.Storage.ERROR_PARENT_DIR_NOT_FOUND);
- return;
-
- default:
- callback(uri, Zotero.Sync.Storage.ERROR_UNKNOWN);
- return;
- }
- }, headers);
- return;
-
- case 500:
- callback(uri, Zotero.Sync.Storage.ERROR_SERVER_ERROR);
- return;
-
- default:
- callback(uri, Zotero.Sync.Storage.ERROR_UNKNOWN);
- return;
- }
- }, headers);
- });
-
- if (!request) {
- callback(uri, Zotero.Sync.Storage.ERROR_OFFLINE);
- }
-
- requestHolder.request = request;
- return requestHolder;
- }
-
-
- this.checkServerCallback = function (uri, status, authRequired, window, skipSuccessMessage) {
- var promptService =
- Components.classes["@mozilla.org/embedcomp/prompt-service;1"].
- createInstance(Components.interfaces.nsIPromptService);
- if (uri) {
- var spec = uri.scheme + '://' + uri.hostPort + uri.path;
- }
-
- switch (status) {
- case Zotero.Sync.Storage.SUCCESS:
- if (!skipSuccessMessage) {
- promptService.alert(
- window,
- "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 WebDAV URL.";
- break;
-
- case Zotero.Sync.Storage.ERROR_NO_PASSWORD:
- var errorMessage = "Please enter a password.";
- 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 WebDAV server did not accept the "
- + "username and password you entered." + " "
- + "Please check your storage 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 the WebDAV server." + " "
- + "Please check your file sync 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(
- window,
- // 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:
- if (!skipSuccessMessage) {
- promptService.alert(
- window,
- "Server configuration verified",
- "File sync 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 file sync settings or "
- + "contact your server administrator.";
- break;
- }
-
- // TEMP
- if (!errorMessage) {
- var errorMessage = status;
- }
- promptService.alert(window, errorTitle, errorMessage);
- });
-
- return false;
- }
-
- if (!skipSuccessMessage) {
- if (!errorTitle) {
- var errorTitle = Zotero.getString("general.error");
- }
- // TEMP
- if (!errorMessage) {
- var errorMessage = status;
- }
- promptService.alert(window, errorTitle, errorMessage);
- }
- return false;
- }
-
-
this.finish = function (cancelled, skipSuccessFile) {
if (!_syncInProgress) {
throw ("Sync not in progress in Zotero.Sync.Storage.finish()");
}
// Upload success file when done
- if (!cancelled && !this.resyncOnFinish && !skipSuccessFile) {
- _uploadSuccessFile(function () {
- Zotero.Sync.Storage.finish(false, true);
- });
+ if (!this.resyncOnFinish && !skipSuccessFile) {
+ // If we finished successfully and didn't upload any files, save the
+ // last sync time locally rather than setting a new one on the server,
+ // since we don't want other clients to check for new files
+ var uploadQueue = Zotero.Sync.Storage.QueueManager.get('upload');
+ var useLastSyncTime = !cancelled && uploadQueue.totalRequests == 0;
+
+ _session.setLastSyncTime(function () {
+ Zotero.Sync.Storage.finish(cancelled, true);
+ }, useLastSyncTime);
return;
}
- Zotero.debug("Storage sync is complete");
+ Zotero.debug(_session.name + " sync is complete");
_syncInProgress = false;
- if (!cancelled && this.resyncOnFinish) {
+ if (this.resyncOnFinish) {
Zotero.debug("Force-resyncing items in conflict");
this.resyncOnFinish = false;
- this.sync();
+ this.sync(_session.module, _callbacks);
return;
}
- if (cancelled || !_changesMade) {
- if (!_changesMade) {
- Zotero.debug("No changes made during storage sync");
- }
- Zotero.Sync.Runner.reset();
- }
+ _session = null;
- Zotero.Sync.Runner.next();
- }
-
-
- function _getSuccessFileTimestamp(callback) {
- try {
- var uri = Zotero.Sync.Storage.rootURI;
- var successFileURI = uri.clone();
- successFileURI.spec += "lastsync";
- Zotero.Utilities.HTTP.doGet(successFileURI, function (req) {
- var ts = undefined;
- try {
- Zotero.debug(req.responseText);
- Zotero.debug(req.status);
- var lastModified = req.getResponseHeader("Last-modified");
- var date = new Date(lastModified);
- Zotero.debug("Last successful storage sync was " + date);
- var ts = Zotero.Date.toUnixTimestamp(date);
- }
- finally {
- callback(ts);
- }
- });
- return;
+ if (!_changesMade) {
+ Zotero.debug("No changes made during storage sync");
}
- catch (e) {
- Zotero.debug(e);
- Components.utils.reportError(e);
- callback();
+
+ if (cancelled) {
+ _callbacks.onStop();
return;
}
- }
-
-
- function _uploadSuccessFile(callback) {
- try {
- var uri = Zotero.Sync.Storage.rootURI;
- var successFileURI = uri.clone();
- successFileURI.spec += "lastsync";
- Zotero.Utilities.HTTP.WebDAV.doPut(successFileURI, "", function (req) {
- Zotero.debug(req.responseText);
- Zotero.debug(req.status);
-
- switch (req.status) {
- case 200:
- case 201:
- case 204:
- _getSuccessFileTimestamp(function (ts) {
- if (ts) {
- var sql = "REPLACE INTO version VALUES ('storage', ?)";
- Zotero.DB.query(sql, { int: ts });
- }
- if (callback) {
- callback();
- }
- });
- return;
- }
-
- var msg = "Unexpected error code " + req.status + " uploading storage success file";
- Zotero.debug(msg, 2);
- Components.utils.reportError(msg);
- if (callback) {
- callback();
- }
- });
- }
- catch (e) {
- Zotero.debug(e);
- Components.utils.reportError(e);
- if (callback) {
- callback();
- }
+
+ if (!_changesMade) {
+ _callbacks.onSkip();
return;
}
+
+ _callbacks.onSuccess();
}
- /**
- * Get the storage URI for an item
- *
- * @inner
- * @param {Zotero.Item}
- * @return {nsIURI} URI of file on storage server
- */
- function _getItemURI(item) {
- var uri = Zotero.Sync.Storage.rootURI;
- uri.spec = uri.spec + item.key + '.zip';
- return uri;
- }
-
-
- /**
- * Get the storage property file URI for an item
- *
- * @inner
- * @param {Zotero.Item}
- * @return {nsIURI} URI of property file on storage server
- */
- function _getItemPropertyURI(item) {
- var uri = Zotero.Sync.Storage.rootURI;
- uri.spec = uri.spec + item.key + '.prop';
- return uri;
- }
-
-
- /**
- * Get the storage property file URI corresponding to a given item storage URI
- *
- * @param {nsIURI} Item storage URI
- * @return {nsIURI|FALSE} Property file URI, or FALSE if not an item storage URI
- */
- function _getPropertyURIFromItemURI(uri) {
- if (!uri.spec.match(/\.zip$/)) {
- return false;
- }
- var propURI = uri.clone();
- propURI.QueryInterface(Components.interfaces.nsIURL);
- propURI.fileName = uri.fileName.replace(/\.zip$/, '.prop');
- propURI.QueryInterface(Components.interfaces.nsIURI);
- return propURI;
- }
-
-
- /**
- * @inner
- * @param {XMLHTTPRequest} req
- * @throws
- */
- function _checkResponse(req) {
- if (!req.responseText) {
- _error('Empty response from server');
- }
- if (!req.responseXML ||
- !req.responseXML.firstChild ||
- !(req.responseXML.firstChild.namespaceURI == 'DAV:' &&
- req.responseXML.firstChild.localName == 'multistatus') ||
- !req.responseXML.childNodes[0].firstChild) {
- Zotero.debug(req.responseText);
- _error('Invalid response from storage server');
- }
- }
-
-
-
//
// Stop requests, log error, and
//
function _error(e) {
if (_syncInProgress) {
- Zotero.Sync.Storage.QueueManager.cancel();
+ Zotero.Sync.Storage.QueueManager.cancel(true);
_syncInProgress = false;
+ _session = null;
}
Zotero.DB.rollbackAllTransactions();
Zotero.debug(e, 1);
- Zotero.Sync.Runner.setError(e.message ? e.message : e);
- Zotero.Sync.Runner.reset();
- throw (e);
+
+ // If we get a quota error, log and continue
+ if (e.error && e.error == Zotero.Error.ERROR_ZFS_OVER_QUOTA && _callbacks.onWarning) {
+ _callbacks.onWarning(e);
+ _callbacks.onSuccess();
+ }
+ else if (e.error && e.error == Zotero.Error.ERROR_ZFS_FILE_EDITING_DENIED) {
+ setTimeout(function () {
+ var group = Zotero.Groups.get(e.data.groupID);
+
+ var pr = Components.classes["@mozilla.org/network/default-prompt;1"]
+ .createInstance(Components.interfaces.nsIPrompt);
+ var buttonFlags = (pr.BUTTON_POS_0) * (pr.BUTTON_TITLE_IS_STRING)
+ + (pr.BUTTON_POS_1) * (pr.BUTTON_TITLE_CANCEL)
+ + pr.BUTTON_DELAY_ENABLE;
+ var index = pr.confirmEx(
+ Zotero.getString('general.warning'),
+ // TODO: localize
+ "You no longer have file editing access to the Zotero group '" + group.name + "', "
+ + "and files you've added or edited cannot be synced to the server.\n\n"
+ + "If you continue, your copy of the group will be reset to its state "
+ + "on the server, and local modifications to items and files will be lost.\n\n"
+ + "If you would like a chance to copy changed items and files elsewhere, "
+ + "cancel the sync now.",
+ buttonFlags,
+ "Reset Group and Sync",
+ null, null, null, {}
+ );
+
+ if (index == 0) {
+ group.erase();
+ Zotero.Sync.Server.resetClient();
+ Zotero.Sync.Storage.resetAllSyncStates();
+ Zotero.Sync.Runner.sync();
+ return;
+ }
+ }, 1);
+ _callbacks.onError(e);
+ }
+ else if (_callbacks.onError) {
+ _callbacks.onError(e);
+ }
+ else {
+ throw (e);
+ }
}
}
@@ -2212,6 +1170,9 @@ Zotero.Sync.Storage.QueueManager = new function () {
queue.maxConcurrentRequests =
Zotero.Prefs.get('sync.storage.maxUploads')
break;
+
+ default:
+ throw ("Invalid queue '" + queueName + "' in Zotero.Sync.Storage.QueueManager.get()");
}
_queues[queueName] = queue;
}
@@ -2222,11 +1183,20 @@ Zotero.Sync.Storage.QueueManager = new function () {
/**
* Stop all queues
+ *
+ * @param {Boolean} [skipStorageFinish=false] Don't call Zotero.Sync.Storage.finish()
+ * when done (used when we stopped because of
+ * an error)
*/
- this.cancel = function () {
+ this.cancel = function (skipStorageFinish) {
this._cancelled = true;
+ if (skipStorageFinish) {
+ this._skipStorageFinish = true;
+ }
for each(var queue in _queues) {
- queue.stop();
+ if (!queue.isFinished() && !queue.isStopping()) {
+ queue.stop();
+ }
}
_conflicts = [];
}
@@ -2244,6 +1214,12 @@ Zotero.Sync.Storage.QueueManager = new function () {
_conflicts = [];
}
+ if (this._skipStorageFinish) {
+ this._cancelled = false;
+ this._skipStorageFinish = false;
+ return;
+ }
+
Zotero.Sync.Storage.finish(this._cancelled);
this._cancelled = false;
}
@@ -2336,6 +1312,10 @@ Zotero.Sync.Storage.QueueManager = new function () {
this.addConflict = function (requestName, localData, remoteData) {
+ Zotero.debug('===========');
+ Zotero.debug(localData);
+ Zotero.debug(remoteData);
+
_conflicts.push({
name: requestName,
localData: localData,
@@ -2380,7 +1360,7 @@ Zotero.Sync.Storage.QueueManager = new function () {
function _reconcileConflicts() {
var objectPairs = [];
for each(var conflict in _conflicts) {
- var item = Zotero.Items.getByLibraryAndKey(null, conflict.name);
+ var item = Zotero.Sync.Storage.getItemFromRequestName(conflict.name);
var item1 = item.clone(false, false, true);
item1.setField('dateModified',
Zotero.Date.dateToSQL(new Date(conflict.localData.modTime * 1000), true));
@@ -2415,7 +1395,7 @@ Zotero.Sync.Storage.QueueManager = new function () {
// Since we're only putting cloned items into the merge window,
// we have to manually set the ids
for (var i=0; i<_conflicts.length; i++) {
- io.dataOut[i].id = Zotero.Items.getByLibraryAndKey(null, _conflicts[i].name).id;
+ io.dataOut[i].id = Zotero.Sync.Storage.getItemFromRequestName(_conflicts[i].name).id;
}
return io.dataOut;
@@ -2467,6 +1447,8 @@ Zotero.Sync.Storage.Queue = function (name) {
});
this.maxConcurrentRequests = 1;
+ this.__defineGetter__('running', function () _running);
+ this.__defineGetter__('stopping', function () _stopping);
this.activeRequests = 0;
this.__defineGetter__('finishedRequests', function () {
return _finishedReqs;
@@ -2486,8 +1468,10 @@ Zotero.Sync.Storage.Queue = function (name) {
Zotero.debug(this.Name + " queue is done");
// DEBUG info
Zotero.debug("Active requests: " + this.activeRequests);
- Zotero.debug("Errors:");
- Zotero.debug(this._errors);
+ if (this._errors) {
+ Zotero.debug("Errors:");
+ Zotero.debug(this._errors);
+ }
if (this.activeRequests) {
throw (this.Name + " queue can't be finished if there "
@@ -2745,7 +1729,7 @@ function _updateSizeMultiplier(mult) {
/**
* Transfer request for storage sync
*
- * @param {String} name Identifier for request (e.g., item key)
+ * @param {String} name Identifier for request (e.g., "[libraryID]/[key]")
* @param {Function} onStart Callback when request is started
*/
Zotero.Sync.Storage.Request = function (name, onStart) {
@@ -2805,11 +1789,11 @@ Zotero.Sync.Storage.Request.prototype.__defineGetter__('remaining', function ()
}
else if (remaining > this._remaining) {
Zotero.debug(remaining + " is greater than the last remaining amount of "
- + this._remaining);
+ + this._remaining + " for request " + this.name);
remaining = this._remaining;
}
else if (remaining < 0) {
- Zotero.debug();
+ Zotero.debug(remaining + " is less than 0 for request " + this.name);
}
else {
this._remaining = remaining;
@@ -2819,6 +1803,11 @@ Zotero.Sync.Storage.Request.prototype.__defineGetter__('remaining', function ()
});
+Zotero.Sync.Storage.Request.prototype.setChannel = function (channel) {
+ this.channel = channel;
+}
+
+
Zotero.Sync.Storage.Request.prototype.start = function () {
if (!this.queue) {
throw ("Request '" + this.name + "' must be added to a queue before starting");
@@ -2858,7 +1847,7 @@ Zotero.Sync.Storage.Request.prototype.isFinished = function () {
*/
Zotero.Sync.Storage.Request.prototype.onProgress = function (channel, progress, progressMax) {
if (!this._running) {
- throw ("Trying to update a finished request in "
+ throw ("Trying to update finished request " + this.name + " in "
+ "Zotero.Sync.Storage.Request.onProgress()");
}
@@ -2899,7 +1888,24 @@ Zotero.Sync.Storage.Request.prototype.error = function (msg) {
* Stop the request's underlying network request, if there is one
*/
Zotero.Sync.Storage.Request.prototype.stop = function () {
- if (!this._running || !this.channel) {
+ var finishNow = false;
+ try {
+ // If upload already finished, finish() will never be called otherwise
+ if (this.channel) {
+ this.channel.QueryInterface(Components.interfaces.nsIHttpChannel);
+ // Throws error if request not finished
+ this.channel.requestSucceeded;
+ Zotero.debug("Channel is no longer running for request " + this.name);
+ Zotero.debug(this.channel.requestSucceeded);
+ finishNow = true;
+ }
+ else {
+ Zotero.debug("No channel to stop for request " + this.name);
+ }
+ }
+ catch (e) { Zotero.debug(e); }
+
+ if (!this._running || !this.channel || finishNow) {
this.finish();
return;
}
@@ -3097,7 +2103,8 @@ Zotero.Sync.Storage.StreamListener.prototype = {
_onStart: function (request) {
//Zotero.debug('Starting request');
if (this._data && this._data.onStart) {
- this._data.onStart(request);
+ var data = this._getPassData();
+ this._data.onStart(request, data);
}
},
@@ -3122,33 +2129,37 @@ Zotero.Sync.Storage.StreamListener.prototype = {
}
}
- // Make copy of data without callbacks to pass along
- var passData = {};
- for (var i in this._data) {
- switch (i) {
- case "onStart":
- case "onProgress":
- case "onStop":
- case "onCancel":
- continue;
- }
- passData[i] = this._data[i];
- }
+ var data = this._getPassData();
if (cancelled) {
if (this._data.onCancel) {
- this._data.onCancel(request, status, passData);
+ this._data.onCancel(request, status, data);
}
}
else {
if (this._data.onStop) {
- this._data.onStop(request, status, this._response, passData);
+ this._data.onStop(request, status, this._response, data);
}
}
this._channel = null;
},
+ _getPassData: function () {
+ // Make copy of data without callbacks to pass along
+ var passData = {};
+ for (var i in this._data) {
+ switch (i) {
+ case "onStart":
+ case "onProgress":
+ case "onStop":
+ case "onCancel":
+ continue;
+ }
+ passData[i] = this._data[i];
+ }
+ return passData;
+ },
// nsIInterfaceRequestor
getInterface: function (iid) {
diff --git a/chrome/content/zotero/xpcom/storage/session.js b/chrome/content/zotero/xpcom/storage/session.js
@@ -0,0 +1,166 @@
+Zotero.Sync.Storage.Session = function (module, callbacks) {
+ switch (module) {
+ case 'webdav':
+ this._session = new Zotero.Sync.Storage.Session.WebDAV(callbacks);
+ break;
+
+ case 'zfs':
+ this._session = new Zotero.Sync.Storage.Session.ZFS(callbacks);
+ break;
+
+ default:
+ throw ("Invalid storage session module '" + module + "'");
+ }
+
+ this.module = module;
+ this.onError = callbacks.onError;
+}
+
+Zotero.Sync.Storage.Session.prototype.__defineGetter__('name', function () this._session.name);
+Zotero.Sync.Storage.Session.prototype.__defineGetter__('includeUserFiles', function () this._session.includeUserFiles);
+Zotero.Sync.Storage.Session.prototype.__defineGetter__('includeGroupFiles', function () this._session.includeGroupFiles);
+
+Zotero.Sync.Storage.Session.prototype.__defineGetter__('enabled', function () {
+ try {
+ return this._session.enabled;
+ }
+ catch (e) {
+ this.onError(e);
+ }
+});
+
+Zotero.Sync.Storage.Session.prototype.__defineGetter__('verified', function () {
+ try {
+ return this._session.verified;
+ }
+ catch (e) {
+ this.onError(e);
+ }
+});
+
+Zotero.Sync.Storage.Session.prototype.__defineGetter__('active', function () {
+ try {
+ return this._session.active;
+ }
+ catch (e) {
+ this.onError(e);
+ }
+});
+
+Zotero.Sync.Storage.Session.prototype.__defineGetter__('username', function () {
+ try {
+ return this._session.username;
+ }
+ catch (e) {
+ this.onError(e);
+ }
+});
+
+Zotero.Sync.Storage.Session.prototype.__defineGetter__('password', function () {
+ try {
+ return this._session.password;
+ }
+ catch (e) {
+ this.onError(e);
+ }
+});
+
+Zotero.Sync.Storage.Session.prototype.__defineSetter__('password', function (val) {
+ try {
+ this._session.password = val;
+ }
+ catch (e) {
+ this.onError(e);
+ }
+});
+
+
+Zotero.Sync.Storage.Session.prototype.initFromPrefs = function () {
+ try {
+ return this._session.init();
+ }
+ catch (e) {
+ this.onError(e);
+ }
+}
+
+Zotero.Sync.Storage.Session.prototype.initFromPrefs = function () {
+ try {
+ return this._session.initFromPrefs();
+ }
+ catch (e) {
+ this.onError(e);
+ }
+}
+
+Zotero.Sync.Storage.Session.prototype.downloadFile = function (request) {
+ try {
+ this._session.downloadFile(request);
+ }
+ catch (e) {
+ this.onError(e);
+ }
+}
+
+Zotero.Sync.Storage.Session.prototype.uploadFile = function (request) {
+ try {
+ this._session.uploadFile(request);
+ }
+ catch (e) {
+ this.onError(e);
+ }
+}
+
+Zotero.Sync.Storage.Session.prototype.getLastSyncTime = function (callback) {
+ try {
+ this._session.getLastSyncTime(callback);
+ }
+ catch (e) {
+ this.onError(e);
+ }
+}
+
+Zotero.Sync.Storage.Session.prototype.setLastSyncTime = function (callback, useLastSyncTime) {
+ try {
+ this._session.setLastSyncTime(callback, useLastSyncTime);
+ }
+ catch (e) {
+ this.onError(e);
+ }
+}
+
+Zotero.Sync.Storage.Session.prototype.checkServer = function (callback) {
+ try {
+ this._session.checkServer(callback);
+ }
+ catch (e) {
+ this.onError(e);
+ }
+}
+
+Zotero.Sync.Storage.Session.prototype.checkServerCallback = function (uri, status, authRequired, window, skipSuccessMessage) {
+ try {
+ return this._session.checkServerCallback(uri, status, authRequired, window, skipSuccessMessage);
+ }
+ catch (e) {
+ this.onError(e);
+ }
+}
+
+Zotero.Sync.Storage.Session.prototype.purgeDeletedStorageFiles = function (callback) {
+ try {
+ this._session.purgeDeletedStorageFiles(callback);
+ }
+ catch (e) {
+ this.onError(e);
+ }
+}
+
+Zotero.Sync.Storage.Session.prototype.purgeOrphanedStorageFiles = function (callback) {
+ try {
+ this._session.purgeOrphanedStorageFiles(callback);
+ }
+ catch (e) {
+ this.onError(e);
+ }
+}
diff --git a/chrome/content/zotero/xpcom/storage/webdav.js b/chrome/content/zotero/xpcom/storage/webdav.js
@@ -0,0 +1,1401 @@
+Zotero.Sync.Storage.Session.WebDAV = function (callbacks) {
+ this.onChangesMade = callbacks.onChangesMade ? callbacks.onChangesMade : function () {};
+ this.onError = callbacks.onError ? callbacks.onError : function () {};
+
+ this._parentURI;
+ this._rootURI;
+ this._cachedCredentials = false;
+}
+
+Zotero.Sync.Storage.Session.WebDAV.prototype.name = "WebDAV";
+
+Zotero.Sync.Storage.Session.WebDAV.prototype.__defineGetter__('includeUserFiles', function () {
+ return Zotero.Prefs.get("sync.storage.enabled") && Zotero.Prefs.get("sync.storage.protocol") == 'webdav';
+});
+
+Zotero.Sync.Storage.Session.WebDAV.prototype.includeGroupItems = false;
+
+Zotero.Sync.Storage.Session.WebDAV.prototype.__defineGetter__('enabled', function () {
+ return this.includeUserFiles;
+});
+
+Zotero.Sync.Storage.Session.WebDAV.prototype.__defineGetter__('verified', function () {
+ return Zotero.Prefs.get("sync.storage.verified");
+});
+
+Zotero.Sync.Storage.Session.WebDAV.prototype.__defineGetter__('active', function () {
+ return this.enabled && this.verified;
+});
+
+Zotero.Sync.Storage.Session.WebDAV.prototype._loginManagerHost = 'chrome://zotero';
+Zotero.Sync.Storage.Session.WebDAV.prototype._loginManagerURL = 'Zotero Storage Server';
+
+Zotero.Sync.Storage.Session.WebDAV.prototype.__defineGetter__('username', function () {
+ return Zotero.Prefs.get('sync.storage.username');
+});
+
+Zotero.Sync.Storage.Session.WebDAV.prototype.__defineGetter__('password', function () {
+ var username = this.username;
+
+ if (!username) {
+ Zotero.debug('Username not set before setting Zotero.Sync.Storage.password');
+ return '';
+ }
+
+ Zotero.debug('Getting WebDAV password');
+ var loginManager = Components.classes["@mozilla.org/login-manager;1"]
+ .getService(Components.interfaces.nsILoginManager);
+ var logins = loginManager.findLogins({}, this._loginManagerHost, this._loginManagerURL, null);
+
+ // Find user from returned array of nsILoginInfo objects
+ for (var i = 0; i < logins.length; i++) {
+ if (logins[i].username == username) {
+ return logins[i].password;
+ }
+ }
+
+ return '';
+});
+
+Zotero.Sync.Storage.Session.WebDAV.prototype.__defineSetter__('password', function (password) {
+ var username = this.username;
+ if (!username) {
+ Zotero.debug('Username not set before setting Zotero.Sync.Server.password');
+ return;
+ }
+
+ this._cachedCredentials = false;
+
+ var loginManager = Components.classes["@mozilla.org/login-manager;1"]
+ .getService(Components.interfaces.nsILoginManager);
+ var logins = loginManager.findLogins({}, this._loginManagerHost, this._loginManagerURL, null);
+
+ for (var i = 0; i < logins.length; i++) {
+ Zotero.debug('Clearing Zotero storage passwords');
+ loginManager.removeLogin(logins[i]);
+ break;
+ }
+
+ if (password) {
+ Zotero.debug(this._loginManagerURL);
+ var nsLoginInfo = new Components.Constructor("@mozilla.org/login-manager/loginInfo;1",
+ Components.interfaces.nsILoginInfo, "init");
+ var loginInfo = new nsLoginInfo(this._loginManagerHost, this._loginManagerURL,
+ null, username, password, "", "");
+ loginManager.addLogin(loginInfo);
+ }
+});
+
+Zotero.Sync.Storage.Session.WebDAV.prototype.__defineGetter__('rootURI', function () {
+ if (!this._rootURI) {
+ throw ("Root URI not initialized in Zotero.Sync.Storage.Session.WebDAV.rootURI");
+ }
+ return this._rootURI.clone();
+});
+
+Zotero.Sync.Storage.Session.WebDAV.prototype.__defineGetter__('parentURI', function () {
+ if (!this._parentURI) {
+ throw ("Parent URI not initialized in Zotero.Sync.Storage.Session.WebDAV.parentURI");
+ }
+ return this._parentURI.clone();
+});
+
+
+Zotero.Sync.Storage.Session.WebDAV.prototype.init = function (url, dir, username, password) {
+ if (!url) {
+ var msg = "Zotero storage URL not provided";
+ Zotero.debug(msg);
+ throw ({
+ message: msg,
+ name: "Z_ERROR_NO_URL",
+ filename: "webdav.js",
+ toString: function () { return this.message; }
+ });
+ }
+
+ if (username && !password) {
+ var msg = "Zotero storage password not provided";
+ Zotero.debug(msg);
+ throw ({
+ message: msg,
+ name: "Z_ERROR_NO_PASSWORD",
+ filename: "webdav.js",
+ toString: function () { return this.message; }
+ });
+ }
+
+ var ios = Components.classes["@mozilla.org/network/io-service;1"].
+ getService(Components.interfaces.nsIIOService);
+ try {
+ var uri = ios.newURI(url, null, null);
+ if (username) {
+ uri.username = username;
+ uri.password = password;
+ }
+ }
+ catch (e) {
+ Zotero.debug(e);
+ Components.utils.reportError(e);
+ return false;
+ }
+ if (!uri.spec.match(/\/$/)) {
+ uri.spec += "/";
+ }
+ this._parentURI = uri;
+
+ var uri = uri.clone();
+ uri.spec += "zotero/";
+ this._rootURI = uri;
+ return true;
+}
+
+
+Zotero.Sync.Storage.Session.WebDAV.prototype.initFromPrefs = function () {
+ var scheme = Zotero.Prefs.get('sync.storage.scheme');
+ switch (scheme) {
+ case 'http':
+ case 'https':
+ break;
+
+ default:
+ throw ("Invalid storage scheme '" + scheme
+ + "' in Zotero.Sync.Storage.Session.WebDAV.rootURI");
+ }
+
+ var url = Zotero.Prefs.get('sync.storage.url');
+
+ url = scheme + '://' + url;
+ var dir = "zotero";
+ var username = this.username;
+ var password = this.password;
+
+ return this.init(url, dir, username, password);
+}
+
+
+/**
+ * Get mod time of file on storage server
+ *
+ * @param {Zotero.Item} item
+ * @param {Function} callback Callback f(item, mdate)
+ */
+Zotero.Sync.Storage.Session.WebDAV.prototype._getStorageModificationTime = function (item, callback) {
+ var uri = this._getItemPropertyURI(item);
+
+ var self = this;
+
+ Zotero.Utilities.HTTP.doGet(uri, function (req) {
+ var funcName = "Zotero.Sync.Storage._getStorageModificationTime()";
+
+ // mod_speling can return 300s for 404s with base name matches
+ if (req.status == 404 || req.status == 300) {
+ callback(item, false);
+ return;
+ }
+ else if (req.status != 200) {
+ Zotero.debug(req.responseText);
+ self.onError("Unexpected status code " + req.status + " in " + funcName);
+ return;
+ }
+
+ Zotero.debug(req.responseText);
+
+ var mtime = req.responseText;
+ // No modification time set
+ if (!mtime) {
+ callback(item, false);
+ return;
+ }
+
+ var mdate = new Date(mtime * 1000);
+ callback(item, mdate);
+ });
+}
+
+
+/**
+ * Set mod time of file on storage server
+ *
+ * @param {Zotero.Item} item
+ * @param {Function} callback Callback f(item, mtime)
+ */
+Zotero.Sync.Storage.Session.WebDAV.prototype._setStorageModificationTime = function (item, callback) {
+ var uri = this._getItemPropertyURI(item);
+
+ Zotero.Utilities.HTTP.WebDAV.doPut(uri, item.attachmentModificationTime + '', function (req) {
+ switch (req.status) {
+ case 200:
+ case 201:
+ case 204:
+ break;
+
+ default:
+ Zotero.debug(req.responseText);
+ throw ("Unexpected status code " + req.status + " in "
+ + "Zotero.Sync.Storage._setStorageModificationTime()");
+ }
+ callback(item, item.attachmentModificationTime);
+ });
+}
+
+
+
+/**
+ * Begin download process for individual file
+ *
+ * @param {Zotero.Sync.Storage.Request} [request]
+ */
+Zotero.Sync.Storage.Session.WebDAV.prototype.downloadFile = function (request) {
+ var item = Zotero.Sync.Storage.getItemFromRequestName(request.name);
+ if (!item) {
+ throw ("Item '" + request.name + "' not found in Zotero.Sync.Storage.Session.WebDAV.downloadFile()");
+ }
+
+ var self = this;
+
+ // Retrieve modification time from server to store locally afterwards
+ this._getStorageModificationTime(item, function (item, mdate) {
+ if (!request.isRunning()) {
+ Zotero.debug("Download request '" + request.name
+ + "' is no longer running after getting mod time");
+ return;
+ }
+
+ if (!mdate) {
+ Zotero.debug("Remote file not found for item " + item.key);
+ request.finish();
+ return;
+ }
+
+ try {
+ var syncModTime = Zotero.Date.toUnixTimestamp(mdate);
+
+ // Skip download if local file exists and matches mod time
+ var file = item.getFile();
+ if (file && file.exists()
+ && syncModTime == Math.round(file.lastModifiedTime / 1000)) {
+ Zotero.debug("File mod time matches remote file -- skipping download");
+
+ Zotero.DB.beginTransaction();
+ var syncState = Zotero.Sync.Storage.getSyncState(item.id);
+ var updateItem = syncState != 1;
+ Zotero.Sync.Storage.setSyncedModificationTime(item.id, syncModTime, updateItem);
+ Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
+ Zotero.DB.commitTransaction();
+ self.onChangesMade();
+ request.finish();
+ return;
+ }
+
+ var uri = self._getItemURI(item);
+ var destFile = Zotero.getTempDirectory();
+ destFile.append(item.key + '.zip.tmp');
+ if (destFile.exists()) {
+ destFile.remove(false);
+ }
+
+ var listener = new Zotero.Sync.Storage.StreamListener(
+ {
+ onStart: function (request, data) {
+ if (data.request.isFinished()) {
+ Zotero.debug("Download request " + data.request.name
+ + " stopped before download started -- closing channel");
+ request.cancel(0x804b0002); // NS_BINDING_ABORTED
+ return;
+ }
+ },
+ onProgress: function (a, b, c) {
+ request.onProgress(a, b, c)
+ },
+ onStop: function (request, status, response, data) {
+ if (status != 200) {
+ self.onError("Unexpected status code " + status
+ + " for request " + data.request.name + " in Zotero.Sync.Storage.Session.WebDAV.downloadFile()");
+ return;
+ }
+
+ // Don't try to process if the request has been cancelled
+ if (data.request.isFinished()) {
+ Zotero.debug("Download request " + data.request.name
+ + " is no longer running after file download");
+ return;
+ }
+
+ Zotero.debug("Finished download of " + destFile.path);
+
+ try {
+ Zotero.Sync.Storage.processDownload(data);
+ data.request.finish();
+ }
+ catch (e) {
+ self.onError(e);
+ }
+ },
+ request: request,
+ item: item,
+ compressed: true,
+ syncModTime: syncModTime
+ }
+ );
+
+ Zotero.debug('Saving with saveURI()');
+ const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
+ var wbp = Components
+ .classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+ .createInstance(nsIWBP);
+ wbp.persistFlags = nsIWBP.PERSIST_FLAGS_BYPASS_CACHE;
+
+ wbp.progressListener = listener;
+ wbp.saveURI(uri, null, null, null, null, destFile);
+ }
+ catch (e) {
+ request.error(e);
+ }
+ });
+}
+
+
+Zotero.Sync.Storage.Session.WebDAV.prototype.uploadFile = function (request) {
+ var self = this;
+ Zotero.Sync.Storage.createUploadFile(request, function (data) { self._processUploadFile(data); });
+}
+
+/**
+ * Upload the generated ZIP file to the server
+ *
+ * @param {Object} Object with 'request' property
+ * @return {void}
+ */
+Zotero.Sync.Storage.Session.WebDAV.prototype._processUploadFile = function (data) {
+ /*
+ _updateSizeMultiplier(
+ (100 - Zotero.Sync.Storage.compressionTracker.ratio) / 100
+ );
+ */
+ var request = data.request;
+ var item = Zotero.Sync.Storage.getItemFromRequestName(request.name);
+
+ var self = this;
+
+ this._getStorageModificationTime(item, function (item, mdate) {
+ try {
+ if (!request.isRunning()) {
+ Zotero.debug("Upload request '" + request.name
+ + "' is no longer running after getting mod time");
+ return;
+ }
+
+ // Check for conflict
+ if (Zotero.Sync.Storage.getSyncState(item.id)
+ != Zotero.Sync.Storage.SYNC_STATE_FORCE_UPLOAD) {
+ if (mdate) {
+ // Remote prop time
+ var mtime = Zotero.Date.toUnixTimestamp(mdate);
+ // Local file time
+ var fmtime = item.attachmentModificationTime;
+
+ if (fmtime == mtime) {
+ Zotero.debug("File mod time matches remote file -- skipping upload");
+
+ Zotero.DB.beginTransaction();
+ var syncState = Zotero.Sync.Storage.getSyncState(item.id);
+ Zotero.Sync.Storage.setSyncedModificationTime(item.id, fmtime, true);
+ Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
+ Zotero.DB.commitTransaction();
+ self.onChangesMade();
+ request.finish();
+ return;
+ }
+
+ var smtime = Zotero.Sync.Storage.getSyncedModificationTime(item.id);
+ if (smtime != mtime) {
+ var localData = { modTime: fmtime };
+ var remoteData = { modTime: mtime };
+ Zotero.Sync.Storage.QueueManager.addConflict(
+ request.name, localData, remoteData
+ );
+ Zotero.debug("Conflict -- last synced file mod time "
+ + "does not match time on storage server"
+ + " (" + smtime + " != " + mtime + ")");
+ request.finish();
+ return;
+ }
+ }
+ else {
+ Zotero.debug("Remote file not found for item " + item.id);
+ }
+ }
+
+ var file = Zotero.getTempDirectory();
+ file.append(item.key + '.zip');
+
+ var fis = Components.classes["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Components.interfaces.nsIFileInputStream);
+ fis.init(file, 0x01, 0, 0);
+
+ var bis = Components.classes["@mozilla.org/network/buffered-input-stream;1"]
+ .createInstance(Components.interfaces.nsIBufferedInputStream)
+ bis.init(fis, 64 * 1024);
+
+ var uri = self._getItemURI(item);
+
+ var ios = Components.classes["@mozilla.org/network/io-service;1"].
+ getService(Components.interfaces.nsIIOService);
+ var channel = ios.newChannelFromURI(uri);
+ channel.QueryInterface(Components.interfaces.nsIUploadChannel);
+ channel.setUploadStream(bis, 'application/octet-stream', -1);
+ channel.QueryInterface(Components.interfaces.nsIHttpChannel);
+ channel.requestMethod = 'PUT';
+ channel.allowPipelining = false;
+
+ channel.setRequestHeader('Keep-Alive', '', false);
+ channel.setRequestHeader('Connection', '', false);
+
+ var listener = new Zotero.Sync.Storage.StreamListener(
+ {
+ onProgress: function (a, b, c) {
+ request.onProgress(a, b, c);
+ },
+ onStop: function (httpRequest, status, response, data) { self._onUploadComplete(httpRequest, status, response,data); },
+ onCancel: function (httpRequest, status, data) { self._onUploadCancel(httpRequest, status, data); },
+ request: request,
+ item: item,
+ streams: [fis, bis]
+ }
+ );
+ channel.notificationCallbacks = listener;
+
+ var dispURI = uri.clone();
+ if (dispURI.password) {
+ dispURI.password = '********';
+ }
+ Zotero.debug("HTTP PUT of " + file.leafName + " to " + dispURI.spec);
+
+ channel.asyncOpen(listener, null);
+ }
+ catch (e) {
+ self.onError(e);
+ }
+ });
+}
+
+
+Zotero.Sync.Storage.Session.WebDAV.prototype._onUploadComplete = function (httpRequest, status, response, data) {
+ var request = data.request;
+ var item = data.item;
+ var url = httpRequest.name;
+
+ Zotero.debug("Upload of attachment " + item.key
+ + " finished with status code " + status);
+
+ switch (status) {
+ case 200:
+ case 201:
+ case 204:
+ break;
+
+ default:
+ this.onError("Unexpected file upload status " + status
+ + " in Zotero.Sync.Storage._onUploadComplete()");
+ return;
+ }
+
+ var self = this;
+
+ this._setStorageModificationTime(item, function (item, mtime) {
+ if (!request.isRunning()) {
+ Zotero.debug("Upload request '" + request.name
+ + "' is no longer running after getting mod time");
+ return;
+ }
+
+ Zotero.DB.beginTransaction();
+
+ Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
+ Zotero.Sync.Storage.setSyncedModificationTime(item.id, mtime, true);
+
+ var hash = item.attachmentHash;
+ Zotero.Sync.Storage.setSyncedHash(item.id, hash, true);
+
+ Zotero.DB.commitTransaction();
+
+ try {
+ var file = Zotero.getTempDirectory();
+ file.append(item.key + '.zip');
+ file.remove(false);
+ }
+ catch (e) {
+ Components.utils.reportError(e);
+ }
+
+ self.onChangesMade();
+ request.finish();
+ });
+}
+
+
+Zotero.Sync.Storage.Session.WebDAV.prototype._onUploadCancel = function (httpRequest, status, data) {
+ var request = data.request;
+ var item = data.item;
+
+ Zotero.debug("Upload of attachment " + item.key + " cancelled with status code " + status);
+
+ try {
+ var file = Zotero.getTempDirectory();
+ file.append(item.key + '.zip');
+ file.remove(false);
+ }
+ catch (e) {
+ Components.utils.reportError(e);
+ }
+
+ request.finish();
+}
+
+
+Zotero.Sync.Storage.Session.WebDAV.prototype.getLastSyncTime = function (callback) {
+ // Cache the credentials at the root URI
+ if (!this._cachedCredentials) {
+ var self = this;
+
+ Zotero.Utilities.HTTP.doOptions(this.rootURI, function (req) {
+ if (req.status != 200) {
+ self.onError("Unexpected status code " + req.status + " caching "
+ + "authentication credentials in Zotero.Sync.Storage.Session.WebDAV.getLastSyncTime()");
+ return;
+ }
+ self._cachedCredentials = true;
+ self.getLastSyncTime(callback);
+ });
+ return;
+ }
+
+ try {
+ var uri = this.rootURI;
+ var successFileURI = uri.clone();
+ successFileURI.spec += "lastsync";
+ Zotero.Utilities.HTTP.doHead(successFileURI, function (req) {
+ var ts = undefined;
+ try {
+ if (req.responseText) {
+ Zotero.debug(req.responseText);
+ }
+ Zotero.debug(req.status);
+
+ if (req.status != 200 && req.status != 404) {
+ self.onError("Unexpected status code " + req.status + " getting "
+ + "last file sync time");
+ return;
+ }
+
+ var lastModified = req.getResponseHeader("Last-Modified");
+ var date = new Date(lastModified);
+ Zotero.debug("Last successful storage sync was " + date);
+ ts = Zotero.Date.toUnixTimestamp(date);
+ }
+ finally {
+ callback(ts);
+ }
+ });
+ return;
+ }
+ catch (e) {
+ Zotero.debug(e);
+ Components.utils.reportError(e);
+ callback();
+ return;
+ }
+}
+
+
+Zotero.Sync.Storage.Session.WebDAV.prototype.setLastSyncTime = function (callback) {
+ try {
+ var uri = this.rootURI;
+ var successFileURI = uri.clone();
+ successFileURI.spec += "lastsync";
+
+ var self = this;
+
+ Zotero.Utilities.HTTP.WebDAV.doPut(successFileURI, "", function (req) {
+ Zotero.debug(req.responseText);
+ Zotero.debug(req.status);
+
+ switch (req.status) {
+ case 200:
+ case 201:
+ case 204:
+ self.getLastSyncTime(function (ts) {
+ if (ts) {
+ var sql = "REPLACE INTO version VALUES ('storage_webdav', ?)";
+ Zotero.DB.query(sql, { int: ts });
+ }
+ if (callback) {
+ callback();
+ }
+ });
+ return;
+ }
+
+ var msg = "Unexpected error code " + req.status + " uploading storage success file";
+ Zotero.debug(msg, 2);
+ Components.utils.reportError(msg);
+ if (callback) {
+ callback();
+ }
+ });
+ }
+ catch (e) {
+ Zotero.debug(e);
+ Components.utils.reportError(e);
+ if (callback) {
+ callback();
+ }
+ return;
+ }
+}
+
+
+/**
+ * @param {Function} callback Function to pass URI and result value to
+ * @param {Object} errorCallbacks
+ */
+Zotero.Sync.Storage.Session.WebDAV.prototype.checkServer = function (callback) {
+ try {
+ var parentURI = this.parentURI;
+ var uri = this.rootURI;
+ }
+ catch (e) {
+ switch (e.name) {
+ case 'Z_ERROR_NO_URL':
+ callback(null, Zotero.Sync.Storage.ERROR_NO_URL);
+ return;
+
+ case 'Z_ERROR_NO_PASSWORD':
+ callback(null, Zotero.Sync.Storage.ERROR_NO_PASSWORD);
+ return;
+
+ default:
+ Zotero.debug(e);
+ Components.utils.reportError(e);
+ callback(null, Zotero.Sync.Storage.ERROR_UNKNOWN);
+ return;
+ }
+ }
+
+ var requestHolder = { request: null };
+
+ var prolog = '<?xml version="1.0" encoding="utf-8" ?>\n';
+ var D = new Namespace("D", "DAV:");
+ var nsDeclarations = 'xmlns:' + D.prefix + '=' + '"' + D.uri + '"';
+
+ var requestXML = new XML('<D:propfind ' + nsDeclarations + '/>');
+ requestXML.D::prop = '';
+ // IIS 5.1 requires at least one property in PROPFIND
+ requestXML.D::prop.D::getcontentlength = '';
+
+ var xmlstr = prolog + requestXML.toXMLString();
+
+ var self = this;
+
+ // Test whether URL is WebDAV-enabled
+ var request = Zotero.Utilities.HTTP.doOptions(parentURI, function (req) {
+ Zotero.debug(req.status);
+
+ // Timeout
+ if (req.status == 0) {
+ callback(uri, Zotero.Sync.Storage.ERROR_UNREACHABLE);
+ return;
+ }
+
+ Zotero.debug(req.getAllResponseHeaders());
+ Zotero.debug(req.responseText);
+ Zotero.debug(req.status);
+
+ switch (req.status) {
+ case 400:
+ callback(uri, Zotero.Sync.Storage.ERROR_BAD_REQUEST);
+ return;
+
+ case 401:
+ callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
+ return;
+
+ case 403:
+ callback(uri, Zotero.Sync.Storage.ERROR_FORBIDDEN);
+ return;
+
+ case 500:
+ callback(uri, Zotero.Sync.Storage.ERROR_SERVER_ERROR);
+ return;
+ }
+
+ var dav = req.getResponseHeader("DAV");
+ if (dav == null) {
+ callback(uri, Zotero.Sync.Storage.ERROR_NOT_DAV);
+ return;
+ }
+
+ var headers = { Depth: 0 };
+
+ // Test whether Zotero directory exists
+ Zotero.Utilities.HTTP.WebDAV.doProp("PROPFIND", uri, xmlstr, function (req) {
+ Zotero.debug(req.responseText);
+ Zotero.debug(req.status);
+
+ switch (req.status) {
+ case 207:
+ // Test if Zotero directory is writable
+ var testFileURI = uri.clone();
+ testFileURI.spec += "zotero-test-file";
+ Zotero.Utilities.HTTP.WebDAV.doPut(testFileURI, "", function (req) {
+ Zotero.debug(req.responseText);
+ Zotero.debug(req.status);
+
+ switch (req.status) {
+ case 200:
+ case 201:
+ case 204:
+ // Delete test file
+ Zotero.Utilities.HTTP.WebDAV.doDelete(
+ testFileURI,
+ function (req) {
+ Zotero.debug(req.responseText);
+ Zotero.debug(req.status);
+
+ switch (req.status) {
+ case 200: // IIS 5.1 and Sakai return 200
+ case 204:
+ callback(
+ uri,
+ Zotero.Sync.Storage.SUCCESS
+ );
+ return;
+
+ case 401:
+ callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
+ return;
+
+ case 403:
+ callback(uri, Zotero.Sync.Storage.ERROR_FORBIDDEN);
+ return;
+
+ default:
+ callback(uri, Zotero.Sync.Storage.ERROR_UNKNOWN);
+ return;
+ }
+ }
+ );
+ return;
+
+ case 401:
+ callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
+ return;
+
+ case 403:
+ callback(uri, Zotero.Sync.Storage.ERROR_FORBIDDEN);
+ return;
+
+ case 500:
+ callback(uri, Zotero.Sync.Storage.ERROR_SERVER_ERROR);
+ return;
+
+ default:
+ callback(uri, Zotero.Sync.Storage.ERROR_UNKNOWN);
+ return;
+ }
+ });
+ return;
+
+ case 400:
+ callback(uri, Zotero.Sync.Storage.ERROR_BAD_REQUEST);
+ return;
+
+ case 401:
+ callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
+ return;
+
+ case 403:
+ callback(uri, Zotero.Sync.Storage.ERROR_FORBIDDEN);
+ return;
+
+ case 404:
+ // Zotero directory wasn't found, so see if at least
+ // the parent directory exists
+ Zotero.Utilities.HTTP.WebDAV.doProp("PROPFIND", parentURI, xmlstr,
+ function (req) {
+ Zotero.debug(req.responseText);
+ Zotero.debug(req.status);
+
+ switch (req.status) {
+ // Parent directory existed
+ case 207:
+ callback(uri, Zotero.Sync.Storage.ERROR_ZOTERO_DIR_NOT_FOUND);
+ return;
+
+ case 400:
+ callback(uri, Zotero.Sync.Storage.ERROR_BAD_REQUEST);
+ return;
+
+ case 401:
+ callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
+ return;
+
+ // Parent directory wasn't found either
+ case 404:
+ callback(uri, Zotero.Sync.Storage.ERROR_PARENT_DIR_NOT_FOUND);
+ return;
+
+ default:
+ callback(uri, Zotero.Sync.Storage.ERROR_UNKNOWN);
+ return;
+ }
+ }, headers);
+ return;
+
+ case 500:
+ callback(uri, Zotero.Sync.Storage.ERROR_SERVER_ERROR);
+ return;
+
+ default:
+ callback(uri, Zotero.Sync.Storage.ERROR_UNKNOWN);
+ return;
+ }
+ }, headers);
+ });
+
+ if (!request) {
+ callback(uri, Zotero.Sync.Storage.ERROR_OFFLINE);
+ }
+
+ requestHolder.request = request;
+ return requestHolder;
+}
+
+
+Zotero.Sync.Storage.Session.WebDAV.prototype.checkServerCallback = function (uri, status, window, skipSuccessMessage) {
+ var promptService =
+ Components.classes["@mozilla.org/embedcomp/prompt-service;1"].
+ createInstance(Components.interfaces.nsIPromptService);
+ if (uri) {
+ var spec = uri.scheme + '://' + uri.hostPort + uri.path;
+ }
+
+ switch (status) {
+ case Zotero.Sync.Storage.SUCCESS:
+ if (!skipSuccessMessage) {
+ promptService.alert(
+ window,
+ "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 WebDAV URL.";
+ break;
+
+ case Zotero.Sync.Storage.ERROR_NO_PASSWORD:
+ var errorMessage = "Please enter a password.";
+ 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 WebDAV server did not accept the "
+ + "username and password you entered." + " "
+ + "Please check your storage 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 the WebDAV server." + " "
+ + "Please check your file sync 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(
+ window,
+ // 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;
+ }
+
+ this._createServerDirectory(function (uri, status) {
+ switch (status) {
+ case Zotero.Sync.Storage.SUCCESS:
+ if (!skipSuccessMessage) {
+ promptService.alert(
+ window,
+ "Server configuration verified",
+ "File sync 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 file sync settings or "
+ + "contact your server administrator.";
+ break;
+ }
+
+ // TEMP
+ if (!errorMessage) {
+ var errorMessage = status;
+ }
+ promptService.alert(window, errorTitle, errorMessage);
+ });
+
+ return false;
+ }
+
+ if (!skipSuccessMessage) {
+ if (!errorTitle) {
+ var errorTitle = Zotero.getString("general.error");
+ }
+ // TEMP
+ if (!errorMessage) {
+ var errorMessage = status;
+ }
+ promptService.alert(window, errorTitle, errorMessage);
+ }
+ return false;
+}
+
+
+/**
+ * Remove files on storage server that were deleted locally more than
+ * sync.storage.deleteDelayDays days ago
+ *
+ * @param {Function} callback Passed number of files deleted
+ */
+Zotero.Sync.Storage.Session.WebDAV.prototype.purgeDeletedStorageFiles = function (callback) {
+ if (!this.active) {
+ return;
+ }
+
+ Zotero.debug("Purging deleted storage files");
+ var files = Zotero.Sync.Storage.getDeletedFiles();
+ if (!files) {
+ Zotero.debug("No files to delete remotely");
+ if (callback) {
+ callback();
+ }
+ return;
+ }
+
+ // Add .zip extension
+ var files = files.map(function (file) file + ".zip");
+
+ this._deleteStorageFiles(files, function (results) {
+ // Remove deleted and nonexistent files from storage delete log
+ var toPurge = results.deleted.concat(results.missing);
+ if (toPurge.length > 0) {
+ var done = 0;
+ var maxFiles = 999;
+ var numFiles = toPurge.length;
+
+ Zotero.DB.beginTransaction();
+
+ do {
+ var chunk = toPurge.splice(0, maxFiles);
+ var sql = "DELETE FROM storageDeleteLog WHERE key IN ("
+ + chunk.map(function () '?').join() + ")";
+ Zotero.DB.query(sql, chunk);
+ done += chunk.length;
+ }
+ while (done < numFiles);
+
+ Zotero.DB.commitTransaction();
+ }
+
+ if (callback) {
+ callback(results.deleted.length);
+ }
+ });
+}
+
+
+/**
+ * Delete orphaned storage files older than a day before last sync time
+ *
+ * @param {Function} callback
+ */
+Zotero.Sync.Storage.Session.WebDAV.prototype.purgeOrphanedStorageFiles = function (callback) {
+ const daysBeforeSyncTime = 1;
+
+ Zotero.debug("Purging orphaned storage files");
+ var uri = this.rootURI;
+ var path = uri.path;
+
+ var prolog = '<?xml version="1.0" encoding="utf-8" ?>\n';
+ var D = new Namespace("D", "DAV:");
+ var nsDeclarations = 'xmlns:' + D.prefix + '=' + '"' + D.uri + '"';
+
+ var requestXML = new XML('<D:propfind ' + nsDeclarations + '/>');
+ requestXML.D::prop = '';
+ requestXML.D::prop.D::getlastmodified = '';
+
+ var xmlstr = prolog + requestXML.toXMLString();
+
+ var lastSyncDate = new Date(Zotero.Sync.Server.lastLocalSyncTime * 1000);
+
+ Zotero.Utilities.HTTP.WebDAV.doProp("PROPFIND", uri, xmlstr, function (req) {
+ Zotero.debug(req.responseText);
+
+ var funcName = "Zotero.Sync.Storage.purgeOrphanedStorageFiles()";
+
+ // Strip XML declaration and convert to E4X
+ var xml = new XML(req.responseText.replace(/<\?xml.*\?>/, ''));
+
+ var deleteFiles = [];
+ var trailingSlash = !!path.match(/\/$/);
+ for each(var response in xml.D::response) {
+ var href = response.D::href.toString();
+
+ // Strip trailing slash if there isn't one on the root path
+ if (!trailingSlash) {
+ href = href.replace(/\/$/, "")
+ }
+
+ // Absolute
+ if (href.match(/^https?:\/\//)) {
+ var ios = Components.classes["@mozilla.org/network/io-service;1"].
+ getService(Components.interfaces.nsIIOService);
+ var href = ios.newURI(href, null, null);
+ href = href.path;
+ }
+
+ if (href.indexOf(path) == -1
+ // Try URL-encoded as well, in case there's a '~' or similar
+ // character in the URL and the server (e.g., Sakai) is
+ // encoding the value
+ && decodeURIComponent(href).indexOf(path) == -1) {
+ _error("DAV:href '" + href
+ + "' does not begin with path '" + path + "' in " + funcName);
+ }
+
+ // Skip root URI
+ if (href == path
+ // Try URL-encoded as well, as above
+ || decodeURIComponent(href) == path) {
+ continue;
+ }
+
+ var matches = href.match(/[^\/]+$/);
+ if (!matches) {
+ _error("Unexpected href '" + href + "' in " + funcName)
+ }
+ var file = matches[0];
+
+ if (file.indexOf('.') == 0) {
+ Zotero.debug("Skipping hidden file " + file);
+ continue;
+ }
+ if (!file.match(/\.zip$/) && !file.match(/\.prop$/)) {
+ Zotero.debug("Skipping file " + file);
+ continue;
+ }
+
+ var key = file.replace(/\.(zip|prop)$/, '');
+ var item = Zotero.Items.getByLibraryAndKey(null, key);
+ if (item) {
+ Zotero.debug("Skipping existing file " + file);
+ continue;
+ }
+
+ Zotero.debug("Checking orphaned file " + file);
+
+ // TODO: Parse HTTP date properly
+ var lastModified = response..*::getlastmodified.toString();
+ lastModified = Zotero.Date.strToISO(lastModified);
+ lastModified = Zotero.Date.sqlToDate(lastModified);
+
+ // Delete files older than a day before last sync time
+ var days = (lastSyncDate - lastModified) / 1000 / 60 / 60 / 24;
+
+ // DEBUG!!!!!!!!!!!!
+ //
+ // For now, delete all orphaned files immediately
+ if (true) {
+ deleteFiles.push(file);
+ } else
+
+ if (days > daysBeforeSyncTime) {
+ deleteFiles.push(file);
+ }
+ }
+
+ this._deleteStorageFiles(deleteFiles, callback);
+ },
+ { Depth: 1 });
+}
+
+
+/**
+ * Create a Zotero directory on the storage server
+ */
+Zotero.Sync.Storage.Session.WebDAV.prototype._createServerDirectory = function (callback) {
+ var uri = this.rootURI;
+ Zotero.Utilities.HTTP.WebDAV.doMkCol(uri, function (req) {
+ Zotero.debug(req.responseText);
+ Zotero.debug(req.status);
+
+ switch (req.status) {
+ case 201:
+ callback(uri, Zotero.Sync.Storage.SUCCESS);
+ break;
+
+ case 401:
+ callback(uri, Zotero.Sync.Storage.ERROR_AUTH_FAILED);
+ return;
+
+ case 403:
+ callback(uri, Zotero.Sync.Storage.ERROR_FORBIDDEN);
+ return;
+
+ case 405:
+ callback(uri, Zotero.Sync.Storage.ERROR_NOT_ALLOWED);
+ return;
+
+ case 500:
+ callback(uri, Zotero.Sync.Storage.ERROR_SERVER_ERROR);
+ return;
+
+ default:
+ callback(uri, Zotero.Sync.Storage.ERROR_UNKNOWN);
+ return;
+ }
+ });
+}
+
+
+
+//
+// Private methods
+//
+
+/**
+ * Get the storage URI for an item
+ *
+ * @inner
+ * @param {Zotero.Item}
+ * @return {nsIURI} URI of file on storage server
+ */
+Zotero.Sync.Storage.Session.WebDAV.prototype._getItemURI = function (item) {
+ var uri = this.rootURI;
+ uri.spec = uri.spec + item.key + '.zip';
+ return uri;
+}
+
+
+/**
+ * Get the storage property file URI for an item
+ *
+ * @inner
+ * @param {Zotero.Item}
+ * @return {nsIURI} URI of property file on storage server
+ */
+Zotero.Sync.Storage.Session.WebDAV.prototype._getItemPropertyURI = function (item) {
+ var uri = this.rootURI;
+ uri.spec = uri.spec + item.key + '.prop';
+ return uri;
+}
+
+
+/**
+ * Get the storage property file URI corresponding to a given item storage URI
+ *
+ * @param {nsIURI} Item storage URI
+ * @return {nsIURI|FALSE} Property file URI, or FALSE if not an item storage URI
+ */
+Zotero.Sync.Storage.Session.WebDAV.prototype._getPropertyURIFromItemURI = function (uri) {
+ if (!uri.spec.match(/\.zip$/)) {
+ return false;
+ }
+ var propURI = uri.clone();
+ propURI.QueryInterface(Components.interfaces.nsIURL);
+ propURI.fileName = uri.fileName.replace(/\.zip$/, '.prop');
+ propURI.QueryInterface(Components.interfaces.nsIURI);
+ return propURI;
+}
+
+
+/**
+ * @inner
+ * @param {String[]} files Remote filenames to delete (e.g., ZIPs)
+ * @param {Function} callback Passed object containing three arrays:
+ * 'deleted', 'missing', and 'error',
+ * each containing filenames
+ */
+Zotero.Sync.Storage.Session.WebDAV.prototype._deleteStorageFiles = function (files, callback) {
+ var results = {
+ deleted: [],
+ missing: [],
+ error: []
+ };
+
+ if (files.length == 0) {
+ if (callback) {
+ callback(results);
+ }
+ return;
+ }
+
+ for (var i=0; i<files.length; i++) {
+ let last = (i == files.length - 1);
+ let fileName = files[i];
+
+ let deleteURI = Zotero.Sync.Storage.rootURI;
+ // This should never happen, but let's be safe
+ if (!deleteURI.spec.match(/\/$/)) {
+ callback(deleted);
+ _error("Root URI does not end in slash in "
+ + "Zotero.Sync.Storage._deleteStorageFiles()");
+ }
+ deleteURI.QueryInterface(Components.interfaces.nsIURL);
+ deleteURI.fileName = files[i];
+ deleteURI.QueryInterface(Components.interfaces.nsIURI);
+ Zotero.Utilities.HTTP.WebDAV.doDelete(deleteURI, function (req) {
+ switch (req.status) {
+ case 204:
+ // IIS 5.1 and Sakai return 200
+ case 200:
+ var fileDeleted = true;
+ break;
+
+ case 404:
+ var fileDeleted = false;
+ break;
+
+ default:
+ if (last && callback) {
+ callback(results);
+ }
+
+ results.error.push(fileName);
+ var msg = "An error occurred attempting to delete "
+ + "'" + fileName
+ + "' (" + req.status + " " + req.statusText + ").";
+ _error(msg);
+ return;
+ }
+
+ // If an item file URI, get the property URI
+ var deletePropURI = _getPropertyURIFromItemURI(deleteURI);
+ if (!deletePropURI) {
+ if (fileDeleted) {
+ results.deleted.push(fileName);
+ }
+ else {
+ results.missing.push(fileName);
+ }
+ if (last && callback) {
+ callback(results);
+ }
+ return;
+ }
+
+ // If property file appears separately in delete queue,
+ // remove it, since we're taking care of it here
+ var propIndex = files.indexOf(deletePropURI.fileName);
+ if (propIndex > i) {
+ delete files[propIndex];
+ i--;
+ last = (i == files.length - 1);
+ }
+
+ // Delete property file
+ Zotero.Utilities.HTTP.WebDAV.doDelete(deletePropURI, function (req) {
+ switch (req.status) {
+ case 204:
+ // IIS 5.1 and Sakai return 200
+ case 200:
+ results.deleted.push(fileName);
+ break;
+
+ case 404:
+ if (fileDeleted) {
+ results.deleted.push(fileName);
+ }
+ else {
+ results.missing.push(fileName);
+ }
+ break;
+
+ default:
+ var error = true;
+ }
+
+ if (last && callback) {
+ callback(results);
+ }
+
+ if (error) {
+ results.error.push(fileName);
+ var msg = "An error occurred attempting to delete "
+ + "'" + fileName
+ + "' (" + req.status + " " + req.statusText + ").";
+ _error(msg);
+ }
+ });
+ });
+ }
+}
+
+
+/**
+ * Unused
+ *
+ * @inner
+ * @param {XMLHTTPRequest} req
+ * @throws
+ */
+Zotero.Sync.Storage.Session.WebDAV.prototype._checkResponse = function (req) {
+ if (!req.responseText) {
+ this.onError('Empty response from server');
+ return;
+ }
+ if (!req.responseXML ||
+ !req.responseXML.firstChild ||
+ !(req.responseXML.firstChild.namespaceURI == 'DAV:' &&
+ req.responseXML.firstChild.localName == 'multistatus') ||
+ !req.responseXML.childNodes[0].firstChild) {
+ Zotero.debug(req.responseText);
+ this.onError('Invalid response from storage server');
+ return;
+ }
+}
diff --git a/chrome/content/zotero/xpcom/storage/zfs.js b/chrome/content/zotero/xpcom/storage/zfs.js
@@ -0,0 +1,852 @@
+Zotero.Sync.Storage.Session.ZFS = function (callbacks) {
+ this.onChangesMade = callbacks.onChangesMade ? callbacks.onChangesMade : function () {};
+ this.onError = callbacks.onError ? callbacks.onError : function () {};
+
+ this._rootURI;
+ this._userURI;
+ this._cachedCredentials = false;
+ this._lastSyncTime = null;
+}
+
+Zotero.Sync.Storage.Session.ZFS.prototype.name = "ZFS";
+
+Zotero.Sync.Storage.Session.ZFS.prototype.__defineGetter__('includeUserFiles', function () {
+ return Zotero.Prefs.get("sync.storage.enabled") && Zotero.Prefs.get("sync.storage.protocol") == 'zotero';
+});
+
+Zotero.Sync.Storage.Session.ZFS.prototype.__defineGetter__('includeGroupFiles', function () {
+ return Zotero.Prefs.get("sync.storage.groups.enabled");
+});
+
+Zotero.Sync.Storage.Session.ZFS.prototype.__defineGetter__('enabled', function () {
+ return this.includeUserFiles || this.includeGroupFiles;
+});
+
+Zotero.Sync.Storage.Session.ZFS.prototype.__defineGetter__('active', function () {
+ return this.enabled;
+});
+
+
+Zotero.Sync.Storage.Session.ZFS.prototype.__defineGetter__('rootURI', function () {
+ if (!this._rootURI) {
+ throw ("Root URI not initialized in Zotero.Sync.Storage.Session.ZFS.rootURI");
+ }
+ return this._rootURI.clone();
+});
+
+Zotero.Sync.Storage.Session.ZFS.prototype.__defineGetter__('userURI', function () {
+ if (!this._userURI) {
+ throw ("User URI not initialized in Zotero.Sync.Storage.Session.ZFS.userURI");
+ }
+ return this._userURI.clone();
+});
+
+Zotero.Sync.Storage.Session.ZFS.prototype.init = function (url, username, password) {
+ var ios = Components.classes["@mozilla.org/network/io-service;1"].
+ getService(Components.interfaces.nsIIOService);
+ try {
+ var uri = ios.newURI(url, null, null);
+ if (username) {
+ uri.username = username;
+ uri.password = password;
+ }
+ }
+ catch (e) {
+ Zotero.debug(e);
+ Components.utils.reportError(e);
+ return false;
+ }
+ this._rootURI = uri;
+
+ uri = uri.clone();
+ uri.spec += 'users/' + Zotero.userID + '/';
+ this._userURI = uri;
+
+ return true;
+}
+
+
+Zotero.Sync.Storage.Session.ZFS.prototype.initFromPrefs = function () {
+ var url = ZOTERO_CONFIG.API_URL;
+ var username = Zotero.Sync.Server.username;
+ var password = Zotero.Sync.Server.password;
+ return this.init(url, username, password);
+}
+
+
+/**
+ * Get file metadata on storage server
+ *
+ * @param {Zotero.Item} item
+ * @param {Function} callback Callback f(item, etag)
+ */
+Zotero.Sync.Storage.Session.ZFS.prototype._getStorageFileInfo = function (item, callback) {
+ var uri = this._getItemURI(item);
+
+ var self = this;
+
+ Zotero.Utilities.HTTP.doHead(uri, function (req) {
+ var funcName = "Zotero.Sync.Storage.Session.ZFS._getStorageFileInfo()";
+
+ if (req.status == 404) {
+ callback(item, false);
+ return;
+ }
+ else if (req.status != 200) {
+ Zotero.debug(req.responseText);
+ self.onError("Unexpected status code " + req.status + " in " + funcName);
+ return;
+ }
+
+ var info = {};
+ info.hash = req.getResponseHeader('ETag');
+ info.filename = req.getResponseHeader('X-Zotero-Filename');
+ info.mtime = req.getResponseHeader('X-Zotero-Modification-Time');
+ info.compressed = req.getResponseHeader('X-Zotero-Compressed') == 'Yes';
+ Zotero.debug(info);
+
+ if (!info) {
+ callback(item, false);
+ return;
+ }
+
+ callback(item, info);
+ });
+}
+
+
+/**
+ * Begin download process for individual file
+ *
+ * @param {Zotero.Sync.Storage.Request} [request]
+ */
+Zotero.Sync.Storage.Session.ZFS.prototype.downloadFile = function (request) {
+ var item = Zotero.Sync.Storage.getItemFromRequestName(request.name);
+ if (!item) {
+ throw ("Item '" + request.name + "' not found in Zotero.Sync.Storage.Session.ZFS.downloadFile()");
+ }
+
+ var self = this;
+
+ // Retrieve file info from server to store locally afterwards
+ this._getStorageFileInfo(item, function (item, info) {
+ if (!request.isRunning()) {
+ Zotero.debug("Download request '" + request.name
+ + "' is no longer running after getting remote file info");
+ return;
+ }
+
+ if (!info) {
+ Zotero.debug("Remote file not found for item " + item.libraryID + "/" + item.key);
+ request.finish();
+ return;
+ }
+
+ try {
+ var syncModTime = info.mtime;
+ var syncHash = info.hash;
+
+ var file = item.getFile();
+ // Skip download if local file exists and matches mod time
+ if (file && file.exists()) {
+ if (syncModTime == Math.round(file.lastModifiedTime / 1000)) {
+ Zotero.debug("File mod time matches remote file -- skipping download");
+
+ Zotero.DB.beginTransaction();
+ var syncState = Zotero.Sync.Storage.getSyncState(item.id);
+ //var updateItem = syncState != 1;
+ var updateItem = false;
+ Zotero.Sync.Storage.setSyncedModificationTime(item.id, syncModTime, updateItem);
+ Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
+ Zotero.DB.commitTransaction();
+ self.onChangesMade();
+ request.finish();
+ return;
+ }
+ // If not compressed, check hash, in case only timestamp changed
+ else if (!info.compressed && Zotero.Utilities.prototype.md5(file) == syncHash) {
+ Zotero.debug("File hash matches remote file -- skipping download");
+
+ Zotero.DB.beginTransaction();
+ var syncState = Zotero.Sync.Storage.getSyncState(item.id);
+ //var updateItem = syncState != 1;
+ var updateItem = false;
+ if (!info.compressed) {
+ Zotero.Sync.Storage.setSyncedHash(item.id, syncHash, false);
+ }
+ Zotero.Sync.Storage.setSyncedModificationTime(item.id, syncModTime, updateItem);
+ Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
+ Zotero.DB.commitTransaction();
+ self.onChangesMade();
+ request.finish();
+ return;
+ }
+ }
+
+ var destFile = Zotero.getTempDirectory();
+ if (info.compressed) {
+ destFile.append(item.key + '.zip.tmp');
+ }
+ else {
+ destFile.append(item.key + '.tmp');
+ if (destFile.exists()) {
+ destFile.remove(false);
+ }
+ }
+ if (destFile.exists()) {
+ destFile.remove(false);
+ }
+
+ var listener = new Zotero.Sync.Storage.StreamListener(
+ {
+ onStart: function (request, data) {
+ if (data.request.isFinished()) {
+ Zotero.debug("Download request " + data.request.name
+ + " stopped before download started -- closing channel");
+ request.cancel(0x804b0002); // NS_BINDING_ABORTED
+ return;
+ }
+ },
+ onProgress: function (a, b, c) {
+ request.onProgress(a, b, c)
+ },
+ onStop: function (request, status, response, data) {
+ if (status != 200) {
+ self.onError("Unexpected status code " + status
+ + " for request " + data.request.name + " in Zotero.Sync.Storage.Session.ZFS.downloadFile()");
+ return;
+ }
+
+ // Don't try to process if the request has been cancelled
+ if (data.request.isFinished()) {
+ Zotero.debug("Download request " + data.request.name
+ + " is no longer running after file download");
+ return;
+ }
+
+ Zotero.debug("Finished download of " + destFile.path);
+
+ try {
+ Zotero.Sync.Storage.processDownload(data);
+ data.request.finish();
+ }
+ catch (e) {
+ self.onError(e);
+ }
+ },
+ request: request,
+ item: item,
+ compressed: info.compressed,
+ syncModTime: syncModTime,
+ syncHash: syncHash
+ }
+ );
+
+ var uri = self._getItemURI(item);
+
+ // Don't display password in console
+ var disp = uri.clone();
+ if (disp.password) {
+ disp.password = "********";
+ }
+
+ Zotero.debug('Saving ' + disp.spec + ' with saveURI()');
+ const nsIWBP = Components.interfaces.nsIWebBrowserPersist;
+ var wbp = Components
+ .classes["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
+ .createInstance(nsIWBP);
+ wbp.persistFlags = nsIWBP.PERSIST_FLAGS_BYPASS_CACHE;
+
+ wbp.progressListener = listener;
+ wbp.saveURI(uri, null, null, null, null, destFile);
+ }
+ catch (e) {
+ self.onError(e);
+ }
+ });
+}
+
+
+Zotero.Sync.Storage.Session.ZFS.prototype.uploadFile = function (request) {
+ var self = this;
+ var item = Zotero.Sync.Storage.getItemFromRequestName(request.name);
+ if (Zotero.Attachments.getNumFiles(item) > 1) {
+ Zotero.Sync.Storage.createUploadFile(request, function (data) { self._processUploadFile(data); });
+ }
+ else {
+ this._processUploadFile({ request: request });
+ }
+}
+
+
+/**
+ * Upload the file to the server
+ *
+ * @param {Object} Object with 'request' property
+ * @return {void}
+ */
+Zotero.Sync.Storage.Session.ZFS.prototype._processUploadFile = function (data) {
+ /*
+ _updateSizeMultiplier(
+ (100 - Zotero.Sync.Storage.compressionTracker.ratio) / 100
+ );
+ */
+
+ var request = data.request;
+ var item = Zotero.Sync.Storage.getItemFromRequestName(request.name);
+
+ var self = this;
+
+ this._getStorageFileInfo(item, function (item, info) {
+ if (request.isFinished()) {
+ Zotero.debug("Upload request '" + request.name
+ + "' is no longer running after getting file info");
+ return;
+ }
+
+ try {
+ // Check for conflict
+ if (Zotero.Sync.Storage.getSyncState(item.id)
+ != Zotero.Sync.Storage.SYNC_STATE_FORCE_UPLOAD) {
+ if (info) {
+ // Remote mod time
+ var mtime = info.mtime;
+ // Local file time
+ var fmtime = item.attachmentModificationTime;
+
+ if (fmtime == mtime) {
+ Zotero.debug("File mod time matches remote file -- skipping upload");
+
+ Zotero.debug(Zotero.Sync.Storage.getSyncedModificationTime(item.id));
+
+ Zotero.DB.beginTransaction();
+ var syncState = Zotero.Sync.Storage.getSyncState(item.id);
+ //Zotero.Sync.Storage.setSyncedModificationTime(item.id, fmtime, true);
+ Zotero.Sync.Storage.setSyncedModificationTime(item.id, fmtime);
+ Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
+ Zotero.DB.commitTransaction();
+ self.onChangesMade();
+ request.finish();
+ return;
+ }
+
+ var smtime = Zotero.Sync.Storage.getSyncedModificationTime(item.id);
+ if (smtime != mtime) {
+ var localData = { modTime: fmtime };
+ var remoteData = { modTime: mtime };
+ Zotero.Sync.Storage.QueueManager.addConflict(
+ request.name, localData, remoteData
+ );
+ Zotero.debug("Conflict -- last synced file mod time "
+ + "does not match time on storage server"
+ + " (" + smtime + " != " + mtime + ")");
+ request.finish();
+ return;
+ }
+ }
+ else {
+ Zotero.debug("Remote file not found for item " + item.id);
+ }
+ }
+
+ self._getFileUploadParameters(
+ item,
+ function (item, target, uploadKey, params) {
+ try {
+ self._postFile(request, item, target, uploadKey, params);
+ }
+ catch (e) {
+ self.onError(e);
+ }
+ },
+ function () {
+ self._updateItemFileInfo(item);
+ request.finish();
+ }
+ );
+ }
+ catch (e) {
+ self.onError(e);
+ }
+ });
+}
+
+
+/**
+ * Get mod time of file on storage server
+ *
+ * @param {Zotero.Item} item
+ * @param {Function} uploadCallback Callback f(request, item, target, params)
+ * @param {Function} existsCallback Callback f() to call when file already exists
+ * on server and uploading isn't necessary
+ */
+Zotero.Sync.Storage.Session.ZFS.prototype._getFileUploadParameters = function (item, uploadCallback, existsCallback) {
+ var uri = this._getItemURI(item);
+
+ if (Zotero.Attachments.getNumFiles(item) > 1) {
+ var file = Zotero.getTempDirectory();
+ var filename = item.key + '.zip';
+ file.append(filename);
+ uri.spec = uri.spec;
+ var zip = true;
+ }
+ else {
+ var file = item.getFile();
+ var filename = file.leafName;
+ var zip = false;
+ }
+
+ var mtime = item.attachmentModificationTime;
+ var hash = Zotero.Utilities.prototype.md5(file);
+
+ var body = "md5=" + hash + "&filename=" + encodeURIComponent(filename)
+ + "&filesize=" + file.fileSize + "&mtime=" + mtime;
+ if (zip) {
+ body += "&zip=1";
+ }
+
+ var self = this;
+
+ Zotero.Utilities.HTTP.doPost(uri, body, function (req) {
+ var funcName = "Zotero.Sync.Storage.Session.ZFS._getFileUploadParameters()";
+
+ if (req.status == 413) {
+ var retry = req.getResponseHeader('Retry-After');
+ if (retry) {
+ var minutes = Math.round(retry / 60);
+ var e = new Zotero.Error("You have too many queued uploads. Please try again in " + minutes + " minutes.", "ZFS_UPLOAD_QUEUE_LIMIT");
+ self.onError(e);
+ }
+ else {
+ Zotero.debug(req.responseText);
+ var e = new Zotero.Error("File would exceed Zotero File Storage quota", "ZFS_OVER_QUOTA");
+ self.onError(e);
+ }
+ return;
+ }
+ else if (req.status == 403) {
+ Zotero.debug(req.responseText);
+
+ var groupID = Zotero.Groups.getGroupIDFromLibraryID(item.libraryID);
+ var e = new Zotero.Error("File editing denied for group", "ZFS_FILE_EDITING_DENIED", { groupID: groupID });
+ self.onError(e);
+ return;
+ }
+ else if (req.status != 200) {
+ Zotero.debug(req.responseText);
+ self.onError("Unexpected status code " + req.status + " in " + funcName);
+ return;
+ }
+
+ Zotero.debug(req.responseText);
+
+ try {
+ // Strip XML declaration and convert to E4X
+ var xml = new XML(req.responseText.replace(/<\?xml.*\?>/, ''));
+ }
+ catch (e) {
+ self.onError("Invalid response retrieving file upload parameters");
+ return;
+ }
+
+ if (xml.name() != 'upload' && xml.name() != 'exists') {
+ self.onError("Invalid response retrieving file upload parameters");
+ return;
+ }
+ // File was already available, so uploading isn't required
+ if (xml.name() == 'exists') {
+ existsCallback();
+ return;
+ }
+
+ var url = xml.url.toString();
+ var uploadKey = xml.key.toString();
+ var params = {}, p = '';
+ for each(var param in xml.params.children()) {
+ params[param.name()] = param.toString();
+ }
+ Zotero.debug(params);
+ uploadCallback(item, url, uploadKey, params);
+ });
+}
+
+
+Zotero.Sync.Storage.Session.ZFS.prototype._postFile = function (request, item, url, uploadKey, params) {
+ if (request.isFinished()) {
+ Zotero.debug("Upload request " + request.name + " is no longer running after getting upload parameters");
+ return;
+ }
+
+ var file = this._getUploadFile(item);
+
+ // TODO: make sure this doesn't appear in file
+ var boundary = "---------------------------" + Math.random().toString().substr(2);
+
+ var mis = Components.classes["@mozilla.org/io/multiplex-input-stream;1"]
+ .createInstance(Components.interfaces.nsIMultiplexInputStream);
+
+ // Add parameters
+ for (var key in params) {
+ var storage = Components.classes["@mozilla.org/storagestream;1"]
+ .createInstance(Components.interfaces.nsIStorageStream);
+ storage.init(4096, 4294967295, null); // PR_UINT32_MAX
+ var out = storage.getOutputStream(0);
+
+ var conv = Components.classes["@mozilla.org/intl/converter-output-stream;1"]
+ .createInstance(Components.interfaces.nsIConverterOutputStream);
+ conv.init(out, null, 4096, "?");
+
+ var str = "--" + boundary + '\r\nContent-Disposition: form-data; name="' + key + '"'
+ + '\r\n\r\n' + params[key] + '\r\n';
+ conv.writeString(str);
+ conv.close();
+
+ var instr = storage.newInputStream(0);
+ mis.appendStream(instr);
+ }
+
+ // Add file
+ var sis = Components.classes["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Components.interfaces.nsIStringInputStream);
+ var str = "--" + boundary + '\r\nContent-Disposition: form-data; name="file"\r\n\r\n';
+ sis.setData(str, -1);
+ mis.appendStream(sis);
+
+ var fis = Components.classes["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Components.interfaces.nsIFileInputStream);
+ fis.init(file, 0x01, 0, Components.interfaces.nsIFileInputStream.CLOSE_ON_EOF);
+
+ var bis = Components.classes["@mozilla.org/network/buffered-input-stream;1"]
+ .createInstance(Components.interfaces.nsIBufferedInputStream)
+ bis.init(fis, 64 * 1024);
+ mis.appendStream(bis);
+
+ // End request
+ var sis = Components.classes["@mozilla.org/io/string-input-stream;1"]
+ .createInstance(Components.interfaces.nsIStringInputStream);
+ var str = "\r\n--" + boundary + "--";
+ sis.setData(str, -1);
+ mis.appendStream(sis);
+
+
+/* var cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"].
+ createInstance(Components.interfaces.nsIConverterInputStream);
+ cstream.init(mis, "UTF-8", 0, 0); // you can use another encoding here if you wish
+
+ let (str = {}) {
+ cstream.readString(-1, str); // read the whole file and put it in str.value
+ data = str.value;
+ }
+ cstream.close(); // this closes fstream
+ alert(data);
+*/
+
+ var ios = Components.classes["@mozilla.org/network/io-service;1"].
+ getService(Components.interfaces.nsIIOService);
+ var uri = ios.newURI(url, null, null);
+ var channel = ios.newChannelFromURI(uri);
+
+ channel.QueryInterface(Components.interfaces.nsIUploadChannel);
+ channel.setUploadStream(mis, "multipart/form-data", -1);
+ channel.QueryInterface(Components.interfaces.nsIHttpChannel);
+ channel.requestMethod = 'POST';
+ channel.allowPipelining = false;
+ channel.setRequestHeader('Keep-Alive', '', false);
+ channel.setRequestHeader('Connection', '', false);
+ channel.setRequestHeader("Content-Type", "multipart/form-data; boundary=" + boundary, false);
+ //channel.setRequestHeader('Date', date, false);
+
+ var self = this;
+
+ request.setChannel(channel);
+
+ var listener = new Zotero.Sync.Storage.StreamListener(
+ {
+ onProgress: function (a, b, c) {
+ request.onProgress(a, b, c);
+ },
+ onStop: function (httpRequest, status, response, data) { self._onUploadComplete(httpRequest, status, response, data); },
+ onCancel: function (httpRequest, status, data) { self._onUploadCancel(httpRequest, status, data); },
+ request: request,
+ item: item,
+ uploadKey: uploadKey,
+ streams: [mis]
+ }
+ );
+ channel.notificationCallbacks = listener;
+
+ var dispURI = uri.clone();
+ if (dispURI.password) {
+ dispURI.password = '********';
+ }
+ Zotero.debug("HTTP POST of " + file.leafName + " to " + dispURI.spec);
+
+ channel.asyncOpen(listener, null);
+}
+
+
+Zotero.Sync.Storage.Session.ZFS.prototype._onUploadComplete = function (httpRequest, status, response, data) {
+ var request = data.request;
+ var item = data.item;
+ var uploadKey = data.uploadKey;
+
+ Zotero.debug("Upload of attachment " + item.key
+ + " finished with status code " + status);
+
+ Zotero.debug(response);
+
+ switch (status) {
+ case 201:
+ break;
+
+ default:
+ this.onError("Unexpected file upload status " + status
+ + " in Zotero.Sync.Storage._onUploadComplete()");
+ return;
+ }
+
+ var uri = this._getItemURI(item);
+ var body = "update=" + uploadKey + "&mtime=" + item.attachmentModificationTime;
+
+ var self = this;
+
+ // Register upload on server
+ Zotero.Utilities.HTTP.doPost(uri, body, function (req) {
+ Zotero.debug(req.responseText);
+
+ if (req.status != 204) {
+ self.onError("Unexpected file registration status " + status
+ + " in Zotero.Sync.Storage._onUploadComplete()");
+ return;
+ }
+
+ self._updateItemFileInfo(item);
+ request.finish();
+ });
+}
+
+
+Zotero.Sync.Storage.Session.ZFS.prototype._updateItemFileInfo = function (item) {
+ // Mark as changed locally
+ Zotero.DB.beginTransaction();
+ Zotero.Sync.Storage.setSyncState(item.id, Zotero.Sync.Storage.SYNC_STATE_IN_SYNC);
+
+ // Store file mod time
+ var mtime = item.attachmentModificationTime;
+ Zotero.Sync.Storage.setSyncedModificationTime(item.id, mtime, false);
+
+ // Store file hash of individual files
+ if (Zotero.Attachments.getNumFiles(item) == 1) {
+ var hash = item.attachmentHash;
+ Zotero.Sync.Storage.setSyncedHash(item.id, hash, true);
+ }
+
+ Zotero.DB.commitTransaction();
+
+ try {
+ if (Zotero.Attachments.getNumFiles(item) > 1) {
+ var file = Zotero.getTempDirectory();
+ file.append(item.key + '.zip');
+ file.remove(false);
+ }
+ }
+ catch (e) {
+ Components.utils.reportError(e);
+ }
+
+ this.onChangesMade();
+}
+
+
+Zotero.Sync.Storage.Session.ZFS.prototype._onUploadCancel = function (httpRequest, status, data) {
+ var request = data.request;
+ var item = data.item;
+
+ Zotero.debug("Upload of attachment " + item.key + " cancelled with status code " + status);
+
+ try {
+ if (Zotero.Attachments.getNumFiles(item) > 1) {
+ var file = Zotero.getTempDirectory();
+ file.append(item.key + '.zip');
+ file.remove(false);
+ }
+ }
+ catch (e) {
+ Components.utils.reportError(e);
+ }
+
+ request.finish();
+}
+
+
+Zotero.Sync.Storage.Session.ZFS.prototype.getLastSyncTime = function (callback) {
+ var uri = this.userURI;
+ var successFileURI = uri.clone();
+ successFileURI.spec += "laststoragesync?auth=1";
+
+ var self = this;
+
+ // Cache the credentials
+ if (!this._cachedCredentials) {
+ var uri = this.rootURI;
+ // TODO: move to root uri
+ uri.spec += "?auth=1";
+ Zotero.Utilities.HTTP.doHead(uri, function (req) {
+ if (req.status != 200) {
+ self.onError("Unexpected status code " + req.status + " caching "
+ + "authentication credentials in Zotero.Sync.Storage.Session.ZFS.getLastSyncTime()");
+ return;
+ }
+ self._cachedCredentials = true;
+ self.getLastSyncTime(callback);
+ });
+ return;
+ }
+
+ Zotero.Utilities.HTTP.doGet(successFileURI, function (req) {
+ if (req.responseText) {
+ Zotero.debug(req.responseText);
+ }
+ Zotero.debug(req.status);
+
+ if (req.status != 200 && req.status != 404) {
+ self.onError("Unexpected status code " + req.status + " getting "
+ + "last file sync time");
+ return;
+ }
+
+ var ts = req.responseText;
+ var date = new Date(req.responseText * 1000);
+ Zotero.debug("Last successful storage sync was " + date);
+ self._lastSyncTime = ts;
+ callback(ts);
+ });
+}
+
+
+Zotero.Sync.Storage.Session.ZFS.prototype.setLastSyncTime = function (callback, useLastSyncTime) {
+ if (useLastSyncTime) {
+ var sql = "REPLACE INTO version VALUES ('storage_zfs', ?)";
+ Zotero.DB.query(sql, { int: this._lastSyncTime });
+
+ this._lastSyncTime = null;
+ this._cachedCredentials = false;
+
+ if (callback) {
+ callback();
+ }
+ return;
+ }
+ this._lastSyncTime = null;
+
+ var uri = this.userURI;
+ var successFileURI = uri.clone();
+ successFileURI.spec += "laststoragesync?auth=1";
+
+ var self = this;
+
+ Zotero.Utilities.HTTP.doPost(successFileURI, "", function (req) {
+ Zotero.debug(req.responseText);
+ Zotero.debug(req.status);
+
+ if (req.status != 200) {
+ self.onError("Unexpected status code " + req.status + " setting "
+ + "last file sync time");
+ return;
+ }
+
+ var ts = req.responseText;
+
+ var sql = "REPLACE INTO version VALUES ('storage_zfs', ?)";
+ Zotero.DB.query(sql, { int: ts });
+
+ self._cachedCredentials = false;
+
+ if (callback) {
+ callback();
+ }
+ });
+}
+
+
+Zotero.Sync.Storage.Session.ZFS.prototype.purgeDeletedStorageFiles = function (callback) {
+ // If we don't have a user id we've never synced and don't need to bother
+ if (!Zotero.userID) {
+ return;
+ }
+
+ var sql = "SELECT value FROM settings WHERE setting=? AND key=?";
+ var values = Zotero.DB.columnQuery(sql, ['storage', 'zfsPurge']);
+ if (!values) {
+ return;
+ }
+
+ Zotero.debug("Unlinking synced files on ZFS");
+
+ var uri = this.userURI;
+ uri.spec += "removestoragefiles?";
+ for each(var value in values) {
+ switch (value) {
+ case 'user':
+ uri.spec += "user=1&";
+ break;
+
+ case 'group':
+ uri.spec += "group=1&";
+ break;
+
+ default:
+ throw ("Invalid zfsPurge value '" + value + "' in ZFS purgeDeletedStorageFiles()");
+ }
+ }
+ uri.spec = uri.spec.substr(0, uri.spec.length - 1);
+
+ var self = this;
+
+ Zotero.Utilities.HTTP.doPost(uri, "", function (xmlhttp) {
+ if (xmlhttp.status != 204) {
+ if (callback) {
+ callback(false);
+ }
+ self.onError("Unexpected status code " + xmlhttp.status + " purging ZFS files");
+ }
+
+ var sql = "DELETE FROM settings WHERE setting=? AND key=?";
+ Zotero.DB.query(sql, ['storage', 'zfsPurge']);
+
+ if (callback) {
+ callback(true);
+ }
+ });
+}
+
+
+//
+// Private methods
+//
+
+/**
+ * Get the storage URI for an item
+ *
+ * @inner
+ * @param {Zotero.Item}
+ * @return {nsIURI} URI of file on storage server
+ */
+Zotero.Sync.Storage.Session.ZFS.prototype._getItemURI = function (item) {
+ var uri = this.rootURI;
+ uri.spec += Zotero.URI.getItemPath(item) + '/file?auth=1';
+ return uri;
+}
+
+
+Zotero.Sync.Storage.Session.ZFS.prototype._getUploadFile = function (item) {
+ if (Zotero.Attachments.getNumFiles(item) > 1) {
+ var file = Zotero.getTempDirectory();
+ var filename = item.key + '.zip';
+ file.append(filename);
+ }
+ else {
+ var file = item.getFile();
+ }
+ return file;
+}
diff --git a/chrome/content/zotero/xpcom/sync.js b/chrome/content/zotero/xpcom/sync.js
@@ -118,7 +118,7 @@ Zotero.Sync = new function() {
}
- /**
+ /**
* @param {Date} lastSyncDate JS Date object
* @param {Zotero.Sync.Server.ObjectKeySet}
* @return TRUE if found, FALSE if none, or -1 if last sync time is before start of log
@@ -443,30 +443,28 @@ Zotero.Sync.EventListener = new function () {
Zotero.Sync.Runner = new function () {
- this.__defineGetter__("lastSyncError", function () {
- return _lastSyncError;
- });
-
this.__defineGetter__("background", function () {
return _background;
});
- var _lastSyncError;
var _autoSyncTimer;
var _queue;
var _running;
var _background;
+ var _warning = null;
+
this.init = function () {
this.EventListener.init();
this.IdleListener.init();
}
this.sync = function (background) {
+ _warning = null;
+
if (Zotero.Utilities.HTTP.browserIsOffline()){
- _lastSyncError = "Browser is offline"; // TODO: localize
this.clearSyncTimeout(); // DEBUG: necessary?
- this.setSyncIcon('error');
+ this.setSyncIcon('error', "Browser is offline");
return false;
}
@@ -478,40 +476,135 @@ Zotero.Sync.Runner = new function () {
Zotero.purgeDataObjects(true);
_background = !!background;
-
- _queue = [
- Zotero.Sync.Server.sync,
- Zotero.Sync.Storage.sync,
- Zotero.Sync.Server.sync,
- Zotero.Sync.Storage.sync
- ];
_running = true;
- _lastSyncError = '';
this.setSyncIcon('animate');
- this.next();
+
+ var storageSync = function () {
+ var syncNeeded = false;
+
+ Zotero.Sync.Storage.sync(
+ 'webdav',
+
+ {
+ // WebDAV success
+ onSuccess: function () {
+ syncNeeded = true;
+
+ Zotero.Sync.Storage.sync(
+ 'zfs',
+
+ {
+ // ZFS success
+ onSuccess: function () {
+ Zotero.Sync.Server.sync(
+ Zotero.Sync.Runner.stop,
+ Zotero.Sync.Runner.stop,
+ Zotero.Sync.Runner.error
+ )
+ },
+
+ // ZFS skip
+ onSkip: function () {
+ if (syncNeeded) {
+ Zotero.Sync.Server.sync(
+ Zotero.Sync.Runner.stop,
+ Zotero.Sync.Runner.stop,
+ Zotero.Sync.Runner.error
+ )
+ }
+ },
+
+ // ZFS cancel
+ onStop: Zotero.Sync.Runner.stop,
+
+ // ZFS failure
+ onError: Zotero.Sync.Runner.error,
+
+ onWarning: Zotero.Sync.Runner.warning
+ }
+ )
+ },
+
+ // WebDAV skip
+ onSkip: function () {
+ Zotero.Sync.Storage.sync(
+ 'zfs',
+
+ {
+ // ZFS success
+ onSuccess: function () {
+ Zotero.Sync.Server.sync({
+ onSuccess: Zotero.Sync.Runner.stop,
+ onSkip: Zotero.Sync.Runner.stop,
+ onStop: Zotero.Sync.Runner.stop,
+ onError: Zotero.Sync.Runner.error
+ })
+ },
+
+ // ZFS skip
+ onSkip: Zotero.Sync.Runner.stop,
+
+ // ZFS cancel
+ onStop: Zotero.Sync.Runner.stop,
+
+ // ZFS failure
+ onError: Zotero.Sync.Runner.error,
+
+ onWarning: Zotero.Sync.Runner.warning
+ }
+ )
+ },
+
+ // WebDAV cancel
+ onStop: Zotero.Sync.Runner.stop,
+
+ // WebDAV failure
+ onError: Zotero.Sync.Runner.error
+ }
+ )
+ }
+
+ Zotero.Sync.Server.sync({
+ // Sync 1 success
+ onSuccess: storageSync,
+
+ // Sync 1 skip
+ onSkip: storageSync,
+
+ // Sync 1 stop
+ onStop: Zotero.Sync.Runner.stop,
+
+ // Sync 1 error
+ onError: Zotero.Sync.Runner.error
+ });
}
- this.next = function () {
- if (!_queue.length) {
- this.setSyncIcon();
- _running = false;
- return;
+ this.stop = function () {
+ if (_warning) {
+ Zotero.Sync.Runner.setSyncIcon('warning', _warning);
+ _warning = null;
+ }
+ else {
+ Zotero.Sync.Runner.setSyncIcon();
}
- var func = _queue.shift();
- func();
+ _running = false;
}
- this.setError = function (msg) {
- this.setSyncIcon('error');
- _lastSyncError = msg;
+ /**
+ * Log a warning, but don't throw an error
+ */
+ this.warning = function (e) {
+ Components.utils.reportError(e);
+ _warning = e;
}
- this.reset = function () {
- _queue = [];
+ this.error = function (e) {
+ Zotero.Sync.Runner.setSyncIcon('error', e);
_running = false;
+ throw (e);
}
@@ -593,12 +686,13 @@ Zotero.Sync.Runner = new function () {
}
- this.setSyncIcon = function (status) {
+ this.setSyncIcon = function (status, e) {
status = status ? status : '';
switch (status) {
case '':
case 'animate':
+ case 'warning':
case 'error':
break;
@@ -610,17 +704,113 @@ Zotero.Sync.Runner = new function () {
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var win = wm.getMostRecentWindow('navigator:browser');
+
+ var warning = win.document.getElementById('zotero-tb-sync-warning');
var icon = win.document.getElementById('zotero-tb-sync');
- icon.setAttribute('status', status);
- switch (status) {
- case 'animate':
- icon.setAttribute('disabled', true);
- break;
+
+ var message;
+ var buttonText;
+ var buttonCallback;
+
+ if (e) {
+ if (e.error == Zotero.Error.ERROR_ZFS_OVER_QUOTA) {
+ // TODO: localize
+ message = "You have reached your Zotero File Storage quota. Some files were not synced.\n\n"
+ + "See your zotero.org account settings for additional storage options.";
+
+ buttonText = "Open Account Settings";
+ buttonCallback = function () {
+ var url = "https://www.zotero.org/settings/storage";
+
+ var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Components.interfaces.nsIWindowMediator);
+ var win = wm.getMostRecentWindow("navigator:browser");
+ var browser = win.getBrowser();
+ browser.selectedTab = browser.addTab(url);
+ }
+ }
- default:
- icon.setAttribute('disabled', false);
+ if (!message) {
+ message = e.message ? e.message : e;
+ }
+ }
+
+ if (status == 'warning' || status == 'error') {
+ icon.setAttribute('status', '');
+ warning.hidden = false;
+ warning.setAttribute('mode', status);
+ warning.tooltipText = "A sync error occurred. Click to view details.";
+ warning.onclick = function () {
+ var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Components.interfaces.nsIWindowMediator);
+ var win = wm.getMostRecentWindow("navigator:browser");
+
+ var pr = Components.classes["@mozilla.org/network/default-prompt;1"]
+ .createInstance(Components.interfaces.nsIPrompt);
+ // Warning
+ if (status == 'warning') {
+ var title = Zotero.getString('general.warning');
+
+ // If secondary button not specified, just use an alert
+ if (!buttonText) {
+ prompt.alert(title, message);
+ return;
+ }
+
+ var buttonFlags = pr.BUTTON_POS_0 * pr.BUTTON_TITLE_OK
+ + pr.BUTTON_POS_1 * pr.BUTTON_TITLE_IS_STRING;
+ Zotero.debug(buttonFlags);
+ var index = pr.confirmEx(
+ title,
+ message,
+ buttonFlags,
+ "",
+ buttonText,
+ "", null, {}
+ );
+
+ if (index == 1) {
+ setTimeout(buttonCallback, 1);
+ }
+ }
+
+ // Error
+ else if (status == 'error') {
+ var errorsLogged = Zotero.getErrors().length > 0;
+ // Probably not necessary, but let's be sure
+ if (!errorsLogged) {
+ Components.utils.reportError(message);
+ }
+
+ var buttonFlags = pr.BUTTON_POS_0 * pr.BUTTON_TITLE_OK
+ + pr.BUTTON_POS_1 * pr.BUTTON_TITLE_IS_STRING;
+ var index = pr.confirmEx(
+ Zotero.getString('general.error'),
+ message,
+ buttonFlags,
+ "",
+ // TODO: localize
+ "Report Error...",
+ "", null, {}
+ );
+
+ if (index == 1) {
+ win.setTimeout(function () {
+ win.ZoteroPane.reportErrors();
+ }, 1);
+ }
+ }
+ }
+ }
+ else {
+ icon.setAttribute('status', status);
+ warning.hidden = true;
+ warning.onclick = null;
}
+
+ // Disable button while spinning
+ icon.disabled = status == 'animate';
}
}
@@ -852,7 +1042,22 @@ Zotero.Sync.Server = new function () {
var _throttleTimeout;
var _canAutoResetClient = true;
- function login(callback, callbackCallback) {
+ var _callbacks = {
+ onSuccess: function () {
+ Zotero.Sync.Runner.setSyncIcon();
+ },
+ onSkip: function () {
+ Zotero.Sync.Runner.setSyncIcon();
+ },
+ onStop: function () {
+ Zotero.Sync.Runner.setSyncIcon();
+ },
+ onError: function (msg) {
+ Zotero.Sync.Runner.error(msg);
+ }
+ };
+
+ function login(callback) {
var url = _serverURL + "login";
var username = Zotero.Sync.Server.username;
@@ -906,18 +1111,26 @@ Zotero.Sync.Server = new function () {
//Zotero.debug('Got session ID ' + _sessionID + ' from server');
if (callback) {
- callback(callbackCallback);
+ callback();
}
});
}
- function sync(callback) {
+ function sync(callbacks) {
+ for (var func in callbacks) {
+ _callbacks[func] = callbacks[func];
+ }
+
+ var self = this;
+
Zotero.Sync.Runner.setSyncIcon('animate');
if (!_sessionID) {
Zotero.debug("Session ID not available -- logging in");
- Zotero.Sync.Server.login(Zotero.Sync.Server.sync, callback);
+ Zotero.Sync.Server.login(function () {
+ Zotero.Sync.Server.sync(_callbacks);
+ });
return;
}
@@ -955,7 +1168,9 @@ Zotero.Sync.Server = new function () {
Zotero.debug("Invalid session ID -- logging in");
_sessionID = false;
_syncInProgress = false;
- Zotero.Sync.Server.login(Zotero.Sync.Server.sync, callback);
+ Zotero.Sync.Server.login(function () {
+ Zotero.Sync.Server.sync(_callbacks);
+ });
return;
}
@@ -976,32 +1191,36 @@ Zotero.Sync.Server = new function () {
// Strip XML declaration and convert to E4X
var xml = new XML(xmlhttp.responseText.replace(/<\?xml.*\?>/, ''));
- Zotero.DB.beginTransaction();
-
try {
+ // If no earliest date is provided by the server, the server
+ // account is empty
+ var earliestRemoteDate = parseInt(xml.@earliest) ?
+ new Date((xml.@earliest + 43200) * 1000) : false;
+ var noServerData = !!earliestRemoteDate;
+
+ // Check to see if we're syncing with a different user
var userID = parseInt(xml.@userID);
var libraryID = parseInt(xml.@defaultLibraryID);
- if (!_checkSyncUser(userID, libraryID)) {
+ var c = _checkSyncUser(userID, libraryID, noServerData);
+ if (c == 0) {
+ // Groups were deleted, so restart sync
+ Zotero.debug("Restarting sync");
+ _syncInProgress = false;
+ Zotero.Sync.Server.sync(_callbacks);
+ return;
+ }
+ else if (c == -1) {
Zotero.debug("Sync cancelled");
- Zotero.DB.rollbackTransaction();
Zotero.Sync.Server.unlock(function () {
- if (callback) {
- Zotero.Sync.Runner.setSyncIcon();
- callback();
- }
- else {
- Zotero.Sync.Runner.reset();
- Zotero.Sync.Runner.next();
- }
+ _callbacks.onStop();
});
_syncInProgress = false;
return;
}
- Zotero.UnresponsiveScriptIndicator.disable();
+ Zotero.DB.beginTransaction();
- var earliestRemoteDate = parseInt(xml.@earliest) ?
- new Date((xml.@earliest + 43200) * 1000) : false;
+ Zotero.UnresponsiveScriptIndicator.disable();
var lastLocalSyncTime = Zotero.Sync.Server.lastLocalSyncTime;
var lastLocalSyncDate = lastLocalSyncTime ?
@@ -1039,14 +1258,7 @@ Zotero.Sync.Server = new function () {
Zotero.debug("Sync cancelled");
Zotero.DB.rollbackTransaction();
Zotero.Sync.Server.unlock(function () {
- if (callback) {
- Zotero.Sync.Runner.setSyncIcon();
- callback();
- }
- else {
- Zotero.Sync.Runner.reset();
- Zotero.Sync.Runner.next();
- }
+ _callbacks.onSkip();
});
Zotero.reloadDataObjects();
Zotero.Sync.EventListener.resetIgnored();
@@ -1067,13 +1279,7 @@ Zotero.Sync.Server = new function () {
Zotero.DB.commitTransaction();
Zotero.Sync.Server.unlock(function () {
_syncInProgress = false;
- if (callback) {
- Zotero.Sync.Runner.setSyncIcon();
- callback();
- }
- else {
- Zotero.Sync.Runner.next();
- }
+ _callbacks.onSuccess();
});
return;
}
@@ -1114,13 +1320,7 @@ Zotero.Sync.Server = new function () {
Zotero.DB.commitTransaction();
Zotero.Sync.Server.unlock(function () {
_syncInProgress = false;
- if (callback) {
- Zotero.Sync.Runner.setSyncIcon();
- callback();
- }
- else {
- Zotero.Sync.Runner.next();
- }
+ _callbacks.onSuccess();
});
}
@@ -1203,7 +1403,7 @@ Zotero.Sync.Server = new function () {
}
- function lock(callback, callbackCallback) {
+ function lock(callback) {
Zotero.debug("Getting session lock");
if (!_sessionID) {
@@ -1234,11 +1434,7 @@ Zotero.Sync.Server = new function () {
if (response.firstChild.tagName == 'error') {
if (_checkServerSessionLock(response.firstChild)) {
- Zotero.Sync.Server.lock(function () {
- if (callback) {
- callback(callbackCallback);
- }
- });
+ Zotero.Sync.Server.lock(callback ? function () { callback(); } : null);
return;
}
@@ -1252,7 +1448,7 @@ Zotero.Sync.Server = new function () {
_sessionLock = true;
if (callback) {
- callback(callbackCallback);
+ callback();
}
});
}
@@ -1300,7 +1496,9 @@ Zotero.Sync.Server = new function () {
function clear(callback) {
if (!_sessionID) {
Zotero.debug("Session ID not available -- logging in");
- Zotero.Sync.Server.login(Zotero.Sync.Server.clear, callback);
+ Zotero.Sync.Server.login(function () {
+ Zotero.Sync.Server.clear();
+ });
return;
}
@@ -1312,7 +1510,9 @@ Zotero.Sync.Server = new function () {
if (_invalidSession(xmlhttp)) {
Zotero.debug("Invalid session ID -- logging in");
_sessionID = false;
- Zotero.Sync.Server.login(Zotero.Sync.Server.clear, callback);
+ Zotero.Sync.Server.login(function () {
+ Zotero.Sync.Server.clear(callback);
+ });
return;
}
@@ -1343,7 +1543,9 @@ Zotero.Sync.Server = new function () {
function resetServer(callback) {
if (!_sessionID) {
Zotero.debug("Session ID not available -- logging in");
- Zotero.Sync.Server.login(Zotero.Sync.Server.resetServer, callback);
+ Zotero.Sync.Server.login(function () {
+ Zotero.Sync.Server.resetServer(callback);
+ });
return;
}
@@ -1355,7 +1557,9 @@ Zotero.Sync.Server = new function () {
if (_invalidSession(xmlhttp)) {
Zotero.debug("Invalid session ID -- logging in");
_sessionID = false;
- Zotero.Sync.Server.login(Zotero.Sync.Server.reset, callback);
+ Zotero.Sync.Server.login(function () {
+ Zotero.Sync.Server.reset();
+ });
return;
}
@@ -1391,11 +1595,12 @@ Zotero.Sync.Server = new function () {
+ "('lastlocalsync', 'lastremotesync', 'syncdeletelog')";
Zotero.DB.query(sql);
+ var sql = "DELETE FROM version WHERE schema IN "
+ + "('lastlocalsync', 'lastremotesync', 'syncdeletelog')";
+ Zotero.DB.query(sql);
+
Zotero.DB.query("DELETE FROM syncDeleteLog");
Zotero.DB.query("DELETE FROM storageDeleteLog");
- sql = "DELETE FROM settings WHERE setting='account' AND "
- + "key IN ('userID', 'username')";
- Zotero.DB.query(sql);
sql = "INSERT INTO version VALUES ('syncdeletelog', ?)";
Zotero.DB.query(sql, Zotero.Date.getUnixTimestamp());
@@ -1545,6 +1750,41 @@ Zotero.Sync.Server = new function () {
}, 1);
break;
+ case 'LIBRARY_ACCESS_DENIED':
+ var background = Zotero.Sync.Runner.background;
+ setTimeout(function () {
+ var libraryID = parseInt(firstChild.getAttribute('libraryID'));
+ var group = Zotero.Groups.getByLibraryID(libraryID);
+
+ var pr = Components.classes["@mozilla.org/network/default-prompt;1"]
+ .createInstance(Components.interfaces.nsIPrompt);
+ var buttonFlags = (pr.BUTTON_POS_0) * (pr.BUTTON_TITLE_IS_STRING)
+ + (pr.BUTTON_POS_1) * (pr.BUTTON_TITLE_CANCEL)
+ + pr.BUTTON_DELAY_ENABLE;
+ var index = pr.confirmEx(
+ Zotero.getString('general.warning'),
+ // TODO: localize
+ "You no longer have write access to the Zotero group '" + group.name + "', "
+ + "and changes you've made cannot be synced to the server.\n\n"
+ + "If you continue, your copy of the group will be reset to its state "
+ + "on the server, and your local modifications will be lost.\n\n"
+ + "If you would like a chance to copy your changes elsewhere or to request "
+ + "write access from a group administrator, cancel the sync now.",
+ buttonFlags,
+ "Reset Group and Sync",
+ null, null, null, {}
+ );
+
+ if (index == 0) {
+ group.erase();
+ Zotero.Sync.Server.resetClient();
+ Zotero.Sync.Storage.resetAllSyncStates();
+ Zotero.Sync.Runner.sync();
+ return;
+ }
+ }, 1);
+ break;
+
case 'TAG_TOO_LONG':
if (!Zotero.Sync.Runner.background) {
var tag = xmlhttp.responseXML.firstChild.getElementsByTagName('tag');
@@ -1626,9 +1866,19 @@ Zotero.Sync.Server = new function () {
/**
* Make sure we're syncing with the same account we used last time
*
- * @return TRUE if sync should continue, FALSE if cancelled
+ * @param {Integer} userID New userID
+ * @param {Integer} libraryID New libraryID
+ * @param {Integer} noServerData The server account is empty — this is
+ * the account after a server clear
+ * @return 1 if sync should continue, 0 if sync should restart, -1 if sync should cancel
*/
- function _checkSyncUser(userID, libraryID) {
+ function _checkSyncUser(userID, libraryID, noServerData) {
+ if (Zotero.DB.transactionInProgress()) {
+ throw ("Transaction in progress in Zotero.Sync.Server._checkSyncUser");
+ }
+
+ Zotero.DB.beginTransaction();
+
var sql = "SELECT value FROM settings WHERE "
+ "setting='account' AND key='username'";
var lastUsername = Zotero.DB.valueQuery(sql);
@@ -1636,7 +1886,11 @@ Zotero.Sync.Server = new function () {
var lastUserID = Zotero.userID;
var lastLibraryID = Zotero.libraryID;
+ var restartSync = false;
+
if (lastUserID && lastUserID != userID) {
+ var groups = Zotero.Groups.getAll();
+
var pr = Components.classes["@mozilla.org/network/default-prompt;1"]
.createInstance(Components.interfaces.nsIPrompt);
var buttonFlags = (pr.BUTTON_POS_0) * (pr.BUTTON_TITLE_IS_STRING)
@@ -1644,54 +1898,106 @@ Zotero.Sync.Server = new function () {
+ (pr.BUTTON_POS_2) * (pr.BUTTON_TITLE_IS_STRING)
+ pr.BUTTON_POS_1_DEFAULT
+ pr.BUTTON_DELAY_ENABLE;
- var index = pr.confirmEx(
- Zotero.getString('general.warning'),
- // TODO: localize
- "This Zotero database was last synced with a different "
+
+ var msg = "This Zotero database was last synced with a different "
+ "zotero.org account ('" + lastUsername + "') from the "
- + "current one ('" + username + "'). "
- + "If you continue, local Zotero data will be "
- + "combined with data from the '" + username + "' account "
- + "stored on the server.\n\n"
- + "To avoid combining data, revert to the '"
- + lastUsername + "' account or use the Reset options "
- + "in the Sync pane of the Zotero preferences.",
- buttonFlags,
- "Sync",
- null,
- "Open Sync Preferences",
- null, {}
- );
+ + "current one ('" + username + "'). ";
- if (index > 0) {
- if (index == 1) {
- // Cancel
+ if (!noServerData) {
+ // TODO: localize
+ msg += "If you continue, local Zotero data will be "
+ + "combined with data from the '" + username + "' account "
+ + "stored on the server.";
+ // If there are local groups belonging to the previous user,
+ // we need to remove them
+ if (groups.length) {
+ msg += "Local groups, including any with changed items, will also "
+ + "be removed.";
}
- else if (index == 2) {
- var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
- .getService(Components.interfaces.nsIWindowMediator);
- var lastWin = wm.getMostRecentWindow("navigator:browser");
- lastWin.ZoteroPane.openPreferences('zotero-prefpane-sync');
+ msg += "\n\n"
+ + "To avoid combining or losing data, revert to the '"
+ + lastUsername + "' account or use the Reset options "
+ + "in the Sync pane of the Zotero preferences.";
+
+ var syncButtonText = "Sync";
+ }
+ else if (groups.length) {
+ msg += "If you continue, local groups, including any with changed items, "
+ + "will be removed and replaced with groups linked to the '"
+ + username + "' account."
+ + "\n\n"
+ + "To avoid losing local changes to groups, be sure you "
+ + "have synced with the '" + lastUsername + "' account before "
+ + "syncing with the '" + username + "' account.";
+
+ var syncButtonText = "Remove Groups and Sync";
+ }
+ // If there are no local groups and the server is empty,
+ // don't bother prompting
+ else {
+ var noPrompt = true;
+ }
+
+ if (!noPrompt) {
+ var index = pr.confirmEx(
+ Zotero.getString('general.warning'),
+ msg,
+ buttonFlags,
+ syncButtonText,
+ null,
+ "Open Sync Preferences",
+ null, {}
+ );
+
+ if (index > 0) {
+ if (index == 1) {
+ // Cancel
+ }
+ else if (index == 2) {
+ var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Components.interfaces.nsIWindowMediator);
+ var lastWin = wm.getMostRecentWindow("navigator:browser");
+ lastWin.ZoteroPane.openPreferences('zotero-prefpane-sync');
+ }
+
+ return -1;
+ }
+
+ // Delete all local groups
+ for each(var group in groups) {
+ group.erase();
}
- return false;
+ restartSync = true;
}
}
if (lastUserID != userID || lastLibraryID != libraryID) {
Zotero.userID = userID;
Zotero.libraryID = libraryID;
+
+ // Update userID in relations
+ if (lastUserID && lastLibraryID) {
+ Zotero.Relations.updateUser(lastUserID, lastLibraryID, userID, libraryID);
+ }
+
+ Zotero.Sync.Server.resetClient();
+ Zotero.Sync.Storage.resetAllSyncStates();
}
if (lastUsername != username) {
- var sql = "REPLACE INTO settings VALUES ('account', 'username', ?)";
- Zotero.DB.query(sql, username);
+ Zotero.username = username;
}
- return true;
+ Zotero.DB.commitTransaction();
+
+ return restartSync ? 0 : 1;
}
+
+
+
function _invalidSession(xmlhttp) {
if (xmlhttp.responseXML.childNodes[0].firstChild.tagName != 'error') {
return false;
@@ -1783,8 +2089,7 @@ Zotero.Sync.Server = new function () {
Zotero.Sync.Server.unlock()
}
- Zotero.Sync.Runner.setError(e.message ? e.message : e);
- Zotero.Sync.Runner.reset();
+ _callbacks.onError(e);
throw (e);
}
@@ -1970,6 +2275,7 @@ Zotero.Sync.Server.Data = new function() {
var itemStorageModTimes = {};
var childItemStore = [];
+ // Remotely changed groups
if (xml.groups.length()) {
Zotero.debug("Processing remotely changed groups");
for each(var xmlNode in xml.groups.group) {
@@ -1978,6 +2284,7 @@ Zotero.Sync.Server.Data = new function() {
}
}
+ // Remotely deleted groups
if (xml.deleted.groups.toString()) {
Zotero.debug("Processing remotely deleted groups");
var groupIDs = xml.deleted.groups.toString().split(' ');
@@ -1988,21 +2295,14 @@ Zotero.Sync.Server.Data = new function() {
if (!group) {
continue;
}
- // TODO: prompt to save
- Zotero.Notifier.disable();
+ // TODO: prompt to save data to local library?
- // TODO: figure out a better way to do this
- var notifierData = {};
- notifierData[groupID] = group.serialize();
group.erase();
-
- Zotero.Notifier.enable();
-
- Zotero.Notifier.trigger('delete', 'group', groupID, notifierData);
}
}
+ // Other objects
for each(var syncObject in Zotero.Sync.syncObjects) {
var Type = syncObject.singular; // 'Item'
var Types = syncObject.plural; // 'Items'
@@ -2271,11 +2571,7 @@ Zotero.Sync.Server.Data = new function() {
syncSession.removeFromDeleted(creator.ref);
}
}
- else if (obj.isAttachment() &&
- (obj.attachmentLinkMode ==
- Zotero.Attachments.LINK_MODE_IMPORTED_FILE ||
- obj.attachmentLinkMode ==
- Zotero.Attachments.LINK_MODE_IMPORTED_URL)) {
+ else if (obj.isImportedAttachment()) {
// Mark new attachments for download
if (isNewObject) {
obj.attachmentSyncState =
@@ -2285,7 +2581,8 @@ Zotero.Sync.Server.Data = new function() {
else {
var mtime = xmlNode.@storageModTime.toString();
if (mtime) {
- itemStorageModTimes[obj.key] = parseInt(mtime);
+ var lk = Zotero.Items.getLibraryKeyHash(obj)
+ itemStorageModTimes[lk] = parseInt(mtime);
}
}
}
@@ -2504,15 +2801,16 @@ Zotero.Sync.Server.Data = new function() {
}
}
- // Check mod times of updated items against stored time to see
+ // Check mod times and hashes of updated items against stored values to see
// if they've been updated elsewhere and mark for download if so
if (type == 'item') {
var ids = [];
var modTimes = {};
- for (var key in itemStorageModTimes) {
- var item = Zotero.Items.getByLibraryAndKey(null, key);
+ for (var libraryKeyHash in itemStorageModTimes) {
+ var lk = Zotero.Items.parseLibraryKeyHash(libraryKeyHash);
+ var item = Zotero.Items.getByLibraryAndKey(lk.libraryID, lk.key);
ids.push(item.id);
- modTimes[item.id] = itemStorageModTimes[key];
+ modTimes[item.id] = itemStorageModTimes[libraryKeyHash];
}
if (ids.length > 0) {
Zotero.Sync.Storage.checkForUpdatedFiles(ids, modTimes);
@@ -3003,15 +3301,23 @@ Zotero.Sync.Server.Data = new function() {
xml.@charset = charset;
}
- // 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;
- }
-
+ // Include paths for non-links
var path = <path>{item.attachment.path}</path>;
xml.path += path;
+
+ // Include storage sync time and hash for imported files
+ if (item.attachment.linkMode != Zotero.Attachments.LINK_MODE_LINKED_FILE) {
+ var mtime = Zotero.Sync.Storage.getSyncedModificationTime(item.primary.itemID);
+ if (mtime) {
+ xml.@storageModTime = mtime;
+ }
+
+ var hash = Zotero.Sync.Storage.getSyncedHash(item.primary.itemID);
+ if (hash) {
+ xml.@storageHash = hash;
+ }
+ }
}
if (item.note) {
diff --git a/chrome/content/zotero/xpcom/uri.js b/chrome/content/zotero/xpcom/uri.js
@@ -1,5 +1,6 @@
Zotero.URI = new function () {
var _baseURI = ZOTERO_CONFIG.BASE_URI;
+ var _apiURI = ZOTERO_CONFIG.API_URI;
/**
@@ -22,8 +23,11 @@ Zotero.URI = new function () {
*
* @return {String}
*/
- this.getCurrentUserURI = function () {
+ this.getCurrentUserURI = function (noLocal) {
var userID = Zotero.userID;
+ if (!userID && noLocal) {
+ throw new Exception("Local userID not available and noLocal set in Zotero.URI.getCurrentUserURI()");
+ }
if (userID) {
return _baseURI + "users/" + userID;
}
@@ -42,22 +46,44 @@ Zotero.URI = new function () {
this.getLibraryURI = function (libraryID) {
- var libraryType = Zotero.Libraries.getType(libraryID);
+ var path = this.getLibraryPath(libraryID);
+ return _baseURI + path;
+ }
+
+
+ /**
+ * Get path portion of library URI (e.g., users/6 or groups/1)
+ */
+ this.getLibraryPath = function (libraryID) {
+ if (libraryID) {
+ var libraryType = Zotero.Libraries.getType(libraryID);
+ }
+ else {
+ libraryType = 'user';
+ }
switch (libraryType) {
+ case 'user':
+ var id = Zotero.userID;
+ if (!id) {
+ throw new Exception("User id not available in Zotero.URI.getLibraryPath()");
+ }
+ break;
+
case 'group':
var id = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
break;
- case 'user':
- throw ("User library ids are not supported in Zotero.URI.getLibraryURI");
-
default:
- throw ("Unsupported library type '" + libraryType + "' in Zotero.URI.getLibraryURI()");
+ throw ("Unsupported library type '" + libraryType + "' in Zotero.URI.getLibraryPath()");
}
- return _baseURI + libraryType + "s/" + id;
+
+ return libraryType + "s/" + id;
}
+ /**
+ * Return URI of item, which might be a local URI if user hasn't synced
+ */
this.getItemURI = function (item) {
if (item.libraryID) {
var baseURI = this.getLibraryURI(item.libraryID);
@@ -69,6 +95,14 @@ Zotero.URI = new function () {
}
+ /**
+ * Get path portion of item URI (e.g., users/6/items/ABCD1234 or groups/1/items/ABCD1234)
+ */
+ this.getItemPath = function (item) {
+ return this.getLibraryPath(item.libraryID) + "/items/" + item.key;
+ }
+
+
this.getGroupsURL = function () {
return ZOTERO_CONFIG.WWW_BASE_URL + "groups";
}
diff --git a/chrome/content/zotero/xpcom/utilities.js b/chrome/content/zotero/xpcom/utilities.js
@@ -396,19 +396,51 @@ Zotero.Utilities.prototype.getSQLDataType = function(value) {
/*
- * From http://developer.mozilla.org/en/docs/nsICryptoHash#Computing_the_Hash_of_a_String
+ * Adapted from http://developer.mozilla.org/en/docs/nsICryptoHash
+ *
+ * @param {String|nsIFile} strOrFile
+ * @param {Boolean} [base64=false] Return as base-64-encoded string rather than hex string
+ * @return {String}
*/
-Zotero.Utilities.prototype.md5 = function(str) {
- var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
- createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
- converter.charset = "UTF-8";
- var result = {};
- var data = converter.convertToByteArray(str, result);
- var ch = Components.classes["@mozilla.org/security/hash;1"]
- .createInstance(Components.interfaces.nsICryptoHash);
- ch.init(ch.MD5);
- ch.update(data, data.length);
- var hash = ch.finish(false);
+Zotero.Utilities.prototype.md5 = function(strOrFile, base64) {
+ if (typeof strOrFile == 'string') {
+ var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ var result = {};
+ var data = converter.convertToByteArray(strOrFile, result);
+ var ch = Components.classes["@mozilla.org/security/hash;1"]
+ .createInstance(Components.interfaces.nsICryptoHash);
+ ch.init(ch.MD5);
+ ch.update(data, data.length);
+ }
+ else if (strOrFile instanceof Components.interfaces.nsIFile) {
+ var istream = Components.classes["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Components.interfaces.nsIFileInputStream);
+ // open for reading
+ istream.init(strOrFile, 0x01, 0444, 0);
+ var ch = Components.classes["@mozilla.org/security/hash;1"]
+ .createInstance(Components.interfaces.nsICryptoHash);
+ // we want to use the MD5 algorithm
+ ch.init(ch.MD5);
+ // this tells updateFromStream to read the entire file
+ const PR_UINT32_MAX = 0xffffffff;
+ ch.updateFromStream(istream, PR_UINT32_MAX);
+ }
+
+ // pass false here to get binary data back
+ var hash = ch.finish(base64);
+
+ if (istream) {
+ istream.close();
+ }
+
+ if (base64) {
+ return hash;
+ }
+
+ /*
+ // This created 36-character hashes
// return the two-digit hexadecimal code for a byte
function toHexString(charCode) {
@@ -417,6 +449,17 @@ Zotero.Utilities.prototype.md5 = function(str) {
// convert the binary hash data to a hex string.
return [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
+ */
+
+ // From http://rcrowley.org/2007/11/15/md5-in-xulrunner-or-firefox-extensions/
+ var ascii = []; ii = hash.length;
+ for (var i = 0; i < ii; ++i) {
+ var c = hash.charCodeAt(i);
+ var ones = c % 16;
+ var tens = c >> 4;
+ ascii.push(String.fromCharCode(tens + (tens > 9 ? 87 : 48)) + String.fromCharCode(ones + (ones > 9 ? 87 : 48)));
+ }
+ return ascii.join('');
}
@@ -795,17 +838,18 @@ Zotero.Utilities.Translate.prototype.retrieveDocument = function(url) {
* @param {String} url URL to load
* @param {String} [body=null] Request body to POST to the URL; a GET request is
* executed if no body is present
- * @param {String} [requestContentType=application/x-www-form-urlencoded]
- * Request content type for POST; ignored if no body
+ * @param {Object} [headers] HTTP headers to include in request;
+ * Content-Type defaults to application/x-www-form-urlencoded
+ * for POST; ignored if no body
* @param {String} [responseCharset] Character set to force on the response
* @return {String} Request body
*/
-Zotero.Utilities.Translate.prototype.retrieveSource = function(url, body, requestContentType, responseCharset) {
+Zotero.Utilities.Translate.prototype.retrieveSource = function(url, body, headers, responseCharset) {
/* Apparently, a synchronous XMLHttpRequest would have the behavior of this routine in FF3, but
* in FF3.5, synchronous XHR blocks all JavaScript on the thread. See
* http://hacks.mozilla.org/2009/07/synchronous-xhr/. */
if(this.translate.locationIsProxied) url = this._convertURL(url);
- if(!requestContentType) requestContentType = null;
+ if(!headers) headers = null;
if(!responseCharset) responseCharset = null;
var mainThread = Zotero.mainThread;
@@ -813,7 +857,7 @@ Zotero.Utilities.Translate.prototype.retrieveSource = function(url, body, reques
var listener = function(aXmlhttp) { xmlhttp = aXmlhttp };
if(body) {
- Zotero.Utilities.HTTP.doPost(url, body, listener, requestContentType, responseCharset);
+ Zotero.Utilities.HTTP.doPost(url, body, listener, headers, responseCharset);
} else {
Zotero.Utilities.HTTP.doGet(url, listener, responseCharset);
}
@@ -870,7 +914,7 @@ Zotero.Utilities.Translate.prototype.doGet = function(urls, processor, done, res
* Already documented in Zotero.Utilities.HTTP
* @ignore
*/
-Zotero.Utilities.Translate.prototype.doPost = function(url, body, onDone, requestContentType, responseCharset) {
+Zotero.Utilities.Translate.prototype.doPost = function(url, body, onDone, headers, responseCharset) {
url = this._convertURL(url);
var translate = this.translate;
@@ -880,7 +924,7 @@ Zotero.Utilities.Translate.prototype.doPost = function(url, body, onDone, reques
} catch(e) {
translate.error(false, e);
}
- }, requestContentType, responseCharset);
+ }, headers, responseCharset);
}
/**
@@ -936,7 +980,6 @@ Zotero.Utilities.HTTP = new function() {
}
else {
Zotero.debug("HTTP GET " + url);
-
}
if (this.browserIsOffline()){
return false;
@@ -981,12 +1024,20 @@ Zotero.Utilities.HTTP = new function() {
* @param {String} url URL to request
* @param {String} body Request body
* @param {Function} onDone Callback to be executed upon request completion
- * @param {String} requestContentType Request content type (usually
- * application/x-www-form-urlencoded)
+ * @param {String} headers Request HTTP headers
* @param {String} responseCharset Character set to force on the response
* @return {Boolean} True if the request was sent, or false if the browser is offline
*/
- this.doPost = function(url, body, onDone, requestContentType, responseCharset) {
+ this.doPost = function(url, body, onDone, headers, responseCharset) {
+ if (url instanceof Components.interfaces.nsIURI) {
+ // Don't display password in console
+ var disp = url.clone();
+ if (disp.password) {
+ disp.password = "********";
+ }
+ url = url.spec;
+ }
+
var bodyStart = body.substr(0, 1024);
// Don't display sync password or session id in console
bodyStart = bodyStart.replace(/password=[^&]+/, 'password=********');
@@ -995,7 +1046,7 @@ Zotero.Utilities.HTTP = new function() {
Zotero.debug("HTTP POST "
+ (body.length > 1024 ?
bodyStart + '... (' + body.length + ' chars)' : bodyStart)
- + " to " + url);
+ + " to " + (disp ? disp.spec : url));
if (this.browserIsOffline()){
@@ -1025,7 +1076,27 @@ Zotero.Utilities.HTTP = new function() {
xmlhttp.channel.loadGroup = ds.getInterface(Ci.nsILoadGroup);
xmlhttp.channel.loadFlags |= Ci.nsIChannel.LOAD_DOCUMENT_URI;
- xmlhttp.setRequestHeader("Content-Type", (requestContentType ? requestContentType : "application/x-www-form-urlencoded" ));
+ if (headers) {
+ if (typeof headers == 'string') {
+ var msg = "doPost() now takes a headers object rather than a requestContentType -- update your code";
+ Zotero.debug(msg, 2);
+ Components.utils.reportError(msg);
+ headers = {
+ "Content-Type": headers
+ };
+ }
+ }
+ else {
+ headers = {};
+ }
+
+ if (!headers["Content-Type"]) {
+ headers["Content-Type"] = "application/x-www-form-urlencoded";
+ }
+
+ for (var header in headers) {
+ xmlhttp.setRequestHeader(header, headers[header]);
+ }
/** @ignore */
xmlhttp.onreadystatechange = function(){
@@ -1042,10 +1113,24 @@ Zotero.Utilities.HTTP = new function() {
*
* @param {String} url URL to request
* @param {Function} onDone Callback to be executed upon request completion
+ * @param {Object} requestHeaders HTTP headers to include with request
* @return {Boolean} True if the request was sent, or false if the browser is offline
*/
- this.doHead = function(url, onDone) {
- Zotero.debug("HTTP HEAD "+url);
+ this.doHead = function(url, onDone, requestHeaders) {
+ if (url instanceof Components.interfaces.nsIURI) {
+ // Don't display password in console
+ var disp = url.clone();
+ if (disp.password) {
+ disp.password = "********";
+ }
+ Zotero.debug("HTTP HEAD " + disp.spec);
+ url = url.spec;
+ }
+ else {
+ Zotero.debug("HTTP HEAD " + url);
+
+ }
+
if (this.browserIsOffline()){
return false;
}
@@ -1068,10 +1153,20 @@ Zotero.Utilities.HTTP = new function() {
ds.itemType = Ci.nsIDocShellTreeItem.typeContent;
var xmlhttp = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Ci.nsIXMLHttpRequest);
+ // Prevent certificate/authentication dialogs from popping up
+ xmlhttp.mozBackgroundRequest = true;
xmlhttp.open("HEAD", url, true);
+ if (requestHeaders) {
+ for (var header in requestHeaders) {
+ xmlhttp.setRequestHeader(header, requestHeaders[header]);
+ }
+ }
xmlhttp.channel.loadGroup = ds.getInterface(Ci.nsILoadGroup);
xmlhttp.channel.loadFlags |= Ci.nsIChannel.LOAD_DOCUMENT_URI;
+ // Don't cache HEAD requests
+ xmlhttp.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+
/** @ignore */
xmlhttp.onreadystatechange = function(){
_stateChange(xmlhttp, onDone);
diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js
@@ -28,7 +28,8 @@ const ZOTERO_CONFIG = {
REPOSITORY_RETRY_INTERVAL: 3600, // 1 hour
BASE_URI: 'http://zotero.org/',
WWW_BASE_URL: 'http://www.zotero.org/',
- SYNC_URL: 'https://sync.zotero.org/'
+ SYNC_URL: 'https://sync.zotero.org/',
+ API_URL: 'https://api.zotero.org/'
};
/*
@@ -100,6 +101,17 @@ var Zotero = new function(){
Zotero.DB.query(sql, parseInt(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.getLocalUserKey = function (generate) {
if (_localUserKey) {
return _localUserKey;
@@ -1160,8 +1172,9 @@ var Zotero = new function(){
Zotero.Fulltext.purgeUnusedWords();
Zotero.Items.purge();
- if (!skipStoragePurge && Zotero.Sync.Storage.active && Zotero.Utilities.prototype.probability(10)) {
- Zotero.Sync.Storage.purgeDeletedStorageFiles();
+ if (!skipStoragePurge && Zotero.Utilities.prototype.probability(10)) {
+ Zotero.Sync.Storage.purgeDeletedStorageFiles('zfs');
+ Zotero.Sync.Storage.purgeDeletedStorageFiles('webdav');
}
}
diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties
@@ -1,5 +1,6 @@
extensions.zotero@chnm.gmu.edu.description = The Next-Generation Research Tool
+general.success = Success
general.error = Error
general.warning = Warning
general.dontShowWarningAgain = Don't show this warning again.
diff --git a/chrome/skin/default/zotero/error.png b/chrome/skin/default/zotero/error.png
Binary files differ.
diff --git a/chrome/skin/default/zotero/exclamation.png b/chrome/skin/default/zotero/exclamation.png
Binary files differ.
diff --git a/chrome/skin/default/zotero/overlay.css b/chrome/skin/default/zotero/overlay.css
@@ -205,9 +205,15 @@
font-weight: bold;
}
-#zotero-tb-storage-sync
+#zotero-tb-sync-warning
{
- list-style-image: url(chrome://zotero/skin/drive_network.png);
+ list-style-image: url(chrome://zotero/skin/error.png);
+ margin-right: -5px;
+}
+
+#zotero-tb-sync-warning[error=true]
+{
+ list-style-image: url(chrome://zotero/skin/exclamation.png);
}
#zotero-tb-sync {
@@ -220,10 +226,6 @@
list-style-image: url(chrome://zotero/skin/arrow_rotate_animated.png);
}
-#zotero-tb-sync[status=error] {
- list-style-image: url(chrome://zotero/skin/arrow_rotate_error.png);
-}
-
#zotero-tb-sync #zotero-last-sync-time
{
color: gray;
diff --git a/components/zotero-service.js b/components/zotero-service.js
@@ -64,6 +64,9 @@ var xpcomFiles = [
'style',
'sync',
'storage',
+ 'storage/session',
+ 'storage/zfs',
+ 'storage/webdav',
'timeline',
'translate',
'uri',
diff --git a/defaults/preferences/zotero.js b/defaults/preferences/zotero.js
@@ -37,6 +37,10 @@ pref("extensions.zotero.sortAttachmentsChronologically", false);
pref("extensions.zotero.showTrashWhenEmpty", true);
pref("extensions.zotero.viewOnDoubleClick", true);
+pref("extensions.zotero.groups.copyChildLinks", true);
+pref("extensions.zotero.groups.copyChildFileAttachments", true);
+pref("extensions.zotero.groups.copyChildNotes", true);
+
pref("extensions.zotero.backup.numBackups", 2);
pref("extensions.zotero.backup.interval", 1440);
@@ -113,14 +117,16 @@ pref("extensions.zotero.annotations.warnOnClose", true);
pref("extensions.zotero.sync.autoSync", true);
pref("extensions.zotero.sync.server.username", '');
pref("extensions.zotero.sync.server.compressData", true);
-pref("extensions.zotero.sync.storage.protocol", "webdavs");
-pref("extensions.zotero.sync.storage.enabled", false);
+pref("extensions.zotero.sync.storage.enabled", true);
+pref("extensions.zotero.sync.storage.protocol", "zotero");
pref("extensions.zotero.sync.storage.verified", false);
+pref("extensions.zotero.sync.storage.scheme", 'https');
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);
+pref("extensions.zotero.sync.storage.groups.enabled", true);
// Proxy
pref("extensions.zotero.proxies.transparent", true);
diff --git a/userdata.sql b/userdata.sql
@@ -1,4 +1,4 @@
--- 62
+-- 63
-- This file creates tables containing user-specific data for new users --
-- any changes made here must be mirrored in transition steps in schema.js::_migrateSchema()
@@ -70,6 +70,7 @@ CREATE TABLE itemAttachments (
originalPath TEXT,
syncState INT DEFAULT 0,
storageModTime INT,
+ storageHash TEXT,
FOREIGN KEY (itemID) REFERENCES items(itemID),
FOREIGN KEY (sourceItemID) REFERENCES items(itemID)
);