Commit cf9855c7 authored by Tomáš Peterka's avatar Tomáš Peterka

[hal_json_style] Hardening and simplification of scripts

-  Items in Report Sections (name, params ...) are considered independently
-  Not all Products can handle `hasProperty` call with pride - we mitigate
-  (Date)Time is always serialized in ISO format
parent cd410667
......@@ -53,15 +53,16 @@ def ensure_serializable(obj):
if isinstance(obj, dict):
for key in obj:
obj[key] = ensure_serializable(obj[key])
# throw away date's type information and later reconstruct as Zope's DateTime
if isinstance(obj, DateTime):
return serialize_DateTime(obj)
return obj.ISO()
if isinstance(obj, (datetime.datetime, datetime.date, datetime.time)):
return serialize_datetime(obj)
return obj.isoformat()
# we don't check other isinstances - we believe that iterables don't contain unserializable objects
return obj
date_re = re.compile(r'\d{4}-\d{2}-\d{2}')
datetime_re = re.compile(r'\d{4}-\d{2}-\d{2} |T\d{2}:\d{2}:\d{2}.*')
datetime_iso_re = re.compile(r'^\d{4}-\d{2}-\d{2} |T\d{2}:\d{2}:\d{2}.*$')
time_iso_re = re.compile(r'^(\d{2}):(\d{2}):(\d{2}).*$')
def ensure_deserialized(obj):
"""Deserialize classes serialized by our own `ensure_serializable`.
......@@ -73,30 +74,14 @@ def ensure_deserialized(obj):
obj[key] = ensure_deserialized(obj[key])
# seems that default __str__ method is good enough
if isinstance(obj, str):
if date_re.match(obj) or datetime_re.match(obj):
# Zope's DateTime must be good enough for everyone
if datetime_iso_re.match(obj):
return DateTime(obj)
if time_iso_re.match(obj):
match_obj = time_iso_re.match(obj)
return datetime.time(*tuple(map(int, match_obj.groups())))
return obj
def serialize_datetime(obj):
"""Serialize date/time into string for further JSON serialization into query parameters."""
if isinstance(obj, datetime.datetime):
if obj.time():
# date containing time as well
return obj.strftime("%Y-%m-%d %H:%M:%S")
obj = obj.date()
if isinstance(obj, datetime.date):
return obj.strftime("%Y-%m-%d")
if isinstance(obj, datetime.time):
return obj.strftime("%H:%M:%S")
raise ValueError("obj {!s} is not instance of datetime.*".format(type(obj)))
def serialize_DateTime(obj):
"""Serialize Date/Time into string for further JSON serialization into query parameters."""
if obj.Time() == '00:00:00':
return obj.Date().replace("/", "-")
return obj.ISO()
def getProtectedProperty(document, select):
"""getProtectedProperty is a security-aware substitution for builtin `getattr`
......@@ -440,6 +425,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
if not list_method_query_dict.get("portal_type", []):
list_method_query_dict["portal_type"] = [x for x, _ in field.get_value('portal_types')]
list_method_custom = None
portal_type_list = list_method_query_dict["portal_type"]
# Search for non-editable documents - all reports goes here
# Reports have custom search scripts which wants parameters from the form
......@@ -448,11 +434,12 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
if list_method_name and list_method_name not in ("portal_catalog", "searchFolder", "objectValues"):
# we avoid accessing known protected objects and builtin functions above
try:
list_method = getattr(form, list_method_name)
except Unauthorized:
list_method = getattr(traversed_document, list_method_name)
except (Unauthorized, AttributeError, ValueError) as error:
# we are touching some specially protected (usually builtin) methods
# which we will not introspect
pass
context.log('ListBox {!s} list_method {} is unavailable because of "{!s}"'.format(
field, list_method_name, error), level=100)
# Put all ListBox's search method params from REQUEST to `default_param_json`
# because old code expects synchronous render thus having all form's values
......@@ -477,6 +464,13 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
list_method_param = list_method_param.strip()
if list_method_param not in list_method_query_dict:
list_method_query_dict[list_method_param] = None
# Now if the list_method does not specify **kwargs we need to remove
# unwanted parameters like "portal_type" which is everywhere
if "**" not in list_method.params():
_param_key_list = tuple(list_method_query_dict.keys()) # copy the keys
for param_key in _param_key_list:
if param_key not in list_method.params(): # we search in raw string
del list_method_query_dict[param_key] # but it is enough
if (editable_column_list):
list_method_custom = url_template_dict["custom_search_template"] % {
......@@ -524,7 +518,6 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
# line[title] = prop
# line["_relative_url"] = document.getRelativeUrl()
# line_list.append(line)
result.update({
"column_list": column_list,
"search_column_list": search_column_list,
......@@ -532,7 +525,7 @@ def renderField(traversed_document, field, form, value=None, meta_type=None, key
"sort_column_list": sort_column_list,
"editable_column_list": editable_column_list,
"show_anchor": field.get_value("anchor"),
"portal_type": field.get_value('portal_types'),
"portal_type": portal_type_list,
"lines": lines,
"default_params": ensure_serializable(default_params),
"list_method": list_method_name,
......@@ -690,7 +683,7 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
report_item_list.extend(field.render())
# ERP5 Report document differs from a ERP5 Form in only one thing: it has
# `report_method` attached to it - thus we call it right here
if hasattr(form, 'report_method'):
if hasattr(form, 'report_method') and getattr(form, 'report_method', ""):
report_method_name = getattr(form, 'report_method')
report_method = getattr(traversed_document, report_method_name)
report_item_list.extend(report_method())
......@@ -712,13 +705,13 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
# unserializable data types thus we need to take care of that
# In order not to lose information we put all ReportSection attributes
# inside the report selection params
report_form_params = report_item.selection_params.copy() \
if report_item.selection_params is not None \
else {}
if report_item.selection_name:
selection_name = report_prefix + "_" + report_item.selection_name
report_form_params = {
"selection_name": selection_name,
"selection_columns": report_item.selection_columns,
"selection_sort_order": report_item.selection_sort_order,
}
report_form_params.update(report_item.selection_params)
report_form_params.update(selection_name=selection_name)
# this should load selections with correct values - since it is modifying
# global state in the backend we have nothing more to do here
# I could not find where the code stores params in selection with render
......@@ -729,6 +722,11 @@ def renderForm(traversed_document, form, response_dict, key_prefix=None, selecti
selection_tool.setSelectionParamsFor(selection_name, report_form_params)
selection_tool.setSelectionColumns(selection_name, report_item.selection_columns)
if report_item.selection_columns:
report_form_params.update(selection_columns=report_item.selection_columns)
if report_item.selection_sort_order:
report_form_params.update(selection_sort_order=report_item.selection_sort_order)
# Report section is just a wrapper around form thus we render it right
# we keep traversed_document because its Portal Type Class should be
# addressable by the user = have actions (object_view) attached to it
......@@ -895,7 +893,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
return ""
elif (mode == 'root') or (mode == 'traverse'):
elif mode in ('root', 'traverse'):
#################################################
# Raw document
#################################################
......@@ -950,6 +948,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
erp5_action_list.append({
'href': '%s' % view_action['url'],
'name': view_action['id'],
'icon': view_action['icon'],
'title': Base_translateString(view_action['title'])
})
# Try to embed the form in the result
......@@ -1341,7 +1340,14 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
contents_relative_url = getRealRelativeUrl(search_result)
# get property in secure way from documents
search_property_getter = getProtectedProperty
search_property_hasser = lambda doc, attr: doc.hasProperty(attr)
def search_property_hasser (doc, attr):
"""Brains cannot access Properties - they use permissioned getters."""
try:
return doc.hasProperty(attr)
except (AttributeError, Unauthorized) as e:
context.log('Cannot state ownership of property "{}" on {!s} because of "{!s}"'.format(
attr, doc, e))
return False
elif hasattr(search_result, "aq_self"):
# Zope products have at least ID thus we work with that
contents_uid = search_result.uid
......@@ -1435,6 +1441,7 @@ def calculateHateoas(is_portal=None, is_site_root=None, traversed_document=None,
# maybe a hidden getter (variable accessible by a getter)
accessor_name = 'get' + UpperCase(select)
else:
# or obvious getter (starts with "get" or Capital letter - Script)
accessor_name = select
# again we check on a unwrapped object to avoid acquisition resolution
# which would certainly find something which we don't want
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment