File: preferences.hpp

package info (click to toggle)
sight 25.2.0-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 42,180 kB
  • sloc: cpp: 289,476; xml: 17,257; ansic: 9,878; python: 1,379; sh: 144; makefile: 33
file content (401 lines) | stat: -rw-r--r-- 15,086 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
/************************************************************************
 *
 * Copyright (C) 2021-2024 IRCAD France
 *
 * This file is part of Sight.
 *
 * Sight is free software: you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Sight is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with Sight. If not, see <https://www.gnu.org/licenses/>.
 *
 ***********************************************************************/

#pragma once

#include <sight/ui/__/config.hpp>

#include <core/crypto/password_keeper.hpp>
#include <core/exception.hpp>
#include <core/exceptionmacros.hpp>
#include <core/macros.hpp>

#include <boost/lexical_cast.hpp>
#include <boost/property_tree/ptree.hpp>

#include <filesystem>
#include <mutex>
#include <optional>
#include <shared_mutex>

namespace sight::ui
{

/// Simple exception class thrown when preferences have been disabled or because of they cannot be enabled due to errors
/// like corrupted preferences files
class SIGHT_UI_CLASS_API preferences_disabled : public core::exception
{
public:

    inline preferences_disabled(const std::string& _err) noexcept :
        core::exception(_err)
    {
    }

    ~preferences_disabled() override = default;
};

/// Subclass of preferences_disabled, thrown when trying to open an encrypted file with a wrong password
class SIGHT_UI_CLASS_API bad_password : public preferences_disabled
{
public:

    inline bad_password(const std::string& _err) noexcept :
        preferences_disabled(_err)
    {
    }

    ~bad_password() override = default;
};

/**
 * @brief application preferences
 *
 * @details Class that holds a global static property tree used to store and retrieve preferences accross all services.
 *
 * @note It uses Boost property tree as its backend, so it mimics its syntax.
 *
 * @section Usage Basic Usage
 * The class is thread safe and use RAII mechanism to load and store data from and to the preference file. In a service,
 * rhe basic usage would be:
 *
 * @code{.cpp}
    try
    {
        // Load
        preferences preferences;
        std::string filename = preferences.get("debug.filename");
        int level = preferences.get<int>("debug.level");
        ...
        // Save
        preferences preferences;
        preferences.put("debug.filename", "log.txt");
        preferences.put("debug.level", 2);
    }
    catch(const ui::preferences_disabled&)
    {
        // Nothing to do..
    }
   @endcode
 *
 * Which will be translated into:
 * @code{.json}
    debug:
        filename: "log.txt"
        level: 2
   @endcode
 *
 * @section Configuration Configuration
 *
 * the configuration is done by static functions:
 *
 * @subsection set_enabled set_enabled
 * Enable or disable preference management as a whole. All functions, including constructor will throw
 * `preferences_disabled` exception if used while "disabled". Default is `true`.
 *
 * @subsection ignoreFilesystem ignoreFilesystem
 * Enable or disable loading and saving from the filesystem. If true, the preference file won't be loaded when the
 * Preference is used, and it won't be saved either when the application exits. This is primarily intended for GUI
 * tests, in order to enhance reproducibility of the tests and to avoid to mess with the preferences of the user.
 *
 * @subsection set_password set_password
 * Set an hardcoded password to be used. It enables defacto the encryption
 *
 * @subsection set_password_policy set_password_policy
 * Defines how and when a password is asked. @see sight::core::crypto::password_keeper::password_policy for possible
 * values. Default is `never`.
 *
 * @note `NEVER` will never display any message box, but if a password has been set, the resulting preference file will
 * still be encrypted. An `BadPassword` exception will be thrown instead of displaying a message box, asking re-enter
 * the password.
 *
 * @subsection set_encryption_policy set_encryption_policy
 * Defines when the preferences file is encrypted: @see sight::core::crypto::password_keeper::encryption_policy for
 * possible values. Default is `password`.
 *
 * @note `FORCE` will encrypt the file, even if no password is given. In this case a pseudo random password is used,
 * which can be guessed if someone has access to the code. Another option is to use an hardcoded password AND
 * encryption_policy::SALTED
 *
 * @subsection preferences_exit_on_password_error preferences_exit_on_password_error
 * Defines if cancelling or making more than 3 attempts close the application or not. Default is `false`.
 *
 * @subsection preferences_password_dialog_title preferences_password_dialog_title
 * Defines the title of the password dialog.
 *
 * @subsection preferences_password_dialog_message preferences_password_dialog_message
 * Defines the message of the password dialog. (will be merged with the icon)
 *
 * @subsection preferences_password_dialog_icon preferences_password_dialog_icon
 * Defines the icon of the password dialog. (will be merged with the message)
 *
 * @section Module  Module Configuration
 * All the above can be configured through the module ui parameters ( @see sight::module::ui::plugin )
 *
 * The preferences are enabled by default, without encryption. An example of configuration would be:
 *
 * @code {.cmake}
    module_param(
        module_ui
        PARAM_LIST
            preferences_enabled
            preferences_password_policy
            preferences_encryption_policy
            preferences_password
            preferences_exit_on_password_error
            preferences_password_dialog_title
            preferences_password_dialog_message
            preferences_password_dialog_icon
        PARAM_VALUES
            true
            once
            salted
            a_bad_hardcoded_password
            true
            "Password required"
            "  Please enter your password: "
            "sight::module::ui::qt/text.svg"
    )
 * @endcode
 *
 */
class SIGHT_UI_CLASS_API preferences final
{
public:

