<commit_before>import os
import json

from datetime import datetime
from collections import OrderedDict

from moulinette import m18n
from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger

logger = getActionLogger('yunohost.settings')

SETTINGS_PATH = "/etc/yunohost/settings.json"
SETTINGS_PATH_OTHER_LOCATION = "/etc/yunohost/settings-%s.json"

# a settings entry is in the form of:
# namespace.subnamespace.name: {type, value, default, description, [choices]}
# choices is only for enum
# the keyname can have as many subnamespace as needed but should have at least
# one level of namespace

# description is implied from the translated strings
# the key is "global_settings_setting_%s" % key.replace(".", "_")

# type can be:
# * bool
# * int
# * string
# * enum (in form a python list)

DEFAULTS = OrderedDict([
    ("example.bool", {"type": "bool", "default": True}),
    ("example.int", {"type": "int", "default": 42}),
    ("example.string", {"type": "string", "default": "yolo swag"}),
    ("example.enum", {"type": "enum", "default": "a", "choices": ["a", "b", "c"]}),

    # Password Validation
    # -1 disabled, 0 alert if listed, 1 8-letter, 2 normal, 3 strong, 4 strongest
    ("security.password.admin.strength", {"type": "int", "default": 1}),
    ("security.password.user.strength", {"type": "int", "default": 1}),
    ("service.ssh.allow_deprecated_dsa_hostkey", {"type": "bool", "default": False}),
])


def settings_get(key, full=False):
    """
    Get an entry value in the settings

    Keyword argument:
        key -- Settings key

    """
    settings = _get_settings()

    if key not in settings:
        raise YunohostError('global_settings_key_doesnt_exists', settings_key=key)

    if full:
        return settings[key]

    return settings[key]['value']


def settings_list():
    """
    List all entries of the settings

    """
    return _get_settings()


def settings_set(key, value):
    """
    Set an entry value in the settings

    Keyword argument:
        key -- Settings key
        value -- New value

    """
    settings = _get_settings()

    if key not in settings:
        raise YunohostError('global_settings_key_doesnt_exists', settings_key=key)

    key_type = settings[key]["type"]

    if key_type == "bool":
        if not isinstance(value, bool):
            raise YunohostError('global_settings_bad_type_for_setting', setting=key,
                                received_type=type(value).__name__, expected_type=key_type)
    elif key_type == "int":
        if not isinstance(value, int) or isinstance(value, bool):
            if isinstance(value, str):
                try:
                    value = int(value)
                except:
                    raise YunohostError('global_settings_bad_type_for_setting',
                                        setting=key,
                                        received_type=type(value).__name__,
                                        expected_type=key_type)
            else:
                raise YunohostError('global_settings_bad_type_for_setting', setting=key,
                                    received_type=type(value).__name__, expected_type=key_type)
    elif key_type == "string":
        if not isinstance(value, basestring):
            raise YunohostError('global_settings_bad_type_for_setting', setting=key,
                                received_type=type(value).__name__, expected_type=key_type)
    elif key_type == "enum":
        if value not in settings[key]["choices"]:
            raise YunohostError('global_settings_bad_choice_for_enum', setting=key,
                                received_type=type(value).__name__,
                                expected_type=", ".join(settings[key]["choices"]))
    else:
        raise YunohostError('global_settings_unknown_type', setting=key,
                            unknown_type=key_type)

    old_value = settings[key].get("value")
    settings[key]["value"] = value
    _save_settings(settings)

    # TODO : whatdo if the old value is the same as
    # the new value...
    try:
        trigger_post_change_hook(key, old_value, value)
    except Exception as e:
        logger.error("Post-change hook for setting %s failed : %s" % (key, e))
        raise


def settings_reset(key):
    """
    Set an entry value to its default one

    Keyword argument:
        key -- Settings key

    """
    settings = _get_settings()

    if key not in settings:
        raise YunohostError('global_settings_key_doesnt_exists', settings_key=key)

    settings[key]["value"] = settings[key]["default"]
    _save_settings(settings)


