Line data Source code
1 : /*
2 : * Copyright (C) 2004-2025 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 <sstream>
23 : #include <fstream>
24 : #include <fmt/core.h>
25 :
26 : #include "logger.h"
27 : #include "fileutils.h"
28 :
29 : namespace jami {
30 :
31 : std::filesystem::path
32 0 : PluginPreferencesUtils::getPreferencesConfigFilePath(const std::filesystem::path& rootPath, const std::string& accountId)
33 : {
34 0 : if (accountId.empty())
35 0 : return rootPath / "data" / "preferences.json";
36 : else
37 0 : return rootPath / "data" / "accountpreferences.json";
38 : }
39 :
40 : std::filesystem::path
41 0 : PluginPreferencesUtils::valuesFilePath(const std::filesystem::path& rootPath, const std::string& accountId)
42 : {
43 0 : if (accountId.empty() || accountId == "default")
44 0 : return rootPath / "preferences.msgpack";
45 0 : auto pluginName = rootPath.filename();
46 0 : auto dir = fileutils::get_data_dir() / accountId / "plugins" / pluginName;
47 0 : dhtnet::fileutils::check_dir(dir);
48 0 : return dir / "preferences.msgpack";
49 0 : }
50 :
51 : std::filesystem::path
52 32 : PluginPreferencesUtils::getAllowDenyListsPath()
53 : {
54 64 : 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 0 : PluginPreferencesUtils::parsePreferenceConfig(const Json::Value& jsonPreference)
82 : {
83 0 : std::map<std::string, std::string> preferenceMap;
84 0 : const auto& members = jsonPreference.getMemberNames();
85 : // Insert other fields
86 0 : for (const auto& member : members) {
87 0 : const Json::Value& value = jsonPreference[member];
88 0 : if (value.isString()) {
89 0 : preferenceMap.emplace(member, jsonPreference[member].asString());
90 0 : } else if (value.isArray()) {
91 0 : preferenceMap.emplace(member, convertArrayToString(jsonPreference[member]));
92 : }
93 : }
94 0 : return preferenceMap;
95 0 : }
96 :
97 : std::vector<std::map<std::string, std::string>>
98 0 : PluginPreferencesUtils::getPreferences(const std::filesystem::path& rootPath, const std::string& accountId)
99 : {
100 0 : auto preferenceFilePath = getPreferencesConfigFilePath(rootPath, accountId);
101 0 : std::lock_guard guard(dhtnet::fileutils::getFileLock(preferenceFilePath));
102 0 : std::ifstream file(preferenceFilePath);
103 0 : Json::Value root;
104 0 : Json::CharReaderBuilder rbuilder;
105 0 : rbuilder["collectComments"] = false;
106 0 : std::string errs;
107 0 : std::set<std::string> keys;
108 0 : std::vector<std::map<std::string, std::string>> preferences;
109 0 : if (file) {
110 : // Get preferences locale
111 0 : const auto& lang = PluginUtils::getLanguage();
112 0 : auto locales = PluginUtils::getLocales(rootPath.string(), std::string(string_remove_suffix(lang, '.')));
113 :
114 : // Read the file to a json format
115 0 : bool ok = Json::parseFromStream(rbuilder, file, &root, &errs);
116 0 : if (ok && root.isArray()) {
117 : // Read each preference described in preference.json individually
118 0 : for (unsigned i = 0; i < root.size(); i++) {
119 0 : const Json::Value& jsonPreference = root[i];
120 0 : std::string category = jsonPreference.get("category", "NoCategory").asString();
121 0 : std::string type = jsonPreference.get("type", "None").asString();
122 0 : std::string key = jsonPreference.get("key", "None").asString();
123 : // The preference must have at least type and key
124 0 : if (type != "None" && key != "None") {
125 0 : if (keys.find(key) == keys.end()) {
126 : // Read the rest of the preference
127 0 : auto preferenceAttributes = parsePreferenceConfig(jsonPreference);
128 : // If the parsing of the attributes was successful, commit the map and the keys
129 0 : auto defaultValue = preferenceAttributes.find("defaultValue");
130 0 : if (type == "Path" && defaultValue != preferenceAttributes.end()) {
131 : // defaultValue in a Path preference is an incomplete path
132 : // starting from the installation path of the plugin.
133 : // Here we complete the path value.
134 0 : defaultValue->second = (rootPath / defaultValue->second).string();
135 : }
136 :
137 0 : if (!preferenceAttributes.empty()) {
138 0 : for (const auto& locale : locales) {
139 0 : for (auto& pair : preferenceAttributes) {
140 0 : string_replace(pair.second, "{{" + locale.first + "}}", locale.second);
141 : }
142 : }
143 0 : preferences.push_back(std::move(preferenceAttributes));
144 0 : keys.insert(key);
145 : }
146 0 : }
147 : }
148 0 : }
149 : } else {
150 0 : JAMI_ERR() << "PluginPreferencesParser:: Failed to parse preferences.json for plugin: "
151 0 : << preferenceFilePath;
152 : }
153 0 : }
154 :
155 0 : return preferences;
156 0 : }
157 :
158 : std::map<std::string, std::string>
159 0 : PluginPreferencesUtils::getUserPreferencesValuesMap(const std::filesystem::path& rootPath, const std::string& accountId)
160 : {
161 0 : auto preferencesValuesFilePath = valuesFilePath(rootPath, accountId);
162 0 : std::lock_guard guard(dhtnet::fileutils::getFileLock(preferencesValuesFilePath));
163 0 : std::ifstream file(preferencesValuesFilePath, std::ios::binary);
164 0 : std::map<std::string, std::string> rmap;
165 :
166 : // If file is accessible
167 0 : if (file.good()) {
168 : // Get file size
169 0 : std::string str;
170 0 : file.seekg(0, std::ios::end);
171 0 : size_t fileSize = static_cast<size_t>(file.tellg());
172 : // If not empty
173 0 : if (fileSize > 0) {
174 : // Read whole file content and put it in the string str
175 0 : str.reserve(static_cast<size_t>(file.tellg()));
176 0 : file.seekg(0, std::ios::beg);
177 0 : str.assign((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
178 0 : file.close();
179 : try {
180 : // Unpack the string
181 0 : msgpack::object_handle oh = msgpack::unpack(str.data(), str.size());
182 : // Deserialized object is valid during the msgpack::object_handle instance is alive.
183 0 : msgpack::object deserialized = oh.get();
184 0 : deserialized.convert(rmap);
185 0 : } catch (const std::exception& e) {
186 0 : JAMI_ERR() << e.what();
187 0 : }
188 : }
189 0 : }
190 0 : return rmap;
191 0 : }
192 :
193 : std::map<std::string, std::string>
194 0 : PluginPreferencesUtils::getPreferencesValuesMap(const std::filesystem::path& rootPath, const std::string& accountId)
195 : {
196 0 : std::map<std::string, std::string> rmap;
197 :
198 : // Read all preferences values
199 0 : std::vector<std::map<std::string, std::string>> preferences = getPreferences(rootPath);
200 0 : auto accPrefs = getPreferences(rootPath, accountId);
201 0 : for (const auto& item : accPrefs) {
202 0 : preferences.push_back(item);
203 : }
204 0 : for (auto& preference : preferences) {
205 0 : rmap[preference["key"]] = preference["defaultValue"];
206 : }
207 :
208 : // If any of these preferences were modified, its value is changed before return
209 0 : for (const auto& pair : getUserPreferencesValuesMap(rootPath)) {
210 0 : rmap[pair.first] = pair.second;
211 0 : }
212 :
213 0 : if (!accountId.empty()) {
214 : // If any of these preferences were modified, its value is changed before return
215 0 : for (const auto& pair : getUserPreferencesValuesMap(rootPath, accountId)) {
216 0 : rmap[pair.first] = pair.second;
217 0 : }
218 : }
219 :
220 0 : return rmap;
221 0 : }
222 :
223 : bool
224 0 : PluginPreferencesUtils::resetPreferencesValuesMap(const std::string& rootPath, const std::string& accountId)
225 : {
226 0 : bool returnValue = true;
227 0 : std::map<std::string, std::string> pluginPreferencesMap {};
228 :
229 0 : auto preferencesValuesFilePath = valuesFilePath(rootPath, accountId);
230 0 : std::lock_guard guard(dhtnet::fileutils::getFileLock(preferencesValuesFilePath));
231 0 : std::ofstream fs(preferencesValuesFilePath, std::ios::binary);
232 0 : if (!fs.good()) {
233 0 : return false;
234 : }
235 : try {
236 0 : msgpack::pack(fs, pluginPreferencesMap);
237 0 : } catch (const std::exception& e) {
238 0 : returnValue = false;
239 0 : JAMI_ERR() << e.what();
240 0 : }
241 :
242 0 : return returnValue;
243 0 : }
244 :
245 : void
246 0 : PluginPreferencesUtils::setAllowDenyListPreferences(const ChatHandlerList& list)
247 : {
248 0 : auto filePath = getAllowDenyListsPath();
249 0 : std::lock_guard guard(dhtnet::fileutils::getFileLock(filePath));
250 0 : std::ofstream fs(filePath, std::ios::binary);
251 0 : if (!fs.good()) {
252 0 : return;
253 : }
254 : try {
255 0 : msgpack::pack(fs, list);
256 0 : } catch (const std::exception& e) {
257 0 : JAMI_ERR() << e.what();
258 0 : }
259 0 : }
260 :
261 : void
262 32 : PluginPreferencesUtils::getAllowDenyListPreferences(ChatHandlerList& list)
263 : {
264 32 : auto filePath = getAllowDenyListsPath();
265 32 : std::lock_guard guard(dhtnet::fileutils::getFileLock(filePath));
266 32 : std::ifstream file(filePath, std::ios::binary);
267 :
268 : // If file is accessible
269 32 : if (file.good()) {
270 : // Get file size
271 0 : std::string str;
272 0 : file.seekg(0, std::ios::end);
273 0 : size_t fileSize = static_cast<size_t>(file.tellg());
274 : // If not empty
275 0 : if (fileSize > 0) {
276 : // Read whole file content and put it in the string str
277 0 : str.reserve(static_cast<size_t>(file.tellg()));
278 0 : file.seekg(0, std::ios::beg);
279 0 : str.assign((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
280 0 : file.close();
281 : try {
282 : // Unpack the string
283 0 : msgpack::object_handle oh = msgpack::unpack(str.data(), str.size());
284 : // Deserialized object is valid during the msgpack::object_handle instance is alive.
285 0 : msgpack::object deserialized = oh.get();
286 0 : deserialized.convert(list);
287 0 : } catch (const std::exception& e) {
288 0 : JAMI_ERR() << e.what();
289 0 : }
290 : }
291 0 : }
292 32 : }
293 :
294 : void
295 0 : PluginPreferencesUtils::addAlwaysHandlerPreference(const std::string& handlerName, const std::string& rootPath)
296 : {
297 : {
298 0 : auto filePath = getPreferencesConfigFilePath(rootPath);
299 0 : Json::Value root;
300 :
301 0 : std::lock_guard guard(dhtnet::fileutils::getFileLock(filePath));
302 0 : std::ifstream file(filePath);
303 0 : Json::CharReaderBuilder rbuilder;
304 0 : Json::Value preference;
305 0 : rbuilder["collectComments"] = false;
306 0 : std::string errs;
307 0 : if (file) {
308 0 : bool ok = Json::parseFromStream(rbuilder, file, &root, &errs);
309 0 : if (ok && root.isArray()) {
310 : // Return if preference already exists
311 0 : for (const auto& child : root)
312 0 : if (child.get("key", "None").asString() == handlerName + "Always")
313 0 : return;
314 : }
315 : }
316 0 : }
317 :
318 0 : auto filePath = getPreferencesConfigFilePath(rootPath, "acc");
319 0 : Json::Value root;
320 : {
321 0 : std::lock_guard guard(dhtnet::fileutils::getFileLock(filePath));
322 0 : std::ifstream file(filePath);
323 0 : Json::CharReaderBuilder rbuilder;
324 0 : Json::Value preference;
325 0 : rbuilder["collectComments"] = false;
326 0 : std::string errs;
327 0 : if (file) {
328 0 : bool ok = Json::parseFromStream(rbuilder, file, &root, &errs);
329 0 : if (ok && root.isArray()) {
330 : // Return if preference already exists
331 0 : for (const auto& child : root)
332 0 : if (child.get("key", "None").asString() == handlerName + "Always")
333 0 : return;
334 : }
335 : }
336 : // Create preference structure otherwise
337 0 : preference["key"] = handlerName + "Always";
338 0 : preference["type"] = "Switch";
339 0 : preference["defaultValue"] = "0";
340 0 : preference["title"] = "Automatically turn " + handlerName + " on";
341 0 : preference["summary"] = handlerName + " will take effect immediately";
342 0 : preference["scope"] = "accountId";
343 0 : root.append(preference);
344 0 : }
345 0 : std::lock_guard guard(dhtnet::fileutils::getFileLock(filePath));
346 0 : std::ofstream outFile(filePath);
347 0 : if (outFile) {
348 : // Save preference.json file with new "always preference"
349 0 : outFile << root.toStyledString();
350 0 : outFile.close();
351 : }
352 0 : }
353 :
354 : bool
355 0 : PluginPreferencesUtils::getAlwaysPreference(const std::string& rootPath,
356 : const std::string& handlerName,
357 : const std::string& accountId)
358 : {
359 0 : auto preferences = getPreferences(rootPath);
360 0 : auto accPrefs = getPreferences(rootPath, accountId);
361 0 : for (const auto& item : accPrefs) {
362 0 : preferences.push_back(item);
363 : }
364 0 : auto preferencesValues = getPreferencesValuesMap(rootPath, accountId);
365 :
366 0 : for (const auto& preference : preferences) {
367 0 : auto key = preference.at("key");
368 0 : if (preference.at("type") == "Switch" && key == handlerName + "Always"
369 0 : && preferencesValues.find(key)->second == "1")
370 0 : return true;
371 0 : }
372 :
373 0 : return false;
374 0 : }
375 : } // namespace jami
|