From 497ea861f481c6a3c52fe2aed9d0df1b6c99e9d7 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Tue, 30 Mar 2021 12:52:14 +0530 Subject: [PATCH] feat: frappe.whitelist for class methods --- frappe/__init__.py | 21 +++- .../doctype/auto_repeat/auto_repeat.py | 2 + frappe/core/doctype/report/report.py | 3 +- .../role_permission_for_page_and_report.py | 3 + .../doctype/customize_form/customize_form.py | 3 + .../data_migration_run/data_migration_run.py | 1 + frappe/desk/form/run_method.py | 81 ------------- frappe/desk/search.py | 5 +- frappe/email/doctype/newsletter/newsletter.py | 1 + frappe/handler.py | 111 ++++++++++++------ frappe/model/document.py | 18 +-- .../public/js/frappe/form/controls/button.js | 2 +- frappe/public/js/frappe/request.js | 2 +- .../energy_point_log/energy_point_log.py | 1 + frappe/website/doctype/blog_post/blog_post.py | 1 + .../portal_settings/portal_settings.py | 1 + .../doctype/website_theme/website_theme.py | 2 + 17 files changed, 123 insertions(+), 135 deletions(-) delete mode 100644 frappe/desk/form/run_method.py diff --git a/frappe/__init__.py b/frappe/__init__.py index 844a9238e3..c3667d9637 100644 --- a/frappe/__init__.py +++ b/frappe/__init__.py @@ -555,8 +555,13 @@ def whitelist(allow_guest=False, xss_safe=False, methods=None): def innerfn(fn): global whitelisted, guest_methods, xss_safe_methods, allowed_http_methods_for_whitelisted_func - whitelisted.append(fn) + # get function from the unbound / bound method + # this is needed because functions can be compared, but not methods + if hasattr(fn, '__func__'): + fn = fn.__func__ + + whitelisted.append(fn) allowed_http_methods_for_whitelisted_func[fn] = methods if allow_guest: @@ -569,6 +574,20 @@ def whitelist(allow_guest=False, xss_safe=False, methods=None): return innerfn +def is_whitelisted(method): + from frappe.utils import sanitize_html + + is_guest = session['user'] == 'Guest' + if method not in whitelisted or is_guest and method not in guest_methods: + throw(_("Not permitted"), PermissionError) + + if is_guest and method not in xss_safe_methods: + # strictly sanitize form_dict + # escapes html characters like <> except for predefined tags like a, b, ul etc. + for key, value in form_dict.items(): + if isinstance(value, string_types): + form_dict[key] = sanitize_html(value) + def read_only(): def innfn(fn): def wrapper_fn(*args, **kwargs): diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py index 281e699640..bf05baf5b6 100644 --- a/frappe/automation/doctype/auto_repeat/auto_repeat.py +++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py @@ -118,6 +118,7 @@ class AutoRepeat(Document): def is_completed(self): return self.end_date and getdate(self.end_date) < getdate(today()) + @frappe.whitelist() def get_auto_repeat_schedule(self): schedule_details = [] start_date = getdate(self.start_date) @@ -328,6 +329,7 @@ class AutoRepeat(Document): make(doctype=new_doc.doctype, name=new_doc.name, recipients=recipients, subject=subject, content=message, attachments=attachments, send_email=1) + @frappe.whitelist() def fetch_linked_contacts(self): if self.reference_doctype and self.reference_document: res = get_contacts_linking_to(self.reference_doctype, self.reference_document, fields=['email_id']) diff --git a/frappe/core/doctype/report/report.py b/frappe/core/doctype/report/report.py index 01c32bcb57..fb44e61cc8 100644 --- a/frappe/core/doctype/report/report.py +++ b/frappe/core/doctype/report/report.py @@ -58,6 +58,7 @@ class Report(Document): def get_columns(self): return [d.as_dict(no_default_fields = True) for d in self.columns] + @frappe.whitelist() def set_doctype_roles(self): if not self.get('roles') and self.is_standard == 'No': meta = frappe.get_meta(self.ref_doctype) @@ -304,7 +305,7 @@ class Report(Document): return data - @Document.whitelist + @frappe.whitelist() def toggle_disable(self, disable): self.db_set("disabled", cint(disable)) diff --git a/frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py b/frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py index f5081ef595..77b523987c 100644 --- a/frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py +++ b/frappe/core/doctype/role_permission_for_page_and_report/role_permission_for_page_and_report.py @@ -8,6 +8,7 @@ from frappe.core.doctype.report.report import is_prepared_report_disabled from frappe.model.document import Document class RolePermissionforPageandReport(Document): + @frappe.whitelist() def set_report_page_data(self): self.set_custom_roles() self.check_prepared_report_disabled() @@ -35,12 +36,14 @@ class RolePermissionforPageandReport(Document): doc = frappe.get_doc(doctype, docname) return doc.roles + @frappe.whitelist() def reset_roles(self): roles = self.get_standard_roles() self.set('roles', roles) self.update_custom_roles() self.update_disable_prepared_report() + @frappe.whitelist() def update_report_page_data(self): self.update_custom_roles() self.update_disable_prepared_report() diff --git a/frappe/custom/doctype/customize_form/customize_form.py b/frappe/custom/doctype/customize_form/customize_form.py index ad8d80e675..c79c965aae 100644 --- a/frappe/custom/doctype/customize_form/customize_form.py +++ b/frappe/custom/doctype/customize_form/customize_form.py @@ -24,6 +24,7 @@ class CustomizeForm(Document): frappe.db.sql("delete from tabSingles where doctype='Customize Form'") frappe.db.sql("delete from `tabCustomize Form Field`") + @frappe.whitelist() def fetch_to_customize(self): self.clear_existing_doc() if not self.doc_type: @@ -133,6 +134,7 @@ class CustomizeForm(Document): self.doc_type = doc_type self.name = "Customize Form" + @frappe.whitelist() def save_customization(self): if not self.doc_type: return @@ -448,6 +450,7 @@ class CustomizeForm(Document): self.flags.update_db = True + @frappe.whitelist() def reset_to_defaults(self): if not self.doc_type: return diff --git a/frappe/data_migration/doctype/data_migration_run/data_migration_run.py b/frappe/data_migration/doctype/data_migration_run/data_migration_run.py index 473acfb3d0..aed9c6cb1d 100644 --- a/frappe/data_migration/doctype/data_migration_run/data_migration_run.py +++ b/frappe/data_migration/doctype/data_migration_run/data_migration_run.py @@ -10,6 +10,7 @@ from frappe.utils import cstr from frappe.data_migration.doctype.data_migration_mapping.data_migration_mapping import get_source_value class DataMigrationRun(Document): + @frappe.whitelist() def run(self): self.begin() if self.total_pages > 0: diff --git a/frappe/desk/form/run_method.py b/frappe/desk/form/run_method.py deleted file mode 100644 index 7952f3b68d..0000000000 --- a/frappe/desk/form/run_method.py +++ /dev/null @@ -1,81 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# MIT License. See license.txt - -from __future__ import unicode_literals -import json, inspect -import frappe -from frappe import _ -from frappe.utils import cint -from six import text_type, string_types - -@frappe.whitelist() -def runserverobj(method, docs=None, dt=None, dn=None, arg=None, args=None): - """run controller method - old style""" - if not args: args = arg or "" - - if dt: # not called from a doctype (from a page) - if not dn: dn = dt # single - doc = frappe.get_doc(dt, dn) - - else: - doc = frappe.get_doc(json.loads(docs)) - doc._original_modified = doc.modified - doc.check_if_latest() - - if not doc.has_permission("read"): - frappe.msgprint(_("Not permitted"), raise_exception = True) - - if doc: - try: - args = json.loads(args) - except ValueError: - args = args - - try: - fnargs, varargs, varkw, defaults = inspect.getargspec(getattr(doc, method)) - except ValueError: - fnargs = inspect.getfullargspec(getattr(doc, method)).args - varargs = inspect.getfullargspec(getattr(doc, method)).varargs - varkw = inspect.getfullargspec(getattr(doc, method)).varkw - defaults = inspect.getfullargspec(getattr(doc, method)).defaults - - if not fnargs or (len(fnargs)==1 and fnargs[0]=="self"): - r = doc.run_method(method) - - elif "args" in fnargs or not isinstance(args, dict): - r = doc.run_method(method, args) - - else: - r = doc.run_method(method, **args) - - if r: - #build output as csv - if cint(frappe.form_dict.get('as_csv')): - make_csv_output(r, doc.doctype) - else: - frappe.response['message'] = r - - frappe.response.docs.append(doc) - -def make_csv_output(res, dt): - """send method response as downloadable CSV file""" - import frappe - - from six import StringIO - import csv - - f = StringIO() - writer = csv.writer(f) - for r in res: - row = [] - for v in r: - if isinstance(v, string_types): - v = v.encode("utf-8") - row.append(v) - writer.writerow(row) - - f.seek(0) - - frappe.response['result'] = text_type(f.read(), 'utf-8') - frappe.response['type'] = 'csv' - frappe.response['doctype'] = dt.replace(' ','') diff --git a/frappe/desk/search.py b/frappe/desk/search.py index 6faa827dde..6181261fc2 100644 --- a/frappe/desk/search.py +++ b/frappe/desk/search.py @@ -6,8 +6,7 @@ from __future__ import unicode_literals import frappe, json from frappe.utils import cstr, unique, cint from frappe.permissions import has_permission -from frappe.handler import is_whitelisted -from frappe import _ +from frappe import _, is_whitelisted from six import string_types import re import wrapt @@ -221,4 +220,4 @@ def validate_and_sanitize_search_inputs(fn, instance, args, kwargs): if kwargs['doctype'] and not frappe.db.exists('DocType', kwargs['doctype']): return [] - return fn(**kwargs) \ No newline at end of file + return fn(**kwargs) diff --git a/frappe/email/doctype/newsletter/newsletter.py b/frappe/email/doctype/newsletter/newsletter.py index ad985ee20e..c792347c09 100755 --- a/frappe/email/doctype/newsletter/newsletter.py +++ b/frappe/email/doctype/newsletter/newsletter.py @@ -29,6 +29,7 @@ class Newsletter(WebsiteGenerator): self.queue_all(test_email=True) frappe.msgprint(_("Test email sent to {0}").format(self.test_email_id)) + @frappe.whitelist() def send_emails(self): """send emails to leads and customers""" if self.email_sent: diff --git a/frappe/handler.py b/frappe/handler.py index cac9c3a460..44efdde2d4 100755 --- a/frappe/handler.py +++ b/frappe/handler.py @@ -2,17 +2,22 @@ # MIT License. See license.txt from __future__ import unicode_literals + +from werkzeug.wrappers import Response +from six import text_type, string_types, StringIO + import frappe -from frappe import _ import frappe.utils import frappe.sessions import frappe.desk.form.run_method -from frappe.utils.response import build_response -from frappe.api import validate_auth + from frappe.utils import cint +from frappe.api import validate_auth +from frappe import _, is_whitelisted +from frappe.utils.response import build_response +from frappe.utils.csvutils import build_csv_response from frappe.core.doctype.server_script.server_script_utils import run_server_script_api -from werkzeug.wrappers import Response -from six import string_types + ALLOWED_MIMETYPES = ('image/png', 'image/jpeg', 'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', @@ -64,8 +69,9 @@ def execute_cmd(cmd, from_async=False): if from_async: method = method.queue - is_whitelisted(method) - is_valid_http_method(method) + if method != run_doc_method: + is_whitelisted(method) + is_valid_http_method(method) return frappe.call(method, **frappe.form_dict) @@ -75,31 +81,10 @@ def is_valid_http_method(method): if http_method not in frappe.allowed_http_methods_for_whitelisted_func[method]: frappe.throw(_("Not permitted"), frappe.PermissionError) -def is_whitelisted(method): - # check if whitelisted - if frappe.session['user'] == 'Guest': - if (method not in frappe.guest_methods): - frappe.throw(_("Not permitted"), frappe.PermissionError) - - if method not in frappe.xss_safe_methods: - # strictly sanitize form_dict - # escapes html characters like <> except for predefined tags like a, b, ul etc. - for key, value in frappe.form_dict.items(): - if isinstance(value, string_types): - frappe.form_dict[key] = frappe.utils.sanitize_html(value) - - else: - if not method in frappe.whitelisted: - frappe.throw(_("Not permitted"), frappe.PermissionError) - @frappe.whitelist(allow_guest=True) def version(): return frappe.__version__ -@frappe.whitelist() -def runserverobj(method, docs=None, dt=None, dn=None, arg=None, args=None): - frappe.desk.form.run_method.runserverobj(method, docs=docs, dt=dt, dn=dn, arg=arg, args=args) - @frappe.whitelist(allow_guest=True) def logout(): frappe.local.login_manager.logout() @@ -112,15 +97,6 @@ def web_logout(): frappe.respond_as_web_page(_("Logged Out"), _("You have been successfully logged out"), indicator_color='green') -@frappe.whitelist(allow_guest=True) -def run_custom_method(doctype, name, custom_method): - """cmd=run_custom_method&doctype={doctype}&name={name}&custom_method={custom_method}""" - doc = frappe.get_doc(doctype, name) - if getattr(doc, custom_method, frappe._dict()).is_whitelisted: - frappe.call(getattr(doc, custom_method), **frappe.local.form_dict) - else: - frappe.throw(_("Not permitted"), frappe.PermissionError) - @frappe.whitelist() def uploadfile(): ret = None @@ -222,6 +198,65 @@ def get_attr(cmd): frappe.log("method:" + cmd) return method -@frappe.whitelist(allow_guest = True) +@frappe.whitelist(allow_guest=True) def ping(): return "pong" + +@frappe.whitelist() +def run_doc_method(method, docs=None, dt=None, dn=None, arg=None, args=None): + """run controller method - old style""" + import json, inspect + + if not args: args = arg or "" + + if dt: # not called from a doctype (from a page) + if not dn: dn = dt # single + doc = frappe.get_doc(dt, dn) + + else: + doc = frappe.get_doc(json.loads(docs)) + doc._original_modified = doc.modified + doc.check_if_latest() + + if not doc.has_permission("read"): + frappe.msgprint(_("Not permitted"), raise_exception = True) + + if not doc: + return + + try: + args = json.loads(args) + except ValueError: + args = args + + method_obj = getattr(doc, method) + is_whitelisted(getattr(method_obj, '__func__', method_obj)) + + try: + fnargs = inspect.getargspec(method_obj)[0] + except ValueError: + fnargs = inspect.getfullargspec(method_obj).args + + if not fnargs or (len(fnargs)==1 and fnargs[0]=="self"): + r = doc.run_method(method) + + elif "args" in fnargs or not isinstance(args, dict): + r = doc.run_method(method, args) + + else: + r = doc.run_method(method, **args) + + frappe.response.docs.append(doc) + + if not r: + return + + # build output as csv + if cint(frappe.form_dict.get('as_csv')): + build_csv_response(r, doc.doctype.replace(' ', '')) + return + + frappe.response['message'] = r + +# for backwards compatibility +runserverobj = run_doc_method diff --git a/frappe/model/document.py b/frappe/model/document.py index 50025597c4..aaa8331d97 100644 --- a/frappe/model/document.py +++ b/frappe/model/document.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals, print_function import frappe import time -from frappe import _, msgprint +from frappe import _, msgprint, is_whitelisted from frappe.utils import flt, cstr, now, get_datetime_str, file_lock, date_diff from frappe.model.base_document import BaseDocument, get_controller from frappe.model.naming import set_new_name @@ -126,10 +126,10 @@ class Document(BaseDocument): raise ValueError('Illegal arguments') @staticmethod - def whitelist(f): + def whitelist(fn): """Decorator: Whitelist method to be called remotely via REST API.""" - f.whitelisted = True - return f + frappe.whitelist()(fn) + return fn def reload(self): """Reload document from database""" @@ -1148,12 +1148,12 @@ class Document(BaseDocument): return composer - def is_whitelisted(self, method): - fn = getattr(self, method, None) + def is_whitelisted(self, method_name): + method = getattr(self, method_name, None) if not fn: - raise NotFound("Method {0} not found".format(method)) - elif not getattr(fn, "whitelisted", False): - raise Forbidden("Method {0} not whitelisted".format(method)) + raise NotFound("Method {0} not found".format(method_name)) + + is_whitelisted(getattr(method, '__func__', method)) def validate_value(self, fieldname, condition, val2, doc=None, raise_exception=None): """Check that value of fieldname should be 'condition' val2 diff --git a/frappe/public/js/frappe/form/controls/button.js b/frappe/public/js/frappe/form/controls/button.js index 28814531da..b44c9d9dcd 100644 --- a/frappe/public/js/frappe/form/controls/button.js +++ b/frappe/public/js/frappe/form/controls/button.js @@ -34,7 +34,7 @@ frappe.ui.form.ControlButton = frappe.ui.form.ControlData.extend({ var me = this; if(this.frm && this.frm.docname) { frappe.call({ - method: "runserverobj", + method: "run_doc_method", args: {'docs': this.frm.doc, 'method': this.df.options }, btn: this.$input, callback: function(r) { diff --git a/frappe/public/js/frappe/request.js b/frappe/public/js/frappe/request.js index 88912e12da..12fa9c8e21 100644 --- a/frappe/public/js/frappe/request.js +++ b/frappe/public/js/frappe/request.js @@ -55,7 +55,7 @@ frappe.call = function(opts) { args.cmd = opts.module+'.page.'+opts.page+'.'+opts.page+'.'+opts.method; } else if(opts.doc) { $.extend(args, { - cmd: "runserverobj", + cmd: "run_doc_method", docs: frappe.get_doc(opts.doc.doctype, opts.doc.name), method: opts.method, args: opts.args, diff --git a/frappe/social/doctype/energy_point_log/energy_point_log.py b/frappe/social/doctype/energy_point_log/energy_point_log.py index e9425cec86..2e2289aed4 100644 --- a/frappe/social/doctype/energy_point_log/energy_point_log.py +++ b/frappe/social/doctype/energy_point_log/energy_point_log.py @@ -52,6 +52,7 @@ class EnergyPointLog(Document): reference_log.reverted = 0 reference_log.save() + @frappe.whitelist() def revert(self, reason, ignore_permissions=False): if not ignore_permissions: frappe.only_for('System Manager') diff --git a/frappe/website/doctype/blog_post/blog_post.py b/frappe/website/doctype/blog_post/blog_post.py index 28549225be..bfccc0bbc7 100644 --- a/frappe/website/doctype/blog_post/blog_post.py +++ b/frappe/website/doctype/blog_post/blog_post.py @@ -18,6 +18,7 @@ class BlogPost(WebsiteGenerator): order_by = "published_on desc" ) + @frappe.whitelist() def make_route(self): if not self.route: return frappe.db.get_value('Blog Category', self.blog_category, diff --git a/frappe/website/doctype/portal_settings/portal_settings.py b/frappe/website/doctype/portal_settings/portal_settings.py index 5c1cee20fb..1bfbc70d60 100644 --- a/frappe/website/doctype/portal_settings/portal_settings.py +++ b/frappe/website/doctype/portal_settings/portal_settings.py @@ -19,6 +19,7 @@ class PortalSettings(Document): self.append('menu', item) return True + @frappe.whitelist() def reset(self): '''Restore defaults''' self.menu = [] diff --git a/frappe/website/doctype/website_theme/website_theme.py b/frappe/website/doctype/website_theme/website_theme.py index bb612c5d3e..d64077cccd 100644 --- a/frappe/website/doctype/website_theme/website_theme.py +++ b/frappe/website/doctype/website_theme/website_theme.py @@ -98,6 +98,7 @@ class WebsiteTheme(Document): else: self.generate_bootstrap_theme() + @frappe.whitelist() def set_as_default(self): self.generate_bootstrap_theme() self.save() @@ -106,6 +107,7 @@ class WebsiteTheme(Document): website_settings.ignore_validate = True website_settings.save() + @frappe.whitelist() def get_apps(self): from frappe.utils.change_log import get_versions apps = get_versions()