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