gnome-shell/js/ui/mpris.js
Philip Chimento 975280fc50 mpris: Validate received data against the expected types from the spec
In the wild we have buggy clients (notably Chromium 77 and earlier) that
send metadata with the wrong types. Previously, this would throw an
exception and prevent the MPRIS information from showing up in the
message list.

This changes the code to check if any incoming metadata is of the type
it is expected to be, and logs a warning if not, then continues on with
a default value.

https://gitlab.gnome.org/GNOME/gnome-shell/issues/1362
2019-10-29 19:25:16 +00:00

281 lines
8.7 KiB
JavaScript

/* exported MediaSection */
const { Gio, GObject, Shell, St } = imports.gi;
const Signals = imports.signals;
const Calendar = imports.ui.calendar;
const Main = imports.ui.main;
const MessageList = imports.ui.messageList;
const { loadInterfaceXML } = imports.misc.fileUtils;
const DBusIface = loadInterfaceXML('org.freedesktop.DBus');
const DBusProxy = Gio.DBusProxy.makeProxyWrapper(DBusIface);
const MprisIface = loadInterfaceXML('org.mpris.MediaPlayer2');
const MprisProxy = Gio.DBusProxy.makeProxyWrapper(MprisIface);
const MprisPlayerIface = loadInterfaceXML('org.mpris.MediaPlayer2.Player');
const MprisPlayerProxy = Gio.DBusProxy.makeProxyWrapper(MprisPlayerIface);
const MPRIS_PLAYER_PREFIX = 'org.mpris.MediaPlayer2.';
var MediaMessage = GObject.registerClass(
class MediaMessage extends MessageList.Message {
_init(player) {
super._init('', '');
this._player = player;
this._icon = new St.Icon({ style_class: 'media-message-cover-icon' });
this.setIcon(this._icon);
this._prevButton = this.addMediaControl('media-skip-backward-symbolic',
() => {
this._player.previous();
});
this._playPauseButton = this.addMediaControl(null,
() => {
this._player.playPause();
});
this._nextButton = this.addMediaControl('media-skip-forward-symbolic',
() => {
this._player.next();
});
this._player.connect('changed', this._update.bind(this));
this._player.connect('closed', this.close.bind(this));
this._update();
}
vfunc_clicked() {
this._player.raise();
Main.panel.closeCalendar();
}
_updateNavButton(button, sensitive) {
button.reactive = sensitive;
}
_update() {
this.setTitle(this._player.trackArtists.join(', '));
this.setBody(this._player.trackTitle);
if (this._player.trackCoverUrl) {
let file = Gio.File.new_for_uri(this._player.trackCoverUrl);
this._icon.gicon = new Gio.FileIcon({ file: file });
this._icon.remove_style_class_name('fallback');
} else {
this._icon.icon_name = 'audio-x-generic-symbolic';
this._icon.add_style_class_name('fallback');
}
let isPlaying = this._player.status == 'Playing';
let iconName = isPlaying
? 'media-playback-pause-symbolic'
: 'media-playback-start-symbolic';
this._playPauseButton.child.icon_name = iconName;
this._updateNavButton(this._prevButton, this._player.canGoPrevious);
this._updateNavButton(this._nextButton, this._player.canGoNext);
}
});
var MprisPlayer = class MprisPlayer {
constructor(busName) {
this._mprisProxy = new MprisProxy(Gio.DBus.session, busName,
'/org/mpris/MediaPlayer2',
this._onMprisProxyReady.bind(this));
this._playerProxy = new MprisPlayerProxy(Gio.DBus.session, busName,
'/org/mpris/MediaPlayer2',
this._onPlayerProxyReady.bind(this));
this._visible = false;
this._trackArtists = [];
this._trackTitle = '';
this._trackCoverUrl = '';
this._busName = busName;
}
get status() {
return this._playerProxy.PlaybackStatus;
}
get trackArtists() {
return this._trackArtists;
}
get trackTitle() {
return this._trackTitle;
}
get trackCoverUrl() {
return this._trackCoverUrl;
}
playPause() {
this._playerProxy.PlayPauseRemote();
}
get canGoNext() {
return this._playerProxy.CanGoNext;
}
next() {
this._playerProxy.NextRemote();
}
get canGoPrevious() {
return this._playerProxy.CanGoPrevious;
}
previous() {
this._playerProxy.PreviousRemote();
}
raise() {
// The remote Raise() method may run into focus stealing prevention,
// so prefer activating the app via .desktop file if possible
let app = null;
if (this._mprisProxy.DesktopEntry) {
let desktopId = `${this._mprisProxy.DesktopEntry}.desktop`;
app = Shell.AppSystem.get_default().lookup_app(desktopId);
}
if (app)
app.activate();
else if (this._mprisProxy.CanRaise)
this._mprisProxy.RaiseRemote();
}
_close() {
this._mprisProxy.disconnect(this._ownerNotifyId);
this._mprisProxy = null;
this._playerProxy.disconnect(this._propsChangedId);
this._playerProxy = null;
this.emit('closed');
}
_onMprisProxyReady() {
this._ownerNotifyId = this._mprisProxy.connect('notify::g-name-owner',
() => {
if (!this._mprisProxy.g_name_owner)
this._close();
});
}
_onPlayerProxyReady() {
this._propsChangedId = this._playerProxy.connect('g-properties-changed',
this._updateState.bind(this));
this._updateState();
}
_updateState() {
let metadata = {};
for (let prop in this._playerProxy.Metadata)
metadata[prop] = this._playerProxy.Metadata[prop].deep_unpack();
// Validate according to the spec; some clients send buggy metadata:
// https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata
this._trackArtists = metadata['xesam:artist'];
if (!Array.isArray(this._trackArtists) ||
!this._trackArtists.every(artist => typeof artist === 'string')) {
if (typeof this._trackArtists !== 'undefined')
log(`Received faulty track artist metadata from ${
this._busName}; expected an array of strings, got ${
this._trackArtists} (${typeof this._trackArtists})`);
this._trackArtists = [_("Unknown artist")];
}
this._trackTitle = metadata['xesam:title'];
if (typeof this._trackTitle !== 'string') {
if (typeof this._trackTitle !== 'undefined')
log(`Received faulty track title metadata from ${
this._busName}; expected a string, got ${
this._trackTitle} (${typeof this._trackTitle})`);
this._trackTitle = _("Unknown title");
}
this._trackCoverUrl = metadata['mpris:artUrl'];
if (typeof this._trackCoverUrl !== 'string') {
if (typeof this._trackCoverUrl !== 'undefined')
log(`Received faulty track cover art metadata from ${
this._busName}; expected a string, got ${
this._trackCoverUrl} (${typeof this._trackCoverUrl})`);
this._trackCoverUrl = '';
}
this.emit('changed');
let visible = this._playerProxy.CanPlay;
if (this._visible != visible) {
this._visible = visible;
if (visible)
this.emit('show');
else
this._close();
}
}
};
Signals.addSignalMethods(MprisPlayer.prototype);
var MediaSection = GObject.registerClass(
class MediaSection extends MessageList.MessageListSection {
_init() {
super._init();
this._players = new Map();
this._proxy = new DBusProxy(Gio.DBus.session,
'org.freedesktop.DBus',
'/org/freedesktop/DBus',
this._onProxyReady.bind(this));
}
_shouldShow() {
return !this.empty && Calendar.isToday(this._date);
}
_addPlayer(busName) {
if (this._players.get(busName))
return;
let player = new MprisPlayer(busName);
player.connect('closed',
() => {
this._players.delete(busName);
});
player.connect('show',
() => {
let message = new MediaMessage(player);
this.addMessage(message, true);
});
this._players.set(busName, player);
}
_onProxyReady() {
this._proxy.ListNamesRemote(([names]) => {
names.forEach(name => {
if (!name.startsWith(MPRIS_PLAYER_PREFIX))
return;
this._addPlayer(name);
});
});
this._proxy.connectSignal('NameOwnerChanged',
this._onNameOwnerChanged.bind(this));
}
_onNameOwnerChanged(proxy, sender, [name, oldOwner, newOwner]) {
if (!name.startsWith(MPRIS_PLAYER_PREFIX))
return;
if (newOwner && !oldOwner)
this._addPlayer(name);
}
});