選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。
 
 
 
 
 
 

456 行
11 KiB

  1. import copy
  2. import inspect
  3. import json
  4. import mimetypes
  5. import RestrictedPython.Guards
  6. from html2text import html2text
  7. from RestrictedPython import compile_restricted, safe_globals
  8. import frappe
  9. import frappe.exceptions
  10. import frappe.integrations.utils
  11. import frappe.utils
  12. import frappe.utils.data
  13. from frappe import _
  14. from frappe.handler import execute_cmd
  15. from frappe.frappeclient import FrappeClient
  16. from frappe.modules import scrub
  17. from frappe.website.utils import get_next_link, get_shade, get_toc
  18. from frappe.www.printview import get_visible_columns
  19. from frappe.utils.background_jobs import enqueue, get_jobs
  20. class ServerScriptNotEnabled(frappe.PermissionError):
  21. pass
  22. class NamespaceDict(frappe._dict):
  23. """Raise AttributeError if function not found in namespace"""
  24. def __getattr__(self, key):
  25. ret = self.get(key)
  26. if (not ret and key.startswith("__")) or (key not in self):
  27. def default_function(*args, **kwargs):
  28. raise AttributeError(f"module has no attribute '{key}'")
  29. return default_function
  30. return ret
  31. def safe_exec(script, _globals=None, _locals=None, restrict_commit_rollback=False):
  32. # server scripts can be disabled via site_config.json
  33. # they are enabled by default
  34. if 'server_script_enabled' in frappe.conf:
  35. enabled = frappe.conf.server_script_enabled
  36. else:
  37. enabled = True
  38. if not enabled:
  39. frappe.throw(_('Please Enable Server Scripts'), ServerScriptNotEnabled)
  40. # build globals
  41. exec_globals = get_safe_globals()
  42. if _globals:
  43. exec_globals.update(_globals)
  44. if restrict_commit_rollback:
  45. exec_globals.frappe.db.pop('commit', None)
  46. exec_globals.frappe.db.pop('rollback', None)
  47. # execute script compiled by RestrictedPython
  48. frappe.flags.in_safe_exec = True
  49. exec(compile_restricted(script), exec_globals, _locals) # pylint: disable=exec-used
  50. frappe.flags.in_safe_exec = False
  51. return exec_globals, _locals
  52. def get_safe_globals():
  53. datautils = frappe._dict()
  54. if frappe.db:
  55. date_format = frappe.db.get_default("date_format") or "yyyy-mm-dd"
  56. time_format = frappe.db.get_default("time_format") or "HH:mm:ss"
  57. else:
  58. date_format = "yyyy-mm-dd"
  59. time_format = "HH:mm:ss"
  60. add_data_utils(datautils)
  61. form_dict = getattr(frappe.local, 'form_dict', frappe._dict())
  62. if "_" in form_dict:
  63. del frappe.local.form_dict["_"]
  64. user = getattr(frappe.local, "session", None) and frappe.local.session.user or "Guest"
  65. out = NamespaceDict(
  66. # make available limited methods of frappe
  67. json=NamespaceDict(
  68. loads=json.loads,
  69. dumps=json.dumps
  70. ),
  71. as_json=frappe.as_json,
  72. dict=dict,
  73. log=frappe.log,
  74. _dict=frappe._dict,
  75. args=form_dict,
  76. frappe=NamespaceDict(
  77. call=call_whitelisted_function,
  78. flags=frappe._dict(),
  79. format=frappe.format_value,
  80. format_value=frappe.format_value,
  81. date_format=date_format,
  82. time_format=time_format,
  83. format_date=frappe.utils.data.global_date_format,
  84. form_dict=form_dict,
  85. bold=frappe.bold,
  86. copy_doc=frappe.copy_doc,
  87. errprint=frappe.errprint,
  88. qb=frappe.qb,
  89. get_meta=frappe.get_meta,
  90. get_doc=frappe.get_doc,
  91. get_cached_doc=frappe.get_cached_doc,
  92. get_list=frappe.get_list,
  93. get_all=frappe.get_all,
  94. get_system_settings=frappe.get_system_settings,
  95. rename_doc=frappe.rename_doc,
  96. utils=datautils,
  97. get_url=frappe.utils.get_url,
  98. render_template=frappe.render_template,
  99. msgprint=frappe.msgprint,
  100. throw=frappe.throw,
  101. sendmail=frappe.sendmail,
  102. get_print=frappe.get_print,
  103. attach_print=frappe.attach_print,
  104. user=user,
  105. get_fullname=frappe.utils.get_fullname,
  106. get_gravatar=frappe.utils.get_gravatar_url,
  107. full_name=frappe.local.session.data.full_name if getattr(frappe.local, "session", None) else "Guest",
  108. request=getattr(frappe.local, 'request', {}),
  109. session=frappe._dict(
  110. user=user,
  111. csrf_token=frappe.local.session.data.csrf_token if getattr(frappe.local, "session", None) else ''
  112. ),
  113. make_get_request=frappe.integrations.utils.make_get_request,
  114. make_post_request=frappe.integrations.utils.make_post_request,
  115. socketio_port=frappe.conf.socketio_port,
  116. get_hooks=get_hooks,
  117. enqueue=safe_enqueue,
  118. sanitize_html=frappe.utils.sanitize_html,
  119. log_error=frappe.log_error,
  120. db = NamespaceDict(
  121. get_list=frappe.get_list,
  122. get_all=frappe.get_all,
  123. get_value=frappe.db.get_value,
  124. set_value=frappe.db.set_value,
  125. get_single_value=frappe.db.get_single_value,
  126. get_default=frappe.db.get_default,
  127. exists=frappe.db.exists,
  128. count=frappe.db.count,
  129. escape=frappe.db.escape,
  130. sql=read_sql,
  131. commit=frappe.db.commit,
  132. rollback=frappe.db.rollback,
  133. ),
  134. ),
  135. FrappeClient=FrappeClient,
  136. style=frappe._dict(
  137. border_color='#d1d8dd'
  138. ),
  139. get_toc=get_toc,
  140. get_next_link=get_next_link,
  141. _=frappe._,
  142. get_shade=get_shade,
  143. scrub=scrub,
  144. guess_mimetype=mimetypes.guess_type,
  145. html2text=html2text,
  146. dev_server=1 if frappe._dev_server else 0,
  147. run_script=run_script,
  148. is_job_queued=is_job_queued,
  149. get_visible_columns=get_visible_columns,
  150. )
  151. add_module_properties(frappe.exceptions, out.frappe, lambda obj: inspect.isclass(obj) and issubclass(obj, Exception))
  152. if frappe.response:
  153. out.frappe.response = frappe.response
  154. out.update(safe_globals)
  155. # default writer allows write access
  156. out._write_ = _write
  157. out._getitem_ = _getitem
  158. out._getattr_ = _getattr
  159. # allow iterators and list comprehension
  160. out._getiter_ = iter
  161. out._iter_unpack_sequence_ = RestrictedPython.Guards.guarded_iter_unpack_sequence
  162. # add common python builtins
  163. out.update(get_python_builtins())
  164. return out
  165. def is_job_queued(job_name, queue="default"):
  166. '''
  167. :param job_name: used to identify a queued job, usually dotted path to function
  168. :param queue: should be either long, default or short
  169. '''
  170. site = frappe.local.site
  171. queued_jobs = get_jobs(site=site, queue=queue, key='job_name').get(site)
  172. return queued_jobs and job_name in queued_jobs
  173. def safe_enqueue(function, **kwargs):
  174. '''
  175. Enqueue function to be executed using a background worker
  176. Accepts frappe.enqueue params like job_name, queue, timeout, etc.
  177. in addition to params to be passed to function
  178. :param function: whitelised function or API Method set in Server Script
  179. '''
  180. return enqueue(
  181. 'frappe.utils.safe_exec.call_whitelisted_function',
  182. function=function,
  183. **kwargs
  184. )
  185. def call_whitelisted_function(function, **kwargs):
  186. '''Executes a whitelisted function or Server Script of type API'''
  187. return call_with_form_dict(lambda: execute_cmd(function), kwargs)
  188. def run_script(script, **kwargs):
  189. '''run another server script'''
  190. return call_with_form_dict(
  191. lambda: frappe.get_doc('Server Script', script).execute_method(),
  192. kwargs
  193. )
  194. def call_with_form_dict(function, kwargs):
  195. # temporarily update form_dict, to use inside below call
  196. form_dict = getattr(frappe.local, 'form_dict', frappe._dict())
  197. if kwargs:
  198. frappe.local.form_dict = form_dict.copy().update(kwargs)
  199. try:
  200. return function()
  201. finally:
  202. frappe.local.form_dict = form_dict
  203. def get_python_builtins():
  204. return {
  205. 'abs': abs,
  206. 'all': all,
  207. 'any': any,
  208. 'bool': bool,
  209. 'dict': dict,
  210. 'enumerate': enumerate,
  211. 'isinstance': isinstance,
  212. 'issubclass': issubclass,
  213. 'list': list,
  214. 'max': max,
  215. 'min': min,
  216. 'range': range,
  217. 'set': set,
  218. 'sorted': sorted,
  219. 'sum': sum,
  220. 'tuple': tuple,
  221. }
  222. def get_hooks(hook=None, default=None, app_name=None):
  223. hooks = frappe.get_hooks(hook=hook, default=default, app_name=app_name)
  224. return copy.deepcopy(hooks)
  225. def read_sql(query, *args, **kwargs):
  226. '''a wrapper for frappe.db.sql to allow reads'''
  227. query = str(query)
  228. if frappe.flags.in_safe_exec:
  229. check_safe_sql_query(query)
  230. return frappe.db.sql(query, *args, **kwargs)
  231. def check_safe_sql_query(query: str, throw: bool = True) -> bool:
  232. """ Check if SQL query is safe for running in restricted context.
  233. Safe queries:
  234. 1. Read only 'select' or 'explain' queries
  235. 2. CTE on mariadb where writes are not allowed.
  236. """
  237. query = query.strip().lower()
  238. whitelisted_statements = ("select", "explain")
  239. if (query.startswith(whitelisted_statements)
  240. or (query.startswith("with") and frappe.db.db_type == "mariadb")):
  241. return True
  242. if throw:
  243. frappe.throw(_("Query must be of SELECT or read-only WITH type."),
  244. title=_("Unsafe SQL query"), exc=frappe.PermissionError)
  245. return False
  246. def _getitem(obj, key):
  247. # guard function for RestrictedPython
  248. # allow any key to be accessed as long as it does not start with underscore
  249. if isinstance(key, str) and key.startswith('_'):
  250. raise SyntaxError('Key starts with _')
  251. return obj[key]
  252. def _getattr(object, name, default=None):
  253. # guard function for RestrictedPython
  254. # allow any key to be accessed as long as
  255. # 1. it does not start with an underscore (safer_getattr)
  256. # 2. it is not an UNSAFE_ATTRIBUTES
  257. UNSAFE_ATTRIBUTES = {
  258. # Generator Attributes
  259. "gi_frame", "gi_code",
  260. # Coroutine Attributes
  261. "cr_frame", "cr_code", "cr_origin",
  262. # Async Generator Attributes
  263. "ag_code", "ag_frame",
  264. # Traceback Attributes
  265. "tb_frame", "tb_next",
  266. }
  267. if isinstance(name, str) and (name in UNSAFE_ATTRIBUTES):
  268. raise SyntaxError("{name} is an unsafe attribute".format(name=name))
  269. return RestrictedPython.Guards.safer_getattr(object, name, default=default)
  270. def _write(obj):
  271. # guard function for RestrictedPython
  272. # allow writing to any object
  273. return obj
  274. def add_data_utils(data):
  275. for key, obj in frappe.utils.data.__dict__.items():
  276. if key in VALID_UTILS:
  277. data[key] = obj
  278. def add_module_properties(module, data, filter_method):
  279. for key, obj in module.__dict__.items():
  280. if key.startswith("_"):
  281. # ignore
  282. continue
  283. if filter_method(obj):
  284. # only allow functions
  285. data[key] = obj
  286. VALID_UTILS = (
  287. "DATE_FORMAT",
  288. "TIME_FORMAT",
  289. "DATETIME_FORMAT",
  290. "is_invalid_date_string",
  291. "getdate",
  292. "get_datetime",
  293. "to_timedelta",
  294. "get_timedelta",
  295. "add_to_date",
  296. "add_days",
  297. "add_months",
  298. "add_years",
  299. "date_diff",
  300. "month_diff",
  301. "time_diff",
  302. "time_diff_in_seconds",
  303. "time_diff_in_hours",
  304. "now_datetime",
  305. "get_timestamp",
  306. "get_eta",
  307. "get_time_zone",
  308. "convert_utc_to_user_timezone",
  309. "now",
  310. "nowdate",
  311. "today",
  312. "nowtime",
  313. "get_first_day",
  314. "get_quarter_start",
  315. "get_first_day_of_week",
  316. "get_year_start",
  317. "get_last_day_of_week",
  318. "get_last_day",
  319. "get_time",
  320. "get_datetime_in_timezone",
  321. "get_datetime_str",
  322. "get_date_str",
  323. "get_time_str",
  324. "get_user_date_format",
  325. "get_user_time_format",
  326. "format_date",
  327. "format_time",
  328. "format_datetime",
  329. "format_duration",
  330. "get_weekdays",
  331. "get_weekday",
  332. "get_timespan_date_range",
  333. "global_date_format",
  334. "has_common",
  335. "flt",
  336. "cint",
  337. "floor",
  338. "ceil",
  339. "cstr",
  340. "rounded",
  341. "remainder",
  342. "safe_div",
  343. "round_based_on_smallest_currency_fraction",
  344. "encode",
  345. "parse_val",
  346. "fmt_money",
  347. "get_number_format_info",
  348. "money_in_words",
  349. "in_words",
  350. "is_html",
  351. "is_image",
  352. "get_thumbnail_base64_for_image",
  353. "image_to_base64",
  354. "pdf_to_base64",
  355. "strip_html",
  356. "escape_html",
  357. "pretty_date",
  358. "comma_or",
  359. "comma_and",
  360. "comma_sep",
  361. "new_line_sep",
  362. "filter_strip_join",
  363. "get_url",
  364. "get_host_name_from_request",
  365. "url_contains_port",
  366. "get_host_name",
  367. "get_link_to_form",
  368. "get_link_to_report",
  369. "get_absolute_url",
  370. "get_url_to_form",
  371. "get_url_to_list",
  372. "get_url_to_report",
  373. "get_url_to_report_with_filters",
  374. "evaluate_filters",
  375. "compare",
  376. "get_filter",
  377. "make_filter_tuple",
  378. "make_filter_dict",
  379. "sanitize_column",
  380. "scrub_urls",
  381. "expand_relative_urls",
  382. "quoted",
  383. "quote_urls",
  384. "unique",
  385. "strip",
  386. "to_markdown",
  387. "md_to_html",
  388. "markdown",
  389. "is_subset",
  390. "generate_hash",
  391. "formatdate",
  392. "get_user_info_for_avatar",
  393. "get_abbr"
  394. )