diff --git a/frappe/core/doctype/server_script/server_script_utils.py b/frappe/core/doctype/server_script/server_script_utils.py index b4cfdf0a17..d8c945fb6d 100644 --- a/frappe/core/doctype/server_script/server_script_utils.py +++ b/frappe/core/doctype/server_script/server_script_utils.py @@ -19,13 +19,6 @@ EVENT_MAP = { 'on_update_after_submit': 'After Save (Submitted Document)' } -def run_server_script_api(method): - # called via handler, execute an API script - script_name = get_server_script_map().get('_api', {}).get(method) - if script_name: - frappe.get_doc('Server Script', script_name).execute_method() - return True - def run_server_script_for_doc_event(doc, event): # run document event method if not event in EVENT_MAP: diff --git a/frappe/handler.py b/frappe/handler.py index 2c2b5723fa..3fd1c096e4 100755 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -12,7 +12,7 @@ from frappe.utils.response import build_response from frappe.utils.csvutils import build_csv_response from frappe.utils.image import optimize_image from mimetypes import guess_type -from frappe.core.doctype.server_script.server_script_utils import run_server_script_api +from frappe.core.doctype.server_script.server_script_utils import get_server_script_map ALLOWED_MIMETYPES = ('image/png', 'image/jpeg', 'application/pdf', 'application/msword', @@ -49,8 +49,9 @@ def execute_cmd(cmd, from_async=False): break # via server script - if run_server_script_api(cmd): - return None + server_script = get_server_script_map().get('_api', {}).get(cmd) + if server_script: + return run_server_script(server_script) try: method = get_attr(cmd) @@ -66,7 +67,20 @@ def execute_cmd(cmd, from_async=False): return frappe.call(method, **frappe.form_dict) + +def run_server_script(server_script): + response = frappe.get_doc('Server Script', server_script).execute_method() + + # some server scripts return output using flags (empty dict by default), + # while others directly modify frappe.response + # return flags if not empty dict (this overwrites frappe.response.message) + if response != {}: + return response + def is_valid_http_method(method): + if frappe.flags.in_safe_exec: + return + http_method = frappe.local.request.method if http_method not in frappe.allowed_http_methods_for_whitelisted_func[method]: diff --git a/frappe/tests/test_safe_exec.py b/frappe/tests/test_safe_exec.py index 783d05ff37..7fec292c49 100644 --- a/frappe/tests/test_safe_exec.py +++ b/frappe/tests/test_safe_exec.py @@ -31,4 +31,27 @@ class TestSafeExec(unittest.TestCase): self.assertEqual(frappe.db.sql("SELECT Max(name) FROM tabUser"), _locals["out"]) def test_safe_query_builder(self): - self.assertRaises(frappe.PermissionError, safe_exec, '''frappe.qb.from_("User").delete().run()''') \ No newline at end of file + self.assertRaises(frappe.PermissionError, safe_exec, '''frappe.qb.from_("User").delete().run()''') + + def test_call(self): + # call non whitelisted method + self.assertRaises( + frappe.PermissionError, + safe_exec, + """frappe.call("frappe.get_user")""" + ) + + # call whitelisted method + safe_exec("""frappe.call("ping")""") + + + def test_enqueue(self): + # enqueue non whitelisted method + self.assertRaises( + frappe.PermissionError, + safe_exec, + """frappe.enqueue("frappe.get_user", now=True)""" + ) + + # enqueue whitelisted method + safe_exec("""frappe.enqueue("ping", now=True)""") diff --git a/frappe/utils/safe_exec.py b/frappe/utils/safe_exec.py index f51efefc85..2042c1f2ce 100644 --- a/frappe/utils/safe_exec.py +++ b/frappe/utils/safe_exec.py @@ -14,11 +14,12 @@ import frappe.integrations.utils import frappe.utils import frappe.utils.data from frappe import _ +from frappe.handler import execute_cmd from frappe.frappeclient import FrappeClient from frappe.modules import scrub from frappe.website.utils import get_next_link, get_shade, get_toc from frappe.www.printview import get_visible_columns - +from frappe.utils.background_jobs import enqueue, get_jobs class ServerScriptNotEnabled(frappe.PermissionError): pass @@ -74,7 +75,9 @@ def get_safe_globals(): add_data_utils(datautils) - if "_" in getattr(frappe.local, 'form_dict', {}): + form_dict = getattr(frappe.local, 'form_dict', frappe._dict()) + + if "_" in form_dict: del frappe.local.form_dict["_"] user = getattr(frappe.local, "session", None) and frappe.local.session.user or "Guest" @@ -89,14 +92,16 @@ def get_safe_globals(): dict=dict, log=frappe.log, _dict=frappe._dict, + args=form_dict, frappe=NamespaceDict( + call=call_whitelisted_function, flags=frappe._dict(), format=frappe.format_value, format_value=frappe.format_value, date_format=date_format, time_format=time_format, format_date=frappe.utils.data.global_date_format, - form_dict=getattr(frappe.local, 'form_dict', {}), + form_dict=form_dict, bold=frappe.bold, copy_doc=frappe.copy_doc, errprint=frappe.errprint, @@ -132,6 +137,7 @@ def get_safe_globals(): make_post_request=frappe.integrations.utils.make_post_request, socketio_port=frappe.conf.socketio_port, get_hooks=get_hooks, + enqueue=safe_enqueue, sanitize_html=frappe.utils.sanitize_html, log_error=frappe.log_error ), @@ -147,7 +153,8 @@ def get_safe_globals(): guess_mimetype=mimetypes.guess_type, html2text=html2text, dev_server=1 if frappe._dev_server else 0, - run_script=run_script + run_script=run_script, + is_job_queued=is_job_queued, ) add_module_properties(frappe.exceptions, out.frappe, lambda obj: inspect.isclass(obj) and issubclass(obj, Exception)) @@ -190,6 +197,55 @@ def get_safe_globals(): return out +def is_job_queued(job_name, queue="default"): + ''' + :param job_name: used to identify a queued job, usually dotted path to function + :param queue: should be either long, default or short + ''' + + site = frappe.local.site + queued_jobs = get_jobs(site=site, queue=queue, key='job_name').get(site) + return queued_jobs and job_name in queued_jobs + +def safe_enqueue(function, **kwargs): + ''' + Enqueue function to be executed using a background worker + Accepts frappe.enqueue params like job_name, queue, timeout, etc. + in addition to params to be passed to function + + :param function: whitelised function or API Method set in Server Script + ''' + + return enqueue( + 'frappe.utils.safe_exec.call_whitelisted_function', + function=function, + **kwargs + ) + +def call_whitelisted_function(function, **kwargs): + '''Executes a whitelisted function or Server Script of type API''' + + return call_with_form_dict(lambda: execute_cmd(function), kwargs) + +def run_script(script, **kwargs): + '''run another server script''' + + return call_with_form_dict( + lambda: frappe.get_doc('Server Script', script).execute_method(), + kwargs + ) + +def call_with_form_dict(function, kwargs): + # temporarily update form_dict, to use inside below call + form_dict = getattr(frappe.local, 'form_dict', frappe._dict()) + if kwargs: + frappe.local.form_dict = form_dict.copy().update(kwargs) + + try: + return function() + finally: + frappe.local.form_dict = form_dict + def get_python_builtins(): return { 'abs': abs, @@ -221,9 +277,6 @@ def read_sql(query, *args, **kwargs): raise frappe.PermissionError('Only SELECT SQL allowed in scripting') return frappe.db.sql(query, *args, **kwargs) -def run_script(script): - '''run another server script''' - return frappe.get_doc('Server Script', script).execute_method() def _getitem(obj, key): # guard function for RestrictedPython