diff --git a/.eslintrc.json b/.eslintrc.json index 36c3632..52ba2b0 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -30,6 +30,7 @@ "default": "array" } ], + "@typescript-eslint/no-magic-numbers": "off", "@typescript-eslint/await-thenable": "error", "@typescript-eslint/ban-ts-comment": "warn", "@typescript-eslint/ban-types": "off", @@ -199,10 +200,6 @@ "no-invalid-this": "off", "no-irregular-whitespace": "error", "no-magic-numbers": "off", - "@typescript-eslint/no-magic-numbers": [ "error", { - "ignore": [ -1, 0, 1, 2 ], - "ignoreEnums": true - }], "no-new-wrappers": "error", "no-redeclare": "error", "no-throw-literal": "error", diff --git a/esbuild.js b/esbuild.js index 8535129..3c8e2bf 100644 --- a/esbuild.js +++ b/esbuild.js @@ -81,6 +81,7 @@ async function bundle(config) { } async function build() { + await bundle({ exportGlobals: true, entryPoints: [ 'bootstrap.ts' ], diff --git a/lib.ts b/lib.ts index 7ac4021..4ca4f49 100644 --- a/lib.ts +++ b/lib.ts @@ -1,6 +1,10 @@ declare const Zotero: any declare const Components: any declare const Services: any +declare let OS: any + +import { OS as $OS } from './osfile' +if (typeof OS === 'undefined') OS = $OS const l10n = require('./locale/en-US/zotero-edtechhub.ftl') @@ -22,9 +26,6 @@ var EdTechHub: EdTechHubMain // eslint-disable-line no-var import { DebugLog as DebugLogSender } from 'zotero-plugin/debug-log' import { patch as $patch$, unpatch as $unpatch$ } from './monkey-patch' -Components.utils.import('resource://gre/modules/osfile.jsm') -declare const OS: any - import sanitize_filename = require('sanitize-filename') function flash(title, body = null, timeout = 8) { // eslint-disable-line @typescript-eslint/no-magic-numbers @@ -420,16 +421,17 @@ class EdTechHubMain { return merged // eslint-disable-line @typescript-eslint/no-unsafe-return }) - const addons = await Zotero.getInstalledExtensions() - // if (!addons.find(addon => addon.startsWith('Zotero DOI Manager '))) { - // flash('Zotero-ShortDOI not installed', 'The short-doi plugin is not available, please install it from https://github.com/bwiernik/zotero-shortdoi') - // } - if (!addons.find((addon: string) => addon.startsWith('ZotFile '))) { - flash('ZotFile not installed', 'The ZotFile plugin is not available, please install it from http://zotfile.com/') + /* + const checks = { + 'Zotero DOI Manager ': [ 'Zotero-ShortDOI not installed', 'The short-doi plugin is not available, please install it from https://github.com/bwiernik/zotero-shortdoi' ], + 'ZotFile ': [ 'ZotFile not installed', 'The ZotFile plugin is not available, please install it from http://zotfile.com/' ], + 'Zutilo Utility for Zotero ': [ 'Zutilo not installed', 'The Zutilo plugin is not available, please install it from https://github.com/willsALMANJ/Zutilo' ], } - if (!addons.find((addon: string) => addon.startsWith('Zutilo Utility for Zotero '))) { - flash('Zutilo not installed', 'The Zutilo plugin is not available, please install it from https://github.com/willsALMANJ/Zutilo') + const addons = await Zotero.getInstalledExtensions() + for (const [ name, [ title, body ] ] of Object.entries(checks)) { + if (!addons.find((addon: string) => addon.startsWith(name))) flash(title, body) } + */ try { debug('installing translators') diff --git a/osfile.js b/osfile.js new file mode 100644 index 0000000..0c1892c --- /dev/null +++ b/osfile.js @@ -0,0 +1,418 @@ +// +// Compatibility shims from the Mozilla codebase +// +const isWin = Services.appinfo.OS == 'WINNT'; + +export let OS = { + Constants: { + Path: { + get homeDir() { + return FileUtils.getDir("Home", []).path; + }, + + get libDir() { + return FileUtils.getDir("GreBinD", []).path; + }, + + get profileDir() { + return FileUtils.getDir("ProfD", []).path; + }, + + get tmpDir() { + return FileUtils.getDir("TmpD", []).path; + }, + } + }, + + File: { + DirectoryIterator: function (path) { + var initialized = false; + var paths = []; + + async function init() { + paths.push(...await IOUtils.getChildren(path)); + initialized = true; + } + + async function getEntry(path) { + var info = await IOUtils.stat(path); + return { + name: PathUtils.filename(path), + path, + isDir: info.type == 'directory' + }; + } + + this.nextBatch = async function (num) { + if (!initialized) { + await init(); + } + var entries = []; + while (paths.length && num > 0) { + entries.push(await getEntry(paths.shift())); + num--; + } + return entries; + }; + + this.forEach = async function (func) { + if (!initialized) { + await init(); + } + var i = 0; + while (paths.length) { + let entry = await getEntry(paths.shift()); + await func(entry, i++, this); + } + }; + + this.close = function () {}; + }, + + Error: function (msg) { + this.message = msg; + this.stack = new Error().stack; + }, + + copy: wrapWrite(async function (src, dest) { + return IOUtils.copy(src, dest); + }), + + exists: async function (path) { + try { + return await IOUtils.exists(path); + } + catch (e) { + if (e.message.includes('NS_ERROR_FILE_UNRECOGNIZED_PATH')) { + dump(e.message + "\n\n" + e.stack + "\n\n"); + Components.utils.reportError(e); + return false; + } + } + }, + + makeDir: wrapWrite(async function (path, options = {}) { + try { + return await IOUtils.makeDirectory( + path, + { + ignoreExisting: options.ignoreExisting !== false, + createAncestors: !!options.from, + permissions: options.unixMode + } + ); + } + catch (e) { + // Broken symlink + if (e.name == 'InvalidAccessError') { + if (/Could not create directory because the target file(.+) exists and is not a directory/.test(e.message)) { + let osFileError = new OS.File.Error(e.message); + osFileError.becauseExists = true; + throw osFileError; + } + } + } + }), + + move: wrapWrite(async function (src, dest, options = {}) { + if (options.noCopy) { + throw new Error("noCopy is no longer supported"); + } + + // Check noOverwrite + var destFileInfo = null; + try { + destFileInfo = await IOUtils.stat(dest) + } + catch (e) { + if (e.name != 'NotFoundError') { + throw e; + } + } + if (destFileInfo) { + if (destFileInfo.type == 'directory') { + throw new Error("OS.File.move() destination cannot be a directory -- use IOUtils.move()"); + } + if (options.noOverwrite) { + let e = new OS.File.Error; + e.becauseExists = true; + throw e; + } + } + + return IOUtils.move(src, dest, options); + }), + + read: async function (path, options = {}) { + if (options.encoding) { + if (!/^utf\-?8$/i.test(options.encoding)) { + throw new Error("Can only read UTF-8"); + } + return IOUtils.readUTF8(path); + } + return IOUtils.read( + path, + { + maxBytes: options.bytes + } + ); + }, + + remove: async function (path, options = {}) { + return IOUtils.remove(path, options); + }, + + removeDir: async function (path, options = {}) { + return IOUtils.remove( + path, + { + recursive: true, + // OS.File.removeDir defaulted to ignoreAbsent: true + ignoreAbsent: options.ignoreAbsent !== false + } + ); + }, + + removeEmptyDir: async function (path) { + return IOUtils.remove(path); + }, + + setDates: async function (path, atime, mtime) { + if (atime) { + await IOUtils.setAccessTime(path, atime.valueOf()); + } + return await IOUtils.setModificationTime(path, mtime ? mtime.valueOf() : undefined); + }, + + setPermissions: async function (path, { unixMode, winAttributes } = {}) { + await IOUtils.setPermissions(path, unixMode); + if (winAttributes && isWin) { + let { readOnly, hidden, system } = winAttributes; + await IOUtils.setWindowsAttributes(path, { readOnly, hidden, system }); + } + }, + + stat: async function stat(path) { + var info; + try { + info = await IOUtils.stat(path); + } + catch (e) { + if (e.name == 'NotFoundError') { + let osFileError = new this.Error("File not found"); + osFileError.becauseNoSuchFile = true; + throw osFileError; + } + throw e; + } + return { + isDir: info.type == 'directory', + isSymLink: true, // Supposedly was broken in Firefox + size: info.size, + lastAccessDate: new Date(info.lastAccessed), + lastModificationDate: new Date(info.lastModified), + unixMode: !isWin ? info.permissions : undefined, + }; + }, + + unixSymLink: async function (pathTarget, pathCreate) { + if (await IOUtils.exists(pathCreate)) { + let osFileError = new this.Error(pathCreate + " already exists"); + osFileError.becauseExists = true; + throw osFileError; + } + + // Copy of Zotero.File.createSymlink + const { ctypes } = ChromeUtils.importESModule( + "resource://gre/modules/ctypes.sys.mjs" + ); + + try { + if (Services.appinfo.OS === "Darwin") { + const libc = ctypes.open( + Services.appinfo.OS === "Darwin" ? "libSystem.B.dylib" : "libc.so" + ); + + const symlink = libc.declare( + "symlink", + ctypes.default_abi, + ctypes.int, // return value + ctypes.char.ptr, // target + ctypes.char.ptr //linkpath + ); + + if (symlink(pathTarget, pathCreate)) { + throw new Error("Failed to create symlink at " + pathCreate); + } + } + // The above is failing with "invalid ELF header" for libc.so on GitHub Actions, so + // just use ln -s on non-macOS systems + else { + let ln = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); + ln.initWithPath("/bin/ln"); + let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess); + process.init(ln); + let args = ["-s", pathTarget, pathCreate]; + process.run(true, args, args.length); + } + } + catch (e) { + dump(e.message + "\n\n"); + throw new Error("Failed to create symlink at " + pathCreate); + } + }, + + writeAtomic: async function (path, bytes, options = {}) { + if (options.backupTo) { + options.backupFile = options.backupTo; + } + if (options.noOverwrite) { + options.mode = 'create'; + } + if (options.encoding == 'utf-8') { + return IOUtils.writeUTF8(path, bytes, options); + } + return IOUtils.write(path, bytes, options); + }, + }, + + Path: { + basename: function (path) { + return PathUtils.filename(path); + }, + + dirname: function (path) { + return PathUtils.parent(path); + }, + + fromFileURI: function (uri) { + let url = new URL(uri); + if (url.protocol != "file:") { + throw new Error("fromFileURI expects a file URI"); + } + let path = this.normalize(decodeURIComponent(url.pathname)); + return path; + }, + + join: function (path, ...args) { + var platformSlash = Services.appinfo.OS == 'WINNT' ? '\\' : '/'; + try { + if (args.length == 0) { + return path; + } + if (args.length == 1 && args[0].includes(platformSlash)) { + return PathUtils.joinRelative(path, ...args); + } + return PathUtils.join(path, ...args); + } + catch (e) { + if (e.message.includes('NS_ERROR_FILE_UNRECOGNIZED_PATH')) { + Cu.reportError("WARNING: " + e.message + " -- update for IOUtils"); + return [path, ...args].join(platformSlash); + } + throw e; + } + }, + + // From Firefox 102 + normalize: function (path) { + let stack = []; + let absolute; + if (path.length >= 0 && path[0] == "/") { + absolute = true; + } + else { + absolute = false; + } + path.split("/").forEach(function (v) { + switch (v) { + case "": + case ".": // fallthrough + break; + case "..": + if (!stack.length) { + if (absolute) { + throw new Error("Path is ill-formed: attempting to go past root"); + } + else { + stack.push(".."); + } + } + else if (stack[stack.length - 1] == "..") { + stack.push(".."); + } + else { + stack.pop(); + } + break; + default: + stack.push(v); + } + }); + let string = stack.join("/"); + return absolute ? "/" + string : string; + }, + + split: function (path) { + if (Services.appinfo.OS == "WINNT") { + // winIsAbsolute() + let index = path.indexOf(":"); + let absolute = path.length > index + 1 && path[index + 1] == "\\"; + + return { + absolute, + winDrive: winGetDrive(path), + components: path.split("\\") + }; + } + + return { + absolute: path.length && path[0] == "/", + components: path.split("/") + }; + }, + + toFileURI: function (path) { + return PathUtils.toFileURI(path); + }, + } +}; + +// From Fx60 ospath_win.jsm +var winGetDrive = function (path) { + if (path == null) { + throw new TypeError("path is invalid"); + } + + if (path.startsWith("\\\\")) { + // UNC path + if (path.length == 2) { + return null; + } + let index = path.indexOf("\\", 2); + if (index == -1) { + return path; + } + return path.slice(0, index); + } + // Non-UNC path + let index = path.indexOf(":"); + if (index <= 0) return null; + return path.slice(0, index + 1); +}; + +function wrapWrite(func) { + return async function () { + try { + return await func(...arguments); + } + catch (e) { + if (DOMException.isInstance(e)) { + if (e.name == 'NoModificationAllowedError') { + e.becauseExists = true; + } + } + throw e; + } + }; +} \ No newline at end of file