2011-09-28 13:16:26 +00:00
|
|
|
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
2019-01-31 14:07:06 +00:00
|
|
|
/* exported findUrls, spawn, spawnCommandLine, spawnApp, trySpawnCommandLine,
|
2023-07-10 04:58:21 +00:00
|
|
|
createTimeLabel, insertSorted, lerp, GNOMEversionCompare,
|
|
|
|
DBusSenderChecker, Highlighter */
|
2010-10-03 19:48:56 +00:00
|
|
|
|
2023-06-08 04:52:46 +00:00
|
|
|
const Gio = imports.gi.Gio;
|
|
|
|
const GLib = imports.gi.GLib;
|
|
|
|
const Shell = imports.gi.Shell;
|
|
|
|
const St = imports.gi.St;
|
|
|
|
const GnomeDesktop = imports.gi.GnomeDesktop;
|
2010-11-17 16:43:08 +00:00
|
|
|
|
|
|
|
const Main = imports.ui.main;
|
2023-07-08 00:58:11 +00:00
|
|
|
const {formatTime} = imports.misc.dateUtils;
|
2013-03-11 17:43:38 +00:00
|
|
|
|
2011-04-13 13:18:00 +00:00
|
|
|
// http://daringfireball.net/2010/07/improved_regex_for_matching_urls
|
2018-02-27 12:20:02 +00:00
|
|
|
const _balancedParens = '\\([^\\s()<>]+\\)';
|
2011-04-13 13:40:28 +00:00
|
|
|
const _leadingJunk = '[\\s`(\\[{\'\\"<\u00AB\u201C\u2018]';
|
2019-09-13 22:03:02 +00:00
|
|
|
const _notTrailingJunk = '[^\\s`!()\\[\\]{};:\'\\".,<>?\u00AB\u00BB\u200E\u200F\u201C\u201D\u2018\u2019\u202A\u202C]';
|
2011-04-13 13:18:00 +00:00
|
|
|
|
|
|
|
const _urlRegexp = new RegExp(
|
2022-02-07 14:14:06 +00:00
|
|
|
`(^|${_leadingJunk})` +
|
2011-04-13 13:40:28 +00:00
|
|
|
'(' +
|
2011-04-13 13:18:00 +00:00
|
|
|
'(?:' +
|
2013-04-14 20:15:04 +00:00
|
|
|
'(?:http|https|ftp)://' + // scheme://
|
2011-04-13 13:18:00 +00:00
|
|
|
'|' +
|
|
|
|
'www\\d{0,3}[.]' + // www.
|
|
|
|
'|' +
|
|
|
|
'[a-z0-9.\\-]+[.][a-z]{2,4}/' + // foo.xx/
|
|
|
|
')' +
|
|
|
|
'(?:' + // one or more:
|
|
|
|
'[^\\s()<>]+' + // run of non-space non-()
|
|
|
|
'|' + // or
|
2022-02-07 14:14:06 +00:00
|
|
|
`${_balancedParens}` + // balanced parens
|
2011-04-13 13:18:00 +00:00
|
|
|
')+' +
|
|
|
|
'(?:' + // end with:
|
2022-02-07 14:14:06 +00:00
|
|
|
`${_balancedParens}` + // balanced parens
|
2011-04-13 13:18:00 +00:00
|
|
|
'|' + // or
|
2022-02-07 14:14:06 +00:00
|
|
|
`${_notTrailingJunk}` + // last non-junk char
|
2011-04-13 13:18:00 +00:00
|
|
|
')' +
|
|
|
|
')', 'gi');
|
2010-10-03 19:48:56 +00:00
|
|
|
|
2015-02-25 15:43:59 +00:00
|
|
|
let _desktopSettings = null;
|
|
|
|
|
2010-10-03 19:48:56 +00:00
|
|
|
// findUrls:
|
|
|
|
// @str: string to find URLs in
|
|
|
|
//
|
|
|
|
// Searches @str for URLs and returns an array of objects with %url
|
|
|
|
// properties showing the matched URL string, and %pos properties indicating
|
|
|
|
// the position within @str where the URL was found.
|
|
|
|
//
|
|
|
|
// Return value: the list of match objects, as described above
|
|
|
|
function findUrls(str) {
|
|
|
|
let res = [], match;
|
|
|
|
while ((match = _urlRegexp.exec(str)))
|
2011-04-13 13:40:28 +00:00
|
|
|
res.push({ url: match[2], pos: match.index + match[1].length });
|
2010-10-03 19:48:56 +00:00
|
|
|
return res;
|
|
|
|
}
|
2010-11-17 16:43:08 +00:00
|
|
|
|
|
|
|
// spawn:
|
|
|
|
// @argv: an argv array
|
|
|
|
//
|
|
|
|
// Runs @argv in the background, handling any errors that occur
|
|
|
|
// when trying to start the program.
|
|
|
|
function spawn(argv) {
|
|
|
|
try {
|
|
|
|
trySpawn(argv);
|
|
|
|
} catch (err) {
|
|
|
|
_handleSpawnError(argv[0], err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// spawnCommandLine:
|
2019-01-31 13:43:52 +00:00
|
|
|
// @commandLine: a command line
|
2010-11-17 16:43:08 +00:00
|
|
|
//
|
2019-01-31 13:43:52 +00:00
|
|
|
// Runs @commandLine in the background, handling any errors that
|
2010-11-17 16:43:08 +00:00
|
|
|
// occur when trying to parse or start the program.
|
2019-01-31 13:43:52 +00:00
|
|
|
function spawnCommandLine(commandLine) {
|
2010-11-17 16:43:08 +00:00
|
|
|
try {
|
2019-01-31 14:08:00 +00:00
|
|
|
let [success_, argv] = GLib.shell_parse_argv(commandLine);
|
2010-11-17 16:43:08 +00:00
|
|
|
trySpawn(argv);
|
|
|
|
} catch (err) {
|
2019-01-31 13:43:52 +00:00
|
|
|
_handleSpawnError(commandLine, err);
|
2010-11-17 16:43:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2013-10-02 17:39:58 +00:00
|
|
|
// spawnApp:
|
|
|
|
// @argv: an argv array
|
|
|
|
//
|
|
|
|
// Runs @argv as if it was an application, handling startup notification
|
|
|
|
function spawnApp(argv) {
|
|
|
|
try {
|
|
|
|
let app = Gio.AppInfo.create_from_commandline(argv.join(' '), null,
|
|
|
|
Gio.AppInfoCreateFlags.SUPPORTS_STARTUP_NOTIFICATION);
|
|
|
|
|
2014-01-19 17:34:32 +00:00
|
|
|
let context = global.create_app_launch_context(0, -1);
|
2017-07-15 01:42:06 +00:00
|
|
|
app.launch([], context);
|
2019-01-29 01:26:39 +00:00
|
|
|
} catch (err) {
|
2013-10-02 17:39:58 +00:00
|
|
|
_handleSpawnError(argv[0], err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-11-17 16:43:08 +00:00
|
|
|
// trySpawn:
|
|
|
|
// @argv: an argv array
|
|
|
|
//
|
|
|
|
// Runs @argv in the background. If launching @argv fails,
|
|
|
|
// this will throw an error.
|
2019-01-29 21:02:57 +00:00
|
|
|
function trySpawn(argv) {
|
2019-01-31 14:08:00 +00:00
|
|
|
var success_, pid;
|
2010-11-17 16:43:08 +00:00
|
|
|
try {
|
2022-01-19 09:15:51 +00:00
|
|
|
[success_, pid] = GLib.spawn_async(
|
|
|
|
null, argv, null,
|
|
|
|
GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD,
|
|
|
|
() => {
|
|
|
|
try {
|
|
|
|
global.context.restore_rlimit_nofile();
|
|
|
|
} catch (err) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
);
|
2010-11-17 16:43:08 +00:00
|
|
|
} catch (err) {
|
2012-06-19 21:32:45 +00:00
|
|
|
/* Rewrite the error in case of ENOENT */
|
|
|
|
if (err.matches(GLib.SpawnError, GLib.SpawnError.NOENT)) {
|
2020-03-29 21:51:13 +00:00
|
|
|
throw new GLib.SpawnError({
|
|
|
|
code: GLib.SpawnError.NOENT,
|
|
|
|
message: _('Command not found'),
|
|
|
|
});
|
2012-06-19 21:32:45 +00:00
|
|
|
} else if (err instanceof GLib.Error) {
|
2011-01-27 20:26:58 +00:00
|
|
|
// The exception from gjs contains an error string like:
|
|
|
|
// Error invoking GLib.spawn_command_line_async: Failed to
|
|
|
|
// execute child process "foo" (No such file or directory)
|
|
|
|
// We are only interested in the part in the parentheses. (And
|
|
|
|
// we can't pattern match the text, since it gets localized.)
|
2012-06-19 21:32:45 +00:00
|
|
|
let message = err.message.replace(/.*\((.+)\)/, '$1');
|
2019-08-19 19:38:51 +00:00
|
|
|
throw new err.constructor({ code: err.code, message });
|
2012-06-19 21:32:45 +00:00
|
|
|
} else {
|
|
|
|
throw err;
|
2011-01-27 20:26:58 +00:00
|
|
|
}
|
2010-11-17 16:43:08 +00:00
|
|
|
}
|
2019-11-27 15:35:45 +00:00
|
|
|
|
|
|
|
// Async call, we don't need the reply though
|
|
|
|
GnomeDesktop.start_systemd_scope(argv[0], pid, null, null, null, () => {});
|
|
|
|
|
2012-05-10 01:23:19 +00:00
|
|
|
// Dummy child watch; we don't want to double-fork internally
|
|
|
|
// because then we lose the parent-child relationship, which
|
|
|
|
// can break polkit. See https://bugzilla.redhat.com//show_bug.cgi?id=819275
|
2017-10-31 00:38:18 +00:00
|
|
|
GLib.child_watch_add(GLib.PRIORITY_DEFAULT, pid, () => {});
|
2010-11-17 16:43:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// trySpawnCommandLine:
|
2019-01-31 13:43:52 +00:00
|
|
|
// @commandLine: a command line
|
2010-11-17 16:43:08 +00:00
|
|
|
//
|
2019-01-31 13:43:52 +00:00
|
|
|
// Runs @commandLine in the background. If launching @commandLine
|
2010-11-17 16:43:08 +00:00
|
|
|
// fails, this will throw an error.
|
2019-01-31 13:43:52 +00:00
|
|
|
function trySpawnCommandLine(commandLine) {
|
2019-01-31 14:08:00 +00:00
|
|
|
let success_, argv;
|
2010-11-17 16:43:08 +00:00
|
|
|
|
|
|
|
try {
|
2019-01-31 14:08:00 +00:00
|
|
|
[success_, argv] = GLib.shell_parse_argv(commandLine);
|
2010-11-17 16:43:08 +00:00
|
|
|
} catch (err) {
|
|
|
|
// Replace "Error invoking GLib.shell_parse_argv: " with
|
|
|
|
// something nicer
|
2022-02-07 14:14:06 +00:00
|
|
|
err.message = err.message.replace(/[^:]*: /, `${_('Could not parse command:')}\n`);
|
2010-11-17 16:43:08 +00:00
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
|
|
|
|
trySpawn(argv);
|
|
|
|
}
|
|
|
|
|
|
|
|
function _handleSpawnError(command, err) {
|
2014-01-17 21:30:49 +00:00
|
|
|
let title = _("Execution of “%s” failed:").format(command);
|
2011-03-19 17:59:22 +00:00
|
|
|
Main.notifyError(title, err.message);
|
2010-11-17 16:43:08 +00:00
|
|
|
}
|
2010-12-17 20:30:12 +00:00
|
|
|
|
2015-02-25 22:23:49 +00:00
|
|
|
function createTimeLabel(date, params) {
|
|
|
|
if (_desktopSettings == null)
|
|
|
|
_desktopSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.interface' });
|
|
|
|
|
|
|
|
let label = new St.Label({ text: formatTime(date, params) });
|
2021-08-15 22:36:59 +00:00
|
|
|
_desktopSettings.connectObject(
|
|
|
|
'changed::clock-format', () => (label.text = formatTime(date, params)),
|
|
|
|
label);
|
2015-02-25 22:23:49 +00:00
|
|
|
return label;
|
|
|
|
}
|
|
|
|
|
2011-12-17 22:52:11 +00:00
|
|
|
// lowerBound:
|
|
|
|
// @array: an array or array-like object, already sorted
|
|
|
|
// according to @cmp
|
|
|
|
// @val: the value to add
|
|
|
|
// @cmp: a comparator (or undefined to compare as numbers)
|
|
|
|
//
|
|
|
|
// Returns the position of the first element that is not
|
|
|
|
// lower than @val, according to @cmp.
|
|
|
|
// That is, returns the first position at which it
|
|
|
|
// is possible to insert @val without violating the
|
|
|
|
// order.
|
|
|
|
// This is quite like an ordinary binary search, except
|
|
|
|
// that it doesn't stop at first element comparing equal.
|
|
|
|
|
|
|
|
function lowerBound(array, val, cmp) {
|
|
|
|
let min, max, mid, v;
|
2022-01-18 20:02:04 +00:00
|
|
|
cmp ||= (a, b) => a - b;
|
2011-12-17 22:52:11 +00:00
|
|
|
|
|
|
|
if (array.length == 0)
|
|
|
|
return 0;
|
|
|
|
|
2019-08-19 19:13:52 +00:00
|
|
|
min = 0;
|
|
|
|
max = array.length;
|
2011-12-17 22:52:11 +00:00
|
|
|
while (min < (max - 1)) {
|
|
|
|
mid = Math.floor((min + max) / 2);
|
|
|
|
v = cmp(array[mid], val);
|
|
|
|
|
|
|
|
if (v < 0)
|
|
|
|
min = mid + 1;
|
|
|
|
else
|
|
|
|
max = mid;
|
|
|
|
}
|
|
|
|
|
2019-08-19 19:38:51 +00:00
|
|
|
return min == max || cmp(array[min], val) < 0 ? max : min;
|
2011-12-17 22:52:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// insertSorted:
|
|
|
|
// @array: an array sorted according to @cmp
|
|
|
|
// @val: a value to insert
|
|
|
|
// @cmp: the sorting function
|
|
|
|
//
|
|
|
|
// Inserts @val into @array, preserving the
|
|
|
|
// sorting invariants.
|
|
|
|
// Returns the position at which it was inserted
|
|
|
|
function insertSorted(array, val, cmp) {
|
|
|
|
let pos = lowerBound(array, val, cmp);
|
|
|
|
array.splice(pos, 0, val);
|
|
|
|
|
|
|
|
return pos;
|
|
|
|
}
|
2013-02-18 20:35:02 +00:00
|
|
|
|
2020-12-31 17:18:42 +00:00
|
|
|
function lerp(start, end, progress) {
|
|
|
|
return start + progress * (end - start);
|
|
|
|
}
|
2021-02-04 11:26:15 +00:00
|
|
|
|
|
|
|
// _GNOMEversionToNumber:
|
|
|
|
// @version: a GNOME version element
|
|
|
|
//
|
|
|
|
// Like Number() but returns sortable values for special-cases
|
|
|
|
// 'alpha' and 'beta'. Returns NaN for unhandled 'versions'.
|
|
|
|
function _GNOMEversionToNumber(version) {
|
|
|
|
let ret = Number(version);
|
|
|
|
if (!isNaN(ret))
|
|
|
|
return ret;
|
|
|
|
if (version === 'alpha')
|
|
|
|
return -2;
|
|
|
|
if (version === 'beta')
|
|
|
|
return -1;
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
// GNOMEversionCompare:
|
|
|
|
// @version1: a string containing a GNOME version
|
|
|
|
// @version2: a string containing another GNOME version
|
|
|
|
//
|
|
|
|
// Returns an integer less than, equal to, or greater than
|
|
|
|
// zero, if version1 is older, equal or newer than version2
|
|
|
|
function GNOMEversionCompare(version1, version2) {
|
|
|
|
const v1Array = version1.split('.');
|
|
|
|
const v2Array = version2.split('.');
|
|
|
|
|
|
|
|
for (let i = 0; i < Math.max(v1Array.length, v2Array.length); i++) {
|
|
|
|
let elemV1 = _GNOMEversionToNumber(v1Array[i] || '0');
|
|
|
|
let elemV2 = _GNOMEversionToNumber(v2Array[i] || '0');
|
|
|
|
if (elemV1 < elemV2)
|
|
|
|
return -1;
|
|
|
|
if (elemV1 > elemV2)
|
|
|
|
return 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
return 0;
|
|
|
|
}
|
2021-06-16 17:09:42 +00:00
|
|
|
|
|
|
|
var DBusSenderChecker = class {
|
|
|
|
/**
|
|
|
|
* @param {string[]} allowList - list of allowed well-known names
|
|
|
|
*/
|
|
|
|
constructor(allowList) {
|
|
|
|
this._allowlistMap = new Map();
|
|
|
|
|
2021-11-23 01:48:04 +00:00
|
|
|
this._uninitializedNames = new Set(allowList);
|
|
|
|
this._initializedPromise = new Promise(resolve => {
|
|
|
|
this._resolveInitialized = resolve;
|
|
|
|
});
|
|
|
|
|
2021-06-16 17:09:42 +00:00
|
|
|
this._watchList = allowList.map(name => {
|
|
|
|
return Gio.DBus.watch_name(Gio.BusType.SESSION,
|
|
|
|
name,
|
|
|
|
Gio.BusNameWatcherFlags.NONE,
|
2021-11-23 01:48:04 +00:00
|
|
|
(conn_, name_, owner) => {
|
|
|
|
this._allowlistMap.set(name, owner);
|
|
|
|
this._checkAndResolveInitialized(name);
|
|
|
|
},
|
|
|
|
() => {
|
|
|
|
this._allowlistMap.delete(name);
|
|
|
|
this._checkAndResolveInitialized(name);
|
|
|
|
});
|
2021-06-16 17:09:42 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-11-23 01:48:04 +00:00
|
|
|
* @param {string} name - bus name for which the watcher got initialized
|
|
|
|
*/
|
|
|
|
_checkAndResolveInitialized(name) {
|
|
|
|
if (this._uninitializedNames.delete(name) &&
|
|
|
|
this._uninitializedNames.size === 0)
|
|
|
|
this._resolveInitialized();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @async
|
2021-06-16 17:09:42 +00:00
|
|
|
* @param {string} sender - the bus name that invoked the checked method
|
|
|
|
* @returns {bool}
|
|
|
|
*/
|
2021-11-23 01:48:04 +00:00
|
|
|
async _isSenderAllowed(sender) {
|
|
|
|
await this._initializedPromise;
|
2021-06-16 17:09:42 +00:00
|
|
|
return [...this._allowlistMap.values()].includes(sender);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check whether the bus name that invoked @invocation maps
|
|
|
|
* to an entry in the allow list.
|
|
|
|
*
|
2021-11-23 01:48:04 +00:00
|
|
|
* @async
|
2021-06-16 17:09:42 +00:00
|
|
|
* @throws
|
|
|
|
* @param {Gio.DBusMethodInvocation} invocation - the invocation
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
2021-11-23 01:48:04 +00:00
|
|
|
async checkInvocation(invocation) {
|
2021-06-16 17:09:42 +00:00
|
|
|
if (global.context.unsafe_mode)
|
|
|
|
return;
|
|
|
|
|
2021-11-23 01:48:04 +00:00
|
|
|
if (await this._isSenderAllowed(invocation.get_sender()))
|
2021-06-16 17:09:42 +00:00
|
|
|
return;
|
|
|
|
|
|
|
|
throw new GLib.Error(Gio.DBusError,
|
|
|
|
Gio.DBusError.ACCESS_DENIED,
|
2022-02-07 14:14:06 +00:00
|
|
|
`${invocation.get_method_name()} is not allowed`);
|
2021-06-16 17:09:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @returns {void}
|
|
|
|
*/
|
|
|
|
destroy() {
|
|
|
|
for (const id in this._watchList)
|
|
|
|
Gio.DBus.unwatch_name(id);
|
|
|
|
this._watchList = [];
|
|
|
|
}
|
|
|
|
};
|
2021-11-16 17:57:26 +00:00
|
|
|
|
|
|
|
/* @class Highlighter Highlight given terms in text using markup. */
|
|
|
|
var Highlighter = class {
|
|
|
|
/**
|
|
|
|
* @param {?string[]} terms - list of terms to highlight
|
|
|
|
*/
|
|
|
|
constructor(terms) {
|
|
|
|
if (!terms)
|
|
|
|
return;
|
|
|
|
|
|
|
|
const escapedTerms = terms
|
|
|
|
.map(term => Shell.util_regex_escape(term))
|
|
|
|
.filter(term => term.length > 0);
|
|
|
|
|
|
|
|
if (escapedTerms.length === 0)
|
|
|
|
return;
|
|
|
|
|
2022-02-07 14:14:06 +00:00
|
|
|
this._highlightRegex = new RegExp(
|
|
|
|
`(${escapedTerms.join('|')})`, 'gi');
|
2021-11-16 17:57:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Highlight all occurences of the terms defined for this
|
|
|
|
* highlighter in the provided text using markup.
|
|
|
|
*
|
|
|
|
* @param {string} text - text to highlight the defined terms in
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
|
|
|
highlight(text) {
|
|
|
|
if (!this._highlightRegex)
|
2021-11-17 01:50:39 +00:00
|
|
|
return GLib.markup_escape_text(text, -1);
|
|
|
|
|
|
|
|
let escaped = [];
|
|
|
|
let lastMatchEnd = 0;
|
|
|
|
let match;
|
|
|
|
while ((match = this._highlightRegex.exec(text))) {
|
|
|
|
if (match.index > lastMatchEnd) {
|
|
|
|
let unmatched = GLib.markup_escape_text(
|
|
|
|
text.slice(lastMatchEnd, match.index), -1);
|
|
|
|
escaped.push(unmatched);
|
|
|
|
}
|
|
|
|
let matched = GLib.markup_escape_text(match[0], -1);
|
2022-02-07 14:14:06 +00:00
|
|
|
escaped.push(`<b>${matched}</b>`);
|
2021-11-17 01:50:39 +00:00
|
|
|
lastMatchEnd = match.index + match[0].length;
|
|
|
|
}
|
|
|
|
let unmatched = GLib.markup_escape_text(
|
|
|
|
text.slice(lastMatchEnd), -1);
|
|
|
|
escaped.push(unmatched);
|
2021-11-16 17:57:26 +00:00
|
|
|
|
2021-11-17 01:50:39 +00:00
|
|
|
return escaped.join('');
|
2021-11-16 17:57:26 +00:00
|
|
|
}
|
|
|
|
};
|