437 lines
11 KiB
Python
437 lines
11 KiB
Python
|
|
import copy
|
|
import inspect
|
|
import json
|
|
import mimetypes
|
|
|
|
import RestrictedPython.Guards
|
|
from html2text import html2text
|
|
from RestrictedPython import compile_restricted, safe_globals
|
|
|
|
import frappe
|
|
import frappe.exceptions
|
|
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
|
|
|
|
|
|
class NamespaceDict(frappe._dict):
|
|
"""Raise AttributeError if function not found in namespace"""
|
|
def __getattr__(self, key):
|
|
ret = self.get(key)
|
|
if (not ret and key.startswith("__")) or (key not in self):
|
|
def default_function(*args, **kwargs):
|
|
raise AttributeError(f"module has no attribute '{key}'")
|
|
return default_function
|
|
return ret
|
|
|
|
|
|
def safe_exec(script, _globals=None, _locals=None, restrict_commit_rollback=False):
|
|
# server scripts can be disabled via site_config.json
|
|
# they are enabled by default
|
|
if 'server_script_enabled' in frappe.conf:
|
|
enabled = frappe.conf.server_script_enabled
|
|
else:
|
|
enabled = True
|
|
|
|
if not enabled:
|
|
frappe.throw(_('Please Enable Server Scripts'), ServerScriptNotEnabled)
|
|
|
|
# build globals
|
|
exec_globals = get_safe_globals()
|
|
if _globals:
|
|
exec_globals.update(_globals)
|
|
|
|
if restrict_commit_rollback:
|
|
exec_globals.frappe.db.pop('commit', None)
|
|
exec_globals.frappe.db.pop('rollback', None)
|
|
|
|
# execute script compiled by RestrictedPython
|
|
frappe.flags.in_safe_exec = True
|
|
exec(compile_restricted(script), exec_globals, _locals) # pylint: disable=exec-used
|
|
frappe.flags.in_safe_exec = False
|
|
|
|
return exec_globals, _locals
|
|
|
|
def get_safe_globals():
|
|
datautils = frappe._dict()
|
|
|
|
if frappe.db:
|
|
date_format = frappe.db.get_default("date_format") or "yyyy-mm-dd"
|
|
time_format = frappe.db.get_default("time_format") or "HH:mm:ss"
|
|
else:
|
|
date_format = "yyyy-mm-dd"
|
|
time_format = "HH:mm:ss"
|
|
|
|
add_data_utils(datautils)
|
|
|
|
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"
|
|
|
|
out = NamespaceDict(
|
|
# make available limited methods of frappe
|
|
json=NamespaceDict(
|
|
loads=json.loads,
|
|
dumps=json.dumps
|
|
),
|
|
as_json=frappe.as_json,
|
|
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=form_dict,
|
|
bold=frappe.bold,
|
|
copy_doc=frappe.copy_doc,
|
|
errprint=frappe.errprint,
|
|
qb=frappe.qb,
|
|
|
|
get_meta=frappe.get_meta,
|
|
get_doc=frappe.get_doc,
|
|
get_cached_doc=frappe.get_cached_doc,
|
|
get_list=frappe.get_list,
|
|
get_all=frappe.get_all,
|
|
get_system_settings=frappe.get_system_settings,
|
|
rename_doc=frappe.rename_doc,
|
|
|
|
utils=datautils,
|
|
get_url=frappe.utils.get_url,
|
|
render_template=frappe.render_template,
|
|
msgprint=frappe.msgprint,
|
|
throw=frappe.throw,
|
|
sendmail=frappe.sendmail,
|
|
get_print=frappe.get_print,
|
|
attach_print=frappe.attach_print,
|
|
|
|
user=user,
|
|
get_fullname=frappe.utils.get_fullname,
|
|
get_gravatar=frappe.utils.get_gravatar_url,
|
|
full_name=frappe.local.session.data.full_name if getattr(frappe.local, "session", None) else "Guest",
|
|
request=getattr(frappe.local, 'request', {}),
|
|
session=frappe._dict(
|
|
user=user,
|
|
csrf_token=frappe.local.session.data.csrf_token if getattr(frappe.local, "session", None) else ''
|
|
),
|
|
make_get_request=frappe.integrations.utils.make_get_request,
|
|
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
|
|
),
|
|
FrappeClient=FrappeClient,
|
|
style=frappe._dict(
|
|
border_color='#d1d8dd'
|
|
),
|
|
get_toc=get_toc,
|
|
get_next_link=get_next_link,
|
|
_=frappe._,
|
|
get_shade=get_shade,
|
|
scrub=scrub,
|
|
guess_mimetype=mimetypes.guess_type,
|
|
html2text=html2text,
|
|
dev_server=1 if frappe._dev_server else 0,
|
|
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))
|
|
|
|
if not frappe.flags.in_setup_help:
|
|
out.get_visible_columns = get_visible_columns
|
|
out.frappe.date_format = date_format
|
|
out.frappe.time_format = time_format
|
|
out.frappe.db = NamespaceDict(
|
|
get_list=frappe.get_list,
|
|
get_all=frappe.get_all,
|
|
get_value=frappe.db.get_value,
|
|
set_value=frappe.db.set_value,
|
|
get_single_value=frappe.db.get_single_value,
|
|
get_default=frappe.db.get_default,
|
|
exists=frappe.db.exists,
|
|
count=frappe.db.count,
|
|
escape=frappe.db.escape,
|
|
sql=read_sql,
|
|
commit=frappe.db.commit,
|
|
rollback=frappe.db.rollback,
|
|
)
|
|
|
|
if frappe.response:
|
|
out.frappe.response = frappe.response
|
|
|
|
out.update(safe_globals)
|
|
|
|
# default writer allows write access
|
|
out._write_ = _write
|
|
out._getitem_ = _getitem
|
|
out._getattr_ = _getattr
|
|
|
|
# allow iterators and list comprehension
|
|
out._getiter_ = iter
|
|
out._iter_unpack_sequence_ = RestrictedPython.Guards.guarded_iter_unpack_sequence
|
|
|
|
# add common python builtins
|
|
out.update(get_python_builtins())
|
|
|
|
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,
|
|
'all': all,
|
|
'any': any,
|
|
'bool': bool,
|
|
'dict': dict,
|
|
'enumerate': enumerate,
|
|
'isinstance': isinstance,
|
|
'issubclass': issubclass,
|
|
'list': list,
|
|
'max': max,
|
|
'min': min,
|
|
'range': range,
|
|
'set': set,
|
|
'sorted': sorted,
|
|
'sum': sum,
|
|
'tuple': tuple,
|
|
}
|
|
|
|
def get_hooks(hook=None, default=None, app_name=None):
|
|
hooks = frappe.get_hooks(hook=hook, default=default, app_name=app_name)
|
|
return copy.deepcopy(hooks)
|
|
|
|
def read_sql(query, *args, **kwargs):
|
|
'''a wrapper for frappe.db.sql to allow reads'''
|
|
query = str(query)
|
|
if frappe.flags.in_safe_exec and not query.strip().lower().startswith('select'):
|
|
raise frappe.PermissionError('Only SELECT SQL allowed in scripting')
|
|
return frappe.db.sql(query, *args, **kwargs)
|
|
|
|
|
|
def _getitem(obj, key):
|
|
# guard function for RestrictedPython
|
|
# allow any key to be accessed as long as it does not start with underscore
|
|
if isinstance(key, str) and key.startswith('_'):
|
|
raise SyntaxError('Key starts with _')
|
|
return obj[key]
|
|
|
|
def _getattr(object, name, default=None):
|
|
# guard function for RestrictedPython
|
|
# allow any key to be accessed as long as
|
|
# 1. it does not start with an underscore (safer_getattr)
|
|
# 2. it is not an UNSAFE_ATTRIBUTES
|
|
|
|
UNSAFE_ATTRIBUTES = {
|
|
# Generator Attributes
|
|
"gi_frame", "gi_code",
|
|
# Coroutine Attributes
|
|
"cr_frame", "cr_code", "cr_origin",
|
|
# Async Generator Attributes
|
|
"ag_code", "ag_frame",
|
|
# Traceback Attributes
|
|
"tb_frame", "tb_next",
|
|
}
|
|
|
|
if isinstance(name, str) and (name in UNSAFE_ATTRIBUTES):
|
|
raise SyntaxError("{name} is an unsafe attribute".format(name=name))
|
|
return RestrictedPython.Guards.safer_getattr(object, name, default=default)
|
|
|
|
def _write(obj):
|
|
# guard function for RestrictedPython
|
|
# allow writing to any object
|
|
return obj
|
|
|
|
def add_data_utils(data):
|
|
for key, obj in frappe.utils.data.__dict__.items():
|
|
if key in VALID_UTILS:
|
|
data[key] = obj
|
|
|
|
def add_module_properties(module, data, filter_method):
|
|
for key, obj in module.__dict__.items():
|
|
if key.startswith("_"):
|
|
# ignore
|
|
continue
|
|
|
|
if filter_method(obj):
|
|
# only allow functions
|
|
data[key] = obj
|
|
|
|
VALID_UTILS = (
|
|
"DATE_FORMAT",
|
|
"TIME_FORMAT",
|
|
"DATETIME_FORMAT",
|
|
"is_invalid_date_string",
|
|
"getdate",
|
|
"get_datetime",
|
|
"to_timedelta",
|
|
"get_timedelta",
|
|
"add_to_date",
|
|
"add_days",
|
|
"add_months",
|
|
"add_years",
|
|
"date_diff",
|
|
"month_diff",
|
|
"time_diff",
|
|
"time_diff_in_seconds",
|
|
"time_diff_in_hours",
|
|
"now_datetime",
|
|
"get_timestamp",
|
|
"get_eta",
|
|
"get_time_zone",
|
|
"convert_utc_to_user_timezone",
|
|
"now",
|
|
"nowdate",
|
|
"today",
|
|
"nowtime",
|
|
"get_first_day",
|
|
"get_quarter_start",
|
|
"get_first_day_of_week",
|
|
"get_year_start",
|
|
"get_last_day_of_week",
|
|
"get_last_day",
|
|
"get_time",
|
|
"get_datetime_in_timezone",
|
|
"get_datetime_str",
|
|
"get_date_str",
|
|
"get_time_str",
|
|
"get_user_date_format",
|
|
"get_user_time_format",
|
|
"format_date",
|
|
"format_time",
|
|
"format_datetime",
|
|
"format_duration",
|
|
"get_weekdays",
|
|
"get_weekday",
|
|
"get_timespan_date_range",
|
|
"global_date_format",
|
|
"has_common",
|
|
"flt",
|
|
"cint",
|
|
"floor",
|
|
"ceil",
|
|
"cstr",
|
|
"rounded",
|
|
"remainder",
|
|
"safe_div",
|
|
"round_based_on_smallest_currency_fraction",
|
|
"encode",
|
|
"parse_val",
|
|
"fmt_money",
|
|
"get_number_format_info",
|
|
"money_in_words",
|
|
"in_words",
|
|
"is_html",
|
|
"is_image",
|
|
"get_thumbnail_base64_for_image",
|
|
"image_to_base64",
|
|
"pdf_to_base64",
|
|
"strip_html",
|
|
"escape_html",
|
|
"pretty_date",
|
|
"comma_or",
|
|
"comma_and",
|
|
"comma_sep",
|
|
"new_line_sep",
|
|
"filter_strip_join",
|
|
"get_url",
|
|
"get_host_name_from_request",
|
|
"url_contains_port",
|
|
"get_host_name",
|
|
"get_link_to_form",
|
|
"get_link_to_report",
|
|
"get_absolute_url",
|
|
"get_url_to_form",
|
|
"get_url_to_list",
|
|
"get_url_to_report",
|
|
"get_url_to_report_with_filters",
|
|
"evaluate_filters",
|
|
"compare",
|
|
"get_filter",
|
|
"make_filter_tuple",
|
|
"make_filter_dict",
|
|
"sanitize_column",
|
|
"scrub_urls",
|
|
"expand_relative_urls",
|
|
"quoted",
|
|
"quote_urls",
|
|
"unique",
|
|
"strip",
|
|
"to_markdown",
|
|
"md_to_html",
|
|
"markdown",
|
|
"is_subset",
|
|
"generate_hash",
|
|
"formatdate",
|
|
"get_user_info_for_avatar",
|
|
"get_abbr"
|
|
)
|