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