class Form(object): moya_render_targets = ['html'] def __init__(self, element, context, app, style, template, action, enctype, _class=None): self.element = element self.context = context self.app = app self.style = style self.template = template self.action = action self.enctype = enctype setattr(self, 'class', _class) self._fields = OrderedDict() self.bound = False self.validated = False self.raw_data = {} self._data = {} self.error = None self.errors = ErrorContainer(self._fields) self.id = make_id() self.current_field_id = 1 self.legend = element.legend(context) self.root = RootFormRenderable(self, template) self.current_node = self.root self.content = Content(app, template) self.content.new_section('fields', None) self.field_validators = defaultdict(list) self.field_adapters = defaultdict(list) self.field_applyers = defaultdict(list) def __repr__(self): return "<form '{}'>".format(self.element.libid) def reset(self): """Reset the form to a blank state""" self._data = {} self.raw_data = {} for field in self.all_fields: field.value = None @property def renderables(self): return self.root.children @property def ok(self): return self.validated and not self.errors and not self.error @property def fields(self): return [field_list[0] for field_list in itervalues(self._fields)] @property def fields_map(self): return {k: v[0] for k, v in self._fields.items()} def get_value(self): return self @property def data(self): return self._data @property def current_section(self): return self def on_content_insert(self, content): content.merge(self.content, ignore_sections=["fields"]) def update_field_value(self, name, value): for field in self._fields[name]: field.value = value def set_field_data(self, name, value): self._data[name] = value def update(self, value_map): if value_map: for k, v in value_map.items(): if k in self._fields: self.update_field_value(k, v) __moyaupdate__ = update def __contains__(self, key): return key in self._fields and self._fields[key] is not None @property def all_fields(self): for field_list in itervalues(self._fields): for field in field_list: yield field def add_field(self, params, enctype=None, style=None, template=None, default=None, process_value=None, adapt_value=None, data=None): if enctype is not None and self.enctype is None: self.enctype = enctype style = style or params.pop('style', None) or self.style field = Field(self.app, default=default, template=template, process_value=process_value, adapt_value=adapt_value, style=style, data=data, **params) field.id = "field{}_{}".format(self.id, self.current_field_id) self.current_field_id += 1 if params.get('name'): name = params['name'] self._fields.setdefault(name, []).append(field) return field def add_field_validator(self, field_name, element): self.field_validators[field_name].append(element) def add_field_adapter(self, field_name, element): self.field_adapters[field_name].append(element) def add_field_applyer(self, field_name, element): self.field_applyers[field_name].append(element) def get_field_validators(self, field_name): return self.field_validators[field_name] def get_field_adapters(self, field_name): return self.field_adapters[field_name] def push_node(self): self.current_node = self.current_node.children[-1] def pop_node(self): self.current_node = self.current_node.parent def add_renderable(self, name, renderable): self.current_node.add_child(renderable) def add_fail(self, field, msg): """Add a message to a list of errors for a given field""" self._fields[field][0].errors.append(msg) self.errors[field].append(msg) def set_fail(self, field, msg): """Replace the current list of errors for a field""" self._fields[field][0].errors[:] = [msg] self.errors[field][:] = [msg] def __moyaconsole__(self, console): if self.bound: console.text("Bound form", fg="cyan", bold=True) else: console.text("Unbound form", fg="blue", bold=True) table = [] for field in self.all_fields: if field.value is None: v = Cell('None', dim=True) else: v = field.value table.append([field.name, v]) console.table(table, ['name', 'value']) if self.validated: if self.errors: console.text("Form errors", fg="red", bold=True) error_table = [(field, '\n'.join('* %s' % e.strip() for e in _errors)) for field, _errors in iteritems(self.errors)] console.table(error_table, ["field", "error"]) else: if self.error: console.text('Form error "%s"' % self.error.strip(), fg="red", bold=True) else: console.text("Validated, no errors", fg="green", bold=True) def fill_initial(self, context): for field in self.all_fields(): if field.initial is not None: value = field.initial field.value = self.raw_data[field.value] = value def get_initial_binding(self, context): binding = {} for field in self.all_fields: if field.initial is not None: value = field.initial binding[field.name] = field.adapt_value(context, value=value) return binding def fill(self, obj): for field in self: field.value = obj.get(field.name, None) def get_binding(self, context, bind): binding = {} if not bind: return binding with context.data_frame(bind): for field in self.all_fields: if field.name: binding[field.name] = field.process_context_value(context) return binding def get_src_binding(self, context, bind_root): binding = {} with context.frame(bind_root): for field in self.all_fields: if field.src and field.src in context: value = context[field.src] value = field.process_value(context, value=value) binding[field.name] = value return binding def bind(self, context, *bindings): for field in self.all_fields: for binding in reversed(bindings): if field.name in binding: value = binding[field.name] if value is not None: field.value = self.raw_data[field.name] = value break self.bound = True def moya_render(self, archive, context, target, options): form_template = self.template if form_template is None: form_template = "/moya.forms/styles/%s/form.html" % self.style self.content.template = form_template self.content.td['form'] = self return self.content.moya_render(archive, context, target, options)
class Form(AttributeExposer): moya_render_targets = ['html'] __moya_exposed_attributes__ = sorted(['action', 'app', 'bound', 'content', 'id', 'template', 'csrf_check', 'class', 'csrf', 'csrf_token', 'data', 'element', 'enctype', 'raw_data', 'error', 'errors', 'fields', 'legend', 'ok', 'style', 'validated']) def __init__(self, element, context, app, style, template, action, enctype, csrf=True, _class=None): super(Form, self).__init__() self.element = element self.context = context self.app = app self.style = style self.template = template self.action = action self.enctype = enctype self.csrf = csrf setattr(self, 'class', _class) self._fields = OrderedDict() self.bound = False self.validated = False self.raw_data = {} self._data = {} self.error = None self.errors = ErrorContainer(self._fields) self.id = make_id() self.current_field_id = 1 self.legend = element.legend(context) self.root = RootFormRenderable(self, template) self.current_node = self.root self.content = Content(app, template) self.content.new_section('fields', None) self.field_validators = defaultdict(list) self.field_adapters = defaultdict(list) self.field_applyers = defaultdict(list) def add_csrf(self): context = self.context field_data = { 'name': '_moya_csrf', 'fieldname': 'hidden', 'src': None, 'dst': None, 'initial': self.csrf_token, 'required': True, 'label': 'csrf', 'visible': False, 'type': 'text', '_if': True } field = self.add_field(field_data, template=None) content = context['.content'] context['field'] = field content.add_renderable('hiddeninput', field) def __repr__(self): return "<form '{}'>".format(self.element.libid) def reset(self): """Reset the form to a blank state""" self._data = {} self.raw_data = {} for field in self.all_fields: field.value = None @property def csrf_token(self): """Return a csrf token""" context = self.context user_id = text_type(context['.session_key'] or '') form_id = self.element.libid secret = text_type(self.element.archive.secret) raw_token = "{}{}{}".format(user_id, secret, form_id).encode('utf-8', 'ignore') m = hashlib.md5() m.update(raw_token) token_hash = m.hexdigest() return token_hash def validate_csrf(self, context): """Validate CSRF token and raise forbidden error if it fails""" if not self.csrf: return if context['.user'] and context['.request.method'] in ('POST', 'PUT', 'DELETE'): csrf = context['.request.POST._moya_csrf'] if csrf != self.csrf_token: request = context['.request'] if request: security_log.info('''CSRF detected on request "%s %s" referer='%s' user='******'''', request.method, request.url, request.referer, context['.user.username']) raise logic.EndLogic(http.RespondForbidden()) @property def csrf_check(self): if not self.csrf: return True context = self.context if context['.user'] and context['.request.method'] in ('POST', 'PUT', 'DELETE'): csrf = context['.request.POST._moya_csrf'] if csrf != self.csrf_token: request = context['.request'] if request: security_log.info('''CSRF detected on request "%s %s" referer='%s' user='******'''', request.method, request.url, request.referer, context['.user.username']) return False return True @property def renderables(self): return self.root.children @property def ok(self): return self.validated and not self.errors and not self.error @property def fields(self): return [field_list[0] for field_list in itervalues(self._fields)] @property def fields_map(self): return {k: v[0] for k, v in self._fields.items()} def get_value(self): return self @property def data(self): return self._data @property def current_section(self): return self def on_content_insert(self, content): content.merge(self.content, ignore_sections=["fields"]) def update_field_value(self, name, value): for field in self._fields[name]: field.value = value def set_field_data(self, name, value): self._data[name] = value def update(self, value_map): if value_map: for k, v in value_map.items(): if k in self._fields: self.update_field_value(k, v) __moyaupdate__ = update def __contains__(self, key): return key in self._fields and self._fields[key] is not None @property def all_fields(self): for field_list in itervalues(self._fields): for field in field_list: yield field def add_field(self, params, enctype=None, style=None, template=None, default=None, process_value=None, adapt_value=None, data=None): if enctype is not None and self.enctype is None: self.enctype = enctype style = style or params.pop('style', None) or self.style field = Field(self.app, default=default, template=template, process_value=process_value, adapt_value=adapt_value, style=style, data=data, **params) field.id = "field{}_{}".format(self.id, self.current_field_id) self.current_field_id += 1 if params.get('name'): name = params['name'] self._fields.setdefault(name, []).append(field) return field def add_field_validator(self, field_name, element): self.field_validators[field_name].append(element) def add_field_adapter(self, field_name, element): self.field_adapters[field_name].append(element) def add_field_applyer(self, field_name, element): self.field_applyers[field_name].append(element) def get_field_validators(self, field_name): return self.field_validators[field_name] def get_field_adapters(self, field_name): return self.field_adapters[field_name] def push_node(self): self.current_node = self.current_node.children[-1] def pop_node(self): self.current_node = self.current_node.parent def add_renderable(self, name, renderable): self.current_node.add_child(renderable) def add_fail(self, field, msg): """Add a message to a list of errors for a given field""" self._fields[field][0].errors.append(msg) self.errors[field].append(msg) def set_fail(self, field, msg): """Replace the current list of errors for a field""" self._fields[field][0].errors[:] = [msg] self.errors[field][:] = [msg] def __moyaconsole__(self, console): if self.bound: console.text("Bound form", fg="cyan", bold=True) else: console.text("Unbound form", fg="blue", bold=True) table = [] for field in self.all_fields: if field.value is None: v = Cell('None', dim=True) else: v = field.value table.append([field.name, v]) console.table(table, ['name', 'value']) if self.validated: if self.errors: console.text("Form errors", fg="red", bold=True) error_table = [(field, '\n' .join('* %s' % e.strip() for e in _errors)) for field, _errors in iteritems(self.errors)] console.table(error_table, ["field", "error"]) else: if self.error: console.text('Form error "%s"' % self.error.strip(), fg="red", bold=True) else: console.text("Validated, no errors", fg="green", bold=True) if not self.csrf_check: console.text('CSRF check failed -- form did not originate from here!', fg="red", bold=True) # def fill_initial(self, context): # for field in self.all_fields: # if field.initial is not None: # value = field.initial # field.value = self.raw_data[field.value] = value def get_initial_binding(self, context): binding = {} for field in self.all_fields: if field.initial is not None: value = field.initial binding[field.name] = field.adapt_value(context, value=value) return binding def fill(self, obj): for field in self: field.value = obj.get(field.name, None) def get_binding(self, context, bind): binding = {} if not bind: return binding with context.data_frame(bind): for field in self.all_fields: if field.name: binding[field.name] = field.process_context_value(context) return binding def get_src_binding(self, context, bind_root): binding = {} with context.frame(bind_root): for field in self.all_fields: if field.src and field.src in context: value = context[field.src] value = field.process_value(context, value=value) binding[field.name] = value return binding def bind(self, context, *bindings): self.set_data(context, *bindings) self.bound = True def set_data(self, context, *data): for field in self.all_fields: for binding in reversed(data): if field.name in binding: value = binding[field.name] if value is not None: field.value = self.raw_data[field.name] = value break def moya_render(self, archive, context, target, options): form_template = self.template if form_template is None: form_template = "/moya.forms/styles/%s/form.html" % self.style self.content.template = form_template self.content.td['form'] = self return self.content.moya_render(archive, context, target, options)