[gnome] Update extensions for version 48

This commit is contained in:
2025-03-29 17:52:56 -04:00
parent e01589e836
commit a84b79ca08
153 changed files with 3479 additions and 2189 deletions

View File

@ -3,13 +3,13 @@
// SPDX-License-Identifier: GPL-2.0-or-later
export default {
PACKAGE_VERSION: 58,
PACKAGE_VERSION: 62,
PACKAGE_URL: 'https://github.com/GSConnect/gnome-shell-extension-gsconnect',
PACKAGE_BUGREPORT: 'https://github.com/GSConnect/gnome-shell-extension-gsconnect/issues/new',
PACKAGE_DATADIR: '/usr/local/share/gnome-shell/extensions/gsconnect@andyholmes.github.io',
PACKAGE_LOCALEDIR: '/usr/local/share/locale',
GSETTINGS_SCHEMA_DIR: '/usr/local/share/glib-2.0/schemas',
GNOME_SHELL_LIBDIR: '/usr/local/lib64',
GNOME_SHELL_LIBDIR: '/usr/local/lib/x86_64-linux-gnu',
APP_ID: 'org.gnome.Shell.Extensions.GSConnect',
APP_PATH: '/org/gnome/Shell/Extensions/GSConnect',

View File

@ -22,9 +22,8 @@ import * as Device from './shell/device.js';
import * as Keybindings from './shell/keybindings.js';
import * as Notification from './shell/notification.js';
import * as Input from './shell/input.js';
import * as Utils from './shell/utils.js';
import * as Remote from './utils/remote.js';
import setup from './utils/setup.js';
import * as Setup from './utils/setup.js';
const QuickSettingsMenu = Main.panel.statusArea.quickSettings;
@ -358,7 +357,7 @@ export default class GSConnectExtension extends Extension {
constructor(metadata) {
super(metadata);
setup(this.path);
Setup.setup(this.path);
// If installed as a user extension, this checks the permissions
// on certain critical files in the extension directory
@ -366,13 +365,13 @@ export default class GSConnectExtension extends Extension {
// and makes them executable if not. Some packaging methods
// (particularly GitHub Actions artifacts) automatically remove
// executable bits from all contents, presumably for security.
Utils.ensurePermissions();
Setup.ensurePermissions();
// If installed as a user extension, this will install the Desktop entry,
// DBus and systemd service files necessary for DBus activation and
// GNotifications. Since there's no uninit()/uninstall() hook for extensions
// and they're only used *by* GSConnect, they should be okay to leave.
Utils.installService();
Setup.installService();
// These modify the notification source for GSConnect's GNotifications and
// need to be active even when the extension is disabled (eg. lock screen).

View File

@ -4,9 +4,10 @@
"name": "GSConnect",
"shell-version": [
"46",
"47"
"47",
"48"
],
"url": "https://github.com/GSConnect/gnome-shell-extension-gsconnect/wiki",
"uuid": "gsconnect@andyholmes.github.io",
"version": 58
"version": 62
}

View File

@ -60,7 +60,7 @@ export function rowSeparators(row, before) {
*
* @param {Gtk.ListBoxRow} row1 - The first row
* @param {Gtk.ListBoxRow} row2 - The second row
* @return {number} -1, 0 or 1
* @returns {number} -1, 0 or 1
*/
export function titleSortFunc(row1, row2) {
if (!row1.title || !row2.title)
@ -605,7 +605,7 @@ export const Panel = GObject.registerClass({
const isPresent = value.get_boolean();
resolve(isPresent);
} catch (e) {
} catch {
resolve(false);
}
}
@ -614,7 +614,7 @@ export const Panel = GObject.registerClass({
this.battery_system_label.visible = hasBattery;
this.battery_system.visible = hasBattery;
} catch (e) {
} catch {
this.battery_system_label.visible = false;
this.battery_system.visible = false;
}
@ -814,7 +814,7 @@ export const Panel = GObject.registerClass({
try {
applications = JSON.parse(settings.get_string('applications'));
} catch (e) {
} catch {
applications = {};
}
@ -858,7 +858,7 @@ export const Panel = GObject.registerClass({
try {
applications = JSON.parse(settings.get_string('applications'));
} catch (e) {
} catch {
applications = {};
}

View File

@ -4,7 +4,7 @@
import GLib from 'gi://GLib';
import setup, {setupGettext} from '../utils/setup.js';
import {setup, setupGettext} from '../utils/setup.js';
// Bootstrap

View File

@ -205,7 +205,7 @@ export const ShortcutChooserDialog = GObject.registerClass({
* @param {string} accelerator - An accelerator
* @param {number} [modeFlags] - Mode Flags
* @param {number} [grabFlags] - Grab Flags
* @param {boolean} %true if available, %false on error or unavailable
* @returns {boolean} %true if available, %false on error or unavailable
*/
export async function checkAccelerator(accelerator, modeFlags = 0, grabFlags = 0) {
try {
@ -272,7 +272,7 @@ export async function checkAccelerator(accelerator, modeFlags = 0, grabFlags = 0
*
* @param {string} summary - A description of the keybinding's function
* @param {string} accelerator - An accelerator as taken by Gtk.ShortcutLabel
* @return {string} An accelerator or %null if it should be unset.
* @returns {string} An accelerator or %null if it should be unset.
*/
export async function getAccelerator(summary, accelerator = null) {
try {
@ -311,4 +311,3 @@ export async function getAccelerator(summary, accelerator = null) {
return accelerator;
}
}

View File

@ -422,6 +422,28 @@ export const Window = GObject.registerClass({
dialog.show_all();
}
_validateName(name) {
// None of the forbidden characters and at least one non-whitespace
if (name.trim() && /^[^"',;:.!?()[\]<>]{1,32}$/.test(name))
return true;
const dialog = new Gtk.MessageDialog({
text: _('Invalid Device Name'),
// TRANSLATOR: %s is a list of forbidden characters
secondary_text: _('Device name must not contain any of %s ' +
'and have a length of 1-32 characters')
.format('<b><tt>^"\',;:.!?()[]&lt;&gt;</tt></b>'),
secondary_use_markup: true,
buttons: Gtk.ButtonsType.OK,
modal: true,
transient_for: this,
});
dialog.connect('response', (dialog) => dialog.destroy());
dialog.show_all();
return false;
}
/*
* "Help" GAction
*/
@ -456,7 +478,7 @@ export const Window = GObject.registerClass({
}
_onSetServiceName(widget) {
if (this.rename_entry.text.length) {
if (this._validateName(this.rename_entry.text)) {
this.headerbar.title = this.rename_entry.text;
this.settings.set_string('name', this.rename_entry.text);
}

View File

@ -7,15 +7,15 @@ import GLib from 'gi://GLib';
import Adw from 'gi://Adw';
// Bootstrap
import * as Utils from './shell/utils.js';
import setup from './utils/setup.js';
import * as Setup from './utils/setup.js';
import {ExtensionPreferences} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
export default class GSConnectExtensionPreferences extends ExtensionPreferences {
constructor(metadata) {
super(metadata);
setup(this.path);
Utils.installService();
Setup.setup(this.path);
Setup.ensurePermissions();
Setup.installService();
}
fillPreferencesWindow(window) {

View File

@ -8,12 +8,13 @@ import GObject from 'gi://GObject';
import Config from '../../config.js';
import * as Core from '../core.js';
import Device from '../device.js';
// Retain compatibility with GLib < 2.80, which lacks GioUnix
let GioUnix;
try {
GioUnix = (await import('gi://GioUnix')).default;
} catch (e) {
} catch {
GioUnix = {
InputStream: Gio.UnixInputStream,
OutputStream: Gio.UnixOutputStream,
@ -42,7 +43,7 @@ try {
Gio.SocketType.STREAM,
Gio.SocketProtocol.TCP
).get_option(6, 5);
} catch (e) {
} catch {
_LINUX_SOCKETS = false;
}
@ -148,6 +149,10 @@ export const ChannelService = GObject.registerClass({
return this._channels;
}
get id() {
return this.certificate.common_name;
}
get port() {
if (this._port === undefined)
this._port = PROTOCOL_PORT_DEFAULT;
@ -172,13 +177,6 @@ export const ChannelService = GObject.registerClass({
}
_initCertificate() {
if (GLib.find_program_in_path(Config.OPENSSL_PATH) === null) {
const error = new Error();
error.name = _('OpenSSL not found');
error.url = `${Config.PACKAGE_URL}/wiki/Error#openssl-not-found`;
throw error;
}
const certPath = GLib.build_filenamev([
Config.CONFIGDIR,
'certificate.pem',
@ -190,12 +188,7 @@ export const ChannelService = GObject.registerClass({
// Ensure a certificate exists with our id as the common name
this._certificate = Gio.TlsCertificate.new_for_paths(certPath, keyPath,
this.id);
// If the service ID doesn't match the common name, this is probably a
// certificate from an older version and we should amend ours to match
if (this.id !== this._certificate.common_name)
this._id = this._certificate.common_name;
null);
}
_initTcpListener() {
@ -282,7 +275,7 @@ export const ChannelService = GObject.registerClass({
this._udp6_source = this._udp6.create_source(GLib.IOCondition.IN, null);
this._udp6_source.set_callback(this._onIncomingIdentity.bind(this, this._udp6));
this._udp6_source.attach(null);
} catch (e) {
} catch {
this._udp6 = null;
}
@ -360,13 +353,24 @@ export const ChannelService = GObject.registerClass({
async _onIdentity(packet) {
try {
// Bail if the deviceId is missing
if (!packet.body.hasOwnProperty('deviceId'))
return;
if (!this.identity.body.deviceId)
throw new Error('missing deviceId');
// Silently ignore our own broadcasts
if (packet.body.deviceId === this.identity.body.deviceId)
return;
// Reject invalid device IDs
if (!Device.validateId(packet.body.deviceId))
throw new Error(`invalid deviceId "${packet.body.deviceId}"`);
if (!packet.body.deviceName)
throw new Error('missing deviceName');
// Reject invalid device names
if (!Device.validateName(packet.body.deviceName))
throw new Error(`invalid deviceName "${packet.body.deviceName}"`);
debug(packet);
// Create a new channel
@ -596,7 +600,7 @@ export const Channel = GObject.registerClass({
* Authenticate a TLS connection.
*
* @param {Gio.TlsConnection} connection - A TLS connection
* @return {Promise} A promise for the operation
* @returns {Promise} A promise for the operation
*/
async _authenticate(connection) {
// Standard TLS Handshake
@ -668,7 +672,7 @@ export const Channel = GObject.registerClass({
* Wrap the connection in Gio.TlsClientConnection and initiate handshake
*
* @param {Gio.TcpConnection} connection - The unauthenticated connection
* @return {Gio.TlsClientConnection} The authenticated connection
* @returns {Gio.TlsClientConnection} The authenticated connection
*/
_encryptClient(connection) {
_configureSocket(connection);
@ -684,7 +688,7 @@ export const Channel = GObject.registerClass({
* Wrap the connection in Gio.TlsServerConnection and initiate handshake
*
* @param {Gio.TcpConnection} connection - The unauthenticated connection
* @return {Gio.TlsServerConnection} The authenticated connection
* @returns {Gio.TlsServerConnection} The authenticated connection
*/
_encryptServer(connection) {
_configureSocket(connection);
@ -729,7 +733,25 @@ export const Channel = GObject.registerClass({
if (!this.identity.body.deviceId)
throw new Error('missing deviceId');
// Reject invalid device IDs
if (!Device.validateId(this.identity.body.deviceId))
throw new Error(`invalid deviceId "${this.identity.body.deviceId}"`);
if (!this.identity.body.deviceName)
throw new Error('missing deviceName');
// Reject invalid device names
if (!Device.validateName(this.identity.body.deviceName))
throw new Error(`invalid deviceName "${this.identity.body.deviceName}"`);
this._connection = await this._encryptClient(connection);
// Starting with protocol version 8, the devices are expected to
// exchange identity packets again after TLS negotiation
if (this.identity.body.protocolVersion >= 8) {
await this.sendPacket(this.backend.identity);
this.identity = await this.readPacket();
}
} catch (e) {
this.close();
throw e;
@ -754,6 +776,13 @@ export const Channel = GObject.registerClass({
this.cancellable);
this._connection = await this._encryptServer(connection);
// Starting with protocol version 8, the devices are expected to
// exchange identity packets again after TLS negotiation
if (this.identity.body.protocolVersion >= 8) {
await this.sendPacket(this.backend.identity);
this.identity = await this.readPacket();
}
} catch (e) {
this.close();
throw e;
@ -886,4 +915,3 @@ export const Channel = GObject.registerClass({
}
}
});

View File

@ -62,7 +62,7 @@ export default class Controller {
modifier = keymap.get_entries_for_keyval(Gdk.KEY_Super_L)[1][0];
XKeycode.Super_L = modifier.keycode;
} catch (e) {
} catch {
debug('using default modifier keycodes');
}
}
@ -304,9 +304,8 @@ export default class Controller {
destroy() {
try {
Atspi.exit();
} catch (e) {
} catch {
// Silence errors
}
}
}

View File

@ -17,7 +17,7 @@ try {
EBook = (await import('gi://EBook')).default;
EBookContacts = (await import('gi://EBookContacts')).default;
EDataServer = (await import('gi://EDataServer')).default;
} catch (e) {
} catch {
HAVE_EDS = false;
}
@ -267,7 +267,7 @@ const Store = GObject.registerClass({
this._ebooks = new Map();
// Get the current EBooks
const registry = await this._getESourceRegistry();
const registry = await EDataServer.SourceRegistry.new(null);
for (const source of registry.list_sources('Address Book'))
await this._onAppeared(null, source);
@ -329,7 +329,7 @@ const Store = GObject.registerClass({
* Save a Uint8Array to file and return the path
*
* @param {Uint8Array} contents - An image byte array
* @return {string|undefined} File path or %undefined on failure
* @returns {string|undefined} File path or %undefined on failure
*/
async storeAvatar(contents) {
const md5 = GLib.compute_checksum_for_data(GLib.ChecksumType.MD5,
@ -353,10 +353,10 @@ const Store = GObject.registerClass({
/**
* Query the Store for a contact by name and/or number.
*
* @param {Object} query - A query object
* @param {object} query - A query object
* @param {string} [query.name] - The contact's name
* @param {string} query.number - The contact's number
* @return {Object} A contact object
* @returns {object} A contact object
*/
query(query) {
// First look for an existing contact by number
@ -410,7 +410,7 @@ const Store = GObject.registerClass({
/**
* Add a contact, checking for validity
*
* @param {Object} contact - A contact object
* @param {object} contact - A contact object
* @param {boolean} write - Write to disk
*/
add(contact, write = true) {
@ -468,8 +468,8 @@ const Store = GObject.registerClass({
*
* { "555-5555": { "name": "...", "numbers": [], ... } }
*
* @param {Object[]} addresses - A list of address objects
* @return {Object} A dictionary of phone numbers and contacts
* @param {object[]} addresses - A list of address objects
* @returns {object} A dictionary of phone numbers and contacts
*/
lookupAddresses(addresses) {
const contacts = {};
@ -502,7 +502,7 @@ const Store = GObject.registerClass({
/**
* Update the contact store from a dictionary of our custom contact objects.
*
* @param {Object} json - an Object of contact Objects
* @param {object} json - an Object of contact Objects
*/
async update(json = {}) {
try {
@ -610,4 +610,3 @@ const Store = GObject.registerClass({
});
export default Store;

View File

@ -41,7 +41,7 @@ const Default = new Map();
* followed by a call to `release()`.
*
* @param {string} name - The module name
* @return {*} The default instance of a component
* @returns {*} The default instance of a component
*/
export function acquire(name) {
if (functionOverrides.acquire)
@ -78,7 +78,7 @@ export function acquire(name) {
* holder, the component will be freed.
*
* @param {string} name - The module name
* @return {null} A %null value, useful for overriding a traced variable
* @returns {null} A %null value, useful for overriding a traced variable
*/
export function release(name) {
if (functionOverrides.release)
@ -99,4 +99,3 @@ export function release(name) {
return null;
}

View File

@ -51,7 +51,7 @@ const RemoteSession = GObject.registerClass({
get session_id() {
try {
return this.get_cached_property('SessionId').unpack();
} catch (e) {
} catch {
return null;
}
}
@ -327,22 +327,24 @@ export default class Controller {
}
async _ensureAdapter() {
// Update the timestamp of the last event
this._sessionExpiry = Math.floor((Date.now() / 1000) + SESSION_TIMEOUT);
// Session is active
if (this._session !== null)
return this._session;
// Mutter's RemoteDesktop is not available, fall back to Atspi
if (this.connection === null) {
debug('Falling back to Atspi');
this._session = new AtspiController();
return this._session;
}
try {
// Update the timestamp of the last event
this._sessionExpiry = Math.floor((Date.now() / 1000) + SESSION_TIMEOUT);
// Session is active
if (this._session !== null)
return;
// Mutter's RemoteDesktop is not available, fall back to Atspi
if (this.connection === null) {
debug('Falling back to Atspi');
this._session = new AtspiController();
// Mutter is available and there isn't another session starting
} else if (this._sessionStarting === false) {
if (this._sessionStarting === false) {
this._sessionStarting = true;
debug('Creating Mutter RemoteDesktop session');
@ -368,18 +370,17 @@ export default class Controller {
this._onSessionExpired.bind(this)
);
}
this._sessionStarting = false;
}
return this._session;
} catch (e) {
logError(e);
if (this._session !== null) {
this._session.destroy();
this._session = null;
}
this._sessionStarting = false;
throw e;
}
}
@ -387,111 +388,81 @@ export default class Controller {
* Pointer Events
*/
movePointer(dx, dy) {
try {
if (dx === 0 && dy === 0)
return;
if (dx === 0 && dy === 0)
return;
this._ensureAdapter();
this._session.movePointer(dx, dy);
} catch (e) {
debug(e);
}
this._ensureAdapter()
.then(session => session?.movePointer(dx, dy))
.catch(e => debug(e));
}
pressPointer(button) {
try {
this._ensureAdapter();
this._session.pressPointer(button);
} catch (e) {
debug(e);
}
this._ensureAdapter()
.then(session => session?.pressPointer(button))
.catch(e => debug(e));
}
releasePointer(button) {
try {
this._ensureAdapter();
this._session.releasePointer(button);
} catch (e) {
debug(e);
}
this._ensureAdapter()
.then(session => session?.releasePointer(button))
.catch(e => debug(e));
}
clickPointer(button) {
try {
this._ensureAdapter();
this._session.clickPointer(button);
} catch (e) {
debug(e);
}
this._ensureAdapter()
.then(session => session?.clickPointer(button))
.catch(e => debug(e));
}
doubleclickPointer(button) {
try {
this._ensureAdapter();
this._session.doubleclickPointer(button);
} catch (e) {
debug(e);
}
this._ensureAdapter()
.then(session => session?.doubleclickPointer(button))
.catch(e => debug(e));
}
scrollPointer(dx, dy) {
if (dx === 0 && dy === 0)
return;
try {
this._ensureAdapter();
this._session.scrollPointer(dx, dy);
} catch (e) {
debug(e);
}
this._ensureAdapter()
.then(session => session?.scrollPointer(dx, dy))
.catch(e => debug(e));
}
/*
* Keyboard Events
*/
pressKeysym(keysym) {
try {
this._ensureAdapter();
this._session.pressKeysym(keysym);
} catch (e) {
debug(e);
}
this._ensureAdapter()
.then(session => session?.pressKeysym(keysym))
.catch(e => debug(e));
}
releaseKeysym(keysym) {
try {
this._ensureAdapter();
this._session.releaseKeysym(keysym);
} catch (e) {
debug(e);
}
this._ensureAdapter()
.then(session => session?.releaseKeysym(keysym))
.catch(e => debug(e));
}
pressreleaseKeysym(keysym) {
try {
this._ensureAdapter();
this._session.pressreleaseKeysym(keysym);
} catch (e) {
debug(e);
}
this._ensureAdapter()
.then(session => session?.pressreleaseKeysym(keysym))
.catch(e => debug(e));
}
/*
* High-level keyboard input
*/
pressKeys(input, modifiers) {
try {
this._ensureAdapter();
this._ensureAdapter()
.then(session => {
if (typeof input === 'string') {
for (let i = 0; i < input.length; i++)
this._session.pressKey(input[i], modifiers);
session?.pressKey(input[i], modifiers);
} else {
this._session.pressKey(input, modifiers);
session?.pressKey(input, modifiers);
}
} catch (e) {
debug(e);
}
}).catch(e => debug(e));
}
destroy() {

View File

@ -472,6 +472,10 @@ const PlayerProxy = GObject.registerClass({
GTypeName: 'GSConnectMPRISPlayer',
}, class PlayerProxy extends Player {
/**
* @constructs GSConnectMPRISPlayer
* @param {string} name - The name of the player
*/
_init(name) {
super._init();
@ -539,7 +543,7 @@ const PlayerProxy = GObject.registerClass({
_get(proxy, name, fallback = null) {
try {
return proxy.get_cached_property(name).recursiveUnpack();
} catch (e) {
} catch {
return fallback;
}
}
@ -707,7 +711,7 @@ const PlayerProxy = GObject.registerClass({
);
return reply.recursiveUnpack()[0];
} catch (e) {
} catch {
return 0;
}
}
@ -914,7 +918,7 @@ const Manager = GObject.registerClass({
* Check for a player by its Identity.
*
* @param {string} identity - A player name
* @return {boolean} %true if the player was found
* @returns {boolean} %true if the player was found
*/
hasPlayer(identity) {
for (const player of this._players.values()) {
@ -929,7 +933,7 @@ const Manager = GObject.registerClass({
* Get a player by its Identity.
*
* @param {string} identity - A player name
* @return {GSConnectMPRISPlayer|null} A player or %null
* @returns {GSConnectMPRISPlayer|null} A player or %null
*/
getPlayer(identity) {
for (const player of this._players.values()) {
@ -943,7 +947,7 @@ const Manager = GObject.registerClass({
/**
* Get a list of player identities.
*
* @return {string[]} A list of player identities
* @returns {string[]} A list of player identities
*/
getIdentities() {
const identities = [];
@ -1000,4 +1004,3 @@ const Manager = GObject.registerClass({
* The service class for this component
*/
export default Manager;

View File

@ -144,7 +144,7 @@ const Listener = GObject.registerClass({
*
* @param {string} sender - A DBus unique name (eg. :1.2282)
* @param {string} appName - @appName passed to Notify() (Optional)
* @return {string} A well-known name or %null
* @returns {string} A well-known name or %null
*/
async _getAppId(sender, appName) {
try {
@ -189,7 +189,7 @@ const Listener = GObject.registerClass({
*
* @param {string} sender - A DBus unique name
* @param {string} [appName] - `appName` supplied by Notify()
* @return {string} A well-known name or %null
* @returns {string} A well-known name or %null
*/
async _getAppName(sender, appName = null) {
// Check the cache first
@ -201,7 +201,7 @@ const Listener = GObject.registerClass({
const appInfo = Gio.DesktopAppInfo.new(`${appId}.desktop`);
this._names[appName] = appInfo.get_name();
appName = appInfo.get_name();
} catch (e) {
} catch {
// Silence errors
}
@ -261,7 +261,7 @@ const Listener = GObject.registerClass({
/**
* Export interfaces for proxying notifications and become a monitor
*
* @return {Promise} A promise for the operation
* @returns {Promise} A promise for the operation
*/
_monitorConnection() {
// libnotify Interface

View File

@ -19,7 +19,7 @@ try {
GIRepository.Repository.prepend_library_path(typelibDir);
Gvc = (await import('gi://Gvc')).default;
} catch (e) {}
} catch {}
/**
@ -33,7 +33,7 @@ if (Gvc) {
return this.description;
return `${this.get_port().human_port} (${this.description})`;
} catch (e) {
} catch {
return this.description;
}
},

View File

@ -9,7 +9,7 @@ import GLib from 'gi://GLib';
let GSound = null;
try {
GSound = (await import('gi://GSound')).default;
} catch (e) {}
} catch {}
const Player = class Player {
@ -169,4 +169,3 @@ const Player = class Player {
* The service class for this component
*/
export default Player;

View File

@ -6,13 +6,14 @@ import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Device from './device.js';
import plugins from './plugins/index.js';
/**
* Get the local device type.
*
* @return {string} A device type string
* @returns {string} A device type string
*/
export function _getDeviceType() {
try {
@ -27,7 +28,7 @@ export function _getDeviceType() {
return 'laptop';
return 'desktop';
} catch (e) {
} catch {
return 'desktop';
}
}
@ -69,8 +70,8 @@ export class Packet {
/**
* Deserialize and return a new Packet from an Object or string.
*
* @param {Object|string} data - A string or dictionary to deserialize
* @return {Core.Packet} A new packet object
* @param {object|string} data - A string or dictionary to deserialize
* @returns {Packet} A new packet object
*/
static deserialize(data) {
return new Packet(data);
@ -80,7 +81,7 @@ export class Packet {
* Serialize the packet as a single line with a terminating new-line (`\n`)
* character, ready to be written to a channel.
*
* @return {string} A serialized packet
* @returns {string} A serialized packet
*/
serialize() {
this.id = Date.now();
@ -90,7 +91,7 @@ export class Packet {
/**
* Update the packet from a dictionary or string of JSON
*
* @param {Object|string} data - Source data
* @param {object|string} data - Source data
*/
update(data) {
try {
@ -106,7 +107,7 @@ export class Packet {
/**
* Check if the packet has a payload.
*
* @return {boolean} %true if @packet has a payload
* @returns {boolean} %true if @packet has a payload
*/
hasPayload() {
if (!this.hasOwnProperty('payloadSize'))
@ -219,7 +220,7 @@ export const Channel = GObject.registerClass({
* Read a packet.
*
* @param {Gio.Cancellable} [cancellable] - A cancellable
* @return {Promise<Core.Packet>} The packet
* @returns {Promise<Packet>} The packet
*/
async readPacket(cancellable = null) {
if (cancellable === null)
@ -247,9 +248,9 @@ export const Channel = GObject.registerClass({
/**
* Send a packet.
*
* @param {Core.Packet} packet - The packet to send
* @param {Packet} packet - The packet to send
* @param {Gio.Cancellable} [cancellable] - A cancellable
* @return {Promise<boolean>} %true if successful
* @returns {Promise<boolean>} %true if successful
*/
sendPacket(packet, cancellable = null) {
if (cancellable === null)
@ -262,7 +263,7 @@ export const Channel = GObject.registerClass({
/**
* Reject a transfer.
*
* @param {Core.Packet} packet - A packet with payload info
* @param {Packet} packet - A packet with payload info
*/
rejectTransfer(packet) {
throw new GObject.NotImplementedError();
@ -272,7 +273,7 @@ export const Channel = GObject.registerClass({
* Download a payload from a device. Typically implementations will override
* this with an async function.
*
* @param {Core.Packet} packet - A packet
* @param {Packet} packet - A packet
* @param {Gio.OutputStream} target - The target stream
* @param {Gio.Cancellable} [cancellable] - A cancellable for the upload
*/
@ -285,7 +286,7 @@ export const Channel = GObject.registerClass({
* Upload a payload to a device. Typically implementations will override
* this with an async function.
*
* @param {Core.Packet} packet - The packet describing the transfer
* @param {Packet} packet - The packet describing the transfer
* @param {Gio.InputStream} source - The source stream
* @param {number} size - The payload size
* @param {Gio.Cancellable} [cancellable] - A cancellable for the upload
@ -350,7 +351,7 @@ export const ChannelService = GObject.registerClass({
get name() {
if (this._name === undefined)
this._name = GLib.get_host_name();
this._name = GLib.get_host_name().slice(0, 32);
return this._name;
}
@ -365,7 +366,7 @@ export const ChannelService = GObject.registerClass({
get id() {
if (this._id === undefined)
this._id = GLib.uuid_string_random();
this._id = Device.generateId();
return this._id;
}
@ -406,7 +407,7 @@ export const ChannelService = GObject.registerClass({
deviceId: this.id,
deviceName: this.name,
deviceType: _getDeviceType(),
protocolVersion: 7,
protocolVersion: 8,
incomingCapabilities: [],
outgoingCapabilities: [],
},
@ -429,7 +430,7 @@ export const ChannelService = GObject.registerClass({
/**
* Emit Core.ChannelService::channel
*
* @param {Core.Channel} channel - The new channel
* @param {Channel} channel - The new channel
*/
channel(channel) {
if (!this.emit('channel', channel))
@ -542,7 +543,7 @@ export const Transfer = GObject.registerClass({
/**
* Ensure there is a stream for the transfer item.
*
* @param {Object} item - A transfer item
* @param {object} item - A transfer item
* @param {Gio.Cancellable} [cancellable] - A cancellable
*/
async _ensureStream(item, cancellable = null) {
@ -583,7 +584,7 @@ export const Transfer = GObject.registerClass({
/**
* Add a file to the transfer.
*
* @param {Core.Packet} packet - A packet
* @param {Packet} packet - A packet
* @param {Gio.File} file - A file to transfer
*/
addFile(packet, file) {
@ -600,7 +601,7 @@ export const Transfer = GObject.registerClass({
/**
* Add a filepath to the transfer.
*
* @param {Core.Packet} packet - A packet
* @param {Packet} packet - A packet
* @param {string} path - A filepath to transfer
*/
addPath(packet, path) {
@ -617,7 +618,7 @@ export const Transfer = GObject.registerClass({
/**
* Add a stream to the transfer.
*
* @param {Core.Packet} packet - A packet
* @param {Packet} packet - A packet
* @param {Gio.InputStream|Gio.OutputStream} stream - A stream to transfer
* @param {number} [size] - Payload size
*/

View File

@ -18,6 +18,7 @@ import system from 'system';
import './init.js';
import Config from '../config.js';
import Device from './device.js';
import Manager from './manager.js';
import * as ServiceUI from './ui/service.js';
@ -45,6 +46,61 @@ const Service = GObject.registerClass({
this._initOptions();
}
_migrateConfiguration() {
if (!Device.validateName(this.settings.get_string('name')))
this.settings.set('name', GLib.get_host_name().slice(0, 32));
const [certPath, keyPath] = [
GLib.build_filenamev([Config.CONFIGDIR, 'certificate.pem']),
GLib.build_filenamev([Config.CONFIGDIR, 'private.pem']),
];
const certificate = Gio.TlsCertificate.new_for_paths(certPath, keyPath,
null);
if (Device.validateId(certificate.common_name))
return;
// Remove the old certificate, serving as the single source of truth
// for the device ID
try {
Gio.File.new_for_path(certPath).delete(null);
Gio.File.new_for_path(keyPath).delete(null);
} catch {
// Silence errors
}
// For each device, remove it entirely if it violates the device ID
// constraints, otherwise mark it unpaired to preserve the settings.
const deviceList = this.settings.get_strv('devices').filter(id => {
const settingsPath = `/org/gnome/shell/extensions/gsconnect/device/${id}/`;
if (!Device.validateId(id)) {
GLib.spawn_command_line_async(`dconf reset -f ${settingsPath}`);
Gio.File.rm_rf(GLib.build_filenamev([Config.CACHEDIR, id]));
debug(`Invalid device ID ${id} removed.`);
return false;
}
const settings = new Gio.Settings({
settings_schema: Config.GSCHEMA.lookup(
'org.gnome.Shell.Extensions.GSConnect.Device', true),
path: settingsPath,
});
settings.set_boolean('paired', false);
return true;
});
this.settings.set_strv('devices', deviceList);
// Notify the user
const notification = Gio.Notification.new(_('Settings Migrated'));
notification.set_body(_('GSConnect has updated to support changes to the KDE Connect protocol. Some devices may need to be repaired.'));
notification.set_icon(new Gio.ThemedIcon({name: 'dialog-warning'}));
notification.set_priority(Gio.NotificationPriority.HIGH);
this.send_notification('settings-migrated', notification);
// Finally, reset the service ID to trigger re-generation.
this.settings.reset('id');
}
get settings() {
if (this._settings === undefined) {
this._settings = new Gio.Settings({
@ -154,7 +210,7 @@ const Service = GObject.registerClass({
/**
* Report a service-level error
*
* @param {Object} error - An Error or object with name, message and stack
* @param {object} error - An Error or object with name, message and stack
*/
notify_error(error) {
try {
@ -241,6 +297,9 @@ const Service = GObject.registerClass({
// GActions & GSettings
this._initActions();
// TODO: remove after a reasonable period of time
this._migrateConfiguration();
this.manager.start();
}
@ -699,4 +758,3 @@ const Service = GObject.registerClass({
});
await (new Service()).runAsync([system.programInvocationName].concat(ARGV));

View File

@ -11,6 +11,8 @@ import * as Components from './components/index.js';
import * as Core from './core.js';
import plugins from './plugins/index.js';
const ALLOWED_TIMESTAMP_TIME_DIFFERENCE_SECONDS = 1800; // 30 min
/**
* An object representing a remote device.
*
@ -88,6 +90,7 @@ const Device = GObject.registerClass({
// GLib.Source timeout id's for pairing requests
this._incomingPairRequest = 0;
this._outgoingPairRequest = 0;
this._pairingTimestamp = 0;
// Maps of name->Plugin, packet->Plugin, uuid->Transfer
this._plugins = new Map();
@ -128,6 +131,19 @@ const Device = GObject.registerClass({
this._loadPlugins();
}
static generateId() {
return GLib.uuid_string_random().replaceAll('-', '');
}
static validateId(id) {
return /^[a-zA-Z0-9_-]{32,38}$/.test(id);
}
static validateName(name) {
// None of the forbidden characters and at least one non-whitespace
return name.trim() && /^[^"',;:.!?()[\]<>]{1,32}$/.test(name);
}
get channel() {
if (this._channel === undefined)
this._channel = null;
@ -162,47 +178,34 @@ const Device = GObject.registerClass({
// FIXME: backend should do this stuff
get encryption_info() {
let localCert = null;
let remoteCert = null;
if (!this.channel)
return '';
// Bluetooth connections have no certificate so we use the host address
if (this.connection_type === 'bluetooth') {
// TRANSLATORS: Bluetooth address for remote device
return _('Bluetooth device at %s').format('???');
// If the device is connected use the certificate from the connection
} else if (this.connected) {
remoteCert = this.channel.peer_certificate;
// Otherwise pull it out of the settings
} else if (this.paired) {
remoteCert = Gio.TlsCertificate.new_from_pem(
this.settings.get_string('certificate-pem'),
-1
);
}
// FIXME: another ugly reach-around
let lanBackend;
const localCert = this.service.manager.backends.get('lan')?.certificate;
const remoteCert = this.channel?.peer_certificate;
if (!localCert || !remoteCert)
return '';
if (this.service !== null)
lanBackend = this.service.manager.backends.get('lan');
const checksum = new GLib.Checksum(GLib.ChecksumType.SHA256);
let [a, b] = [localCert.pubkey_der(), remoteCert.pubkey_der()];
if (a.compare(b) < 0)
[a, b] = [b, a]; // swap
checksum.update(a.toArray());
checksum.update(b.toArray());
if (lanBackend && lanBackend.certificate)
localCert = lanBackend.certificate;
if (this.channel?.identity.body.protocolVersion >= 8)
checksum.update(String(this._pairingTimestamp));
let verificationKey = '';
if (localCert && remoteCert) {
let a = localCert.pubkey_der();
let b = remoteCert.pubkey_der();
if (a.compare(b) < 0)
[a, b] = [b, a]; // swap
const checksum = new GLib.Checksum(GLib.ChecksumType.SHA256);
checksum.update(a.toArray());
checksum.update(b.toArray());
verificationKey = checksum.get_string();
}
const verificationKey = checksum.get_string()
.substring(0, 8)
.toUpperCase();
// TRANSLATORS: Label for TLS connection verification key
//
@ -384,7 +387,7 @@ const Device = GObject.registerClass({
*
* @param {string[]} args - process arguments
* @param {Gio.Cancellable} [cancellable] - optional cancellable
* @return {Gio.Subprocess} The subprocess
* @returns {Gio.Subprocess} The subprocess
*/
launchProcess(args, cancellable = null) {
if (this._launcher === undefined) {
@ -418,7 +421,7 @@ const Device = GObject.registerClass({
* Handle a packet and pass it to the appropriate plugin.
*
* @param {Core.Packet} packet - The incoming packet object
* @return {undefined} no return value
* @returns {undefined} no return value
*/
handlePacket(packet) {
try {
@ -443,7 +446,7 @@ const Device = GObject.registerClass({
/**
* Send a packet to the device.
*
* @param {Object} packet - An object of packet data...
* @param {object} packet - An object of packet data...
*/
async sendPacket(packet) {
try {
@ -524,7 +527,7 @@ const Device = GObject.registerClass({
* device menu.
*
* @param {string} actionName - An action name with scope (eg. device.foo)
* @return {number} An 0-based index or -1 if not found
* @returns {number} An 0-based index or -1 if not found
*/
getMenuAction(actionName) {
for (let i = 0, len = this.menu.get_n_items(); i < len; i++) {
@ -533,7 +536,7 @@ const Device = GObject.registerClass({
if (val.unpack() === actionName)
return i;
} catch (e) {
} catch {
continue;
}
}
@ -546,7 +549,7 @@ const Device = GObject.registerClass({
*
* @param {Gio.MenuItem} menuItem - A GMenuItem
* @param {number} [index] - The position to place the item
* @return {number} The position the item was placed
* @returns {number} The position the item was placed
*/
addMenuItem(menuItem, index = -1) {
try {
@ -570,7 +573,7 @@ const Device = GObject.registerClass({
* @param {number} [index] - The position to place the item
* @param {string} label - A label for the item
* @param {string} icon_name - A themed icon name for the item
* @return {number} The position the item was placed
* @returns {number} The position the item was placed
*/
addMenuAction(action, index = -1, label, icon_name) {
try {
@ -600,7 +603,7 @@ const Device = GObject.registerClass({
* Remove a GAction from the top level of the device menu by action name
*
* @param {string} actionName - A GAction name, including scope
* @return {number} The position the item was removed from or -1
* @returns {number} The position the item was removed from or -1
*/
removeMenuAction(actionName) {
try {
@ -631,7 +634,7 @@ const Device = GObject.registerClass({
/**
* Show a device notification.
*
* @param {Object} params - A dictionary of notification parameters
* @param {object} params - A dictionary of notification parameters
* @param {number} [params.id] - A UNIX epoch timestamp (ms)
* @param {string} [params.title] - A title
* @param {string} [params.body] - A body
@ -728,7 +731,7 @@ const Device = GObject.registerClass({
/**
* Create a transfer object.
*
* @return {Core.Transfer} A new transfer
* @returns {Core.Transfer} A new transfer
*/
createTransfer() {
const transfer = new Core.Transfer({device: this});
@ -747,13 +750,11 @@ const Device = GObject.registerClass({
* Reject the transfer payload described by @packet.
*
* @param {Core.Packet} packet - A packet
* @return {Promise} A promise for the operation
* @returns {void}
*/
rejectTransfer(packet) {
if (!packet || !packet.hasPayload())
return;
return this.channel.rejectTransfer(packet);
if (packet?.hasPayload())
return this.channel.rejectTransfer(packet);
}
openPath(action, parameter) {
@ -816,15 +817,19 @@ const Device = GObject.registerClass({
// The device thinks we're unpaired
} else if (this.paired) {
this._setPaired(true);
this._pairingTimestamp = Math.floor(Date.now() / 1000);
this.sendPacket({
type: 'kdeconnect.pair',
body: {pair: true},
body: {
pair: true,
timestamp: this._pairingTimestamp,
},
});
this._triggerPlugins();
// The device is requesting pairing
} else {
this._notifyPairRequest();
this._notifyPairRequest(packet.body?.timestamp);
}
// Device is requesting unpairing/rejecting our request
} else {
@ -835,11 +840,14 @@ const Device = GObject.registerClass({
/**
* Notify the user of an incoming pair request and set a 30s timeout
*
* @param {number} [timestamp] - Timestamp for the pair request
*/
_notifyPairRequest() {
_notifyPairRequest(timestamp = 0) {
// Reset any active request
this._resetPairRequest();
this._pairingTimestamp = timestamp;
this.showNotification({
id: 'pair-request',
// TRANSLATORS: eg. Pair Request from Google Pixel
@ -884,6 +892,8 @@ const Device = GObject.registerClass({
GLib.source_remove(this._outgoingPairRequest);
this._outgoingPairRequest = 0;
}
this._pairingTimestamp = 0;
}
/**
@ -931,8 +941,24 @@ const Device = GObject.registerClass({
// If we're accepting an incoming pair request, set the internal
// paired state and send the confirmation before loading plugins.
if (this._incomingPairRequest) {
this._setPaired(true);
if (this.identity?.body.protocolVersion >= 8) {
const currentTimestamp = Math.floor(Date.now() / 1000);
const diffTimestamp = Number.abs(this._pairingTimestamp - currentTimestamp);
if (diffTimestamp > ALLOWED_TIMESTAMP_TIME_DIFFERENCE_SECONDS) {
this._setPaired(false);
this.showNotification({
id: 'pair-request',
// TRANSLATORS: eg. Failed to pair with Google Pixel
title: _('Failed to pair with %s').format(this.name),
body: _('Device clocks are out of sync'),
icon: new Gio.ThemedIcon({name: 'dialog-warning-symbolic'}),
priority: Gio.NotificationPriority.URGENT,
});
return;
}
}
this._setPaired(true);
this.sendPacket({
type: 'kdeconnect.pair',
body: {pair: true},
@ -945,9 +971,13 @@ const Device = GObject.registerClass({
} else if (!this.paired) {
this._resetPairRequest();
this._pairingTimestamp = Math.floor(Date.now() / 1000);
this.sendPacket({
type: 'kdeconnect.pair',
body: {pair: true},
body: {
pair: true,
timestamp: this._pairingTimestamp,
},
});
this._outgoingPairRequest = GLib.timeout_add_seconds(

View File

@ -9,7 +9,7 @@ import GIRepository from 'gi://GIRepository';
import GLib from 'gi://GLib';
import Config from '../config.js';
import setup, {setupGettext} from '../utils/setup.js';
import {setup, setupGettext} from '../utils/setup.js';
// Promise Wrappers
@ -163,7 +163,7 @@ if (!globalThis.HAVE_GNOME) {
* 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 ' ()-+'
* @returns {string} Return the string stripped of leading 0, and ' ()-+'
*/
String.prototype.toPhoneNumber = function () {
const strippedNumber = this.replace(/^0*|[ ()+-]/g, '');
@ -179,7 +179,7 @@ String.prototype.toPhoneNumber = function () {
* 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
* @returns {boolean} If `this` and @number are equivalent phone numbers
*/
String.prototype.equalsPhoneNumber = function (number) {
const a = this.toPhoneNumber();
@ -212,12 +212,12 @@ Gio.File.rm_rf = function (file) {
Gio.File.rm_rf(iter.get_child(info));
iter.close(null);
} catch (e) {
} catch {
// Silence errors
}
file.delete(null);
} catch (e) {
} catch {
// Silence errors
}
};
@ -227,7 +227,7 @@ Gio.File.rm_rf = function (file) {
* 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
* @returns {GLib.Variant} The resulting GVariant
*/
function _full_pack(obj) {
let packed;
@ -283,7 +283,7 @@ 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
* @returns {*} The resulting object
*/
function _full_unpack(obj) {
obj = (obj === undefined) ? this : obj;
@ -310,7 +310,7 @@ function _full_unpack(obj) {
unpacked[key] = Gio.Icon.deserialize(value);
else
unpacked[key] = _full_unpack(value);
} catch (e) {
} catch {
unpacked[key] = _full_unpack(value);
}
}
@ -326,8 +326,8 @@ 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.
* 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.
@ -337,9 +337,16 @@ GLib.Variant.prototype.full_unpack = _full_unpack;
* @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
* @returns {Gio.TlsCertificate} A TLS certificate
*/
Gio.TlsCertificate.new_for_paths = function (certPath, keyPath, commonName = null) {
if (GLib.find_program_in_path(Config.OPENSSL_PATH) === null) {
const error = new Error();
error.name = _('OpenSSL not found');
error.url = `${Config.PACKAGE_URL}/wiki/Error#openssl-not-found`;
throw error;
}
// 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);
@ -348,17 +355,18 @@ Gio.TlsCertificate.new_for_paths = function (certPath, keyPath, commonName = nul
if (!certExists || !keyExists) {
// If we weren't passed a common name, generate a random one
if (!commonName)
commonName = GLib.uuid_string_random();
commonName = GLib.uuid_string_random().replaceAll('-', '');
const proc = new Gio.Subprocess({
argv: [
Config.OPENSSL_PATH, 'req',
'-new', '-x509', '-sha256',
'-out', certPath,
'-newkey', 'rsa:4096', '-nodes',
'-newkey', 'ec',
'-pkeyopt', 'ec_paramgen_curve:prime256v1',
'-keyout', keyPath,
'-new', '-x509', '-nodes',
'-days', '3650',
'-subj', `/O=andyholmes.github.io/OU=GSConnect/CN=${commonName}`,
'-out', certPath,
],
flags: (Gio.SubprocessFlags.STDOUT_SILENCE |
Gio.SubprocessFlags.STDERR_SILENCE),
@ -396,7 +404,7 @@ Object.defineProperties(Gio.TlsCertificate.prototype, {
/**
* Get just the pubkey as a DER ByteArray of a certificate.
*
* @return {GLib.Bytes} The pubkey as DER of the certificate.
* @returns {GLib.Bytes} The pubkey as DER of the certificate.
*/
'pubkey_der': {
value: function () {

View File

@ -7,6 +7,7 @@ import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Config from '../config.js';
import * as Core from './core.js';
import * as DBus from './utils/dbus.js';
import Device from './device.js';
@ -42,6 +43,13 @@ const Manager = GObject.registerClass({
GObject.ParamFlags.READWRITE,
false
),
'certificate': GObject.ParamSpec.object(
'certificate',
'Certificate',
'The local TLS certificate',
GObject.ParamFlags.READABLE,
Gio.TlsCertificate
),
'discoverable': GObject.ParamSpec.boolean(
'discoverable',
'Discoverable',
@ -53,7 +61,7 @@ const Manager = GObject.registerClass({
'id',
'Id',
'The hostname or other network unique id',
GObject.ParamFlags.READWRITE,
GObject.ParamFlags.READABLE,
null
),
'name': GObject.ParamSpec.string(
@ -92,6 +100,17 @@ const Manager = GObject.registerClass({
return this._backends;
}
get certificate() {
if (this._certificate === undefined) {
this._certificate = Gio.TlsCertificate.new_for_paths(
GLib.build_filenamev([Config.CONFIGDIR, 'certificate.pem']),
GLib.build_filenamev([Config.CONFIGDIR, 'private.pem']),
null);
}
return this._certificate;
}
get debug() {
if (this._debug === undefined)
this._debug = this.settings.get_boolean('debug');
@ -156,18 +175,7 @@ const Manager = GObject.registerClass({
}
get id() {
if (this._id === undefined)
this._id = this.settings.get_string('id');
return this._id;
}
set id(value) {
if (this.id === value)
return;
this._id = value;
this.notify('id');
return this.certificate.common_name;
}
get name() {
@ -217,17 +225,12 @@ const Manager = GObject.registerClass({
* GSettings
*/
_initSettings() {
// Initialize the ID and name of the service
if (this.settings.get_string('id').length === 0)
this.settings.set_string('id', GLib.uuid_string_random());
if (this.settings.get_string('name').length === 0)
this.settings.set_string('name', GLib.get_host_name());
// Bound Properties
this.settings.bind('debug', this, 'debug', 0);
this.settings.bind('discoverable', this, 'discoverable', 0);
this.settings.bind('id', this, 'id', 0);
this.settings.bind('name', this, 'name', 0);
}
@ -382,7 +385,7 @@ const Manager = GObject.registerClass({
* of known devices if it doesn't exist.
*
* @param {Core.Packet} packet - An identity packet for the device
* @return {Device} A device object
* @returns {Device} A device object
*/
_ensureDevice(packet) {
let device = this.devices.get(packet.body.deviceId);
@ -422,7 +425,7 @@ const Manager = GObject.registerClass({
*/
_removeDevice(id) {
// Delete all GSettings
const settings_path = `/org/gnome/shell/extensions/gsconnect/${id}/`;
const settings_path = `/org/gnome/shell/extensions/gsconnect/device/${id}/`;
GLib.spawn_command_line_async(`dconf reset -f ${settings_path}`);
// Delete the cache
@ -438,7 +441,7 @@ const Manager = GObject.registerClass({
* A GSourceFunc that tries to reconnect to each paired device, while
* pruning unpaired devices that have disconnected.
*
* @return {boolean} Always %true
* @returns {boolean} Always %true
*/
_reconnect() {
for (const [id, device] of this.devices) {

View File

@ -14,7 +14,7 @@ import system from 'system';
let GioUnix;
try {
GioUnix = (await import('gi://GioUnix?version=2.0')).default;
} catch (e) {
} catch {
GioUnix = {
InputStream: Gio.UnixInputStream,
OutputStream: Gio.UnixOutputStream,
@ -142,7 +142,7 @@ const NativeMessagingHost = GObject.registerClass({
}
return GLib.SOURCE_CONTINUE;
} catch (e) {
} catch {
this.quit();
}
}
@ -152,7 +152,7 @@ const NativeMessagingHost = GObject.registerClass({
const data = JSON.stringify(message);
this._stdout.put_int32(data.length, null);
this._stdout.put_string(data, null);
} catch (e) {
} catch {
this.quit();
}
}
@ -186,7 +186,7 @@ const NativeMessagingHost = GObject.registerClass({
_proxyGetter(name) {
try {
return this.get_cached_property(name).unpack();
} catch (e) {
} catch {
return null;
}
}
@ -222,4 +222,3 @@ const NativeMessagingHost = GObject.registerClass({
// NOTE: must not pass ARGV
await (new NativeMessagingHost()).runAsync([system.programInvocationName]);

View File

@ -7,6 +7,7 @@ import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Config from '../config.js';
import * as Core from './core.js';
import plugins from './plugins/index.js';
@ -248,4 +249,3 @@ const Plugin = GObject.registerClass({
});
export default Plugin;

View File

@ -7,6 +7,7 @@ import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import * as Components from '../components/index.js';
import * as Core from '../core.js';
import Plugin from '../plugin.js';

View File

@ -6,6 +6,7 @@ import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import * as Core from '../core.js';
import Plugin from '../plugin.js';

View File

@ -7,6 +7,7 @@ import GObject from 'gi://GObject';
import Plugin from '../plugin.js';
import Contacts from '../components/contacts.js';
import * as Core from '../core.js';
/*
* We prefer libebook's vCard parser if it's available
@ -18,7 +19,7 @@ export const setEBookContacts = (ebook) => { // This function is only for tests
try {
EBookContacts = (await import('gi://EBookContacts')).default;
} catch (e) {
} catch {
EBookContacts = null;
}
@ -150,7 +151,7 @@ const ContactsPlugin = GObject.registerClass({
* See: https://github.com/mathiasbynens/quoted-printable/blob/master/src/quoted-printable.js
*
* @param {string} input - The QUOTED-PRINTABLE string
* @return {string} The decoded string
* @returns {string} The decoded string
*/
_decodeQuotedPrintable(input) {
return input
@ -171,7 +172,7 @@ const ContactsPlugin = GObject.registerClass({
* See: https://github.com/kvz/locutus/blob/master/src/php/xml/utf8_decode.js
*
* @param {string} input - The UTF-8 string
* @return {string} The decoded string
* @returns {string} The decoded string
*/
_decodeUTF8(input) {
try {
@ -215,7 +216,7 @@ const ContactsPlugin = GObject.registerClass({
return output.join('');
// Fallback to old unfaithful
} catch (e) {
} catch {
try {
return decodeURIComponent(escape(input));
@ -233,7 +234,7 @@ const ContactsPlugin = GObject.registerClass({
* See: http://jsfiddle.net/ARTsinn/P2t2P/
*
* @param {string} vcard_data - The raw VCard data
* @return {Object} dictionary of vCard data
* @returns {object} dictionary of vCard data
*/
_parseVCard21(vcard_data) {
// vcard skeleton

View File

@ -183,7 +183,7 @@ const MousepadPlugin = GObject.registerClass({
/**
* Handle a input event.
*
* @param {Object} input - The body of a `kdeconnect.mousepad.request`
* @param {object} input - The body of a `kdeconnect.mousepad.request`
*/
_handleInput(input) {
if (!this.settings.get_boolean('share-control'))
@ -285,7 +285,7 @@ const MousepadPlugin = GObject.registerClass({
/**
* Handle an echo/ACK of a event we sent, displaying it the dialog entry.
*
* @param {Object} input - The body of a `kdeconnect.mousepad.echo`
* @param {object} input - The body of a `kdeconnect.mousepad.echo`
*/
_handleEcho(input) {
if (!this._dialog || !this._dialog.visible)
@ -308,7 +308,7 @@ const MousepadPlugin = GObject.registerClass({
* Handle a state change from the remote keyboard. This is an indication
* that the remote keyboard is ready to accept input.
*
* @param {Object} packet - A `kdeconnect.mousepad.keyboardstate` packet
* @param {object} packet - A `kdeconnect.mousepad.keyboardstate` packet
*/
_handleState(packet) {
this._state = !!packet.body.state;
@ -318,7 +318,7 @@ const MousepadPlugin = GObject.registerClass({
/**
* Send an echo/ACK of @input, if requested
*
* @param {Object} input - The body of a 'kdeconnect.mousepad.request'
* @param {object} input - The body of a 'kdeconnect.mousepad.request'
*/
_sendEcho(input) {
if (!input.sendAck)
@ -335,8 +335,6 @@ const MousepadPlugin = GObject.registerClass({
/**
* Send the local keyboard state
*
* @param {boolean} state - Whether we're ready to accept input
*/
_sendState() {
this.device.sendPacket({

View File

@ -8,6 +8,7 @@ import GObject from 'gi://GObject';
import * as Components from '../components/index.js';
import Config from '../../config.js';
import * as Core from '../core.js';
import * as DBus from '../utils/dbus.js';
import {Player} from '../components/mpris.js';
import Plugin from '../plugin.js';
@ -42,6 +43,8 @@ const MPRISPlugin = GObject.registerClass({
this._transferring = new WeakSet();
this._updating = new WeakSet();
this._queueTimers = new Map();
this._mpris = Components.acquire('mpris');
this._playerAddedId = this._mpris.connect(
@ -75,6 +78,12 @@ const MPRISPlugin = GObject.registerClass({
disconnected() {
super.disconnected();
for (const [identity, timer] of this._queueTimers) {
if (timer)
GLib.source_remove(timer);
this._queueTimers.delete(identity);
}
for (const [identity, player] of this._players) {
this._players.delete(identity);
player.destroy();
@ -144,7 +153,7 @@ const MPRISPlugin = GObject.registerClass({
/**
* Handle an update for a remote player.
*
* @param {Object} packet - A `kdeconnect.mpris` packet
* @param {object} packet - A `kdeconnect.mpris` packet
*/
_handlePlayerUpdate(packet) {
const player = this._players.get(packet.body.player);
@ -174,7 +183,7 @@ const MPRISPlugin = GObject.registerClass({
* Handle a request for player information or action.
*
* @param {Core.Packet} packet - a `kdeconnect.mpris.request`
* @return {undefined} no return value
* @returns {undefined} no return value
*/
_handleRequest(packet) {
// A request for the list of players
@ -258,18 +267,35 @@ const MPRISPlugin = GObject.registerClass({
}
}
// Information Request
let hasResponse = false;
if (packet.body.hasOwnProperty('requestNowPlaying') ||
packet.body.hasOwnProperty('requestVolume')) {
const response = this._getUpdate(player.Identity, packet);
this._sendUpdate(player, response);
}
const response = {
type: 'kdeconnect.mpris',
body: {
player: packet.body.player,
},
};
} catch (e) {
debug(e, this.device.name);
} finally {
this._updating.delete(player);
}
}
// Respond to information request (or push updated information)
_getUpdate(identity, packet) {
const player = this._mpris?.getPlayer(identity);
if (!player)
return;
const response = {
type: 'kdeconnect.mpris',
body: {
player: player.Identity,
},
};
try {
if (packet.body.hasOwnProperty('requestNowPlaying')) {
hasResponse = true;
Object.assign(response.body, {
pos: Math.floor(player.Position / 1000),
@ -330,31 +356,61 @@ const MPRISPlugin = GObject.registerClass({
}
}
if (packet.body.hasOwnProperty('requestVolume')) {
hasResponse = true;
if (packet.body.hasOwnProperty('requestVolume'))
response.body.volume = Math.floor(player.Volume * 100);
}
if (hasResponse)
this.device.sendPacket(response);
return response;
} catch (e) {
debug(e, this.device.name);
} finally {
this._updating.delete(player);
}
}
_sendUpdate(player, packet = null) {
if (!player || (!packet && this._updating.has(player)))
return GLib.SOURCE_REMOVE;
debug(`Sending update for ${player.Identity}`);
this._updating.add(player);
if (this._queueTimers.has(player.Identity)) {
const timer_id = this._queueTimers.get(player.Identity);
if (timer_id) {
debug(`Stopping timer id ${timer_id}`);
GLib.source_remove(timer_id);
}
this._queueTimers.delete(player.Identity);
}
if (!packet) {
packet = this._getUpdate(player.Identity, {
body: {
requestNowPlaying: true,
requestVolume: true,
},
}, false);
}
this.device.sendPacket(packet);
this._updating.delete(player);
return GLib.SOURCE_REMOVE;
}
_onPlayerChanged(mpris, player) {
if (!this.settings.get_boolean('share-players'))
return;
this._handleCommand({
body: {
player: player.Identity,
requestNowPlaying: true,
requestVolume: true,
},
});
// Set a timer to send the updated state after a short delay.
// Allows further state changes to be bundled into a single packet.
if (this._queueTimers.has(player.Identity))
return;
this._queueTimers.set(player.Identity, 0);
const timer_id = GLib.timeout_add(
GLib.PRIORITY_DEFAULT,
250, // ms (0.25 seconds)
this._sendUpdate.bind(this, player)
);
this._queueTimers.set(player.Identity, timer_id);
debug(`Set update timer id ${timer_id} for ${player.Identity}`);
}
_onPlayerSeeked(mpris, player, offset) {
@ -434,6 +490,11 @@ const MPRISPlugin = GObject.registerClass({
}
destroy() {
for (const [identity, timer] of this._queueTimers) {
this._queueTimers.delete(identity);
GLib.source_remove(timer);
}
if (this._mpris !== undefined) {
this._mpris.disconnect(this._playerAddedId);
this._mpris.disconnect(this._playerRemovedId);

View File

@ -8,6 +8,7 @@ import GObject from 'gi://GObject';
import Gtk from 'gi://Gtk';
import * as Components from '../components/index.js';
import * as Core from '../core.js';
import Config from '../../config.js';
import Plugin from '../plugin.js';
import ReplyDialog from '../ui/notification.js';
@ -103,7 +104,7 @@ const SMS_APPS = [
* Try to determine if an notification is from an SMS app
*
* @param {Core.Packet} packet - A `kdeconnect.notification`
* @return {boolean} Whether the notification is from an SMS app
* @returns {boolean} Whether the notification is from an SMS app
*/
function _isSmsNotification(packet) {
const id = packet.body.id;
@ -123,8 +124,8 @@ function _isSmsNotification(packet) {
/**
* Remove a local libnotify or Gtk notification.
*
* @param {String|Number} id - Gtk (string) or libnotify id (uint32)
* @param {String|null} application - Application Id if Gtk or null
* @param {string | number} id - Gtk (string) or libnotify id (uint32)
* @param {string | null} application - Application Id if Gtk or null
*/
function _removeNotification(id, application = null) {
let name, path, method, variant;
@ -416,7 +417,7 @@ const NotificationPlugin = GObject.registerClass({
*
* @param {Core.Packet} packet - A `kdeconnect.notification`
* @param {Gio.Icon|string|null} icon - An icon or %null
* @return {Promise} A promise for the operation
* @returns {Promise} A promise for the operation
*/
_uploadIcon(packet, icon = null) {
// Normalize strings into GIcons
@ -438,7 +439,7 @@ const NotificationPlugin = GObject.registerClass({
/**
* Send a local notification to the remote device.
*
* @param {Object} notif - A dictionary of notification parameters
* @param {object} notif - A dictionary of notification parameters
* @param {string} notif.appName - The notifying application
* @param {string} notif.id - The notification ID
* @param {string} notif.title - The notification title
@ -631,7 +632,7 @@ const NotificationPlugin = GObject.registerClass({
*
* @param {string} uuid - The requestReplyId for the repliable notification
* @param {string} message - The message to reply with
* @param {Object} notification - The original notification packet
* @param {object} notification - The original notification packet
*/
replyNotification(uuid, message, notification) {
// If this happens for some reason, things will explode

View File

@ -150,14 +150,14 @@ const RunCommandPlugin = GObject.registerClass({
* Parse the response to a request for the remote command list. Remove the
* command menu if there are no commands, otherwise amend the menu.
*
* @param {string|Object[]} commandList - A list of remote commands
* @param {string | object[]} commandList - A list of remote commands
*/
_handleCommandList(commandList) {
// See: https://github.com/GSConnect/gnome-shell-extension-gsconnect/issues/1051
if (typeof commandList === 'string') {
try {
commandList = JSON.parse(commandList);
} catch (e) {
} catch {
commandList = {};
}
}

View File

@ -7,6 +7,7 @@ import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import Config from '../../config.js';
import * as Core from '../core.js';
import Plugin from '../plugin.js';
@ -246,7 +247,7 @@ const SFTPPlugin = GObject.registerClass({
* Add GSConnect's private key identity to the authentication agent so our
* identity can be verified by Android during private key authentication.
*
* @return {Promise} A promise for the operation
* @returns {Promise} A promise for the operation
*/
async _addPrivateKey() {
const ssh_add = this._launcher.spawnv([

View File

@ -443,7 +443,7 @@ const FileChooserDialog = GObject.registerClass({
chooser.preview_widget.pixbuf = pixbuf;
chooser.preview_widget.visible = true;
chooser.preview_widget_active = true;
} catch (e) {
} catch {
chooser.preview_widget.visible = false;
chooser.preview_widget_active = false;
}

View File

@ -206,7 +206,7 @@ const SMSPlugin = GObject.registerClass({
/**
* Handle a digest of threads.
*
* @param {Object[]} messages - A list of message objects
* @param {object[]} messages - A list of message objects
* @param {string[]} thread_ids - A list of thread IDs as strings
*/
_handleDigest(messages, thread_ids) {
@ -244,7 +244,7 @@ const SMSPlugin = GObject.registerClass({
/**
* Handle a new single message
*
* @param {Object} message - A message object
* @param {object} message - A message object
*/
_handleMessage(message) {
let conversation = null;
@ -261,7 +261,7 @@ const SMSPlugin = GObject.registerClass({
/**
* Parse a conversation (thread of messages) and sort them
*
* @param {Object[]} thread - A list of sms message objects from a thread
* @param {object[]} thread - A list of sms message objects from a thread
*/
_handleThread(thread) {
// If there are no addresses this will cause major problems...
@ -300,7 +300,7 @@ const SMSPlugin = GObject.registerClass({
/**
* Handle a response to telephony.request_conversation(s)
*
* @param {Object[]} messages - A list of sms message objects
* @param {object[]} messages - A list of sms message objects
*/
_handleMessages(messages) {
try {
@ -394,7 +394,7 @@ const SMSPlugin = GObject.registerClass({
/**
* Send a message
*
* @param {Object[]} addresses - A list of address objects
* @param {object[]} addresses - A list of address objects
* @param {string} messageBody - The message text
* @param {number} [event] - An event bitmask
* @param {boolean} [forceSms] - Whether to force SMS
@ -508,8 +508,8 @@ const SMSPlugin = GObject.registerClass({
/**
* Try to find a thread_id in @smsPlugin for @addresses.
*
* @param {Object[]} addresses - a list of address objects
* @return {string|null} a thread ID
* @param {object[]} addresses - a list of address objects
* @returns {string|null} a thread ID
*/
getThreadIdForAddresses(addresses = []) {
const threads = Object.values(this.threads);

View File

@ -2,13 +2,27 @@
//
// SPDX-License-Identifier: GPL-2.0-or-later
import GIRepository from 'gi://GIRepository';
import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import * as Components from '../components/index.js';
import Config from '../../config.js';
import * as Core from '../core.js';
import Plugin from '../plugin.js';
let Gvc = null;
try {
// Add gnome-shell's typelib dir to the search path
const typelibDir = GLib.build_filenamev([Config.GNOME_SHELL_LIBDIR, 'gnome-shell']);
GIRepository.Repository.prepend_search_path(typelibDir);
GIRepository.Repository.prepend_library_path(typelibDir);
Gvc = (await import('gi://Gvc')).default;
} catch {}
export const Metadata = {
label: _('System Volume'),
description: _('Enable the paired device to control the system volume'),
@ -73,12 +87,6 @@ const SystemVolumePlugin = GObject.registerClass({
}
}
connected() {
super.connected();
this._sendSinkList();
}
/**
* Handle a request to change an output
*
@ -121,7 +129,7 @@ const SystemVolumePlugin = GObject.registerClass({
* Update the cache for @stream
*
* @param {Gvc.MixerStream} stream - The stream to cache
* @return {Object} The updated cache object
* @returns {object} The updated cache object
*/
_updateCache(stream) {
const state = {

View File

@ -8,6 +8,7 @@ import GLib from 'gi://GLib';
import GObject from 'gi://GObject';
import * as Components from '../components/index.js';
import * as Core from '../core.js';
import Plugin from '../plugin.js';
@ -114,7 +115,7 @@ const TelephonyPlugin = GObject.registerClass({
* Load a Gdk.Pixbuf from base64 encoded data
*
* @param {string} data - Base64 encoded JPEG data
* @return {Gdk.Pixbuf|null} A contact photo
* @returns {GdkPixbuf.Pixbuf|null} A contact photo
*/
_getThumbnailPixbuf(data) {
const loader = new GdkPixbuf.PixbufLoader();

View File

@ -16,7 +16,7 @@ import system from 'system';
*
* @param {*} [salt] - If not %null, will be used as salt for generating a color
* @param {number} alpha - A value in the [0...1] range for the alpha channel
* @return {Gdk.RGBA} A new Gdk.RGBA object generated from the input
* @returns {Gdk.RGBA} A new Gdk.RGBA object generated from the input
*/
function randomRGBA(salt = null, alpha = 1.0) {
let red, green, blue;
@ -41,7 +41,7 @@ function randomRGBA(salt = null, alpha = 1.0) {
* See: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
*
* @param {Gdk.RGBA} rgba - A GdkRGBA object
* @return {number} The relative luminance of the color
* @returns {number} The relative luminance of the color
*/
function relativeLuminance(rgba) {
const {red, green, blue} = rgba;
@ -59,7 +59,7 @@ function relativeLuminance(rgba) {
* See: https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
*
* @param {Gdk.RGBA} rgba - A GdkRGBA object for the background color
* @return {Gdk.RGBA} A GdkRGBA object for the foreground color
* @returns {Gdk.RGBA} A GdkRGBA object for the foreground color
*/
function getFgRGBA(rgba) {
const bgLuminance = relativeLuminance(rgba);
@ -78,7 +78,7 @@ function getFgRGBA(rgba) {
* @param {string} path - A local file path
* @param {number} size - Size in pixels
* @param {scale} [scale] - Scale factor for the size
* @return {Gdk.Pixbuf} A pixbuf
* @returns {Gdk.Pixbuf} A pixbuf
*/
function getPixbufForPath(path, size, scale = 1.0) {
let data, loader;
@ -107,6 +107,15 @@ function getPixbufForPath(path, size, scale = 1.0) {
return pixbuf.scale_simple(size, size, GdkPixbuf.InterpType.HYPER);
}
/**
* Retrieve the GdkPixbuf for a named icon
*
* @param {string} name - The icon name to load
* @param {number} size - The pixel size requested
* @param {number} scale - The scale multiplier
* @param {string} bgColor - The background color the icon will be used against
* @returns {GdkPixbuf.pixbuf|null} The icon image
*/
function getPixbufForIcon(name, size, scale, bgColor) {
const color = getFgRGBA(bgColor);
const theme = Gtk.IconTheme.get_default();
@ -126,7 +135,7 @@ function getPixbufForIcon(name, size, scale, bgColor) {
* See: http://www.ietf.org/rfc/rfc2426.txt
*
* @param {string} type - An RFC2426 phone number type
* @return {string} A localized string like 'Mobile'
* @returns {string} A localized string like 'Mobile'
*/
function getNumberTypeLabel(type) {
if (type.includes('fax'))
@ -152,9 +161,9 @@ function getNumberTypeLabel(type) {
/**
* Get a display number from @contact for @address.
*
* @param {Object} contact - A contact object
* @param {object} contact - A contact object
* @param {string} address - A phone number
* @return {string} A (possibly) better display number for the address
* @returns {string} A (possibly) better display number for the address
*/
export function getDisplayNumber(contact, address) {
const number = address.toPhoneNumber();
@ -623,7 +632,7 @@ export const ContactChooser = GObject.registerClass({
/**
* Get a dictionary of number-contact pairs for each selected phone number.
*
* @return {Object[]} A dictionary of contacts
* @returns {object[]} A dictionary of contacts
*/
getSelected() {
try {
@ -639,4 +648,3 @@ export const ContactChooser = GObject.registerClass({
}
}
});

View File

@ -92,7 +92,7 @@ const _cFormat = new Intl.DateTimeFormat('default', {
* Return a human-readable timestamp, formatted for longer contexts.
*
* @param {number} time - Milliseconds since the epoch (local time)
* @return {string} A localized timestamp similar to what Android Messages uses
* @returns {string} A localized timestamp similar to what Android Messages uses
*/
function getTime(time) {
const date = new Date(time);
@ -134,7 +134,7 @@ function getTime(time) {
* Return a human-readable timestamp, formatted for shorter contexts.
*
* @param {number} time - Milliseconds since the epoch (local time)
* @return {string} A localized timestamp similar to what Android Messages uses
* @returns {string} A localized timestamp similar to what Android Messages uses
*/
function getShortTime(time) {
const date = new Date(time);
@ -175,13 +175,19 @@ function getShortTime(time) {
* Return a human-readable timestamp, similar to `strftime()` with `%c`.
*
* @param {number} time - Milliseconds since the epoch (local time)
* @return {string} A localized timestamp
* @returns {string} A localized timestamp
*/
function getDetailedTime(time) {
return _cFormat.format(time);
}
/**
* Make the avatar for an incoming message visible or invisible
*
* @param {ConversationMessage} row - The message row to modify
* @param {boolean} visible - Whether the avatar should be visible
*/
function setAvatarVisible(row, visible) {
const incoming = row.message.type === Sms.MessageBox.INBOX;
@ -546,8 +552,8 @@ const Conversation = GObject.registerClass({
* Create a message row, ensuring a contact object has been retrieved or
* generated for the message.
*
* @param {Object} message - A dictionary of message data
* @return {ConversationMessage} A message row
* @param {object} message - A dictionary of message data
* @returns {ConversationMessage} A message row
*/
_createMessageRow(message) {
// Ensure we have a contact
@ -655,7 +661,7 @@ const Conversation = GObject.registerClass({
/**
* Log the next message in the conversation.
*
* @param {Object} message - A message object
* @param {object} message - A message object
*/
logNext(message) {
try {
@ -1073,8 +1079,8 @@ export const Window = GObject.registerClass({
/**
* Find the thread row for @contacts
*
* @param {Object[]} contacts - A contact group
* @return {ConversationSummary|null} The thread row or %null
* @param {object[]} contacts - A contact group
* @returns {ConversationSummary|null} The thread row or %null
*/
_getRowForContacts(contacts) {
const addresses = Object.keys(contacts).map(address => {
@ -1156,8 +1162,8 @@ export const Window = GObject.registerClass({
/**
* Try and find an existing conversation widget for @message.
*
* @param {Object} message - A message object
* @return {Conversation|null} A conversation widget or %null
* @param {object} message - A message object
* @returns {Conversation|null} A conversation widget or %null
*/
getConversationForMessage(message) {
// TODO: This shouldn't happen?
@ -1317,4 +1323,3 @@ export const ConversationChooser = GObject.registerClass({
this.destroy();
}
});

View File

@ -11,12 +11,26 @@ import GObject from 'gi://GObject';
/*
* Some utility methods
*/
/**
* Convert a label from kebab-case to CamelCase.
* Will also remove snake_case separators (without capitalizing)
*
* @param {string} string - The label to reformat
* @returns {string} The CamelCased label
*/
function toDBusCase(string) {
return string.replace(/(?:^\w|[A-Z]|\b\w)/g, (ltr, offset) => {
return ltr.toUpperCase();
}).replace(/[\s_-]+/g, '');
}
/**
* Convert a label from CamelCase to snake_case.
*
* @param {string} string - The label to reformat
* @returns {string} - The snake_cased label
*/
function toUnderscoreCase(string) {
return string.replace(/(?:^\w|[A-Z]|_|\b\w)/g, (ltr, offset) => {
if (ltr === '_')
@ -95,7 +109,7 @@ export const Interface = GObject.registerClass({
* 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 {Gio.DBusInterface} 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
@ -231,7 +245,7 @@ export const Interface = GObject.registerClass({
*
* @param {Gio.BusType} [busType] - a Gio.BusType constant
* @param {Gio.Cancellable} [cancellable] - an optional Gio.Cancellable
* @return {Promise<Gio.DBusConnection>} A new DBus connection
* @returns {Promise<Gio.DBusConnection>} A new DBus connection
*/
export function newConnection(busType = Gio.BusType.SESSION, cancellable = null) {
return new Promise((resolve, reject) => {
@ -252,4 +266,3 @@ export function newConnection(busType = Gio.BusType.SESSION, cancellable = null)
});
}

View File

@ -79,7 +79,7 @@ const _numberRegex = new RegExp(
* 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
* @returns {object[]} the list of match objects, as described above
*/
export function findUrls(str) {
_urlRegexp.lastIndex = 0;
@ -103,7 +103,7 @@ export function findUrls(str) {
*
* @param {string} str - The string to be modified
* @param {string} [title] - An optional title (eg. alt text, tooltip)
* @return {string} the modified text
* @returns {string} the modified text
*/
export function linkify(str, title = null) {
const text = GLib.markup_escape_text(str, -1);
@ -166,4 +166,3 @@ export default class URI {
return this.body ? `${uri}?body=${escape(this.body)}` : uri;
}
}

View File

@ -172,7 +172,7 @@ export const Clipboard = GObject.registerClass({
invocation.return_value(retval);
// Without a response, the client will wait for timeout
} catch (e) {
} catch {
invocation.return_dbus_error(
'org.gnome.gjs.JSError.ValueError',
'Service implementation returned an incorrect value type'
@ -183,7 +183,7 @@ export const Clipboard = GObject.registerClass({
/**
* Get the available mimetypes of the current clipboard content
*
* @return {Promise<string[]>} A list of mime-types
* @returns {Promise<string[]>} A list of mime-types
*/
GetMimetypes() {
return new Promise((resolve, reject) => {
@ -202,7 +202,7 @@ export const Clipboard = GObject.registerClass({
/**
* Get the text content of the clipboard
*
* @return {Promise<string>} Text content of the clipboard
* @returns {Promise<string>} Text content of the clipboard
*/
GetText() {
return new Promise((resolve, reject) => {
@ -241,7 +241,7 @@ export const Clipboard = GObject.registerClass({
* Set the text content of the clipboard
*
* @param {string} text - text content to set
* @return {Promise} A promise for the operation
* @returns {Promise} A promise for the operation
*/
SetText(text) {
return new Promise((resolve, reject) => {
@ -270,7 +270,7 @@ export const Clipboard = GObject.registerClass({
* Get the content of the clipboard with the type @mimetype.
*
* @param {string} mimetype - the mimetype to request
* @return {Promise<Uint8Array>} The content of the clipboard
* @returns {Promise<Uint8Array>} The content of the clipboard
*/
GetValue(mimetype) {
return new Promise((resolve, reject) => {
@ -300,7 +300,7 @@ export const Clipboard = GObject.registerClass({
*
* @param {Uint8Array} value - the value to set
* @param {string} mimetype - the mimetype of the value
* @return {Promise} - A promise for the operation
* @returns {Promise} - A promise for the operation
*/
SetValue(value, mimetype) {
return new Promise((resolve, reject) => {
@ -377,4 +377,3 @@ export function unwatchService() {
_portalId = 0;
}
}

View File

@ -10,7 +10,7 @@ import St from 'gi://St';
import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
import {getIcon} from './utils.js';
import {HAS_ST_ORIENTATION, getIcon} from './utils.js';
import Tooltip from './tooltip.js';
@ -20,7 +20,7 @@ import Tooltip from './tooltip.js';
*
* @param {Gio.MenuModel} model - The menu model containing the item
* @param {number} index - The index of the item in @model
* @return {Object} A dictionary of the item's attributes
* @returns {object} A dictionary of the item's attributes
*/
function getItemInfo(model, index) {
const info = {
@ -92,12 +92,19 @@ export class ListBox extends PopupMenu.PopupMenuSection {
this.actor.add_child(this.box);
// Submenu Container
this.sub = new St.BoxLayout({
clip_to_allocation: true,
vertical: false,
visible: false,
x_expand: true,
});
this.sub = HAS_ST_ORIENTATION
? new St.BoxLayout({
clip_to_allocation: true,
orientation: Clutter.Orientation.HORIZONTAL, // GNOME 48
visible: false,
x_expand: true,
})
: new St.BoxLayout({
clip_to_allocation: true,
vertical: false, // GNOME 46/47
visible: false,
x_expand: true,
});
this.sub.set_pivot_point(1, 1);
this.sub._delegate = this;
this.actor.add_child(this.sub);
@ -464,24 +471,38 @@ export class IconBox extends PopupMenu.PopupMenuSection {
Object.assign(this, params);
// Main Actor
this.actor = new St.BoxLayout({
vertical: true,
x_expand: true,
});
this.actor = HAS_ST_ORIENTATION
? new St.BoxLayout({
orientation: Clutter.Orientation.VERTICAL, // GNOME 48
x_expand: true,
})
: new St.BoxLayout({
vertical: true, // GNOME 46/47
x_expand: true,
});
this.actor._delegate = this;
// Button Box
this.box._delegate = this;
this.box.style_class = 'gsconnect-icon-box';
this.box.vertical = false;
if (HAS_ST_ORIENTATION)
this.box.orientation = Clutter.Orientation.HORIZONTAL; // GNOME 48
else
this.box.vertical = false; // GNOME 46/47
this.actor.add_child(this.box);
// Submenu Container
this.sub = new St.BoxLayout({
clip_to_allocation: true,
vertical: true,
x_expand: true,
});
this.sub = HAS_ST_ORIENTATION
? new St.BoxLayout({
clip_to_allocation: true,
orientation: Clutter.Orientation.VERTICAL, // GNOME 48
x_expand: true,
})
: new St.BoxLayout({
clip_to_allocation: true,
vertical: true, // GNOME 46/47
x_expand: true,
});
this.sub.connect('transitions-completed', this._onTransitionsCompleted);
this.sub._delegate = this;
this.actor.add_child(this.sub);
@ -644,4 +665,3 @@ export class IconBox extends PopupMenu.PopupMenuSection {
this._onItemsChanged(this.model, 0, 0, this.model.get_n_items());
}
}

View File

@ -49,7 +49,7 @@ export class Manager {
*
* @param {string} accelerator - An accelerator in the form '<Control>q'
* @param {Function} callback - A callback for the accelerator
* @return {number} A non-zero action id on success, or 0 on failure
* @returns {*} A non-zero action id on success, or 0 on failure
*/
add(accelerator, callback) {
try {
@ -100,4 +100,3 @@ export class Manager {
this.removeAll();
}
}

View File

@ -9,11 +9,14 @@ import St from 'gi://St';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import * as MessageTray from 'resource:///org/gnome/shell/ui/messageTray.js';
import * as Calendar from 'resource:///org/gnome/shell/ui/calendar.js';
import * as NotificationDaemon from 'resource:///org/gnome/shell/ui/notificationDaemon.js';
import {gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js';
import {getIcon} from './utils.js';
import {HAS_MESSAGELIST_NOTIFICATIONMESSAGE, getIcon} from './utils.js';
const {NotificationMessage} = HAS_MESSAGELIST_NOTIFICATIONMESSAGE
? await import('resource:///org/gnome/shell/ui/messageList.js') // GNOME 48
: await import('resource:///org/gnome/shell/ui/calendar.js'); // GNOME 46/47
const APP_ID = 'org.gnome.Shell.Extensions.GSConnect';
const APP_PATH = '/org/gnome/Shell/Extensions/GSConnect';
@ -29,6 +32,7 @@ const REPLY_REGEX = new RegExp(/^([^|]+)\|([\s\S]+)\|([0-9a-f]{8}-[0-9a-f]{4}-[1
/**
* Extracted from notificationDaemon.js, as it's no longer exported
* https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/notificationDaemon.js#L556
*
* @returns {{ 'desktop-startup-id': string }} Object with ID containing current time
*/
function getPlatformData() {
@ -45,7 +49,7 @@ const GtkNotificationDaemon = Main.notificationDaemon._gtkNotificationDaemon.con
*/
const NotificationBanner = GObject.registerClass({
GTypeName: 'GSConnectNotificationBanner',
}, class NotificationBanner extends Calendar.NotificationMessage {
}, class NotificationBanner extends NotificationMessage {
constructor(notification) {
super(notification);
@ -147,7 +151,7 @@ const NotificationBanner = GObject.registerClass({
(connection, res) => {
try {
connection.call_finish(res);
} catch (e) {
} catch {
// Silence errors
}
}
@ -198,7 +202,7 @@ const Source = GObject.registerClass({
(connection, res) => {
try {
connection.call_finish(res);
} catch (e) {
} catch {
// If we fail, reset in case we can try again
notification._remoteClosed = false;
}
@ -384,11 +388,17 @@ const _ensureAppSource = function (appId) {
};
/**
* Update the prototype for {@link GtkNotificationDaemon}.
*/
export function patchGtkNotificationDaemon() {
GtkNotificationDaemon.prototype._ensureAppSource = _ensureAppSource;
}
/**
* Restore the prototype for {@link GtkNotificationDaemon}.
*/
export function unpatchGtkNotificationDaemon() {
GtkNotificationDaemon.prototype._ensureAppSource = __ensureAppSource;
}
@ -399,6 +409,9 @@ export function unpatchGtkNotificationDaemon() {
*/
const _addNotification = NotificationDaemon.GtkNotificationDaemonAppSource.prototype.addNotification;
/**
* Update the prototype for {@link NotificationDaemon.GtkNotificationDaemonAppSource}.
*/
export function patchGtkNotificationSources() {
// eslint-disable-next-line func-style
const _withdrawGSConnectNotification = function (id, notification, reason) {
@ -433,7 +446,7 @@ export function patchGtkNotificationSources() {
(connection, res) => {
try {
connection.call_finish(res);
} catch (e) {
} catch {
// If we fail, reset in case we can try again
notification._remoteWithdrawn = false;
}
@ -445,8 +458,10 @@ export function patchGtkNotificationSources() {
}
/**
* Restore the prototype for {@link NotificationDaemon.GtkNotificationDaemonAppSource}.
*/
export function unpatchGtkNotificationSources() {
NotificationDaemon.GtkNotificationDaemonAppSource.prototype.addNotification = _addNotification;
delete NotificationDaemon.GtkNotificationDaemonAppSource.prototype._withdrawGSConnectNotification;
}

View File

@ -10,6 +10,7 @@ import St from 'gi://St';
import * as Main from 'resource:///org/gnome/shell/ui/main.js';
import {HAS_ST_ORIENTATION} from './utils.js';
/**
* An StTooltip for ClutterActors
@ -151,7 +152,15 @@ export default class Tooltip {
if (this.custom) {
this._bin.child = this.custom;
} else {
this._bin.child = new St.BoxLayout({vertical: false});
if (HAS_ST_ORIENTATION) {
// GNOME 48
this._bin.child = new St.BoxLayout(
{orientation: Clutter.Orientation.HORIZONTAL}
);
} else {
// GNOME 46/47
this._bin.child = new St.BoxLayout({vertical: false});
}
if (this.gicon) {
this._bin.child.icon = new St.Icon({

View File

@ -3,22 +3,22 @@
// SPDX-License-Identifier: GPL-2.0-or-later
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import Gtk from 'gi://Gtk';
import St from 'gi://St';
import Config from '../config.js';
import {PACKAGE_VERSION} from 'resource:///org/gnome/shell/misc/config.js';
let St = null; // St is not available for prefs.js importing this file.
try {
St = (await import('gi://St')).default;
} catch (e) { }
export const SHELL_MAJOR_VERSION = Number(PACKAGE_VERSION.split('.')[0]);
export const HAS_ST_ORIENTATION = SHELL_MAJOR_VERSION >= 48;
export const HAS_MESSAGELIST_NOTIFICATIONMESSAGE = SHELL_MAJOR_VERSION >= 48;
/**
* Get a themed icon, using fallbacks from GSConnect's GResource when necessary.
*
* @param {string} name - A themed icon name
* @return {Gio.Icon} A themed icon
* @returns {Gio.Icon} A themed icon
*/
export function getIcon(name) {
if (getIcon._resource === undefined) {
@ -64,220 +64,3 @@ export function getIcon(name) {
// Fallback to hoping it's in the theme somewhere
return new Gio.ThemedIcon({name: name});
}
/**
* Get the contents of a GResource file, replacing `@PACKAGE_DATADIR@` where
* necessary.
*
* @param {string} relativePath - A path relative to GSConnect's resource path
* @return {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
* @return {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
* @return {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]));
}
}

View File

@ -22,6 +22,13 @@ const _PROPERTIES = {
};
/**
* Initialize a Gio.DBusProxy in an awaitable manner
*
* @param {Gio.DBusProxy} proxy - The proxy object to initialize
* @param {Gio.Cancellable} [cancellable] - An optional cancellable object
* @returns {Promise} An awaitable Promise
*/
function _proxyInit(proxy, cancellable = null) {
if (proxy.__initialized !== undefined)
return Promise.resolve();
@ -127,7 +134,7 @@ export const Device = GObject.registerClass({
_get(name, fallback = null) {
try {
return this.get_cached_property(name).unpack();
} catch (e) {
} catch {
return fallback;
}
}
@ -297,7 +304,7 @@ export const Service = GObject.registerClass({
* org.freedesktop.DBus.ObjectManager.InterfacesAdded
*
* @param {string} object_path - Path interfaces have been added to
* @param {Object} interfaces - A dictionary of interface objects
* @param {object} interfaces - A dictionary of interface objects
*/
async _onInterfacesAdded(object_path, interfaces) {
try {

View File

@ -21,11 +21,228 @@ export function setupGettext() {
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 default function setup(extensionPath) {
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']);

View File

@ -141,7 +141,7 @@ export const Clipboard = GObject.registerClass(
invocation.return_value(retval);
// Without a response, the client will wait for timeout
} catch (e) {
} catch {
invocation.return_dbus_error(
'org.gnome.gjs.JSError.ValueError',
'Service implementation returned an incorrect value type'
@ -150,11 +150,10 @@ export const Clipboard = GObject.registerClass(
}
/**
* Get the available mimetypes of the current clipboard content
*
* @return {Promise<string[]>} A list of mime-types
*/
* Get the available mimetypes of the current clipboard content
*
* @returns {Promise<string[]>} A list of mime-types
*/
GetMimetypes() {
return new Promise((resolve, reject) => {
const proc = launcher.spawnv([
@ -179,10 +178,10 @@ export const Clipboard = GObject.registerClass(
}
/**
* Get the text content of the clipboard
*
* @return {Promise<string>} Text content of the clipboard
*/
* Get the text content of the clipboard
*
* @returns {Promise<string>} Text content of the clipboard
*/
GetText() {
return new Promise((resolve, reject) => {
this.GetMimetypes().then((mimetypes) => {
@ -213,11 +212,11 @@ export const Clipboard = GObject.registerClass(
}
/**
* Set the text content of the clipboard
*
* @param {string} text - text content to set
* @return {Promise} A promise for the operation
*/
* Set the text content of the clipboard
*
* @param {string} text - text content to set
* @returns {Promise} A promise for the operation
*/
SetText(text) {
return new Promise((resolve, reject) => {
try {