LCOV - code coverage report
Current view: top level - src/plugin - pluginpreferencesutils.cpp (source / functions) Coverage Total Hit
Test: jami-coverage-filtered.info Lines: 71.4 % 245 175
Test Date: 2026-07-06 08:25:38 Functions: 40.0 % 30 12

            Line data    Source code
       1              : /*
       2              :  *  Copyright (C) 2004-2026 Savoir-faire Linux Inc.
       3              :  *
       4              :  *  This program is free software: you can redistribute it and/or modify
       5              :  *  it under the terms of the GNU General Public License as published by
       6              :  *  the Free Software Foundation, either version 3 of the License, or
       7              :  *  (at your option) any later version.
       8              :  *
       9              :  *  This program is distributed in the hope that it will be useful,
      10              :  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
      11              :  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
      12              :  *  GNU General Public License for more details.
      13              :  *
      14              :  *  You should have received a copy of the GNU General Public License
      15              :  *  along with this program. If not, see <https://www.gnu.org/licenses/>.
      16              :  */
      17              : 
      18              : #include "pluginpreferencesutils.h"
      19              : #include "pluginsutils.h"
      20              : 
      21              : #include <msgpack.hpp>
      22              : #include <fstream>
      23              : #include <fmt/core.h>
      24              : 
      25              : #include "logger.h"
      26              : #include "fileutils.h"
      27              : #include "string_utils.h"
      28              : 
      29              : namespace jami {
      30              : 
      31              : std::filesystem::path
      32          161 : PluginPreferencesUtils::getPreferencesConfigFilePath(const std::filesystem::path& rootPath, const std::string& accountId)
      33              : {
      34          161 :     if (accountId.empty())
      35          176 :         return rootPath / "data" / "preferences.json";
      36              :     else
      37          146 :         return rootPath / "data" / "accountpreferences.json";
      38              : }
      39              : 
      40              : std::filesystem::path
      41           72 : PluginPreferencesUtils::valuesFilePath(const std::filesystem::path& rootPath, const std::string& accountId)
      42              : {
      43           72 :     if (accountId.empty() || accountId == "default")
      44           40 :         return rootPath / "preferences.msgpack";
      45           32 :     auto pluginName = rootPath.filename();
      46           32 :     auto dir = fileutils::get_data_dir() / accountId / "plugins" / pluginName;
      47           32 :     dhtnet::fileutils::check_dir(dir);
      48           32 :     return dir / "preferences.msgpack";
      49           32 : }
      50              : 
      51              : std::filesystem::path
      52           35 : PluginPreferencesUtils::getAllowDenyListsPath()
      53              : {
      54           70 :     return fileutils::get_data_dir() / "plugins" / "allowdeny.msgpack";
      55              : }
      56              : 
      57              : std::string
      58            0 : PluginPreferencesUtils::convertArrayToString(const Json::Value& jsonArray)
      59              : {
      60            0 :     std::string stringArray {};
      61              : 
      62            0 :     if (jsonArray.size()) {
      63            0 :         for (unsigned i = 0; i < jsonArray.size() - 1; i++) {
      64            0 :             if (jsonArray[i].isString()) {
      65            0 :                 stringArray += jsonArray[i].asString() + ",";
      66            0 :             } else if (jsonArray[i].isArray()) {
      67            0 :                 stringArray += convertArrayToString(jsonArray[i]) + ",";
      68              :             }
      69              :         }
      70              : 
      71            0 :         unsigned lastIndex = jsonArray.size() - 1;
      72            0 :         if (jsonArray[lastIndex].isString()) {
      73            0 :             stringArray += jsonArray[lastIndex].asString();
      74              :         }
      75              :     }
      76              : 
      77            0 :     return stringArray;
      78            0 : }
      79              : 
      80              : std::map<std::string, std::string>
      81          260 : PluginPreferencesUtils::parsePreferenceConfig(const Json::Value& jsonPreference)
      82              : {
      83          260 :     std::map<std::string, std::string> preferenceMap;
      84          260 :     const auto& members = jsonPreference.getMemberNames();
      85              :     // Insert other fields
      86         1704 :     for (const auto& member : members) {
      87         1444 :         const Json::Value& value = jsonPreference[member];
      88         1447 :         if (value.isString()) {
      89         1447 :             preferenceMap.emplace(member, jsonPreference[member].asString());
      90            0 :         } else if (value.isArray()) {
      91            0 :             preferenceMap.emplace(member, convertArrayToString(jsonPreference[member]));
      92              :         }
      93              :     }
      94          520 :     return preferenceMap;
      95          260 : }
      96              : 
      97              : std::vector<std::map<std::string, std::string>>
      98          113 : PluginPreferencesUtils::getPreferences(const std::filesystem::path& rootPath, const std::string& accountId)
      99              : {
     100          113 :     auto preferenceFilePath = getPreferencesConfigFilePath(rootPath, accountId);
     101          113 :     std::lock_guard guard(dhtnet::fileutils::getFileLock(preferenceFilePath));
     102          113 :     std::ifstream file(preferenceFilePath);
     103          113 :     Json::Value root;
     104          113 :     Json::CharReaderBuilder rbuilder;
     105          339 :     rbuilder["collectComments"] = false;
     106          113 :     std::string errs;
     107          113 :     std::set<std::string> keys;
     108          113 :     std::vector<std::map<std::string, std::string>> preferences;
     109          113 :     if (file) {
     110              :         // Get preferences locale
     111          113 :         const auto& lang = PluginUtils::getLanguage();
     112          113 :         auto locales = PluginUtils::getLocales(rootPath.string(), std::string(string_remove_suffix(lang, '.')));
     113              : 
     114              :         // Read the file to a json format
     115          113 :         bool ok = Json::parseFromStream(rbuilder, file, &root, &errs);
     116          113 :         if (ok && root.isArray()) {
     117              :             // Read each preference described in preference.json individually
     118          372 :             for (unsigned i = 0; i < root.size(); i++) {
     119          260 :                 const Json::Value& jsonPreference = root[i];
     120          260 :                 std::string type = jsonPreference.get("type", "None").asString();
     121          260 :                 std::string key = jsonPreference.get("key", "None").asString();
     122              :                 // The preference must have at least type and key
     123          260 :                 if (type != "None" && key != "None") {
     124          259 :                     if (keys.find(key) == keys.end()) {
     125              :                         // Read the rest of the preference
     126          260 :                         auto preferenceAttributes = parsePreferenceConfig(jsonPreference);
     127              :                         // If the parsing of the attributes was successful, commit the map and the keys
     128          260 :                         auto defaultValue = preferenceAttributes.find("defaultValue");
     129          260 :                         if (type == "Path" && defaultValue != preferenceAttributes.end()) {
     130              :                             // defaultValue in a Path preference is an incomplete path
     131              :                             // starting from the installation path of the plugin.
     132              :                             // Here we complete the path value.
     133            0 :                             defaultValue->second = (rootPath / defaultValue->second).string();
     134              :                         }
     135              : 
     136          260 :                         if (!preferenceAttributes.empty()) {
     137         2047 :                             for (const auto& locale : locales) {
     138        11875 :                                 for (auto& pair : preferenceAttributes) {
     139        10081 :                                     string_replace(pair.second, "{{" + locale.first + "}}", locale.second);
     140              :                                 }
     141              :                             }
     142          258 :                             preferences.push_back(std::move(preferenceAttributes));
     143          260 :                             keys.insert(key);
     144              :                         }
     145          259 :                     }
     146              :                 }
     147          259 :             }
     148              :         } else {
     149            0 :             JAMI_ERROR("PluginPreferencesParser:: Failed to parse preferences.json for plugin: {}", preferenceFilePath);
     150              :         }
     151          112 :     }
     152              : 
     153          226 :     return preferences;
     154          113 : }
     155              : 
     156              : std::map<std::string, std::string>
     157           67 : PluginPreferencesUtils::getUserPreferencesValuesMap(const std::filesystem::path& rootPath, const std::string& accountId)
     158              : {
     159           67 :     auto preferencesValuesFilePath = valuesFilePath(rootPath, accountId);
     160           67 :     std::lock_guard guard(dhtnet::fileutils::getFileLock(preferencesValuesFilePath));
     161           67 :     std::ifstream file(preferencesValuesFilePath, std::ios::binary);
     162           67 :     std::map<std::string, std::string> rmap;
     163              : 
     164              :     // If file is accessible
     165           67 :     if (file.good()) {
     166              :         // Get file size
     167           15 :         std::string str;
     168           15 :         file.seekg(0, std::ios::end);
     169           15 :         size_t fileSize = static_cast<size_t>(file.tellg());
     170              :         // If not empty
     171           15 :         if (fileSize > 0) {
     172              :             // Read whole file content and put it in the string str
     173           15 :             str.reserve(static_cast<size_t>(file.tellg()));
     174           15 :             file.seekg(0, std::ios::beg);
     175           15 :             str.assign((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
     176           15 :             file.close();
     177              :             try {
     178              :                 // Unpack the string
     179           15 :                 msgpack::object_handle oh = msgpack::unpack(str.data(), str.size());
     180              :                 // Deserialized object is valid during the msgpack::object_handle instance is alive.
     181           15 :                 msgpack::object deserialized = oh.get();
     182           15 :                 deserialized.convert(rmap);
     183           15 :             } catch (const std::exception& e) {
     184            0 :                 JAMI_ERROR("{}", e.what());
     185            0 :             }
     186              :         }
     187           15 :     }
     188          134 :     return rmap;
     189           67 : }
     190              : 
     191              : std::map<std::string, std::string>
     192           36 : PluginPreferencesUtils::getPreferencesValuesMap(const std::filesystem::path& rootPath, const std::string& accountId)
     193              : {
     194           36 :     std::map<std::string, std::string> rmap;
     195              : 
     196              :     // Read all preferences values
     197           36 :     std::vector<std::map<std::string, std::string>> preferences = getPreferences(rootPath);
     198           36 :     auto accPrefs = getPreferences(rootPath, accountId);
     199          159 :     for (const auto& item : accPrefs) {
     200          123 :         preferences.push_back(item);
     201              :     }
     202          195 :     for (auto& preference : preferences) {
     203          636 :         rmap[preference["key"]] = preference["defaultValue"];
     204              :     }
     205              : 
     206              :     // If any of these preferences were modified, its value is changed before return
     207           74 :     for (const auto& pair : getUserPreferencesValuesMap(rootPath)) {
     208            2 :         rmap[pair.first] = pair.second;
     209           36 :     }
     210              : 
     211           36 :     if (!accountId.empty()) {
     212              :         // If any of these preferences were modified, its value is changed before return
     213           31 :         for (const auto& pair : getUserPreferencesValuesMap(rootPath, accountId)) {
     214            2 :             rmap[pair.first] = pair.second;
     215           29 :         }
     216              :     }
     217              : 
     218           72 :     return rmap;
     219           36 : }
     220              : 
     221              : bool
     222            0 : PluginPreferencesUtils::setUserPreferenceValue(const std::filesystem::path& rootPath,
     223              :                                                const std::string& accountId,
     224              :                                                const std::string& key,
     225              :                                                const std::string& value)
     226              : {
     227            0 :     auto preferencesValuesFilePath = valuesFilePath(rootPath, accountId);
     228            0 :     std::lock_guard const guard(dhtnet::fileutils::getFileLock(preferencesValuesFilePath));
     229            0 :     std::map<std::string, std::string> rmap;
     230            0 :     if (std::ifstream rfs(preferencesValuesFilePath, std::ios::binary); rfs.good()) {
     231            0 :         std::string str;
     232            0 :         rfs.seekg(0, std::ios::end);
     233            0 :         if (const auto fileSize = static_cast<size_t>(rfs.tellg()); fileSize > 0) {
     234            0 :             str.reserve(fileSize);
     235            0 :             rfs.seekg(0, std::ios::beg);
     236            0 :             str.assign(std::istreambuf_iterator<char>(rfs), std::istreambuf_iterator<char>());
     237              :             try {
     238            0 :                 msgpack::unpack(str.data(), str.size()).get().convert(rmap);
     239            0 :             } catch (const std::exception& e) {
     240            0 :                 JAMI_ERROR("{}", e.what());
     241            0 :             }
     242              :         }
     243            0 :     }
     244            0 :     rmap[key] = value;
     245            0 :     if (std::ofstream wfs(preferencesValuesFilePath, std::ios::binary); wfs.good()) {
     246            0 :         msgpack::pack(wfs, rmap);
     247            0 :         JAMI_DEBUG("setUserPreferenceValue: wrote {}={} for account={} path={}",
     248              :                    key,
     249              :                    value,
     250              :                    accountId,
     251              :                    preferencesValuesFilePath.string());
     252            0 :         return true;
     253            0 :     }
     254            0 :     JAMI_WARNING("setUserPreferenceValue: failed to open {} for writing", preferencesValuesFilePath.string());
     255            0 :     return false;
     256            0 : }
     257              : 
     258              : bool
     259            3 : PluginPreferencesUtils::resetPreferencesValuesMap(const std::string& rootPath, const std::string& accountId)
     260              : {
     261            3 :     bool returnValue = true;
     262            3 :     std::map<std::string, std::string> pluginPreferencesMap {};
     263              : 
     264            3 :     auto preferencesValuesFilePath = valuesFilePath(rootPath, accountId);
     265            3 :     std::lock_guard guard(dhtnet::fileutils::getFileLock(preferencesValuesFilePath));
     266            3 :     std::ofstream fs(preferencesValuesFilePath, std::ios::binary);
     267            3 :     if (!fs.good()) {
     268            0 :         return false;
     269              :     }
     270              :     try {
     271            3 :         msgpack::pack(fs, pluginPreferencesMap);
     272            0 :     } catch (const std::exception& e) {
     273            0 :         returnValue = false;
     274            0 :         JAMI_ERROR("{}", e.what());
     275            0 :     }
     276              : 
     277            3 :     return returnValue;
     278            3 : }
     279              : 
     280              : void
     281            2 : PluginPreferencesUtils::setAllowDenyListPreferences(const ChatHandlerList& list)
     282              : {
     283            2 :     auto filePath = getAllowDenyListsPath();
     284            2 :     std::lock_guard guard(dhtnet::fileutils::getFileLock(filePath));
     285            2 :     std::ofstream fs(filePath, std::ios::binary);
     286            2 :     if (!fs.good()) {
     287            0 :         return;
     288              :     }
     289              :     try {
     290            2 :         msgpack::pack(fs, list);
     291            0 :     } catch (const std::exception& e) {
     292            0 :         JAMI_ERROR("{}", e.what());
     293            0 :     }
     294            2 : }
     295              : 
     296              : void
     297           33 : PluginPreferencesUtils::getAllowDenyListPreferences(ChatHandlerList& list)
     298              : {
     299           33 :     auto filePath = getAllowDenyListsPath();
     300           33 :     std::lock_guard guard(dhtnet::fileutils::getFileLock(filePath));
     301           33 :     std::ifstream file(filePath, std::ios::binary);
     302              : 
     303              :     // If file is accessible
     304           33 :     if (file.good()) {
     305              :         // Get file size
     306            0 :         std::string str;
     307            0 :         file.seekg(0, std::ios::end);
     308            0 :         size_t fileSize = static_cast<size_t>(file.tellg());
     309              :         // If not empty
     310            0 :         if (fileSize > 0) {
     311              :             // Read whole file content and put it in the string str
     312            0 :             str.reserve(static_cast<size_t>(file.tellg()));
     313            0 :             file.seekg(0, std::ios::beg);
     314            0 :             str.assign((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
     315            0 :             file.close();
     316              :             try {
     317              :                 // Unpack the string
     318            0 :                 msgpack::object_handle oh = msgpack::unpack(str.data(), str.size());
     319              :                 // Deserialized object is valid during the msgpack::object_handle instance is alive.
     320            0 :                 msgpack::object deserialized = oh.get();
     321            0 :                 deserialized.convert(list);
     322            0 :             } catch (const std::exception& e) {
     323            0 :                 JAMI_ERROR("{}", e.what());
     324            0 :             }
     325              :         }
     326            0 :     }
     327           33 : }
     328              : 
     329              : void
     330           24 : PluginPreferencesUtils::addAlwaysHandlerPreference(const std::string& handlerName, const std::string& rootPath)
     331              : {
     332              :     {
     333           24 :         auto filePath = getPreferencesConfigFilePath(rootPath);
     334           24 :         Json::Value root;
     335              : 
     336           24 :         std::lock_guard guard(dhtnet::fileutils::getFileLock(filePath));
     337           24 :         std::ifstream file(filePath);
     338           24 :         Json::CharReaderBuilder rbuilder;
     339           24 :         Json::Value preference;
     340           72 :         rbuilder["collectComments"] = false;
     341           24 :         std::string errs;
     342           24 :         if (file) {
     343           24 :             bool ok = Json::parseFromStream(rbuilder, file, &root, &errs);
     344           24 :             if (ok && root.isArray()) {
     345              :                 // Return if preference already exists
     346           48 :                 for (const auto& child : root)
     347           24 :                     if (child.get("key", "None").asString() == handlerName + "Always")
     348            0 :                         return;
     349              :             }
     350              :         }
     351           24 :     }
     352              : 
     353           24 :     auto filePath = getPreferencesConfigFilePath(rootPath, "acc");
     354           24 :     Json::Value root;
     355              :     {
     356           24 :         std::lock_guard guard(dhtnet::fileutils::getFileLock(filePath));
     357           24 :         std::ifstream file(filePath);
     358           24 :         Json::CharReaderBuilder rbuilder;
     359           24 :         Json::Value preference;
     360           72 :         rbuilder["collectComments"] = false;
     361           24 :         std::string errs;
     362           24 :         if (file) {
     363           24 :             bool ok = Json::parseFromStream(rbuilder, file, &root, &errs);
     364           24 :             if (ok && root.isArray()) {
     365              :                 // Return if preference already exists
     366           72 :                 for (const auto& child : root)
     367           51 :                     if (child.get("key", "None").asString() == handlerName + "Always")
     368            3 :                         return;
     369              :             }
     370              :         }
     371              :         // Create preference structure otherwise
     372           21 :         preference["key"] = handlerName + "Always";
     373           21 :         preference["type"] = "Switch";
     374           21 :         preference["defaultValue"] = "0";
     375           21 :         preference["title"] = "Automatically turn " + handlerName + " on";
     376           21 :         preference["summary"] = handlerName + " will take effect immediately";
     377           21 :         preference["scope"] = "accountId";
     378           21 :         root.append(preference);
     379           36 :     }
     380           21 :     std::lock_guard guard(dhtnet::fileutils::getFileLock(filePath));
     381           21 :     std::ofstream outFile(filePath);
     382           21 :     if (outFile) {
     383              :         // Save preference.json file with new "always preference"
     384           21 :         outFile << root.toStyledString();
     385           21 :         outFile.close();
     386              :     }
     387           27 : }
     388              : 
     389              : bool
     390           19 : PluginPreferencesUtils::getAlwaysPreference(const std::string& rootPath,
     391              :                                             const std::string& handlerName,
     392              :                                             const std::string& accountId)
     393              : {
     394           19 :     auto preferences = getPreferences(rootPath);
     395           19 :     auto accPrefs = getPreferences(rootPath, accountId);
     396           95 :     for (const auto& item : accPrefs) {
     397           76 :         preferences.push_back(item);
     398              :     }
     399           19 :     auto preferencesValues = getPreferencesValuesMap(rootPath, accountId);
     400              : 
     401          114 :     for (const auto& preference : preferences) {
     402           95 :         auto key = preference.at("key");
     403          437 :         if (preference.at("type") == "Switch" && key == handlerName + "Always"
     404          342 :             && preferencesValues.find(key)->second == "1")
     405            0 :             return true;
     406           95 :     }
     407              : 
     408           19 :     return false;
     409           19 : }
     410              : } // namespace jami
        

Generated by: LCOV version 2.0-1