diff --git a/CHANGELOG.md b/CHANGELOG.md index fd0e7e5d..e053ba54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +# v4.0.0 +## 10/06/2019 + +1. [](#new) + * Added `tabindex` to global attributes of default field + * Add ability to Sanitize SVGs on upload (Grav 1.7+ required) +1. [](#improved) + * Deprecate `select_optgroup` as `select` can handle optgroups now + * Added missing tabindex checks + * Refactored field inheritance to make things more reliable + * Removed jQuery dependency for the reCaptcha field and VanillaJS-ified it instead + * Removed a stray `dump()` command + * Refactored the base `templates/forms/default` twig templates to make things more extensible + * Added a new `templates/forms/layouts` set of twit templates to allow for easier customization +1. [](#bugfix) + * Fixed `Badly encoded JSON data` warning when uploading files [grav#2663](https://github.com/getgrav/grav/issues/2663) + * Fixed a number of escaping issues [#368](https://github.com/getgrav/grav-plugin-form/issues/368) + # v3.0.9 ## 09/19/2019 diff --git a/blueprints.yaml b/blueprints.yaml index e4940e0b..7b7d19fc 100644 --- a/blueprints.yaml +++ b/blueprints.yaml @@ -1,5 +1,5 @@ name: Form -version: 3.0.9 +version: 4.0.0 testing: false description: Enables the forms handling icon: check-square diff --git a/classes/Form.php b/classes/Form.php index f36f3c7d..285c735e 100644 --- a/classes/Form.php +++ b/classes/Form.php @@ -11,6 +11,7 @@ use Grav\Common\Inflector; use Grav\Common\Language\Language; use Grav\Common\Page\Interfaces\PageInterface; +use Grav\Common\Security; use Grav\Common\Uri; use Grav\Common\Utils; use Grav\Framework\Filesystem\Filesystem; @@ -652,6 +653,11 @@ public function uploadFiles() $upload['file']['name'] = $filename; $upload['file']['path'] = $path; + // Special Sanitization for SVG + if (method_exists('Grav\Common\Security', 'sanitizeSVG') && Utils::contains($mime, 'svg', false)) { + Security::sanitizeSVG($upload['file']['tmp_name']); + } + // We need to store the file into flash object or it will not be available upon save later on. $flash = $this->getFlash(); $flash->setUrl($url)->setUser($grav['user'] ?? null); diff --git a/templates/forms/default/field.html.twig b/templates/forms/default/field.html.twig index 68a87e76..eb4c9908 100644 --- a/templates/forms/default/field.html.twig +++ b/templates/forms/default/field.html.twig @@ -1,4 +1,7 @@ -{% if not field.validate.ignore %} +{% if not field.validate.ignore %} + +{% use 'forms/layouts/field-variables.html.twig' %} +{% block field_override_variables_before %}{% endblock %} {% set field_name = (scope ~ field.name)|fieldName %} {% set vertical = field.style == 'vertical' %} @@ -27,6 +30,12 @@ {# DEPRECATED: Needed by old form fields; remove when backwards compatibility breaks are allowed #} {% set isDisabledToggleable = toggleable and not toggleableChecked %} +{% if toggleable %} + {% set form_field_toggleable %} + {% include 'forms/default/toggleable.html.twig' with {checked: toggleableChecked} %} + {% endset %} +{% endif %} + {% set errors = attribute(form.messages, field.name) %} {% set required = client_side_validation and field.validate.required in ['on', 'true', 1] %} {% set autofocus = (inline_errors == false) and field.autofocus in ['on', 'true', 1] %} @@ -35,105 +44,112 @@ {% set autofocus = true %} {% endif %} -{% block field %} -
- {% block contents %} - {% if field.label is not same as(false) and field.display_label is not same as(false) %} -
- {% if toggleable %} - {% include 'forms/default/toggleable.html.twig' with {field_name: field_name, field: field, checked: toggleableChecked} %} - {% endif %} - -
- {% endif %} -
- {% block group %} - {% block input %} -
- {% block prepend %}{% endblock prepend %} - - {% block append %}{% endblock append %} - {% if inline_errors and errors %} -
-

