diff --git a/chrome/content/zotero/osfile.mjs b/chrome/content/zotero/osfile.mjs new file mode 100644 index 0000000000..db1c0694b5 --- /dev/null +++ b/chrome/content/zotero/osfile.mjs @@ -0,0 +1,362 @@ +// +// Compatibility shims from the Mozilla codebase +// +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 && Zotero.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) + }; + }, + + 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 { + 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); + } + } + 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; + }, + + toFileURI: function (path) { + return PathUtils.toFileURI(path); + }, + } +}; + + +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 diff --git a/chrome/content/zotero/xpcom/file.js b/chrome/content/zotero/xpcom/file.js index 29dc566b13..9366f82fc2 100644 --- a/chrome/content/zotero/xpcom/file.js +++ b/chrome/content/zotero/xpcom/file.js @@ -1097,7 +1097,42 @@ Zotero.File = new function(){ } }; - + // From public-domain Mozilla code + // https://searchfox.org/mozilla-central/rev/78a2c17cc80680a5a82446e4ce7c45a73b935383/security/sandbox/test/browser_content_sandbox_utils.js#85-115 + // + // @param {String} sourcePath - The file to create a symlink to + // @param {String} targetPath - The location of the symlink to create + // @return {Promise} - True if successfully created, false otherwise + this.createSymlink = function (sourcePath, targetPath) { + const { ctypes } = ChromeUtils.importESModule( + "resource://gre/modules/ctypes.sys.mjs" + ); + + try { + 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(sourcePath, targetPath)) { + return false; + } + } + catch (e) { + Zotero.logError(e); + return false; + } + + return true; + } + /** * Normalize to a Unix-style path, replacing backslashes (interpreted as * separators only on Windows) with forward slashes (interpreted as diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js index 7bd11929b3..228e3728c8 100644 --- a/chrome/content/zotero/xpcom/zotero.js +++ b/chrome/content/zotero/xpcom/zotero.js @@ -27,7 +27,7 @@ Components.utils.importGlobalProperties(["XMLHttpRequest"]); Components.utils.import("resource://zotero/config.js"); Components.utils.import("resource://gre/modules/Services.jsm"); -Components.utils.import("resource://gre/modules/osfile.jsm"); +var { OS } = ChromeUtils.importESModule("chrome://zotero/content/osfile.mjs"); Components.classes["@mozilla.org/net/osfileconstantsservice;1"] .getService(Components.interfaces.nsIOSFileConstantsService) .init();