def settings_reset_all():
    """
    Reset all settings to their default value

    Keyword argument:
        yes -- Yes I'm sure I want to do that

    """
    settings = _get_settings()

    # For now on, we backup the previous settings in case of but we don't have
    # any mecanism to take advantage of those backups. It could be a nice
    # addition but we'll see if this is a common need.
    # Another solution would be to use etckeeper and integrate those
    # modification inside of it and take advantage of its git history
    old_settings_backup_path = SETTINGS_PATH_OTHER_LOCATION % datetime.utcnow().strftime("%F_%X")
    _save_settings(settings, location=old_settings_backup_path)

    for value in settings.values():
        value["value"] = value["default"]

    _save_settings(settings)

    return {
        "old_settings_backup_path": old_settings_backup_path,
        "message": m18n.n("global_settings_reset_success", path=old_settings_backup_path)
    }


def _get_settings():
    settings = {}

    for key, value in DEFAULTS.copy().items():
        settings[key] = value
        settings[key]["value"] = value["default"]
        settings[key]["description"] = m18n.n("global_settings_setting_%s" % key.replace(".", "_"))

    if not os.path.exists(SETTINGS_PATH):
        return settings

    # we have a very strict policy on only allowing settings that we know in
    # the OrderedDict DEFAULTS
    # For various reason, while reading the local settings we might encounter
    # settings that aren't in DEFAULTS, those can come from settings key that
    # we have removed, errors or the user trying to modify
    # /etc/yunohost/settings.json
    # To avoid to simply overwrite them, we store them in
    # /etc/yunohost/settings-unknown.json in case of
    unknown_settings = {}
    unknown_settings_path = SETTINGS_PATH_OTHER_LOCATION % "unknown"

    if os.path.exists(unknown_settings_path):
        try:
            unknown_settings = json.load(open(unknown_settings_path, "r"))
        except Exception as e:
            logger.warning("Error while loading unknown settings %s" % e)

    try:
        with open(SETTINGS_PATH) as settings_fd:
            local_settings = json.load(settings_fd)

            for key, value in local_settings.items():
                if key in settings:
                    settings[key] = value
                    settings[key]["description"] = m18n.n("global_settings_setting_%s" % key.replace(".", "_"))
                else:
                    logger.warning(m18n.n('global_settings_unknown_setting_from_settings_file',
                                          setting_key=key))
                    unknown_settings[key] = value
    except Exception as e:
        raise YunohostError('global_settings_cant_open_settings', reason=e)

    if unknown_settings:
        try:
            _save_settings(unknown_settings, location=unknown_settings_path)
        except Exception as e:
            logger.warning("Failed to save unknown settings (because %s), aborting." % e)

    return settings


def _save_settings(settings, location=SETTINGS_PATH):
    settings_without_description = {}
    for key, value in settings.items():
        settings_without_description[key] = value
        if "description" in value:
            del settings_without_description[key]["description"]

    try:
        result = json.dumps(settings_without_description, indent=4)
    except Exception as e:
        raise YunohostError('global_settings_cant_serialize_settings', reason=e)

    try:
        with open(location, "w") as settings_fd:
            settings_fd.write(result)
    except Exception as e:
        raise YunohostError('global_settings_cant_write_settings', reason=e)


# Meant to be a dict of setting_name -> function to call
post_change_hooks = {}


def post_change_hook(setting_name):
    def decorator(func):
        assert setting_name in DEFAULTS.keys(), "The setting %s does not exists" % setting_name
        assert setting_name not in post_change_hooks, "You can only register one post change hook per setting (in particular for %s)" % setting_name
        post_change_hooks[setting_name] = func
        return func
    return decorator


def trigger_post_change_hook(setting_name, old_value, new_value):
    if setting_name not in post_change_hooks:
        logger.debug("Nothing to do after changing setting %s" % setting_name)
        return

    f = post_change_hooks[setting_name]
    f(old_value, new_value)


# ===========================================
#
# Actions to trigger when changing a setting
# You can define such an action with :
#
# @post_change_hook("your.setting.name")
# def some_function_name(old_value, new_value):
#     # Do some stuff
#
# ===========================================


#@post_change_hook("example.int")
#def myfunc(old_value, new_value):
#    print("In hook")
#    print(old_value)
#    print(new_value)<commit_msg>Also feed the setting name when calling setting hook<commit_after>import os
import json

from datetime import datetime
from collections import OrderedDict

from moulinette import m18n
from yunohost.utils.error import YunohostError
from moulinette.utils.log import getActionLogger

