Skip to content

Commit

Permalink
Add support for opening links in incognito mode on Linux & BSD (#4745)
Browse files Browse the repository at this point in the history
Co-authored-by: Rasmus Karlsson <rasmus.karlsson@pajlada.com>
  • Loading branch information
sheybey and pajlada authored Aug 6, 2023
1 parent 168f346 commit 69c983e
Show file tree
Hide file tree
Showing 18 changed files with 739 additions and 59 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
- Minor: All channels opened in browser tabs are synced when using the extension for quicker switching between tabs. (#4741)
- Minor: Show channel point redemptions without messages in usercard. (#4557)
- Minor: Allow for customizing the behavior of `Right Click`ing of usernames. (#4622, #4751)
- Minor: Added support for opening incognito links in firefox-esr and chromium. (#4745)
- Minor: Added support for opening incognito links under Linux/BSD using XDG. (#4745)
- Bugfix: Increased amount of blocked users loaded from 100 to 1,000. (#4721)
- Bugfix: Fixed generation of crashdumps by the browser-extension process when the browser was closed. (#4667)
- Bugfix: Fix spacing issue with mentions inside RTL text. (#4677)
Expand Down Expand Up @@ -55,6 +57,7 @@
- Dev: Added the ability to use an alternate linker using the `-DUSE_ALTERNATE_LINKER=...` CMake parameter. (#4711)
- Dev: The Windows installer is now built in CI. (#4408)
- Dev: Removed `getApp` and `getSettings` calls from message rendering. (#4535)
- Dev: Get the default browser executable instead of the entire command line when opening incognito links. (#4745)

## 2.4.4

Expand Down
6 changes: 6 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,12 @@ set(SOURCE_FILES
util/TypeName.hpp
util/WindowsHelper.cpp
util/WindowsHelper.hpp
util/XDGDesktopFile.cpp
util/XDGDesktopFile.hpp
util/XDGDirectory.cpp
util/XDGDirectory.hpp
util/XDGHelper.cpp
util/XDGHelper.hpp

util/serialize/Container.hpp

Expand Down
1 change: 1 addition & 0 deletions src/common/QLogging.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,4 @@ Q_LOGGING_CATEGORY(chatterinoWebsocket, "chatterino.websocket", logThreshold);
Q_LOGGING_CATEGORY(chatterinoWidget, "chatterino.widget", logThreshold);
Q_LOGGING_CATEGORY(chatterinoWindowmanager, "chatterino.windowmanager",
logThreshold);
Q_LOGGING_CATEGORY(chatterinoXDG, "chatterino.xdg", logThreshold);
1 change: 1 addition & 0 deletions src/common/QLogging.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ Q_DECLARE_LOGGING_CATEGORY(chatterinoUpdate);
Q_DECLARE_LOGGING_CATEGORY(chatterinoWebsocket);
Q_DECLARE_LOGGING_CATEGORY(chatterinoWidget);
Q_DECLARE_LOGGING_CATEGORY(chatterinoWindowmanager);
Q_DECLARE_LOGGING_CATEGORY(chatterinoXDG);
105 changes: 51 additions & 54 deletions src/util/IncognitoBrowser.cpp
Original file line number Diff line number Diff line change
@@ -1,112 +1,109 @@
#include "util/IncognitoBrowser.hpp"
#ifdef USEWINSDK
# include "util/WindowsHelper.hpp"
#elif defined(Q_OS_UNIX) and !defined(Q_OS_DARWIN)
# include "util/XDGHelper.hpp"
#endif

#include <QProcess>
#include <QRegularExpression>
#include <QVariant>

namespace {

using namespace chatterino;

#ifdef USEWINSDK
QString injectPrivateSwitch(QString command)
QString getPrivateSwitch(const QString &browserExecutable)
{
// list of command line switches to turn on private browsing in browsers
static auto switches = std::vector<std::pair<QString, QString>>{
{"firefox", "-private-window"}, {"librewolf", "-private-window"},
{"waterfox", "-private-window"}, {"icecat", "-private-window"},
{"chrome", "-incognito"}, {"vivaldi", "-incognito"},
{"opera", "-newprivatetab"}, {"opera\\\\launcher", "--private"},
{"iexplore", "-private"}, {"msedge", "-inprivate"},
{"firefox", "-private-window"}, {"librewolf", "-private-window"},
{"waterfox", "-private-window"}, {"icecat", "-private-window"},
{"chrome", "-incognito"}, {"vivaldi", "-incognito"},
{"opera", "-newprivatetab"}, {"opera\\launcher", "--private"},
{"iexplore", "-private"}, {"msedge", "-inprivate"},
{"firefox-esr", "-private-window"}, {"chromium", "-incognito"},
};

// transform into regex and replacement string
std::vector<std::pair<QRegularExpression, QString>> replacers;
for (const auto &switch_ : switches)
// compare case-insensitively
auto lowercasedBrowserExecutable = browserExecutable.toLower();

#ifdef Q_OS_WINDOWS
if (lowercasedBrowserExecutable.endsWith(".exe"))
{
replacers.emplace_back(
QRegularExpression("(" + switch_.first + "\\.exe\"?).*",
QRegularExpression::CaseInsensitiveOption),
"\\1 " + switch_.second);
lowercasedBrowserExecutable.chop(4);
}
#endif

// try to find matching regex and apply it
for (const auto &replacement : replacers)
for (const auto &switch_ : switches)
{
if (replacement.first.match(command).hasMatch())
if (lowercasedBrowserExecutable.endsWith(switch_.first))
{
command.replace(replacement.first, replacement.second);
return command;
return switch_.second;
}
}

// couldn't match any browser -> unknown browser
return QString();
return {};
}

QString getCommand()
QString getDefaultBrowserExecutable()
{
#ifdef USEWINSDK
// get default browser start command, by protocol if possible, falling back to extension if not
QString command =
getAssociatedCommand(AssociationQueryType::Protocol, L"http");
getAssociatedExecutable(AssociationQueryType::Protocol, L"http");

if (command.isNull())
{
// failed to fetch default browser by protocol, try by file extension instead
command =
getAssociatedCommand(AssociationQueryType::FileExtension, L".html");
command = getAssociatedExecutable(AssociationQueryType::FileExtension,
L".html");
}

if (command.isNull())
{
// also try the equivalent .htm extension
command =
getAssociatedCommand(AssociationQueryType::FileExtension, L".htm");
}

if (command.isNull())
{
// failed to find browser command
return QString();
}

// inject switch to enable private browsing
command = injectPrivateSwitch(command);
if (command.isNull())
{
return QString();
command = getAssociatedExecutable(AssociationQueryType::FileExtension,
L".htm");
}

return command;
}
#elif defined(Q_OS_UNIX) && !defined(Q_OS_DARWIN)
static QString defaultBrowser = []() -> QString {
auto desktopFile = getDefaultBrowserDesktopFile();
if (desktopFile.has_value())
{
auto entry = desktopFile->getEntries("Desktop Entry");
auto exec = entry.find("Exec");
if (exec != entry.end())
{
return parseDesktopExecProgram(exec->second.trimmed());
}
}
return {};
}();

return defaultBrowser;
#else
return {};
#endif
}

} // namespace

namespace chatterino {

bool supportsIncognitoLinks()
{
#ifdef USEWINSDK
return !getCommand().isNull();
#else
return false;
#endif
auto browserExe = getDefaultBrowserExecutable();
return !browserExe.isNull() && !getPrivateSwitch(browserExe).isNull();
}

bool openLinkIncognito(const QString &link)
{
#ifdef USEWINSDK
auto command = getCommand();

// TODO: split command into program path and incognito argument
return QProcess::startDetached(command, {link});
#else
return false;
#endif
auto browserExe = getDefaultBrowserExecutable();
return QProcess::startDetached(browserExe,
{getPrivateSwitch(browserExe), link});
}

} // namespace chatterino
8 changes: 4 additions & 4 deletions src/util/WindowsHelper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ void setRegisteredForStartup(bool isRegistered)
}
}

QString getAssociatedCommand(AssociationQueryType queryType, LPCWSTR query)
QString getAssociatedExecutable(AssociationQueryType queryType, LPCWSTR query)
{
static HINSTANCE shlwapi = LoadLibrary(L"shlwapi");
if (shlwapi == nullptr)
Expand Down Expand Up @@ -122,7 +122,7 @@ QString getAssociatedCommand(AssociationQueryType queryType, LPCWSTR query)
}

DWORD resultSize = 0;
if (FAILED(assocQueryString(flags, ASSOCSTR_COMMAND, query, nullptr,
if (FAILED(assocQueryString(flags, ASSOCSTR_EXECUTABLE, query, nullptr,
nullptr, &resultSize)))
{
return QString();
Expand All @@ -137,8 +137,8 @@ QString getAssociatedCommand(AssociationQueryType queryType, LPCWSTR query)

QString result;
auto buf = new wchar_t[resultSize];
if (SUCCEEDED(assocQueryString(flags, ASSOCSTR_COMMAND, query, nullptr, buf,
&resultSize)))
if (SUCCEEDED(assocQueryString(flags, ASSOCSTR_EXECUTABLE, query, nullptr,
buf, &resultSize)))
{
// QString::fromWCharArray expects the length in characters *not
// including* the null terminator, but AssocQueryStringW calculates
Expand Down
2 changes: 1 addition & 1 deletion src/util/WindowsHelper.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ void flushClipboard();
bool isRegisteredForStartup();
void setRegisteredForStartup(bool isRegistered);

QString getAssociatedCommand(AssociationQueryType queryType, LPCWSTR query);
QString getAssociatedExecutable(AssociationQueryType queryType, LPCWSTR query);

} // namespace chatterino

Expand Down
118 changes: 118 additions & 0 deletions src/util/XDGDesktopFile.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
#include "util/XDGDesktopFile.hpp"

#include "util/XDGDirectory.hpp"

#include <QDir>
#include <QFile>

#include <functional>

#if defined(Q_OS_UNIX) and !defined(Q_OS_DARWIN)

namespace chatterino {

XDGDesktopFile::XDGDesktopFile(const QString &filename)
{
QFile file(filename);
if (!file.open(QIODevice::ReadOnly))
{
return;
}
this->valid = true;

std::optional<std::reference_wrapper<XDGEntries>> entries;

while (!file.atEnd())
{
auto lineBytes = file.readLine().trimmed();

// Ignore comments & empty lines
if (lineBytes.startsWith('#') || lineBytes.size() == 0)
{
continue;
}

auto line = QString::fromUtf8(lineBytes);

if (line.startsWith('['))
{
// group header
auto end = line.indexOf(']', 1);
if (end == -1 || end == 1)
{
// malformed header - either empty or no closing bracket
continue;
}
auto groupName = line.mid(1, end - 1);

// it is against spec for the group name to already exist, but the
// parsing behavior for that case is not specified. operator[] will
// result in duplicate groups being merged, which makes the most
// sense for a read-only parser
entries = this->groups[groupName];

continue;
}

// group entry
if (!entries.has_value())
{
// no group header yet, entry before a group header is against spec
// and should be ignored
continue;
}

auto delimiter = line.indexOf('=');
if (delimiter == -1)
{
// line is not a group header or a key value pair, ignore it
continue;
}

auto key = QStringView(line).left(delimiter).trimmed().toString();
// QStringView.mid() does not do bounds checking before qt 5.15, so
// we have to do it ourselves
auto valueStart = delimiter + 1;
QString value;
if (valueStart < line.size())
{
value = QStringView(line).mid(valueStart).trimmed().toString();
}

// existing keys are against spec, so we can overwrite them with
// wild abandon
entries->get().emplace(key, value);
}
}

XDGEntries XDGDesktopFile::getEntries(const QString &groupHeader) const
{
auto group = this->groups.find(groupHeader);
if (group != this->groups.end())
{
return group->second;
}

return {};
}

std::optional<XDGDesktopFile> XDGDesktopFile::findDesktopFile(
const QString &desktopFileID)
{
for (const auto &dataDir : getXDGDirectories(XDGDirectoryType::Data))
{
auto fileName =
QDir::cleanPath(dataDir + QDir::separator() + "applications" +
QDir::separator() + desktopFileID);
XDGDesktopFile desktopFile(fileName);
if (desktopFile.isValid())
{
return desktopFile;
}
}
return {};
}

} // namespace chatterino

#endif
Loading

0 comments on commit 69c983e

Please sign in to comment.