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 
8 #include "Overlay.h"
9 
10 #include "OverlayClient.h"
11 #include "Channel.h"
12 #include "ClientUser.h"
13 #include "Database.h"
14 #include "GlobalShortcut.h"
15 #include "MainWindow.h"
16 #include "Message.h"
17 #include "OverlayText.h"
18 #include "RichTextEditor.h"
19 #include "ServerHandler.h"
20 #include "User.h"
21 #include "WebFetch.h"
22 
23 // 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.
24 #include "Global.h"
25 
applicationIdentifierForPath(const QString & path)26 QString OverlayAppInfo::applicationIdentifierForPath(const QString &path) {
27 #ifdef Q_OS_MAC
28 	QString qsIdentifier;
29 	CFDictionaryRef plist = NULL;
30 	CFDataRef data = NULL;
31 
32 	QFile qfAppBundle(QString::fromLatin1("%1/Contents/Info.plist").arg(path));
33 	if (qfAppBundle.exists()) {
34 		qfAppBundle.open(QIODevice::ReadOnly);
35 		QByteArray qbaPlistData = qfAppBundle.readAll();
36 
37 		data = CFDataCreateWithBytesNoCopy(NULL, reinterpret_cast<UInt8 *>(qbaPlistData.data()), qbaPlistData.count(), kCFAllocatorNull);
38 		plist = static_cast<CFDictionaryRef>(CFPropertyListCreateFromXMLData(NULL, data, kCFPropertyListImmutable, NULL));
39 		if (plist) {
40 			CFStringRef ident = static_cast<CFStringRef>(CFDictionaryGetValue(plist, CFSTR("CFBundleIdentifier")));
41 			if (ident) {
42 				char buf[4096];
43 				CFStringGetCString(ident, buf, 4096, kCFStringEncodingUTF8);
44 				qsIdentifier = QString::fromUtf8(buf);
45 			}
46 		}
47 	}
48 
49 	if (data) {
50 		CFRelease(data);
51 	}
52 	if (plist) {
53 		CFRelease(plist);
54 	}
55 
56 	return qsIdentifier;
57 #else
58 	return QDir::toNativeSeparators(path);
59 #endif
60 }
61 
applicationInfoForId(const QString & identifier)62 OverlayAppInfo OverlayAppInfo::applicationInfoForId(const QString &identifier) {
63 	QString qsAppName(identifier);
64 	QIcon qiAppIcon;
65 #if defined(Q_OS_MAC)
66 	CFStringRef bundleId = NULL;
67 	CFURLRef bundleUrl = NULL;
68 	CFBundleRef bundle = NULL;
69 	OSStatus err = noErr;
70 	char buf[4096];
71 
72 	bundleId = CFStringCreateWithCharacters(kCFAllocatorDefault, reinterpret_cast<const UniChar *>(identifier.unicode()), identifier.length());
73 	err = LSFindApplicationForInfo(kLSUnknownCreator, bundleId, NULL, NULL, &bundleUrl);
74 	if (err == noErr) {
75 		// Figure out the bundle name of the application.
76 		CFStringRef absBundlePath = CFURLCopyFileSystemPath(bundleUrl, kCFURLPOSIXPathStyle);
77 		CFStringGetCString(absBundlePath, buf, 4096, kCFStringEncodingUTF8);
78 		QString qsBundlePath = QString::fromUtf8(buf);
79 		CFRelease(absBundlePath);
80 		qsAppName = QFileInfo(qsBundlePath).bundleName();
81 
82 		// Load the bundle's icon.
83 		bundle = CFBundleCreate(NULL, bundleUrl);
84 		if (bundle) {
85 			CFDictionaryRef info = CFBundleGetInfoDictionary(bundle);
86 			if (info) {
87 				CFStringRef iconFileName = reinterpret_cast<CFStringRef>(CFDictionaryGetValue(info, CFSTR("CFBundleIconFile")));
88 				if (iconFileName) {
89 					CFStringGetCString(iconFileName, buf, 4096, kCFStringEncodingUTF8);
90 					QString qsIconPath = QString::fromLatin1("%1/Contents/Resources/%2")
91 					                     .arg(qsBundlePath, QString::fromUtf8(buf));
92 					if (! QFile::exists(qsIconPath)) {
93 						qsIconPath += QString::fromLatin1(".icns");
94 					}
95 					if (QFile::exists(qsIconPath)) {
96 						qiAppIcon = QIcon(qsIconPath);
97 					}
98 				}
99 			}
100 		}
101 	}
102 
103 	if (bundleId) {
104 		CFRelease(bundleId);
105 	}
106 	if (bundleUrl) {
107 		CFRelease(bundleUrl);
108 	}
109 	if (bundle) {
110 		CFRelease(bundle);
111 	}
112 
113 #elif defined(Q_OS_WIN)
114 	// qWinAppInst(), whose return value we used to pass
115 	// to ExtractIcon below, was removed in Qt 5.8.
116 	//
117 	// It was removed via
118 	// https://github.com/qt/qtbase/commit/64507c7165e42c2a5029353d8f97a0d841fa6b01
119 	//
120 	// In both Qt 4 and Qt 5, the qWinAppInst() implementation
121 	// simply calls GetModuleHandle(0).
122 	//
123 	// To sidestep the removal of the function, we simply
124 	// call through to GetModuleHandle() directly.
125 	HINSTANCE qWinAppInstValue = GetModuleHandle(NULL);
126 	HICON icon = ExtractIcon(qWinAppInstValue, identifier.toStdWString().c_str(), 0);
127 	if (icon) {
128 #if QT_VERSION >= 0x050000
129 		extern QPixmap qt_pixmapFromWinHICON(HICON icon);
130 		qiAppIcon = QIcon(qt_pixmapFromWinHICON(icon));
131 #else
132 		qiAppIcon = QIcon(QPixmap::fromWinHICON(icon));
133 #endif
134 		DestroyIcon(icon);
135 	}
136 #endif
137 	return OverlayAppInfo(qsAppName, qiAppIcon);
138 }
139 
OverlayAppInfo(QString name,QIcon icon)140 OverlayAppInfo::OverlayAppInfo(QString name, QIcon icon) {
141 	qsDisplayName = name;
142 	qiIcon = icon;
143 }
144 
OverlayMouse(QGraphicsItem * p)145 OverlayMouse::OverlayMouse(QGraphicsItem *p) : QGraphicsPixmapItem(p) {
146 }
147 
contains(const QPointF &) const148 bool OverlayMouse::contains(const QPointF &) const {
149 	return false;
150 }
151 
collidesWithPath(const QPainterPath &,Qt::ItemSelectionMode) const152 bool OverlayMouse::collidesWithPath(const QPainterPath &, Qt::ItemSelectionMode) const {
153 	return false;
154 }
155 
OverlayGroup()156 OverlayGroup::OverlayGroup() : QGraphicsItem() {
157 }
158 
type() const159 int OverlayGroup::type() const {
160 	return Type;
161 }
162 
paint(QPainter *,const QStyleOptionGraphicsItem *,QWidget *)163 void OverlayGroup::paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) {
164 }
165 
boundingRect() const166 QRectF OverlayGroup::boundingRect() const {
167 	QRectF qr;
168 	foreach(const QGraphicsItem *item, childItems())
169 		if (item->isVisible())
170 			qr |= item->boundingRect().translated(item->pos());
171 
172 	return qr;
173 }
174 
Overlay()175 Overlay::Overlay() : QObject() {
176 	d = NULL;
177 
178 	m_initialized = false;
179 
180 	qlsServer = NULL;
181 
182 	QMetaObject::connectSlotsByName(this);
183 }
184 
~Overlay()185 Overlay::~Overlay() {
186 	setActive(false);
187 	if (d) {
188 		delete d;
189 	}
190 
191 	// Need to be deleted first, since destructor references lingering QLocalSockets
192 	foreach(OverlayClient *oc, qlClients) {
193 		// As we're the one closing the connection, we do not need to be
194 		// notified of disconnects. This is important because on disconnect we
195 		// also remove (and 'delete') the overlay client.
196 		disconnect(oc->qlsSocket, SIGNAL(disconnected()), this, SLOT(disconnected()));
197 		disconnect(oc->qlsSocket, SIGNAL(error(QLocalSocket::LocalSocketError)), this, SLOT(error(QLocalSocket::LocalSocketError)));
198 		delete oc;
199 	}
200 }
201 
initialize()202 void Overlay::initialize() {
203 	if (!m_initialized) {
204 		platformInit();
205 		forceSettings();
206 
207 		createPipe();
208 
209 		m_initialized = true;
210 	}
211 }
212 
setActive(bool act)213 void Overlay::setActive(bool act) {
214 	if (!act && !m_initialized) {
215 		// Disabling when the Overlay hasn't been initialized yet, doesn't make much sense
216 		return;
217 	}
218 
219 	// Make sure the Overlay is initialized
220 	initialize();
221 
222 	setActiveInternal(act);
223 }
224 
createPipe()225 void Overlay::createPipe() {
226 	qlsServer = new QLocalServer(this);
227 	QString pipepath;
228 #ifdef Q_OS_WIN
229 	pipepath = QLatin1String("MumbleOverlayPipe");
230 #else
231 	{
232 		QString xdgRuntimePath = QProcessEnvironment::systemEnvironment().value(QLatin1String("XDG_RUNTIME_DIR"));
233 		QDir xdgRuntimeDir = QDir(xdgRuntimePath);
234 
235 		if (! xdgRuntimePath.isNull() && xdgRuntimeDir.exists()) {
236 			pipepath = xdgRuntimeDir.absoluteFilePath(QLatin1String("MumbleOverlayPipe"));
237 		} else {
238 			pipepath = QDir::home().absoluteFilePath(QLatin1String(".MumbleOverlayPipe"));
239 		}
240 	}
241 
242 	{
243 		QFile f(pipepath);
244 		if (f.exists()) {
245 			qWarning() << "Overlay: Removing old socket on" << pipepath;
246 			f.remove();
247 		}
248 	}
249 #endif
250 
251 	if (! qlsServer->listen(pipepath)) {
252 		QMessageBox::warning(NULL, QLatin1String("Mumble"), tr("Failed to create communication with overlay at %2: %1. No overlay will be available.").arg(Qt::escape(qlsServer->errorString()), Qt::escape(pipepath)), QMessageBox::Ok, QMessageBox::NoButton);
253 	} else {
254 		qWarning() << "Overlay: Listening on" << qlsServer->fullServerName();
255 		connect(qlsServer, SIGNAL(newConnection()), this, SLOT(newConnection()));
256 	}
257 }
258 
newConnection()259 void Overlay::newConnection() {
260 	while (qlsServer && qlsServer->hasPendingConnections()) {
261 		QLocalSocket *qls = qlsServer->nextPendingConnection();
262 
263 		OverlayClient *oc = new OverlayClient(qls, this);
264 		qlClients << oc;
265 
266 		connect(qls, SIGNAL(disconnected()), this, SLOT(disconnected()));
267 		connect(qls, SIGNAL(error(QLocalSocket::LocalSocketError)), this, SLOT(error(QLocalSocket::LocalSocketError)));
268 	}
269 }
270 
disconnected()271 void Overlay::disconnected() {
272 	QLocalSocket *qls = qobject_cast<QLocalSocket *>(sender());
273 	foreach(OverlayClient *oc, qlClients) {
274 		if (oc->qlsSocket == qls) {
275 			qlClients.removeAll(oc);
276 			delete oc;
277 			return;
278 		}
279 	}
280 }
281 
error(QLocalSocket::LocalSocketError)282 void Overlay::error(QLocalSocket::LocalSocketError) {
283 	disconnected();
284 }
285 
isActive() const286 bool Overlay::isActive() const {
287 	return ! qlClients.isEmpty();
288 }
289 
toggleShow()290 void Overlay::toggleShow() {
291 	if (g.ocIntercept) {
292 		g.ocIntercept->hideGui();
293 	} else {
294 		foreach(OverlayClient *oc, qlClients) {
295 			if (oc->uiPid) {
296 #if defined(Q_OS_WIN)
297 				HWND hwnd = GetForegroundWindow();
298 				DWORD pid = 0;
299 				GetWindowThreadProcessId(hwnd, &pid);
300 				if (pid != oc->uiPid)
301 					continue;
302 #elif defined(Q_OS_MAC)
303 				pid_t pid = 0;
304 				ProcessSerialNumber psn;
305 				GetFrontProcess(&psn);
306 				GetProcessPID(&psn, &pid);
307 				if (static_cast<quint64>(pid) != oc->uiPid)
308 					continue;
309 #if 0
310 				// Fullscreen only.
311 				if (! CGDisplayIsCaptured(CGMainDisplayID()))
312 					continue;
313 #endif
314 #endif
315 				oc->showGui();
316 				return;
317 			}
318 		}
319 	}
320 }
321 
forceSettings()322 void Overlay::forceSettings() {
323 	foreach(OverlayClient *oc, qlClients) {
324 		oc->reset();
325 	}
326 
327 	updateOverlay();
328 }
329 
verifyTexture(ClientUser * cp,bool allowupdate)330 void Overlay::verifyTexture(ClientUser *cp, bool allowupdate) {
331 	qsQueried.remove(cp->uiSession);
332 
333 	ClientUser *self = ClientUser::get(g.uiSession);
334 	allowupdate = allowupdate && self && self->cChannel->isLinked(cp->cChannel);
335 
336 	if (allowupdate && ! cp->qbaTextureHash.isEmpty() && cp->qbaTexture.isEmpty())
337 		cp->qbaTexture = g.db->blob(cp->qbaTextureHash);
338 
339 	if (! cp->qbaTexture.isEmpty()) {
340 		bool valid = true;
341 
342 		if (cp->qbaTexture.length() < static_cast<int>(sizeof(unsigned int))) {
343 			valid = false;
344 		} else if (qFromBigEndian<unsigned int>(reinterpret_cast<const unsigned char *>(cp->qbaTexture.constData())) == 600 * 60 * 4) {
345 			QByteArray qba = qUncompress(cp->qbaTexture);
346 			if (qba.length() != 600 * 60 * 4) {
347 				valid = false;
348 			} else {
349 				int width = 0;
350 				int height = 0;
351 				const unsigned int *ptr = reinterpret_cast<const unsigned int *>(qba.constData());
352 
353 				// If we have an alpha only part on the right side of the image ignore it
354 				for (int y=0;y<60;++y) {
355 					for (int x=0;x<600; ++x) {
356 						if (ptr[y*600+x] & 0xff000000) {
357 							if (x > width)
358 								width = x;
359 							if (y > height)
360 								height = y;
361 						}
362 					}
363 				}
364 
365 				// Full size image? More likely image without alpha; fix it.
366 				if ((width == 599) && (height == 59)) {
367 					width = 0;
368 					height = 0;
369 					for (int y=0;y<60;++y) {
370 						for (int x=0;x<600; ++x) {
371 							if (ptr[y*600+x] & 0x00ffffff) {
372 								if (x > width)
373 									width = x;
374 								if (y > height)
375 									height = y;
376 							}
377 						}
378 					}
379 				}
380 
381 				if (! width || ! height) {
382 					valid = false;
383 				} else {
384 					QImage img = QImage(width+1, height+1, QImage::Format_ARGB32);
385 					{
386 						QImage srcimg(reinterpret_cast<const uchar *>(qba.constData()), 600, 60, QImage::Format_ARGB32);
387 
388 						QPainter imgp(&img);
389 						img.fill(0);
390 						imgp.setRenderHint(QPainter::Antialiasing);
391 						imgp.setRenderHint(QPainter::TextAntialiasing);
392 						imgp.setBackground(QColor(0,0,0,0));
393 						imgp.setCompositionMode(QPainter::CompositionMode_Source);
394 						imgp.drawImage(0, 0, srcimg);
395 					}
396 					cp->qbaTexture = QByteArray();
397 
398 					QBuffer qb(& cp->qbaTexture);
399 					qb.open(QIODevice::WriteOnly);
400 					QImageWriter qiw(&qb, "png");
401 					qiw.write(img);
402 
403 					cp->qbaTextureFormat = QString::fromLatin1("png").toUtf8();
404 				}
405 			}
406 		} else {
407 			QBuffer qb(& cp->qbaTexture);
408 			qb.open(QIODevice::ReadOnly);
409 
410 			QImageReader qir;
411 			qir.setAutoDetectImageFormat(false);
412 
413 			QByteArray fmt;
414 			if (RichTextImage::isValidImage(cp->qbaTexture, fmt)) {
415 				qir.setFormat(fmt);
416 				qir.setDevice(&qb);
417 				if (! qir.canRead() || (qir.size().width() > 1024) || (qir.size().height() > 1024)) {
418 					valid = false;
419 				} else {
420 					cp->qbaTextureFormat = qir.format();
421 					QImage qi = qir.read();
422 					valid = ! qi.isNull();
423 				}
424 			} else {
425 				valid = false;
426 			}
427 		}
428 		if (! valid) {
429 			cp->qbaTexture = QByteArray();
430 			cp->qbaTextureHash = QByteArray();
431 		}
432 	}
433 
434 	if (allowupdate)
435 		updateOverlay();
436 }
437 
438 typedef QPair<QString, quint32> qpChanCol;
439 
updateOverlay()440 void Overlay::updateOverlay() {
441 	if (! g.uiSession)
442 		qsQueried.clear();
443 
444 	if (qlClients.isEmpty())
445 		return;
446 
447 	qsQuery.clear();
448 
449 	foreach(OverlayClient *oc, qlClients) {
450 		if (! oc->update()) {
451 			qWarning() << "Overlay: Dead client detected. PID" << oc->uiPid << oc->qsExecutablePath;
452 			qlClients.removeAll(oc);
453 			oc->scheduleDelete();
454 			break;
455 		}
456 	}
457 
458 	if (! qsQuery.isEmpty()) {
459 		MumbleProto::RequestBlob mprb;
460 		foreach(unsigned int session, qsQuery) {
461 			qsQueried.insert(session);
462 			mprb.add_session_texture(session);
463 		}
464 		g.sh->sendMessage(mprb);
465 	}
466 }
467 
requestTexture(ClientUser * cu)468 void Overlay::requestTexture(ClientUser *cu) {
469 	if (cu->qbaTexture.isEmpty() && ! qsQueried.contains(cu->uiSession)) {
470 		cu->qbaTexture=g.db->blob(cu->qbaTextureHash);
471 		if (cu->qbaTexture.isEmpty())
472 			qsQuery.insert(cu->uiSession);
473 		else
474 			verifyTexture(cu, false);
475 	}
476 }
477