1// Copyright 2005-2019 The Mumble Developers. All rights reserved.
2// Use of this source code is governed by a BSD-style license
3// that can be found in the LICENSE file at the root of the
4// Mumble source tree or at <https://www.mumble.info/LICENSE>.
5
6#include "mumble_pch.hpp"
7#import <ScriptingBridge/ScriptingBridge.h>
8#import <Cocoa/Cocoa.h>
9#include <Carbon/Carbon.h>
10#include "OverlayConfig.h"
11#include "OverlayClient.h"
12#include "MainWindow.h"
13
14// We define a global macro called 'g'. This can lead to issues when included code uses 'g' as a type or parameter name (like protobuf 3.7 does). As such, for now, we have to make this our last include.
15#include "Global.h"
16
17extern "C" {
18#include <xar/xar.h>
19}
20
21// Ignore deprecation warnings for the whole file, for now.
22#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
23
24static NSString *MumbleOverlayLoaderBundle = @"/Library/ScriptingAdditions/MumbleOverlay.osax";
25static NSString *MumbleOverlayLoaderBundleIdentifier = @"net.sourceforge.mumble.OverlayScriptingAddition";
26
27@interface OverlayInjectorMac : NSObject {
28	BOOL active;
29}
30- (id) init;
31- (void) dealloc;
32- (void) appLaunched:(NSNotification *)notification;
33- (void) setActive:(BOOL)flag;
34- (void) eventDidFail:(const AppleEvent *)event withError:(NSError *)error;
35@end
36
37#if MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_6
38@interface OverlayInjectorMac () <SBApplicationDelegate>
39@end
40#endif
41
42@implementation OverlayInjectorMac
43
44- (id) init {
45	self = [super init];
46
47	if (self) {
48		active = NO;
49		NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
50		[[workspace notificationCenter] addObserver:self
51		                                selector:@selector(appLaunched:)
52		                                name:NSWorkspaceDidLaunchApplicationNotification
53		                                object:workspace];
54		return self;
55	}
56
57	return nil;
58}
59
60- (void) dealloc {
61	NSWorkspace *workspace = [NSWorkspace sharedWorkspace];
62	[[workspace notificationCenter] removeObserver:self
63		                                name:NSWorkspaceDidLaunchApplicationNotification
64		                                object:workspace];
65
66	[super dealloc];
67}
68
69- (void) appLaunched:(NSNotification *)notification {
70	if (active) {
71		BOOL overlayEnabled = NO;
72		NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
73		NSDictionary *userInfo = [notification userInfo];
74
75		NSString *bundleId = [userInfo objectForKey:@"NSApplicationBundleIdentifier"];
76		if ([bundleId isEqualToString:[[NSBundle mainBundle] bundleIdentifier]])
77			return;
78
79		QString qsBundleIdentifier = QString::fromUtf8([bundleId UTF8String]);
80
81		switch (g.s.os.oemOverlayExcludeMode) {
82			case OverlaySettings::LauncherFilterExclusionMode: {
83				qWarning("Overlay_macx: launcher filter mode not implemented on macOS, allowing everything");
84				overlayEnabled = YES;
85				break;
86			}
87			case OverlaySettings::WhitelistExclusionMode: {
88				if (g.s.os.qslWhitelist.contains(qsBundleIdentifier)) {
89					overlayEnabled = YES;
90				}
91				break;
92			}
93			case OverlaySettings::BlacklistExclusionMode: {
94				if (! g.s.os.qslBlacklist.contains(qsBundleIdentifier)) {
95					overlayEnabled = YES;
96				}
97				break;
98			}
99		}
100
101		if (overlayEnabled) {
102			pid_t pid = [[userInfo objectForKey:@"NSApplicationProcessIdentifier"] intValue];
103			SBApplication *app = [SBApplication applicationWithProcessIdentifier:pid];
104			[app setDelegate:self];
105
106			// This timeout is specified in 'ticks'.
107			// A tick defined as: "[...] (a tick is approximately 1/60 of a second) [...]" in the
108			// Apple Event Manager Refernce documentation:
109			// http://developer.apple.com/legacy/mac/library/documentation/Carbon/reference/Event_Manager/Event_Manager.pdf
110			[app setTimeout:10*60];
111
112			[app setSendMode:kAEWaitReply];
113			[app sendEvent:kASAppleScriptSuite id:kGetAEUT parameters:0];
114
115			[app setSendMode:kAENoReply];
116			if (QSysInfo::MacintoshVersion == QSysInfo::MV_LEOPARD) {
117				[app sendEvent:'MUOL' id:'daol' parameters:0];
118			} else if (QSysInfo::MacintoshVersion >= QSysInfo::MV_SNOWLEOPARD) {
119				[app sendEvent:'MUOL' id:'load' parameters:0];
120			}
121		}
122
123		[pool release];
124	}
125}
126
127- (void) setActive:(BOOL)flag {
128	active = flag;
129}
130
131// SBApplication delegate method
132- (void)eventDidFail:(const AppleEvent *)event withError:(NSError *)error {
133	Q_UNUSED(event);
134	Q_UNUSED(error);
135
136	// Do nothing. This method is only here to avoid an exception.
137}
138
139@end
140
141class OverlayPrivateMac : public OverlayPrivate {
142	protected:
143		OverlayInjectorMac *olm;
144	public:
145		void setActive(bool);
146		OverlayPrivateMac(QObject *);
147		~OverlayPrivateMac();
148};
149
150OverlayPrivateMac::OverlayPrivateMac(QObject *p) : OverlayPrivate(p) {
151	olm = [[OverlayInjectorMac alloc] init];
152}
153
154OverlayPrivateMac::~OverlayPrivateMac() {
155	[olm release];
156}
157
158void OverlayPrivateMac::setActive(bool act) {
159	[olm setActive:act];
160}
161
162void Overlay::platformInit() {
163	d = new OverlayPrivateMac(this);
164}
165
166void Overlay::setActiveInternal(bool act) {
167	if (d) {
168		/// Only act if the private instance has been created already
169		static_cast<OverlayPrivateMac *>(d)->setActive(act);
170	}
171}
172
173bool OverlayConfig::supportsInstallableOverlay() {
174	return true;
175}
176
177void OverlayClient::updateMouse() {
178	QCursor c = qgv.viewport()->cursor();
179	NSCursor *cursor = nil;
180	Qt::CursorShape csShape = c.shape();
181
182	switch (csShape) {
183		case Qt::IBeamCursor:        cursor = [NSCursor IBeamCursor]; break;
184		case Qt::CrossCursor:        cursor = [NSCursor crosshairCursor]; break;
185		case Qt::ClosedHandCursor:   cursor = [NSCursor closedHandCursor]; break;
186		case Qt::OpenHandCursor:     cursor = [NSCursor openHandCursor]; break;
187		case Qt::PointingHandCursor: cursor = [NSCursor pointingHandCursor]; break;
188		case Qt::SizeVerCursor:      cursor = [NSCursor resizeUpDownCursor]; break;
189		case Qt::SplitVCursor:       cursor = [NSCursor resizeUpDownCursor]; break;
190		case Qt::SizeHorCursor:      cursor = [NSCursor resizeLeftRightCursor]; break;
191		case Qt::SplitHCursor:       cursor = [NSCursor resizeLeftRightCursor]; break;
192		default:                     cursor = [NSCursor arrowCursor]; break;
193	}
194
195	QPixmap pm = qmCursors.value(csShape);
196	if (pm.isNull()) {
197		NSImage *img = [cursor image];
198		CGImageRef cgimg = NULL;
199		NSArray *reps = [img representations];
200		for (NSUInteger i = 0; i < [reps count]; i++) {
201			NSImageRep *rep = [reps objectAtIndex:i];
202			if ([rep class] == [NSBitmapImageRep class]) {
203				cgimg = [(NSBitmapImageRep *)rep CGImage];
204			}
205		}
206
207#if QT_VERSION < 0x050000
208		if (cgimg) {
209			pm = QPixmap::fromMacCGImageRef(cgimg);
210			qmCursors.insert(csShape, pm);
211		}
212#endif
213	}
214
215	NSPoint p = [cursor hotSpot];
216	iOffsetX = (int) p.x;
217	iOffsetY = (int) p.y;
218
219	qgpiCursor->setPixmap(pm);
220	qgpiCursor->setPos(iMouseX - iOffsetX, iMouseY - iOffsetY);
221}
222
223QString installerPath() {
224	NSString *installerPath = [[NSBundle mainBundle] pathForResource:@"MumbleOverlay" ofType:@"pkg"];
225	if (installerPath) {
226		return QString::fromUtf8([installerPath UTF8String]);
227	}
228	return QString();
229}
230
231bool OverlayConfig::isInstalled() {
232	bool ret = false;
233
234	// Determine if the installed bundle is correctly installed (i.e. it's loadable)
235	NSBundle *bundle = [NSBundle bundleWithPath:MumbleOverlayLoaderBundle];
236	ret = [bundle preflightAndReturnError:NULL];
237
238	// Do the bundle identifiers match?
239	if (ret) {
240		ret = [[bundle bundleIdentifier] isEqualToString:MumbleOverlayLoaderBundleIdentifier];
241	}
242
243	return ret;
244}
245
246// Check whether this installer installs something 'newer' than what we already have.
247// Also checks whether the new installer is compatiable with the current version of
248// Mumble.
249static bool isInstallerNewer(QString path, NSUInteger curVer) {
250	xar_t pkg = NULL;
251	xar_iter_t iter = NULL;
252	xar_file_t file = NULL;
253	char *data = NULL;
254	size_t size = 0;
255	bool ret = false;
256	QString qsMinVer, qsOverlayVer;
257
258	pkg = xar_open(path.toUtf8().constData(), READ);
259	if (pkg == NULL) {
260		qWarning("isInstallerNewer: Unable to open pkg.");
261		goto out;
262	}
263
264	iter = xar_iter_new();
265	if (iter == NULL) {
266		qWarning("isInstallerNewer: Unable to allocate iter");
267		goto out;
268	}
269
270	file = xar_file_first(pkg, iter);
271	while (file != NULL) {
272		if (!strcmp(xar_get_path(file), "upgrade.xml"))
273			break;
274		file = xar_file_next(iter);
275	}
276
277	if (file != NULL) {
278		if (xar_extract_tobuffersz(pkg, file, &data, &size) == -1) {
279			goto out;
280		}
281
282		QXmlStreamReader reader(QByteArray::fromRawData(data, size));
283		while (! reader.atEnd()) {
284			QXmlStreamReader::TokenType tok = reader.readNext();
285			if (tok == QXmlStreamReader::StartElement) {
286				if (reader.name() == QLatin1String("upgrade")) {
287					qsOverlayVer = reader.attributes().value(QLatin1String("version")).toString();
288					qsMinVer = reader.attributes().value(QLatin1String("minclient")).toString();
289				}
290			}
291		}
292
293		if (reader.hasError() || qsMinVer.isNull() || qsOverlayVer.isNull()) {
294			qWarning("isInstallerNewer: Error while parsing XML version info.");
295			goto out;
296		}
297
298		NSUInteger newVer = qsOverlayVer.toUInt();
299
300		QRegExp rx(QLatin1String("(\\d+)\\.(\\d+)\\.(\\d+)"));
301		int major, minor, patch;
302		int minmajor, minminor, minpatch;
303		if (! rx.exactMatch(QLatin1String(MUMTEXT(MUMBLE_VERSION_STRING))))
304			goto out;
305		major = rx.cap(1).toInt();
306		minor = rx.cap(2).toInt();
307		patch = rx.cap(3).toInt();
308		if (! rx.exactMatch(qsMinVer))
309			goto out;
310		minmajor = rx.cap(1).toInt();
311		minminor = rx.cap(2).toInt();
312		minpatch = rx.cap(3).toInt();
313
314		ret = (major >= minmajor) && (minor >= minminor) && (patch >= minpatch) && (newVer > curVer);
315	}
316
317out:
318	xar_close(pkg);
319	xar_iter_free(iter);
320	free(data);
321	return ret;
322}
323
324bool OverlayConfig::needsUpgrade() {
325	NSDictionary *infoPlist = [NSDictionary dictionaryWithContentsOfFile:[NSString stringWithFormat:@"%@/Contents/Info.plist", MumbleOverlayLoaderBundle]];
326	if (infoPlist) {
327		NSUInteger curVersion = [[infoPlist objectForKey:@"MumbleOverlayVersion"] unsignedIntegerValue];
328
329		QString path = installerPath();
330		if (path.isEmpty())
331			return false;
332
333		return isInstallerNewer(path, curVersion);
334	}
335
336	return false;
337}
338
339static bool authExec(AuthorizationRef ref, const char **argv) {
340	OSStatus err = noErr;
341	int pid = 0, status = 0;
342
343	err = AuthorizationExecuteWithPrivileges(ref, argv[0], kAuthorizationFlagDefaults, const_cast<char * const *>(&argv[1]), NULL);
344	if (err == errAuthorizationSuccess) {
345		do {
346			pid = wait(&status);
347		} while (pid == -1 && errno == EINTR);
348		return (pid != -1 && WIFEXITED(status) && WEXITSTATUS(status) == 0);
349	}
350
351	qWarning("Overlay_macx: Failed to AuthorizeExecuteWithPrivileges. (err=%i)", err);
352	qWarning("Overlay_macx: Status: (pid=%i, exited=%u, exitStatus=%u)", pid, WIFEXITED(status), WEXITSTATUS(status));
353
354	return false;
355}
356
357bool OverlayConfig::installFiles() {
358	bool ret = false;
359
360	QString path = installerPath();
361	if (path.isEmpty()) {
362		qWarning("OverlayConfig: No installers found in search paths.");
363		return false;
364	}
365
366	QProcess installer(this);
367	QStringList args;
368	args << QString::fromLatin1("-W");
369	args << path;
370	installer.start(QLatin1String("/usr/bin/open"), args, QIODevice::ReadOnly);
371
372	while (!installer.waitForFinished(1000)) {
373		qApp->processEvents();
374	}
375
376	return ret;
377}
378
379bool OverlayConfig::uninstallFiles() {
380	AuthorizationRef auth;
381	NSBundle *loaderBundle;
382	bool ret = false, bundleOk = false;
383	OSStatus err;
384
385	// Load the installed loader bundle and check if it's something we're willing to uninstall.
386	loaderBundle = [NSBundle bundleWithPath:MumbleOverlayLoaderBundle];
387	bundleOk = [[loaderBundle bundleIdentifier] isEqualToString:MumbleOverlayLoaderBundleIdentifier];
388
389	// Perform uninstallation using Authorization Services. (Pops up a dialog asking for admin privileges)
390	if (bundleOk) {
391		err = AuthorizationCreate(NULL, kAuthorizationEmptyEnvironment, kAuthorizationFlagDefaults, &auth);
392		if (err == errAuthorizationSuccess) {
393			QByteArray tmp = QString::fromLatin1("/tmp/%1_Uninstalled_MumbleOverlay.osax").arg(QDateTime::currentMSecsSinceEpoch()).toLocal8Bit();
394			const char *remove[] = { "/bin/mv", [MumbleOverlayLoaderBundle UTF8String], tmp.constData(), NULL };
395			ret = authExec(auth, remove);
396		}
397		AuthorizationFree(auth, kAuthorizationFlagDefaults);
398	}
399
400	return ret;
401}
402