logger = getActionLogger('yunohost.settings')

SETTINGS_PATH = "/etc/yunohost/settings.json"
SETTINGS_PATH_OTHER_LOCATION = "/etc/yunohost/settings-%s.json"

# a settings entry is in the form of:
# namespace.subnamespace.name: {type, value, default, description, [choices]}
# choices is only for enum
# the keyname can have as many subnamespace as needed but should have at least
# one level of namespace

# description is implied from the translated strings
# the key is "global_settings_setting_%s" % key.replace(".", "_")

# type can be:
# * bool
# * int
# * string
# * enum (in form a python list)

DEFAULTS = OrderedDict([
    ("example.bool", {"type": "bool", "default": True}),
    ("example.int", {"type": "int", "default": 42}),
    ("example.string", {"type": "string", "default": "yolo swag"}),
    ("example.enum", {"type": "enum", "default": "a", "choices": ["a", "b", "c"]}),

    # Password Validation
    # -1 disabled, 0 alert if listed, 1 8-letter, 2 normal, 3 strong, 4 strongest
    ("security.password.admin.strength", {"type": "int", "default": 1}),
    ("security.password.user.strength", {"type": "int", "default": 1}),
    ("service.ssh.allow_deprecated_dsa_hostkey", {"type": "bool", "default": False}),
])


def settings_get(key, full=False):
    """
    Get an entry value in the settings

    Keyword argument:
        key -- Settings key

    """
    settings = _get_settings()

    if key not in settings:
        raise YunohostError('global_settings_key_doesnt_exists', settings_key=key)

    if full:
        return settings[key]

    return settings[key]['value']


def settings_list():
    """
    List all entries of the settings

    """
    return _get_settings()


def settings_set(key, value):
    """
    Set an entry value in the settings

    Keyword argument:
        key -- Settings key
        value -- New value

    """
    settings = _get_settings()

    if key not in settings:
        raise YunohostError('global_settings_key_doesnt_exists', settings_key=key)

    key_type = settings[key]["type"]

    if key_type == "bool":
        if not isinstance(value, bool):
            raise YunohostError('global_settings_bad_type_for_setting', setting=key,
                                received_type=type(value).__name__, expected_type=key_type)
    elif key_type == "int":
        if not isinstance(value, int) or isinstance(value, bool):
            if isinstance(value, str):
                try:
                    value = int(value)
                except:
                    raise YunohostError('global_settings_bad_type_for_setting',
                                        setting=key,
                                        received_type=type(value).__name__,
                                        expected_type=key_type)
            else:
                raise YunohostError('global_settings_bad_type_for_setting', setting=key,
                                    received_type=type(value).__name__, expected_type=key_type)
    elif key_type == "string":
        if not isinstance(value, basestring):
            raise YunohostError('global_settings_bad_type_for_setting', setting=key,
                                received_type=type(value).__name__, expected_type=key_type)
    elif key_type == "enum":
        if value not in settings[key]["choices"]:
            raise YunohostError('global_settings_bad_choice_for_enum', setting=key,
                                received_type=type(value).__name__,
                                expected_type=", ".join(settings[key]["choices"]))
    else:
        raise YunohostError('global_settings_unknown_type', setting=key,
                            unknown_type=key_type)

    old_value = settings[key].get("value")
    settings[key]["value"] = value
    _save_settings(settings)

    # TODO : whatdo if the old value is the same as
    # the new value...
    try:
        trigger_post_change_hook(key, old_value, value)
    except Exception as e:
        logger.error("Post-change hook for setting %s failed : %s" % (key, e))
        raise


def settings_reset(key):
    """
    Set an entry value to its default one

    Keyword argument:
        key -- Settings key

    """
    settings = _get_settings()

    if key not in settings:
        raise YunohostError('global_settings_key_doesnt_exists', settings_key=key)

    settings[key]["value"] = settings[key]["default"]
    _save_settings(settings)


