Skip to content

Commit 7825e00

Browse files
committed
inital commit
0 parents  commit 7825e00

File tree

10 files changed

+408
-0
lines changed

10 files changed

+408
-0
lines changed

.gitattributes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Auto detect text files and perform LF normalization
2+
* text=auto

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
/Makefile.in
2+
.DS_Store
3+
*.xpi
4+
*.icloud

Makefile

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
all: Makefile.in
2+
3+
-include Makefile.in
4+
5+
RELEASE:=$(shell grep version manifest.json | sed '2q;d' | sed -e 's/^ *"version": "//' -e 's/",//')
6+
7+
ZotFileRecovery.xpi: FORCE
8+
rm -rf $@
9+
zip -FSr $@ bootstrap.js locale manifest.json ZotFileRecovery.js ZotFileRecovery_menus.js -x \*.DS_Store
10+
11+
ZotFileRecovery-%-fx.xpi: ZotFileRecovery.xpi
12+
mv $< $@
13+
14+
Makefile.in: manifest.json
15+
echo "all: ZotFileRecovery-${RELEASE}-fx.xpi" > Makefile.in
16+
17+
FORCE:

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# ZotFile Recovery
2+
3+
Unfortunatly, ZotFile is not compatible with Zotero 7 and most likely never will be. Several alternative projects implement some of ZotFile's functionality for Zotero 7. Take a look at [Zotero File](https://github.com/MuiseDestiny/zotero-file) and [ZotMoov](https://github.com/wileyyugioh/zotmoov). However, one of zotfiles features might still makes certain files inaccesible from Zotero 7. This problem impacts users who use send and get files from the tablet (optional setting under "Tablet Settings") and have set the ".tablet.mode" setting to 1, which is the default called "background mode" in the documentation. While these tablet still exist in the tablet folder specified in the zotfile settings, you won't be able to access them from Zotero 7. To address this problem `ZotFile Recovery` adds a menu item to recover files from the tablet. This menu item is disables (as in the screenshot below) unless you select a zotero item or attachment. Similar to the ZotFile function "Get from Tablet", this function removes the tablet file from from the tablet location.
4+
5+
Please use the issue tracker for this GitHub repro to report any bugs.
6+
7+
<img src="res/ZotFileRecoveryMenu.png" width="500"/>

