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
|