Skip to content

Commit

Permalink
Add default URI scheme handler manipulators to qx-system
Browse files Browse the repository at this point in the history
Allows for registering an application to be the default handler for a
given scheme-based protocol:

setDefaultProtocolHandler()
isDefaultProtocolHandler()
removeDefaultProtocolHandler()
  • Loading branch information
oblivioncth committed Oct 25, 2023
1 parent b3a2a54 commit 8823851
Show file tree
Hide file tree
Showing 6 changed files with 381 additions and 1 deletion.
3 changes: 3 additions & 0 deletions lib/core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
qx_add_component("Core"
HEADERS_PRIVATE
qx-json_p.h
qx-system_p.h
HEADERS_API
qx-abstracterror.h
qx-algorithm.h
Expand Down Expand Up @@ -57,6 +58,8 @@ qx_add_component("Core"
qx-system.cpp
qx-system_linux.cpp
qx-system_win.cpp
qx-system_p_win.cpp
qx-system_p_linux.cpp
qx-systemerror.cpp
qx-systemerror_linux.cpp
qx-systemerror_win.cpp
Expand Down
5 changes: 4 additions & 1 deletion lib/core/include/qx/core/qx-system.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,15 @@ QX_CORE_EXPORT SystemError forceKillProcess(quint32 processId);

QX_CORE_EXPORT bool enforceSingleInstance(QString uniqueAppId);

QX_CORE_EXPORT bool setDefaultProtocolHandler(const QString& scheme, const QString& name, const QString& path = {}, const QStringList& args = {});
QX_CORE_EXPORT bool isDefaultProtocolHandler(const QString& scheme, const QString& path = {});
QX_CORE_EXPORT bool removeDefaultProtocolHandler(const QString& scheme, const QString& path = {});

#ifdef __linux__
// Temporary means to and end, will replace with full parser eventually
QX_CORE_EXPORT QSettings::Format xdgSettingsFormat();
QX_CORE_EXPORT QSettings::Format xdgDesktopSettingsFormat();
#endif

}

#endif // QX_SYSTEM_H
96 changes: 96 additions & 0 deletions lib/core/src/qx-system.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
// Unit Includes
#include "qx/core/qx-system.h"
#include "qx-system_p.h"

// Qt Includes
#include <QDir>
#include <QCoreApplication>

/*!
* @file qx-system.h
Expand All @@ -11,6 +16,13 @@
namespace Qx
{

namespace // Anonymous namespace for effectively private (to this cpp) functions
{

bool isValidScheme(QStringView scheme) { return !scheme.isEmpty() && !scheme.contains(QChar::Space); };

}

//-Namespace Functions-------------------------------------------------------------------------------------------------------------
/*!
* @fn quint32 processId(QString processName)
Expand Down Expand Up @@ -123,4 +135,88 @@ bool processIsRunning(quint32 processId) { return processName(processId).isNull(
* changed in future revisions once set.
*/

/*!
* Sets the application at @a path as the default handler for URI requests of @a scheme for the
* current user. The registration is configured so that when a URL that uses the protocol is followed,
* the program at the given path will be executed with the scheme URL as the last argument. Generally,
* the user is shown a prompt with the friendly name of the application @a name
* when the protocol is used.
*
* @a scheme cannot contain whitespace. If @a path is left empty, it defaults to
* QDir::toNativeSeparators(QCoreApplication::applicationFilePath()).
*
* Returns @c true if the operation was successful; otherwise, returns @c false.
*
* Commonly, applications are designed to handle scheme URLs as a singular argument:
*
* @code
* myapp myscheme://some-data-here
* @endcode
*
* as most operating system facilities that allow a user to select a default protocol handler do not
* for adding additional arguments; however, additional arguments can be provided via @a args, which
* are placed before the scheme URL.
*
* @note On Linux this function relies on FreeDesktop conformance.
*
* @warning The provided arguments are automatically quoted, but not escaped. If the provided arguments
* contain reserved characters, they will need to be escaped manually.
*
* @sa isDefaultProtocolHandler() and removeDefaultProtocolHandler().
*/
bool setDefaultProtocolHandler(const QString& scheme, const QString& name, const QString& path, const QStringList& args)
{
if(!isValidScheme(scheme))
return false;

QString command = '"' + (!path.isEmpty() ? path : QDir::toNativeSeparators(QCoreApplication::applicationFilePath())) + '"';
if(!args.isEmpty())
command += uR"( ")"_s + args.join(uR"(", ")"_s) + '"';

return registerUriSchemeHandler(scheme, name, command);
}