def settings_reset_all():
    """
    Reset all settings to their default value

    Keyword argument:
        yes -- Yes I'm sure I want to do that

    """
    settings = _get_settings()

    # For now on, we backup the previous settings in case of but we don't have
    # any mecanism to take advantage of those backups. It could be a nice
    # addition but we'll see if this is a common need.
    # Another solution would be to use etckeeper and integrate those
    # modification inside of it and take advantage of its git history
    old_settings_backup_path = SETTINGS_PATH_OTHER_LOCATION % datetime.utcnow().strftime("%F_%X")
    _save_settings(settings, location=old_settings_backup_path)

    for value in settings.values():
        value["value"] = value["default"]

    _save_settings(settings)

    return {
        "old_settings_backup_path": old_settings_backup_path,
        "message": m18n.n("global_settings_reset_success", path=old_settings_backup_path)
    }


def _get_settings():
    settings = {}

    for key, value in DEFAULTS.copy().items():
        settings[key] = value
        settings[key]["value"] = value["default"]
        settings[key]["description"] = m18n.n("global_settings_setting_%s" % key.replace(".", "_"))

    if not os.path.exists(SETTINGS_PATH):
        return settings

    # we have a very strict policy on only allowing settings that we know in
    # the OrderedDict DEFAULTS
    # For various reason, while reading the local settings we might encounter
    # settings that aren't in DEFAULTS, those can come from settings key that
    # we have removed, errors or the user trying to modify
    # /etc/yunohost/settings.json
    # To avoid to simply overwrite them, we store them in
    # /etc/yunohost/settings-unknown.json in case of
    unknown_settings = {}
    unknown_settings_path = SETTINGS_PATH_OTHER_LOCATION % "unknown"

    if os.path.exists(unknown_settings_path):
        try:
            unknown_settings = json.load(open(unknown_settings_path, "r"))
        except Exception as e:
            logger.warning("Error while loading unknown settings %s" % e)

    try:
        with open(SETTINGS_PATH) as settings_fd:
            local_settings = json.load(settings_fd)

            for key, value in local_settings.items():
                if key in settings:
                    settings[key] = value
                    settings[key]["description"] = m18n.n("global_settings_setting_%s" % key.replace(".", "_"))
                else:
                    logger.warning(m18n.n('global_settings_unknown_setting_from_settings_file',
                                          setting_key=key))
                    unknown_settings[key] = value
    except Exception as e:
        raise YunohostError('global_settings_cant_open_settings', reason=e)

    if unknown_settings:
        try:
            _save_settings(unknown_settings, location=unknown_settings_path)
        except Exception as e:
            logger.warning("Failed to save unknown settings (because %s), aborting." % e)

    return settings


def _save_settings(settings, location=SETTINGS_PATH):
    settings_without_description = {}
    for key, value in settings.items():
        settings_without_description[key] = value
        if "description" in value:
            del settings_without_description[key]["description"]

    try:
        result = json.dumps(settings_without_description, indent=4)
    except Exception as e:
        raise YunohostError('global_settings_cant_serialize_settings', reason=e)

    try:
        with open(location, "w") as settings_fd:
            settings_fd.write(result)
    except Exception as e:
        raise YunohostError('global_settings_cant_write_settings', reason=e)


# Meant to be a dict of setting_name -> function to call
post_change_hooks = {}


def post_change_hook(setting_name):
    def decorator(func):
        assert setting_name in DEFAULTS.keys(), "The setting %s does not exists" % setting_name
        assert setting_name not in post_change_hooks, "You can only register one post change hook per setting (in particular for %s)" % setting_name
        post_change_hooks[setting_name] = func
        return func
    return decorator


def trigger_post_change_hook(setting_name, old_value, new_value):
    if setting_name not in post_change_hooks:
        logger.debug("Nothing to do after changing setting %s" % setting_name)
        return

    f = post_change_hooks[setting_name]
    f(setting_name, old_value, new_value)


# ===========================================
#
# Actions to trigger when changing a setting
# You can define such an action with :
#
# @post_change_hook("your.setting.name")
# def some_function_name(setting_name, old_value, new_value):
#     # Do some stuff
#
# ===========================================


#@post_change_hook("example.int")
#def myfunc(setting_name, old_value, new_value):
#    print("In hook")
#    print(setting_name)
#    print(old_value)
#    print(new_value)
{'fraction_non_alphanumeric': 0.10673166683953947, 'fraction_numerical': 0.0022819209625557515, 'mean_word_length': 2.8759798994974872, 'pattern_counts': {'":': 42, '<': 3, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 12, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '324046', 'n_tokens_mistral': 5561, 'n_tokens_neox': 5400, 'n_words': 1527}