@@ -740,17 +740,26 @@ def has_permission(doctype=None, ptype="read", doc=None, user=None, verbose=Fals | |||||
:param doc: [optional] Checks User permissions for given doc. | :param doc: [optional] Checks User permissions for given doc. | ||||
:param user: [optional] Check for given user. Default: current user. | :param user: [optional] Check for given user. Default: current user. | ||||
:param parent_doctype: Required when checking permission for a child DocType (unless doc is specified).""" | :param parent_doctype: Required when checking permission for a child DocType (unless doc is specified).""" | ||||
import frappe.permissions | |||||
if not doctype and doc: | if not doctype and doc: | ||||
doctype = doc.doctype | doctype = doc.doctype | ||||
import frappe.permissions | |||||
out = frappe.permissions.has_permission(doctype, ptype, doc=doc, verbose=verbose, user=user, | out = frappe.permissions.has_permission(doctype, ptype, doc=doc, verbose=verbose, user=user, | ||||
raise_exception=throw, parent_doctype=parent_doctype) | raise_exception=throw, parent_doctype=parent_doctype) | ||||
if throw and not out: | if throw and not out: | ||||
if doc: | |||||
frappe.throw(_("No permission for {0}").format(doc.doctype + " " + doc.name)) | |||||
else: | |||||
frappe.throw(_("No permission for {0}").format(doctype)) | |||||
# mimics frappe.throw | |||||
document_label = f"{doc.doctype} {doc.name}" if doc else doctype | |||||
msgprint( | |||||
_("No permission for {0}").format(document_label), | |||||
raise_exception=ValidationError, | |||||
title=None, | |||||
indicator='red', | |||||
is_minimizable=None, | |||||
wide=None, | |||||
as_list=False | |||||
) | |||||
return out | return out | ||||
@@ -32,6 +32,7 @@ def get_list(doctype, fields=None, filters=None, order_by=None, | |||||
args = frappe._dict( | args = frappe._dict( | ||||
doctype=doctype, | doctype=doctype, | ||||
parent_doctype=parent, | |||||
fields=fields, | fields=fields, | ||||
filters=filters, | filters=filters, | ||||
or_filters=or_filters, | or_filters=or_filters, | ||||
@@ -19,13 +19,6 @@ EVENT_MAP = { | |||||
'on_update_after_submit': 'After Save (Submitted Document)' | '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): | def run_server_script_for_doc_event(doc, event): | ||||
# run document event method | # run document event method | ||||
if not event in EVENT_MAP: | if not event in EVENT_MAP: | ||||
@@ -0,0 +1,168 @@ | |||||
{ | |||||
"docstatus": 0, | |||||
"doctype": "DocType", | |||||
"name": "new-doctype-2", | |||||
"__islocal": 1, | |||||
"__unsaved": 1, | |||||
"owner": "Administrator", | |||||
"is_submittable": 0, | |||||
"istable": 0, | |||||
"issingle": 0, | |||||
"is_tree": 0, | |||||
"editable_grid": 1, | |||||
"quick_entry": 1, | |||||
"track_changes": 1, | |||||
"track_seen": 0, | |||||
"track_views": 0, | |||||
"custom": 1, | |||||
"beta": 0, | |||||
"is_virtual": 0, | |||||
"naming_rule": "", | |||||
"name_case": "", | |||||
"allow_rename": 1, | |||||
"hide_toolbar": 0, | |||||
"allow_copy": 0, | |||||
"allow_import": 0, | |||||
"allow_events_in_timeline": 0, | |||||
"allow_auto_repeat": 0, | |||||
"sort_field": "modified", | |||||
"sort_order": "DESC", | |||||
"document_type": "", | |||||
"show_preview_popup": 0, | |||||
"show_name_in_global_search": 0, | |||||
"email_append_to": 0, | |||||
"read_only": 0, | |||||
"in_create": 0, | |||||
"has_web_view": 0, | |||||
"allow_guest_to_view": 0, | |||||
"index_web_pages_for_search": 1, | |||||
"engine": "InnoDB", | |||||
"permissions": [ | |||||
{ | |||||
"docstatus": 0, | |||||
"doctype": "DocPerm", | |||||
"name": "new-docperm-2", | |||||
"__islocal": 1, | |||||
"__unsaved": 1, | |||||
"owner": "Administrator", | |||||
"if_owner": 0, | |||||
"permlevel": 0, | |||||
"select": 0, | |||||
"read": 1, | |||||
"write": 1, | |||||
"create": 1, | |||||
"delete": 1, | |||||
"submit": 0, | |||||
"cancel": 0, | |||||
"amend": 0, | |||||
"report": 1, | |||||
"export": 1, | |||||
"import": 0, | |||||
"set_user_permissions": 0, | |||||
"share": 1, | |||||
"print": 1, | |||||
"email": 1, | |||||
"parent": "new-doctype-2", | |||||
"parentfield": "permissions", | |||||
"parenttype": "DocType", | |||||
"idx": 1, | |||||
"role": "System Manager" | |||||
} | |||||
], | |||||
"__newname": "temp_doctype", | |||||
"module": "Custom", | |||||
"fields": [ | |||||
{ | |||||
"docstatus": 0, | |||||
"doctype": "DocField", | |||||
"name": "new-docfield-1", | |||||
"__islocal": 1, | |||||
"__unsaved": 1, | |||||
"owner": "Administrator", | |||||
"fieldtype": "Data", | |||||
"precision": "", | |||||
"non_negative": 0, | |||||
"hide_days": 0, | |||||
"hide_seconds": 0, | |||||
"reqd": 1, | |||||
"search_index": 0, | |||||
"fetch_if_empty": 0, | |||||
"hidden": 0, | |||||
"bold": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"translatable": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"report_hide": 0, | |||||
"collapsible": 0, | |||||
"hide_border": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"in_preview": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"read_only": 0, | |||||
"allow_on_submit": 0, | |||||
"ignore_user_permissions": 0, | |||||
"allow_bulk_edit": 0, | |||||
"permlevel": 0, | |||||
"ignore_xss_filter": 0, | |||||
"unique": 0, | |||||
"no_copy": 0, | |||||
"set_only_once": 0, | |||||
"remember_last_selected_value": 0, | |||||
"parent": "new-doctype-2", | |||||
"parentfield": "fields", | |||||
"parenttype": "DocType", | |||||
"idx": 1, | |||||
"__unedited": false, | |||||
"label": "member_name" | |||||
}, | |||||
{ | |||||
"docstatus": 0, | |||||
"doctype": "DocField", | |||||
"name": "new-docfield-2", | |||||
"__islocal": 1, | |||||
"__unsaved": 1, | |||||
"owner": "Administrator", | |||||
"fieldtype": "Data", | |||||
"precision": "", | |||||
"non_negative": 0, | |||||
"hide_days": 0, | |||||
"hide_seconds": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"fetch_if_empty": 0, | |||||
"hidden": 0, | |||||
"bold": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"translatable": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"report_hide": 0, | |||||
"collapsible": 0, | |||||
"hide_border": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"in_preview": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"read_only": 0, | |||||
"allow_on_submit": 0, | |||||
"ignore_user_permissions": 0, | |||||
"allow_bulk_edit": 0, | |||||
"permlevel": 0, | |||||
"ignore_xss_filter": 0, | |||||
"unique": 0, | |||||
"no_copy": 0, | |||||
"set_only_once": 0, | |||||
"remember_last_selected_value": 0, | |||||
"parent": "new-doctype-2", | |||||
"parentfield": "fields", | |||||
"parenttype": "DocType", | |||||
"idx": 2, | |||||
"__unedited": false, | |||||
"label": "email" | |||||
} | |||||
] | |||||
} |
@@ -0,0 +1,168 @@ | |||||
{ | |||||
"docstatus": 0, | |||||
"doctype": "DocType", | |||||
"name": "new-doctype-1", | |||||
"__islocal": 1, | |||||
"__unsaved": 1, | |||||
"owner": "Administrator", | |||||
"is_submittable": 0, | |||||
"istable": 0, | |||||
"issingle": 1, | |||||
"is_tree": 0, | |||||
"editable_grid": 1, | |||||
"quick_entry": 0, | |||||
"track_changes": 1, | |||||
"track_seen": 0, | |||||
"track_views": 0, | |||||
"custom": 1, | |||||
"beta": 0, | |||||
"is_virtual": 0, | |||||
"naming_rule": "", | |||||
"name_case": "", | |||||
"allow_rename": 1, | |||||
"hide_toolbar": 0, | |||||
"allow_copy": 0, | |||||
"allow_import": 0, | |||||
"allow_events_in_timeline": 0, | |||||
"allow_auto_repeat": 0, | |||||
"sort_field": "modified", | |||||
"sort_order": "DESC", | |||||
"document_type": "", | |||||
"show_preview_popup": 0, | |||||
"show_name_in_global_search": 0, | |||||
"email_append_to": 0, | |||||
"read_only": 0, | |||||
"in_create": 0, | |||||
"has_web_view": 0, | |||||
"allow_guest_to_view": 0, | |||||
"index_web_pages_for_search": 1, | |||||
"engine": "InnoDB", | |||||
"permissions": [ | |||||
{ | |||||
"docstatus": 0, | |||||
"doctype": "DocPerm", | |||||
"name": "new-docperm-1", | |||||
"__islocal": 1, | |||||
"__unsaved": 1, | |||||
"owner": "Administrator", | |||||
"if_owner": 0, | |||||
"permlevel": 0, | |||||
"select": 0, | |||||
"read": 1, | |||||
"write": 1, | |||||
"create": 1, | |||||
"delete": 1, | |||||
"submit": 0, | |||||
"cancel": 0, | |||||
"amend": 0, | |||||
"report": 1, | |||||
"export": 1, | |||||
"import": 0, | |||||
"set_user_permissions": 0, | |||||
"share": 1, | |||||
"print": 1, | |||||
"email": 1, | |||||
"parent": "new-doctype-1", | |||||
"parentfield": "permissions", | |||||
"parenttype": "DocType", | |||||
"idx": 1, | |||||
"role": "System Manager" | |||||
} | |||||
], | |||||
"__newname": "temp_singles", | |||||
"module": "Custom", | |||||
"fields": [ | |||||
{ | |||||
"docstatus": 0, | |||||
"doctype": "DocField", | |||||
"name": "new-docfield-1", | |||||
"__islocal": 1, | |||||
"__unsaved": 1, | |||||
"owner": "Administrator", | |||||
"fieldtype": "Data", | |||||
"precision": "", | |||||
"non_negative": 0, | |||||
"hide_days": 0, | |||||
"hide_seconds": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"fetch_if_empty": 0, | |||||
"hidden": 0, | |||||
"bold": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"translatable": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"report_hide": 0, | |||||
"collapsible": 0, | |||||
"hide_border": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"in_preview": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"read_only": 0, | |||||
"allow_on_submit": 0, | |||||
"ignore_user_permissions": 0, | |||||
"allow_bulk_edit": 0, | |||||
"permlevel": 0, | |||||
"ignore_xss_filter": 0, | |||||
"unique": 0, | |||||
"no_copy": 0, | |||||
"set_only_once": 0, | |||||
"remember_last_selected_value": 0, | |||||
"parent": "new-doctype-1", | |||||
"parentfield": "fields", | |||||
"parenttype": "DocType", | |||||
"idx": 1, | |||||
"__unedited": false, | |||||
"label": "member_name" | |||||
}, | |||||
{ | |||||
"docstatus": 0, | |||||
"doctype": "DocField", | |||||
"name": "new-docfield-2", | |||||
"__islocal": 1, | |||||
"__unsaved": 1, | |||||
"owner": "Administrator", | |||||
"fieldtype": "Data", | |||||
"precision": "", | |||||
"non_negative": 0, | |||||
"hide_days": 0, | |||||
"hide_seconds": 0, | |||||
"reqd": 0, | |||||
"search_index": 0, | |||||
"fetch_if_empty": 0, | |||||
"hidden": 0, | |||||
"bold": 0, | |||||
"allow_in_quick_entry": 0, | |||||
"translatable": 0, | |||||
"print_hide": 0, | |||||
"print_hide_if_no_value": 0, | |||||
"report_hide": 0, | |||||
"collapsible": 0, | |||||
"hide_border": 0, | |||||
"in_list_view": 0, | |||||
"in_standard_filter": 0, | |||||
"in_preview": 0, | |||||
"in_filter": 0, | |||||
"in_global_search": 0, | |||||
"read_only": 0, | |||||
"allow_on_submit": 0, | |||||
"ignore_user_permissions": 0, | |||||
"allow_bulk_edit": 0, | |||||
"permlevel": 0, | |||||
"ignore_xss_filter": 0, | |||||
"unique": 0, | |||||
"no_copy": 0, | |||||
"set_only_once": 0, | |||||
"remember_last_selected_value": 0, | |||||
"parent": "new-doctype-1", | |||||
"parentfield": "fields", | |||||
"parenttype": "DocType", | |||||
"idx": 2, | |||||
"__unedited": false, | |||||
"label": "email" | |||||
} | |||||
] | |||||
} |
@@ -12,7 +12,7 @@ from frappe.utils.response import build_response | |||||
from frappe.utils.csvutils import build_csv_response | from frappe.utils.csvutils import build_csv_response | ||||
from frappe.utils.image import optimize_image | from frappe.utils.image import optimize_image | ||||
from mimetypes import guess_type | 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', | ALLOWED_MIMETYPES = ('image/png', 'image/jpeg', 'application/pdf', 'application/msword', | ||||
@@ -49,8 +49,9 @@ def execute_cmd(cmd, from_async=False): | |||||
break | break | ||||
# via server script | # 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: | try: | ||||
method = get_attr(cmd) | method = get_attr(cmd) | ||||
@@ -66,7 +67,20 @@ def execute_cmd(cmd, from_async=False): | |||||
return frappe.call(method, **frappe.form_dict) | 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): | def is_valid_http_method(method): | ||||
if frappe.flags.in_safe_exec: | |||||
return | |||||
http_method = frappe.local.request.method | http_method = frappe.local.request.method | ||||
if http_method not in frappe.allowed_http_methods_for_whitelisted_func[method]: | if http_method not in frappe.allowed_http_methods_for_whitelisted_func[method]: | ||||
@@ -7,7 +7,7 @@ import frappe | |||||
def run_webhooks(doc, method): | def run_webhooks(doc, method): | ||||
'''Run webhooks for this method''' | '''Run webhooks for this method''' | ||||
if frappe.flags.in_import or frappe.flags.in_patch or frappe.flags.in_install: | |||||
if frappe.flags.in_import or frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_migrate: | |||||
return | return | ||||
if frappe.flags.webhooks_executed is None: | if frappe.flags.webhooks_executed is None: | ||||
@@ -36,10 +36,12 @@ class DatabaseQuery(object): | |||||
ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False, | ignore_ifnull=False, save_user_settings=False, save_user_settings_fields=False, | ||||
update=None, add_total_row=None, user_settings=None, reference_doctype=None, | update=None, add_total_row=None, user_settings=None, reference_doctype=None, | ||||
run=True, strict=True, pluck=None, ignore_ddl=False, parent_doctype=None) -> List: | run=True, strict=True, pluck=None, ignore_ddl=False, parent_doctype=None) -> List: | ||||
if not ignore_permissions and \ | |||||
not frappe.has_permission(self.doctype, "select", user=user, parent_doctype=parent_doctype) and \ | |||||
not frappe.has_permission(self.doctype, "read", user=user, parent_doctype=parent_doctype): | |||||
if ( | |||||
not ignore_permissions | |||||
and not frappe.has_permission(self.doctype, "select", user=user, parent_doctype=parent_doctype) | |||||
and not frappe.has_permission(self.doctype, "read", user=user, parent_doctype=parent_doctype) | |||||
): | |||||
frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(self.doctype)) | frappe.flags.error_message = _('Insufficient Permission for {0}').format(frappe.bold(self.doctype)) | ||||
raise frappe.PermissionError(self.doctype) | raise frappe.PermissionError(self.doctype) | ||||
@@ -787,12 +789,15 @@ class DatabaseQuery(object): | |||||
def check_parent_permission(parent, child_doctype): | def check_parent_permission(parent, child_doctype): | ||||
if parent: | if parent: | ||||
# User may pass fake parent and get the information from the child table | # User may pass fake parent and get the information from the child table | ||||
if child_doctype and not frappe.db.exists('DocField', | |||||
{'parent': parent, 'options': child_doctype}): | |||||
if child_doctype and not ( | |||||
frappe.db.exists('DocField', {'parent': parent, 'options': child_doctype}) | |||||
or frappe.db.exists('Custom Field', {'dt': parent, 'options': child_doctype}) | |||||
): | |||||
raise frappe.PermissionError | raise frappe.PermissionError | ||||
if frappe.permissions.has_permission(parent): | if frappe.permissions.has_permission(parent): | ||||
return | return | ||||
# Either parent not passed or the user doesn't have permission on parent doctype of child table! | # Either parent not passed or the user doesn't have permission on parent doctype of child table! | ||||
raise frappe.PermissionError | raise frappe.PermissionError | ||||
@@ -81,6 +81,9 @@ frappe.ui.form.ControlDatetime = class ControlDatetime extends frappe.ui.form.Co | |||||
get_model_value() { | get_model_value() { | ||||
let value = super.get_model_value(); | let value = super.get_model_value(); | ||||
if (!value && !this.doc) { | |||||
value = this.last_value; | |||||
} | |||||
return frappe.datetime.get_datetime_as_string(value); | return frappe.datetime.get_datetime_as_string(value); | ||||
} | } | ||||
}; | }; |
@@ -156,26 +156,29 @@ | |||||
.result, .no-result, .freeze { | .result, .no-result, .freeze { | ||||
min-height: #{"calc(100vh - 284px)"}; | min-height: #{"calc(100vh - 284px)"}; | ||||
} | } | ||||
} | |||||
.msg-box { | |||||
margin-bottom: 4em; | |||||
font-size: var(--text-sm); | |||||
.msg-box { | |||||
margin-bottom: 4em; | |||||
font-size: var(--text-sm); | |||||
// To compensate for perceived centering | |||||
.null-state { | |||||
height: 85px; | |||||
width: auto; | |||||
margin-bottom: var(--margin-md); | |||||
img { | |||||
fill: var(--fg-color); | |||||
} | |||||
// To compensate for perceived centering | |||||
.null-state { | |||||
height: 85px; | |||||
width: auto; | |||||
margin-bottom: var(--margin-md); | |||||
img { | |||||
fill: var(--fg-color); | |||||
} | } | ||||
} | |||||
p { | |||||
font-size: var(--text-md); | |||||
} | |||||
.meta-description { | |||||
width: 45%; | |||||
margin-right: auto; | |||||
margin-left: auto; | |||||
} | |||||
.meta-description { | |||||
width: 45%; | |||||
margin-right: auto; | |||||
margin-left: auto; | |||||
} | } | ||||
} | } | ||||
} | } | ||||
@@ -0,0 +1,81 @@ | |||||
import os | |||||
import unittest | |||||
from typing import List | |||||
import frappe | |||||
from frappe.core.doctype.data_import.data_import import export_json, import_doc | |||||
from frappe.desk.form.save import savedocs | |||||
from frappe.model.delete_doc import delete_doc | |||||
class TestFixtureImport(unittest.TestCase): | |||||
def create_new_doctype(self, DocType: str) -> None: | |||||
file = frappe.get_app_path("frappe", "custom", "fixtures", f"{DocType}.json") | |||||
file = open(file, "r") | |||||
doc = file.read() | |||||
file.close() | |||||
savedocs(doc, "Save") | |||||
def insert_dummy_data_and_export(self, DocType: str, dummy_name_list: List[str]) -> str: | |||||
for name in dummy_name_list: | |||||
doc = frappe.get_doc({"doctype": DocType, "member_name": name}) | |||||
doc.insert() | |||||
path_to_exported_fixtures = os.path.join(os.getcwd(), f"{DocType}_data.json") | |||||
export_json(DocType, path_to_exported_fixtures) | |||||
return path_to_exported_fixtures | |||||
def test_fixtures_import(self): | |||||
self.assertFalse(frappe.db.exists("DocType", "temp_doctype")) | |||||
self.create_new_doctype("temp_doctype") | |||||
dummy_name_list = ["jhon", "jane"] | |||||
path_to_exported_fixtures = self.insert_dummy_data_and_export("temp_doctype", dummy_name_list) | |||||
frappe.db.truncate("temp_doctype") | |||||
import_doc(path_to_exported_fixtures) | |||||
delete_doc("DocType", "temp_doctype", delete_permanently=True) | |||||
os.remove(path_to_exported_fixtures) | |||||
self.assertEqual(frappe.db.count("temp_doctype"), len(dummy_name_list)) | |||||
data = frappe.get_all("temp_doctype", "member_name") | |||||
frappe.db.truncate("temp_doctype") | |||||
imported_data = set() | |||||
for item in data: | |||||
imported_data.add(item["member_name"]) | |||||
self.assertEqual(set(dummy_name_list), imported_data) | |||||
def test_singles_fixtures_import(self): | |||||
self.assertFalse(frappe.db.exists("DocType", "temp_singles")) | |||||
self.create_new_doctype("temp_singles") | |||||
dummy_name_list = ["Phoebe"] | |||||
path_to_exported_fixtures = self.insert_dummy_data_and_export("temp_singles", dummy_name_list) | |||||
singles_doctype = frappe.qb.DocType("Singles") | |||||
truncate_query = ( | |||||
frappe.qb.from_(singles_doctype) | |||||
.delete() | |||||
.where(singles_doctype.doctype == "temp_singles") | |||||
) | |||||
truncate_query.run() | |||||
import_doc(path_to_exported_fixtures) | |||||
delete_doc("DocType", "temp_singles", delete_permanently=True) | |||||
os.remove(path_to_exported_fixtures) | |||||
data = frappe.db.get_single_value("temp_singles", "member_name") | |||||
truncate_query.run() | |||||
self.assertEqual(data, dummy_name_list[0]) |
@@ -31,4 +31,27 @@ class TestSafeExec(unittest.TestCase): | |||||
self.assertEqual(frappe.db.sql("SELECT Max(name) FROM tabUser"), _locals["out"]) | self.assertEqual(frappe.db.sql("SELECT Max(name) FROM tabUser"), _locals["out"]) | ||||
def test_safe_query_builder(self): | 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 | ||||
import frappe.utils.data | import frappe.utils.data | ||||
from frappe import _ | from frappe import _ | ||||
from frappe.handler import execute_cmd | |||||
from frappe.frappeclient import FrappeClient | from frappe.frappeclient import FrappeClient | ||||
from frappe.modules import scrub | from frappe.modules import scrub | ||||
from frappe.website.utils import get_next_link, get_shade, get_toc | from frappe.website.utils import get_next_link, get_shade, get_toc | ||||
from frappe.www.printview import get_visible_columns | from frappe.www.printview import get_visible_columns | ||||
from frappe.utils.background_jobs import enqueue, get_jobs | |||||
class ServerScriptNotEnabled(frappe.PermissionError): | class ServerScriptNotEnabled(frappe.PermissionError): | ||||
pass | pass | ||||
@@ -74,7 +75,9 @@ def get_safe_globals(): | |||||
add_data_utils(datautils) | 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["_"] | del frappe.local.form_dict["_"] | ||||
user = getattr(frappe.local, "session", None) and frappe.local.session.user or "Guest" | user = getattr(frappe.local, "session", None) and frappe.local.session.user or "Guest" | ||||
@@ -89,14 +92,16 @@ def get_safe_globals(): | |||||
dict=dict, | dict=dict, | ||||
log=frappe.log, | log=frappe.log, | ||||
_dict=frappe._dict, | _dict=frappe._dict, | ||||
args=form_dict, | |||||
frappe=NamespaceDict( | frappe=NamespaceDict( | ||||
call=call_whitelisted_function, | |||||
flags=frappe._dict(), | flags=frappe._dict(), | ||||
format=frappe.format_value, | format=frappe.format_value, | ||||
format_value=frappe.format_value, | format_value=frappe.format_value, | ||||
date_format=date_format, | date_format=date_format, | ||||
time_format=time_format, | time_format=time_format, | ||||
format_date=frappe.utils.data.global_date_format, | format_date=frappe.utils.data.global_date_format, | ||||
form_dict=getattr(frappe.local, 'form_dict', {}), | |||||
form_dict=form_dict, | |||||
bold=frappe.bold, | bold=frappe.bold, | ||||
copy_doc=frappe.copy_doc, | copy_doc=frappe.copy_doc, | ||||
errprint=frappe.errprint, | errprint=frappe.errprint, | ||||
@@ -132,6 +137,7 @@ def get_safe_globals(): | |||||
make_post_request=frappe.integrations.utils.make_post_request, | make_post_request=frappe.integrations.utils.make_post_request, | ||||
socketio_port=frappe.conf.socketio_port, | socketio_port=frappe.conf.socketio_port, | ||||
get_hooks=get_hooks, | get_hooks=get_hooks, | ||||
enqueue=safe_enqueue, | |||||
sanitize_html=frappe.utils.sanitize_html, | sanitize_html=frappe.utils.sanitize_html, | ||||
log_error=frappe.log_error | log_error=frappe.log_error | ||||
), | ), | ||||
@@ -147,7 +153,8 @@ def get_safe_globals(): | |||||
guess_mimetype=mimetypes.guess_type, | guess_mimetype=mimetypes.guess_type, | ||||
html2text=html2text, | html2text=html2text, | ||||
dev_server=1 if frappe._dev_server else 0, | 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)) | 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 | 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(): | def get_python_builtins(): | ||||
return { | return { | ||||
'abs': abs, | 'abs': abs, | ||||
@@ -221,9 +277,6 @@ def read_sql(query, *args, **kwargs): | |||||
raise frappe.PermissionError('Only SELECT SQL allowed in scripting') | raise frappe.PermissionError('Only SELECT SQL allowed in scripting') | ||||
return frappe.db.sql(query, *args, **kwargs) | 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): | def _getitem(obj, key): | ||||
# guard function for RestrictedPython | # guard function for RestrictedPython | ||||
@@ -15,10 +15,10 @@ | |||||
{{ _("There's nothing here") }} | {{ _("There's nothing here") }} | ||||
</h2> | </h2> | ||||
<div class="text-muted error-text"> | <div class="text-muted error-text"> | ||||
{{ _("The page you are looking for have gone missing.") }} | |||||
{{ _("The page you are looking for has gone missing.") }} | |||||
</div> | </div> | ||||
<div class="mt-6 back-to-home"><a href='/' class='btn btn-primary'>{{ _("Back to Home") }}</a></div> | <div class="mt-6 back-to-home"><a href='/' class='btn btn-primary'>{{ _("Back to Home") }}</a></div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
{% endblock %} | |||||
{% endblock %} |