/*!
* Returns @c true if the application at @a path is set as the default handler for URI requests of
* @a scheme for the current user; otherwise, returns @c false.
*
* If @a path is left empty, it defaults to
* QDir::toNativeSeparators(QCoreApplication::applicationFilePath()).
*
* @note On Linux this function relies on FreeDesktop conformance.
*
* @sa setDefaultProtocolHandler() and removeDefaultProtocolHandler().
*/
bool isDefaultProtocolHandler(const QString& scheme, const QString& path)
{
if(!isValidScheme(scheme))
return false;

return checkUriSchemeHandler(scheme, !path.isEmpty() ? path : QDir::toNativeSeparators(QCoreApplication::applicationFilePath()));
}

/*!
* Removes the application at @a path as the default handler for UR requests of @a scheme if it is
* currently set as such for the current user. This function can only remove the default on a per-user
* basis, so it can fail if the default is set system-wide on platforms where users cannot
* override defaults with an unset value.
*
* If @a path is left empty, it defaults to
* QDir::toNativeSeparators(QCoreApplication::applicationFilePath()).
*
* Returns @c true if the operation was successful, or the application is not the default;
* otherwise, returns @c false.
*
* @note On Linux this function relies on FreeDesktop conformance.
*
* @sa isDefaultProtocolHandler() and setDefaultProtocolHandler().
*/
bool removeDefaultProtocolHandler(const QString& scheme, const QString& path)
{
if(!isValidScheme(scheme))
return false;

return removeUriSchemeHandler(scheme, !path.isEmpty() ? path : QDir::toNativeSeparators(QCoreApplication::applicationFilePath()));
}

}
19 changes: 19 additions & 0 deletions lib/core/src/qx-system_p.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#ifndef QX_SYSTEM_P_H
#define QX_SYSTEM_P_H

// Qt Includes
#include <QString>

namespace Qx
{
/*! @cond */

//-Component Private Functions--------------------------------------------------------------------
bool registerUriSchemeHandler(const QString& scheme, const QString& name, const QString& command);
bool checkUriSchemeHandler(const QString& scheme, const QString& path);
bool removeUriSchemeHandler(const QString& scheme, const QString& path);

/*! @endcond */
}

#endif // QX_SYSTEM_P_H
169 changes: 169 additions & 0 deletions lib/core/src/qx-system_p_linux.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// Unit Includes
#include "qx-system_p.h"
#include "qx/core/qx-system.h"

// Qt Includes
#include <QSettings>
#include <QStandardPaths>
#include <QProcess>
#include <QFile>

using namespace Qt::Literals::StringLiterals;