{{ errors|first }}

-
- {% endif %} -
- {% endblock %} - {% endblock %} - {% if field.description %} -
- - {% if field.markdown %} - {{ field.description|t|markdown(false)|raw }} - {% else %} - {{ field.description|t|raw }} - {% endif %} - -
- {% endif %} - -
- {% endblock %} -
+{% set embed_outer_field_classes %} + {% block outer_field_classes %}{% endblock %} +{% endset %} + +{# Field Classes #} +{%- if errors %}{% set form_field_outer_core = form_field_outer_core ~ ' has-errors' %}{% endif -%} +{%- if toggleable %}{% set form_field_outer_core = form_field_outer_core ~ ' form-field-toggleable' %}{% endif -%} + +{% set layout_form_field_outer_classes = field.outerclasses %} +{% set layout_form_field_outer_classes = layout_form_field_outer_classes|trim ~ ' ' ~ form_field_outer_classes %} +{% set layout_form_field_outer_classes = layout_form_field_outer_classes|trim ~ ' ' ~ embed_outer_field_classes %} + +{# Show Label logic #} +{% set show_label = field.label is not same as(false) and field.display_label is not same as(false )%} + +{# Label Classes #} +{% set layout_form_field_outer_label_classes = ((form_field_outer_label_classes ?: 'form-label') ~ ' ' ~ field.labelclasses)|trim %} +{% set layout_form_field_label_classes = (form_field_label_classes ?: 'inline')|trim %} +{% set form_field_label_trim = toggleable ? 'toggleable' %} + +{# Field Outer Data classes #} +{% set layout_form_field_outer_data_classes = ((form_field_outer_data_classes ?: ' form-data') ~ ' ' ~ field.dataclasses)|trim %} + +{# Field Wrapper classes #} +{% set layout_form_field_wrapper_classes = ((form_field_wrapper_classes ?: ' form-input-wrapper') ~ ' ' ~ field.wrapper_classes)|trim %} + +{# Field input classes #} +{% if field|of_type('array') %} + {% if field.classes %} + {% set field = field|merge({'classes': field.classes ~ ' ' ~ block('field_input_classes')|trim }) %} + {% else %} + {% set field = field|merge({'classes': block('field_input_classes') }) %} + {% endif %} +{% endif %} +{% set layout_form_field_input_classes = (form_field_input_classes ~ ' ' ~ field.classes)|trim %} + +{# Inline error classes #} +{% set form_field_inline_error_classes = form_field_inline_error_classes ?: ' form-errors' %} + +{# Field extra classes #} +{% set form_field_extra_wrapper_classes = 'form-extra-wrapper ' ~ field.wrapper_classes %} + +{# Field For #} +{% set form_field_for = toggleable ? 'toggleable_' ~ field.name : field.id|e %} + +{# Field Label #} +{% set form_field_label = field.markdown ? field.label|markdown(false) : field.label %} +{% set form_field_label = form_field_label|default(field.name|capitalize)|t|e %} + +{# Field Help #} +{% if field.help %} + {% set form_field_help = field.markdown ? field.help|t|markdown(false)|e : field.help|t|e %} +{% endif %} + +{# Field Requied #} +{% set form_field_required = field.validate.required in ['on', 'true', 1] ? true : false %} + +{# Field Description #} +{% set form_field_description = field.markdown ? field.description|t|markdown(false)|raw : field.description|t|raw %} + +{% extends 'forms/layouts/field.html.twig' %} + +{% block global_attributes %} + data-grav-field="{{ field.type }}" + data-grav-disabled="{{ toggleable and toggleableChecked }}" + data-grav-default="{{ field.default|json_encode()|e('html_attr') }}" {% endblock %} +{% block input_attributes %} + class="{{ layout_form_field_input_classes|trim }} {{ field.size }}" + {% if field.id is defined %}id="{{ field.id|e }}" {% endif %} + {% if field.style is defined %}style="{{ field.style|e }}" {% endif %} + {% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %} + {% if field.placeholder %}placeholder="{{ field.placeholder|t|e('html_attr') }}"{% endif %} + {% if autofocus %}autofocus="autofocus"{% endif %} + {% if field.novalidate in ['on', 'true', 1] %}novalidate="novalidate"{% endif %} + {% if field.readonly in ['on', 'true', 1] %}readonly="readonly"{% endif %} + {% if field.autocomplete is defined %}autocomplete="{{ field.autocomplete }}"{% endif %} + {% if field.autocapitalize in ['off', 'characters', 'words', 'sentences'] %}autocapitalize="{{ field.autocapitalize }}"{% endif %} + {% if field.inputmode in ['none', 'text', 'decimal', 'numeric', 'tel', 'search', 'email', 'url'] %}inputmode="{{ field.inputmode }}"{% endif %} + {% if field.tabindex %}tabindex="{{ field.tabindex }}" %}{% endif %} + {% if field.spellcheck in ['true', 'false'] %}spellcheck="{{ field.spellcheck }}"{% endif %} + {% if required %}required="required"{% endif %} + {% if field.validate.pattern %}pattern="{{ field.validate.pattern|e }}"{% endif %} + {% if field.validate.message %}title="{{ field.validate.message|t|e }}" + {% elseif field.title is defined %}title="{{ field.title|t|e }}" {% endif %} + + {# Support key/value and .name/.value styles #} + {% if field.attributes is defined %} + {% for key,attribute in field.attributes %} + {% if attribute|of_type('array') %} + {{ attribute.name }}="{{ attribute.value|e('html_attr') }}" + {% else %} + {{ key }}="{{ attribute|e('html_attr') }}" + {% endif %} + {% endfor %} + {% endif %} + + {# Support for Custom data attributes#} + {% if field.datasets %} + {% for key, attribute in field.datasets %} + data-{{ key }}="{{ attribute|e('html_attr') }}" + {% endfor %} + {% endif %} +{% endblock %} + + + {% endif %} diff --git a/templates/forms/default/fields.html.twig b/templates/forms/default/fields.html.twig index 2baf312c..8e88096e 100644 --- a/templates/forms/default/fields.html.twig +++ b/templates/forms/default/fields.html.twig @@ -6,10 +6,10 @@ {% endif %} {% set value = form ? form.value(field_name) : data.value(field_name) %} - {% block field_open %}{% endblock %} + {% block inner_markup_field_open %}{% endblock %} {% block field %} {% include ["forms/fields/#{field.type}/#{field.type}.html.twig", 'forms/fields/text/text.html.twig'] %} {% endblock %} - {% block field_close %}{% endblock %} + {% block inner_markup_field_close %}{% endblock %} {% endif %} {% endfor %} diff --git a/templates/forms/default/form.html.twig b/templates/forms/default/form.html.twig index f44d5f04..74f19148 100644 --- a/templates/forms/default/form.html.twig +++ b/templates/forms/default/form.html.twig @@ -11,6 +11,9 @@ {% set client_side_validation = form.client_side_validation is not null ? form.client_side_validation : config.plugins.form.client_side_validation|default(true) %} {% set inline_errors = form.inline_errors is not null ? form.inline_errors : config.plugins.form.inline_errors(false) %} +{% set data = data ?? form.data %} +{% set context = context ?? data %} + {% for field in form.fields %} {% if (method == 'POST' and field.type == 'file') %} {% set multipart = ' enctype="multipart/form-data"' %} @@ -44,88 +47,125 @@ window.GravForm.translations = Object.assign({}, window.GravForm.translations || {}, { PLUGIN_FORM: {} }); ", {'group': 'bottom', 'position': 'before'}) %} -
+{# Backwards Compatibility for block overrides #} +{% set override_form_classes %} + {% block form_classes -%} + {{ form_outer_classes }} {{ form.classes }} + {%- endblock %} +{% endset %} +{% set override_inner_markup_fields_start %} {% block inner_markup_fields_start %}{% endblock %} +{% endset %} - {% set data = data ?? form.data %} - {% set context = context ?? data %} +{% set override_inner_markup_fields_end %} + {% block inner_markup_fields_end %}{% endblock %} +{% endset %} +{% set override_inner_markup_fields %} {% block inner_markup_fields %} - {% for field_name, field in form.fields %} - {% set field_name = field.name ?? field_name %} - {% if field_name and not field.validate.ignore %} - {%- if field_name starts with '.' -%} - {% set field_name = field_name[1:] %} - {% set field = field|merge({ name: field_name }) %} + {% for field_name, field in form.fields %} + {% set field_name = field.name ?? field_name %} + {% if field_name and not field.validate.ignore %} + {%- if field_name starts with '.' -%} + {% set field_name = field_name[1:] %} + {% set field = field|merge({ name: field_name }) %} + {% endif %} + + {% set value = form ? form.value(field_name) : data.value(field_name) %} + {% block inner_markup_field_open %}{% endblock %} + {% block field %} + {% include "forms/fields/#{field.type}/#{field.type}.html.twig" ignore missing %} + {% endblock %} + {% block inner_markup_field_close %}{% endblock %} {% endif %} + {% endfor %} + {% endblock %} +{% endset %} - {% set value = form ? form.value(field_name) : data.value(field_name) %} - {% block inner_markup_field_open %}{% endblock %} - {% block process_field %} - {% include "forms/fields/#{field.type}/#{field.type}.html.twig" ignore missing %} - {% endblock %} - {% block inner_markup_field_close %}{% endblock %} - {% endif %} - {% endfor %} +{% set override_inner_markup_buttons_start %} + {% block inner_markup_buttons_start %} +
{% endblock %} +{% endset %} - {% include "forms/fields/formname/formname.html.twig" %} - {% include "forms/fields/formtask/formtask.html.twig" %} +{% set override_inner_markup_buttons_end %} + {% block inner_markup_buttons_end %} +
+ {% endblock %} +{% endset %} + +{# Embed for HTML layout #} +{% embed 'forms/layouts/form.html.twig' %} + + {% block embed_form_core %} + name="{{ form.name }}" + action="{{ action | trim('/', 'right') }}" + method="{{ method }}"{{ multipart }} + {% if form.id %}id="{{ form.id }}"{% endif %} + {% if form.novalidate %}novalidate{% endif %} + {% if form.keep_alive %}data-grav-keepalive="true"{% endif %} + {% endblock %} - {% block inner_markup_fields_end %}{% endblock %} + {% block embed_form_classes -%} + class="{{ parent() }} {{ override_form_classes|trim }}" + {%- endblock %} - {% block inner_markup_buttons_start %} -
+ {% block embed_fields %} + {{ override_inner_markup_fields_start|raw }} + {{ override_inner_markup_fields|raw }} + + {% include "forms/fields/formname/formname.html.twig" %} + {% include "forms/fields/formtask/formtask.html.twig" %} + {% include 'forms/fields/uniqueid/uniqueid.html.twig' %} + {{ nonce_field(form.getNonceAction() ?? 'form', form.getNonceName() ?? 'form-nonce')|raw }} + + {{ override_inner_markup_fields_end|raw }} {% endblock %} - {% for button in form.buttons %} - {% if button.outerclasses is defined %}
{% endif %} - {% if button.url %} - - {% endif %} - - {% if button.url %} - - {% endif %} + {% block embed_buttons %} + {{ override_inner_markup_buttons_start|raw }} + + {% for button in form.buttons %} + {% if button.outerclasses is defined %}
{% endif %} + + {% if button.url %} + {% set button_url = button.url starts with 'http' ? button.url : base_url ~ button.url %} + {% endif %} + + {% embed 'forms/layouts/button.html.twig' %} + {% block embed_button_core %} + {% if button.id %}id="{{ button.id }}"{% endif %} + {% if button.disabled %}disabled="disabled"{% endif %} + {% if button.task %}name="task" value="{{ button.task }}"{% endif %} + type="{{ button.type|default('submit') }}" + {% endblock %} + + {% block embed_button_classes %} + {% block button_classes %} + class="{{ form_button_classes ?: 'button' }} {{ button.classes }}" + {% endblock %} + {% endblock %} + + {% block embed_button_content -%} + {%- set button_value = button.value|t|default('Submit') -%} + {%- if button.html -%} + {{- button_value|trim|raw -}} + {%- else -%} + {{- button_value|trim|e -}} + {%- endif -%} + {%- endblock %} + + {% endembed %} + {% if button.outerclasses is defined %}
{% endif %} - {% endfor %} + {% endfor %} - {% block inner_markup_buttons_end %} -
+ {{ override_inner_markup_buttons_end }} {% endblock %} - {% include 'forms/fields/uniqueid/uniqueid.html.twig' %} +{% endembed %} - {{ nonce_field(form.getNonceAction() ?? 'form', form.getNonceName() ?? 'form-nonce')|raw }} - {% if config.forms.dropzone.enabled %} diff --git a/templates/forms/fields/select/select.html.twig b/templates/forms/fields/select/select.html.twig index b2b55f6d..a366a7e7 100644 --- a/templates/forms/fields/select/select.html.twig +++ b/templates/forms/fields/select/select.html.twig @@ -17,6 +17,7 @@ {% if required %}required="required"{% endif %} {% if field.multiple in ['on', 'true', 1] %}multiple="multiple"{% endif %} {% if field.disabled or isDisabledToggleable %}disabled="disabled"{% endif %} + {% if field.tabindex %}tabindex="{{ field.tabindex }}" %}{% endif %} {% if field.form %}form="{{ field.form }}"{% endif %} {% if field.key %} data-key-observe="{{ (scope ~ field.name)|fieldName }}" @@ -46,14 +47,15 @@ {{ avalue|raw }} {% elseif item_value is iterable %} - - {%for subkey, suboption in item_value %} - {% set selected = field.selectize ? suboption : subkey %} - {% set item_value = field.selectize and field.multiple ? suboption : subkey %} - - {% endfor %} + {% set optgroup_label = item_value|keys|first %} + + {% for subkey, suboption in field.options[key][optgroup_label] %} + {% set selected = field.selectize ? suboption : subkey %} + {% set item_value = field.selectize and field.multiple ? suboption : subkey %} + + {% endfor %} {% else %} {% set selected = field.selectize ? item_value : key %} diff --git a/templates/forms/fields/select_optgroup/select_optgroup.html.twig b/templates/forms/fields/select_optgroup/select_optgroup.html.twig index 3fcb85e0..fbb114a4 100644 --- a/templates/forms/fields/select_optgroup/select_optgroup.html.twig +++ b/templates/forms/fields/select_optgroup/select_optgroup.html.twig @@ -1,42 +1,2 @@ -{% extends "forms/field.html.twig" %} - -{% block global_attributes %} - data-grav-selectize="{{ (field.selectize is defined ? field.selectize : {})|json_encode()|e('html_attr') }}" - {{ parent() }} -{% endblock %} - -{% block input %} -
- -
-{% endblock %} +{# Deprecated Form 4.0: Just use `select` field #} +{% extends "forms/fields/select/select.html.twig" %} diff --git a/templates/forms/fields/tabs/tabs.html.twig b/templates/forms/fields/tabs/tabs.html.twig index 07f47b95..4d51f439 100644 --- a/templates/forms/fields/tabs/tabs.html.twig +++ b/templates/forms/fields/tabs/tabs.html.twig @@ -52,10 +52,10 @@
{% embed 'forms/default/fields.html.twig' with {name: field.name, fields: field.fields} %} - {% block field_open %} + {% block inner_markup_field_open %}
{% endblock %} - {% block field_close %} + {% block inner_markup_field_close %}
{% endblock %} {% endembed %} diff --git a/templates/forms/fields/text/text.html.twig b/templates/forms/fields/text/text.html.twig index ee15f8e1..db139bbb 100644 --- a/templates/forms/fields/text/text.html.twig +++ b/templates/forms/fields/text/text.html.twig @@ -1,3 +1,7 @@ +{% if field.prepend or field.append or field.copy_to_clipboard %} + {% set field = field|merge({'wrapper_classes': 'form-input-addon-wrapper'}) %} +{% endif %} + {% extends "forms/field.html.twig" %} {% block prepend %} @@ -19,7 +23,6 @@ {% block append %} {% if field.copy_to_clipboard %}
- {{ dump(field.copy_to_clipboard|t|raw) }} {% if field.copy_to_clipboard in ['0', '1'] %} {% else %} @@ -33,9 +36,4 @@ {% endif %} {% endblock %} -{% block input %} -{% if field.prepend or field.append or field.copy_to_clipboard %} - {% set field = field|merge({'wrapper_classes': 'form-input-addon-wrapper'}) %} -{% endif %} -{{ parent() }} -{% endblock %} + diff --git a/templates/forms/fields/textarea/textarea.html.twig b/templates/forms/fields/textarea/textarea.html.twig index 6ff616a0..a3088ee2 100644 --- a/templates/forms/fields/textarea/textarea.html.twig +++ b/templates/forms/fields/textarea/textarea.html.twig @@ -17,6 +17,7 @@ {% if field.novalidate in ['on', 'true', 1] %}novalidate="novalidate"{% endif %} {% if field.readonly in ['on', 'true', 1] %}readonly="readonly"{% endif %} {% if field.autocomplete in ['on', 'off'] %}autocomplete="{{ field.autocomplete }}"{% endif %} + {% if field.tabindex %}tabindex="{{ field.tabindex }}" %}{% endif %} {% if required %}required="required"{% endif %} {% if field.validate.pattern %}pattern="{{ field.validate.pattern }}"{% endif %} {% if field.validate.message %}title="{{ field.validate.message|t|e }}"{% endif %} diff --git a/templates/forms/fields/toggle/toggle.html.twig b/templates/forms/fields/toggle/toggle.html.twig index b8845702..cea180f4 100644 --- a/templates/forms/fields/toggle/toggle.html.twig +++ b/templates/forms/fields/toggle/toggle.html.twig @@ -55,6 +55,7 @@ {% endif %} {% endif %} {% if field.validate.required in ['on', 'true', 1] %}required="required"{% endif %} + {% if field.tabindex %}tabindex="{{ field.tabindex }}" %}{% endif %} /> {% endfor %} diff --git a/templates/forms/layouts/button.html.twig b/templates/forms/layouts/button.html.twig new file mode 100644 index 00000000..ef603640 --- /dev/null +++ b/templates/forms/layouts/button.html.twig @@ -0,0 +1,12 @@ +{% set button_tag %} + +{% endset %} + +{% if button_url %} + {{ button_tag|trim|raw }} +{% else %} + {{ button_tag|trim|raw }} +{% endif %} diff --git a/templates/forms/layouts/field-variables.html.twig b/templates/forms/layouts/field-variables.html.twig new file mode 100644 index 00000000..74091326 --- /dev/null +++ b/templates/forms/layouts/field-variables.html.twig @@ -0,0 +1 @@ +{% block field_input_classes %}{% endblock %} \ No newline at end of file diff --git a/templates/forms/layouts/field.html.twig b/templates/forms/layouts/field.html.twig new file mode 100644 index 00000000..dd037da8 --- /dev/null +++ b/templates/forms/layouts/field.html.twig @@ -0,0 +1,52 @@ +{% block field %} +
+ {% block contents %} + {% if show_label %} +
+ {{- form_field_toggleable -}} + +
+ {% endif %} +
+ {% block group %} + {% block input %} +
+ {% block prepend %}{% endblock prepend %} + + {% block append %}{% endblock append %} + {% if inline_errors and errors %} +
+

{{ errors|first|raw }}

+
+ {% endif %} +
+ {% endblock %} + {% endblock %} + {% if field.description %} +
+ + {{ form_field_description|raw }} + +
+ {% endif %} +
+ {% endblock %} +
+{% endblock %} \ No newline at end of file diff --git a/templates/forms/layouts/form.html.twig b/templates/forms/layouts/form.html.twig new file mode 100644 index 00000000..ef6b1693 --- /dev/null +++ b/templates/forms/layouts/form.html.twig @@ -0,0 +1,7 @@ +
+ {% block embed_fields %}{% endblock %} + {% block embed_buttons %}{% endblock %} +
\ No newline at end of file