소스 검색

Merge pull request #15407 from resilient-tech/server-scripts

feat: `frappe.enqueue` and `frappe.call` for Server Scripts
version-14
mergify[bot] 3 년 전
committed by GitHub
부모
커밋
1b28c3b4ed
No known key found for this signature in database GPG 키 ID: 4AEE18F83AFDEB23
4개의 변경된 파일101개의 추가작업 그리고 18개의 파일을 삭제
  1. +0
    -7
      frappe/core/doctype/server_script/server_script_utils.py
  2. +17
    -3
      frappe/handler.py
  3. +24
    -1
      frappe/tests/test_safe_exec.py
  4. +60
    -7
      frappe/utils/safe_exec.py

+ 0
- 7
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:


+ 17
- 3
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]:


+ 24
- 1
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()''')
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)""")

+ 60
- 7
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


불러오는 중...
취소
저장