feat: frappe.enqueue and frappe.call for server scripts
This commit is contained in:
parent
97cd221f24
commit
01f4ba2061
4 changed files with 101 additions and 18 deletions
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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]:
|
||||
|
|
|
|||
|
|
@ -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)""")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue