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