// // DomainServerSettingsManager.cpp // domain-server/src // // Created by Stephen Birarda on 2014-06-24. // Copyright 2014 High Fidelity, Inc. // // Distributed under the Apache License, Version 2.0. // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // #include #include #include #include #include #include #include #include #include #include #include #include "DomainServerSettingsManager.h" const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json"; const QString DESCRIPTION_SETTINGS_KEY = "settings"; const QString SETTING_DEFAULT_KEY = "default"; const QString DESCRIPTION_NAME_KEY = "name"; const QString SETTING_DESCRIPTION_TYPE_KEY = "type"; const QString DESCRIPTION_COLUMNS_KEY = "columns"; const QString SETTINGS_VIEWPOINT_KEY = "viewpoint"; DomainServerSettingsManager::DomainServerSettingsManager() : _descriptionArray(), _configMap() { // load the description object from the settings description QFile descriptionFile(QCoreApplication::applicationDirPath() + SETTINGS_DESCRIPTION_RELATIVE_PATH); descriptionFile.open(QIODevice::ReadOnly); _descriptionArray = QJsonDocument::fromJson(descriptionFile.readAll()).array(); } void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList) { _configMap.loadMasterAndUserConfig(argumentList); // for now we perform a temporary transition from http-username and http-password to http_username and http_password const QVariant* oldUsername = valueForKeyPath(_configMap.getUserConfig(), "security.http-username"); const QVariant* oldPassword = valueForKeyPath(_configMap.getUserConfig(), "security.http-password"); if (oldUsername || oldPassword) { QVariantMap& settingsMap = *reinterpret_cast(_configMap.getUserConfig()["security"].data()); // remove old keys, move to new format if (oldUsername) { settingsMap["http_username"] = oldUsername->toString(); settingsMap.remove("http-username"); } if (oldPassword) { settingsMap["http_password"] = oldPassword->toString(); settingsMap.remove("http-password"); } // save the updated settings persistToFile(); } } QVariant DomainServerSettingsManager::valueOrDefaultValueForKeyPath(const QString &keyPath) { const QVariant* foundValue = valueForKeyPath(_configMap.getMergedConfig(), keyPath); if (foundValue) { return *foundValue; } else { int dotIndex = keyPath.indexOf('.'); QString groupKey = keyPath.mid(0, dotIndex); QString settingKey = keyPath.mid(dotIndex + 1); foreach(const QVariant& group, _descriptionArray.toVariantList()) { QVariantMap groupMap = group.toMap(); if (groupMap[DESCRIPTION_NAME_KEY].toString() == groupKey) { foreach(const QVariant& setting, groupMap[DESCRIPTION_SETTINGS_KEY].toList()) { QVariantMap settingMap = setting.toMap(); if (settingMap[DESCRIPTION_NAME_KEY].toString() == settingKey) { return settingMap[SETTING_DEFAULT_KEY]; } } return QVariant(); } } } return QVariant(); } const QString SETTINGS_PATH = "/settings.json"; bool DomainServerSettingsManager::handlePublicHTTPRequest(HTTPConnection* connection, const QUrl &url) { if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == SETTINGS_PATH) { // this is a GET operation for our settings // check if there is a query parameter for settings affecting a particular type of assignment const QString SETTINGS_TYPE_QUERY_KEY = "type"; QUrlQuery settingsQuery(url); QString typeValue = settingsQuery.queryItemValue(SETTINGS_TYPE_QUERY_KEY); if (!typeValue.isEmpty()) { QJsonObject responseObject = responseObjectForType(typeValue); connection->respond(HTTPConnection::StatusCode200, QJsonDocument(responseObject).toJson(), "application/json"); return true; } else { return false; } } return false; } bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection *connection, const QUrl &url) { if (connection->requestOperation() == QNetworkAccessManager::PostOperation && url.path() == SETTINGS_PATH) { // this is a POST operation to change one or more settings QJsonDocument postedDocument = QJsonDocument::fromJson(connection->requestContent()); QJsonObject postedObject = postedDocument.object(); qDebug() << "DomainServerSettingsManager postedObject -" << postedObject; // we recurse one level deep below each group for the appropriate setting recurseJSONObjectAndOverwriteSettings(postedObject, _configMap.getUserConfig()); // store whatever the current _settingsMap is to file persistToFile(); // return success to the caller QString jsonSuccess = "{\"status\": \"success\"}"; connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json"); // defer a restart to the domain-server, this gives our HTTPConnection enough time to respond const int DOMAIN_SERVER_RESTART_TIMER_MSECS = 1000; QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart())); return true; } else if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == SETTINGS_PATH) { // setup a JSON Object with descriptions and non-omitted settings const QString SETTINGS_RESPONSE_DESCRIPTION_KEY = "descriptions"; const QString SETTINGS_RESPONSE_VALUE_KEY = "values"; const QString SETTINGS_RESPONSE_LOCKED_VALUES_KEY = "locked"; QJsonObject rootObject; rootObject[SETTINGS_RESPONSE_DESCRIPTION_KEY] = _descriptionArray; rootObject[SETTINGS_RESPONSE_VALUE_KEY] = responseObjectForType("", true); rootObject[SETTINGS_RESPONSE_LOCKED_VALUES_KEY] = QJsonDocument::fromVariant(_configMap.getMasterConfig()).object(); connection->respond(HTTPConnection::StatusCode200, QJsonDocument(rootObject).toJson(), "application/json"); } return false; } QJsonObject DomainServerSettingsManager::responseObjectForType(const QString& typeValue, bool isAuthenticated) { QJsonObject responseObject; if (!typeValue.isEmpty() || isAuthenticated) { // convert the string type value to a QJsonValue QJsonValue queryType = typeValue.isEmpty() ? QJsonValue() : QJsonValue(typeValue.toInt()); const QString AFFECTED_TYPES_JSON_KEY = "assignment-types"; // enumerate the groups in the description object to find which settings to pass foreach(const QJsonValue& groupValue, _descriptionArray) { QJsonObject groupObject = groupValue.toObject(); QString groupKey = groupObject[DESCRIPTION_NAME_KEY].toString(); QJsonArray groupSettingsArray = groupObject[DESCRIPTION_SETTINGS_KEY].toArray(); QJsonObject groupResponseObject; foreach(const QJsonValue& settingValue, groupSettingsArray) { const QString VALUE_HIDDEN_FLAG_KEY = "value-hidden"; QJsonObject settingObject = settingValue.toObject(); if (!settingObject[VALUE_HIDDEN_FLAG_KEY].toBool()) { QJsonArray affectedTypesArray = settingObject[AFFECTED_TYPES_JSON_KEY].toArray(); if (affectedTypesArray.isEmpty()) { affectedTypesArray = groupObject[AFFECTED_TYPES_JSON_KEY].toArray(); } if (affectedTypesArray.contains(queryType) || (queryType.isNull() && isAuthenticated)) { // this is a setting we should include in the responseObject QString settingName = settingObject[DESCRIPTION_NAME_KEY].toString(); // we need to check if the settings map has a value for this setting QVariant variantValue; if (!groupKey.isEmpty()) { QVariant settingsMapGroupValue = _configMap.getMergedConfig().value(groupKey); if (!settingsMapGroupValue.isNull()) { variantValue = settingsMapGroupValue.toMap().value(settingName); } } else { variantValue = _configMap.getMergedConfig().value(settingName); } QJsonValue result; if (variantValue.isNull()) { // no value for this setting, pass the default if (settingObject.contains(SETTING_DEFAULT_KEY)) { result = settingObject[SETTING_DEFAULT_KEY]; } else { // users are allowed not to provide a default for string values // if so we set to the empty string result = QString(""); } } else { result = QJsonValue::fromVariant(variantValue); } if (!groupKey.isEmpty()) { // this belongs in the group object groupResponseObject[settingName] = result; } else { // this is a value that should be at the root responseObject[settingName] = result; } } } } if (!groupKey.isEmpty() && !groupResponseObject.isEmpty()) { // set this group's object to the constructed object responseObject[groupKey] = groupResponseObject; } } } return responseObject; } void DomainServerSettingsManager::updateSetting(const QString& key, const QJsonValue& newValue, QVariantMap& settingMap, const QJsonObject& settingDescription) { if (newValue.isString()) { if (newValue.toString().isEmpty()) { // this is an empty value, clear it in settings variant so the default is sent settingMap.remove(key); } else { // make sure the resulting json value has the right type QString settingType = settingDescription[SETTING_DESCRIPTION_TYPE_KEY].toString(); const QString INPUT_DOUBLE_TYPE = "double"; const QString INPUT_INTEGER_TYPE = "int"; if (settingType == INPUT_DOUBLE_TYPE) { settingMap[key] = newValue.toString().toDouble(); } else if (settingType == INPUT_INTEGER_TYPE) { settingMap[key] = newValue.toString().toInt(); } else { QString sanitizedValue = newValue.toString(); // we perform special handling for viewpoints here // we do not want them to be prepended with a slash if (key == SETTINGS_VIEWPOINT_KEY && !sanitizedValue.startsWith('/')) { sanitizedValue.prepend('/'); } qDebug() << "Setting" << key << "in" << settingMap << "to" << sanitizedValue; settingMap[key] = sanitizedValue; } } } else if (newValue.isBool()) { settingMap[key] = newValue.toBool(); } else if (newValue.isObject()) { if (!settingMap.contains(key)) { // we don't have a map below this key yet, so set it up now settingMap[key] = QVariantMap(); } QVariant& possibleMap = settingMap[key]; if (!possibleMap.canConvert(QMetaType::QVariantMap)) { // if this isn't a map then we need to make it one, otherwise we're about to crash qDebug() << "Value at" << key << "was not the expected QVariantMap while updating DS settings" << "- removing existing value and making it a QVariantMap"; possibleMap = QVariantMap(); } QVariantMap& thisMap = *reinterpret_cast(possibleMap.data()); foreach(const QString childKey, newValue.toObject().keys()) { QJsonObject childDescriptionObject = settingDescription; // is this the key? if so we have the description already if (key != settingDescription[DESCRIPTION_NAME_KEY].toString()) { // otherwise find the description object for this childKey under columns foreach(const QJsonValue& column, settingDescription[DESCRIPTION_COLUMNS_KEY].toArray()) { if (column.isObject()) { QJsonObject thisDescription = column.toObject(); if (thisDescription[DESCRIPTION_NAME_KEY] == childKey) { childDescriptionObject = column.toObject(); break; } } } } QString sanitizedKey = childKey; if (key == SETTINGS_PATHS_KEY && !sanitizedKey.startsWith('/')) { // We perform special handling for paths here. // If we got sent a path without a leading slash then we add it. sanitizedKey.prepend("/"); } updateSetting(sanitizedKey, newValue.toObject()[childKey], thisMap, childDescriptionObject); } } if (settingMap[key].toMap().isEmpty()) { // we've cleared all of the settings below this value, so remove this one too settingMap.remove(key); } } QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJsonObject& groupObject, const QString& settingName) { foreach(const QJsonValue& settingValue, groupObject[DESCRIPTION_SETTINGS_KEY].toArray()) { QJsonObject settingObject = settingValue.toObject(); if (settingObject[DESCRIPTION_NAME_KEY].toString() == settingName) { return settingObject; } } return QJsonObject(); } void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject, QVariantMap& settingsVariant) { // Iterate on the setting groups foreach(const QString& rootKey, postedObject.keys()) { QJsonValue rootValue = postedObject[rootKey]; if (!settingsVariant.contains(rootKey)) { // we don't have a map below this key yet, so set it up now settingsVariant[rootKey] = QVariantMap(); } QVariantMap* thisMap = &settingsVariant; QJsonObject groupDescriptionObject; // we need to check the description array to see if this is a root setting or a group setting foreach(const QJsonValue& groupValue, _descriptionArray) { if (groupValue.toObject()[DESCRIPTION_NAME_KEY] == rootKey) { // we matched a group - keep this since we'll use it below to update the settings groupDescriptionObject = groupValue.toObject(); // change the map we will update to be the map for this group thisMap = reinterpret_cast(settingsVariant[rootKey].data()); break; } } if (groupDescriptionObject.isEmpty()) { // this is a root value, so we can call updateSetting for it directly // first we need to find our description value for it QJsonObject matchingDescriptionObject; foreach(const QJsonValue& groupValue, _descriptionArray) { // find groups with root values (they don't have a group name) QJsonObject groupObject = groupValue.toObject(); if (!groupObject.contains(DESCRIPTION_NAME_KEY)) { // this is a group with root values - check if our setting is in here matchingDescriptionObject = settingDescriptionFromGroup(groupObject, rootKey); if (!matchingDescriptionObject.isEmpty()) { break; } } } if (!matchingDescriptionObject.isEmpty()) { updateSetting(rootKey, rootValue, *thisMap, matchingDescriptionObject); } else { qDebug() << "Setting for root key" << rootKey << "does not exist - cannot update setting."; } } else { // this is a group - iterate on the settings in the group foreach(const QString& settingKey, rootValue.toObject().keys()) { // make sure this particular setting exists and we have a description object for it QJsonObject matchingDescriptionObject = settingDescriptionFromGroup(groupDescriptionObject, settingKey); // if we matched the setting then update the value if (!matchingDescriptionObject.isEmpty()) { QJsonValue settingValue = rootValue.toObject()[settingKey]; updateSetting(settingKey, settingValue, *thisMap, matchingDescriptionObject); } else { qDebug() << "Could not find description for setting" << settingKey << "in group" << rootKey << "- cannot update setting."; } } } if (settingsVariant[rootKey].toMap().empty()) { // we've cleared all of the settings below this value, so remove this one too settingsVariant.remove(rootKey); } } } void DomainServerSettingsManager::persistToFile() { // make sure we have the dir the settings file is supposed to live in QFileInfo settingsFileInfo(_configMap.getUserConfigFilename()); if (!settingsFileInfo.dir().exists()) { settingsFileInfo.dir().mkpath("."); } QFile settingsFile(_configMap.getUserConfigFilename()); if (settingsFile.open(QIODevice::WriteOnly)) { settingsFile.write(QJsonDocument::fromVariant(_configMap.getUserConfig()).toJson()); } else { qCritical("Could not write to JSON settings file. Unable to persist settings."); } }