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.
This commit is contained in:
Dan Stillman 2009-09-13 07:23:29 +00:00
parent a611706f86
commit 884e5474fe
31 changed files with 4261 additions and 1997 deletions

View File

@ -177,11 +177,25 @@ var ZoteroPane = new function()
); );
if (index == 0) { if (index == 0) {
Zotero.Sync.Server.sync(function () { Zotero.Sync.Server.sync({
pr.alert( onSuccess: function () {
"Restore Completed", Zotero.Sync.Runner.setSyncIcon();
"The local Zotero database has been successfully restored."
); 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); }, 1000);
@ -898,7 +912,7 @@ var ZoteroPane = new function()
if (!tagSelector.getAttribute('collapsed') || if (!tagSelector.getAttribute('collapsed') ||
tagSelector.getAttribute('collapsed') == 'false') { tagSelector.getAttribute('collapsed') == 'false') {
Zotero.debug('Updating tag selector with current tags'); Zotero.debug('Updating tag selector with current tags');
if (itemGroup.isEditable()) { if (itemGroup.editable) {
tagSelector.mode = 'edit'; tagSelector.mode = 'edit';
} }
else { else {
@ -2281,7 +2295,7 @@ var ZoteroPane = new function()
var disabled = Zotero.locked; var disabled = Zotero.locked;
if (!disabled && self.collectionsView.selection && self.collectionsView.selection.count) { if (!disabled && self.collectionsView.selection && self.collectionsView.selection.count) {
var itemGroup = self.collectionsView._getItemAtRow(self.collectionsView.selection.currentIndex); var itemGroup = self.collectionsView._getItemAtRow(self.collectionsView.selection.currentIndex);
disabled = !itemGroup.isEditable() disabled = !itemGroup.editable;
} }
for each(var menuitem in menu.firstChild.childNodes) { for each(var menuitem in menu.firstChild.childNodes) {
menuitem.disabled = disabled; menuitem.disabled = disabled;
@ -2837,7 +2851,26 @@ var ZoteroPane = new function()
} }
var itemGroup = this.collectionsView._getItemAtRow(row); 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) { this.setLastSyncStatus = function (tooltip) {
var label = tooltip.firstChild.nextSibling; 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; var lastSyncTime = Zotero.Sync.Server.lastLocalSyncTime;
// TODO: localize // TODO: localize
msg = 'Last sync: '; msg = 'Last sync: ';

View File

@ -164,8 +164,8 @@
<menuitem hidden="true" label=" Reset Client" oncommand="Zotero.Sync.Server.resetClient()"/> <menuitem hidden="true" label=" Reset Client" oncommand="Zotero.Sync.Server.resetClient()"/>
<menuitem label="Storage Debugging" disabled="true"/> <menuitem label="Storage Debugging" disabled="true"/>
<menuitem hidden="true" label=" Reset Storage History" oncommand="Zotero.Sync.Storage.resetAllSyncStates()"/> <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 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(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"/> <menuseparator id="zotero-tb-actions-separator"/>
<menuitem id="zotero-tb-actions-prefs" label="&zotero.toolbar.preferences.label;" <menuitem id="zotero-tb-actions-prefs" label="&zotero.toolbar.preferences.label;"
oncommand="ZoteroPane.openPreferences()"/> oncommand="ZoteroPane.openPreferences()"/>
@ -375,6 +375,7 @@
</grid> </grid>
</tooltip> </tooltip>
</hbox> </hbox>
<toolbarbutton id="zotero-tb-sync-warning" hidden="true"/>
<toolbarbutton id="zotero-tb-sync" class="zotero-tb-button" tooltip="_child" <toolbarbutton id="zotero-tb-sync" class="zotero-tb-button" tooltip="_child"
oncommand="Zotero.Sync.Server.canAutoResetClient = true; Zotero.Sync.Runner.sync()"> oncommand="Zotero.Sync.Server.canAutoResetClient = true; Zotero.Sync.Runner.sync()">
<tooltip <tooltip

View File

@ -42,7 +42,7 @@ function init()
rows[i].firstChild.nextSibling.value = Zotero.isMac ? 'Cmd+Shift+' : 'Ctrl+Alt+'; rows[i].firstChild.nextSibling.value = Zotero.isMac ? 'Cmd+Shift+' : 'Ctrl+Alt+';
} }
updateStorageSettings(); updateStorageSettings(null, null, true);
refreshStylesList(); refreshStylesList();
refreshProxyList(); refreshProxyList();
populateQuickCopyList(); populateQuickCopyList();
@ -171,43 +171,85 @@ function populateOpenURLResolvers() {
// //
// Sync // Sync
// //
/* function updateStorageSettings(enabled, protocol, skipWarnings) {
function updateSyncStatus() { if (enabled === null) {
var disabled = !Zotero.Sync.Server.enabled; enabled = document.getElementById('pref-storage-enabled').value;
}
var radioGroup = document.getElementById('zotero-reset').firstChild; var oldProtocol = document.getElementById('pref-storage-protocol').value;
radioGroup.disabled = disabled; if (protocol === null) {
var labels = radioGroup.getElementsByTagName('label'); protocol = oldProtocol;
for each(var label in labels) {
label.disabled = disabled;
} }
var labels = radioGroup.getElementsByTagName('description');
for each(var label in labels) { var protocolMenu = document.getElementById('storage-protocol');
label.disabled = disabled; var settings = document.getElementById('storage-webdav-settings');
var sep = document.getElementById('storage-separator');
if (!enabled || protocol == 'zotero') {
settings.hidden = true;
sep.hidden = false;
}
else {
settings.hidden = false;
sep.hidden = true;
}
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."
);
}
});
}
}
} }
document.getElementById('zotero-reset-button').disabled = disabled;
} }
*/
function updateStorageSettings(value) {
if (!value) {
value = document.getElementById('pref-storage-protocol').value;
}
var prefix = document.getElementById('storage-url-prefix');
switch (value) {
case 'webdav':
prefix.value = 'http://';
break;
case 'webdavs':
prefix.value = 'https://';
break;
}
}
function unverifyStorageServer() { function unverifyStorageServer() {
Zotero.Sync.Storage.clearSettingsCache();
Zotero.Prefs.set('sync.storage.verified', false); Zotero.Prefs.set('sync.storage.verified', false);
Zotero.Sync.Storage.resetAllSyncStates(null, true, false);
} }
function verifyStorageServer() { function verifyStorageServer() {
@ -220,7 +262,7 @@ function verifyStorageServer() {
var usernameField = document.getElementById("storage-username"); var usernameField = document.getElementById("storage-username");
var passwordField = document.getElementById("storage-password"); var passwordField = document.getElementById("storage-password");
var callback = function (uri, status, authRequired) { var callback = function (uri, status) {
verifyButton.hidden = false; verifyButton.hidden = false;
abortButton.hidden = true; abortButton.hidden = true;
progressMeter.hidden = true; progressMeter.hidden = true;
@ -245,13 +287,13 @@ function verifyStorageServer() {
break; break;
} }
Zotero.Sync.Storage.checkServerCallback(uri, status, authRequired, window); Zotero.Sync.Storage.checkServerCallback(uri, status, window);
} }
verifyButton.hidden = true; verifyButton.hidden = true;
abortButton.hidden = false; abortButton.hidden = false;
progressMeter.hidden = false; progressMeter.hidden = false;
var requestHolder = Zotero.Sync.Storage.checkServer(callback); var requestHolder = Zotero.Sync.Storage.checkServer('webdav', callback);
abortButton.onclick = function () { abortButton.onclick = function () {
if (requestHolder.request) { if (requestHolder.request) {
requestHolder.request.onreadystatechange = undefined; requestHolder.request.onreadystatechange = undefined;

View File

@ -38,7 +38,8 @@ To add a new preference:
--> -->
<prefwindow id="zotero-prefs" title="&zotero.preferences.title;" onload="moveToAlertPosition(); init()" onunload="Zotero_Preferences.onUnload()" <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" <prefpane id="zotero-prefpane-general"
label="&zotero.preferences.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-automaticSnapshots" name="extensions.zotero.automaticSnapshots" type="bool"/>
<preference id="pref-downloadAssociatedFiles" name="extensions.zotero.downloadAssociatedFiles" 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-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> </preferences>
<groupbox> <groupbox>
@ -138,31 +141,18 @@ To add a new preference:
</groupbox> </groupbox>
<groupbox> <groupbox>
<caption label="&zotero.preferences.openurl.caption;"/> <caption label="Groups"/>
<button id="openURLSearchButton" label="&zotero.preferences.openurl.search;" oncommand="populateOpenURLResolvers()"/> <!-- TODO: localize -->
<menulist id="openURLMenu" oncommand="onOpenURLSelected();"> <label value="When copying items between libraries, include:"/>
<menupopup> <vbox style="margin-left: 2em">
<menuseparator/> <checkbox label="child notes" preference="pref-groups-copyChildNotes"/>
<menuitem label="&zotero.preferences.openurl.custom;" value="custom" selected="true"/> <checkbox label="child snapshots and imported files" preference="pref-groups-copyChildFileAttachments"/>
</menupopup> <checkbox label="child links" preference="pref-groups-copyChildLinks"/>
</menulist> </vbox>
<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>
</groupbox> </groupbox>
<separator/>
</prefpane> </prefpane>
@ -174,10 +164,12 @@ To add a new preference:
<preferences> <preferences>
<preference id="pref-sync-autosync" name="extensions.zotero.sync.autoSync" type="bool"/> <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-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-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-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-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> </preferences>
<!-- This doesn't wrap without an explicit width, for some reason --> <!-- This doesn't wrap without an explicit width, for some reason -->
@ -250,50 +242,44 @@ To add a new preference:
</groupbox> </groupbox>
<!-- TODO: localize -->
<groupbox> <groupbox>
<caption label="Storage Server"/> <caption label="File Syncing"/>
<hbox> <!-- My Library -->
<checkbox label="Enable file syncing" preference="pref-storage-enabled"/> <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> </hbox>
<separator class="thin"/> <stack id="storage-webdav-settings" style="margin-top: .5em; margin-bottom: .8em; border: 1px gray solid; -moz-border-radius: 3px">
<hbox align="center"> <!-- Background shading -->
<label value="A" style="margin-right: 0"/> <box style="background: black; opacity:.03"/>
<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"/>
</hbox>
<separator class="thin"/> <grid style="padding: .7em .4em .7em 0">
<label value="Syncing of attachment files in group libraries is not currently supported."/>
<separator/>
<grid id="storage-settings">
<columns> <columns>
<column/> <column/>
<column flex="1"/> <column flex="1"/>
</columns> </columns>
<rows> <rows>
<row>
<label value="Protocol:"/>
<hbox>
<menulist id="storage-url-protocol"
preference="pref-storage-protocol"
onsynctopreference="updateStorageSettings(this.value); unverifyStorageServer();">
<menupopup>
<menuitem label="WebDAV" value="webdav"/>
<!-- TODO: localize -->
<menuitem label="WebDAV (Secure)" value="webdavs"/>
</menupopup>
</menulist>
</hbox>
</row>
<row> <row>
<label value="URL:"/> <label value="URL:"/>
<hbox> <hbox>
<label id="storage-url-prefix"/> <menulist id="storage-url-prefix"
preference="pref-storage-scheme"
onsynctopreference="unverifyStorageServer()" style="padding: 0; width: 7em">
<menupopup>
<menuitem label="http" value="http" style="padding: 0"/>
<menuitem label="https" value="https" style="padding: 0"/>
</menupopup>
</menulist>
<label value="://"/>
<textbox id="storage-url" flex="1" <textbox id="storage-url" flex="1"
preference="pref-storage-url" preference="pref-storage-url"
onkeypress="if (Zotero.isMac &amp;&amp; event.keyCode == 13) { this.blur(); verifyStorageServer(); }" onkeypress="if (Zotero.isMac &amp;&amp; event.keyCode == 13) { this.blur(); verifyStorageServer(); }"
@ -309,7 +295,7 @@ To add a new preference:
preference="pref-storage-username" preference="pref-storage-username"
onkeypress="if (Zotero.isMac &amp;&amp; event.keyCode == 13) { this.blur(); setTimeout('verifyStorageServer();', 1); }" onkeypress="if (Zotero.isMac &amp;&amp; event.keyCode == 13) { this.blur(); setTimeout('verifyStorageServer();', 1); }"
onsynctopreference="unverifyStorageServer();" 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> </hbox>
</row> </row>
<row> <row>
@ -318,7 +304,7 @@ To add a new preference:
<textbox id="storage-password" flex="0" type="password" <textbox id="storage-password" flex="0" type="password"
onkeypress="if (Zotero.isMac &amp;&amp; event.keyCode == 13) { this.blur(); setTimeout('verifyStorageServer();', 1); }" onkeypress="if (Zotero.isMac &amp;&amp; event.keyCode == 13) { this.blur(); setTimeout('verifyStorageServer();', 1); }"
oninput="unverifyStorageServer()" oninput="unverifyStorageServer()"
onchange="Zotero.Sync.Storage.password = this.value"/> onchange="Zotero.Sync.Storage.Session.WebDAV.prototype.password = this.value"/>
</hbox> </hbox>
</row> </row>
<row> <row>
@ -333,6 +319,20 @@ To add a new preference:
</row> </row>
</rows> </rows>
</grid> </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> </groupbox>
</tabpanel> </tabpanel>
@ -377,7 +377,7 @@ To add a new preference:
</groupbox> </groupbox>
<groupbox> <groupbox>
<caption label="Storage Server"/> <caption label="File Syncing"/>
<grid> <grid>
<columns> <columns>
@ -404,6 +404,8 @@ To add a new preference:
</tabpanel> </tabpanel>
</tabpanels> </tabpanels>
</tabbox> </tabbox>
<separator/>
</prefpane> </prefpane>
@ -492,6 +494,8 @@ To add a new preference:
</rows> </rows>
</grid> </grid>
</groupbox> </groupbox>
<separator/>
</prefpane> </prefpane>
@ -499,21 +503,10 @@ To add a new preference:
label="&zotero.preferences.prefpane.export;" label="&zotero.preferences.prefpane.export;"
image="chrome://zotero/skin/prefs-export.png"> image="chrome://zotero/skin/prefs-export.png">
<preferences> <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-setting" name="extensions.zotero.export.quickCopy.setting" type="string"/>
<preference id="pref-quickCopy-dragLimit" name="extensions.zotero.export.quickCopy.dragLimit" type="int"/> <preference id="pref-quickCopy-dragLimit" name="extensions.zotero.export.quickCopy.dragLimit" type="int"/>
</preferences> </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> <groupbox>
<caption label="&zotero.preferences.quickCopy.caption;"/> <caption label="&zotero.preferences.quickCopy.caption;"/>
@ -548,23 +541,38 @@ To add a new preference:
<button label="+" onclick="showQuickCopySiteEditor()"/> <button label="+" onclick="showQuickCopySiteEditor()"/>
</hbox> </hbox>
<separator/>
<!-- TODO: localize --> <!-- TODO: localize -->
<hbox align="center"> <hbox align="center">
<label value="Disable Quick Copy when dragging more than"/> <label value="Disable Quick Copy when dragging more than"/>
<textbox preference="pref-quickCopy-dragLimit" size="3"/> <textbox preference="pref-quickCopy-dragLimit" size="3"/>
<label value="items" flex="1"/> <label value="items" flex="1"/>
</hbox> </hbox>
<separator/>
</groupbox> </groupbox>
<groupbox>
<caption label="&zotero.preferences.charset;"/>
<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>
<prefpane id="zotero-prefpane-styles" <prefpane id="zotero-prefpane-styles"
label="&zotero.preferences.prefpane.styles;" label="&zotero.preferences.prefpane.styles;"
image="chrome://zotero/skin/prefs-styles.png"> image="chrome://zotero/skin/prefs-styles.png">
<preferences>
<preference id="pref-styles-citePaperJournalArticleURL" name="extensions.zotero.export.citePaperJournalArticleURL" type="bool"/>
</preferences>
<groupbox flex="1"> <groupbox flex="1">
<caption label="&zotero.preferences.styles.styleManager;"/> <caption label="&zotero.preferences.styles.styleManager;"/>
@ -579,13 +587,24 @@ To add a new preference:
<treechildren id="styleManager-rows"/> <treechildren id="styleManager-rows"/>
</tree> </tree>
<separator class="thin"/> <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 disabled="true" id="styleManager-delete" label="-" onclick="deleteStyle()"/>
<button label="+" onclick="addStyle()"/> <button label="+" onclick="addStyle()"/>
</hbox> </hbox>
<separator/>
<label class="text-link" href="http://www.zotero.org/styles/" value="&zotero.preferences.export.getAdditionalStyles;"/>
</groupbox> </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> </prefpane>
@ -621,6 +640,8 @@ To add a new preference:
<button label="+" onclick="showProxyEditor()"/> <button label="+" onclick="showProxyEditor()"/>
</hbox> </hbox>
</groupbox> </groupbox>
<separator/>
</prefpane> </prefpane>
@ -714,6 +735,8 @@ To add a new preference:
<checkbox label="&zotero.preferences.keys.overrideGlobal;" preference="pref-keys-overrideGlobal"/> <checkbox label="&zotero.preferences.keys.overrideGlobal;" preference="pref-keys-overrideGlobal"/>
<label class="statusLine" value="&zotero.preferences.keys.changesTakeEffect;"/> <label class="statusLine" value="&zotero.preferences.keys.changesTakeEffect;"/>
<separator/>
</prefpane> </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-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-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-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> </preferences>
<groupbox> <groupbox>
@ -750,9 +775,6 @@ To add a new preference:
<hbox> <hbox>
<button label="&zotero.preferences.dbMaintenance.integrityCheck;" oncommand="runIntegrityCheck()"/> <button label="&zotero.preferences.dbMaintenance.integrityCheck;" oncommand="runIntegrityCheck()"/>
</hbox>
<hbox>
<button label="&zotero.preferences.dbMaintenance.resetTranslatorsAndStyles;" oncommand="resetTranslatorsAndStyles()"/> <button label="&zotero.preferences.dbMaintenance.resetTranslatorsAndStyles;" oncommand="resetTranslatorsAndStyles()"/>
</hbox> </hbox>
</groupbox> </groupbox>
@ -782,16 +804,38 @@ To add a new preference:
</groupbox> </groupbox>
<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"> <hbox align="center">
<label value="&zotero.preferences.charset.importCharset;:" control="zotero-import-charsetMenu"/> <!-- vbox prevents some weird vertical stretching of the menulist -->
<menulist id="zotero-import-charsetMenu" preference="pref-import-charset"/> <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.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> </hbox>
</groupbox> </groupbox>
<separator/>
</prefpane> </prefpane>
<!-- These mess up the prefwindow (more) if they come before the prefpanes <!-- These mess up the prefwindow (more) if they come before the prefpanes

View File

@ -43,7 +43,7 @@ Zotero.Attachments = new function(){
var self = this; var self = this;
function importFromFile(file, sourceItemID){ function importFromFile(file, sourceItemID, libraryID) {
Zotero.debug('Importing attachment from file'); Zotero.debug('Importing attachment from file');
var title = file.leafName; var title = file.leafName;
@ -61,6 +61,9 @@ Zotero.Attachments = new function(){
var parentItem = Zotero.Items.get(sourceItemID); var parentItem = Zotero.Items.get(sourceItemID);
attachmentItem.libraryID = parentItem.libraryID; attachmentItem.libraryID = parentItem.libraryID;
} }
else if (libraryID) {
attachmentItem.libraryID = libraryID;
}
attachmentItem.setField('title', title); attachmentItem.setField('title', title);
attachmentItem.setSource(sourceItemID); attachmentItem.setSource(sourceItemID);
attachmentItem.attachmentLinkMode = this.LINK_MODE_IMPORTED_FILE; attachmentItem.attachmentLinkMode = this.LINK_MODE_IMPORTED_FILE;
@ -979,6 +982,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 {Zotero.Item} item
* @param {Boolean} [skipHidden=FALSE] Don't count hidden files * @param {Boolean} [skipHidden=FALSE] Don't count hidden files
@ -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){ function _getFileNameFromURL(url, mimeType){
var nsIURL = Components.classes["@mozilla.org/network/standard-url;1"] var nsIURL = Components.classes["@mozilla.org/network/standard-url;1"]
.createInstance(Components.interfaces.nsIURL); .createInstance(Components.interfaces.nsIURL);

View File

@ -326,9 +326,14 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids)
break; break;
case 'group': case 'group':
if (this._groupRowMap[ids[i]] != null) { //if (this._groupRowMap[ids[i]] != null) {
rows.push(this._groupRowMap[ids[i]]); // 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; break;
} }
} }
@ -635,7 +640,7 @@ Zotero.CollectionTreeView.prototype.isSelectable = function (row, col) {
Zotero.CollectionTreeView.prototype.__defineGetter__('editable', function () { 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 var itemGroup = this._getItemAtRow(row); //the collection we are dragging over
if (!itemGroup.isEditable()) { if (!itemGroup.editable) {
return false; return false;
} }
if (dataType == 'zotero/item') { if (dataType == 'zotero/item') {
if(itemGroup.isBucket()) { if(itemGroup.isBucket()) {
return true; return true;
} }
var ids = data; var ids = data;
var items = Zotero.Items.get(ids); var items = Zotero.Items.get(ids);
var skip = true; var skip = true;
@ -1048,10 +1052,13 @@ Zotero.CollectionTreeView.prototype.canDrop = function(row, orient, dragData)
return false return false
} }
// TODO: for now, only allow regular items to be dragged to groups if (itemGroup.isWithinGroup() && item.isAttachment()) {
if (itemGroup.isWithinGroup() && itemGroup.ref.libraryID != item.libraryID // Linked files can't be added to groups
&& !item.isRegularItem()) { if (item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
return false; return false;
}
skip = false;
continue;
} }
// TODO: for now, skip items that are already linked // TODO: for now, skip items that are already linked
@ -1070,7 +1077,7 @@ Zotero.CollectionTreeView.prototype.canDrop = function(row, orient, dragData)
continue; continue;
} }
// Allow drag of group items to library // Allow drag of group items to personal library
if (item.libraryID && (itemGroup.isLibrary() if (item.libraryID && (itemGroup.isLibrary()
|| itemGroup.isCollection() && !itemGroup.isWithinGroup())) { || itemGroup.isCollection() && !itemGroup.isWithinGroup())) {
// TODO: for now, skip items that are already linked // TODO: for now, skip items that are already linked
@ -1106,10 +1113,17 @@ Zotero.CollectionTreeView.prototype.canDrop = function(row, orient, dragData)
if (itemGroup.isSearch()) { if (itemGroup.isSearch()) {
return false; return false;
} }
// Don't allow folder drag if (dataType == 'application/x-moz-file') {
if (dataType == 'application/x-moz-file' && data[0].isDirectory()) { // Don't allow folder drag
return false; if (data[0].isDirectory()) {
return false;
}
// Don't allow drop if no permissions
if (!itemGroup.filesEditable) {
return false;
}
} }
return true; return true;
} }
else if (dataType == 'zotero/collection') { else if (dataType == 'zotero/collection') {
@ -1179,7 +1193,7 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient)
else { else {
var targetLibraryID = null; var targetLibraryID = null;
} }
if(itemGroup.isBucket()) { if(itemGroup.isBucket()) {
itemGroup.ref.uploadItems(ids); itemGroup.ref.uploadItems(ids);
return; 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 // Create new unsaved clone item in target library
var newItem = new Zotero.Item(item.itemTypeID); var newItem = new Zotero.Item(item.itemTypeID);
newItem.libraryID = targetLibraryID; newItem.libraryID = targetLibraryID;
// DEBUG: save here because clone() doesn't currently work on unsaved tagged items // DEBUG: save here because clone() doesn't currently work on unsaved tagged items
var id = newItem.save(); var id = newItem.save();
var newItem = Zotero.Items.get(id); newItem = Zotero.Items.get(id);
item.clone(false, newItem); item.clone(false, newItem);
newItem.save(); newItem.save();
//var id = newItem.save(); //var id = newItem.save();
@ -1277,6 +1298,62 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient)
// Record link // Record link
item.addLinkedItem(newItem); item.addLinkedItem(newItem);
newIDs.push(id); 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) { if (toReconcile.length) {
@ -1342,12 +1419,11 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient)
return; return;
} }
else if (dataType == 'text/x-moz-url' || dataType == 'application/x-moz-file') { else if (dataType == 'text/x-moz-url' || dataType == 'application/x-moz-file') {
// FIXME: temporarily disable dragging in of files if (itemGroup.isWithinGroup()) {
if (dataType == 'application/x-moz-file' && itemGroup.isWithinGroup()) { var targetLibraryID = itemGroup.ref.libraryID;
var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] }
.getService(Components.interfaces.nsIPromptService); else {
ps.alert(null, "", "Files cannot currently be added to group libraries."); var targetLibraryID = null;
return;
} }
if (itemGroup.isCollection()) { if (itemGroup.isCollection()) {
@ -1398,7 +1474,7 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient)
try { try {
Zotero.DB.beginTransaction(); Zotero.DB.beginTransaction();
var itemID = Zotero.Attachments.importFromFile(file, false); var itemID = Zotero.Attachments.importFromFile(file, false, targetLibraryID);
if (parentCollectionID) { if (parentCollectionID) {
var col = Zotero.Collections.get(parentCollectionID); var col = Zotero.Collections.get(parentCollectionID);
if (col) { if (col) {
@ -1545,11 +1621,10 @@ Zotero.ItemGroup.prototype.isWithinGroup = function () {
return this.ref && !!this.ref.libraryID; return this.ref && !!this.ref.libraryID;
} }
Zotero.ItemGroup.prototype.isEditable = function () { Zotero.ItemGroup.prototype.__defineGetter__('editable', function () {
if (this.isTrash() || this.isShare()) { if (this.isTrash() || this.isShare()) {
return false; return false;
} }
if (!this.isWithinGroup()) { if (!this.isWithinGroup()) {
return true; return true;
} }
@ -1564,12 +1639,33 @@ Zotero.ItemGroup.prototype.isEditable = function () {
var group = Zotero.Groups.get(groupID); var group = Zotero.Groups.get(groupID);
return group.editable; return group.editable;
} }
else { throw ("Unknown library type '" + type + "' in Zotero.ItemGroup.editable");
throw ("Unknown library type '" + type + "' in Zotero.ItemGroup.isEditable()");
}
} }
} 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() Zotero.ItemGroup.prototype.getName = function()
{ {

View File

@ -327,7 +327,7 @@ Zotero.DataObjects = function (object, objectPlural, id, table) {
this.editCheck = function (obj) { 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"); throw ("Cannot edit " + this._ZDO_object + " in read-only Zotero library");
} }
} }

View File

@ -286,6 +286,10 @@ Zotero.Group.prototype.save = function () {
* Deletes group and all descendant objects * Deletes group and all descendant objects
**/ **/
Zotero.Group.prototype.erase = function() { 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(); Zotero.DB.beginTransaction();
var sql, ids, obj; var sql, ids, obj;
@ -331,6 +335,9 @@ Zotero.Group.prototype.erase = function() {
sql = "DELETE FROM syncDeleteLog WHERE libraryID=?"; sql = "DELETE FROM syncDeleteLog WHERE libraryID=?";
Zotero.DB.query(sql, this.libraryID); Zotero.DB.query(sql, this.libraryID);
var prefix = "groups/" + this.id;
Zotero.Relations.eraseByPathPrefix(prefix);
// Delete group // Delete group
sql = "DELETE FROM groups WHERE groupID=?"; sql = "DELETE FROM groups WHERE groupID=?";
ids = Zotero.DB.query(sql, this.id) ids = Zotero.DB.query(sql, this.id)
@ -342,6 +349,10 @@ Zotero.Group.prototype.erase = function() {
Zotero.DB.commitTransaction(); Zotero.DB.commitTransaction();
if (notifierDisabled) {
Zotero.Notifier.enable();
}
Zotero.Notifier.trigger('delete', 'group', this.id, notifierData); Zotero.Notifier.trigger('delete', 'group', this.id, notifierData);
} }

View File

@ -2540,28 +2540,16 @@ Zotero.Item.prototype.getFile = function(row, skipExistsCheck) {
/** /**
* _row_ is optional itemAttachments row if available to skip queries * _row_ is optional itemAttachments row if available to skip queries
*/ */
Zotero.Item.prototype.getFilename = function (row) { Zotero.Item.prototype.getFilename = function () {
if (!this.isAttachment()) { if (!this.isAttachment()) {
throw ("getFileName() can only be called on attachment items in Zotero.Item.getFilename()"); throw ("getFileName() can only be called on attachment items in Zotero.Item.getFilename()");
} }
if (!row) { if (this.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
var row = {
linkMode: this.attachmentLinkMode,
path: this.attachmentPath
};
}
if (row.linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
throw ("getFilename() cannot be called on link attachments in Zotero.Item.getFilename()"); throw ("getFilename() cannot be called on link attachments in Zotero.Item.getFilename()");
} }
if (this.isImportedAttachment()) { var file = this.getFile(null, true);
var matches = row.path.match("^storage:(.+)$");
return matches[1];
}
var file = this.getFile();
if (!file) { if (!file) {
return false; return false;
} }
@ -2602,10 +2590,12 @@ Zotero.Item.prototype.renameAttachmentFile = function(newName, overwrite) {
} }
file.moveTo(null, newName); 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(); dest.lastModifiedTime = new Date();
this.relinkAttachmentFile(dest); this.relinkAttachmentFile(dest);
Zotero.Sync.Storage.setSyncedHash(this.id, null, false);
return true; return true;
} }
catch (e) { catch (e) {
@ -2947,6 +2937,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 * Return plain text of attachment content
* *

View File

@ -421,7 +421,6 @@ Zotero.Items = new function() {
var item = this.get(id); var item = this.get(id);
if (!item) { if (!item) {
Zotero.debug('Item ' + id + ' does not exist in Items.erase()!', 1); Zotero.debug('Item ' + id + ' does not exist in Items.erase()!', 1);
Zotero.Notifier.trigger('delete', 'item', id);
continue; continue;
} }
item.erase(eraseChildren); // calls unload() item.erase(eraseChildren); // calls unload()

View File

@ -6,6 +6,7 @@ Zotero.Relations = new function () {
owl: 'http://www.w3.org/2002/07/owl#' owl: 'http://www.w3.org/2002/07/owl#'
}; };
var _prefix = "http://zotero.org/";
this.get = function (id) { this.get = function (id) {
if (typeof id != 'number') { 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) { this.add = function (libraryID, subject, predicate, object) {
predicate = _getPrefixAndValue(predicate).join(':'); 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) { this.xmlToRelation = function (xml) {
var relation = new Zotero.Relation; var relation = new Zotero.Relation;
var libraryID = xml.@libraryID.toString(); var libraryID = xml.@libraryID.toString();

View File

@ -421,7 +421,9 @@ Zotero.DBConnection.prototype.getLastErrorString = function () {
Zotero.DBConnection.prototype.beginTransaction = function () { Zotero.DBConnection.prototype.beginTransaction = function () {
// TODO: limit to Zotero.DB, not all Zotero.DBConnections? // TODO: limit to Zotero.DB, not all Zotero.DBConnections?
if (Zotero.waiting) { 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(); var db = this._getDBConnection();

View File

@ -1,6 +1,7 @@
Zotero.Error = function (message, error) { Zotero.Error = function (message, error, data) {
this.name = "ZOTERO_ERROR"; this.name = "ZOTERO_ERROR";
this.message = message; this.message = message;
this.data = data;
if (parseInt(error) == error) { if (parseInt(error) == error) {
this.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_FULL_SYNC_REQUIRED = 2;
Zotero.Error.ERROR_SYNC_USERNAME_NOT_SET = 3; Zotero.Error.ERROR_SYNC_USERNAME_NOT_SET = 3;
Zotero.Error.ERROR_INVALID_SYNC_LOGIN = 4; 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 () { Zotero.Error.prototype.toString = function () {
return this.message; return this.message;

View File

@ -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 * 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) { this.createDirectoryIfMissing = function (dir) {
if (!dir.exists() || !dir.isDirectory()) { if (!dir.exists() || !dir.isDirectory()) {
if (dir.exists() && !dir.isDirectory()) { if (dir.exists() && !dir.isDirectory()) {

View File

@ -2218,16 +2218,8 @@ Zotero.ItemTreeView.prototype.drop = function(row, orient)
} }
} }
else if (dataType == 'text/x-moz-url' || dataType == 'application/x-moz-file') { 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 // Disallow drop into read-only libraries
if (!itemGroup.isEditable()) { if (!itemGroup.editable) {
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator); .getService(Components.interfaces.nsIWindowMediator);
var win = wm.getMostRecentWindow("navigator:browser"); var win = wm.getMostRecentWindow("navigator:browser");
@ -2235,6 +2227,13 @@ Zotero.ItemTreeView.prototype.drop = function(row, orient)
return; return;
} }
if (itemGroup.isWithinGroup()) {
var targetLibraryID = itemGroup.ref.libraryID;
}
else {
var targetLibraryID = null;
}
var sourceItemID = false; var sourceItemID = false;
var parentCollectionID = false; var parentCollectionID = false;
@ -2245,9 +2244,6 @@ Zotero.ItemTreeView.prototype.drop = function(row, orient)
else if (itemGroup.isCollection()) { else if (itemGroup.isCollection()) {
var parentCollectionID = itemGroup.ref.id; var parentCollectionID = itemGroup.ref.id;
} }
else if (itemGroup.isLibrary(true)) {
var libraryID = itemGroup.ref.libraryID;
}
var unlock = Zotero.Notifier.begin(true); var unlock = Zotero.Notifier.begin(true);
try { try {
@ -2278,15 +2274,10 @@ Zotero.ItemTreeView.prototype.drop = function(row, orient)
// Still string, so remote URL // Still string, so remote URL
if (typeof file == 'string') { if (typeof file == 'string') {
if (sourceItemID) { var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
Zotero.Attachments.importFromURL(url, sourceItemID); .getService(Components.interfaces.nsIWindowMediator);
} var win = wm.getMostRecentWindow("navigator:browser");
else { win.ZoteroPane.addItemFromURL(url, 'temporaryPDFHack', row); // 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'); // TODO: don't do this
}
continue; continue;
} }
@ -2295,7 +2286,7 @@ Zotero.ItemTreeView.prototype.drop = function(row, orient)
try { try {
Zotero.DB.beginTransaction(); Zotero.DB.beginTransaction();
var itemID = Zotero.Attachments.importFromFile(file, sourceItemID); var itemID = Zotero.Attachments.importFromFile(file, sourceItemID, targetLibraryID);
if (parentCollectionID) { if (parentCollectionID) {
var col = Zotero.Collections.get(parentCollectionID); var col = Zotero.Collections.get(parentCollectionID);
if (col) { if (col) {

View File

@ -2451,6 +2451,21 @@ Zotero.Schema = new function(){
Zotero.DB.query("CREATE INDEX IF NOT EXISTS itemData_fieldID ON itemData(fieldID)"); 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(); Zotero.wait();
} }

File diff suppressed because it is too large Load Diff

View File

@ -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);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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;
}

View File

@ -118,7 +118,7 @@ Zotero.Sync = new function() {
} }
/** /**
* @param {Date} lastSyncDate JS Date object * @param {Date} lastSyncDate JS Date object
* @param {Zotero.Sync.Server.ObjectKeySet} * @param {Zotero.Sync.Server.ObjectKeySet}
* @return TRUE if found, FALSE if none, or -1 if last sync time is before start of log * @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 () { Zotero.Sync.Runner = new function () {
this.__defineGetter__("lastSyncError", function () {
return _lastSyncError;
});
this.__defineGetter__("background", function () { this.__defineGetter__("background", function () {
return _background; return _background;
}); });
var _lastSyncError;
var _autoSyncTimer; var _autoSyncTimer;
var _queue; var _queue;
var _running; var _running;
var _background; var _background;
var _warning = null;
this.init = function () { this.init = function () {
this.EventListener.init(); this.EventListener.init();
this.IdleListener.init(); this.IdleListener.init();
} }
this.sync = function (background) { this.sync = function (background) {
_warning = null;
if (Zotero.Utilities.HTTP.browserIsOffline()){ if (Zotero.Utilities.HTTP.browserIsOffline()){
_lastSyncError = "Browser is offline"; // TODO: localize
this.clearSyncTimeout(); // DEBUG: necessary? this.clearSyncTimeout(); // DEBUG: necessary?
this.setSyncIcon('error'); this.setSyncIcon('error', "Browser is offline");
return false; return false;
} }
@ -478,43 +476,138 @@ Zotero.Sync.Runner = new function () {
Zotero.purgeDataObjects(true); Zotero.purgeDataObjects(true);
_background = !!background; _background = !!background;
_queue = [
Zotero.Sync.Server.sync,
Zotero.Sync.Storage.sync,
Zotero.Sync.Server.sync,
Zotero.Sync.Storage.sync
];
_running = true; _running = true;
_lastSyncError = '';
this.setSyncIcon('animate'); this.setSyncIcon('animate');
this.next();
} var storageSync = function () {
var syncNeeded = false;
this.next = function () { Zotero.Sync.Storage.sync(
if (!_queue.length) { 'webdav',
this.setSyncIcon();
_running = false; {
return; // 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
}
)
} }
var func = _queue.shift();
func(); 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.setError = function (msg) { this.stop = function () {
this.setSyncIcon('error'); if (_warning) {
_lastSyncError = msg; Zotero.Sync.Runner.setSyncIcon('warning', _warning);
} _warning = null;
}
else {
this.reset = function () { Zotero.Sync.Runner.setSyncIcon();
_queue = []; }
_running = false; _running = false;
} }
/**
* Log a warning, but don't throw an error
*/
this.warning = function (e) {
Components.utils.reportError(e);
_warning = e;
}
this.error = function (e) {
Zotero.Sync.Runner.setSyncIcon('error', e);
_running = false;
throw (e);
}
/** /**
* @param {Integer} [timeout=15] Timeout in seconds * @param {Integer} [timeout=15] Timeout in seconds
* @param {Boolean} [recurring=false] * @param {Boolean} [recurring=false]
@ -593,12 +686,13 @@ Zotero.Sync.Runner = new function () {
} }
this.setSyncIcon = function (status) { this.setSyncIcon = function (status, e) {
status = status ? status : ''; status = status ? status : '';
switch (status) { switch (status) {
case '': case '':
case 'animate': case 'animate':
case 'warning':
case 'error': case 'error':
break; break;
@ -610,17 +704,113 @@ Zotero.Sync.Runner = new function () {
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator); .getService(Components.interfaces.nsIWindowMediator);
var win = wm.getMostRecentWindow('navigator:browser'); var win = wm.getMostRecentWindow('navigator:browser');
var icon = win.document.getElementById('zotero-tb-sync');
icon.setAttribute('status', status);
switch (status) { var warning = win.document.getElementById('zotero-tb-sync-warning');
case 'animate': var icon = win.document.getElementById('zotero-tb-sync');
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: if (!message) {
icon.setAttribute('disabled', false); 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 _throttleTimeout;
var _canAutoResetClient = true; 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 url = _serverURL + "login";
var username = Zotero.Sync.Server.username; var username = Zotero.Sync.Server.username;
@ -906,18 +1111,26 @@ Zotero.Sync.Server = new function () {
//Zotero.debug('Got session ID ' + _sessionID + ' from server'); //Zotero.debug('Got session ID ' + _sessionID + ' from server');
if (callback) { 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'); Zotero.Sync.Runner.setSyncIcon('animate');
if (!_sessionID) { if (!_sessionID) {
Zotero.debug("Session ID not available -- logging in"); 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; return;
} }
@ -955,7 +1168,9 @@ Zotero.Sync.Server = new function () {
Zotero.debug("Invalid session ID -- logging in"); Zotero.debug("Invalid session ID -- logging in");
_sessionID = false; _sessionID = false;
_syncInProgress = false; _syncInProgress = false;
Zotero.Sync.Server.login(Zotero.Sync.Server.sync, callback); Zotero.Sync.Server.login(function () {
Zotero.Sync.Server.sync(_callbacks);
});
return; return;
} }
@ -976,32 +1191,36 @@ Zotero.Sync.Server = new function () {
// Strip XML declaration and convert to E4X // Strip XML declaration and convert to E4X
var xml = new XML(xmlhttp.responseText.replace(/<\?xml.*\?>/, '')); var xml = new XML(xmlhttp.responseText.replace(/<\?xml.*\?>/, ''));
Zotero.DB.beginTransaction();
try { 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 userID = parseInt(xml.@userID);
var libraryID = parseInt(xml.@defaultLibraryID); 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.debug("Sync cancelled");
Zotero.DB.rollbackTransaction();
Zotero.Sync.Server.unlock(function () { Zotero.Sync.Server.unlock(function () {
if (callback) { _callbacks.onStop();
Zotero.Sync.Runner.setSyncIcon();
callback();
}
else {
Zotero.Sync.Runner.reset();
Zotero.Sync.Runner.next();
}
}); });
_syncInProgress = false; _syncInProgress = false;
return; return;
} }
Zotero.UnresponsiveScriptIndicator.disable(); Zotero.DB.beginTransaction();
var earliestRemoteDate = parseInt(xml.@earliest) ? Zotero.UnresponsiveScriptIndicator.disable();
new Date((xml.@earliest + 43200) * 1000) : false;
var lastLocalSyncTime = Zotero.Sync.Server.lastLocalSyncTime; var lastLocalSyncTime = Zotero.Sync.Server.lastLocalSyncTime;
var lastLocalSyncDate = lastLocalSyncTime ? var lastLocalSyncDate = lastLocalSyncTime ?
@ -1039,14 +1258,7 @@ Zotero.Sync.Server = new function () {
Zotero.debug("Sync cancelled"); Zotero.debug("Sync cancelled");
Zotero.DB.rollbackTransaction(); Zotero.DB.rollbackTransaction();
Zotero.Sync.Server.unlock(function () { Zotero.Sync.Server.unlock(function () {
if (callback) { _callbacks.onSkip();
Zotero.Sync.Runner.setSyncIcon();
callback();
}
else {
Zotero.Sync.Runner.reset();
Zotero.Sync.Runner.next();
}
}); });
Zotero.reloadDataObjects(); Zotero.reloadDataObjects();
Zotero.Sync.EventListener.resetIgnored(); Zotero.Sync.EventListener.resetIgnored();
@ -1067,13 +1279,7 @@ Zotero.Sync.Server = new function () {
Zotero.DB.commitTransaction(); Zotero.DB.commitTransaction();
Zotero.Sync.Server.unlock(function () { Zotero.Sync.Server.unlock(function () {
_syncInProgress = false; _syncInProgress = false;
if (callback) { _callbacks.onSuccess();
Zotero.Sync.Runner.setSyncIcon();
callback();
}
else {
Zotero.Sync.Runner.next();
}
}); });
return; return;
} }
@ -1114,13 +1320,7 @@ Zotero.Sync.Server = new function () {
Zotero.DB.commitTransaction(); Zotero.DB.commitTransaction();
Zotero.Sync.Server.unlock(function () { Zotero.Sync.Server.unlock(function () {
_syncInProgress = false; _syncInProgress = false;
if (callback) { _callbacks.onSuccess();
Zotero.Sync.Runner.setSyncIcon();
callback();
}
else {
Zotero.Sync.Runner.next();
}
}); });
} }
@ -1203,7 +1403,7 @@ Zotero.Sync.Server = new function () {
} }
function lock(callback, callbackCallback) { function lock(callback) {
Zotero.debug("Getting session lock"); Zotero.debug("Getting session lock");
if (!_sessionID) { if (!_sessionID) {
@ -1234,11 +1434,7 @@ Zotero.Sync.Server = new function () {
if (response.firstChild.tagName == 'error') { if (response.firstChild.tagName == 'error') {
if (_checkServerSessionLock(response.firstChild)) { if (_checkServerSessionLock(response.firstChild)) {
Zotero.Sync.Server.lock(function () { Zotero.Sync.Server.lock(callback ? function () { callback(); } : null);
if (callback) {
callback(callbackCallback);
}
});
return; return;
} }
@ -1252,7 +1448,7 @@ Zotero.Sync.Server = new function () {
_sessionLock = true; _sessionLock = true;
if (callback) { if (callback) {
callback(callbackCallback); callback();
} }
}); });
} }
@ -1300,7 +1496,9 @@ Zotero.Sync.Server = new function () {
function clear(callback) { function clear(callback) {
if (!_sessionID) { if (!_sessionID) {
Zotero.debug("Session ID not available -- logging in"); 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; return;
} }
@ -1312,7 +1510,9 @@ Zotero.Sync.Server = new function () {
if (_invalidSession(xmlhttp)) { if (_invalidSession(xmlhttp)) {
Zotero.debug("Invalid session ID -- logging in"); Zotero.debug("Invalid session ID -- logging in");
_sessionID = false; _sessionID = false;
Zotero.Sync.Server.login(Zotero.Sync.Server.clear, callback); Zotero.Sync.Server.login(function () {
Zotero.Sync.Server.clear(callback);
});
return; return;
} }
@ -1343,7 +1543,9 @@ Zotero.Sync.Server = new function () {
function resetServer(callback) { function resetServer(callback) {
if (!_sessionID) { if (!_sessionID) {
Zotero.debug("Session ID not available -- logging in"); 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; return;
} }
@ -1355,7 +1557,9 @@ Zotero.Sync.Server = new function () {
if (_invalidSession(xmlhttp)) { if (_invalidSession(xmlhttp)) {
Zotero.debug("Invalid session ID -- logging in"); Zotero.debug("Invalid session ID -- logging in");
_sessionID = false; _sessionID = false;
Zotero.Sync.Server.login(Zotero.Sync.Server.reset, callback); Zotero.Sync.Server.login(function () {
Zotero.Sync.Server.reset();
});
return; return;
} }
@ -1391,11 +1595,12 @@ Zotero.Sync.Server = new function () {
+ "('lastlocalsync', 'lastremotesync', 'syncdeletelog')"; + "('lastlocalsync', 'lastremotesync', 'syncdeletelog')";
Zotero.DB.query(sql); 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 syncDeleteLog");
Zotero.DB.query("DELETE FROM storageDeleteLog"); 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', ?)"; sql = "INSERT INTO version VALUES ('syncdeletelog', ?)";
Zotero.DB.query(sql, Zotero.Date.getUnixTimestamp()); Zotero.DB.query(sql, Zotero.Date.getUnixTimestamp());
@ -1545,6 +1750,41 @@ Zotero.Sync.Server = new function () {
}, 1); }, 1);
break; 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': case 'TAG_TOO_LONG':
if (!Zotero.Sync.Runner.background) { if (!Zotero.Sync.Runner.background) {
var tag = xmlhttp.responseXML.firstChild.getElementsByTagName('tag'); 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 * 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 " var sql = "SELECT value FROM settings WHERE "
+ "setting='account' AND key='username'"; + "setting='account' AND key='username'";
var lastUsername = Zotero.DB.valueQuery(sql); var lastUsername = Zotero.DB.valueQuery(sql);
@ -1636,7 +1886,11 @@ Zotero.Sync.Server = new function () {
var lastUserID = Zotero.userID; var lastUserID = Zotero.userID;
var lastLibraryID = Zotero.libraryID; var lastLibraryID = Zotero.libraryID;
var restartSync = false;
if (lastUserID && lastUserID != userID) { if (lastUserID && lastUserID != userID) {
var groups = Zotero.Groups.getAll();
var pr = Components.classes["@mozilla.org/network/default-prompt;1"] var pr = Components.classes["@mozilla.org/network/default-prompt;1"]
.createInstance(Components.interfaces.nsIPrompt); .createInstance(Components.interfaces.nsIPrompt);
var buttonFlags = (pr.BUTTON_POS_0) * (pr.BUTTON_TITLE_IS_STRING) 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_2) * (pr.BUTTON_TITLE_IS_STRING)
+ pr.BUTTON_POS_1_DEFAULT + pr.BUTTON_POS_1_DEFAULT
+ pr.BUTTON_DELAY_ENABLE; + pr.BUTTON_DELAY_ENABLE;
var index = pr.confirmEx(
Zotero.getString('general.warning'),
// TODO: localize
"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, {}
);
if (index > 0) { var msg = "This Zotero database was last synced with a different "
if (index == 1) { + "zotero.org account ('" + lastUsername + "') from the "
// Cancel + "current one ('" + username + "'). ";
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) { msg += "\n\n"
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"] + "To avoid combining or losing data, revert to the '"
.getService(Components.interfaces.nsIWindowMediator); + lastUsername + "' account or use the Reset options "
var lastWin = wm.getMostRecentWindow("navigator:browser"); + "in the Sync pane of the Zotero preferences.";
lastWin.ZoteroPane.openPreferences('zotero-prefpane-sync');
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;
} }
return false; // Delete all local groups
for each(var group in groups) {
group.erase();
}
restartSync = true;
} }
} }
if (lastUserID != userID || lastLibraryID != libraryID) { if (lastUserID != userID || lastLibraryID != libraryID) {
Zotero.userID = userID; Zotero.userID = userID;
Zotero.libraryID = libraryID; 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) { if (lastUsername != username) {
var sql = "REPLACE INTO settings VALUES ('account', 'username', ?)"; Zotero.username = username;
Zotero.DB.query(sql, username);
} }
return true; Zotero.DB.commitTransaction();
return restartSync ? 0 : 1;
} }
function _invalidSession(xmlhttp) { function _invalidSession(xmlhttp) {
if (xmlhttp.responseXML.childNodes[0].firstChild.tagName != 'error') { if (xmlhttp.responseXML.childNodes[0].firstChild.tagName != 'error') {
return false; return false;
@ -1783,8 +2089,7 @@ Zotero.Sync.Server = new function () {
Zotero.Sync.Server.unlock() Zotero.Sync.Server.unlock()
} }
Zotero.Sync.Runner.setError(e.message ? e.message : e); _callbacks.onError(e);
Zotero.Sync.Runner.reset();
throw (e); throw (e);
} }
@ -1970,6 +2275,7 @@ Zotero.Sync.Server.Data = new function() {
var itemStorageModTimes = {}; var itemStorageModTimes = {};
var childItemStore = []; var childItemStore = [];
// Remotely changed groups
if (xml.groups.length()) { if (xml.groups.length()) {
Zotero.debug("Processing remotely changed groups"); Zotero.debug("Processing remotely changed groups");
for each(var xmlNode in xml.groups.group) { 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()) { if (xml.deleted.groups.toString()) {
Zotero.debug("Processing remotely deleted groups"); Zotero.debug("Processing remotely deleted groups");
var groupIDs = xml.deleted.groups.toString().split(' '); var groupIDs = xml.deleted.groups.toString().split(' ');
@ -1988,21 +2295,14 @@ Zotero.Sync.Server.Data = new function() {
if (!group) { if (!group) {
continue; 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(); group.erase();
Zotero.Notifier.enable();
Zotero.Notifier.trigger('delete', 'group', groupID, notifierData);
} }
} }
// Other objects
for each(var syncObject in Zotero.Sync.syncObjects) { for each(var syncObject in Zotero.Sync.syncObjects) {
var Type = syncObject.singular; // 'Item' var Type = syncObject.singular; // 'Item'
var Types = syncObject.plural; // 'Items' var Types = syncObject.plural; // 'Items'
@ -2271,11 +2571,7 @@ Zotero.Sync.Server.Data = new function() {
syncSession.removeFromDeleted(creator.ref); syncSession.removeFromDeleted(creator.ref);
} }
} }
else if (obj.isAttachment() && else if (obj.isImportedAttachment()) {
(obj.attachmentLinkMode ==
Zotero.Attachments.LINK_MODE_IMPORTED_FILE ||
obj.attachmentLinkMode ==
Zotero.Attachments.LINK_MODE_IMPORTED_URL)) {
// Mark new attachments for download // Mark new attachments for download
if (isNewObject) { if (isNewObject) {
obj.attachmentSyncState = obj.attachmentSyncState =
@ -2285,7 +2581,8 @@ Zotero.Sync.Server.Data = new function() {
else { else {
var mtime = xmlNode.@storageModTime.toString(); var mtime = xmlNode.@storageModTime.toString();
if (mtime) { 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 they've been updated elsewhere and mark for download if so
if (type == 'item') { if (type == 'item') {
var ids = []; var ids = [];
var modTimes = {}; var modTimes = {};
for (var key in itemStorageModTimes) { for (var libraryKeyHash in itemStorageModTimes) {
var item = Zotero.Items.getByLibraryAndKey(null, key); var lk = Zotero.Items.parseLibraryKeyHash(libraryKeyHash);
var item = Zotero.Items.getByLibraryAndKey(lk.libraryID, lk.key);
ids.push(item.id); ids.push(item.id);
modTimes[item.id] = itemStorageModTimes[key]; modTimes[item.id] = itemStorageModTimes[libraryKeyHash];
} }
if (ids.length > 0) { if (ids.length > 0) {
Zotero.Sync.Storage.checkForUpdatedFiles(ids, modTimes); Zotero.Sync.Storage.checkForUpdatedFiles(ids, modTimes);
@ -3003,15 +3301,23 @@ Zotero.Sync.Server.Data = new function() {
xml.@charset = charset; xml.@charset = charset;
} }
// Include storage sync time and paths for non-links
if (item.attachment.linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) { if (item.attachment.linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) {
var mtime = Zotero.Sync.Storage.getSyncedModificationTime(item.primary.itemID); // Include paths for non-links
if (mtime) {
xml.@storageModTime = mtime;
}
var path = <path>{item.attachment.path}</path>; var path = <path>{item.attachment.path}</path>;
xml.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) { if (item.note) {

View File

@ -1,5 +1,6 @@
Zotero.URI = new function () { Zotero.URI = new function () {
var _baseURI = ZOTERO_CONFIG.BASE_URI; var _baseURI = ZOTERO_CONFIG.BASE_URI;
var _apiURI = ZOTERO_CONFIG.API_URI;
/** /**
@ -22,8 +23,11 @@ Zotero.URI = new function () {
* *
* @return {String} * @return {String}
*/ */
this.getCurrentUserURI = function () { this.getCurrentUserURI = function (noLocal) {
var userID = Zotero.userID; var userID = Zotero.userID;
if (!userID && noLocal) {
throw new Exception("Local userID not available and noLocal set in Zotero.URI.getCurrentUserURI()");
}
if (userID) { if (userID) {
return _baseURI + "users/" + userID; return _baseURI + "users/" + userID;
} }
@ -42,22 +46,44 @@ Zotero.URI = new function () {
this.getLibraryURI = function (libraryID) { 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) { switch (libraryType) {
case 'user':
var id = Zotero.userID;
if (!id) {
throw new Exception("User id not available in Zotero.URI.getLibraryPath()");
}
break;
case 'group': case 'group':
var id = Zotero.Groups.getGroupIDFromLibraryID(libraryID); var id = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
break; break;
case 'user':
throw ("User library ids are not supported in Zotero.URI.getLibraryURI");
default: 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) { this.getItemURI = function (item) {
if (item.libraryID) { if (item.libraryID) {
var baseURI = this.getLibraryURI(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 () { this.getGroupsURL = function () {
return ZOTERO_CONFIG.WWW_BASE_URL + "groups"; return ZOTERO_CONFIG.WWW_BASE_URL + "groups";
} }

View File

@ -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) { Zotero.Utilities.prototype.md5 = function(strOrFile, base64) {
var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"]. if (typeof strOrFile == 'string') {
createInstance(Components.interfaces.nsIScriptableUnicodeConverter); var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
converter.charset = "UTF-8"; createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
var result = {}; converter.charset = "UTF-8";
var data = converter.convertToByteArray(str, result); var result = {};
var ch = Components.classes["@mozilla.org/security/hash;1"] var data = converter.convertToByteArray(strOrFile, result);
.createInstance(Components.interfaces.nsICryptoHash); var ch = Components.classes["@mozilla.org/security/hash;1"]
ch.init(ch.MD5); .createInstance(Components.interfaces.nsICryptoHash);
ch.update(data, data.length); ch.init(ch.MD5);
var hash = ch.finish(false); 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 // return the two-digit hexadecimal code for a byte
function toHexString(charCode) { function toHexString(charCode) {
@ -417,6 +449,17 @@ Zotero.Utilities.prototype.md5 = function(str) {
// convert the binary hash data to a hex string. // convert the binary hash data to a hex string.
return [toHexString(hash.charCodeAt(i)) for (i in hash)].join(""); 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} url URL to load
* @param {String} [body=null] Request body to POST to the URL; a GET request is * @param {String} [body=null] Request body to POST to the URL; a GET request is
* executed if no body is present * executed if no body is present
* @param {String} [requestContentType=application/x-www-form-urlencoded] * @param {Object} [headers] HTTP headers to include in request;
* Request content type for POST; ignored if no body * 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 * @param {String} [responseCharset] Character set to force on the response
* @return {String} Request body * @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 /* 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 * in FF3.5, synchronous XHR blocks all JavaScript on the thread. See
* http://hacks.mozilla.org/2009/07/synchronous-xhr/. */ * http://hacks.mozilla.org/2009/07/synchronous-xhr/. */
if(this.translate.locationIsProxied) url = this._convertURL(url); if(this.translate.locationIsProxied) url = this._convertURL(url);
if(!requestContentType) requestContentType = null; if(!headers) headers = null;
if(!responseCharset) responseCharset = null; if(!responseCharset) responseCharset = null;
var mainThread = Zotero.mainThread; var mainThread = Zotero.mainThread;
@ -813,7 +857,7 @@ Zotero.Utilities.Translate.prototype.retrieveSource = function(url, body, reques
var listener = function(aXmlhttp) { xmlhttp = aXmlhttp }; var listener = function(aXmlhttp) { xmlhttp = aXmlhttp };
if(body) { if(body) {
Zotero.Utilities.HTTP.doPost(url, body, listener, requestContentType, responseCharset); Zotero.Utilities.HTTP.doPost(url, body, listener, headers, responseCharset);
} else { } else {
Zotero.Utilities.HTTP.doGet(url, listener, responseCharset); 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 * Already documented in Zotero.Utilities.HTTP
* @ignore * @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); url = this._convertURL(url);
var translate = this.translate; var translate = this.translate;
@ -880,7 +924,7 @@ Zotero.Utilities.Translate.prototype.doPost = function(url, body, onDone, reques
} catch(e) { } catch(e) {
translate.error(false, e); translate.error(false, e);
} }
}, requestContentType, responseCharset); }, headers, responseCharset);
} }
/** /**
@ -936,7 +980,6 @@ Zotero.Utilities.HTTP = new function() {
} }
else { else {
Zotero.debug("HTTP GET " + url); Zotero.debug("HTTP GET " + url);
} }
if (this.browserIsOffline()){ if (this.browserIsOffline()){
return false; return false;
@ -981,12 +1024,20 @@ Zotero.Utilities.HTTP = new function() {
* @param {String} url URL to request * @param {String} url URL to request
* @param {String} body Request body * @param {String} body Request body
* @param {Function} onDone Callback to be executed upon request completion * @param {Function} onDone Callback to be executed upon request completion
* @param {String} requestContentType Request content type (usually * @param {String} headers Request HTTP headers
* application/x-www-form-urlencoded)
* @param {String} responseCharset Character set to force on the response * @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 * @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); var bodyStart = body.substr(0, 1024);
// Don't display sync password or session id in console // Don't display sync password or session id in console
bodyStart = bodyStart.replace(/password=[^&]+/, 'password=********'); bodyStart = bodyStart.replace(/password=[^&]+/, 'password=********');
@ -995,7 +1046,7 @@ Zotero.Utilities.HTTP = new function() {
Zotero.debug("HTTP POST " Zotero.debug("HTTP POST "
+ (body.length > 1024 ? + (body.length > 1024 ?
bodyStart + '... (' + body.length + ' chars)' : bodyStart) bodyStart + '... (' + body.length + ' chars)' : bodyStart)
+ " to " + url); + " to " + (disp ? disp.spec : url));
if (this.browserIsOffline()){ if (this.browserIsOffline()){
@ -1025,7 +1076,27 @@ Zotero.Utilities.HTTP = new function() {
xmlhttp.channel.loadGroup = ds.getInterface(Ci.nsILoadGroup); xmlhttp.channel.loadGroup = ds.getInterface(Ci.nsILoadGroup);
xmlhttp.channel.loadFlags |= Ci.nsIChannel.LOAD_DOCUMENT_URI; 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 */ /** @ignore */
xmlhttp.onreadystatechange = function(){ xmlhttp.onreadystatechange = function(){
@ -1042,10 +1113,24 @@ Zotero.Utilities.HTTP = new function() {
* *
* @param {String} url URL to request * @param {String} url URL to request
* @param {Function} onDone Callback to be executed upon request completion * @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 * @return {Boolean} True if the request was sent, or false if the browser is offline
*/ */
this.doHead = function(url, onDone) { this.doHead = function(url, onDone, requestHeaders) {
Zotero.debug("HTTP HEAD "+url); 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()){ if (this.browserIsOffline()){
return false; return false;
} }
@ -1068,10 +1153,20 @@ Zotero.Utilities.HTTP = new function() {
ds.itemType = Ci.nsIDocShellTreeItem.typeContent; ds.itemType = Ci.nsIDocShellTreeItem.typeContent;
var xmlhttp = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]. var xmlhttp = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Ci.nsIXMLHttpRequest); createInstance(Ci.nsIXMLHttpRequest);
// Prevent certificate/authentication dialogs from popping up
xmlhttp.mozBackgroundRequest = true;
xmlhttp.open("HEAD", url, 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.loadGroup = ds.getInterface(Ci.nsILoadGroup);
xmlhttp.channel.loadFlags |= Ci.nsIChannel.LOAD_DOCUMENT_URI; xmlhttp.channel.loadFlags |= Ci.nsIChannel.LOAD_DOCUMENT_URI;
// Don't cache HEAD requests
xmlhttp.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
/** @ignore */ /** @ignore */
xmlhttp.onreadystatechange = function(){ xmlhttp.onreadystatechange = function(){
_stateChange(xmlhttp, onDone); _stateChange(xmlhttp, onDone);

View File

@ -28,7 +28,8 @@ const ZOTERO_CONFIG = {
REPOSITORY_RETRY_INTERVAL: 3600, // 1 hour REPOSITORY_RETRY_INTERVAL: 3600, // 1 hour
BASE_URI: 'http://zotero.org/', BASE_URI: 'http://zotero.org/',
WWW_BASE_URL: 'http://www.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)); 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) { this.getLocalUserKey = function (generate) {
if (_localUserKey) { if (_localUserKey) {
return _localUserKey; return _localUserKey;
@ -1160,8 +1172,9 @@ var Zotero = new function(){
Zotero.Fulltext.purgeUnusedWords(); Zotero.Fulltext.purgeUnusedWords();
Zotero.Items.purge(); Zotero.Items.purge();
if (!skipStoragePurge && Zotero.Sync.Storage.active && Zotero.Utilities.prototype.probability(10)) { if (!skipStoragePurge && Zotero.Utilities.prototype.probability(10)) {
Zotero.Sync.Storage.purgeDeletedStorageFiles(); Zotero.Sync.Storage.purgeDeletedStorageFiles('zfs');
Zotero.Sync.Storage.purgeDeletedStorageFiles('webdav');
} }
} }

View File

@ -1,5 +1,6 @@
extensions.zotero@chnm.gmu.edu.description = The Next-Generation Research Tool extensions.zotero@chnm.gmu.edu.description = The Next-Generation Research Tool
general.success = Success
general.error = Error general.error = Error
general.warning = Warning general.warning = Warning
general.dontShowWarningAgain = Don't show this warning again. general.dontShowWarningAgain = Don't show this warning again.

Binary file not shown.

After

(image error) Size: 666 B

Binary file not shown.

After

(image error) Size: 701 B

View File

@ -205,9 +205,15 @@
font-weight: bold; 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 { #zotero-tb-sync {
@ -220,10 +226,6 @@
list-style-image: url(chrome://zotero/skin/arrow_rotate_animated.png); 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 #zotero-tb-sync #zotero-last-sync-time
{ {
color: gray; color: gray;

View File

@ -64,6 +64,9 @@ var xpcomFiles = [
'style', 'style',
'sync', 'sync',
'storage', 'storage',
'storage/session',
'storage/zfs',
'storage/webdav',
'timeline', 'timeline',
'translate', 'translate',
'uri', 'uri',

View File

@ -37,6 +37,10 @@ pref("extensions.zotero.sortAttachmentsChronologically", false);
pref("extensions.zotero.showTrashWhenEmpty", true); pref("extensions.zotero.showTrashWhenEmpty", true);
pref("extensions.zotero.viewOnDoubleClick", 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.numBackups", 2);
pref("extensions.zotero.backup.interval", 1440); 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.autoSync", true);
pref("extensions.zotero.sync.server.username", ''); pref("extensions.zotero.sync.server.username", '');
pref("extensions.zotero.sync.server.compressData", true); pref("extensions.zotero.sync.server.compressData", true);
pref("extensions.zotero.sync.storage.protocol", "webdavs"); pref("extensions.zotero.sync.storage.enabled", true);
pref("extensions.zotero.sync.storage.enabled", false); pref("extensions.zotero.sync.storage.protocol", "zotero");
pref("extensions.zotero.sync.storage.verified", false); 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.url", '');
pref("extensions.zotero.sync.storage.username", ''); pref("extensions.zotero.sync.storage.username", '');
pref("extensions.zotero.sync.storage.maxDownloads", 4); pref("extensions.zotero.sync.storage.maxDownloads", 4);
pref("extensions.zotero.sync.storage.maxUploads", 4); pref("extensions.zotero.sync.storage.maxUploads", 4);
pref("extensions.zotero.sync.storage.deleteDelayDays", 30); pref("extensions.zotero.sync.storage.deleteDelayDays", 30);
pref("extensions.zotero.sync.storage.groups.enabled", true);
// Proxy // Proxy
pref("extensions.zotero.proxies.transparent", true); pref("extensions.zotero.proxies.transparent", true);

View File

@ -1,4 +1,4 @@
-- 62 -- 63
-- This file creates tables containing user-specific data for new users -- -- 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() -- any changes made here must be mirrored in transition steps in schema.js::_migrateSchema()
@ -70,6 +70,7 @@ CREATE TABLE itemAttachments (
originalPath TEXT, originalPath TEXT,
syncState INT DEFAULT 0, syncState INT DEFAULT 0,
storageModTime INT, storageModTime INT,
storageHash TEXT,
FOREIGN KEY (itemID) REFERENCES items(itemID), FOREIGN KEY (itemID) REFERENCES items(itemID),
FOREIGN KEY (sourceItemID) REFERENCES items(itemID) FOREIGN KEY (sourceItemID) REFERENCES items(itemID)
); );