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