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