394 lines
11 KiB
Plaintext
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;
|
|
}
|