[gnome] Fix extension locations
This commit is contained in:
@ -0,0 +1,263 @@
|
||||
// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
'use strict';
|
||||
|
||||
const Gio = imports.gi.Gio;
|
||||
const GjsPrivate = imports.gi.GjsPrivate;
|
||||
const GLib = imports.gi.GLib;
|
||||
const GObject = imports.gi.GObject;
|
||||
|
||||
|
||||
/*
|
||||
* Some utility methods
|
||||
*/
|
||||
function toDBusCase(string) {
|
||||
return string.replace(/(?:^\w|[A-Z]|\b\w)/g, (ltr, offset) => {
|
||||
return ltr.toUpperCase();
|
||||
}).replace(/[\s_-]+/g, '');
|
||||
}
|
||||
|
||||
function toHyphenCase(string) {
|
||||
return string.replace(/(?:[A-Z])/g, (ltr, offset) => {
|
||||
return (offset > 0) ? `-${ltr.toLowerCase()}` : ltr.toLowerCase();
|
||||
}).replace(/[\s_]+/g, '');
|
||||
}
|
||||
|
||||
function toUnderscoreCase(string) {
|
||||
return string.replace(/(?:^\w|[A-Z]|_|\b\w)/g, (ltr, offset) => {
|
||||
if (ltr === '_')
|
||||
return '';
|
||||
|
||||
return (offset > 0) ? `_${ltr.toLowerCase()}` : ltr.toLowerCase();
|
||||
}).replace(/[\s-]+/g, '');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* DBus.Interface represents a DBus interface bound to an object instance, meant
|
||||
* to be exported over DBus.
|
||||
*/
|
||||
var Interface = GObject.registerClass({
|
||||
GTypeName: 'GSConnectDBusInterface',
|
||||
Implements: [Gio.DBusInterface],
|
||||
Properties: {
|
||||
'g-instance': GObject.ParamSpec.object(
|
||||
'g-instance',
|
||||
'Instance',
|
||||
'The delegate GObject',
|
||||
GObject.ParamFlags.READWRITE,
|
||||
GObject.Object.$gtype
|
||||
),
|
||||
},
|
||||
}, class Interface extends GjsPrivate.DBusImplementation {
|
||||
|
||||
_init(params) {
|
||||
super._init({
|
||||
g_instance: params.g_instance,
|
||||
g_interface_info: params.g_interface_info,
|
||||
});
|
||||
|
||||
// Cache member lookups
|
||||
this._instanceHandlers = [];
|
||||
this._instanceMethods = {};
|
||||
this._instanceProperties = {};
|
||||
|
||||
const info = this.get_info();
|
||||
this.connect('handle-method-call', this._call.bind(this._instance, info));
|
||||
this.connect('handle-property-get', this._get.bind(this._instance, info));
|
||||
this.connect('handle-property-set', this._set.bind(this._instance, info));
|
||||
|
||||
// Automatically forward known signals
|
||||
const id = this._instance.connect('notify', this._notify.bind(this));
|
||||
this._instanceHandlers.push(id);
|
||||
|
||||
for (const signal of info.signals) {
|
||||
const type = `(${signal.args.map(arg => arg.signature).join('')})`;
|
||||
const id = this._instance.connect(
|
||||
signal.name,
|
||||
this._emit.bind(this, signal.name, type)
|
||||
);
|
||||
|
||||
this._instanceHandlers.push(id);
|
||||
}
|
||||
|
||||
// Export if connection and object path were given
|
||||
if (params.g_connection && params.g_object_path)
|
||||
this.export(params.g_connection, params.g_object_path);
|
||||
}
|
||||
|
||||
get g_instance() {
|
||||
if (this._instance === undefined)
|
||||
this._instance = null;
|
||||
|
||||
return this._instance;
|
||||
}
|
||||
|
||||
set g_instance(instance) {
|
||||
this._instance = instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke an instance's method for a DBus method call.
|
||||
*
|
||||
* @param {Gio.DBusInterfaceInfo} info - The DBus interface
|
||||
* @param {DBus.Interface} iface - The DBus interface
|
||||
* @param {string} name - The DBus method name
|
||||
* @param {GLib.Variant} parameters - The method parameters
|
||||
* @param {Gio.DBusMethodInvocation} invocation - The method invocation info
|
||||
*/
|
||||
async _call(info, iface, name, parameters, invocation) {
|
||||
let retval;
|
||||
|
||||
// Invoke the instance method
|
||||
try {
|
||||
const args = parameters.unpack().map(parameter => {
|
||||
if (parameter.get_type_string() === 'h') {
|
||||
const message = invocation.get_message();
|
||||
const fds = message.get_unix_fd_list();
|
||||
const idx = parameter.deepUnpack();
|
||||
return fds.get(idx);
|
||||
} else {
|
||||
return parameter.recursiveUnpack();
|
||||
}
|
||||
});
|
||||
|
||||
retval = await this[name](...args);
|
||||
} catch (e) {
|
||||
if (e instanceof GLib.Error) {
|
||||
invocation.return_gerror(e);
|
||||
} else {
|
||||
// likely to be a normal JS error
|
||||
if (!e.name.includes('.'))
|
||||
e.name = `org.gnome.gjs.JSError.${e.name}`;
|
||||
|
||||
invocation.return_dbus_error(e.name, e.message);
|
||||
}
|
||||
|
||||
logError(e, `${this}: ${name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// `undefined` is an empty tuple on DBus
|
||||
if (retval === undefined)
|
||||
retval = new GLib.Variant('()', []);
|
||||
|
||||
// Return the instance result or error
|
||||
try {
|
||||
if (!(retval instanceof GLib.Variant)) {
|
||||
const args = info.lookup_method(name).out_args;
|
||||
retval = new GLib.Variant(
|
||||
`(${args.map(arg => arg.signature).join('')})`,
|
||||
(args.length === 1) ? [retval] : retval
|
||||
);
|
||||
}
|
||||
|
||||
invocation.return_value(retval);
|
||||
} catch (e) {
|
||||
invocation.return_dbus_error(
|
||||
'org.gnome.gjs.JSError.ValueError',
|
||||
'Service implementation returned an incorrect value type'
|
||||
);
|
||||
|
||||
logError(e, `${this}: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
_nativeProp(obj, name) {
|
||||
if (this._instanceProperties[name] === undefined) {
|
||||
let propName = name;
|
||||
|
||||
if (propName in obj)
|
||||
this._instanceProperties[name] = propName;
|
||||
|
||||
if (this._instanceProperties[name] === undefined) {
|
||||
propName = toUnderscoreCase(name);
|
||||
|
||||
if (propName in obj)
|
||||
this._instanceProperties[name] = propName;
|
||||
}
|
||||
}
|
||||
|
||||
return this._instanceProperties[name];
|
||||
}
|
||||
|
||||
_emit(name, type, obj, ...args) {
|
||||
this.emit_signal(name, new GLib.Variant(type, args));
|
||||
}
|
||||
|
||||
_get(info, iface, name) {
|
||||
const nativeValue = this[iface._nativeProp(this, name)];
|
||||
const propertyInfo = info.lookup_property(name);
|
||||
|
||||
if (nativeValue === undefined || propertyInfo === null)
|
||||
return null;
|
||||
|
||||
return new GLib.Variant(propertyInfo.signature, nativeValue);
|
||||
}
|
||||
|
||||
_set(info, iface, name, value) {
|
||||
const nativeValue = value.recursiveUnpack();
|
||||
|
||||
this[iface._nativeProp(this, name)] = nativeValue;
|
||||
}
|
||||
|
||||
_notify(obj, pspec) {
|
||||
const name = toDBusCase(pspec.name);
|
||||
const propertyInfo = this.get_info().lookup_property(name);
|
||||
|
||||
if (propertyInfo === null)
|
||||
return;
|
||||
|
||||
this.emit_property_changed(
|
||||
name,
|
||||
new GLib.Variant(
|
||||
propertyInfo.signature,
|
||||
// Adjust for GJS's '-'/'_' conversion
|
||||
this._instance[pspec.name.replace(/-/gi, '_')]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
try {
|
||||
for (const id of this._instanceHandlers)
|
||||
this._instance.disconnect(id);
|
||||
this._instanceHandlers = [];
|
||||
|
||||
this.flush();
|
||||
this.unexport();
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Get a new, dedicated DBus connection on @busType
|
||||
*
|
||||
* @param {Gio.BusType} [busType] - a Gio.BusType constant
|
||||
* @param {Gio.Cancellable} [cancellable] - an optional Gio.Cancellable
|
||||
* @return {Promise<Gio.DBusConnection>} A new DBus connection
|
||||
*/
|
||||
function newConnection(busType = Gio.BusType.SESSION, cancellable = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Gio.DBusConnection.new_for_address(
|
||||
Gio.dbus_address_get_for_bus_sync(busType, cancellable),
|
||||
Gio.DBusConnectionFlags.AUTHENTICATION_CLIENT |
|
||||
Gio.DBusConnectionFlags.MESSAGE_BUS_CONNECTION,
|
||||
null,
|
||||
cancellable,
|
||||
(connection, res) => {
|
||||
try {
|
||||
resolve(Gio.DBusConnection.new_for_address_finish(res));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@ -0,0 +1,433 @@
|
||||
// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
'use strict';
|
||||
|
||||
const Gio = imports.gi.Gio;
|
||||
const GIRepository = imports.gi.GIRepository;
|
||||
const GLib = imports.gi.GLib;
|
||||
|
||||
const Config = imports.config;
|
||||
const {setup, setupGettext} = imports.utils.setup;
|
||||
|
||||
|
||||
// Promise Wrappers
|
||||
try {
|
||||
const {EBook, EDataServer} = imports.gi;
|
||||
|
||||
Gio._promisify(EBook.BookClient, 'connect');
|
||||
Gio._promisify(EBook.BookClient.prototype, 'get_view');
|
||||
Gio._promisify(EBook.BookClient.prototype, 'get_contacts');
|
||||
Gio._promisify(EDataServer.SourceRegistry, 'new');
|
||||
} catch (e) {
|
||||
// Silence import errors
|
||||
}
|
||||
|
||||
Gio._promisify(Gio.AsyncInitable.prototype, 'init_async');
|
||||
Gio._promisify(Gio.DBusConnection.prototype, 'call');
|
||||
Gio._promisify(Gio.DBusProxy.prototype, 'call');
|
||||
Gio._promisify(Gio.DataInputStream.prototype, 'read_line_async',
|
||||
'read_line_finish_utf8');
|
||||
Gio._promisify(Gio.File.prototype, 'delete_async');
|
||||
Gio._promisify(Gio.File.prototype, 'enumerate_children_async');
|
||||
Gio._promisify(Gio.File.prototype, 'load_contents_async');
|
||||
Gio._promisify(Gio.File.prototype, 'mount_enclosing_volume');
|
||||
Gio._promisify(Gio.File.prototype, 'query_info_async');
|
||||
Gio._promisify(Gio.File.prototype, 'read_async');
|
||||
Gio._promisify(Gio.File.prototype, 'replace_async');
|
||||
Gio._promisify(Gio.File.prototype, 'replace_contents_bytes_async',
|
||||
'replace_contents_finish');
|
||||
Gio._promisify(Gio.FileEnumerator.prototype, 'next_files_async');
|
||||
Gio._promisify(Gio.Mount.prototype, 'unmount_with_operation');
|
||||
Gio._promisify(Gio.InputStream.prototype, 'close_async');
|
||||
Gio._promisify(Gio.OutputStream.prototype, 'close_async');
|
||||
Gio._promisify(Gio.OutputStream.prototype, 'splice_async');
|
||||
Gio._promisify(Gio.OutputStream.prototype, 'write_all_async');
|
||||
Gio._promisify(Gio.SocketClient.prototype, 'connect_async');
|
||||
Gio._promisify(Gio.SocketListener.prototype, 'accept_async');
|
||||
Gio._promisify(Gio.Subprocess.prototype, 'communicate_utf8_async');
|
||||
Gio._promisify(Gio.Subprocess.prototype, 'wait_check_async');
|
||||
Gio._promisify(Gio.TlsConnection.prototype, 'handshake_async');
|
||||
Gio._promisify(Gio.DtlsConnection.prototype, 'handshake_async');
|
||||
|
||||
|
||||
// User Directories
|
||||
Config.CACHEDIR = GLib.build_filenamev([GLib.get_user_cache_dir(), 'gsconnect']);
|
||||
Config.CONFIGDIR = GLib.build_filenamev([GLib.get_user_config_dir(), 'gsconnect']);
|
||||
Config.RUNTIMEDIR = GLib.build_filenamev([GLib.get_user_runtime_dir(), 'gsconnect']);
|
||||
|
||||
// Bootstrap
|
||||
setup(Config.PACKAGE_DATADIR);
|
||||
setupGettext();
|
||||
|
||||
if (Config.IS_USER) {
|
||||
// Infer libdir by assuming gnome-shell shares a common prefix with gjs;
|
||||
// assume the parent directory if it's not there
|
||||
let libdir = GIRepository.Repository.get_search_path().find(path => {
|
||||
return path.endsWith('/gjs/girepository-1.0');
|
||||
}).replace('/gjs/girepository-1.0', '');
|
||||
|
||||
const gsdir = GLib.build_filenamev([libdir, 'gnome-shell']);
|
||||
|
||||
if (!GLib.file_test(gsdir, GLib.FileTest.IS_DIR)) {
|
||||
const currentDir = `/${GLib.path_get_basename(libdir)}`;
|
||||
libdir = libdir.replace(currentDir, '');
|
||||
}
|
||||
|
||||
Config.GNOME_SHELL_LIBDIR = libdir;
|
||||
}
|
||||
|
||||
|
||||
// Load DBus interfaces
|
||||
Config.DBUS = (() => {
|
||||
const bytes = Gio.resources_lookup_data(
|
||||
GLib.build_filenamev([Config.APP_PATH, `${Config.APP_ID}.xml`]),
|
||||
Gio.ResourceLookupFlags.NONE
|
||||
);
|
||||
|
||||
const xml = new TextDecoder().decode(bytes.toArray());
|
||||
const dbus = Gio.DBusNodeInfo.new_for_xml(xml);
|
||||
dbus.nodes.forEach(info => info.cache_build());
|
||||
|
||||
return dbus;
|
||||
})();
|
||||
|
||||
|
||||
// Init User Directories
|
||||
for (const path of [Config.CACHEDIR, Config.CONFIGDIR, Config.RUNTIMEDIR])
|
||||
GLib.mkdir_with_parents(path, 0o755);
|
||||
|
||||
|
||||
/**
|
||||
* Check if we're in a Wayland session (mostly for input synthesis)
|
||||
* https://wiki.gnome.org/Accessibility/Wayland#Bugs.2FIssues_We_Must_Address
|
||||
*/
|
||||
globalThis.HAVE_REMOTEINPUT = GLib.getenv('GDMSESSION') !== 'ubuntu-wayland';
|
||||
globalThis.HAVE_WAYLAND = GLib.getenv('XDG_SESSION_TYPE') === 'wayland';
|
||||
globalThis.HAVE_GNOME = GLib.getenv('GNOME_SETUP_DISPLAY') !== null;
|
||||
|
||||
|
||||
/**
|
||||
* A custom debug function that logs at LEVEL_MESSAGE to avoid the need for env
|
||||
* variables to be set.
|
||||
*
|
||||
* @param {Error|string} message - A string or Error to log
|
||||
* @param {string} [prefix] - An optional prefix for the warning
|
||||
*/
|
||||
const _debugCallerMatch = new RegExp(/([^@]*)@([^:]*):([^:]*)/);
|
||||
// eslint-disable-next-line func-style
|
||||
const _debugFunc = function (error, prefix = null) {
|
||||
let caller, message;
|
||||
|
||||
if (error.stack) {
|
||||
caller = error.stack.split('\n')[0];
|
||||
message = `${error.message}\n${error.stack}`;
|
||||
} else {
|
||||
caller = (new Error()).stack.split('\n')[1];
|
||||
message = JSON.stringify(error, null, 2);
|
||||
}
|
||||
|
||||
if (prefix)
|
||||
message = `${prefix}: ${message}`;
|
||||
|
||||
const [, func, file, line] = _debugCallerMatch.exec(caller);
|
||||
const script = file.replace(Config.PACKAGE_DATADIR, '');
|
||||
|
||||
GLib.log_structured('GSConnect', GLib.LogLevelFlags.LEVEL_MESSAGE, {
|
||||
'MESSAGE': `[${script}:${func}:${line}]: ${message}`,
|
||||
'SYSLOG_IDENTIFIER': 'org.gnome.Shell.Extensions.GSConnect',
|
||||
'CODE_FILE': file,
|
||||
'CODE_FUNC': func,
|
||||
'CODE_LINE': line,
|
||||
});
|
||||
};
|
||||
|
||||
// Swap the function out for a no-op anonymous function for speed
|
||||
const settings = new Gio.Settings({
|
||||
settings_schema: Config.GSCHEMA.lookup(Config.APP_ID, true),
|
||||
});
|
||||
|
||||
settings.connect('changed::debug', (settings, key) => {
|
||||
globalThis.debug = settings.get_boolean(key) ? _debugFunc : () => {};
|
||||
});
|
||||
|
||||
if (settings.get_boolean('debug'))
|
||||
globalThis.debug = _debugFunc;
|
||||
else
|
||||
globalThis.debug = () => {};
|
||||
|
||||
|
||||
/**
|
||||
* Start wl_clipboard if not under Gnome
|
||||
*/
|
||||
if (!globalThis.HAVE_GNOME) {
|
||||
debug('Not running as a Gnome extension');
|
||||
imports.wl_clipboard.watchService();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A simple (for now) pre-comparison sanitizer for phone numbers
|
||||
* See: https://github.com/KDE/kdeconnect-kde/blob/master/smsapp/conversationlistmodel.cpp#L200-L210
|
||||
*
|
||||
* @return {string} Return the string stripped of leading 0, and ' ()-+'
|
||||
*/
|
||||
String.prototype.toPhoneNumber = function () {
|
||||
const strippedNumber = this.replace(/^0*|[ ()+-]/g, '');
|
||||
|
||||
if (strippedNumber.length)
|
||||
return strippedNumber;
|
||||
|
||||
return this;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A simple equality check for phone numbers based on `toPhoneNumber()`
|
||||
*
|
||||
* @param {string} number - A phone number string to compare
|
||||
* @return {boolean} If `this` and @number are equivalent phone numbers
|
||||
*/
|
||||
String.prototype.equalsPhoneNumber = function (number) {
|
||||
const a = this.toPhoneNumber();
|
||||
const b = number.toPhoneNumber();
|
||||
|
||||
return (a.length && b.length && (a.endsWith(b) || b.endsWith(a)));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* An implementation of `rm -rf` in Gio
|
||||
*
|
||||
* @param {Gio.File|string} file - a GFile or filepath
|
||||
*/
|
||||
Gio.File.rm_rf = function (file) {
|
||||
try {
|
||||
if (typeof file === 'string')
|
||||
file = Gio.File.new_for_path(file);
|
||||
|
||||
try {
|
||||
const iter = file.enumerate_children(
|
||||
'standard::name',
|
||||
Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
|
||||
null
|
||||
);
|
||||
|
||||
let info;
|
||||
|
||||
while ((info = iter.next_file(null)))
|
||||
Gio.File.rm_rf(iter.get_child(info));
|
||||
|
||||
iter.close(null);
|
||||
} catch (e) {
|
||||
// Silence errors
|
||||
}
|
||||
|
||||
file.delete(null);
|
||||
} catch (e) {
|
||||
// Silence errors
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Extend GLib.Variant with a static method to recursively pack a variant
|
||||
*
|
||||
* @param {*} [obj] - May be a GLib.Variant, Array, standard Object or literal.
|
||||
* @return {GLib.Variant} The resulting GVariant
|
||||
*/
|
||||
function _full_pack(obj) {
|
||||
let packed;
|
||||
const type = typeof obj;
|
||||
|
||||
switch (true) {
|
||||
case (obj instanceof GLib.Variant):
|
||||
return obj;
|
||||
|
||||
case (type === 'string'):
|
||||
return GLib.Variant.new('s', obj);
|
||||
|
||||
case (type === 'number'):
|
||||
return GLib.Variant.new('d', obj);
|
||||
|
||||
case (type === 'boolean'):
|
||||
return GLib.Variant.new('b', obj);
|
||||
|
||||
case (obj instanceof Uint8Array):
|
||||
return GLib.Variant.new('ay', obj);
|
||||
|
||||
case (obj === null):
|
||||
return GLib.Variant.new('mv', null);
|
||||
|
||||
case (typeof obj.map === 'function'):
|
||||
return GLib.Variant.new(
|
||||
'av',
|
||||
obj.filter(e => e !== undefined).map(e => _full_pack(e))
|
||||
);
|
||||
|
||||
case (obj instanceof Gio.Icon):
|
||||
return obj.serialize();
|
||||
|
||||
case (type === 'object'):
|
||||
packed = {};
|
||||
|
||||
for (const [key, val] of Object.entries(obj)) {
|
||||
if (val !== undefined)
|
||||
packed[key] = _full_pack(val);
|
||||
}
|
||||
|
||||
return GLib.Variant.new('a{sv}', packed);
|
||||
|
||||
default:
|
||||
throw Error(`Unsupported type '${type}': ${obj}`);
|
||||
}
|
||||
}
|
||||
|
||||
GLib.Variant.full_pack = _full_pack;
|
||||
|
||||
|
||||
/**
|
||||
* Extend GLib.Variant with a method to recursively deepUnpack() a variant
|
||||
*
|
||||
* @param {*} [obj] - May be a GLib.Variant, Array, standard Object or literal.
|
||||
* @return {*} The resulting object
|
||||
*/
|
||||
function _full_unpack(obj) {
|
||||
obj = (obj === undefined) ? this : obj;
|
||||
const unpacked = {};
|
||||
|
||||
switch (true) {
|
||||
case (obj === null):
|
||||
return obj;
|
||||
|
||||
case (obj instanceof GLib.Variant):
|
||||
return _full_unpack(obj.deepUnpack());
|
||||
|
||||
case (obj instanceof Uint8Array):
|
||||
return obj;
|
||||
|
||||
case (typeof obj.map === 'function'):
|
||||
return obj.map(e => _full_unpack(e));
|
||||
|
||||
case (typeof obj === 'object'):
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
// Try to detect and deserialize GIcons
|
||||
try {
|
||||
if (key === 'icon' && value.get_type_string() === '(sv)')
|
||||
unpacked[key] = Gio.Icon.deserialize(value);
|
||||
else
|
||||
unpacked[key] = _full_unpack(value);
|
||||
} catch (e) {
|
||||
unpacked[key] = _full_unpack(value);
|
||||
}
|
||||
}
|
||||
|
||||
return unpacked;
|
||||
|
||||
default:
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
GLib.Variant.prototype.full_unpack = _full_unpack;
|
||||
|
||||
|
||||
/**
|
||||
* Creates a GTlsCertificate from the PEM-encoded data in @cert_path and
|
||||
* @key_path. If either are missing a new pair will be generated.
|
||||
*
|
||||
* Additionally, the private key will be added using ssh-add to allow sftp
|
||||
* connections using Gio.
|
||||
*
|
||||
* See: https://github.com/KDE/kdeconnect-kde/blob/master/core/kdeconnectconfig.cpp#L119
|
||||
*
|
||||
* @param {string} certPath - Absolute path to a x509 certificate in PEM format
|
||||
* @param {string} keyPath - Absolute path to a private key in PEM format
|
||||
* @param {string} commonName - A unique common name for the certificate
|
||||
* @return {Gio.TlsCertificate} A TLS certificate
|
||||
*/
|
||||
Gio.TlsCertificate.new_for_paths = function (certPath, keyPath, commonName = null) {
|
||||
// Check if the certificate/key pair already exists
|
||||
const certExists = GLib.file_test(certPath, GLib.FileTest.EXISTS);
|
||||
const keyExists = GLib.file_test(keyPath, GLib.FileTest.EXISTS);
|
||||
|
||||
// Create a new certificate and private key if necessary
|
||||
if (!certExists || !keyExists) {
|
||||
// If we weren't passed a common name, generate a random one
|
||||
if (!commonName)
|
||||
commonName = GLib.uuid_string_random();
|
||||
|
||||
const proc = new Gio.Subprocess({
|
||||
argv: [
|
||||
Config.OPENSSL_PATH, 'req',
|
||||
'-new', '-x509', '-sha256',
|
||||
'-out', certPath,
|
||||
'-newkey', 'rsa:4096', '-nodes',
|
||||
'-keyout', keyPath,
|
||||
'-days', '3650',
|
||||
'-subj', `/O=andyholmes.github.io/OU=GSConnect/CN=${commonName}`,
|
||||
],
|
||||
flags: (Gio.SubprocessFlags.STDOUT_SILENCE |
|
||||
Gio.SubprocessFlags.STDERR_SILENCE),
|
||||
});
|
||||
proc.init(null);
|
||||
proc.wait_check(null);
|
||||
}
|
||||
|
||||
return Gio.TlsCertificate.new_from_files(certPath, keyPath);
|
||||
};
|
||||
|
||||
Object.defineProperties(Gio.TlsCertificate.prototype, {
|
||||
/**
|
||||
* The common name of the certificate.
|
||||
*/
|
||||
'common_name': {
|
||||
get: function () {
|
||||
if (!this.__common_name) {
|
||||
const proc = new Gio.Subprocess({
|
||||
argv: [Config.OPENSSL_PATH, 'x509', '-noout', '-subject', '-inform', 'pem'],
|
||||
flags: Gio.SubprocessFlags.STDIN_PIPE | Gio.SubprocessFlags.STDOUT_PIPE,
|
||||
});
|
||||
proc.init(null);
|
||||
|
||||
const stdout = proc.communicate_utf8(this.certificate_pem, null)[1];
|
||||
this.__common_name = /(?:cn|CN) ?= ?([^,\n]*)/.exec(stdout)[1];
|
||||
}
|
||||
|
||||
return this.__common_name;
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
},
|
||||
|
||||
/**
|
||||
* Get just the pubkey as a DER ByteArray of a certificate.
|
||||
*
|
||||
* @return {GLib.Bytes} The pubkey as DER of the certificate.
|
||||
*/
|
||||
'pubkey_der': {
|
||||
value: function () {
|
||||
if (!this.__pubkey_der) {
|
||||
let proc = new Gio.Subprocess({
|
||||
argv: [Config.OPENSSL_PATH, 'x509', '-noout', '-pubkey', '-inform', 'pem'],
|
||||
flags: Gio.SubprocessFlags.STDIN_PIPE | Gio.SubprocessFlags.STDOUT_PIPE,
|
||||
});
|
||||
proc.init(null);
|
||||
|
||||
const pubkey = proc.communicate_utf8(this.certificate_pem, null)[1];
|
||||
proc = new Gio.Subprocess({
|
||||
argv: [Config.OPENSSL_PATH, 'pkey', '-pubin', '-inform', 'pem', '-outform', 'der'],
|
||||
flags: Gio.SubprocessFlags.STDIN_PIPE | Gio.SubprocessFlags.STDOUT_PIPE,
|
||||
});
|
||||
proc.init(null);
|
||||
this.__pubkey_der = proc.communicate(new TextEncoder().encode(pubkey), null)[1];
|
||||
}
|
||||
|
||||
return this.__pubkey_der;
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: false,
|
||||
},
|
||||
|
||||
});
|
||||
@ -0,0 +1,53 @@
|
||||
// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
'use strict';
|
||||
|
||||
const Gdk = imports.gi.Gdk;
|
||||
const Gio = imports.gi.Gio;
|
||||
const GLib = imports.gi.GLib;
|
||||
const Gtk = imports.gi.Gtk;
|
||||
|
||||
const Config = imports.config;
|
||||
|
||||
|
||||
/*
|
||||
* Window State
|
||||
*/
|
||||
Gtk.Window.prototype.restoreGeometry = function (context = 'default') {
|
||||
this._windowState = new Gio.Settings({
|
||||
settings_schema: Config.GSCHEMA.lookup(
|
||||
'org.gnome.Shell.Extensions.GSConnect.WindowState',
|
||||
true
|
||||
),
|
||||
path: `/org/gnome/shell/extensions/gsconnect/${context}/`,
|
||||
});
|
||||
|
||||
// Size
|
||||
const [width, height] = this._windowState.get_value('window-size').deepUnpack();
|
||||
|
||||
if (width && height)
|
||||
this.set_default_size(width, height);
|
||||
|
||||
// Maximized State
|
||||
if (this._windowState.get_boolean('window-maximized'))
|
||||
this.maximize();
|
||||
};
|
||||
|
||||
Gtk.Window.prototype.saveGeometry = function () {
|
||||
const state = this.get_window().get_state();
|
||||
|
||||
// Maximized State
|
||||
const maximized = (state & Gdk.WindowState.MAXIMIZED);
|
||||
this._windowState.set_boolean('window-maximized', maximized);
|
||||
|
||||
// Leave the size at the value before maximizing
|
||||
if (maximized || (state & Gdk.WindowState.FULLSCREEN))
|
||||
return;
|
||||
|
||||
// Size
|
||||
const size = this.get_size();
|
||||
this._windowState.set_value('window-size', new GLib.Variant('(ii)', size));
|
||||
};
|
||||
|
||||
@ -0,0 +1,171 @@
|
||||
// SPDX-FileCopyrightText: GSConnect Developers https://github.com/GSConnect
|
||||
//
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
'use strict';
|
||||
|
||||
const GLib = imports.gi.GLib;
|
||||
|
||||
|
||||
/**
|
||||
* The same regular expression used in GNOME Shell
|
||||
*
|
||||
* http://daringfireball.net/2010/07/improved_regex_for_matching_urls
|
||||
*/
|
||||
const _balancedParens = '\\((?:[^\\s()<>]+|(?:\\(?:[^\\s()<>]+\\)))*\\)';
|
||||
const _leadingJunk = '[\\s`(\\[{\'\\"<\u00AB\u201C\u2018]';
|
||||
const _notTrailingJunk = '[^\\s`!()\\[\\]{};:\'\\".,<>?\u00AB\u00BB\u201C\u201D\u2018\u2019]';
|
||||
|
||||
const _urlRegexp = new RegExp(
|
||||
'(^|' + _leadingJunk + ')' +
|
||||
'(' +
|
||||
'(?:' +
|
||||
'(?:http|https)://' + // scheme://
|
||||
'|' +
|
||||
'www\\d{0,3}[.]' + // www.
|
||||
'|' +
|
||||
'[a-z0-9.\\-]+[.][a-z]{2,4}/' + // foo.xx/
|
||||
')' +
|
||||
'(?:' + // one or more:
|
||||
'[^\\s()<>]+' + // run of non-space non-()
|
||||
'|' + // or
|
||||
_balancedParens + // balanced parens
|
||||
')+' +
|
||||
'(?:' + // end with:
|
||||
_balancedParens + // balanced parens
|
||||
'|' + // or
|
||||
_notTrailingJunk + // last non-junk char
|
||||
')' +
|
||||
')', 'gi');
|
||||
|
||||
|
||||
/**
|
||||
* sms/tel URI RegExp (https://tools.ietf.org/html/rfc5724)
|
||||
*
|
||||
* A fairly lenient regexp for sms: URIs that allows tel: numbers with chars
|
||||
* from global-number, local-number (without phone-context) and single spaces.
|
||||
* This allows passing numbers directly from libfolks or GData without
|
||||
* pre-processing. It also makes an allowance for URIs passed from Gio.File
|
||||
* that always come in the form "sms:///".
|
||||
*/
|
||||
const _smsParam = "[\\w.!~*'()-]+=(?:[\\w.!~*'()-]|%[0-9A-F]{2})*";
|
||||
const _telParam = ";[a-zA-Z0-9-]+=(?:[\\w\\[\\]/:&+$.!~*'()-]|%[0-9A-F]{2})+";
|
||||
const _lenientDigits = '[+]?(?:[0-9A-F*#().-]| (?! )|%20(?!%20))+';
|
||||
const _lenientNumber = `${_lenientDigits}(?:${_telParam})*`;
|
||||
|
||||
const _smsRegex = new RegExp(
|
||||
'^' +
|
||||
'sms:' + // scheme
|
||||
'(?:[/]{2,3})?' + // Gio.File returns ":///"
|
||||
'(' + // one or more...
|
||||
_lenientNumber + // phone numbers
|
||||
'(?:,' + _lenientNumber + ')*' + // separated by commas
|
||||
')' +
|
||||
'(?:\\?(' + // followed by optional...
|
||||
_smsParam + // parameters...
|
||||
'(?:&' + _smsParam + ')*' + // separated by "&" (unescaped)
|
||||
'))?' +
|
||||
'$', 'g'); // fragments (#foo) not allowed
|
||||
|
||||
|
||||
const _numberRegex = new RegExp(
|
||||
'^' +
|
||||
'(' + _lenientDigits + ')' + // phone number digits
|
||||
'((?:' + _telParam + ')*)' + // followed by optional parameters
|
||||
'$', 'g');
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @param {string} str - the string to search
|
||||
* @return {Object[]} the list of match objects, as described above
|
||||
*/
|
||||
function findUrls(str) {
|
||||
_urlRegexp.lastIndex = 0;
|
||||
|
||||
const res = [];
|
||||
let match;
|
||||
|
||||
while ((match = _urlRegexp.exec(str))) {
|
||||
const name = match[2];
|
||||
const url = GLib.uri_parse_scheme(name) ? name : `http://${name}`;
|
||||
res.push({name, url, pos: match.index + match[1].length});
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a string with URLs couched in <a> tags, parseable by Pango and
|
||||
* using the same RegExp as GNOME Shell.
|
||||
*
|
||||
* @param {string} str - The string to be modified
|
||||
* @param {string} [title] - An optional title (eg. alt text, tooltip)
|
||||
* @return {string} the modified text
|
||||
*/
|
||||
function linkify(str, title = null) {
|
||||
const text = GLib.markup_escape_text(str, -1);
|
||||
|
||||
_urlRegexp.lastIndex = 0;
|
||||
|
||||
if (title) {
|
||||
return text.replace(
|
||||
_urlRegexp,
|
||||
`$1<a href="$2" title="${title}">$2</a>`
|
||||
);
|
||||
} else {
|
||||
return text.replace(_urlRegexp, '$1<a href="$2">$2</a>');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A simple parsing class for sms: URI's (https://tools.ietf.org/html/rfc5724)
|
||||
*/
|
||||
var SmsURI = class URI {
|
||||
constructor(uri) {
|
||||
_smsRegex.lastIndex = 0;
|
||||
const [, recipients, query] = _smsRegex.exec(uri);
|
||||
|
||||
this.recipients = recipients.split(',').map(recipient => {
|
||||
_numberRegex.lastIndex = 0;
|
||||
const [, number, params] = _numberRegex.exec(recipient);
|
||||
|
||||
if (params) {
|
||||
for (const param of params.substr(1).split(';')) {
|
||||
const [key, value] = param.split('=');
|
||||
|
||||
// add phone-context to beginning of
|
||||
if (key === 'phone-context' && value.startsWith('+'))
|
||||
return value + unescape(number);
|
||||
}
|
||||
}
|
||||
|
||||
return unescape(number);
|
||||
});
|
||||
|
||||
if (query) {
|
||||
for (const field of query.split('&')) {
|
||||
const [key, value] = field.split('=');
|
||||
|
||||
if (key === 'body') {
|
||||
if (this.body)
|
||||
throw URIError('duplicate "body" field');
|
||||
|
||||
this.body = value ? decodeURIComponent(value) : undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
const uri = `sms:${this.recipients.join(',')}`;
|
||||
|
||||
return this.body ? `${uri}?body=${escape(this.body)}` : uri;
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user