| 
									
										
										
										
											2024-04-07 15:52:00 +02:00
										 |  |  | # SPDX-License-Identifier: AGPL-3.0-or-later | 
					
						
							|  |  |  | """Calculate mathematical expressions using ack#eval
 | 
					
						
							|  |  |  | """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ast | 
					
						
							| 
									
										
										
										
											2024-10-05 16:10:56 +02:00
										 |  |  | import re | 
					
						
							| 
									
										
										
										
											2024-04-07 15:52:00 +02:00
										 |  |  | import operator | 
					
						
							| 
									
										
										
										
											2024-08-21 17:50:44 +02:00
										 |  |  | from multiprocessing import Process, Queue | 
					
						
							| 
									
										
										
										
											2024-10-05 16:10:56 +02:00
										 |  |  | from typing import Callable | 
					
						
							| 
									
										
										
										
											2024-04-07 15:52:00 +02:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-27 10:25:42 +01:00
										 |  |  | import flask | 
					
						
							|  |  |  | import babel | 
					
						
							| 
									
										
										
										
											2024-04-07 15:52:00 +02:00
										 |  |  | from flask_babel import gettext | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-21 17:50:44 +02:00
										 |  |  | from searx.plugins import logger | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-07 15:52:00 +02:00
										 |  |  | name = "Basic Calculator" | 
					
						
							|  |  |  | description = gettext("Calculate mathematical expressions via the search bar") | 
					
						
							| 
									
										
										
										
											2024-05-30 14:03:25 +02:00
										 |  |  | default_on = True | 
					
						
							| 
									
										
										
										
											2024-04-07 15:52:00 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | preference_section = 'general' | 
					
						
							|  |  |  | plugin_id = 'calculator' | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-21 17:50:44 +02:00
										 |  |  | logger = logger.getChild(plugin_id) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-05 16:10:56 +02:00
										 |  |  | operators: dict[type, Callable] = { | 
					
						
							| 
									
										
										
										
											2024-04-07 15:52:00 +02:00
										 |  |  |     ast.Add: operator.add, | 
					
						
							|  |  |  |     ast.Sub: operator.sub, | 
					
						
							|  |  |  |     ast.Mult: operator.mul, | 
					
						
							|  |  |  |     ast.Div: operator.truediv, | 
					
						
							|  |  |  |     ast.Pow: operator.pow, | 
					
						
							|  |  |  |     ast.BitXor: operator.xor, | 
					
						
							|  |  |  |     ast.USub: operator.neg, | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def _eval_expr(expr): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     >>> _eval_expr('2^6') | 
					
						
							|  |  |  |     4 | 
					
						
							|  |  |  |     >>> _eval_expr('2**6') | 
					
						
							|  |  |  |     64 | 
					
						
							|  |  |  |     >>> _eval_expr('1 + 2*3**(4^5) / (6 + -7)') | 
					
						
							|  |  |  |     -5.0 | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2024-10-05 16:10:56 +02:00
										 |  |  |     try: | 
					
						
							|  |  |  |         return _eval(ast.parse(expr, mode='eval').body) | 
					
						
							|  |  |  |     except ZeroDivisionError: | 
					
						
							|  |  |  |         # This is undefined | 
					
						
							|  |  |  |         return "" | 
					
						
							| 
									
										
										
										
											2024-04-07 15:52:00 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def _eval(node): | 
					
						
							| 
									
										
										
										
											2024-10-05 16:10:56 +02:00
										 |  |  |     if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)): | 
					
						
							| 
									
										
										
										
											2024-04-07 15:52:00 +02:00
										 |  |  |         return node.value | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if isinstance(node, ast.BinOp): | 
					
						
							|  |  |  |         return operators[type(node.op)](_eval(node.left), _eval(node.right)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     if isinstance(node, ast.UnaryOp): | 
					
						
							|  |  |  |         return operators[type(node.op)](_eval(node.operand)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     raise TypeError(node) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-21 17:50:44 +02:00
										 |  |  | def timeout_func(timeout, func, *args, **kwargs): | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def handler(q: Queue, func, args, **kwargs):  # pylint:disable=invalid-name | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             q.put(func(*args, **kwargs)) | 
					
						
							|  |  |  |         except: | 
					
						
							|  |  |  |             q.put(None) | 
					
						
							|  |  |  |             raise | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     que = Queue() | 
					
						
							|  |  |  |     p = Process(target=handler, args=(que, func, args), kwargs=kwargs) | 
					
						
							|  |  |  |     p.start() | 
					
						
							|  |  |  |     p.join(timeout=timeout) | 
					
						
							|  |  |  |     ret_val = None | 
					
						
							|  |  |  |     if not p.is_alive(): | 
					
						
							|  |  |  |         ret_val = que.get() | 
					
						
							|  |  |  |     else: | 
					
						
							|  |  |  |         logger.debug("terminate function after timeout is exceeded") | 
					
						
							|  |  |  |         p.terminate() | 
					
						
							|  |  |  |     p.join() | 
					
						
							|  |  |  |     p.close() | 
					
						
							|  |  |  |     return ret_val | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-07 15:52:00 +02:00
										 |  |  | def post_search(_request, search): | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # only show the result of the expression on the first page | 
					
						
							|  |  |  |     if search.search_query.pageno > 1: | 
					
						
							|  |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     query = search.search_query.query | 
					
						
							|  |  |  |     # in order to avoid DoS attacks with long expressions, ignore long expressions | 
					
						
							|  |  |  |     if len(query) > 100: | 
					
						
							|  |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # replace commonly used math operators with their proper Python operator | 
					
						
							|  |  |  |     query = query.replace("x", "*").replace(":", "/") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-27 10:25:42 +01:00
										 |  |  |     # use UI language | 
					
						
							|  |  |  |     ui_locale = babel.Locale.parse(flask.request.preferences.get_value('locale'), sep='-') | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-05 16:10:56 +02:00
										 |  |  |     # parse the number system in a localized way | 
					
						
							|  |  |  |     def _decimal(match: re.Match) -> str: | 
					
						
							|  |  |  |         val = match.string[match.start() : match.end()] | 
					
						
							| 
									
										
										
										
											2024-10-27 10:25:42 +01:00
										 |  |  |         val = babel.numbers.parse_decimal(val, ui_locale, numbering_system="latn") | 
					
						
							| 
									
										
										
										
											2024-10-05 16:10:56 +02:00
										 |  |  |         return str(val) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-27 10:25:42 +01:00
										 |  |  |     decimal = ui_locale.number_symbols["latn"]["decimal"] | 
					
						
							|  |  |  |     group = ui_locale.number_symbols["latn"]["group"] | 
					
						
							| 
									
										
										
										
											2024-10-05 16:10:56 +02:00
										 |  |  |     query = re.sub(f"[0-9]+[{decimal}|{group}][0-9]+[{decimal}|{group}]?[0-9]?", _decimal, query) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-04-07 15:52:00 +02:00
										 |  |  |     # only numbers and math operators are accepted | 
					
						
							|  |  |  |     if any(str.isalpha(c) for c in query): | 
					
						
							|  |  |  |         return True | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # in python, powers are calculated via ** | 
					
						
							|  |  |  |     query_py_formatted = query.replace("^", "**") | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-08-21 17:50:44 +02:00
										 |  |  |     # Prevent the runtime from being longer than 50 ms | 
					
						
							|  |  |  |     result = timeout_func(0.05, _eval_expr, query_py_formatted) | 
					
						
							| 
									
										
										
										
											2024-10-05 16:10:56 +02:00
										 |  |  |     if result is None or result == "": | 
					
						
							| 
									
										
										
										
											2024-08-21 17:50:44 +02:00
										 |  |  |         return True | 
					
						
							| 
									
										
										
										
											2024-10-27 10:25:42 +01:00
										 |  |  |     result = babel.numbers.format_decimal(result, locale=ui_locale) | 
					
						
							| 
									
										
										
										
											2024-10-05 16:10:56 +02:00
										 |  |  |     search.result_container.answers['calculate'] = {'answer': f"{search.search_query.query} = {result}"} | 
					
						
							| 
									
										
										
										
											2024-04-07 15:52:00 +02:00
										 |  |  |     return True |