    static constexpr auto DEFAULT_DELIMITER = "%";

    /// Constructor
    /// It will load the default preference file if not already done and throw 'preferences_disabled' exception in case
    /// of error or when disabled (or BadPassword, if we use encryption and the password is not the good one)
    SIGHT_UI_API preferences();

    /// Destructor
    /// It will save the preferences, if they have been modified
    SIGHT_UI_API ~preferences();

    /// Returns the preference associated with the given key/path
    /// @param _key the key/path of the preference.
    template<typename T>
    [[nodiscard]] inline T get(const std::string& _key) const
    {
        // Protect preferences for reading
        std::shared_lock guard(s_preferences_mutex);

        // If the preferences are disabled or not loaded, throw an exception to disallow loading
        throw_if_disabled();

        return s_preferences->get<T>(_key);
    }

    /// Returns an optional preference associated with the given key/path.
    /// @param _key the key/path of the preference.
    template<typename T>
    [[nodiscard]] inline boost::optional<T> get_optional(const std::string& _key) const
    {
        // Protect preferences for reading
        std::shared_lock guard(s_preferences_mutex);

        // If the preferences are disabled or not loaded, throw an exception to disallow loading
        throw_if_disabled();

        return s_preferences->get_optional<T>(_key);
    }

    /// Returns the preference associated with the given key/path. If the preference doesn't exist, the default value is
    /// returned
    /// @param _key the key/path of the preference.
    /// @param _default_value the default value used when the key/path is not found.
    template<typename T>
    [[nodiscard]] inline T get(const std::string& _key, const T& _default_value) const noexcept
    {
        try
        {
            return get<T>(_key);
        }
        catch(const boost::property_tree::ptree_error& e)
        {
            SIGHT_WARN(e.what());
            return _default_value;
        }
        catch(...)
        {
            return _default_value;
        }
    }

    //------------------------------------------------------------------------------

    /// Special "get" version mostly used in xml with a magic "%" delimiter. If there are no "%", it means we should
    /// return the key as a value, otherwise, we search in the preference
    /// @param _key the key/path of the preference, that could be the preference if no delimiter are used.
    /// @param _delimiter the magical delimiter.
    template<typename T>
    [[nodiscard]] inline std::pair<std::string, T> parsed_get(
        const std::string& _key,
        const std::string& _delimiter = DEFAULT_DELIMITER
    ) const
    {
        if(const auto& delimiter_start = _key.find_first_of(_delimiter); delimiter_start != std::string::npos)
        {
            if(const auto& delimiter_end = _key.find_last_of(_delimiter); delimiter_end != std::string::npos)
            {
                const auto& real_key = _key.substr(delimiter_start + 1, delimiter_end - (delimiter_start + 1));
                return {real_key, get<T>(real_key)};
            }
        }

        return {"", boost::lexical_cast<T>(_key)};
    }

    /// Special "get" version mostly used in xml with a magic "%" delimiter. If there are no "%", it means we should
    /// return the key as a value, otherwise, we search in the preference
    /// @param _key the key/path of the preference, that could be the preference if no delimiter are used.
    /// @param _delimiter the magical delimiter.
    template<typename T>
    [[nodiscard]] inline T delimited_get(
        const std::string& _key,
        const std::string& _delimiter = DEFAULT_DELIMITER
    ) const
    {
        return this->parsed_get<T>(_key, _delimiter).second;
    }

