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 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 type = jsonPreference.get("type", "None").asString();
121 0 : std::string key = jsonPreference.get("key", "None").asString();
122 : // The preference must have at least type and key
123 0 : if (type != "None" && key != "None") {
124 0 : if (keys.find(key) == keys.end()) {
125 : // Read the rest of the preference
126 0 : auto preferenceAttributes = parsePreferenceConfig(jsonPreference);
127 : // If the parsing of the attributes was successful, commit the map and the keys
128 0 : auto defaultValue = preferenceAttributes.find("defaultValue");
129 0 : 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 0 : if (!preferenceAttributes.empty()) {
137 0 : for (const auto& locale : locales) {
138 0 : for (auto& pair : preferenceAttributes) {
139 0 : string_replace(pair.second, "{{" + locale.first + "}}", locale.second);
140 : }
141 : }
142 0 : preferences.push_back(std::move(preferenceAttributes));
143 0 : keys.insert(key);
144 : }
145 0 : }
146 : }
147 0 : }
148 : } else {
149 0 : JAMI_ERR() << "PluginPreferencesParser:: Failed to parse preferences.json for plugin: "
150 0 : << preferenceFilePath;
151 : }
152 0 : }
153 :
154 0 : return preferences;
155 0 : }
156 :
157 : std::map<std::string, std::string>
158 0 : PluginPreferencesUtils::getUserPreferencesValuesMap(const std::filesystem::path& rootPath, const std::string& accountId)
159 : {
160 0 : auto preferencesValuesFilePath = valuesFilePath(rootPath, accountId);
161 0 : std::lock_guard guard(dhtnet::fileutils::getFileLock(preferencesValuesFilePath));
162 0 : std::ifstream file(preferencesValuesFilePath, std::ios::binary);
163 0 : std::map<std::string, std::string> rmap;
164 :
165 : // If file is accessible
166 0 : if (file.good()) {
167 : // Get file size
168 0 : std::string str;
169 0 : file.seekg(0, std::ios::end);
170 0 : size_t fileSize = static_cast<size_t>(file.tellg());
171 : // If not empty
172 0 : if (fileSize > 0) {
173 : // Read whole file content and put it in the string str
174 0 : str.reserve(static_cast<size_t>(file.tellg()));
175 0 : file.seekg(0, std::ios::beg);
176 0 : str.assign((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
177 0 : file.close();
178 : try {
179 : // Unpack the string
180 0 : msgpack::object_handle oh = msgpack::unpack(str.data(), str.size());
181 : // Deserialized object is valid during the msgpack::object_handle instance is alive.
182 0 : msgpack::object deserialized = oh.get();
183 0 : deserialized.convert(rmap);
184 0 : } catch (const std::exception& e) {
185 0 : JAMI_ERR() << e.what();
186 0 : }
187 : }
188 0 : }
189 0 : return rmap;
190 0 : }
191 :
192 : std::map<std::string, std::string>
193 0 : PluginPreferencesUtils::getPreferencesValuesMap(const std::filesystem::path& rootPath, const std::string& accountId)
194 : {
195 0 : std::map<std::string, std::string> rmap;
196 :
197 : // Read all preferences values
198 0 : std::vector<std::map<std::string, std::string>> preferences = getPreferences(rootPath);
199 0 : auto accPrefs = getPreferences(rootPath, accountId);
200 0 : for (const auto& item : accPrefs) {
201 0 : preferences.push_back(item);
202 : }
203 0 : for (auto& preference : preferences) {
204 0 : rmap[preference["key"]] = preference["defaultValue"];
205 : }
206 :
207 : // If any of these preferences were modified, its value is changed before return
208 0 : for (const auto& pair : getUserPreferencesValuesMap(rootPath)) {
209 0 : rmap[pair.first] = pair.second;
210 0 : }
211 :
212 0 : if (!accountId.empty()) {
213 : // If any of these preferences were modified, its value is changed before return
214 0 : for (const auto& pair : getUserPreferencesValuesMap(rootPath, accountId)) {
215 0 : rmap[pair.first] = pair.second;
216 0 : }
217 : }
218 :
219 0 : return rmap;
220 0 : }
221 :
222 : bool
223 0 : PluginPreferencesUtils::resetPreferencesValuesMap(const std::string& rootPath, const std::string& accountId)
224 : {
225 0 : bool returnValue = true;
226 0 : std::map<std::string, std::string> pluginPreferencesMap {};
227 :
228 0 : auto preferencesValuesFilePath = valuesFilePath(rootPath, accountId);
229 0 : std::lock_guard guard(dhtnet::fileutils::getFileLock(preferencesValuesFilePath));
230 0 : std::ofstream fs(preferencesValuesFilePath, std::ios::binary);
231 0 : if (!fs.good()) {
232 0 : return false;
233 : }
234 : try {
235 0 : msgpack::pack(fs, pluginPreferencesMap);
236 0 : } catch (const std::exception& e) {
237 0 : returnValue = false;
238 0 : JAMI_ERR() << e.what();
239 0 : }
240 :
241 0 : return returnValue;
242 0 : }
243 :
244 : void
245 0 : PluginPreferencesUtils::setAllowDenyListPreferences(const ChatHandlerList& list)
246 : {
247 0 : auto filePath = getAllowDenyListsPath();
248 0 : std::lock_guard guard(dhtnet::fileutils::getFileLock(filePath));
249 0 : std::ofstream fs(filePath, std::ios::binary);
250 0 : if (!fs.good()) {
251 0 : return;
252 : }
253 : try {
254 0 : msgpack::pack(fs, list);
255 0 : } catch (const std::exception& e) {
256 0 : JAMI_ERR() << e.what();
257 0 : }
258 0 : }
259 :
260 : void
261 32 : PluginPreferencesUtils::getAllowDenyListPreferences(ChatHandlerList& list)
262 : {
263 32 : auto filePath = getAllowDenyListsPath();
264 32 : std::lock_guard guard(dhtnet::fileutils::getFileLock(filePath));
265 32 : std::ifstream file(filePath, std::ios::binary);
266 :
267 : // If file is accessible
268 32 : if (file.good()) {
269 : // Get file size
270 0 : std::string str;
271 0 : file.seekg(0, std::ios::end);
272 0 : size_t fileSize = static_cast<size_t>(file.tellg());
273 : // If not empty
274 0 : if (fileSize > 0) {
275 : // Read whole file content and put it in the string str
276 0 : str.reserve(static_cast<size_t>(file.tellg()));
277 0 : file.seekg(0, std::ios::beg);
278 0 : str.assign((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
279 0 : file.close();
280 : try {
281 : // Unpack the string
282 0 : msgpack::object_handle oh = msgpack::unpack(str.data(), str.size());
283 : // Deserialized object is valid during the msgpack::object_handle instance is alive.
284 0 : msgpack::object deserialized = oh.get();
285 0 : deserialized.convert(list);
286 0 : } catch (const std::exception& e) {
287 0 : JAMI_ERR() << e.what();
288 0 : }
289 : }
290 0 : }
291 32 : }
292 :
293 : void
294 0 : PluginPreferencesUtils::addAlwaysHandlerPreference(const std::string& handlerName, const std::string& rootPath)
295 : {
296 : {
297 0 : auto filePath = getPreferencesConfigFilePath(rootPath);
298 0 : Json::Value root;
299 :
300 0 : std::lock_guard guard(dhtnet::fileutils::getFileLock(filePath));
301 0 : std::ifstream file(filePath);
302 0 : Json::CharReaderBuilder rbuilder;
303 0 : Json::Value preference;
304 0 : rbuilder["collectComments"] = false;
305 0 : std::string errs;
306 0 : if (file) {
307 0 : bool ok = Json::parseFromStream(rbuilder, file, &root, &errs);
308 0 : if (ok && root.isArray()) {
309 : // Return if preference already exists
310 0 : for (const auto& child : root)
311 0 : if (child.get("key", "None").asString() == handlerName + "Always")
312 0 : return;
313 : }
314 : }
315 0 : }
316 :
317 0 : auto filePath = getPreferencesConfigFilePath(rootPath, "acc");
318 0 : Json::Value root;
319 : {
320 0 : std::lock_guard guard(dhtnet::fileutils::getFileLock(filePath));
321 0 : std::ifstream file(filePath);
322 0 : Json::CharReaderBuilder rbuilder;
323 0 : Json::Value preference;
324 0 : rbuilder["collectComments"] = false;
325 0 : std::string errs;
326 0 : if (file) {
327 0 : bool ok = Json::parseFromStream(rbuilder, file, &root, &errs);
328 0 : if (ok && root.isArray()) {
329 : // Return if preference already exists
330 0 : for (const auto& child : root)
331 0 : if (child.get("key", "None").asString() == handlerName + "Always")
332 0 : return;
333 : }
334 : }
335 : // Create preference structure otherwise
336 0 : preference["key"] = handlerName + "Always";
337 0 : preference["type"] = "Switch";
338 0 : preference["defaultValue"] = "0";
339 0 : preference["title"] = "Automatically turn " + handlerName + " on";
340 0 : preference["summary"] = handlerName + " will take effect immediately";
341 0 : preference["scope"] = "accountId";
342 0 : root.append(preference);
343 0 : }
344 0 : std::lock_guard guard(dhtnet::fileutils::getFileLock(filePath));
345 0 : std::ofstream outFile(filePath);
346 0 : if (outFile) {
347 : // Save preference.json file with new "always preference"
348 0 : outFile << root.toStyledString();
349 0 : outFile.close();
350 : }
351 0 : }
352 :
353 : bool
354 0 : PluginPreferencesUtils::getAlwaysPreference(const std::string& rootPath,
355 : const std::string& handlerName,
356 : const std::string& accountId)
357 : {
358 0 : auto preferences = getPreferences(rootPath);
359 0 : auto accPrefs = getPreferences(rootPath, accountId);
360 0 : for (const auto& item : accPrefs) {
361 0 : preferences.push_back(item);
362 : }
363 0 : auto preferencesValues = getPreferencesValuesMap(rootPath, accountId);
364 :
365 0 : for (const auto& preference : preferences) {
366 0 : auto key = preference.at("key");
367 0 : if (preference.at("type") == "Switch" && key == handlerName + "Always"
368 0 : && preferencesValues.find(key)->second == "1")
369 0 : return true;
370 0 : }
371 :
372 0 : return false;
373 0 : }
374 : } // namespace jami
|