Skip to content
Permalink
Browse files Browse the repository at this point in the history
feat: frappe.whitelist for class methods
  • Loading branch information
sagarvora committed Mar 30, 2021
1 parent f4655e6 commit 497ea86
Show file tree
Hide file tree
Showing 17 changed files with 123 additions and 135 deletions.
21 changes: 20 additions & 1 deletion frappe/__init__.py
Expand Up @@ -555,8 +555,13 @@ def myfunc(param1, param2):

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:
Expand All @@ -569,6 +574,20 @@ def innerfn(fn):

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):
Expand Down
2 changes: 2 additions & 0 deletions frappe/automation/doctype/auto_repeat/auto_repeat.py
Expand Up @@ -118,6 +118,7 @@ def update_status(self):
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)
Expand Down Expand Up @@ -328,6 +329,7 @@ def send_notification(self, new_doc):
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'])
Expand Down
3 changes: 2 additions & 1 deletion frappe/core/doctype/report/report.py
Expand Up @@ -58,6 +58,7 @@ def on_trash(self):
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)
Expand Down Expand Up @@ -304,7 +305,7 @@ def build_data_dict(self, result, columns):

return data

@Document.whitelist
@frappe.whitelist()
def toggle_disable(self, disable):
self.db_set("disabled", cint(disable))

Expand Down
Expand Up @@ -8,6 +8,7 @@
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()
Expand Down Expand Up @@ -35,12 +36,14 @@ def get_standard_roles(self):
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()
Expand Down
3 changes: 3 additions & 0 deletions frappe/custom/doctype/customize_form/customize_form.py
Expand Up @@ -24,6 +24,7 @@ def on_update(self):
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:
Expand Down Expand Up @@ -133,6 +134,7 @@ def clear_existing_doc(self):
self.doc_type = doc_type
self.name = "Customize Form"

@frappe.whitelist()
def save_customization(self):
if not self.doc_type:
return
Expand Down Expand Up @@ -448,6 +450,7 @@ def validate_fieldtype_length(self):

self.flags.update_db = True

@frappe.whitelist()
def reset_to_defaults(self):
if not self.doc_type:
return
Expand Down
Expand Up @@ -10,6 +10,7 @@
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:
Expand Down
81 changes: 0 additions & 81 deletions frappe/desk/form/run_method.py

This file was deleted.

5 changes: 2 additions & 3 deletions frappe/desk/search.py
Expand Up @@ -6,8 +6,7 @@
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
Expand Down Expand Up @@ -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)
return fn(**kwargs)
1 change: 1 addition & 0 deletions frappe/email/doctype/newsletter/newsletter.py
Expand Up @@ -29,6 +29,7 @@ def test_send(self, doctype="Lead"):
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:
Expand Down
111 changes: 73 additions & 38 deletions frappe/handler.py
Expand Up @@ -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',
Expand Down Expand Up @@ -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)

Expand All @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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

0 comments on commit 497ea86

Please sign in to comment.