    /// Special "get" version mostly used in xml with a magic "%" delimiter. If there are no "%", it means we should
    /// return the key as a value, otherwise, we search in the preference. If no preference is found, we return the
    /// default value
    /// @param _key the key/path of the preference, that could be the preference if no delimiter are used.
    /// @param _default_value the default value used when the key/path is not found.
    /// @param _delimiter the magical delimiter.
    template<typename T>
    [[nodiscard]] inline T delimited_get(
        const std::string& _key,
        const T& _default_value,
        const std::string& _delimiter = DEFAULT_DELIMITER
    ) const noexcept
    {
        try
        {
            return delimited_get<T>(_key, _delimiter);
        }
        catch(const boost::property_tree::ptree_error& e)
        {
            SIGHT_WARN(e.what());
            return _default_value;
        }
        catch(...)
        {
            return _default_value;
        }
    }

    /// Set a preference associated with the given key/path
    /// @param _key the key/path of the preference.
    /// @param _value the value to set.
    template<typename T>
    inline void put(const std::string& _key, const T& _value)
    {
        // Protect preferences for writing
        std::unique_lock guard(s_preferences_mutex);

        // If the preferences are disabled or not loaded, throw an exception to disallow puting values
        throw_if_disabled();

        s_preferences->put(_key, _value);

        // Mark the tree as modified so it will be saved when preferences is deleted
        s_is_preferences_modified = true;
    }

    /// Remove one value from the preferences
    static SIGHT_UI_API void erase(const std::string& _key);

    /// Remove all preferences
    static SIGHT_UI_API void clear();

    /// Enable / disable the preferences system. All functions will throw a preferences_disabled, if disabled
    SIGHT_UI_API static void set_enabled(bool _enable);

    /// Enable / disable loading/saving from the filesystem.
    SIGHT_UI_API static void ignore_filesystem(bool _ignore);

    /// Set a password and enable encryption
    SIGHT_UI_API static void set_password(const core::crypto::secure_string& _password);

    /// Set the password policy
    /// @param _policy @see sight::core::crypto::password_keeper::password_policy
    SIGHT_UI_API static void set_password_policy(core::crypto::password_keeper::password_policy _policy);

    /// Set the encryption policy
    /// @param _policy @see sight::core::crypto::password_keeper::encryption_policy
    SIGHT_UI_API static void set_encryption_policy(core::crypto::password_keeper::encryption_policy _policy);

    /// If true, the application will be terminated in case of password error
    SIGHT_UI_API static void exit_on_password_error(bool _exit);

    /// Password dialog customization
    /// @{
    struct SIGHT_UI_CLASS_API password_dialog_strings final
    {
        std::optional<std::string> title {std::nullopt};
        std::optional<std::string> message {std::nullopt};
        std::optional<std::string> new_title {std::nullopt};
        std::optional<std::string> new_message {std::nullopt};
        std::optional<std::string> weak_title {std::nullopt};
        std::optional<std::string> error_title {std::nullopt};
        std::optional<std::string> error_message {std::nullopt};
        std::optional<std::string> fatal_message {std::nullopt};
        std::optional<std::string> retry_message {std::nullopt};
        std::optional<std::string> cancel_title {std::nullopt};
        std::optional<std::string> cancel_message {std::nullopt};
        std::optional<std::string> cancel_fatal_message {std::nullopt};
    };

    SIGHT_UI_API static void set_password_dialog_strings(const password_dialog_strings& _strings);
    /// @}

    /// Set a custom password validator
    SIGHT_UI_API static void set_password_validator(
        std::function<std::pair<bool, std::string>(const sight::core::crypto::secure_string&)> _validator
    );

private:

    /// Throws a preferences_disabled if preferences management is disabled
    SIGHT_UI_API static void throw_if_disabled();

    /// Guard the preference tree
    SIGHT_UI_API static std::shared_mutex s_preferences_mutex;

    /// Contains the preference tree
    SIGHT_UI_API static std::unique_ptr<boost::property_tree::ptree> s_preferences;

    /// True if the preferences has been modified
    SIGHT_UI_API static bool s_is_preferences_modified;

    /// preferences can be disabled globally
    SIGHT_UI_API static bool s_is_enabled;

    /// If true, the preferences won't be loaded/saved from the filesystem
    SIGHT_UI_API static bool s_ignore_filesystem;
};

} // namespace sight::ui