// 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 #endif // QT_NO_QOBJECT #include #include #include #include #include #include #include # include # include # include 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 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([this, &removed, &key]() { std::vector children = { removed }; const int length = val::global("window")["localStorage"]["length"].as(); for (int i = 0; i < length; ++i) { const QString storedKeyWithPrefix = QString::fromStdString( val::global("window")["localStorage"].call("key", i).as()); 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("removeItem", child); }); } void QWasmLocalStorageSettingsPrivate::set(const QString &key, const QVariant &value) { qstdweb::runTaskOnMainThread([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("setItem", keyString, valueString); }); } std::optional QWasmLocalStorageSettingsPrivate::get(const QString &key) const { return qstdweb::runTaskOnMainThread>( [this, &key]() -> std::optional { for (const auto &prefix : m_keyPrefixes) { const std::string keyString = QString(prefix + key).toStdString(); const emscripten::val value = val::global("window")["localStorage"].call("getItem", keyString); if (!value.isNull()) { return QSettingsPrivate::stringToVariant( QString::fromStdString(value.as())); } if (!fallbacks) { return std::nullopt; } } return std::nullopt; }); } QStringList QWasmLocalStorageSettingsPrivate::children(const QString &prefix, ChildSpec spec) const { return qstdweb::runTaskOnMainThread([this, &prefix, &spec]() -> QStringList { QSet 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(); for (int i = 0; i < length; ++i) { for (const auto &storagePrefix : m_keyPrefixes) { const QString keyString = QString::fromStdString(val::global("window")["localStorage"] .call("key", i) .as()); 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([this]() { // Get all Qt keys from window.localStorage const int length = val::global("window")["localStorage"]["length"].as(); QStringList keys; keys.reserve(length); for (int i = 0; i < length; ++i) keys.append(QString::fromStdString( (val::global("window")["localStorage"].call("key", i).as()))); // 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("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(); void clear() override; void sync() override; private: bool writeSettingsToTemporaryFile(const QString &fileName, void *dataPtr, int size); void loadIndexedDBFiles(); QString databaseName; QString id; }; 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; } databaseName = organization; id = application; loadIndexedDBFiles(); QConfFileSettingsPrivate::initAccess(); } QWasmIDBSettingsPrivate::~QWasmIDBSettingsPrivate() = default; bool QWasmIDBSettingsPrivate::writeSettingsToTemporaryFile(const QString &fileName, 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(dataPtr), size); } void QWasmIDBSettingsPrivate::clear() { QConfFileSettingsPrivate::clear(); int error = 0; emscripten_idb_delete(DbName, fileName().toLocal8Bit(), &error); setStatus(!!error ? QSettings::AccessError : QSettings::NoError); } void QWasmIDBSettingsPrivate::sync() { // Reload the files, in case there were any changes in IndexedDB, and flush them to disk. // Thanks to this, QConfFileSettingsPrivate::sync will handle key merging correctly. loadIndexedDBFiles(); 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(dataPointer.data()), dataPointer.length(), &error); setStatus(!!error ? QSettings::AccessError : QSettings::NoError); } } void QWasmIDBSettingsPrivate::loadIndexedDBFiles() { for (const auto *confFile : getConfFiles()) { int exists = 0; int error = 0; emscripten_idb_exists(DbName, confFile->name.toLocal8Bit(), &exists, &error); if (error) { setStatus(QSettings::AccessError); return; } if (exists) { void *contents; int size; emscripten_idb_load(DbName, confFile->name.toLocal8Bit(), &contents, &size, &error); if (error || !writeSettingsToTemporaryFile(confFile->name, contents, size)) { setStatus(QSettings::AccessError); return; } } } } 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( []() { return val::global("navigator")["cookieEnabled"].as(); }); 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