feat: frappe.enqueue and frappe.call for server scripts

This commit is contained in:
Sagar Vora 2021-12-22 12:58:06 +05:30
parent 97cd221f24
commit 01f4ba2061
4 changed files with 101 additions and 18 deletions

View file

@ -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:

View file

@ -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]:

View file

@ -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()''')
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)""")

View file

@ -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