This is middle ground between caching it completely and requiring a restart/signal to reload vs always reloading it. I don't know any use cases that can break from this, nowhere in code configs should be expected to reload instantly. This change is only applied to requests for now
730 lines
18 KiB
Python
730 lines
18 KiB
Python
import ast
|
|
import copy
|
|
import inspect
|
|
import io
|
|
import json
|
|
import mimetypes
|
|
import types
|
|
from contextlib import contextmanager
|
|
from functools import lru_cache
|
|
from itertools import chain
|
|
from types import FunctionType, MethodType, ModuleType
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
import RestrictedPython.Guards
|
|
from RestrictedPython import PrintCollector, compile_restricted, safe_globals
|
|
from RestrictedPython.transformer import RestrictingNodeTransformer
|
|
|
|
import frappe
|
|
import frappe.exceptions
|
|
import frappe.integrations.utils
|
|
import frappe.utils
|
|
import frappe.utils.data
|
|
from frappe import _
|
|
from frappe.core.utils import html2text
|
|
from frappe.frappeclient import FrappeClient
|
|
from frappe.handler import execute_cmd
|
|
from frappe.locale import get_date_format, get_number_format, get_time_format
|
|
from frappe.model.delete_doc import delete_doc
|
|
from frappe.model.mapper import get_mapped_doc
|
|
from frappe.model.rename_doc import rename_doc
|
|
from frappe.modules import scrub
|
|
from frappe.utils.background_jobs import enqueue, get_jobs
|
|
from frappe.utils.number_format import NumberFormat
|
|
from frappe.website.utils import get_next_link, get_toc
|
|
from frappe.www.printview import get_visible_columns
|
|
|
|
|
|
class ServerScriptNotEnabled(frappe.PermissionError):
|
|
pass
|
|
|
|
|
|
ARGUMENT_NOT_SET = object()
|
|
|
|
SAFE_EXEC_CONFIG_KEY = "server_script_enabled"
|
|
SERVER_SCRIPT_FILE_PREFIX = "<serverscript>"
|
|
|
|
|
|
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
|
|
|
|
|
|
class FrappeTransformer(RestrictingNodeTransformer):
|
|
def check_name(self, node, name, *args, **kwargs):
|
|
if name == "_dict":
|
|
return
|
|
|
|
return super().check_name(node, name, *args, **kwargs)
|
|
|
|
|
|
class FrappePrintCollector(PrintCollector):
|
|
"""Collect written text, and return it when called."""
|
|
|
|
def _call_print(self, *objects, **kwargs):
|
|
output = io.StringIO()
|
|
print(*objects, file=output, **kwargs)
|
|
frappe.log(output.getvalue().strip())
|
|
output.close()
|
|
|
|
|
|
def is_safe_exec_enabled() -> bool:
|
|
# server scripts can only be enabled via common_site_config.json
|
|
return bool(frappe.get_common_site_config(cached=bool(frappe.request)).get(SAFE_EXEC_CONFIG_KEY))
|
|
|
|
|
|
def safe_exec(
|
|
script: str,
|
|
_globals: dict | None = None,
|
|
_locals: dict | None = None,
|
|
*,
|
|
restrict_commit_rollback: bool = False,
|
|
script_filename: str | None = None,
|
|
):
|
|
if not is_safe_exec_enabled():
|
|
msg = _("Server Scripts are disabled. Please enable server scripts from bench configuration.")
|
|
docs_cta = _("Read the documentation to know more")
|
|
msg += f"<br><a href='https://frappeframework.com/docs/user/en/desk/scripting/server-script'>{docs_cta}</a>"
|
|
frappe.throw(msg, ServerScriptNotEnabled, title="Server Scripts Disabled")
|
|
|
|
# build globals
|
|
exec_globals = get_safe_globals()
|
|
if _globals:
|
|
exec_globals.update(_globals)
|
|
|
|
if restrict_commit_rollback:
|
|
# prevent user from using these in docevents
|
|
exec_globals.frappe.db.pop("commit", None)
|
|
exec_globals.frappe.db.pop("rollback", None)
|
|
exec_globals.frappe.db.pop("add_index", None)
|
|
|
|
filename = SERVER_SCRIPT_FILE_PREFIX
|
|
if script_filename:
|
|
filename += f": {frappe.scrub(script_filename)}"
|
|
|
|
with safe_exec_flags(), patched_qb():
|
|
# execute script compiled by RestrictedPython
|
|
exec(
|
|
compile_restricted(script, filename=filename, policy=FrappeTransformer),
|
|
exec_globals,
|
|
_locals,
|
|
)
|
|
|
|
return exec_globals, _locals
|
|
|
|
|
|
def safe_eval(code, eval_globals=None, eval_locals=None):
|
|
import unicodedata
|
|
|
|
code = unicodedata.normalize("NFKC", code)
|
|
|
|
_validate_safe_eval_syntax(code)
|
|
|
|
if not eval_globals:
|
|
eval_globals = {}
|
|
|
|
eval_globals["__builtins__"] = {}
|
|
eval_globals.update(WHITELISTED_SAFE_EVAL_GLOBALS)
|
|
|
|
return eval(
|
|
compile_restricted(code, filename="<safe_eval>", policy=FrappeTransformer, mode="eval"),
|
|
eval_globals,
|
|
eval_locals,
|
|
)
|
|
|
|
|
|
def _validate_safe_eval_syntax(code):
|
|
BLOCKED_NODES = (ast.NamedExpr,)
|
|
|
|
tree = ast.parse(code, mode="eval")
|
|
for node in ast.walk(tree):
|
|
if isinstance(node, BLOCKED_NODES):
|
|
raise SyntaxError(f"Operation not allowed: line {node.lineno} column {node.col_offset}")
|
|
|
|
|
|
@contextmanager
|
|
def safe_exec_flags():
|
|
if frappe.flags.in_safe_exec is None:
|
|
frappe.flags.in_safe_exec = 0
|
|
|
|
frappe.flags.in_safe_exec += 1
|
|
|
|
try:
|
|
yield
|
|
finally:
|
|
# Always ensure that the flag is decremented
|
|
frappe.flags.in_safe_exec -= 1
|
|
|
|
|
|
def get_safe_globals():
|
|
datautils = frappe._dict()
|
|
|
|
if frappe.db:
|
|
date_format = get_date_format()
|
|
time_format = get_time_format()
|
|
number_format = get_number_format()
|
|
else:
|
|
date_format = "yyyy-mm-dd"
|
|
time_format = "HH:mm:ss"
|
|
number_format = NumberFormat.from_string("#,###.##")
|
|
|
|
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,
|
|
number_format=number_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,
|
|
new_doc=frappe.new_doc,
|
|
get_doc=frappe.get_doc,
|
|
get_mapped_doc=get_mapped_doc,
|
|
get_last_doc=frappe.get_last_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=rename_doc,
|
|
delete_doc=delete_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,
|
|
make_put_request=frappe.integrations.utils.make_put_request,
|
|
make_patch_request=frappe.integrations.utils.make_patch_request,
|
|
make_delete_request=frappe.integrations.utils.make_delete_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,
|
|
log=frappe.log,
|
|
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,
|
|
after_commit=frappe.db.after_commit,
|
|
before_commit=frappe.db.before_commit,
|
|
after_rollback=frappe.db.after_rollback,
|
|
before_rollback=frappe.db.before_rollback,
|
|
add_index=frappe.db.add_index,
|
|
),
|
|
website=NamespaceDict(
|
|
abs_url=frappe.website.utils.abs_url,
|
|
extract_title=frappe.website.utils.extract_title,
|
|
get_boot_data=frappe.website.utils.get_boot_data,
|
|
get_home_page=frappe.website.utils.get_home_page,
|
|
get_html_content_based_on_type=frappe.website.utils.get_html_content_based_on_type,
|
|
),
|
|
lang=getattr(frappe.local, "lang", "en"),
|
|
),
|
|
FrappeClient=FrappeClient,
|
|
style=frappe._dict(border_color="#d1d8dd"),
|
|
get_toc=get_toc,
|
|
get_next_link=get_next_link,
|
|
_=frappe._,
|
|
scrub=scrub,
|
|
guess_mimetype=mimetypes.guess_type,
|
|
html2text=html2text,
|
|
dev_server=frappe.local.dev_server,
|
|
run_script=run_script,
|
|
is_job_queued=is_job_queued,
|
|
get_visible_columns=get_visible_columns,
|
|
)
|
|
|
|
add_module_properties(
|
|
frappe.exceptions, out.frappe, lambda obj: inspect.isclass(obj) and issubclass(obj, Exception)
|
|
)
|
|
|
|
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_for_safe_exec
|
|
|
|
# Allow using `print()` calls with `safe_exec()`
|
|
out._print_ = FrappePrintCollector
|
|
|
|
# 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 get_keys_for_autocomplete(
|
|
key: str,
|
|
value: Any,
|
|
prefix: str = "",
|
|
offset: int = 0,
|
|
meta: str = "ctx",
|
|
depth: int = 0,
|
|
max_depth: int | None = None,
|
|
):
|
|
if max_depth and depth > max_depth:
|
|
return
|
|
full_key = f"{prefix}.{key}" if prefix else key
|
|
if key.startswith("_"):
|
|
return
|
|
if isinstance(value, NamespaceDict | dict) and value:
|
|
if key == "form_dict":
|
|
yield {"value": full_key, "score": offset + 7, "meta": meta}
|
|
else:
|
|
yield from chain.from_iterable(
|
|
get_keys_for_autocomplete(
|
|
key,
|
|
value,
|
|
full_key,
|
|
offset,
|
|
meta,
|
|
depth + 1,
|
|
max_depth=max_depth,
|
|
)
|
|
for key, value in value.items()
|
|
)
|
|
else:
|
|
if isinstance(value, type) and issubclass(value, Exception):
|
|
score = offset + 0
|
|
elif isinstance(value, ModuleType):
|
|
score = offset + 10
|
|
elif isinstance(value, FunctionType | MethodType):
|
|
score = offset + 9
|
|
elif isinstance(value, type):
|
|
score = offset + 8
|
|
elif isinstance(value, dict):
|
|
score = offset + 7
|
|
else:
|
|
score = offset + 6
|
|
yield {"value": full_key, "score": score, "meta": meta}
|
|
|
|
|
|
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: whitelisted 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
|
|
|
|
|
|
@contextmanager
|
|
def patched_qb():
|
|
require_patching = isinstance(frappe.qb.terms, types.ModuleType)
|
|
try:
|
|
if require_patching:
|
|
_terms = frappe.qb.terms
|
|
frappe.qb.terms = _flatten(frappe.qb.terms)
|
|
yield
|
|
finally:
|
|
if require_patching:
|
|
frappe.qb.terms = _terms
|
|
|
|
|
|
@lru_cache
|
|
def _flatten(module):
|
|
new_mod = NamespaceDict()
|
|
for name, obj in inspect.getmembers(module, lambda x: not inspect.ismodule(x)):
|
|
if not name.startswith("_"):
|
|
new_mod[name] = obj
|
|
return new_mod
|
|
|
|
|
|
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: str | None = None, default=None, app_name: str | None = None) -> frappe._dict:
|
|
"""Get hooks via `app/hooks.py`
|
|
|
|
:param hook: Name of the hook. Will gather all hooks for this name and return as a list.
|
|
:param default: Default if no hook found.
|
|
:param app_name: Filter by app."""
|
|
|
|
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)
|
|
check_safe_sql_query(query)
|
|
return frappe.db.sql(query, *args, **kwargs)
|
|
|
|
|
|
def check_safe_sql_query(query: str, throw: bool = True) -> bool:
|
|
"""Check if SQL query is safe for running in restricted context.
|
|
|
|
Safe queries:
|
|
1. Read only 'select' or 'explain' queries
|
|
2. CTE on mariadb where writes are not allowed.
|
|
"""
|
|
|
|
query = query.strip().lower()
|
|
whitelisted_statements = ("select", "explain")
|
|
|
|
if query.startswith(whitelisted_statements) or (
|
|
query.startswith("with") and frappe.db.db_type == "mariadb"
|
|
):
|
|
return True
|
|
|
|
if throw:
|
|
frappe.throw(
|
|
_("Query must be of SELECT or read-only WITH type."),
|
|
title=_("Unsafe SQL query"),
|
|
exc=frappe.PermissionError,
|
|
)
|
|
|
|
return False
|
|
|
|
|
|
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]
|
|
|
|
|
|
UNSAFE_ATTRIBUTES = {
|
|
# Generator Attributes
|
|
"gi_frame",
|
|
"gi_code",
|
|
"gi_yieldfrom",
|
|
# Coroutine Attributes
|
|
"cr_frame",
|
|
"cr_code",
|
|
"cr_origin",
|
|
"cr_await",
|
|
# Async Generator Attributes
|
|
"ag_code",
|
|
"ag_frame",
|
|
# Traceback Attributes
|
|
"tb_frame",
|
|
"tb_next",
|
|
# Format Attributes
|
|
"format",
|
|
"format_map",
|
|
# Frame attributes
|
|
"f_back",
|
|
"f_builtins",
|
|
"f_code",
|
|
"f_globals",
|
|
"f_locals",
|
|
"f_trace",
|
|
}
|
|
|
|
|
|
def _getattr_for_safe_exec(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
|
|
_validate_attribute_read(object, name)
|
|
|
|
return RestrictedPython.Guards.safer_getattr(object, name, default=default)
|
|
|
|
|
|
def _get_attr_for_eval(object, name, default=ARGUMENT_NOT_SET):
|
|
_validate_attribute_read(object, name)
|
|
|
|
# Use vanilla getattr to raise correct attribute error. Safe exec has been supressing attribute
|
|
# error which is bad for DX/UX in general.
|
|
return getattr(object, name) if default is ARGUMENT_NOT_SET else getattr(object, name, default)
|
|
|
|
|
|
def _validate_attribute_read(object, name):
|
|
if isinstance(name, str) and (name in UNSAFE_ATTRIBUTES):
|
|
raise SyntaxError(f"{name} is an unsafe attribute")
|
|
|
|
if isinstance(object, types.ModuleType | types.CodeType | types.TracebackType | types.FrameType):
|
|
raise SyntaxError(f"Reading {object} attributes is not allowed")
|
|
|
|
if name.startswith("_"):
|
|
raise AttributeError(f'"{name}" is an invalid attribute name because it ' 'starts with "_"')
|
|
|
|
|
|
def _write(obj):
|
|
# guard function for RestrictedPython
|
|
if isinstance(
|
|
obj,
|
|
types.ModuleType
|
|
| types.CodeType
|
|
| types.TracebackType
|
|
| types.FrameType
|
|
| type
|
|
| types.FunctionType
|
|
| types.MethodType
|
|
| types.BuiltinFunctionType,
|
|
):
|
|
raise SyntaxError(f"Not allowed to write to object {obj} of type {type(obj)}")
|
|
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_system_timezone",
|
|
"convert_utc_to_system_timezone",
|
|
"now",
|
|
"nowdate",
|
|
"today",
|
|
"nowtime",
|
|
"get_first_day",
|
|
"get_quarter_start",
|
|
"get_quarter_ending",
|
|
"get_first_day_of_week",
|
|
"get_year_start",
|
|
"get_year_ending",
|
|
"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",
|
|
"add_trackers_to_url",
|
|
"parse_and_map_trackers_from_url",
|
|
"map_trackers",
|
|
"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",
|
|
"get_month",
|
|
)
|
|
|
|
|
|
WHITELISTED_SAFE_EVAL_GLOBALS = {
|
|
"int": int,
|
|
"float": float,
|
|
"long": int,
|
|
"round": round,
|
|
# RestrictedPython specific overrides
|
|
"_getattr_": _get_attr_for_eval,
|
|
"_getitem_": _getitem,
|
|
"_getiter_": iter,
|
|
"_iter_unpack_sequence_": RestrictedPython.Guards.guarded_iter_unpack_sequence,
|
|
}
|