269 lines
9.0 KiB
JavaScript
269 lines
9.0 KiB
JavaScript
// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
|
|
//
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
import Gio from 'gi://Gio';
|
|
import GLib from 'gi://GLib';
|
|
|
|
import Gettext from 'gettext';
|
|
|
|
import Config from '../config.js';
|
|
|
|
|
|
/**
|
|
* Initialise and setup Gettext.
|
|
*/
|
|
export function setupGettext() {
|
|
// Init Gettext
|
|
String.prototype.format = imports.format.format;
|
|
Gettext.bindtextdomain(Config.APP_ID, Config.PACKAGE_LOCALEDIR);
|
|
globalThis._ = GLib.dgettext.bind(null, Config.APP_ID);
|
|
globalThis.ngettext = GLib.dngettext.bind(null, Config.APP_ID);
|
|
}
|
|
|
|
/**
|
|
* Get the contents of a GResource file, replacing `@PACKAGE_DATADIR@` where
|
|
* necessary.
|
|
*
|
|
* @param {string} relativePath - A path relative to GSConnect's resource path
|
|
* @returns {string} The file contents as a string
|
|
*/
|
|
function getResource(relativePath) {
|
|
try {
|
|
const bytes = Gio.resources_lookup_data(
|
|
GLib.build_filenamev([Config.APP_PATH, relativePath]),
|
|
Gio.ResourceLookupFlags.NONE
|
|
);
|
|
|
|
const source = new TextDecoder().decode(bytes.toArray());
|
|
|
|
return source.replace('@PACKAGE_DATADIR@', Config.PACKAGE_DATADIR);
|
|
} catch (e) {
|
|
logError(e, 'GSConnect');
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Install file contents, to an absolute directory path.
|
|
*
|
|
* @param {string} dirname - An absolute directory path
|
|
* @param {string} basename - The file name
|
|
* @param {string} contents - The file contents
|
|
* @returns {boolean} A success boolean
|
|
*/
|
|
function _installFile(dirname, basename, contents) {
|
|
try {
|
|
const filename = GLib.build_filenamev([dirname, basename]);
|
|
GLib.mkdir_with_parents(dirname, 0o755);
|
|
|
|
return GLib.file_set_contents(filename, contents);
|
|
} catch (e) {
|
|
logError(e, 'GSConnect');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Install file contents from a GResource, to an absolute directory path.
|
|
*
|
|
* @param {string} dirname - An absolute directory path
|
|
* @param {string} basename - The file name
|
|
* @param {string} relativePath - A path relative to GSConnect's resource path
|
|
* @returns {boolean} A success boolean
|
|
*/
|
|
function _installResource(dirname, basename, relativePath) {
|
|
try {
|
|
const contents = getResource(relativePath);
|
|
|
|
return _installFile(dirname, basename, contents);
|
|
} catch (e) {
|
|
logError(e, 'GSConnect');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Use Gio.File to ensure a file's executable bits are set.
|
|
*
|
|
* @param {string} filepath - An absolute path to a file
|
|
* @returns {boolean} - True if the file already was, or is now, executable
|
|
*/
|
|
function _setExecutable(filepath) {
|
|
try {
|
|
const file = Gio.File.new_for_path(filepath);
|
|
const finfo = file.query_info(
|
|
`${Gio.FILE_ATTRIBUTE_STANDARD_TYPE},${Gio.FILE_ATTRIBUTE_UNIX_MODE}`,
|
|
Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
|
|
null);
|
|
|
|
if (!finfo.has_attribute(Gio.FILE_ATTRIBUTE_UNIX_MODE))
|
|
return false;
|
|
|
|
const mode = finfo.get_attribute_uint32(
|
|
Gio.FILE_ATTRIBUTE_UNIX_MODE);
|
|
const new_mode = (mode | 0o111);
|
|
if (mode === new_mode)
|
|
return true;
|
|
|
|
return file.set_attribute_uint32(
|
|
Gio.FILE_ATTRIBUTE_UNIX_MODE,
|
|
new_mode,
|
|
Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
|
|
null);
|
|
} catch (e) {
|
|
logError(e, 'GSConnect');
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ensure critical files in the extension directory have the
|
|
* correct permissions.
|
|
*/
|
|
export function ensurePermissions() {
|
|
if (Config.IS_USER) {
|
|
const executableFiles = [
|
|
'gsconnect-preferences',
|
|
'service/daemon.js',
|
|
'service/nativeMessagingHost.js',
|
|
];
|
|
for (const file of executableFiles)
|
|
_setExecutable(GLib.build_filenamev([Config.PACKAGE_DATADIR, file]));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Install the files necessary for the GSConnect service to run.
|
|
*/
|
|
export function installService() {
|
|
const settings = new Gio.Settings({
|
|
settings_schema: Config.GSCHEMA.lookup(
|
|
'org.gnome.Shell.Extensions.GSConnect',
|
|
null
|
|
),
|
|
path: '/org/gnome/shell/extensions/gsconnect/',
|
|
});
|
|
|
|
const confDir = GLib.get_user_config_dir();
|
|
const dataDir = GLib.get_user_data_dir();
|
|
const homeDir = GLib.get_home_dir();
|
|
|
|
// DBus Service
|
|
const dbusDir = GLib.build_filenamev([dataDir, 'dbus-1', 'services']);
|
|
const dbusFile = `${Config.APP_ID}.service`;
|
|
|
|
// Desktop Entry
|
|
const appDir = GLib.build_filenamev([dataDir, 'applications']);
|
|
const appFile = `${Config.APP_ID}.desktop`;
|
|
const appPrefsFile = `${Config.APP_ID}.Preferences.desktop`;
|
|
|
|
// Application Icon
|
|
const iconDir = GLib.build_filenamev([dataDir, 'icons', 'hicolor', 'scalable', 'apps']);
|
|
const iconFull = `${Config.APP_ID}.svg`;
|
|
const iconSym = `${Config.APP_ID}-symbolic.svg`;
|
|
|
|
// File Manager Extensions
|
|
const fileManagers = [
|
|
[`${dataDir}/nautilus-python/extensions`, 'nautilus-gsconnect.py'],
|
|
[`${dataDir}/nemo-python/extensions`, 'nemo-gsconnect.py'],
|
|
];
|
|
|
|
// WebExtension Manifests
|
|
const manifestFile = 'org.gnome.shell.extensions.gsconnect.json';
|
|
const google = getResource(`webextension/${manifestFile}.google.in`);
|
|
const mozilla = getResource(`webextension/${manifestFile}.mozilla.in`);
|
|
const manifests = [
|
|
[`${confDir}/chromium/NativeMessagingHosts/`, google],
|
|
[`${confDir}/google-chrome/NativeMessagingHosts/`, google],
|
|
[`${confDir}/google-chrome-beta/NativeMessagingHosts/`, google],
|
|
[`${confDir}/google-chrome-unstable/NativeMessagingHosts/`, google],
|
|
[`${confDir}/BraveSoftware/Brave-Browser/NativeMessagingHosts/`, google],
|
|
[`${confDir}/BraveSoftware/Brave-Browser-Beta/NativeMessagingHosts/`, google],
|
|
[`${confDir}/BraveSoftware/Brave-Browser-Nightly/NativeMessagingHosts/`, google],
|
|
[`${homeDir}/.mozilla/native-messaging-hosts/`, mozilla],
|
|
[`${homeDir}/.config/microsoft-edge-dev/NativeMessagingHosts`, google],
|
|
[`${homeDir}/.config/microsoft-edge-beta/NativeMessagingHosts`, google],
|
|
];
|
|
|
|
// If running as a user extension, ensure the DBus service, desktop entry,
|
|
// file manager scripts, and WebExtension manifests are installed.
|
|
if (Config.IS_USER) {
|
|
// DBus Service
|
|
if (!_installResource(dbusDir, dbusFile, `${dbusFile}.in`))
|
|
throw Error('GSConnect: Failed to install DBus Service');
|
|
|
|
// Desktop Entries
|
|
_installResource(appDir, appFile, appFile);
|
|
_installResource(appDir, appPrefsFile, appPrefsFile);
|
|
|
|
// Application Icon
|
|
_installResource(iconDir, iconFull, `icons/${iconFull}`);
|
|
_installResource(iconDir, iconSym, `icons/${iconSym}`);
|
|
|
|
// File Manager Extensions
|
|
const target = `${Config.PACKAGE_DATADIR}/nautilus-gsconnect.py`;
|
|
|
|
for (const [dir, name] of fileManagers) {
|
|
const script = Gio.File.new_for_path(GLib.build_filenamev([dir, name]));
|
|
|
|
if (!script.query_exists(null)) {
|
|
GLib.mkdir_with_parents(dir, 0o755);
|
|
script.make_symbolic_link(target, null);
|
|
}
|
|
}
|
|
|
|
// WebExtension Manifests
|
|
if (settings.get_boolean('create-native-messaging-hosts')) {
|
|
for (const [dirname, contents] of manifests)
|
|
_installFile(dirname, manifestFile, contents);
|
|
}
|
|
|
|
// Otherwise, if running as a system extension, ensure anything previously
|
|
// installed when running as a user extension is removed.
|
|
} else {
|
|
GLib.unlink(GLib.build_filenamev([dbusDir, dbusFile]));
|
|
GLib.unlink(GLib.build_filenamev([appDir, appFile]));
|
|
GLib.unlink(GLib.build_filenamev([appDir, appPrefsFile]));
|
|
GLib.unlink(GLib.build_filenamev([iconDir, iconFull]));
|
|
GLib.unlink(GLib.build_filenamev([iconDir, iconSym]));
|
|
|
|
for (const [dir, name] of fileManagers)
|
|
GLib.unlink(GLib.build_filenamev([dir, name]));
|
|
|
|
for (const manifest of manifests)
|
|
GLib.unlink(GLib.build_filenamev([manifest[0], manifestFile]));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initialise and setup Config, GResources and GSchema.
|
|
*
|
|
* @param {string} extensionPath - The absolute path to the extension directory
|
|
*/
|
|
export function setup(extensionPath) {
|
|
// Ensure config.js is setup properly
|
|
Config.PACKAGE_DATADIR = extensionPath;
|
|
const userDir = GLib.build_filenamev([GLib.get_user_data_dir(), 'gnome-shell']);
|
|
|
|
if (Config.PACKAGE_DATADIR.startsWith(userDir)) {
|
|
Config.IS_USER = true;
|
|
|
|
Config.GSETTINGS_SCHEMA_DIR = `${Config.PACKAGE_DATADIR}/schemas`;
|
|
Config.PACKAGE_LOCALEDIR = `${Config.PACKAGE_DATADIR}/locale`;
|
|
}
|
|
|
|
// Init GResources
|
|
Gio.Resource.load(
|
|
GLib.build_filenamev([Config.PACKAGE_DATADIR, `${Config.APP_ID}.gresource`])
|
|
)._register();
|
|
|
|
// Init GSchema
|
|
Config.GSCHEMA = Gio.SettingsSchemaSource.new_from_directory(
|
|
Config.GSETTINGS_SCHEMA_DIR,
|
|
Gio.SettingsSchemaSource.get_default(),
|
|
false
|
|
);
|
|
}
|