mumble-voip_mumble/src/mumble/Overlay.cpp

490 lines
13 KiB
C++

// Copyright 2007-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 "Overlay.h"
#include "Channel.h"
#include "ClientUser.h"
#include "Database.h"
#include "MainWindow.h"
#include "OverlayClient.h"
#include "OverlayText.h"
#include "RichTextEditor.h"
#include "ServerHandler.h"
#include "User.h"
#include "Utils.h"
#include "WebFetch.h"
#include "Global.h"
#include "GlobalShortcut.h"
#include <QtCore/QProcessEnvironment>
#include <QtCore/QtEndian>
#include <QtGui/QFocusEvent>
#include <QtGui/QImageReader>
#include <QtGui/QImageWriter>
#include <QtNetwork/QLocalServer>
#include <QtWidgets/QMessageBox>
#ifdef Q_OS_WIN
# include <shellapi.h>
#endif
#ifdef Q_OS_MAC
# include <ApplicationServices/ApplicationServices.h>
# include <CoreFoundation/CoreFoundation.h>
#endif
QString OverlayAppInfo::applicationIdentifierForPath(const QString &path) {
#ifdef Q_OS_MAC
QString qsIdentifier;
QFile appBundle(QString::fromLatin1("%1/Contents/Info.plist").arg(path));
if (!appBundle.exists()) {
return {};
}
appBundle.open(QIODevice::ReadOnly);
const QByteArray byteArray = appBundle.readAll();
CFDataRef dataCF = CFDataCreateWithBytesNoCopy(nullptr, reinterpret_cast< const UInt8 * >(byteArray.data()),
byteArray.count(), kCFAllocatorNull);
if (!dataCF) {
return {};
}
auto plist = static_cast< CFDictionaryRef >(
CFPropertyListCreateWithData(nullptr, dataCF, kCFPropertyListImmutable, nullptr, nullptr));
CFRelease(dataCF);
if (!plist) {
return {};
}
auto identifierCF = static_cast< CFStringRef >(CFDictionaryGetValue(plist, CFSTR("CFBundleIdentifier")));
auto identifier = QString::fromCFString(identifierCF);
CFRelease(plist);
return identifier;
#else
return QDir::toNativeSeparators(path);
#endif
}
OverlayAppInfo OverlayAppInfo::applicationInfoForId(const QString &identifier) {
QString qsAppName(identifier);
QIcon qiAppIcon;
#if defined(Q_OS_MAC)
CFStringRef id = identifier.toCFString();
CFArrayRef urls = LSCopyApplicationURLsForBundleIdentifier(id, nullptr);
CFRelease(id);
if (!urls) {
return OverlayAppInfo(qsAppName, qiAppIcon);
}
auto url = static_cast< CFURLRef >(CFArrayGetValueAtIndex(urls, 0));
CFRetain(url);
CFRelease(urls);
// Figure out the bundle name of the application.
CFStringRef bundlePathCF = CFURLCopyFileSystemPath(url, kCFURLPOSIXPathStyle);
const auto bundlePath = QString::fromCFString(bundlePathCF);
CFRelease(bundlePathCF);
qsAppName = QFileInfo(bundlePath).bundleName();
// Load the bundle's icon.
CFBundleRef bundle = CFBundleCreate(nullptr, url);
CFRelease(url);
if (!bundle) {
return OverlayAppInfo(qsAppName, qiAppIcon);
}
CFDictionaryRef info = CFBundleGetInfoDictionary(bundle);
CFRelease(bundle);
if (!info) {
return OverlayAppInfo(qsAppName, qiAppIcon);
}
auto iconFileName = static_cast< CFStringRef >(CFDictionaryGetValue(info, CFSTR("CFBundleIconFile")));
if (!iconFileName) {
return OverlayAppInfo(qsAppName, qiAppIcon);
}
auto iconPath =
QString::fromLatin1("%1/Contents/Resources/%2").arg(bundlePath, QString::fromCFString(iconFileName));
if (!QFile::exists(iconPath)) {
iconPath += QStringLiteral(".icns");
}
if (QFile::exists(iconPath)) {
qiAppIcon = QIcon(iconPath);
}
#elif defined(Q_OS_WIN)
// qWinAppInst(), whose return value we used to pass
// to ExtractIcon below, was removed in Qt 5.8.
//
// It was removed via
// https://github.com/qt/qtbase/commit/64507c7165e42c2a5029353d8f97a0d841fa6b01
//
// In both Qt 4 and Qt 5, the qWinAppInst() implementation
// simply calls GetModuleHandle(0).
//
// To sidestep the removal of the function, we simply
// call through to GetModuleHandle() directly.
HINSTANCE qWinAppInstValue = GetModuleHandle(nullptr);
HICON icon = ExtractIcon(qWinAppInstValue, identifier.toStdWString().c_str(), 0);
if (icon) {
extern QPixmap qt_pixmapFromWinHICON(HICON icon);
qiAppIcon = QIcon(qt_pixmapFromWinHICON(icon));
DestroyIcon(icon);
}
#endif
return OverlayAppInfo(qsAppName, qiAppIcon);
}
OverlayAppInfo::OverlayAppInfo(QString name, QIcon icon) {
qsDisplayName = name;
qiIcon = icon;
}
OverlayMouse::OverlayMouse(QGraphicsItem *p) : QGraphicsPixmapItem(p) {
}
bool OverlayMouse::contains(const QPointF &) const {
return false;
}
bool OverlayMouse::collidesWithPath(const QPainterPath &, Qt::ItemSelectionMode) const {
return false;
}
OverlayGroup::OverlayGroup() : QGraphicsItem() {
}
int OverlayGroup::type() const {
return Type;
}
void OverlayGroup::paint(QPainter *, const QStyleOptionGraphicsItem *, QWidget *) {
}
QRectF OverlayGroup::boundingRect() const {
QRectF qr;
foreach (const QGraphicsItem *item, childItems())
if (item->isVisible())
qr |= item->boundingRect().translated(item->pos());
return qr;
}
Overlay::Overlay() : QObject() {
d = nullptr;
m_initialized.store(false);
qlsServer = nullptr;
QMetaObject::connectSlotsByName(this);
}
Overlay::~Overlay() {
setActive(false);
if (d) {
delete d;
}
// Need to be deleted first, since destructor references lingering QLocalSockets
foreach (OverlayClient *oc, qlClients) {
// As we're the one closing the connection, we do not need to be
// notified of disconnects. This is important because on disconnect we
// also remove (and 'delete') the overlay client.
disconnect(oc->qlsSocket, SIGNAL(disconnected()), this, SLOT(disconnected()));
disconnect(oc->qlsSocket, SIGNAL(error(QLocalSocket::LocalSocketError)), this,
SLOT(error(QLocalSocket::LocalSocketError)));
delete oc;
}
}
void Overlay::initialize() {
if (!m_initialized.load()) {
platformInit();
forceSettings();
createPipe();
m_initialized.store(true);
}
}
void Overlay::setActive(bool act) {
if (!act && !m_initialized.load()) {
// Disabling when the Overlay hasn't been initialized yet, doesn't make much sense
return;
}
// Make sure the Overlay is initialized
initialize();
setActiveInternal(act);
}
void Overlay::createPipe() {
qlsServer = new QLocalServer(this);
// Allow anyone to access the pipe in order to communicate with the overlay
qlsServer->setSocketOptions(QLocalServer::WorldAccessOption);
QString pipepath;
#ifdef Q_OS_WIN
pipepath = QLatin1String("MumbleOverlayPipe");
#else
{
QString xdgRuntimePath = QProcessEnvironment::systemEnvironment().value(QLatin1String("XDG_RUNTIME_DIR"));
QDir xdgRuntimeDir = QDir(xdgRuntimePath);
if (!xdgRuntimePath.isNull() && xdgRuntimeDir.exists()) {
pipepath = xdgRuntimeDir.absoluteFilePath(QLatin1String("MumbleOverlayPipe"));
} else {
pipepath = QDir::home().absoluteFilePath(QLatin1String(".MumbleOverlayPipe"));
}
}
{
QFile f(pipepath);
if (f.exists()) {
qWarning() << "Overlay: Removing old socket on" << pipepath;
f.remove();
}
}
#endif
if (!qlsServer->listen(pipepath)) {
QMessageBox::warning(nullptr, QLatin1String("Mumble"),
tr("Failed to create communication with overlay at %2: %1. No overlay will be available.")
.arg(qlsServer->errorString().toHtmlEscaped(), pipepath.toHtmlEscaped()),
QMessageBox::Ok, QMessageBox::NoButton);
} else {
qWarning() << "Overlay: Listening on" << qlsServer->fullServerName();
connect(qlsServer, SIGNAL(newConnection()), this, SLOT(newConnection()));
}
}
void Overlay::newConnection() {
while (qlsServer && qlsServer->hasPendingConnections()) {
QLocalSocket *qls = qlsServer->nextPendingConnection();
OverlayClient *oc = new OverlayClient(qls, this);
qlClients << oc;
connect(qls, SIGNAL(disconnected()), this, SLOT(disconnected()));
connect(qls, SIGNAL(error(QLocalSocket::LocalSocketError)), this, SLOT(error(QLocalSocket::LocalSocketError)));
}
}
void Overlay::disconnected() {
QLocalSocket *qls = qobject_cast< QLocalSocket * >(sender());
foreach (OverlayClient *oc, qlClients) {
if (oc->qlsSocket == qls) {
qlClients.removeAll(oc);
delete oc;
return;
}
}
}
void Overlay::error(QLocalSocket::LocalSocketError) {
disconnected();
}
bool Overlay::isActive() const {
return !qlClients.isEmpty();
}
void Overlay::toggleShow() {
if (Global::get().ocIntercept) {
Global::get().ocIntercept->hideGui();
} else {
foreach (OverlayClient *oc, qlClients) {
if (oc->uiPid) {
#if defined(Q_OS_WIN)
HWND hwnd = GetForegroundWindow();
DWORD pid = 0;
GetWindowThreadProcessId(hwnd, &pid);
if (pid != oc->uiPid)
continue;
#elif defined(Q_OS_MAC)
if (static_cast< quint64 >(getForegroundProcessId()) != oc->uiPid)
continue;
# if 0
// Fullscreen only.
if (! CGDisplayIsCaptured(CGMainDisplayID()))
continue;
# endif
#endif
oc->showGui();
return;
}
}
}
}
void Overlay::forceSettings() {
foreach (OverlayClient *oc, qlClients) { oc->reset(); }
updateOverlay();
}
void Overlay::verifyTexture(ClientUser *cp, bool allowupdate) {
qsQueried.remove(cp->uiSession);
ClientUser *self = ClientUser::get(Global::get().uiSession);
allowupdate = allowupdate && self && self->cChannel->isLinked(cp->cChannel);
if (allowupdate && !cp->qbaTextureHash.isEmpty() && cp->qbaTexture.isEmpty())
cp->qbaTexture = Global::get().db->blob(cp->qbaTextureHash);
if (!cp->qbaTexture.isEmpty()) {
bool valid = true;
if (cp->qbaTexture.length() < static_cast< int >(sizeof(unsigned int))) {
valid = false;
} else if (qFromBigEndian< unsigned int >(reinterpret_cast< const unsigned char * >(cp->qbaTexture.constData()))
== 600 * 60 * 4) {
QByteArray qba = qUncompress(cp->qbaTexture);
if (qba.length() != 600 * 60 * 4) {
valid = false;
} else {
int width = 0;
int height = 0;
const unsigned int *ptr = reinterpret_cast< const unsigned int * >(qba.constData());
// If we have an alpha only part on the right side of the image ignore it
for (int y = 0; y < 60; ++y) {
for (int x = 0; x < 600; ++x) {
if (ptr[y * 600 + x] & 0xff000000) {
if (x > width)
width = x;
if (y > height)
height = y;
}
}
}
// Full size image? More likely image without alpha; fix it.
if ((width == 599) && (height == 59)) {
width = 0;
height = 0;
for (int y = 0; y < 60; ++y) {
for (int x = 0; x < 600; ++x) {
if (ptr[y * 600 + x] & 0x00ffffff) {
if (x > width)
width = x;
if (y > height)
height = y;
}
}
}
}
if (!width || !height) {
valid = false;
} else {
QImage img = QImage(width + 1, height + 1, QImage::Format_ARGB32);
{
QImage srcimg(reinterpret_cast< const uchar * >(qba.constData()), 600, 60,
QImage::Format_ARGB32);
QPainter imgp(&img);
img.fill(0);
imgp.setRenderHint(QPainter::Antialiasing);
imgp.setRenderHint(QPainter::TextAntialiasing);
imgp.setBackground(QColor(0, 0, 0, 0));
imgp.setCompositionMode(QPainter::CompositionMode_Source);
imgp.drawImage(0, 0, srcimg);
}
cp->qbaTexture = QByteArray();
QBuffer qb(&cp->qbaTexture);
qb.open(QIODevice::WriteOnly);
QImageWriter qiw(&qb, "png");
qiw.write(img);
cp->qbaTextureFormat = QString::fromLatin1("png").toUtf8();
}
}
} else {
QBuffer qb(&cp->qbaTexture);
qb.open(QIODevice::ReadOnly);
QImageReader qir;
qir.setAutoDetectImageFormat(false);
QByteArray fmt;
if (RichTextImage::isValidImage(cp->qbaTexture, fmt)) {
qir.setFormat(fmt);
qir.setDevice(&qb);
if (!qir.canRead() || (qir.size().width() > 1024) || (qir.size().height() > 1024)) {
valid = false;
} else {
cp->qbaTextureFormat = qir.format();
QImage qi = qir.read();
valid = !qi.isNull();
}
} else {
valid = false;
}
}
if (!valid) {
cp->qbaTexture = QByteArray();
cp->qbaTextureHash = QByteArray();
}
}
if (allowupdate)
updateOverlay();
}
typedef QPair< QString, quint32 > qpChanCol;
void Overlay::updateOverlay() {
if (!Global::get().uiSession)
qsQueried.clear();
if (qlClients.isEmpty())
return;
qsQuery.clear();
foreach (OverlayClient *oc, qlClients) {
if (!oc->update()) {
qWarning() << "Overlay: Dead client detected. PID" << oc->uiPid << oc->qsExecutablePath;
qlClients.removeAll(oc);
oc->scheduleDelete();
break;
}
}
if (!qsQuery.isEmpty()) {
MumbleProto::RequestBlob mprb;
foreach (unsigned int session, qsQuery) {
qsQueried.insert(session);
mprb.add_session_texture(session);
}
Global::get().sh->sendMessage(mprb);
}
}
void Overlay::requestTexture(ClientUser *cu) {
if (cu->qbaTexture.isEmpty() && !qsQueried.contains(cu->uiSession)) {
cu->qbaTexture = Global::get().db->blob(cu->qbaTextureHash);
if (cu->qbaTexture.isEmpty())
qsQuery.insert(cu->uiSession);
else
verifyTexture(cu, false);
}
}