diff --git a/src/yunohost/app.py b/src/yunohost/app.py index 2b8d71abf..fe5281384 100644 --- a/src/yunohost/app.py +++ b/src/yunohost/app.py @@ -169,7 +169,7 @@ def app_info(app, full=False): ret["from_catalog"] = _load_apps_catalog()["apps"].get(absolute_app_name, {}) ret["upgradable"] = _app_upgradable(ret) - ret["is_webapp"] = ("domain" in settings and "path" in settings) + ret["is_webapp"] = "domain" in settings and "path" in settings ret["supports_change_url"] = os.path.exists( os.path.join(setting_path, "scripts", "change_url") diff --git a/src/yunohost/tests/test_questions.py b/src/yunohost/tests/test_questions.py index b39990b73..c21ff8c40 100644 --- a/src/yunohost/tests/test_questions.py +++ b/src/yunohost/tests/test_questions.py @@ -15,7 +15,7 @@ from yunohost.utils.config import ( PathQuestion, BooleanQuestion, FileQuestion, - evaluate_simple_js_expression + evaluate_simple_js_expression, ) from yunohost.utils.error import YunohostError, YunohostValidationError @@ -2095,97 +2095,95 @@ def test_normalize_path(): assert PathQuestion.normalize("macnuggets/") == "/macnuggets" assert PathQuestion.normalize("////macnuggets///") == "/macnuggets" + def test_simple_evaluate(): context = { - 'a1': 1, - 'b2': 2, - 'c10': 10, - 'foo': 'bar', - 'comp': '1>2', - 'empty': '', - 'lorem': 'Lorem ipsum dolor et si qua met!', - 'warning': 'Warning! This sentence will fail!', - 'quote': "Je s'apelle Groot", - 'and_': '&&', - 'object': { 'a': 'Security risk' } + "a1": 1, + "b2": 2, + "c10": 10, + "foo": "bar", + "comp": "1>2", + "empty": "", + "lorem": "Lorem ipsum dolor et si qua met!", + "warning": "Warning! This sentence will fail!", + "quote": "Je s'apelle Groot", + "and_": "&&", + "object": {"a": "Security risk"}, } supported = { - '42': 42, - '9.5': 9.5, - "'bopbidibopbopbop'": 'bopbidibopbopbop', - 'true': True, - 'false': False, - 'null': None, - + "42": 42, + "9.5": 9.5, + "'bopbidibopbopbop'": "bopbidibopbopbop", + "true": True, + "false": False, + "null": None, # Math - '1 * (2 + 3 * (4 - 3))': 5, - '1 * (2 + 3 * (4 - 3)) > 10 - 2 || 3 * 2 > 9 - 2 * 3': True, - '(9 - 2) * 3 - 10': 11, - '12 - 2 * -2 + (3 - 4) * 3.1': 12.9, - '9 / 12 + 12 * 3 - 5': 31.75, - '9 / 12 + 12 * (3 - 5)': -23.25, - '12 > 13.1': False, - '12 < 14': True, - '12 <= 14': True, - '12 >= 14': False, - '12 == 14': False, - '12 % 5 > 3': False, - '12 != 14': True, - '9 - 1 > 10 && 3 * 5 > 10': False, - '9 - 1 > 10 || 3 * 5 > 10': True, - 'a1 > 0 || a1 < -12': True, - 'a1 > 0 && a1 < -12': False, - 'a1 + 1 > 0 && -a1 > -12': True, - '-(a1 + 1) < 0 || -(a1 + 2) > -12': True, - '-a1 * 2': -2, - '(9 - 2) * 3 - c10': 11, - '(9 - b2) * 3 - c10': 11, - 'c10 > b2': True, - + "1 * (2 + 3 * (4 - 3))": 5, + "1 * (2 + 3 * (4 - 3)) > 10 - 2 || 3 * 2 > 9 - 2 * 3": True, + "(9 - 2) * 3 - 10": 11, + "12 - 2 * -2 + (3 - 4) * 3.1": 12.9, + "9 / 12 + 12 * 3 - 5": 31.75, + "9 / 12 + 12 * (3 - 5)": -23.25, + "12 > 13.1": False, + "12 < 14": True, + "12 <= 14": True, + "12 >= 14": False, + "12 == 14": False, + "12 % 5 > 3": False, + "12 != 14": True, + "9 - 1 > 10 && 3 * 5 > 10": False, + "9 - 1 > 10 || 3 * 5 > 10": True, + "a1 > 0 || a1 < -12": True, + "a1 > 0 && a1 < -12": False, + "a1 + 1 > 0 && -a1 > -12": True, + "-(a1 + 1) < 0 || -(a1 + 2) > -12": True, + "-a1 * 2": -2, + "(9 - 2) * 3 - c10": 11, + "(9 - b2) * 3 - c10": 11, + "c10 > b2": True, # String - "foo == 'bar'":True, - "foo != 'bar'":False, - 'foo == "bar" && 1 > 0':True, - '!!foo': True, - '!foo': False, - 'foo': 'bar', + "foo == 'bar'": True, + "foo != 'bar'": False, + 'foo == "bar" && 1 > 0': True, + "!!foo": True, + "!foo": False, + "foo": "bar", '!(foo > "baa") || 1 > 2': False, '!(foo > "baa") || 1 < 2': True, 'empty == ""': True, '1 == "1"': True, '1.0 == "1"': True, '1 == "aaa"': False, - "'I am ' + b2 + ' years'": 'I am 2 years', + "'I am ' + b2 + ' years'": "I am 2 years", "quote == 'Je s\\'apelle Groot'": True, "lorem == 'Lorem ipsum dolor et si qua met!'": True, "and_ == '&&'": True, "warning == 'Warning! This sentence will fail!'": True, - # Match "match(lorem, '^Lorem [ia]psumE?')": bool, "match(foo, '^Lorem [ia]psumE?')": None, "match(lorem, '^Lorem [ia]psumE?') && 1 == 1": bool, - # No code "": False, " ": False, } trigger_errors = { - "object.a": YunohostError, # Keep unsupported, for security reasons - 'a1 ** b2': YunohostError, # Keep unsupported, for security reasons - '().__class__.__bases__[0].__subclasses__()': YunohostError, # Very dangerous code - 'a1 > 11 ? 1 : 0': SyntaxError, - 'c10 > b2 == false': YunohostError, # JS and Python doesn't do the same thing for this situation - 'c10 > b2 == true': YunohostError, + "object.a": YunohostError, # Keep unsupported, for security reasons + "a1 ** b2": YunohostError, # Keep unsupported, for security reasons + "().__class__.__bases__[0].__subclasses__()": YunohostError, # Very dangerous code + "a1 > 11 ? 1 : 0": SyntaxError, + "c10 > b2 == false": YunohostError, # JS and Python doesn't do the same thing for this situation + "c10 > b2 == true": YunohostError, } for expression, result in supported.items(): if result == bool: assert bool(evaluate_simple_js_expression(expression, context)), expression else: - assert evaluate_simple_js_expression(expression, context) == result, expression + assert ( + evaluate_simple_js_expression(expression, context) == result + ), expression for expression, error in trigger_errors.items(): with pytest.raises(error): evaluate_simple_js_expression(expression, context) - diff --git a/src/yunohost/utils/config.py b/src/yunohost/utils/config.py index fbaecfbe0..4ee944126 100644 --- a/src/yunohost/utils/config.py +++ b/src/yunohost/utils/config.py @@ -55,24 +55,24 @@ def evaluate_simple_ast(node, context={}): operators = { ast.Not: op.not_, ast.Mult: op.mul, - ast.Div: op.truediv, # number - ast.Mod: op.mod, # number - ast.Add: op.add, #str - ast.Sub: op.sub, #number - ast.USub: op.neg, # Negative number + ast.Div: op.truediv, # number + ast.Mod: op.mod, # number + ast.Add: op.add, # str + ast.Sub: op.sub, # number + ast.USub: op.neg, # Negative number ast.Gt: op.gt, ast.Lt: op.lt, ast.GtE: op.ge, ast.LtE: op.le, ast.Eq: op.eq, - ast.NotEq: op.ne + ast.NotEq: op.ne, } - context['true'] = True - context['false'] = False - context['null'] = None + context["true"] = True + context["false"] = False + context["null"] = None # Variable - if isinstance(node, ast.Name): # Variable + if isinstance(node, ast.Name): # Variable return context[node.id] # Python <=3.7 String @@ -88,14 +88,16 @@ def evaluate_simple_ast(node, context={}): return node.value # + - * / % - elif isinstance(node, ast.BinOp) and type(node.op) in operators: # + elif ( + isinstance(node, ast.BinOp) and type(node.op) in operators + ): # left = evaluate_simple_ast(node.left, context) right = evaluate_simple_ast(node.right, context) if type(node.op) == ast.Add: - if isinstance(left, str) or isinstance(right, str): # support 'I am ' + 42 + if isinstance(left, str) or isinstance(right, str): # support 'I am ' + 42 left = str(left) right = str(right) - elif type(left) != type(right): # support "111" - "1" -> 110 + elif type(left) != type(right): # support "111" - "1" -> 110 left = float(left) right = float(right) @@ -104,7 +106,9 @@ def evaluate_simple_ast(node, context={}): # Comparison # JS and Python don't give the same result for multi operators # like True == 10 > 2. - elif isinstance(node, ast.Compare) and len(node.comparators) == 1: # + elif ( + isinstance(node, ast.Compare) and len(node.comparators) == 1 + ): # left = evaluate_simple_ast(node.left, context) right = evaluate_simple_ast(node.comparators[0], context) operator = node.ops[0] @@ -116,11 +120,11 @@ def evaluate_simple_ast(node, context={}): return type(operator) == ast.NotEq try: return operators[type(operator)](left, right) - except TypeError: # support "e" > 1 -> False like in JS + except TypeError: # support "e" > 1 -> False like in JS return False # and / or - elif isinstance(node, ast.BoolOp): # + elif isinstance(node, ast.BoolOp): # for value in node.values: value = evaluate_simple_ast(value, context) if isinstance(node.op, ast.And) and not value: @@ -130,20 +134,22 @@ def evaluate_simple_ast(node, context={}): return isinstance(node.op, ast.And) # not / USub (it's negation number -\d) - elif isinstance(node, ast.UnaryOp): # e.g., -1 + elif isinstance(node, ast.UnaryOp): # e.g., -1 return operators[type(node.op)](evaluate_simple_ast(node.operand, context)) # match function call - elif isinstance(node, ast.Call) and node.func.__dict__.get('id') == 'match': + elif isinstance(node, ast.Call) and node.func.__dict__.get("id") == "match": return re.match( - evaluate_simple_ast(node.args[1], context), - context[node.args[0].id] + evaluate_simple_ast(node.args[1], context), context[node.args[0].id] ) # Unauthorized opcode else: opcode = str(type(node)) - raise YunohostError(f"Unauthorize opcode '{opcode}' in visible attribute", raw_msg=True) + raise YunohostError( + f"Unauthorize opcode '{opcode}' in visible attribute", raw_msg=True + ) + def js_to_python(expr): in_string = None @@ -162,7 +168,7 @@ def js_to_python(expr): # If we are not in a string, replace operators elif not in_string: - if char == "!" and expr[i +1] != "=": + if char == "!" and expr[i + 1] != "=": char = "not " elif char in "|&" and py_expr[-1:] == char: py_expr = py_expr[:-1] @@ -171,15 +177,17 @@ def js_to_python(expr): # Determine if next loop will be in escaped mode escaped = char == "\\" and not escaped py_expr += char - i+=1 + i += 1 return py_expr + def evaluate_simple_js_expression(expr, context={}): if not expr.strip(): return False node = ast.parse(js_to_python(expr), mode="eval").body return evaluate_simple_ast(node, context) + class ConfigPanel: def __init__(self, config_path, save_path=None): self.config_path = config_path @@ -648,7 +656,9 @@ class Question(object): def ask_if_needed(self): - if self.visible and not evaluate_simple_js_expression(self.visible, context=self.context): + if self.visible and not evaluate_simple_js_expression( + self.visible, context=self.context + ): # FIXME There could be several use case if the question is not displayed: # - we doesn't want to give a specific value # - we want to keep the previous value