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) {
Zotero.Sync.Server.sync(function () {
Zotero.Sync.Server.sync({
onSuccess: function () {
Zotero.Sync.Runner.setSyncIcon();
pr.alert(
"Restore Completed",
"The local Zotero database has been successfully restored."
);
},
onError: function (msg) {
pr.alert(
"Restore Failed",
"An error occurred while restoring from the server:\n\n"
+ msg
);
Zotero.Sync.Runner.error(msg);
}
});
}
}, 1000);
@ -898,7 +912,7 @@ var ZoteroPane = new function()
if (!tagSelector.getAttribute('collapsed') ||
tagSelector.getAttribute('collapsed') == 'false') {
Zotero.debug('Updating tag selector with current tags');
if (itemGroup.isEditable()) {
if (itemGroup.editable) {
tagSelector.mode = 'edit';
}
else {
@ -2281,7 +2295,7 @@ var ZoteroPane = new function()
var disabled = Zotero.locked;
if (!disabled && self.collectionsView.selection && self.collectionsView.selection.count) {
var itemGroup = self.collectionsView._getItemAtRow(self.collectionsView.selection.currentIndex);
disabled = !itemGroup.isEditable()
disabled = !itemGroup.editable;
}
for each(var menuitem in menu.firstChild.childNodes) {
menuitem.disabled = disabled;
@ -2837,7 +2851,26 @@ var ZoteroPane = new function()
}
var itemGroup = this.collectionsView._getItemAtRow(row);
return itemGroup.isEditable();
return itemGroup.editable;
}
/**
* Test if the user can edit the currently selected library/collection,
* and display an error if not
*
* @param {Integer} [row]
*
* @return {Boolean} TRUE if user can edit, FALSE if not
*/
this.canEditFiles = function (row) {
// Currently selected row
if (row === undefined) {
row = this.collectionsView.selection.currentIndex;
}
var itemGroup = this.collectionsView._getItemAtRow(row);
return itemGroup.filesEditable;
}
@ -2999,12 +3032,6 @@ var ZoteroPane = new function()
this.setLastSyncStatus = function (tooltip) {
var label = tooltip.firstChild.nextSibling;
var msg = Zotero.Sync.Runner.lastSyncError;
if (msg) {
label.value = 'Last error: ' + msg; // TODO: localize
return;
}
var lastSyncTime = Zotero.Sync.Server.lastLocalSyncTime;
// TODO: localize
msg = 'Last sync: ';

View file

@ -164,8 +164,8 @@
<menuitem hidden="true" label=" Reset Client" oncommand="Zotero.Sync.Server.resetClient()"/>
<menuitem label="Storage Debugging" disabled="true"/>
<menuitem hidden="true" label=" Reset Storage History" oncommand="Zotero.Sync.Storage.resetAllSyncStates()"/>
<menuitem label=" Purge Deleted Storage Files" oncommand="Zotero.Sync.Storage.purgeDeletedStorageFiles(function(results) { Zotero.debug(results); })"/>
<menuitem label=" Purge Orphaned Storage Files" oncommand="Zotero.Sync.Storage.purgeOrphanedStorageFiles(function(results) { Zotero.debug(results); })"/>
<menuitem label=" Purge Deleted Storage Files" oncommand="Zotero.Sync.Storage.purgeDeletedStorageFiles('webdav', function(results) { Zotero.debug(results); })"/>
<menuitem label=" Purge Orphaned Storage Files" oncommand="Zotero.Sync.Storage.purgeOrphanedStorageFiles('webdav', function(results) { Zotero.debug(results); })"/>
<menuseparator id="zotero-tb-actions-separator"/>
<menuitem id="zotero-tb-actions-prefs" label="&zotero.toolbar.preferences.label;"
oncommand="ZoteroPane.openPreferences()"/>
@ -375,6 +375,7 @@
</grid>
</tooltip>
</hbox>
<toolbarbutton id="zotero-tb-sync-warning" hidden="true"/>
<toolbarbutton id="zotero-tb-sync" class="zotero-tb-button" tooltip="_child"
oncommand="Zotero.Sync.Server.canAutoResetClient = true; Zotero.Sync.Runner.sync()">
<tooltip

View file

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

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

View file

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

View file

@ -326,9 +326,14 @@ Zotero.CollectionTreeView.prototype.notify = function(action, type, ids)
break;
case 'group':
if (this._groupRowMap[ids[i]] != null) {
rows.push(this._groupRowMap[ids[i]]);
}
//if (this._groupRowMap[ids[i]] != null) {
// rows.push(this._groupRowMap[ids[i]]);
//}
// For now, just reload if a group is removed, since otherwise
// we'd have to remove collections too
this.reload();
this.rememberSelection(savedSelection);
break;
}
}
@ -635,7 +640,7 @@ Zotero.CollectionTreeView.prototype.isSelectable = function (row, col) {
Zotero.CollectionTreeView.prototype.__defineGetter__('editable', function () {
return this._getItemAtRow(this.selection.currentIndex).isEditable();
return this._getItemAtRow(this.selection.currentIndex).editable;
});
@ -1029,12 +1034,11 @@ Zotero.CollectionTreeView.prototype.canDrop = function(row, orient, dragData)
{
var itemGroup = this._getItemAtRow(row); //the collection we are dragging over
if (!itemGroup.isEditable()) {
if (!itemGroup.editable) {
return false;
}
if (dataType == 'zotero/item') {
if(itemGroup.isBucket()) {
return true;
}
@ -1048,11 +1052,14 @@ Zotero.CollectionTreeView.prototype.canDrop = function(row, orient, dragData)
return false
}
// TODO: for now, only allow regular items to be dragged to groups
if (itemGroup.isWithinGroup() && itemGroup.ref.libraryID != item.libraryID
&& !item.isRegularItem()) {
if (itemGroup.isWithinGroup() && item.isAttachment()) {
// Linked files can't be added to groups
if (item.attachmentLinkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
return false;
}
skip = false;
continue;
}
// TODO: for now, skip items that are already linked
if (itemGroup.isWithinGroup() && itemGroup.ref.libraryID != item.libraryID) {
@ -1070,7 +1077,7 @@ Zotero.CollectionTreeView.prototype.canDrop = function(row, orient, dragData)
continue;
}
// Allow drag of group items to library
// Allow drag of group items to personal library
if (item.libraryID && (itemGroup.isLibrary()
|| itemGroup.isCollection() && !itemGroup.isWithinGroup())) {
// TODO: for now, skip items that are already linked
@ -1106,10 +1113,17 @@ Zotero.CollectionTreeView.prototype.canDrop = function(row, orient, dragData)
if (itemGroup.isSearch()) {
return false;
}
if (dataType == 'application/x-moz-file') {
// Don't allow folder drag
if (dataType == 'application/x-moz-file' && data[0].isDirectory()) {
if (data[0].isDirectory()) {
return false;
}
// Don't allow drop if no permissions
if (!itemGroup.filesEditable) {
return false;
}
}
return true;
}
else if (dataType == 'zotero/collection') {
@ -1263,12 +1277,19 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient)
*/
}
// Standalone attachment
if (item.isAttachment()) {
var id = Zotero.Attachments.copyAttachmentToLibrary(item, targetLibraryID);
newIDs.push(id);
continue;
}
// Create new unsaved clone item in target library
var newItem = new Zotero.Item(item.itemTypeID);
newItem.libraryID = targetLibraryID;
// DEBUG: save here because clone() doesn't currently work on unsaved tagged items
var id = newItem.save();
var newItem = Zotero.Items.get(id);
newItem = Zotero.Items.get(id);
item.clone(false, newItem);
newItem.save();
//var id = newItem.save();
@ -1277,6 +1298,62 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient)
// Record link
item.addLinkedItem(newItem);
newIDs.push(id);
if (item.isNote()) {
continue;
}
// For regular items, add child items if prefs and permissions allow
// Child notes
if (Zotero.Prefs.get('groups.copyChildNotes')) {
var noteIDs = item.getNotes();
var notes = Zotero.Items.get(noteIDs);
for each(var note in notes) {
var newNote = new Zotero.Item('note');
newNote.libraryID = targetLibraryID;
// DEBUG: save here because clone() doesn't currently work on unsaved tagged items
var id = newNote.save();
newNote = Zotero.Items.get(id);
note.clone(false, newNote);
newNote.setSource(newItem.id);
newNote.save();
note.addLinkedItem(newNote);
}
}
// Child attachments
var copyChildLinks = Zotero.Prefs.get('groups.copyChildLinks');
var copyChildFileAttachments = Zotero.Prefs.get('groups.copyChildFileAttachments');
if (copyChildLinks || copyChildFileAttachments) {
var attachmentIDs = item.getAttachments();
var attachments = Zotero.Items.get(attachmentIDs);
for each(var attachment in attachments) {
var linkMode = attachment.attachmentLinkMode;
// Skip linked files
if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_FILE) {
continue;
}
// Skip imported files if we don't have pref and permissions
if (linkMode == Zotero.Attachments.LINK_MODE_LINKED_URL) {
if (!copyChildLinks) {
Zotero.debug("Skipping child link attachment on drag");
continue;
}
}
else {
if (!copyChildFileAttachments || !itemGroup.filesEditable) {
Zotero.debug("Skipping child file attachment on drag");
continue;
}
}
var id = Zotero.Attachments.copyAttachmentToLibrary(attachment, targetLibraryID, newItem.id);
}
}
}
if (toReconcile.length) {
@ -1342,12 +1419,11 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient)
return;
}
else if (dataType == 'text/x-moz-url' || dataType == 'application/x-moz-file') {
// FIXME: temporarily disable dragging in of files
if (dataType == 'application/x-moz-file' && itemGroup.isWithinGroup()) {
var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
.getService(Components.interfaces.nsIPromptService);
ps.alert(null, "", "Files cannot currently be added to group libraries.");
return;
if (itemGroup.isWithinGroup()) {
var targetLibraryID = itemGroup.ref.libraryID;
}
else {
var targetLibraryID = null;
}
if (itemGroup.isCollection()) {
@ -1398,7 +1474,7 @@ Zotero.CollectionTreeView.prototype.drop = function(row, orient)
try {
Zotero.DB.beginTransaction();
var itemID = Zotero.Attachments.importFromFile(file, false);
var itemID = Zotero.Attachments.importFromFile(file, false, targetLibraryID);
if (parentCollectionID) {
var col = Zotero.Collections.get(parentCollectionID);
if (col) {
@ -1545,11 +1621,10 @@ Zotero.ItemGroup.prototype.isWithinGroup = function () {
return this.ref && !!this.ref.libraryID;
}
Zotero.ItemGroup.prototype.isEditable = function () {
Zotero.ItemGroup.prototype.__defineGetter__('editable', function () {
if (this.isTrash() || this.isShare()) {
return false;
}
if (!this.isWithinGroup()) {
return true;
}
@ -1564,12 +1639,33 @@ Zotero.ItemGroup.prototype.isEditable = function () {
var group = Zotero.Groups.get(groupID);
return group.editable;
}
else {
throw ("Unknown library type '" + type + "' in Zotero.ItemGroup.isEditable()");
}
}
throw ("Unknown library type '" + type + "' in Zotero.ItemGroup.editable");
}
return false;
});
Zotero.ItemGroup.prototype.__defineGetter__('filesEditable', function () {
if (this.isTrash() || this.isShare()) {
return false;
}
if (!this.isWithinGroup()) {
return true;
}
var libraryID = this.ref.libraryID;
if (this.isGroup()) {
return this.ref.filesEditable;
}
if (this.isCollection()) {
var type = Zotero.Libraries.getType(libraryID);
if (type == 'group') {
var groupID = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
var group = Zotero.Groups.get(groupID);
return group.filesEditable;
}
throw ("Unknown library type '" + type + "' in Zotero.ItemGroup.filesEditable");
}
return false;
});
Zotero.ItemGroup.prototype.getName = function()
{

View file

@ -327,7 +327,7 @@ Zotero.DataObjects = function (object, objectPlural, id, table) {
this.editCheck = function (obj) {
if (!Zotero.Sync.Server.syncInProgress && !this.isEditable(obj)) {
if (!Zotero.Sync.Server.syncInProgress && !Zotero.Sync.Storage.syncInProgress && !this.isEditable(obj)) {
throw ("Cannot edit " + this._ZDO_object + " in read-only Zotero library");
}
}

View file

@ -286,6 +286,10 @@ Zotero.Group.prototype.save = function () {
* Deletes group and all descendant objects
**/
Zotero.Group.prototype.erase = function() {
// Don't send notifications for items and other groups objects that are deleted,
// since we're really only removing the group from the client
var notifierDisabled = Zotero.Notifier.disable();
Zotero.DB.beginTransaction();
var sql, ids, obj;
@ -331,6 +335,9 @@ Zotero.Group.prototype.erase = function() {
sql = "DELETE FROM syncDeleteLog WHERE libraryID=?";
Zotero.DB.query(sql, this.libraryID);
var prefix = "groups/" + this.id;
Zotero.Relations.eraseByPathPrefix(prefix);
// Delete group
sql = "DELETE FROM groups WHERE groupID=?";
ids = Zotero.DB.query(sql, this.id)
@ -342,6 +349,10 @@ Zotero.Group.prototype.erase = function() {
Zotero.DB.commitTransaction();
if (notifierDisabled) {
Zotero.Notifier.enable();
}
Zotero.Notifier.trigger('delete', 'group', this.id, notifierData);
}

View file

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

View file

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

View file

@ -6,6 +6,7 @@ Zotero.Relations = new function () {
owl: 'http://www.w3.org/2002/07/owl#'
};
var _prefix = "http://zotero.org/";
this.get = function (id) {
if (typeof id != 'number') {
@ -76,6 +77,37 @@ Zotero.Relations = new function () {
}
this.updateUser = function (fromUserID, fromLibraryID, toUserID, toLibraryID) {
if (!fromUserID) {
throw ("Invalid source userID " + fromUserID + " in Zotero.Relations.updateUserID");
}
if (!fromLibraryID) {
throw ("Invalid source libraryID " + fromLibraryID + " in Zotero.Relations.updateUserID");
}
if (!toUserID) {
throw ("Invalid target userID " + toUserID + " in Zotero.Relations.updateUserID");
}
if (!toLibraryID) {
throw ("Invalid target libraryID " + toLibraryID + " in Zotero.Relations.updateUserID");
}
Zotero.DB.beginTransaction();
var sql = "UPDATE relations SET libraryID=? WHERE libraryID=?";
Zotero.DB.query(sql, [fromLibraryID, toLibraryID]);
sql = "UPDATE relations SET "
+ "subject=REPLACE(subject, 'zotero.org/users/" + fromUserID + "', "
+ "'zotero.org/users/" + toUserID + "'), "
+ "object=REPLACE(object, 'zotero.org/users/" + fromUserID + "', "
+ "'zotero.org/users/" + toUserID + "') "
+ "WHERE predicate='owl:sameAs'";
Zotero.DB.query(sql);
Zotero.DB.commitTransaction();
}
this.add = function (libraryID, subject, predicate, object) {
predicate = _getPrefixAndValue(predicate).join(':');
@ -109,6 +141,13 @@ Zotero.Relations = new function () {
}
this.eraseByPathPrefix = function (prefix) {
prefix = _prefix + prefix + '%';
sql = "DELETE FROM relations WHERE subject LIKE ? OR object LIKE ?";
Zotero.DB.query(sql, [prefix, prefix]);
}
this.xmlToRelation = function (xml) {
var relation = new Zotero.Relation;
var libraryID = xml.@libraryID.toString();

View file

@ -421,7 +421,9 @@ Zotero.DBConnection.prototype.getLastErrorString = function () {
Zotero.DBConnection.prototype.beginTransaction = function () {
// TODO: limit to Zotero.DB, not all Zotero.DBConnections?
if (Zotero.waiting) {
throw ("Cannot access database layer during active Zotero.wait()");
var msg = "Cannot access database layer during active Zotero.wait()";
Zotero.debug(msg, 2);
throw (msg);
}
var db = this._getDBConnection();

View file

@ -1,6 +1,7 @@
Zotero.Error = function (message, error) {
Zotero.Error = function (message, error, data) {
this.name = "ZOTERO_ERROR";
this.message = message;
this.data = data;
if (parseInt(error) == error) {
this.error = error;
}
@ -14,6 +15,11 @@ Zotero.Error.ERROR_MISSING_OBJECT = 1;
Zotero.Error.ERROR_FULL_SYNC_REQUIRED = 2;
Zotero.Error.ERROR_SYNC_USERNAME_NOT_SET = 3;
Zotero.Error.ERROR_INVALID_SYNC_LOGIN = 4;
Zotero.Error.ERROR_ZFS_OVER_QUOTA = 5;
Zotero.Error.ERROR_ZFS_UPLOAD_QUEUE_LIMIT = 6;
Zotero.Error.ERROR_ZFS_FILE_EDITING_DENIED = 7;
//Zotero.Error.ERROR_SYNC_EMPTY_RESPONSE_FROM_SERVER = 6;
//Zotero.Error.ERROR_SYNC_INVALID_RESPONSE_FROM_SERVER = 7;
Zotero.Error.prototype.toString = function () {
return this.message;

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
*/
@ -199,6 +180,19 @@ Zotero.File = new function(){
}
/**
* Copies all files from dir into newDir
*/
this.copyDirectory = function (dir, newDir) {
var otherFiles = dir.directoryEntries;
while (otherFiles.hasMoreElements()) {
var file = otherFiles.getNext();
file.QueryInterface(Components.interfaces.nsIFile);
file.copyTo(newDir, null);
}
}
this.createDirectoryIfMissing = function (dir) {
if (!dir.exists() || !dir.isDirectory()) {
if (dir.exists() && !dir.isDirectory()) {

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

View file

@ -2451,6 +2451,21 @@ Zotero.Schema = new function(){
Zotero.DB.query("CREATE INDEX IF NOT EXISTS itemData_fieldID ON itemData(fieldID)");
}
if (i==63) {
Zotero.DB.query("ALTER TABLE itemAttachments ADD COLUMN storageHash TEXT");
var protocol = Zotero.Prefs.get('sync.storage.protocol');
if (protocol == 'webdav') {
Zotero.Prefs.set('sync.storage.scheme', 'http');
}
else {
Zotero.Prefs.set('sync.storage.protocol', 'webdav');
Zotero.Prefs.set('sync.storage.scheme', 'https');
}
Zotero.DB.query("UPDATE version SET schema='storage_webdav' WHERE schema='storage'");
}
Zotero.wait();
}

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

@ -443,30 +443,28 @@ Zotero.Sync.EventListener = new function () {
Zotero.Sync.Runner = new function () {
this.__defineGetter__("lastSyncError", function () {
return _lastSyncError;
});
this.__defineGetter__("background", function () {
return _background;
});
var _lastSyncError;
var _autoSyncTimer;
var _queue;
var _running;
var _background;
var _warning = null;
this.init = function () {
this.EventListener.init();
this.IdleListener.init();
}
this.sync = function (background) {
_warning = null;
if (Zotero.Utilities.HTTP.browserIsOffline()){
_lastSyncError = "Browser is offline"; // TODO: localize
this.clearSyncTimeout(); // DEBUG: necessary?
this.setSyncIcon('error');
this.setSyncIcon('error', "Browser is offline");
return false;
}
@ -478,40 +476,135 @@ Zotero.Sync.Runner = new function () {
Zotero.purgeDataObjects(true);
_background = !!background;
_queue = [
Zotero.Sync.Server.sync,
Zotero.Sync.Storage.sync,
Zotero.Sync.Server.sync,
Zotero.Sync.Storage.sync
];
_running = true;
_lastSyncError = '';
this.setSyncIcon('animate');
this.next();
var storageSync = function () {
var syncNeeded = false;
Zotero.Sync.Storage.sync(
'webdav',
{
// WebDAV success
onSuccess: function () {
syncNeeded = true;
Zotero.Sync.Storage.sync(
'zfs',
{
// ZFS success
onSuccess: function () {
Zotero.Sync.Server.sync(
Zotero.Sync.Runner.stop,
Zotero.Sync.Runner.stop,
Zotero.Sync.Runner.error
)
},
// ZFS skip
onSkip: function () {
if (syncNeeded) {
Zotero.Sync.Server.sync(
Zotero.Sync.Runner.stop,
Zotero.Sync.Runner.stop,
Zotero.Sync.Runner.error
)
}
},
// ZFS cancel
onStop: Zotero.Sync.Runner.stop,
// ZFS failure
onError: Zotero.Sync.Runner.error,
onWarning: Zotero.Sync.Runner.warning
}
)
},
// WebDAV skip
onSkip: function () {
Zotero.Sync.Storage.sync(
'zfs',
{
// ZFS success
onSuccess: function () {
Zotero.Sync.Server.sync({
onSuccess: Zotero.Sync.Runner.stop,
onSkip: Zotero.Sync.Runner.stop,
onStop: Zotero.Sync.Runner.stop,
onError: Zotero.Sync.Runner.error
})
},
// ZFS skip
onSkip: Zotero.Sync.Runner.stop,
// ZFS cancel
onStop: Zotero.Sync.Runner.stop,
// ZFS failure
onError: Zotero.Sync.Runner.error,
onWarning: Zotero.Sync.Runner.warning
}
)
},
// WebDAV cancel
onStop: Zotero.Sync.Runner.stop,
// WebDAV failure
onError: Zotero.Sync.Runner.error
}
)
}
Zotero.Sync.Server.sync({
// Sync 1 success
onSuccess: storageSync,
// Sync 1 skip
onSkip: storageSync,
// Sync 1 stop
onStop: Zotero.Sync.Runner.stop,
// Sync 1 error
onError: Zotero.Sync.Runner.error
});
}
this.next = function () {
if (!_queue.length) {
this.setSyncIcon();
this.stop = function () {
if (_warning) {
Zotero.Sync.Runner.setSyncIcon('warning', _warning);
_warning = null;
}
else {
Zotero.Sync.Runner.setSyncIcon();
}
_running = false;
return;
}
var func = _queue.shift();
func();
}
this.setError = function (msg) {
this.setSyncIcon('error');
_lastSyncError = msg;
/**
* Log a warning, but don't throw an error
*/
this.warning = function (e) {
Components.utils.reportError(e);
_warning = e;
}
this.reset = function () {
_queue = [];
this.error = function (e) {
Zotero.Sync.Runner.setSyncIcon('error', e);
_running = false;
throw (e);
}
@ -593,12 +686,13 @@ Zotero.Sync.Runner = new function () {
}
this.setSyncIcon = function (status) {
this.setSyncIcon = function (status, e) {
status = status ? status : '';
switch (status) {
case '':
case 'animate':
case 'warning':
case 'error':
break;
@ -610,18 +704,114 @@ Zotero.Sync.Runner = new function () {
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var win = wm.getMostRecentWindow('navigator:browser');
var warning = win.document.getElementById('zotero-tb-sync-warning');
var icon = win.document.getElementById('zotero-tb-sync');
icon.setAttribute('status', status);
switch (status) {
case 'animate':
icon.setAttribute('disabled', true);
break;
default:
icon.setAttribute('disabled', false);
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);
}
}
if (!message) {
message = e.message ? e.message : e;
}
}
if (status == 'warning' || status == 'error') {
icon.setAttribute('status', '');
warning.hidden = false;
warning.setAttribute('mode', status);
warning.tooltipText = "A sync error occurred. Click to view details.";
warning.onclick = function () {
var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
.getService(Components.interfaces.nsIWindowMediator);
var win = wm.getMostRecentWindow("navigator:browser");
var pr = Components.classes["@mozilla.org/network/default-prompt;1"]
.createInstance(Components.interfaces.nsIPrompt);
// Warning
if (status == 'warning') {
var title = Zotero.getString('general.warning');
// If secondary button not specified, just use an alert
if (!buttonText) {
prompt.alert(title, message);
return;
}
var buttonFlags = pr.BUTTON_POS_0 * pr.BUTTON_TITLE_OK
+ pr.BUTTON_POS_1 * pr.BUTTON_TITLE_IS_STRING;
Zotero.debug(buttonFlags);
var index = pr.confirmEx(
title,
message,
buttonFlags,
"",
buttonText,
"", null, {}
);
if (index == 1) {
setTimeout(buttonCallback, 1);
}
}
// Error
else if (status == 'error') {
var errorsLogged = Zotero.getErrors().length > 0;
// Probably not necessary, but let's be sure
if (!errorsLogged) {
Components.utils.reportError(message);
}
var buttonFlags = pr.BUTTON_POS_0 * pr.BUTTON_TITLE_OK
+ pr.BUTTON_POS_1 * pr.BUTTON_TITLE_IS_STRING;
var index = pr.confirmEx(
Zotero.getString('general.error'),
message,
buttonFlags,
"",
// TODO: localize
"Report Error...",
"", null, {}
);
if (index == 1) {
win.setTimeout(function () {
win.ZoteroPane.reportErrors();
}, 1);
}
}
}
}
else {
icon.setAttribute('status', status);
warning.hidden = true;
warning.onclick = null;
}
// Disable button while spinning
icon.disabled = status == 'animate';
}
}
@ -852,7 +1042,22 @@ Zotero.Sync.Server = new function () {
var _throttleTimeout;
var _canAutoResetClient = true;
function login(callback, callbackCallback) {
var _callbacks = {
onSuccess: function () {
Zotero.Sync.Runner.setSyncIcon();
},
onSkip: function () {
Zotero.Sync.Runner.setSyncIcon();
},
onStop: function () {
Zotero.Sync.Runner.setSyncIcon();
},
onError: function (msg) {
Zotero.Sync.Runner.error(msg);
}
};
function login(callback) {
var url = _serverURL + "login";
var username = Zotero.Sync.Server.username;
@ -906,18 +1111,26 @@ Zotero.Sync.Server = new function () {
//Zotero.debug('Got session ID ' + _sessionID + ' from server');
if (callback) {
callback(callbackCallback);
callback();
}
});
}
function sync(callback) {
function sync(callbacks) {
for (var func in callbacks) {
_callbacks[func] = callbacks[func];
}
var self = this;
Zotero.Sync.Runner.setSyncIcon('animate');
if (!_sessionID) {
Zotero.debug("Session ID not available -- logging in");
Zotero.Sync.Server.login(Zotero.Sync.Server.sync, callback);
Zotero.Sync.Server.login(function () {
Zotero.Sync.Server.sync(_callbacks);
});
return;
}
@ -955,7 +1168,9 @@ Zotero.Sync.Server = new function () {
Zotero.debug("Invalid session ID -- logging in");
_sessionID = false;
_syncInProgress = false;
Zotero.Sync.Server.login(Zotero.Sync.Server.sync, callback);
Zotero.Sync.Server.login(function () {
Zotero.Sync.Server.sync(_callbacks);
});
return;
}
@ -976,32 +1191,36 @@ Zotero.Sync.Server = new function () {
// Strip XML declaration and convert to E4X
var xml = new XML(xmlhttp.responseText.replace(/<\?xml.*\?>/, ''));
Zotero.DB.beginTransaction();
try {
// If no earliest date is provided by the server, the server
// account is empty
var earliestRemoteDate = parseInt(xml.@earliest) ?
new Date((xml.@earliest + 43200) * 1000) : false;
var noServerData = !!earliestRemoteDate;
// Check to see if we're syncing with a different user
var userID = parseInt(xml.@userID);
var libraryID = parseInt(xml.@defaultLibraryID);
if (!_checkSyncUser(userID, libraryID)) {
var c = _checkSyncUser(userID, libraryID, noServerData);
if (c == 0) {
// Groups were deleted, so restart sync
Zotero.debug("Restarting sync");
_syncInProgress = false;
Zotero.Sync.Server.sync(_callbacks);
return;
}
else if (c == -1) {
Zotero.debug("Sync cancelled");
Zotero.DB.rollbackTransaction();
Zotero.Sync.Server.unlock(function () {
if (callback) {
Zotero.Sync.Runner.setSyncIcon();
callback();
}
else {
Zotero.Sync.Runner.reset();
Zotero.Sync.Runner.next();
}
_callbacks.onStop();
});
_syncInProgress = false;
return;
}
Zotero.UnresponsiveScriptIndicator.disable();
Zotero.DB.beginTransaction();
var earliestRemoteDate = parseInt(xml.@earliest) ?
new Date((xml.@earliest + 43200) * 1000) : false;
Zotero.UnresponsiveScriptIndicator.disable();
var lastLocalSyncTime = Zotero.Sync.Server.lastLocalSyncTime;
var lastLocalSyncDate = lastLocalSyncTime ?
@ -1039,14 +1258,7 @@ Zotero.Sync.Server = new function () {
Zotero.debug("Sync cancelled");
Zotero.DB.rollbackTransaction();
Zotero.Sync.Server.unlock(function () {
if (callback) {
Zotero.Sync.Runner.setSyncIcon();
callback();
}
else {
Zotero.Sync.Runner.reset();
Zotero.Sync.Runner.next();
}
_callbacks.onSkip();
});
Zotero.reloadDataObjects();
Zotero.Sync.EventListener.resetIgnored();
@ -1067,13 +1279,7 @@ Zotero.Sync.Server = new function () {
Zotero.DB.commitTransaction();
Zotero.Sync.Server.unlock(function () {
_syncInProgress = false;
if (callback) {
Zotero.Sync.Runner.setSyncIcon();
callback();
}
else {
Zotero.Sync.Runner.next();
}
_callbacks.onSuccess();
});
return;
}
@ -1114,13 +1320,7 @@ Zotero.Sync.Server = new function () {
Zotero.DB.commitTransaction();
Zotero.Sync.Server.unlock(function () {
_syncInProgress = false;
if (callback) {
Zotero.Sync.Runner.setSyncIcon();
callback();
}
else {
Zotero.Sync.Runner.next();
}
_callbacks.onSuccess();
});
}
@ -1203,7 +1403,7 @@ Zotero.Sync.Server = new function () {
}
function lock(callback, callbackCallback) {
function lock(callback) {
Zotero.debug("Getting session lock");
if (!_sessionID) {
@ -1234,11 +1434,7 @@ Zotero.Sync.Server = new function () {
if (response.firstChild.tagName == 'error') {
if (_checkServerSessionLock(response.firstChild)) {
Zotero.Sync.Server.lock(function () {
if (callback) {
callback(callbackCallback);
}
});
Zotero.Sync.Server.lock(callback ? function () { callback(); } : null);
return;
}
@ -1252,7 +1448,7 @@ Zotero.Sync.Server = new function () {
_sessionLock = true;
if (callback) {
callback(callbackCallback);
callback();
}
});
}
@ -1300,7 +1496,9 @@ Zotero.Sync.Server = new function () {
function clear(callback) {
if (!_sessionID) {
Zotero.debug("Session ID not available -- logging in");
Zotero.Sync.Server.login(Zotero.Sync.Server.clear, callback);
Zotero.Sync.Server.login(function () {
Zotero.Sync.Server.clear();
});
return;
}
@ -1312,7 +1510,9 @@ Zotero.Sync.Server = new function () {
if (_invalidSession(xmlhttp)) {
Zotero.debug("Invalid session ID -- logging in");
_sessionID = false;
Zotero.Sync.Server.login(Zotero.Sync.Server.clear, callback);
Zotero.Sync.Server.login(function () {
Zotero.Sync.Server.clear(callback);
});
return;
}
@ -1343,7 +1543,9 @@ Zotero.Sync.Server = new function () {
function resetServer(callback) {
if (!_sessionID) {
Zotero.debug("Session ID not available -- logging in");
Zotero.Sync.Server.login(Zotero.Sync.Server.resetServer, callback);
Zotero.Sync.Server.login(function () {
Zotero.Sync.Server.resetServer(callback);
});
return;
}
@ -1355,7 +1557,9 @@ Zotero.Sync.Server = new function () {
if (_invalidSession(xmlhttp)) {
Zotero.debug("Invalid session ID -- logging in");
_sessionID = false;
Zotero.Sync.Server.login(Zotero.Sync.Server.reset, callback);
Zotero.Sync.Server.login(function () {
Zotero.Sync.Server.reset();
});
return;
}
@ -1391,11 +1595,12 @@ Zotero.Sync.Server = new function () {
+ "('lastlocalsync', 'lastremotesync', 'syncdeletelog')";
Zotero.DB.query(sql);
var sql = "DELETE FROM version WHERE schema IN "
+ "('lastlocalsync', 'lastremotesync', 'syncdeletelog')";
Zotero.DB.query(sql);
Zotero.DB.query("DELETE FROM syncDeleteLog");
Zotero.DB.query("DELETE FROM storageDeleteLog");
sql = "DELETE FROM settings WHERE setting='account' AND "
+ "key IN ('userID', 'username')";
Zotero.DB.query(sql);
sql = "INSERT INTO version VALUES ('syncdeletelog', ?)";
Zotero.DB.query(sql, Zotero.Date.getUnixTimestamp());
@ -1545,6 +1750,41 @@ Zotero.Sync.Server = new function () {
}, 1);
break;
case 'LIBRARY_ACCESS_DENIED':
var background = Zotero.Sync.Runner.background;
setTimeout(function () {
var libraryID = parseInt(firstChild.getAttribute('libraryID'));
var group = Zotero.Groups.getByLibraryID(libraryID);
var pr = Components.classes["@mozilla.org/network/default-prompt;1"]
.createInstance(Components.interfaces.nsIPrompt);
var buttonFlags = (pr.BUTTON_POS_0) * (pr.BUTTON_TITLE_IS_STRING)
+ (pr.BUTTON_POS_1) * (pr.BUTTON_TITLE_CANCEL)
+ pr.BUTTON_DELAY_ENABLE;
var index = pr.confirmEx(
Zotero.getString('general.warning'),
// TODO: localize
"You no longer have write access to the Zotero group '" + group.name + "', "
+ "and changes you've made cannot be synced to the server.\n\n"
+ "If you continue, your copy of the group will be reset to its state "
+ "on the server, and your local modifications will be lost.\n\n"
+ "If you would like a chance to copy your changes elsewhere or to request "
+ "write access from a group administrator, cancel the sync now.",
buttonFlags,
"Reset Group and Sync",
null, null, null, {}
);
if (index == 0) {
group.erase();
Zotero.Sync.Server.resetClient();
Zotero.Sync.Storage.resetAllSyncStates();
Zotero.Sync.Runner.sync();
return;
}
}, 1);
break;
case 'TAG_TOO_LONG':
if (!Zotero.Sync.Runner.background) {
var tag = xmlhttp.responseXML.firstChild.getElementsByTagName('tag');
@ -1626,9 +1866,19 @@ Zotero.Sync.Server = new function () {
/**
* Make sure we're syncing with the same account we used last time
*
* @return TRUE if sync should continue, FALSE if cancelled
* @param {Integer} userID New userID
* @param {Integer} libraryID New libraryID
* @param {Integer} noServerData The server account is empty this is
* the account after a server clear
* @return 1 if sync should continue, 0 if sync should restart, -1 if sync should cancel
*/
function _checkSyncUser(userID, libraryID) {
function _checkSyncUser(userID, libraryID, noServerData) {
if (Zotero.DB.transactionInProgress()) {
throw ("Transaction in progress in Zotero.Sync.Server._checkSyncUser");
}
Zotero.DB.beginTransaction();
var sql = "SELECT value FROM settings WHERE "
+ "setting='account' AND key='username'";
var lastUsername = Zotero.DB.valueQuery(sql);
@ -1636,7 +1886,11 @@ Zotero.Sync.Server = new function () {
var lastUserID = Zotero.userID;
var lastLibraryID = Zotero.libraryID;
var restartSync = false;
if (lastUserID && lastUserID != userID) {
var groups = Zotero.Groups.getAll();
var pr = Components.classes["@mozilla.org/network/default-prompt;1"]
.createInstance(Components.interfaces.nsIPrompt);
var buttonFlags = (pr.BUTTON_POS_0) * (pr.BUTTON_TITLE_IS_STRING)
@ -1644,20 +1898,52 @@ Zotero.Sync.Server = new function () {
+ (pr.BUTTON_POS_2) * (pr.BUTTON_TITLE_IS_STRING)
+ pr.BUTTON_POS_1_DEFAULT
+ pr.BUTTON_DELAY_ENABLE;
var msg = "This Zotero database was last synced with a different "
+ "zotero.org account ('" + lastUsername + "') from the "
+ "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.";
}
msg += "\n\n"
+ "To avoid combining or losing data, revert to the '"
+ lastUsername + "' account or use the Reset options "
+ "in the Sync pane of the Zotero preferences.";
var syncButtonText = "Sync";
}
else if (groups.length) {
msg += "If you continue, local groups, including any with changed items, "
+ "will be removed and replaced with groups linked to the '"
+ username + "' account."
+ "\n\n"
+ "To avoid losing local changes to groups, be sure you "
+ "have synced with the '" + lastUsername + "' account before "
+ "syncing with the '" + username + "' account.";
var syncButtonText = "Remove Groups and Sync";
}
// If there are no local groups and the server is empty,
// don't bother prompting
else {
var noPrompt = true;
}
if (!noPrompt) {
var index = pr.confirmEx(
Zotero.getString('general.warning'),
// 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.",
msg,
buttonFlags,
"Sync",
syncButtonText,
null,
"Open Sync Preferences",
null, {}
@ -1674,24 +1960,44 @@ Zotero.Sync.Server = new function () {
lastWin.ZoteroPane.openPreferences('zotero-prefpane-sync');
}
return false;
return -1;
}
// Delete all local groups
for each(var group in groups) {
group.erase();
}
restartSync = true;
}
}
if (lastUserID != userID || lastLibraryID != libraryID) {
Zotero.userID = userID;
Zotero.libraryID = libraryID;
// Update userID in relations
if (lastUserID && lastLibraryID) {
Zotero.Relations.updateUser(lastUserID, lastLibraryID, userID, libraryID);
}
Zotero.Sync.Server.resetClient();
Zotero.Sync.Storage.resetAllSyncStates();
}
if (lastUsername != username) {
var sql = "REPLACE INTO settings VALUES ('account', 'username', ?)";
Zotero.DB.query(sql, username);
Zotero.username = username;
}
return true;
Zotero.DB.commitTransaction();
return restartSync ? 0 : 1;
}
function _invalidSession(xmlhttp) {
if (xmlhttp.responseXML.childNodes[0].firstChild.tagName != 'error') {
return false;
@ -1783,8 +2089,7 @@ Zotero.Sync.Server = new function () {
Zotero.Sync.Server.unlock()
}
Zotero.Sync.Runner.setError(e.message ? e.message : e);
Zotero.Sync.Runner.reset();
_callbacks.onError(e);
throw (e);
}
@ -1970,6 +2275,7 @@ Zotero.Sync.Server.Data = new function() {
var itemStorageModTimes = {};
var childItemStore = [];
// Remotely changed groups
if (xml.groups.length()) {
Zotero.debug("Processing remotely changed groups");
for each(var xmlNode in xml.groups.group) {
@ -1978,6 +2284,7 @@ Zotero.Sync.Server.Data = new function() {
}
}
// Remotely deleted groups
if (xml.deleted.groups.toString()) {
Zotero.debug("Processing remotely deleted groups");
var groupIDs = xml.deleted.groups.toString().split(' ');
@ -1988,21 +2295,14 @@ Zotero.Sync.Server.Data = new function() {
if (!group) {
continue;
}
// TODO: prompt to save
Zotero.Notifier.disable();
// TODO: prompt to save data to local library?
// TODO: figure out a better way to do this
var notifierData = {};
notifierData[groupID] = group.serialize();
group.erase();
Zotero.Notifier.enable();
Zotero.Notifier.trigger('delete', 'group', groupID, notifierData);
}
}
// Other objects
for each(var syncObject in Zotero.Sync.syncObjects) {
var Type = syncObject.singular; // 'Item'
var Types = syncObject.plural; // 'Items'
@ -2271,11 +2571,7 @@ Zotero.Sync.Server.Data = new function() {
syncSession.removeFromDeleted(creator.ref);
}
}
else if (obj.isAttachment() &&
(obj.attachmentLinkMode ==
Zotero.Attachments.LINK_MODE_IMPORTED_FILE ||
obj.attachmentLinkMode ==
Zotero.Attachments.LINK_MODE_IMPORTED_URL)) {
else if (obj.isImportedAttachment()) {
// Mark new attachments for download
if (isNewObject) {
obj.attachmentSyncState =
@ -2285,7 +2581,8 @@ Zotero.Sync.Server.Data = new function() {
else {
var mtime = xmlNode.@storageModTime.toString();
if (mtime) {
itemStorageModTimes[obj.key] = parseInt(mtime);
var lk = Zotero.Items.getLibraryKeyHash(obj)
itemStorageModTimes[lk] = parseInt(mtime);
}
}
}
@ -2504,15 +2801,16 @@ Zotero.Sync.Server.Data = new function() {
}
}
// Check mod times of updated items against stored time to see
// Check mod times and hashes of updated items against stored values to see
// if they've been updated elsewhere and mark for download if so
if (type == 'item') {
var ids = [];
var modTimes = {};
for (var key in itemStorageModTimes) {
var item = Zotero.Items.getByLibraryAndKey(null, key);
for (var libraryKeyHash in itemStorageModTimes) {
var lk = Zotero.Items.parseLibraryKeyHash(libraryKeyHash);
var item = Zotero.Items.getByLibraryAndKey(lk.libraryID, lk.key);
ids.push(item.id);
modTimes[item.id] = itemStorageModTimes[key];
modTimes[item.id] = itemStorageModTimes[libraryKeyHash];
}
if (ids.length > 0) {
Zotero.Sync.Storage.checkForUpdatedFiles(ids, modTimes);
@ -3003,15 +3301,23 @@ Zotero.Sync.Server.Data = new function() {
xml.@charset = charset;
}
// Include storage sync time and paths for non-links
if (item.attachment.linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) {
// Include paths for non-links
var path = <path>{item.attachment.path}</path>;
xml.path += path;
// Include storage sync time and hash for imported files
if (item.attachment.linkMode != Zotero.Attachments.LINK_MODE_LINKED_FILE) {
var mtime = Zotero.Sync.Storage.getSyncedModificationTime(item.primary.itemID);
if (mtime) {
xml.@storageModTime = mtime;
}
var path = <path>{item.attachment.path}</path>;
xml.path += path;
var hash = Zotero.Sync.Storage.getSyncedHash(item.primary.itemID);
if (hash) {
xml.@storageHash = hash;
}
}
}
if (item.note) {

View file

@ -1,5 +1,6 @@
Zotero.URI = new function () {
var _baseURI = ZOTERO_CONFIG.BASE_URI;
var _apiURI = ZOTERO_CONFIG.API_URI;
/**
@ -22,8 +23,11 @@ Zotero.URI = new function () {
*
* @return {String}
*/
this.getCurrentUserURI = function () {
this.getCurrentUserURI = function (noLocal) {
var userID = Zotero.userID;
if (!userID && noLocal) {
throw new Exception("Local userID not available and noLocal set in Zotero.URI.getCurrentUserURI()");
}
if (userID) {
return _baseURI + "users/" + userID;
}
@ -42,22 +46,44 @@ Zotero.URI = new function () {
this.getLibraryURI = function (libraryID) {
var path = this.getLibraryPath(libraryID);
return _baseURI + path;
}
/**
* Get path portion of library URI (e.g., users/6 or groups/1)
*/
this.getLibraryPath = function (libraryID) {
if (libraryID) {
var libraryType = Zotero.Libraries.getType(libraryID);
}
else {
libraryType = 'user';
}
switch (libraryType) {
case 'user':
var id = Zotero.userID;
if (!id) {
throw new Exception("User id not available in Zotero.URI.getLibraryPath()");
}
break;
case 'group':
var id = Zotero.Groups.getGroupIDFromLibraryID(libraryID);
break;
case 'user':
throw ("User library ids are not supported in Zotero.URI.getLibraryURI");
default:
throw ("Unsupported library type '" + libraryType + "' in Zotero.URI.getLibraryURI()");
throw ("Unsupported library type '" + libraryType + "' in Zotero.URI.getLibraryPath()");
}
return _baseURI + libraryType + "s/" + id;
return libraryType + "s/" + id;
}
/**
* Return URI of item, which might be a local URI if user hasn't synced
*/
this.getItemURI = function (item) {
if (item.libraryID) {
var baseURI = this.getLibraryURI(item.libraryID);
@ -69,6 +95,14 @@ Zotero.URI = new function () {
}
/**
* Get path portion of item URI (e.g., users/6/items/ABCD1234 or groups/1/items/ABCD1234)
*/
this.getItemPath = function (item) {
return this.getLibraryPath(item.libraryID) + "/items/" + item.key;
}
this.getGroupsURL = function () {
return ZOTERO_CONFIG.WWW_BASE_URL + "groups";
}

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) {
if (typeof strOrFile == 'string') {
var converter = Components.classes["@mozilla.org/intl/scriptableunicodeconverter"].
createInstance(Components.interfaces.nsIScriptableUnicodeConverter);
converter.charset = "UTF-8";
var result = {};
var data = converter.convertToByteArray(str, result);
var data = converter.convertToByteArray(strOrFile, result);
var ch = Components.classes["@mozilla.org/security/hash;1"]
.createInstance(Components.interfaces.nsICryptoHash);
ch.init(ch.MD5);
ch.update(data, data.length);
var hash = ch.finish(false);
}
else if (strOrFile instanceof Components.interfaces.nsIFile) {
var istream = Components.classes["@mozilla.org/network/file-input-stream;1"]
.createInstance(Components.interfaces.nsIFileInputStream);
// open for reading
istream.init(strOrFile, 0x01, 0444, 0);
var ch = Components.classes["@mozilla.org/security/hash;1"]
.createInstance(Components.interfaces.nsICryptoHash);
// we want to use the MD5 algorithm
ch.init(ch.MD5);
// this tells updateFromStream to read the entire file
const PR_UINT32_MAX = 0xffffffff;
ch.updateFromStream(istream, PR_UINT32_MAX);
}
// pass false here to get binary data back
var hash = ch.finish(base64);
if (istream) {
istream.close();
}
if (base64) {
return hash;
}
/*
// This created 36-character hashes
// return the two-digit hexadecimal code for a byte
function toHexString(charCode) {
@ -417,6 +449,17 @@ Zotero.Utilities.prototype.md5 = function(str) {
// convert the binary hash data to a hex string.
return [toHexString(hash.charCodeAt(i)) for (i in hash)].join("");
*/
// From http://rcrowley.org/2007/11/15/md5-in-xulrunner-or-firefox-extensions/
var ascii = []; ii = hash.length;
for (var i = 0; i < ii; ++i) {
var c = hash.charCodeAt(i);
var ones = c % 16;
var tens = c >> 4;
ascii.push(String.fromCharCode(tens + (tens > 9 ? 87 : 48)) + String.fromCharCode(ones + (ones > 9 ? 87 : 48)));
}
return ascii.join('');
}
@ -795,17 +838,18 @@ Zotero.Utilities.Translate.prototype.retrieveDocument = function(url) {
* @param {String} url URL to load
* @param {String} [body=null] Request body to POST to the URL; a GET request is
* executed if no body is present
* @param {String} [requestContentType=application/x-www-form-urlencoded]
* Request content type for POST; ignored if no body
* @param {Object} [headers] HTTP headers to include in request;
* Content-Type defaults to application/x-www-form-urlencoded
* for POST; ignored if no body
* @param {String} [responseCharset] Character set to force on the response
* @return {String} Request body
*/
Zotero.Utilities.Translate.prototype.retrieveSource = function(url, body, requestContentType, responseCharset) {
Zotero.Utilities.Translate.prototype.retrieveSource = function(url, body, headers, responseCharset) {
/* Apparently, a synchronous XMLHttpRequest would have the behavior of this routine in FF3, but
* in FF3.5, synchronous XHR blocks all JavaScript on the thread. See
* http://hacks.mozilla.org/2009/07/synchronous-xhr/. */
if(this.translate.locationIsProxied) url = this._convertURL(url);
if(!requestContentType) requestContentType = null;
if(!headers) headers = null;
if(!responseCharset) responseCharset = null;
var mainThread = Zotero.mainThread;
@ -813,7 +857,7 @@ Zotero.Utilities.Translate.prototype.retrieveSource = function(url, body, reques
var listener = function(aXmlhttp) { xmlhttp = aXmlhttp };
if(body) {
Zotero.Utilities.HTTP.doPost(url, body, listener, requestContentType, responseCharset);
Zotero.Utilities.HTTP.doPost(url, body, listener, headers, responseCharset);
} else {
Zotero.Utilities.HTTP.doGet(url, listener, responseCharset);
}
@ -870,7 +914,7 @@ Zotero.Utilities.Translate.prototype.doGet = function(urls, processor, done, res
* Already documented in Zotero.Utilities.HTTP
* @ignore
*/
Zotero.Utilities.Translate.prototype.doPost = function(url, body, onDone, requestContentType, responseCharset) {
Zotero.Utilities.Translate.prototype.doPost = function(url, body, onDone, headers, responseCharset) {
url = this._convertURL(url);
var translate = this.translate;
@ -880,7 +924,7 @@ Zotero.Utilities.Translate.prototype.doPost = function(url, body, onDone, reques
} catch(e) {
translate.error(false, e);
}
}, requestContentType, responseCharset);
}, headers, responseCharset);
}
/**
@ -936,7 +980,6 @@ Zotero.Utilities.HTTP = new function() {
}
else {
Zotero.debug("HTTP GET " + url);
}
if (this.browserIsOffline()){
return false;
@ -981,12 +1024,20 @@ Zotero.Utilities.HTTP = new function() {
* @param {String} url URL to request
* @param {String} body Request body
* @param {Function} onDone Callback to be executed upon request completion
* @param {String} requestContentType Request content type (usually
* application/x-www-form-urlencoded)
* @param {String} headers Request HTTP headers
* @param {String} responseCharset Character set to force on the response
* @return {Boolean} True if the request was sent, or false if the browser is offline
*/
this.doPost = function(url, body, onDone, requestContentType, responseCharset) {
this.doPost = function(url, body, onDone, headers, responseCharset) {
if (url instanceof Components.interfaces.nsIURI) {
// Don't display password in console
var disp = url.clone();
if (disp.password) {
disp.password = "********";
}
url = url.spec;
}
var bodyStart = body.substr(0, 1024);
// Don't display sync password or session id in console
bodyStart = bodyStart.replace(/password=[^&]+/, 'password=********');
@ -995,7 +1046,7 @@ Zotero.Utilities.HTTP = new function() {
Zotero.debug("HTTP POST "
+ (body.length > 1024 ?
bodyStart + '... (' + body.length + ' chars)' : bodyStart)
+ " to " + url);
+ " to " + (disp ? disp.spec : url));
if (this.browserIsOffline()){
@ -1025,7 +1076,27 @@ Zotero.Utilities.HTTP = new function() {
xmlhttp.channel.loadGroup = ds.getInterface(Ci.nsILoadGroup);
xmlhttp.channel.loadFlags |= Ci.nsIChannel.LOAD_DOCUMENT_URI;
xmlhttp.setRequestHeader("Content-Type", (requestContentType ? requestContentType : "application/x-www-form-urlencoded" ));
if (headers) {
if (typeof headers == 'string') {
var msg = "doPost() now takes a headers object rather than a requestContentType -- update your code";
Zotero.debug(msg, 2);
Components.utils.reportError(msg);
headers = {
"Content-Type": headers
};
}
}
else {
headers = {};
}
if (!headers["Content-Type"]) {
headers["Content-Type"] = "application/x-www-form-urlencoded";
}
for (var header in headers) {
xmlhttp.setRequestHeader(header, headers[header]);
}
/** @ignore */
xmlhttp.onreadystatechange = function(){
@ -1042,10 +1113,24 @@ Zotero.Utilities.HTTP = new function() {
*
* @param {String} url URL to request
* @param {Function} onDone Callback to be executed upon request completion
* @param {Object} requestHeaders HTTP headers to include with request
* @return {Boolean} True if the request was sent, or false if the browser is offline
*/
this.doHead = function(url, onDone) {
this.doHead = function(url, onDone, requestHeaders) {
if (url instanceof Components.interfaces.nsIURI) {
// Don't display password in console
var disp = url.clone();
if (disp.password) {
disp.password = "********";
}
Zotero.debug("HTTP HEAD " + disp.spec);
url = url.spec;
}
else {
Zotero.debug("HTTP HEAD " + url);
}
if (this.browserIsOffline()){
return false;
}
@ -1068,10 +1153,20 @@ Zotero.Utilities.HTTP = new function() {
ds.itemType = Ci.nsIDocShellTreeItem.typeContent;
var xmlhttp = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
createInstance(Ci.nsIXMLHttpRequest);
// Prevent certificate/authentication dialogs from popping up
xmlhttp.mozBackgroundRequest = true;
xmlhttp.open("HEAD", url, true);
if (requestHeaders) {
for (var header in requestHeaders) {
xmlhttp.setRequestHeader(header, requestHeaders[header]);
}
}
xmlhttp.channel.loadGroup = ds.getInterface(Ci.nsILoadGroup);
xmlhttp.channel.loadFlags |= Ci.nsIChannel.LOAD_DOCUMENT_URI;
// Don't cache HEAD requests
xmlhttp.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
/** @ignore */
xmlhttp.onreadystatechange = function(){
_stateChange(xmlhttp, onDone);

View file

@ -28,7 +28,8 @@ const ZOTERO_CONFIG = {
REPOSITORY_RETRY_INTERVAL: 3600, // 1 hour
BASE_URI: 'http://zotero.org/',
WWW_BASE_URL: 'http://www.zotero.org/',
SYNC_URL: 'https://sync.zotero.org/'
SYNC_URL: 'https://sync.zotero.org/',
API_URL: 'https://api.zotero.org/'
};
/*
@ -100,6 +101,17 @@ var Zotero = new function(){
Zotero.DB.query(sql, parseInt(val));
});
this.__defineGetter__('username', function () {
var sql = "SELECT value FROM settings WHERE "
+ "setting='account' AND key='username'";
return Zotero.DB.valueQuery(sql);
});
this.__defineSetter__('username', function (val) {
var sql = "REPLACE INTO settings VALUES ('account', 'username', ?)";
Zotero.DB.query(sql, val);
});
this.getLocalUserKey = function (generate) {
if (_localUserKey) {
return _localUserKey;
@ -1160,8 +1172,9 @@ var Zotero = new function(){
Zotero.Fulltext.purgeUnusedWords();
Zotero.Items.purge();
if (!skipStoragePurge && Zotero.Sync.Storage.active && Zotero.Utilities.prototype.probability(10)) {
Zotero.Sync.Storage.purgeDeletedStorageFiles();
if (!skipStoragePurge && Zotero.Utilities.prototype.probability(10)) {
Zotero.Sync.Storage.purgeDeletedStorageFiles('zfs');
Zotero.Sync.Storage.purgeDeletedStorageFiles('webdav');
}
}

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 666 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

View file

@ -205,9 +205,15 @@
font-weight: bold;
}
#zotero-tb-storage-sync
#zotero-tb-sync-warning
{
list-style-image: url(chrome://zotero/skin/drive_network.png);
list-style-image: url(chrome://zotero/skin/error.png);
margin-right: -5px;
}
#zotero-tb-sync-warning[error=true]
{
list-style-image: url(chrome://zotero/skin/exclamation.png);
}
#zotero-tb-sync {
@ -220,10 +226,6 @@
list-style-image: url(chrome://zotero/skin/arrow_rotate_animated.png);
}
#zotero-tb-sync[status=error] {
list-style-image: url(chrome://zotero/skin/arrow_rotate_error.png);
}
#zotero-tb-sync #zotero-last-sync-time
{
color: gray;

View file

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

View file

@ -37,6 +37,10 @@ pref("extensions.zotero.sortAttachmentsChronologically", false);
pref("extensions.zotero.showTrashWhenEmpty", true);
pref("extensions.zotero.viewOnDoubleClick", true);
pref("extensions.zotero.groups.copyChildLinks", true);
pref("extensions.zotero.groups.copyChildFileAttachments", true);
pref("extensions.zotero.groups.copyChildNotes", true);
pref("extensions.zotero.backup.numBackups", 2);
pref("extensions.zotero.backup.interval", 1440);
@ -113,14 +117,16 @@ pref("extensions.zotero.annotations.warnOnClose", true);
pref("extensions.zotero.sync.autoSync", true);
pref("extensions.zotero.sync.server.username", '');
pref("extensions.zotero.sync.server.compressData", true);
pref("extensions.zotero.sync.storage.protocol", "webdavs");
pref("extensions.zotero.sync.storage.enabled", false);
pref("extensions.zotero.sync.storage.enabled", true);
pref("extensions.zotero.sync.storage.protocol", "zotero");
pref("extensions.zotero.sync.storage.verified", false);
pref("extensions.zotero.sync.storage.scheme", 'https');
pref("extensions.zotero.sync.storage.url", '');
pref("extensions.zotero.sync.storage.username", '');
pref("extensions.zotero.sync.storage.maxDownloads", 4);
pref("extensions.zotero.sync.storage.maxUploads", 4);
pref("extensions.zotero.sync.storage.deleteDelayDays", 30);
pref("extensions.zotero.sync.storage.groups.enabled", true);
// Proxy
pref("extensions.zotero.proxies.transparent", true);

View file

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