namespace Qx
{
/*! @cond */

namespace
{

bool runXdgMime(QString* output, QStringList args)
{
QProcess xdgMime;
xdgMime.setProgram(u"xdg-mime"_s);
xdgMime.setArguments(args);
if(!output)
xdgMime.setStandardOutputFile(QProcess::nullDevice());
xdgMime.setStandardErrorFile(QProcess::nullDevice());
xdgMime.start();

bool success = xdgMime.waitForFinished(3000) && xdgMime.exitStatus() == xdgMime.NormalExit && xdgMime.exitCode() == 0;
if(output)
*output = QString::fromLocal8Bit(xdgMime.readAllStandardOutput());

return success;
}

bool pathIsDefaultHandler(QString* entryName, const QString& scheme, const QString& path)
{
// Query default MIME handler desktop entry
QString xSchemeHandler = u"x-scheme-handler/"_s + scheme;
QString dEntryFilename;
if(!runXdgMime(&dEntryFilename, {u"query"_s, u"default"_s, xSchemeHandler}) || !dEntryFilename.endsWith(u".desktop"_s))
return false; // No default or xdg-mime has failed us

if(entryName)
*entryName = dEntryFilename;

// Get entry path
QString dEntryPath = QStandardPaths::locate(QStandardPaths::ApplicationsLocation, dEntryFilename);
if(dEntryPath.isEmpty())
return false;

// Read desktop entry
QSettings de(dEntryPath, xdgDesktopSettingsFormat());
if(de.status() != QSettings::NoError)
return false;

/* Imperfect check since it could just contains a reference to the path, i.e as an
* argument, but unlikely to be an issue and allows checking for the program
* without considering arguments.
*/
QString exec = de.value("Desktop Entry/Exec").toString();
return exec.contains(path);
}

void addToValueList(QSettings& set, QStringView key, QStringView v)
{
QString vl = set.value(key).toString();
vl += v.toString() + ';';
set.setValue(key, vl);
}

void removeFromValueList(QSettings& set, QStringView key, QStringView v)
{
QString vl = set.value(key).toString();
if(vl.isEmpty())
return;

qsizetype vIdx = vl.indexOf(v);
if(vIdx == -1)
return;

qsizetype rmCount = v.size();
qsizetype scIdx = vIdx + v.size();
if(scIdx < vl.size() && vl.at(scIdx) == ';')
rmCount++;
vl.remove(vIdx, rmCount);

if(vl.isEmpty())
set.remove(key);
else
set.setValue(key, vl);
}

}

bool registerUriSchemeHandler(const QString& scheme, const QString& name, const QString& command)
{
// Get desktop entry path
QString userAppsDirPath = QStandardPaths::writableLocation(QStandardPaths::ApplicationsLocation);
QString dEntryFilename = scheme + u"-scheme-handler.desktop"_s;
QString dEntryPath = userAppsDirPath + '/' + dEntryFilename;
QString xSchemeHandler = u"x-scheme-handler/"_s + scheme;

// Create desktop entry
QSettings de(dEntryPath, xdgDesktopSettingsFormat());
de.beginGroup(u"Desktop Entry"_s);
de.setValue(u"Type"_s, u"Application"_s);
de.setValue(u"Name"_s, name);
de.setValue(u"Exec"_s, command + u" %u"_s); // %u is already passed as single param, no need for quotes
de.setValue(u"StartupNotify"_s, u"false"_s);
de.setValue(u"MimeType"_s, xSchemeHandler);
de.setValue(u"NoDisplay"_s, true);
de.endGroup();

de.sync();
if(de.status() != QSettings::NoError)
return false;

// Register MIME type
return runXdgMime(nullptr, {u"default"_s, dEntryFilename, xSchemeHandler});

// Alternatively "xdg-settings set default-url-scheme-handler *scheme* *.desktop_file*" can be used
}

bool checkUriSchemeHandler(const QString& scheme, const QString& path)
{
return pathIsDefaultHandler(nullptr, scheme, path);
}

bool removeUriSchemeHandler(const QString& scheme, const QString& path)
{
QString entryName;
if(pathIsDefaultHandler(&entryName, scheme, path))
return false;

// Find mimeapps.list
const QString mimeappslist = u"mimeapps.list"_s;
QString mimeappsPath = QStandardPaths::locate(QStandardPaths::ConfigLocation, mimeappslist);
if(mimeappsPath.isEmpty()) // Check deprecated location
mimeappsPath = QStandardPaths::locate(QStandardPaths::ApplicationsLocation, mimeappslist);
if(mimeappsPath.isEmpty())
return false;

// Read mimeapps.list
QSettings ma(mimeappsPath, xdgDesktopSettingsFormat());
if(ma.status() != QSettings::NoError)
return false;

QString xSchemeHandler = u"x-scheme-handler/"_s + scheme;

// Remove handler as default if present
removeFromValueList(ma, u"Default Applications/"_s + xSchemeHandler, entryName);

// Remove handler from added associations if present
removeFromValueList(ma, u"Added Associations/"_s + xSchemeHandler, entryName);

// Add to removed associations
addToValueList(ma, u"Removed Associations/"_s + xSchemeHandler, entryName);

// Save and check status
ma.sync();
return ma.status() == QSettings::NoError;
}

/*! @endcond */
}


Loading

0 comments on commit 8823851

Please sign in to comment.