mumble-voip_mumble/src/mumble/Overlay_macx.mm

394 lines
11 KiB
Plaintext

// Copyright 2010-2023 The Mumble Developers. All rights reserved.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file at the root of the
// Mumble source tree or at <https://www.mumble.info/LICENSE>.
#include "OverlayConfig.h"
#include "OverlayClient.h"
#include "MainWindow.h"
#include "Global.h"
#include <QtCore/QProcess>
#include <QtCore/QXmlStreamReader>
#import <ScriptingBridge/ScriptingBridge.h>
#import <Cocoa/Cocoa.h>
#include <Carbon/Carbon.h>
extern "C" {
#include <xar/xar.h>
}
// Ignore deprecation warnings for the whole file, for now.
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
static NSString *MumbleOverlayLoaderBundle = @"/Library/ScriptingAdditions/MumbleOverlay.osax";
static NSString *MumbleOverlayLoaderBundleIdentifier = @"net.sourceforge.mumble.OverlayScriptingAddition";
pid_t getForegroundProcessId() {
NSRunningApplication *app = [[NSWorkspace sharedWorkspace] frontmostApplication];
if (app) {
return [app processIdentifier];
}
return 0;
}
@interface OverlayInjectorMac : NSObject {
BOOL active;
}
- (id) init;
- (void) dealloc;
- (void) appLaunched:(NSNotification *)notification;
- (void) setActive:(BOOL)flag;
- (void) eventDidFail:(const AppleEvent *)event withError:(NSError *)error;
@end
#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6
@interface OverlayInjectorMac () <SBApplicationDelegate>
@end
#endif
@implementation OverlayInjectorMac
- (id) init {
self = [super init];
if (self) {
active = NO;
NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
[[workspace notificationCenter] addObserver:self
selector:@selector(appLaunched:)
name:NSWorkspaceDidLaunchApplicationNotification
object:workspace];
return self;
}
return nil;
}
- (void) dealloc {
NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
[[workspace notificationCenter] removeObserver:self
name:NSWorkspaceDidLaunchApplicationNotification
object:workspace];
[super dealloc];
}
- (void) appLaunched:(NSNotification *)notification {
if (active) {
BOOL overlayEnabled = NO;
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
NSDictionary *userInfo = [notification userInfo];
NSString *bundleId = [userInfo objectForKey:@"NSApplicationBundleIdentifier"];
if ([bundleId isEqualToString:[[NSBundle mainBundle] bundleIdentifier]])
return;
QString qsBundleIdentifier = QString::fromUtf8([bundleId UTF8String]);
switch (Global::get().s.os.oemOverlayExcludeMode) {
case OverlaySettings::LauncherFilterExclusionMode: {
qWarning("Overlay_macx: launcher filter mode not implemented on macOS, allowing everything");
overlayEnabled = YES;
break;
}
case OverlaySettings::WhitelistExclusionMode: {
if (Global::get().s.os.qslWhitelist.contains(qsBundleIdentifier)) {
overlayEnabled = YES;
}
break;
}
case OverlaySettings::BlacklistExclusionMode: {
if (! Global::get().s.os.qslBlacklist.contains(qsBundleIdentifier)) {
overlayEnabled = YES;
}
break;
}
}
if (overlayEnabled) {
pid_t pid = [[userInfo objectForKey:@"NSApplicationProcessIdentifier"] intValue];
SBApplication *app = [SBApplication applicationWithProcessIdentifier:pid];
[app setDelegate:self];
// This timeout is specified in 'ticks'.
// A tick defined as: "[...] (a tick is approximately 1/60 of a second) [...]" in the
// Apple Event Manager Reference documentation:
// http://developer.apple.com/legacy/mac/library/documentation/Carbon/reference/Event_Manager/Event_Manager.pdf
[app setTimeout:10*60];
[app setSendMode:kAEWaitReply];
[app sendEvent:kASAppleScriptSuite id:kGetAEUT parameters:0];
[app setSendMode:kAENoReply];
if (QSysInfo::MacintoshVersion == QSysInfo::MV_LEOPARD) {
[app sendEvent:'MUOL' id:'daol' parameters:0];
} else if (QSysInfo::MacintoshVersion >= QSysInfo::MV_SNOWLEOPARD) {
[app sendEvent:'MUOL' id:'load' parameters:0];
}
}
[pool release];
}
}
- (void) setActive:(BOOL)flag {
active = flag;
}
// SBApplication delegate method
- (void)eventDidFail:(const AppleEvent *)event withError:(NSError *)error {
Q_UNUSED(event);
Q_UNUSED(error);
// Do nothing. This method is only here to avoid an exception.
}
@end
class OverlayPrivateMac : public OverlayPrivate {
protected:
OverlayInjectorMac *olm;
public:
void setActive(bool);
OverlayPrivateMac(QObject *);
~OverlayPrivateMac();
};
OverlayPrivateMac::OverlayPrivateMac(QObject *p) : OverlayPrivate(p) {
olm = [[OverlayInjectorMac alloc] init];
}
OverlayPrivateMac::~OverlayPrivateMac() {
[olm release];
}
void OverlayPrivateMac::setActive(bool act) {
[olm setActive:act];
}
void Overlay::platformInit() {
d = new OverlayPrivateMac(this);
}
void Overlay::setActiveInternal(bool act) {
if (d) {
/// Only act if the private instance has been created already
static_cast<OverlayPrivateMac *>(d)->setActive(act);
}
}
bool OverlayConfig::supportsInstallableOverlay() {
return true;
}
void OverlayClient::updateMouse() {
QCursor c = qgv.viewport()->cursor();
NSCursor *cursor = nil;
Qt::CursorShape csShape = c.shape();
switch (csShape) {
case Qt::IBeamCursor: cursor = [NSCursor IBeamCursor]; break;
case Qt::CrossCursor: cursor = [NSCursor crosshairCursor]; break;
case Qt::ClosedHandCursor: cursor = [NSCursor closedHandCursor]; break;
case Qt::OpenHandCursor: cursor = [NSCursor openHandCursor]; break;
case Qt::PointingHandCursor: cursor = [NSCursor pointingHandCursor]; break;
case Qt::SizeVerCursor: cursor = [NSCursor resizeUpDownCursor]; break;
case Qt::SplitVCursor: cursor = [NSCursor resizeUpDownCursor]; break;
case Qt::SizeHorCursor: cursor = [NSCursor resizeLeftRightCursor]; break;
case Qt::SplitHCursor: cursor = [NSCursor resizeLeftRightCursor]; break;
default: cursor = [NSCursor arrowCursor]; break;
}
QPixmap pm = qmCursors.value(csShape);
NSPoint p = [cursor hotSpot];
iOffsetX = (int) p.x;
iOffsetY = (int) p.y;
qgpiCursor->setPixmap(pm);
qgpiCursor->setPos(iMouseX - iOffsetX, iMouseY - iOffsetY);
}
QString installerPath() {
NSString *installerPath = [[NSBundle mainBundle] pathForResource:@"MumbleOverlay" ofType:@"pkg"];
if (installerPath) {
return QString::fromUtf8([installerPath UTF8String]);
}
return QString();
}
bool OverlayConfig::isInstalled() {
bool ret = false;
// Determine if the installed bundle is correctly installed (i.e. it's loadable)
NSBundle *bundle = [NSBundle bundleWithPath:MumbleOverlayLoaderBundle];
ret = [bundle preflightAndReturnError:nullptr];
// Do the bundle identifiers match?
if (ret) {
ret = [[bundle bundleIdentifier] isEqualToString:MumbleOverlayLoaderBundleIdentifier];
}
return ret;
}
// Check whether this installer installs something 'newer' than what we already have.
// Also checks whether the new installer is compatible with the current version of
// Mumble.
static bool isInstallerNewer(QString path, NSUInteger curVer) {
xar_t pkg = nullptr;
xar_iter_t iter = nullptr;
xar_file_t file = nullptr;
char *data = nullptr;
size_t size = 0;
bool ret = false;
QString qsMinVer, qsOverlayVer;
pkg = xar_open(path.toUtf8().constData(), READ);
if (!pkg) {
qWarning("isInstallerNewer: Unable to open pkg.");
goto out;
}
iter = xar_iter_new();
if (!iter) {
qWarning("isInstallerNewer: Unable to allocate iter");
goto out;
}
file = xar_file_first(pkg, iter);
while (file) {
if (!strcmp(xar_get_path(file), "upgrade.xml"))
break;
file = xar_file_next(iter);
}
if (file) {
if (xar_extract_tobuffersz(pkg, file, &data, &size) == -1) {
goto out;
}
QXmlStreamReader reader(QByteArray::fromRawData(data, static_cast<int>(size)));
while (! reader.atEnd()) {
QXmlStreamReader::TokenType tok = reader.readNext();
if (tok == QXmlStreamReader::StartElement) {
if (reader.name() == QLatin1String("upgrade")) {
qsOverlayVer = reader.attributes().value(QLatin1String("version")).toString();
qsMinVer = reader.attributes().value(QLatin1String("minclient")).toString();
}
}
}
if (reader.hasError() || qsMinVer.isNull() || qsOverlayVer.isNull()) {
qWarning("isInstallerNewer: Error while parsing XML version info.");
goto out;
}
NSUInteger newVer = qsOverlayVer.toUInt();
QRegExp rx(QLatin1String("(\\d+)\\.(\\d+)\\.(\\d+)"));
int major, minor, patch;
int minmajor, minminor, minpatch;
if (! rx.exactMatch(QLatin1String(MUMTEXT(MUMBLE_VERSION))))
goto out;
major = rx.cap(1).toInt();
minor = rx.cap(2).toInt();
patch = rx.cap(3).toInt();
if (! rx.exactMatch(qsMinVer))
goto out;
minmajor = rx.cap(1).toInt();
minminor = rx.cap(2).toInt();
minpatch = rx.cap(3).toInt();
ret = (major >= minmajor) && (minor >= minminor) && (patch >= minpatch) && (newVer > curVer);
}
out:
xar_close(pkg);
xar_iter_free(iter);
free(data);
return ret;
}
bool OverlayConfig::needsUpgrade() {
NSDictionary *infoPlist = [NSDictionary dictionaryWithContentsOfFile:[NSString stringWithFormat:@"%@/Contents/Info.plist", MumbleOverlayLoaderBundle]];
if (infoPlist) {
NSUInteger curVersion = [[infoPlist objectForKey:@"MumbleOverlayVersion"] unsignedIntegerValue];
QString path = installerPath();
if (path.isEmpty())
return false;
return isInstallerNewer(path, curVersion);
}
return false;
}
static bool authExec(AuthorizationRef ref, const char **argv) {
OSStatus err = noErr;
int pid = 0, status = 0;
err = AuthorizationExecuteWithPrivileges(ref, argv[0], kAuthorizationFlagDefaults, const_cast<char * const *>(&argv[1]), nullptr);
if (err == errAuthorizationSuccess) {
do {
pid = wait(&status);
} while (pid == -1 && errno == EINTR);
return (pid != -1 && WIFEXITED(status) && WEXITSTATUS(status) == 0);
}
qWarning("Overlay_macx: Failed to AuthorizeExecuteWithPrivileges. (err=%i)", err);
qWarning("Overlay_macx: Status: (pid=%i, exited=%u, exitStatus=%u)", pid, WIFEXITED(status), WEXITSTATUS(status));
return false;
}
bool OverlayConfig::installFiles() {
bool ret = false;
QString path = installerPath();
if (path.isEmpty()) {
qWarning("OverlayConfig: No installers found in search paths.");
return false;
}
QProcess installer(this);
QStringList args;
args << QString::fromLatin1("-W");
args << path;
installer.start(QLatin1String("/usr/bin/open"), args, QIODevice::ReadOnly);
while (!installer.waitForFinished(1000)) {
qApp->processEvents();
}
return ret;
}
bool OverlayConfig::uninstallFiles() {
AuthorizationRef auth;
NSBundle *loaderBundle;
bool ret = false, bundleOk = false;
OSStatus err;
// Load the installed loader bundle and check if it's something we're willing to uninstall.
loaderBundle = [NSBundle bundleWithPath:MumbleOverlayLoaderBundle];
bundleOk = [[loaderBundle bundleIdentifier] isEqualToString:MumbleOverlayLoaderBundleIdentifier];
// Perform uninstallation using Authorization Services. (Pops up a dialog asking for admin privileges)
if (bundleOk) {
err = AuthorizationCreate(nullptr, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &auth);
if (err == errAuthorizationSuccess) {
QByteArray tmp = QString::fromLatin1("/tmp/%1_Uninstalled_MumbleOverlay.osax").arg(QDateTime::currentMSecsSinceEpoch()).toLocal8Bit();
const char *remove[] = { "/bin/mv", [MumbleOverlayLoaderBundle UTF8String], tmp.constData(), nullptr };
ret = authExec(auth, remove);
}
AuthorizationFree(auth, kAuthorizationFlagDefaults);
}
return ret;
}