├── .gitignore ├── .gitattributes ├── res └── ZotFileRecoveryMenu.png ├── locale └── en-US │ └── ZotFileRecovery.ftl ├── manifest.json ├── Makefile ├── updates.json ├── bootstrap.js ├── README.md ├── ZotFileRecovery_menus.js └── ZotFileRecovery.js /.gitignore: -------------------------------------------------------------------------------- 1 | /Makefile.in 2 | .DS_Store 3 | *.xpi 4 | *.icloud 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /res/ZotFileRecoveryMenu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jlegewie/ZotFile-Recovery/HEAD/res/ZotFileRecoveryMenu.png -------------------------------------------------------------------------------- /locale/en-US/ZotFileRecovery.ftl: -------------------------------------------------------------------------------- 1 | ZotFileRecovery-recover-file = 2 | .label = ZotFile: Recover Tablet Attachment 3 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "ZotFileRecovery", 4 | "version": "0.2.0", 5 | "description": "Recover tablet files from Zotfile in Zotero 7", 6 | "author": "Joscha Legewie", 7 | "applications": { 8 | "zotero": { 9 | "id": "ZotFileRecovery@jlegewie.com", 10 | "update_url": "https://raw.githubusercontent.com/jlegewie/ZotFile-Recovery/main/updates.json", 11 | "strict_min_version": "6.999", 12 | "strict_max_version": "7.0.*" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 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: 18 | -------------------------------------------------------------------------------- /updates.json: -------------------------------------------------------------------------------- 1 | { 2 | "addons": { 3 | "ZotFileRecovery@jlegewie.com": { 4 | "updates": [ 5 | { 6 | "version": "0.2.0", 7 | "update_link": "https://github.com/jlegewie/ZotFile-Recovery/releases/download/v0.2.0/ZotFileRecovery-0.2.0-fx.xpi", 8 | "applications": { 9 | "zotero": { 10 | "strict_min_version": "6.999", 11 | "strict_max_version": "7.0.*" 12 | } 13 | } 14 | } 15 | ] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /bootstrap.js: -------------------------------------------------------------------------------- 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 | } 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 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). 4 | 5 | One of zotfiles features might make certain files inaccesible from Zotero 7. This problem impacts users who 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 Zotero 7 to recover files from the tablet. This menu item is disabled (as in the screenshot below) unless you select an attachment with an impacted tablet file. Similar to the ZotFile function "Get from Tablet", this function removes the tablet file from the tablet location. 6 | 7 | Please use the issue tracker for this GitHub repro to report any bugs. 8 | 9 | -------------------------------------------------------------------------------- /ZotFileRecovery_menus.js: -------------------------------------------------------------------------------- 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 | } 94 | -------------------------------------------------------------------------------- /ZotFileRecovery.js: -------------------------------------------------------------------------------- 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(/"/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 | }; 240 | --------------------------------------------------------------------------------