437 lines
16 KiB
C++
437 lines
16 KiB
C++
// Copyright (C) 2022 The Qt Company Ltd.
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
|
|
|
#include "qsettings.h"
|
|
#ifndef QT_NO_SETTINGS
|
|
|
|
#include "qsettings_p.h"
|
|
#ifndef QT_NO_QOBJECT
|
|
#include "qcoreapplication.h"
|
|
#include <QFile>
|
|
#endif // QT_NO_QOBJECT
|
|
#include <QDebug>
|
|
#include <QtCore/private/qstdweb_p.h>
|
|
|
|
#include <QFileInfo>
|
|
#include <QDir>
|
|
#include <QList>
|
|
#include <QSet>
|
|
|
|
#include <emscripten.h>
|
|
# include <emscripten/proxying.h>
|
|
# include <emscripten/threading.h>
|
|
# include <emscripten/val.h>
|
|
|
|
QT_BEGIN_NAMESPACE
|
|
|
|
using emscripten::val;
|
|
using namespace Qt::StringLiterals;
|
|
|
|
namespace {
|
|
QStringView keyNameFromPrefixedStorageName(QStringView prefix, QStringView prefixedStorageName)
|
|
{
|
|
// Return the key slice after m_keyPrefix, or an empty string view if no match
|
|
if (!prefixedStorageName.startsWith(prefix))
|
|
return QStringView();
|
|
return prefixedStorageName.sliced(prefix.length());
|
|
}
|
|
} // namespace
|
|
|
|
//
|
|
// Native settings implementation for WebAssembly using window.localStorage
|
|
// as the storage backend. localStorage is a key-value store with a synchronous
|
|
// API and a 5MB storage limit.
|
|
//
|
|
class QWasmLocalStorageSettingsPrivate final : public QSettingsPrivate
|
|
{
|
|
public:
|
|
QWasmLocalStorageSettingsPrivate(QSettings::Scope scope, const QString &organization,
|
|
const QString &application);
|
|
~QWasmLocalStorageSettingsPrivate() final = default;
|
|
|
|
void remove(const QString &key) final;
|
|
void set(const QString &key, const QVariant &value) final;
|
|
std::optional<QVariant> get(const QString &key) const final;
|
|
QStringList children(const QString &prefix, ChildSpec spec) const final;
|
|
void clear() final;
|
|
void sync() final;
|
|
void flush() final;
|
|
bool isWritable() const final;
|
|
QString fileName() const final;
|
|
|
|
private:
|
|
QStringList m_keyPrefixes;
|
|
};
|
|
|
|
QWasmLocalStorageSettingsPrivate::QWasmLocalStorageSettingsPrivate(QSettings::Scope scope,
|
|
const QString &organization,
|
|
const QString &application)
|
|
: QSettingsPrivate(QSettings::NativeFormat, scope, organization, application)
|
|
{
|
|
if (organization.isEmpty()) {
|
|
setStatus(QSettings::AccessError);
|
|
return;
|
|
}
|
|
|
|
// The key prefix contians "qt" to separate Qt keys from other keys on localStorage, a
|
|
// version tag to allow for making changes to the key format in the future, the org
|
|
// and app names.
|
|
//
|
|
// User code could could create separate settings object with different org and app names,
|
|
// and would expect them to have separate settings. Also, different webassembly instances
|
|
// on the page could write to the same window.localStorage. Add the org and app name
|
|
// to the key prefix to differentiate, even if that leads to keys with redundant sections
|
|
// for the common case of a single org and app name.
|
|
//
|
|
// Also, the common Qt mechanism for user/system scope and all-application settings are
|
|
// implemented, using different prefixes.
|
|
const QString allAppsSetting = QStringLiteral("all-apps");
|
|
const QString systemSetting = QStringLiteral("sys-tem");
|
|
|
|
const QLatin1String separator("-");
|
|
const QLatin1String doubleSeparator("--");
|
|
const QString escapedOrganization = QString(organization).replace(separator, doubleSeparator);
|
|
const QString escapedApplication = QString(application).replace(separator, doubleSeparator);
|
|
const QString prefix = "qt-v0-" + escapedOrganization + separator;
|
|
if (scope == QSettings::Scope::UserScope) {
|
|
if (!escapedApplication.isEmpty())
|
|
m_keyPrefixes.push_back(prefix + escapedApplication + separator);
|
|
m_keyPrefixes.push_back(prefix + allAppsSetting + separator);
|
|
}
|
|
if (!escapedApplication.isEmpty()) {
|
|
m_keyPrefixes.push_back(prefix + escapedApplication + separator + systemSetting
|
|
+ separator);
|
|
}
|
|
m_keyPrefixes.push_back(prefix + allAppsSetting + separator + systemSetting + separator);
|
|
}
|
|
|
|
void QWasmLocalStorageSettingsPrivate::remove(const QString &key)
|
|
{
|
|
const std::string removed = QString(m_keyPrefixes.first() + key).toStdString();
|
|
|
|
qstdweb::runTaskOnMainThread<void>([this, &removed, &key]() {
|
|
std::vector<std::string> children = { removed };
|
|
const int length = val::global("window")["localStorage"]["length"].as<int>();
|
|
for (int i = 0; i < length; ++i) {
|
|
const QString storedKeyWithPrefix = QString::fromStdString(
|
|
val::global("window")["localStorage"].call<val>("key", i).as<std::string>());
|
|
|
|
const QStringView storedKey = keyNameFromPrefixedStorageName(
|
|
m_keyPrefixes.first(), QStringView(storedKeyWithPrefix));
|
|
if (storedKey.isEmpty() || !storedKey.startsWith(key))
|
|
continue;
|
|
|
|
children.push_back(storedKeyWithPrefix.toStdString());
|
|
}
|
|
|
|
for (const auto &child : children)
|
|
val::global("window")["localStorage"].call<val>("removeItem", child);
|
|
});
|
|
}
|
|
|
|
void QWasmLocalStorageSettingsPrivate::set(const QString &key, const QVariant &value)
|
|
{
|
|
qstdweb::runTaskOnMainThread<void>([this, &key, &value]() {
|
|
const std::string keyString = QString(m_keyPrefixes.first() + key).toStdString();
|
|
const std::string valueString = QSettingsPrivate::variantToString(value).toStdString();
|
|
val::global("window")["localStorage"].call<void>("setItem", keyString, valueString);
|
|
});
|
|
}
|
|
|
|
std::optional<QVariant> QWasmLocalStorageSettingsPrivate::get(const QString &key) const
|
|
{
|
|
return qstdweb::runTaskOnMainThread<std::optional<QVariant>>(
|
|
[this, &key]() -> std::optional<QVariant> {
|
|
for (const auto &prefix : m_keyPrefixes) {
|
|
const std::string keyString = QString(prefix + key).toStdString();
|
|
const emscripten::val value =
|
|
val::global("window")["localStorage"].call<val>("getItem", keyString);
|
|
if (!value.isNull()) {
|
|
return QSettingsPrivate::stringToVariant(
|
|
QString::fromStdString(value.as<std::string>()));
|
|
}
|
|
if (!fallbacks) {
|
|
return std::nullopt;
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
});
|
|
}
|
|
|
|
QStringList QWasmLocalStorageSettingsPrivate::children(const QString &prefix, ChildSpec spec) const
|
|
{
|
|
return qstdweb::runTaskOnMainThread<QStringList>([this, &prefix, &spec]() -> QStringList {
|
|
QSet<QString> nodes;
|
|
// Loop through all keys on window.localStorage, return Qt keys belonging to
|
|
// this application, with the correct prefix, and according to ChildSpec.
|
|
QStringList children;
|
|
const int length = val::global("window")["localStorage"]["length"].as<int>();
|
|
for (int i = 0; i < length; ++i) {
|
|
for (const auto &storagePrefix : m_keyPrefixes) {
|
|
const QString keyString =
|
|
QString::fromStdString(val::global("window")["localStorage"]
|
|
.call<val>("key", i)
|
|
.as<std::string>());
|
|
|
|
const QStringView key =
|
|
keyNameFromPrefixedStorageName(storagePrefix, QStringView(keyString));
|
|
if (!key.isEmpty() && key.startsWith(prefix)) {
|
|
QStringList children;
|
|
QSettingsPrivate::processChild(key.sliced(prefix.length()), spec, children);
|
|
if (!children.isEmpty())
|
|
nodes.insert(children.first());
|
|
}
|
|
if (!fallbacks)
|
|
break;
|
|
}
|
|
}
|
|
|
|
return QStringList(nodes.begin(), nodes.end());
|
|
});
|
|
}
|
|
|
|
void QWasmLocalStorageSettingsPrivate::clear()
|
|
{
|
|
qstdweb::runTaskOnMainThread<void>([this]() {
|
|
// Get all Qt keys from window.localStorage
|
|
const int length = val::global("window")["localStorage"]["length"].as<int>();
|
|
QStringList keys;
|
|
keys.reserve(length);
|
|
for (int i = 0; i < length; ++i)
|
|
keys.append(QString::fromStdString(
|
|
(val::global("window")["localStorage"].call<val>("key", i).as<std::string>())));
|
|
|
|
// Remove all Qt keys. Note that localStorage does not guarantee a stable
|
|
// iteration order when the storage is mutated, which is why removal is done
|
|
// in a second step after getting all keys.
|
|
for (const QString &key : keys) {
|
|
if (!keyNameFromPrefixedStorageName(m_keyPrefixes.first(), key).isEmpty())
|
|
val::global("window")["localStorage"].call<val>("removeItem", key.toStdString());
|
|
}
|
|
});
|
|
}
|
|
|
|
void QWasmLocalStorageSettingsPrivate::sync() { }
|
|
|
|
void QWasmLocalStorageSettingsPrivate::flush() { }
|
|
|
|
bool QWasmLocalStorageSettingsPrivate::isWritable() const
|
|
{
|
|
return true;
|
|
}
|
|
|
|
QString QWasmLocalStorageSettingsPrivate::fileName() const
|
|
{
|
|
return QString();
|
|
}
|
|
|
|
//
|
|
// Native settings implementation for WebAssembly using the indexed database as
|
|
// the storage backend
|
|
//
|
|
class QWasmIDBSettingsPrivate : public QConfFileSettingsPrivate
|
|
{
|
|
public:
|
|
QWasmIDBSettingsPrivate(QSettings::Scope scope, const QString &organization,
|
|
const QString &application);
|
|
~QWasmIDBSettingsPrivate();
|
|
|
|
std::optional<QVariant> get(const QString &key) const override;
|
|
QStringList children(const QString &prefix, ChildSpec spec) const override;
|
|
void clear() override;
|
|
void sync() override;
|
|
void flush() override;
|
|
bool isWritable() const override;
|
|
void initAccess() override;
|
|
|
|
void loadLocal();
|
|
void setReady();
|
|
|
|
private:
|
|
bool writeSettingsToTemporaryFile(void *dataPtr, int size);
|
|
|
|
QString databaseName;
|
|
QString id;
|
|
};
|
|
|
|
static bool isReadReady = false;
|
|
|
|
constexpr char DbName[] = "/home/web_user";
|
|
|
|
QWasmIDBSettingsPrivate::QWasmIDBSettingsPrivate(QSettings::Scope scope,
|
|
const QString &organization,
|
|
const QString &application)
|
|
: QConfFileSettingsPrivate(QSettings::WebIndexedDBFormat, scope, organization, application)
|
|
{
|
|
Q_ASSERT_X(qstdweb::haveJspi(), Q_FUNC_INFO, "QWasmIDBSettingsPrivate needs JSPI to work");
|
|
|
|
if (organization.isEmpty()) {
|
|
setStatus(QSettings::AccessError);
|
|
return;
|
|
}
|
|
|
|
setStatus(QSettings::AccessError); // access error until sandbox gets loaded
|
|
databaseName = organization;
|
|
id = application;
|
|
|
|
int exists = 0;
|
|
int error = 0;
|
|
emscripten_idb_exists(DbName, fileName().toLocal8Bit(), &exists, &error);
|
|
if (error) {
|
|
setStatus(QSettings::AccessError);
|
|
return;
|
|
}
|
|
if (exists) {
|
|
void *contents;
|
|
int size;
|
|
emscripten_idb_load(DbName, fileName().toLocal8Bit(), &contents, &size, &error);
|
|
if (error || !writeSettingsToTemporaryFile(contents, size)) {
|
|
setStatus(QSettings::AccessError);
|
|
return;
|
|
}
|
|
}
|
|
|
|
setReady();
|
|
}
|
|
|
|
QWasmIDBSettingsPrivate::~QWasmIDBSettingsPrivate() = default;
|
|
|
|
void QWasmIDBSettingsPrivate::initAccess()
|
|
{
|
|
if (isReadReady)
|
|
QConfFileSettingsPrivate::initAccess();
|
|
}
|
|
|
|
std::optional<QVariant> QWasmIDBSettingsPrivate::get(const QString &key) const
|
|
{
|
|
if (isReadReady)
|
|
return QConfFileSettingsPrivate::get(key);
|
|
|
|
return std::nullopt;
|
|
}
|
|
|
|
bool QWasmIDBSettingsPrivate::writeSettingsToTemporaryFile(void *dataPtr, int size)
|
|
{
|
|
QFile file(fileName());
|
|
QFileInfo fileInfo(fileName());
|
|
QDir dir(fileInfo.path());
|
|
if (!dir.exists())
|
|
dir.mkpath(fileInfo.path());
|
|
|
|
if (!file.open(QFile::WriteOnly))
|
|
return false;
|
|
|
|
return size == file.write(reinterpret_cast<char *>(dataPtr), size);
|
|
}
|
|
|
|
QStringList QWasmIDBSettingsPrivate::children(const QString &prefix, ChildSpec spec) const
|
|
{
|
|
return QConfFileSettingsPrivate::children(prefix, spec);
|
|
}
|
|
|
|
void QWasmIDBSettingsPrivate::clear()
|
|
{
|
|
QConfFileSettingsPrivate::clear();
|
|
|
|
int error = 0;
|
|
emscripten_idb_delete(DbName, fileName().toLocal8Bit(), &error);
|
|
setStatus(!!error ? QSettings::AccessError : QSettings::NoError);
|
|
}
|
|
|
|
void QWasmIDBSettingsPrivate::sync()
|
|
{
|
|
QConfFileSettingsPrivate::sync();
|
|
|
|
QFile file(fileName());
|
|
if (file.open(QFile::ReadOnly)) {
|
|
QByteArray dataPointer = file.readAll();
|
|
|
|
int error = 0;
|
|
emscripten_idb_store(DbName, fileName().toLocal8Bit(),
|
|
reinterpret_cast<void *>(dataPointer.data()), dataPointer.length(),
|
|
&error);
|
|
setStatus(!!error ? QSettings::AccessError : QSettings::NoError);
|
|
}
|
|
}
|
|
|
|
void QWasmIDBSettingsPrivate::flush()
|
|
{
|
|
sync();
|
|
}
|
|
|
|
bool QWasmIDBSettingsPrivate::isWritable() const
|
|
{
|
|
return isReadReady && QConfFileSettingsPrivate::isWritable();
|
|
}
|
|
|
|
void QWasmIDBSettingsPrivate::setReady()
|
|
{
|
|
isReadReady = true;
|
|
setStatus(QSettings::NoError);
|
|
QConfFileSettingsPrivate::initAccess();
|
|
}
|
|
|
|
QSettingsPrivate *QSettingsPrivate::create(QSettings::Format format, QSettings::Scope scope,
|
|
const QString &organization, const QString &application)
|
|
{
|
|
// Make WebLocalStorageFormat the default native format
|
|
if (format == QSettings::NativeFormat)
|
|
format = QSettings::WebLocalStorageFormat;
|
|
|
|
// Check if cookies are enabled (required for using persistent storage)
|
|
|
|
const bool cookiesEnabled = qstdweb::runTaskOnMainThread<bool>(
|
|
[]() { return val::global("navigator")["cookieEnabled"].as<bool>(); });
|
|
|
|
constexpr QLatin1StringView cookiesWarningMessage(
|
|
"QSettings::%1 requires cookies, falling back to IniFormat with temporary file");
|
|
if (!cookiesEnabled) {
|
|
if (format == QSettings::WebLocalStorageFormat) {
|
|
qWarning() << cookiesWarningMessage.arg("WebLocalStorageFormat");
|
|
format = QSettings::IniFormat;
|
|
} else if (format == QSettings::WebIndexedDBFormat) {
|
|
qWarning() << cookiesWarningMessage.arg("WebIndexedDBFormat");
|
|
format = QSettings::IniFormat;
|
|
}
|
|
}
|
|
if (format == QSettings::WebIndexedDBFormat && !qstdweb::haveJspi()) {
|
|
qWarning() << "QSettings::WebIndexedDBFormat requires JSPI, falling back to IniFormat with "
|
|
"temporary file";
|
|
format = QSettings::IniFormat;
|
|
}
|
|
|
|
// Create settings backend according to selected format
|
|
switch (format) {
|
|
case QSettings::Format::WebLocalStorageFormat:
|
|
return new QWasmLocalStorageSettingsPrivate(scope, organization, application);
|
|
case QSettings::Format::WebIndexedDBFormat:
|
|
return new QWasmIDBSettingsPrivate(scope, organization, application);
|
|
case QSettings::Format::IniFormat:
|
|
case QSettings::Format::CustomFormat1:
|
|
case QSettings::Format::CustomFormat2:
|
|
case QSettings::Format::CustomFormat3:
|
|
case QSettings::Format::CustomFormat4:
|
|
case QSettings::Format::CustomFormat5:
|
|
case QSettings::Format::CustomFormat6:
|
|
case QSettings::Format::CustomFormat7:
|
|
case QSettings::Format::CustomFormat8:
|
|
case QSettings::Format::CustomFormat9:
|
|
case QSettings::Format::CustomFormat10:
|
|
case QSettings::Format::CustomFormat11:
|
|
case QSettings::Format::CustomFormat12:
|
|
case QSettings::Format::CustomFormat13:
|
|
case QSettings::Format::CustomFormat14:
|
|
case QSettings::Format::CustomFormat15:
|
|
case QSettings::Format::CustomFormat16:
|
|
return new QConfFileSettingsPrivate(format, scope, organization, application);
|
|
case QSettings::Format::InvalidFormat:
|
|
return nullptr;
|
|
case QSettings::Format::NativeFormat:
|
|
Q_UNREACHABLE();
|
|
break;
|
|
}
|
|
}
|
|
|
|
QT_END_NAMESPACE
|
|
#endif // QT_NO_SETTINGS
|