setup.js 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. // SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
  2. //
  3. // SPDX-License-Identifier: GPL-2.0-or-later
  4. import Gio from 'gi://Gio';
  5. import GLib from 'gi://GLib';
  6. import Gettext from 'gettext';
  7. import Config from '../config.js';
  8. /**
  9. * Initialise and setup Gettext.
  10. */
  11. export function setupGettext() {
  12. // Init Gettext
  13. String.prototype.format = imports.format.format;
  14. Gettext.bindtextdomain(Config.APP_ID, Config.PACKAGE_LOCALEDIR);
  15. globalThis._ = GLib.dgettext.bind(null, Config.APP_ID);
  16. globalThis.ngettext = GLib.dngettext.bind(null, Config.APP_ID);
  17. }
  18. /**
  19. * Get the contents of a GResource file, replacing `@PACKAGE_DATADIR@` where
  20. * necessary.
  21. *
  22. * @param {string} relativePath - A path relative to GSConnect's resource path
  23. * @returns {string} The file contents as a string
  24. */
  25. function getResource(relativePath) {
  26. try {
  27. const bytes = Gio.resources_lookup_data(
  28. GLib.build_filenamev([Config.APP_PATH, relativePath]),
  29. Gio.ResourceLookupFlags.NONE
  30. );
  31. const source = new TextDecoder().decode(bytes.toArray());
  32. return source.replace('@PACKAGE_DATADIR@', Config.PACKAGE_DATADIR);
  33. } catch (e) {
  34. logError(e, 'GSConnect');
  35. return null;
  36. }
  37. }
  38. /**
  39. * Install file contents, to an absolute directory path.
  40. *
  41. * @param {string} dirname - An absolute directory path
  42. * @param {string} basename - The file name
  43. * @param {string} contents - The file contents
  44. * @returns {boolean} A success boolean
  45. */
  46. function _installFile(dirname, basename, contents) {
  47. try {
  48. const filename = GLib.build_filenamev([dirname, basename]);
  49. GLib.mkdir_with_parents(dirname, 0o755);
  50. return GLib.file_set_contents(filename, contents);
  51. } catch (e) {
  52. logError(e, 'GSConnect');
  53. return false;
  54. }
  55. }
  56. /**
  57. * Install file contents from a GResource, to an absolute directory path.
  58. *
  59. * @param {string} dirname - An absolute directory path
  60. * @param {string} basename - The file name
  61. * @param {string} relativePath - A path relative to GSConnect's resource path
  62. * @returns {boolean} A success boolean
  63. */
  64. function _installResource(dirname, basename, relativePath) {
  65. try {
  66. const contents = getResource(relativePath);
  67. return _installFile(dirname, basename, contents);
  68. } catch (e) {
  69. logError(e, 'GSConnect');
  70. return false;
  71. }
  72. }
  73. /**
  74. * Use Gio.File to ensure a file's executable bits are set.
  75. *
  76. * @param {string} filepath - An absolute path to a file
  77. * @returns {boolean} - True if the file already was, or is now, executable
  78. */
  79. function _setExecutable(filepath) {
  80. try {
  81. const file = Gio.File.new_for_path(filepath);
  82. const finfo = file.query_info(
  83. `${Gio.FILE_ATTRIBUTE_STANDARD_TYPE},${Gio.FILE_ATTRIBUTE_UNIX_MODE}`,
  84. Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
  85. null);
  86. if (!finfo.has_attribute(Gio.FILE_ATTRIBUTE_UNIX_MODE))
  87. return false;
  88. const mode = finfo.get_attribute_uint32(
  89. Gio.FILE_ATTRIBUTE_UNIX_MODE);
  90. const new_mode = (mode | 0o111);
  91. if (mode === new_mode)
  92. return true;
  93. return file.set_attribute_uint32(
  94. Gio.FILE_ATTRIBUTE_UNIX_MODE,
  95. new_mode,
  96. Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
  97. null);
  98. } catch (e) {
  99. logError(e, 'GSConnect');
  100. return false;
  101. }
  102. }
  103. /**
  104. * Ensure critical files in the extension directory have the
  105. * correct permissions.
  106. */
  107. export function ensurePermissions() {
  108. if (Config.IS_USER) {
  109. const executableFiles = [
  110. 'gsconnect-preferences',
  111. 'service/daemon.js',
  112. 'service/nativeMessagingHost.js',
  113. ];
  114. for (const file of executableFiles)
  115. _setExecutable(GLib.build_filenamev([Config.PACKAGE_DATADIR, file]));
  116. }
  117. }
  118. /**
  119. * Install the files necessary for the GSConnect service to run.
  120. */
  121. export function installService() {
  122. const settings = new Gio.Settings({
  123. settings_schema: Config.GSCHEMA.lookup(
  124. 'org.gnome.Shell.Extensions.GSConnect',
  125. null
  126. ),
  127. path: '/org/gnome/shell/extensions/gsconnect/',
  128. });
  129. const confDir = GLib.get_user_config_dir();
  130. const dataDir = GLib.get_user_data_dir();
  131. const homeDir = GLib.get_home_dir();
  132. // DBus Service
  133. const dbusDir = GLib.build_filenamev([dataDir, 'dbus-1', 'services']);
  134. const dbusFile = `${Config.APP_ID}.service`;
  135. // Desktop Entry
  136. const appDir = GLib.build_filenamev([dataDir, 'applications']);
  137. const appFile = `${Config.APP_ID}.desktop`;
  138. const appPrefsFile = `${Config.APP_ID}.Preferences.desktop`;
  139. // Application Icon
  140. const iconDir = GLib.build_filenamev([dataDir, 'icons', 'hicolor', 'scalable', 'apps']);
  141. const iconFull = `${Config.APP_ID}.svg`;
  142. const iconSym = `${Config.APP_ID}-symbolic.svg`;
  143. // File Manager Extensions
  144. const fileManagers = [
  145. [`${dataDir}/nautilus-python/extensions`, 'nautilus-gsconnect.py'],
  146. [`${dataDir}/nemo-python/extensions`, 'nemo-gsconnect.py'],
  147. ];
  148. // WebExtension Manifests
  149. const manifestFile = 'org.gnome.shell.extensions.gsconnect.json';
  150. const google = getResource(`webextension/${manifestFile}.google.in`);
  151. const mozilla = getResource(`webextension/${manifestFile}.mozilla.in`);
  152. const manifests = [
  153. [`${confDir}/chromium/NativeMessagingHosts/`, google],
  154. [`${confDir}/google-chrome/NativeMessagingHosts/`, google],
  155. [`${confDir}/google-chrome-beta/NativeMessagingHosts/`, google],
  156. [`${confDir}/google-chrome-unstable/NativeMessagingHosts/`, google],
  157. [`${confDir}/BraveSoftware/Brave-Browser/NativeMessagingHosts/`, google],
  158. [`${confDir}/BraveSoftware/Brave-Browser-Beta/NativeMessagingHosts/`, google],
  159. [`${confDir}/BraveSoftware/Brave-Browser-Nightly/NativeMessagingHosts/`, google],
  160. [`${homeDir}/.mozilla/native-messaging-hosts/`, mozilla],
  161. [`${homeDir}/.config/microsoft-edge-dev/NativeMessagingHosts`, google],
  162. [`${homeDir}/.config/microsoft-edge-beta/NativeMessagingHosts`, google],
  163. ];
  164. // If running as a user extension, ensure the DBus service, desktop entry,
  165. // file manager scripts, and WebExtension manifests are installed.
  166. if (Config.IS_USER) {
  167. // DBus Service
  168. if (!_installResource(dbusDir, dbusFile, `${dbusFile}.in`))
  169. throw Error('GSConnect: Failed to install DBus Service');
  170. // Desktop Entries
  171. _installResource(appDir, appFile, appFile);
  172. _installResource(appDir, appPrefsFile, appPrefsFile);
  173. // Application Icon
  174. _installResource(iconDir, iconFull, `icons/${iconFull}`);
  175. _installResource(iconDir, iconSym, `icons/${iconSym}`);
  176. // File Manager Extensions
  177. const target = `${Config.PACKAGE_DATADIR}/nautilus-gsconnect.py`;
  178. for (const [dir, name] of fileManagers) {
  179. const script = Gio.File.new_for_path(GLib.build_filenamev([dir, name]));
  180. if (!script.query_exists(null)) {
  181. GLib.mkdir_with_parents(dir, 0o755);
  182. script.make_symbolic_link(target, null);
  183. }
  184. }
  185. // WebExtension Manifests
  186. if (settings.get_boolean('create-native-messaging-hosts')) {
  187. for (const [dirname, contents] of manifests)
  188. _installFile(dirname, manifestFile, contents);
  189. }
  190. // Otherwise, if running as a system extension, ensure anything previously
  191. // installed when running as a user extension is removed.
  192. } else {
  193. GLib.unlink(GLib.build_filenamev([dbusDir, dbusFile]));
  194. GLib.unlink(GLib.build_filenamev([appDir, appFile]));
  195. GLib.unlink(GLib.build_filenamev([appDir, appPrefsFile]));
  196. GLib.unlink(GLib.build_filenamev([iconDir, iconFull]));
  197. GLib.unlink(GLib.build_filenamev([iconDir, iconSym]));
  198. for (const [dir, name] of fileManagers)
  199. GLib.unlink(GLib.build_filenamev([dir, name]));
  200. for (const manifest of manifests)
  201. GLib.unlink(GLib.build_filenamev([manifest[0], manifestFile]));
  202. }
  203. }
  204. /**
  205. * Initialise and setup Config, GResources and GSchema.
  206. *
  207. * @param {string} extensionPath - The absolute path to the extension directory
  208. */
  209. export function setup(extensionPath) {
  210. // Ensure config.js is setup properly
  211. Config.PACKAGE_DATADIR = extensionPath;
  212. const userDir = GLib.build_filenamev([GLib.get_user_data_dir(), 'gnome-shell']);
  213. if (Config.PACKAGE_DATADIR.startsWith(userDir)) {
  214. Config.IS_USER = true;
  215. Config.GSETTINGS_SCHEMA_DIR = `${Config.PACKAGE_DATADIR}/schemas`;
  216. Config.PACKAGE_LOCALEDIR = `${Config.PACKAGE_DATADIR}/locale`;
  217. }
  218. // Init GResources
  219. Gio.Resource.load(
  220. GLib.build_filenamev([Config.PACKAGE_DATADIR, `${Config.APP_ID}.gresource`])
  221. )._register();
  222. // Init GSchema
  223. Config.GSCHEMA = Gio.SettingsSchemaSource.new_from_directory(
  224. Config.GSETTINGS_SCHEMA_DIR,
  225. Gio.SettingsSchemaSource.get_default(),
  226. false
  227. );
  228. }