mirror-chatterino2/src/util/XDGHelper.cpp
nerix 189be8c68f
chore: remove old Qt 5.12 code, update docs and FreeBSD runner (#5396)
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
2024-06-23 12:26:59 +00:00

258 lines
8.4 KiB
C++

#include "util/XDGHelper.hpp"
#include "common/Literals.hpp"
#include "common/QLogging.hpp"
#include "util/CombinePath.hpp"
#include "util/XDGDesktopFile.hpp"
#include "util/XDGDirectory.hpp"
#include <QDebug>
#include <QProcess>
#include <QRegularExpression>
#include <QSettings>
#include <QStringLiteral>
#include <QTextCodec>
#include <QtGlobal>
#include <unordered_set>
#if defined(Q_OS_UNIX) and !defined(Q_OS_DARWIN)
using namespace chatterino::literals;
namespace {
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
const auto &LOG = chatterinoXDG;
using namespace chatterino;
const auto HTTPS_MIMETYPE = u"x-scheme-handler/https"_s;
/// Read the given mimeapps file and try to find an association for the HTTPS_MIMETYPE
///
/// If the mimeapps file is invalid (i.e. wasn't read), return nullopt
/// If the file is valid, look for the default Desktop File ID handler for the HTTPS_MIMETYPE
/// If no default Desktop File ID handler is found, populate `associations`
/// and `denyList` with Desktop File IDs from "Added Associations" and "Removed Associations" respectively
std::optional<XDGDesktopFile> processMimeAppsList(
const QString &mimeappsPath, QStringList &associations,
std::unordered_set<QString> &denyList)
{
XDGDesktopFile mimeappsFile(mimeappsPath);
if (!mimeappsFile.isValid())
{
return {};
}
// get the list of Desktop File IDs for the given mimetype under the "Default
// Applications" group in the mimeapps.list file
auto defaultGroup = mimeappsFile.getEntries("Default Applications");
auto defaultApps = defaultGroup.find(HTTPS_MIMETYPE);
if (defaultApps != defaultGroup.cend())
{
// for each desktop ID in the list:
auto desktopIds = defaultApps->second.split(';', Qt::SkipEmptyParts);
for (const auto &entry : desktopIds)
{
auto desktopId = entry.trimmed();
// if a valid desktop file is found, verify that it is associated
// with the type. being in the default list gives it an implicit
// association, so just check that it's not in the denylist
if (!denyList.contains(desktopId))
{
auto desktopFile = XDGDesktopFile::findDesktopFile(desktopId);
// if a valid association is found, we have found the default
// application
if (desktopFile.has_value())
{
return desktopFile;
}
}
}
}
// no definitive default application found. process added and removed
// associations, then return empty
// load any removed associations into the denylist
auto removedGroup = mimeappsFile.getEntries("Removed Associations");
auto removedApps = removedGroup.find(HTTPS_MIMETYPE);
if (removedApps != removedGroup.end())
{
auto desktopIds = removedApps->second.split(';', Qt::SkipEmptyParts);
for (const auto &entry : desktopIds)
{
denyList.insert(entry.trimmed());
}
}
// append any created associations to the associations list
auto addedGroup = mimeappsFile.getEntries("Added Associations");
auto addedApps = addedGroup.find(HTTPS_MIMETYPE);
if (addedApps != addedGroup.end())
{
auto desktopIds = addedApps->second.split(';', Qt::SkipEmptyParts);
for (const auto &entry : desktopIds)
{
associations.push_back(entry.trimmed());
}
}
return {};
}
std::optional<XDGDesktopFile> searchMimeAppsListsInDirectory(
const QString &directory, QStringList &associations,
std::unordered_set<QString> &denyList)
{
static auto desktopNames = qEnvironmentVariable("XDG_CURRENT_DESKTOP")
.split(':', Qt::SkipEmptyParts);
static const QString desktopFilename = QStringLiteral("%1-mimeapps.list");
static const QString nonDesktopFilename = QStringLiteral("mimeapps.list");
// try desktop specific mimeapps.list files first
for (const auto &desktopName : desktopNames)
{
auto fileName =
combinePath(directory, desktopFilename.arg(desktopName));
auto defaultApp = processMimeAppsList(fileName, associations, denyList);
if (defaultApp.has_value())
{
return defaultApp;
}
}
// try the generic mimeapps.list
auto fileName = combinePath(directory, nonDesktopFilename);
auto defaultApp = processMimeAppsList(fileName, associations, denyList);
if (defaultApp.has_value())
{
return defaultApp;
}
// no definitive default application found
return {};
}
} // namespace
namespace chatterino {
/// Try to figure out the most reasonably default web browser to use
///
/// If the `xdg-settings` program is available, use that
/// If not, read through all possible mimapps files in the order specified here: https://specifications.freedesktop.org/mime-apps-spec/mime-apps-spec-1.0.1.html#file
/// If no mimeapps file has a default, try to use the Added Associations in those files
std::optional<XDGDesktopFile> getDefaultBrowserDesktopFile()
{
// no xdg-utils, find it manually by searching mimeapps.list files
QStringList associations;
std::unordered_set<QString> denyList;
// config dirs first
for (const auto &configDir : getXDGDirectories(XDGDirectoryType::Config))
{
auto defaultApp =
searchMimeAppsListsInDirectory(configDir, associations, denyList);
if (defaultApp.has_value())
{
return defaultApp;
}
}
// data dirs for backwards compatibility
for (const auto &dataDir : getXDGDirectories(XDGDirectoryType::Data))
{
auto appsDir = combinePath(dataDir, "applications");
auto defaultApp =
searchMimeAppsListsInDirectory(appsDir, associations, denyList);
if (defaultApp.has_value())
{
return defaultApp;
}
}
// no mimeapps.list has an explicit default, use the most preferred added
// association that exists. We could search here for one we support...
if (!associations.empty())
{
for (const auto &desktopId : associations)
{
auto desktopFile = XDGDesktopFile::findDesktopFile(desktopId);
if (desktopFile.has_value())
{
return desktopFile;
}
}
}
// use xdg-settings if installed
QProcess xdgSettings;
xdgSettings.start("xdg-settings", {"get", "default-web-browser"},
QIODevice::ReadOnly);
xdgSettings.waitForFinished(1000);
if (xdgSettings.exitStatus() == QProcess::ExitStatus::NormalExit &&
xdgSettings.error() == QProcess::UnknownError &&
xdgSettings.exitCode() == 0)
{
return XDGDesktopFile::findDesktopFile(
xdgSettings.readAllStandardOutput().trimmed());
}
return {};
}
QString parseDesktopExecProgram(const QString &execKey)
{
static const QRegularExpression unescapeReservedCharacters(
R"(\\(["`$\\]))");
QString program = execKey;
// string values in desktop files escape all backslashes. This is an
// independent escaping scheme that must be processed first
program.replace(u"\\\\"_s, u"\\"_s);
if (!program.startsWith('"'))
{
// not quoted, trim after the first space (if any)
auto end = program.indexOf(' ');
if (end != -1)
{
program = program.left(end);
}
}
else
{
// quoted
auto endQuote = program.indexOf('"', 1);
if (endQuote == -1)
{
// No end quote found, the returned program might be malformed
program = program.mid(1);
qCWarning(LOG).noquote().nospace()
<< "Malformed desktop entry key " << program << ", originally "
<< execKey << ", you might run into issues";
}
else
{
// End quote found
program = program.mid(1, endQuote - 1);
}
}
// program now contains the first token of the command line.
// this is either the program name with an absolute path, or just the program name
// denoting it's a relative path. Either will be handled by QProcess cleanly
// now, there is a second escaping scheme specific to the
// exec key that must be applied.
program.replace(unescapeReservedCharacters, "\\1");
return program;
}
} // namespace chatterino
#endif