ZotFileRecovery.js

Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
2+
Components.utils.importGlobalProperties(['PathUtils', 'IOUtils']);
3+
4+
Zotero.ZotFileRecovery = {
5+
id: null,
6+
version: null,
7+
rootURI: null,
8+
initialized: false,
9+
10+
init({ id, version, rootURI }) {
11+
if(this.initialized) return;
12+
13+
this.id = id;
14+
this.version = version;
15+
this.rootURI = rootURI;
16+
this.initialized = true;
17+
18+
},
19+
20+
getTabletInfo (att, key) {
21+
try {
22+
var parser = new DOMParser();
23+
var value,
24+
content = att.getNote(),
25+
doc = parser.parseFromString(content, 'text/html'),
26+
p = doc.querySelector('#zotfile-data');
27+
if(p === null) p = doc.querySelector('[title*="lastmod"][title*="projectFolder"]');
28+
if(p === null) {
29+
// support for old system
30+
var search = content.search(key);
31+
value = content.substring(search);
32+
value = value.substring(value.search('{') + 1, value.search('}'));
33+
}
34+
else {
35+
var data = JSON.parse(p.getAttribute('title').replace(/&quot;/g, '"'));
36+
value = key in data ? data[key] : undefined;
37+
}
38+
// for location tag: replace [BaseFolder] with destination folder
39+
if(key == 'location') {
40+
let dest_dir = Zotero.Prefs.get('extensions.zotfile.tablet.dest_dir', true);
41+
dest_dir = dest_dir === undefined ? "" : dest_dir;
42+
value = value.replace('[BaseFolder]', dest_dir);
43+
}
44+
// for location and projectFolder tag: correct window/mac file system
45+
if(['location', 'projectFolder'].includes(key) && Zotero.isWin) value = value.replace(/\//g, '\\');
46+
if(['location', 'projectFolder'].includes(key) && !Zotero.isWin) value = value.replace(/\\/g, '/');
47+
// return
48+
return value;
49+
}
50+
catch (err) {
51+
return '';
52+
}
53+
},
54+
55+
clearInfo (att) {
56+
try {
57+
var parser = new DOMParser();
58+
var content = att.getNote().replace(/zotero:\/\//g, 'http://zotfile.com/'),
59+
doc = parser.parseFromString(content, 'text/html'),
60+
p = doc.querySelector('#zotfile-data');
61+
if(p === null) p = doc.querySelector('[title*="lastmod"][title*="projectFolder"]');
62+
if (p !== null) doc.removeChild(p);
63+
// save content back to note
64+
content = doc.documentElement.innerHTML
65+
// remove old zotfile data
66+
.replace(/(lastmod|mode|location|projectFolder)\{.*?\};?/g,'')
67+
// replace links with zotero links
68+
.replace(/http:\/\/zotfile.com\//g, 'zotero://');
69+
att.setNote(content);
70+
}
71+
catch(e) {
72+
att.setNote('');
73+
}
74+
},
75+
76+
_getSelectedAttachments() {
77+
let atts = Zotero.getActiveZoteroPane().getSelectedItems()
78+
.map(item => item.isRegularItem() ? item.getAttachments() : item)
79+
.reduce((a, b) => a.concat(b), [])
80+
.map(item => typeof item == 'number' ? Zotero.Items.get(item) : item)
81+
.filter(item => item.isAttachment())
82+
.filter(item => this.getTabletInfo(item, 'mode') == 1);
83+
return atts;
84+
},
85+
86+
removeTabletTag(att, tag) {
87+
// remove from attachment
88+
att.removeTag(tag);
89+
// remove from parent item
90+
var item = Zotero.Items.get(att.parentItemID);
91+
if(item.hasTag(tag)) {
92+
var atts = Zotero.Items.get(item.getAttachments());
93+
if(!atts.some(att => att.hasTag(tag)))
94+
item.removeTag(tag);
95+
}
96+
},
97+
98+
// async recoverTabletFile() {
99+
async recoverTabletFile() {
100+
Zotero.debug("ZotFileRecovery: Running 'recoverTabletFile'");
101+
// get selected attachments
102+
let atts = this._getSelectedAttachments();
103+
// filter by tablet tag
104+
let tablet_tag = Zotero.Prefs.get('extensions.zotfile.tablet.tag', true);
105+
tablet_tag = tablet_tag === undefined ? "_tablet" : tablet_tag;
106+
atts = atts.filter(att => att.hasTag(tablet_tag));
107+
if(atts.length == 0) return;
108+
// get attachments from tablet
109+
atts = atts.map(att => this.getAttachmentFromTablet(att));
110+
return;
111+
},
112+
113+
async getTabletFilePath(att) {
114+
Zotero.debug("ZotFileRecovery: Running 'getTabletFilePath'");
115+
// foreground mode
116+
if(this.getTabletInfo(att, 'mode') == 2)
117+
return await att.getFilePathAsync();
118+
// background mode
119+
var path = this.getTabletInfo(att, 'location');
120+
return path;
121+
},
122+
123+
promptUser(message,but_0,but_1_cancel,but_2, title) {
124+
var title = typeof title !== 'ZotFile Dialog' ? title : true;
125+
var prompts = Components.classes['@mozilla.org/embedcomp/prompt-service;1']
126+
.getService(Components.interfaces.nsIPromptService);
127+
128+
var check = {value: false}; // default the checkbox to false
129+
130+
var flags = prompts.BUTTON_POS_0 * prompts.BUTTON_TITLE_IS_STRING +
131+
prompts.BUTTON_POS_1 * prompts.BUTTON_TITLE_IS_STRING +
132+
prompts.BUTTON_POS_2 * prompts.BUTTON_TITLE_IS_STRING;
133+
134+
var button = prompts.confirmEx(null, title, message,
135+
flags, but_0,but_1_cancel,but_2, null, check);
136+
137+
return(button);
138+
139+
},
140+
141+
getPref(pref, def) {
142+
let value = Zotero.Prefs.get('extensions.zotfile.' + pref, true);
143+
value = value === undefined ? def : value;;
144+
return value;
145+
},
146+
147+
// async getAttachmentFromTablet(att) {
148+
async getAttachmentFromTablet(att) {
149+
Zotero.debug("ZotFileRecovery: Running 'getAttachmentFromTablet'");
150+
var item = Zotero.Items.get(att.parentItemID),
151+
tablet_tag = this.getPref('tablet.tag', '_tablet'),
152+
tablet_tagMod = this.getPref('tablet.tagModified', '_tablet_modified'),
153+
tag_parent = this.getPref('tablet.tagParentPush_tag', '_tablet_parent');
154+
// Zotero and tablet file paths
155+
var path_zotero = await att.getFilePathAsync(),
156+
path_tablet = await this.getTabletFilePath(att, false);
157+
Zotero.debug("ZotFileRecovery 'path_tablet': " + path_tablet);
158+
Zotero.debug("ZotFileRecovery 'path_zotero':" + path_zotero);
159+
var path_tablet_exists = await IOUtils.exists(path_tablet);
160+
if (path_tablet == '' | !path_tablet_exists) {
161+
this.removeTabletTag(att, tablet_tag);
162+
this.clearInfo(att);
163+
await att.saveTx();
164+
await item.saveTx();
165+
this.infoWindow('ZotFile Warning', 'The tablet file "' + att.attachmentFilename + '" was manually moved and does not exist.');
166+
return att;
167+
}
168+
// get modification times for files
169+
var time_tablet = 0, time_zotero = 0;
170+
if(await IOUtils.exists(path_tablet)) {
171+
let tablet_file_stat = await IOUtils.stat(path_tablet);
172+
time_tablet = tablet_file_stat.lastModified;
173+
}
174+
var time_saved = parseInt(this.getTabletInfo(att, 'lastmod'), 10);
175+
if(await IOUtils.exists(path_zotero)) {
176+
let zotero_file_stat = await IOUtils.stat(path_zotero);
177+
time_zotero = zotero_file_stat.lastModified;
178+
}
179+
Zotero.debug("ZotFileRecovery lastModified (tablet, saved, zotero): " + time_tablet + " " + time_saved + " " + time_zotero);
180+
// background mode
181+
if((time_tablet == 0 & time_zotero == 0)) {
182+
this.infoWindow('ZotFile Warning', 'Recovery of tablet file "' + att.attachmentFilename + '" failed. Please manually recover the file at "' + path_tablet + '"');
183+
return att;
184+
}
185+
186+
// Status of tablet file 'tablet_status'
187+
// 0 - Tablet file modified -> Replace zotero file
188+
// 1 - Both files modified -> Prompt user
189+
// 2 - Zotero file modified -> Delete tablet file
190+
var tablet_status = 1;
191+
if (time_tablet > time_saved && time_zotero <= time_saved) tablet_status = 0;
192+
if (time_tablet <= time_saved && time_zotero <= time_saved) tablet_status = 2;
193+
if (time_tablet <= time_saved && time_zotero > time_saved) tablet_status = 2;
194+
if (time_tablet > time_saved && time_zotero > time_saved) tablet_status = 1;
195+
Zotero.debug("ZotFileRecovery 'tablet_status': " + tablet_status);
196+
197+
// prompt if both file have been modified
198+
if (tablet_status == 1) {
199+
let message = "Both copies of the attachment file '" + att.attachmentFilename + "' have been modified. What do you want to do?\n\nRemoving the tablet file discards all changes made to the file on the tablet.";
200+
tablet_status = this.promptUser(message, "Replace Zotero File", "Cancel", "Remove Tablet File");
201+
if (tablet_status == 1) return;
202+
}
203+
204+
// Replace zotero file
205+
if(tablet_status == 0)
206+
await IOUtils.move(path_tablet, path_zotero);
207+
// Remove tablet file
208+
if(tablet_status == 2)
209+
await Zotero.File.removeIfExists(path_tablet);
210+
211+
// remove tag from attachment and parent item
212+
this.removeTabletTag(att, tablet_tag);
213+
if(item.hasTag(tag_parent)) item.removeTag(tag_parent);
214+
// clear attachment note
215+
this.Tablet.clearInfo(att);
216+
// remove modified tag from attachment
217+
this.removeTabletTag(att, this.Tablet.tagMod);
218+
await att.saveTx();
219+
await item.saveTx();
220+
// notification
221+
this.infoWindow('ZotFile Recovery', 'The tablet file "' + att.attachmentFilename + '" was removed from the tablet.');
222+
// return...
223+
return att;
224+
},
225+
226+
infoWindow (headline, content) {
227+
// default arguments
228+
main = typeof main !== 'undefined' ? main : 'title';
229+
message = typeof message !== 'undefined' ? message : 'message';
230+
// show window
231+
var progressWin = new Zotero.ProgressWindow();
232+
progressWin.changeHeadline(headline);
233+
progressWin.addLines(content);
234+
progressWin.show();
235+
progressWin.startCloseTimer();
236+
},
237+
238+
239+
};

ZotFileRecovery_menus.js

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
2+
Components.utils.import('resource://gre/modules/Services.jsm');
3+
4+
ZotFileRecovery_Menus = {
5+
_store_added_elements: [],
6+
_opt_disable_elements: [],
7+
8+
_window_listener: {
9+
onOpenWindow: function(a_window) {
10+
let dom_window = a_window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
11+
dom_window.addEventListener('load', function() {
12+
dom_window.removeEventListener('load', arguments.callee, false);
13+
if (dom_window.document.documentElement.getAttribute('windowtype') != 'navigator:browser') return;
14+
ZotFileRecovery_Menus._store_added_elements = []; // Clear tracked elements since destroyed by closed window
15+
ZotFileRecovery_Menus._opt_disable_elements = [];
16+
ZotFileRecovery_Menus._init();
17+
}, false);
18+
}
19+
},
20+
21+
_popupShowing() {
22+
// let should_hide = !ZotFileRecovery_Menus._hasTabletFile();
23+
let should_disabled = !ZotFileRecovery_Menus._hasTabletFile();
24+
for (let element of ZotFileRecovery_Menus._opt_disable_elements) {
25+
// element.hidden = should_hide;
26+
element.disabled = should_disabled;
27+
}
28+
},
29+
30+
_getWindow() {
31+
let enumerator = Services.wm.getEnumerator('navigator:browser');
32+
while (enumerator.hasMoreElements())
33+
{
34+
let win = enumerator.getNext();
35+
if (!win.ZoteroPane) continue;
36+
return win;
37+
}
38+
},
39+
40+
_hasTabletFile() {
41+
let atts = Zotero.ZotFileRecovery._getSelectedAttachments();
42+
let tablet_tag = Zotero.ZotFileRecovery.getPref('tablet.tag', '_tablet');
43+
return atts.some(att => att.hasTag(tablet_tag));
44+
},
45+
46+
init() {
47+
this._init();
48+
Services.wm.addListener(this._window_listener);
49+
},
50+
51+
_init() {
52+
let win = this._getWindow();
53+
let doc = win.document;
54+
55+
// Menu separator
56+
let menuseparator = doc.createXULElement('menuseparator');
57+
// Move Selected Menu item
58+
let recovery_item = doc.createXULElement('menuitem');
59+
recovery_item.id = 'ZotFileRecovery-recover-file';
60+
recovery_item.setAttribute('data-l10n-id', 'ZotFileRecovery-recover-file');
61+
recovery_item.addEventListener('command', function() {
62+
Zotero.ZotFileRecovery.recoverTabletFile();
63+
});
64+
let zotero_itemmenu = doc.getElementById('zotero-itemmenu');
65+
zotero_itemmenu.addEventListener('popupshowing', this._popupShowing);
66+
zotero_itemmenu.appendChild(menuseparator);
67+
zotero_itemmenu.appendChild(recovery_item);
68+
this._store_added_elements.push(menuseparator, recovery_item);
69+
this._opt_disable_elements.push(menuseparator, recovery_item);
70+
71+
// Enable localization
72+
win.MozXULElement.insertFTLIfNeeded('ZotFileRecovery.ftl');
73+
},
74+
75+
destroy() {
76+
this._destroy();
77+
Services.wm.removeListener(this._window_listener);
78+
},
79+
80+
_destroy() {
81+
let doc = this._getWindow().document;
82+
for (let element of this._store_added_elements) {
83+
if (element) element.remove();
84+
}
85+
doc.querySelector('[href="ZotFileRecovery.ftl"]').remove();
86+
87+
let zotero_itemmenu = doc.getElementById('zotero-itemmenu');
88+
zotero_itemmenu.removeEventListener('popupshowing', this._popupShowing);
89+
90+
this._store_added_elements = [];
91+
this._opt_disable_elements = [];
92+
}
93+
}

bootstrap.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
var ZotFileRecovery;
2+
3+
function install() {
4+
Zotero.debug('ZotFileRecovery: Installed');
5+
}
6+
7+
async function startup({ id, version, rootURI}) {
8+
Zotero.debug('ZotFileRecovery: Starting');
9+
Zotero.debug('ZotFileRecovery rootURI: ' + rootURI);
10+
Services.scriptloader.loadSubScript(rootURI + 'ZotFileRecovery.js');
11+
Services.scriptloader.loadSubScript(rootURI + 'ZotFileRecovery_menus.js');
12+
Zotero.ZotFileRecovery.init({ id, version, rootURI });
13+
ZotFileRecovery_Menus.init();
14+
}
15+
16+
function shutdown() {
17+
Zotero.debug('ZotFileRecovery: Shutting down');
18+
// ZotFileRecovery.removeFromAllWindows();
19+
20+
// Zotero.ZotFileRecovery.destroy();
21+
ZotFileRecovery_Menus.destroy();
22+
23+
Zotero.ZotFileRecovery = undefined;
24+
ZotFileRecovery_Menus = undefined;
25+
}
26+
27+
function uninstall() {
28+
Zotero.debug('ZotFileRecovery: Uninstalled');
29+
}

0 commit comments

Comments
 (0)