PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / trunk
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder vtrunk
6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 3.6.65 All 196 releases
fluentform / app / Services / FormBuilder / AutocompleteTokens.php

AutocompleteTokens.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder trunk, at app/Services/FormBuilder/AutocompleteTokens.php

233 lines 8.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentForm\App\Services\FormBuilder;
4
5 defined('ABSPATH') || die;
6
7 /**
8 * Decides the autocomplete attribute a field renders.
9 *
10 * A token tells the browser what a field is for, so it can autofill it. Only
11 * the names in the HTML spec's autofill table work; anything else is ignored
12 * by browsers, so an unrecognised value is dropped rather than rendered.
13 *
14 * Every rendered field passes through normalizeAttributes(), which asks:
15 *
16 * 1. Did the form owner pick a value in the editor? Theirs wins.
17 * 2. If not, is the input type unambiguous? Only email, url and tel are.
18 * 3. Is the result still a real token? Checked here because a form saved by
19 * an admin skips the save-side check (see Updater::sanitizeFields).
20 *
21 * 'none' is the editor's "render no attribute" choice. It is stored so the
22 * choice survives a save, and dropped at step 3 so no attribute is emitted.
23 *
24 * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofill
25 */
26 class AutocompleteTokens
27 {
28 // The only input types whose purpose is unambiguous.
29 protected static $typeDefaults = [
30 'email' => 'email',
31 'url' => 'url',
32 'tel' => 'tel',
33 ];
34
35 protected static $nameDefaults = [
36 'first_name' => 'given-name',
37 'middle_name' => 'additional-name',
38 'last_name' => 'family-name',
39 ];
40
41 // Address autofill is opt-in ('on' on the parent field): a form can carry
42 // two address blocks and the Places widget binds address_line_1, so these
43 // would be wrong more often than right as a default.
44 protected static $addressDefaults = [
45 'address_line_1' => 'address-line1',
46 'address_line_2' => 'address-line2',
47 'city' => 'address-level2',
48 'state' => 'address-level1',
49 'zip' => 'postal-code',
50 'country' => 'country',
51 ];
52
53 // cc-*, transaction-*, one-time-code and current-password are excluded:
54 // card details live in the gateway iframe, so those tokens could only ever
55 // autofill a secret into a plain input stored with the submission.
56 protected static $allowed = [
57 'name', 'honorific-prefix', 'given-name', 'additional-name', 'family-name',
58 'honorific-suffix', 'nickname',
59 'email', 'tel', 'tel-country-code', 'tel-national', 'tel-area-code',
60 'tel-local', 'tel-local-prefix', 'tel-local-suffix', 'tel-extension',
61 'impp', 'url', 'photo',
62 'street-address', 'address-line1', 'address-line2', 'address-line3',
63 'address-level4', 'address-level3', 'address-level2', 'address-level1',
64 'country', 'country-name', 'postal-code',
65 'organization', 'organization-title',
66 'username', 'new-password',
67 'bday', 'bday-day', 'bday-month', 'bday-year', 'sex', 'language',
68 ];
69
70 // Values that say whether to autofill without naming what the field is for,
71 // so they apply to a whole field or composite block: the spec's on/off, plus
72 // our own 'none' sentinel meaning "render no attribute at all".
73 protected static $wholeField = ['on', 'off', 'none'];
74
75 // The single boundary every rendered field passes through.
76 public static function normalizeAttributes($attributes)
77 {
78 // A non-array can arrive here: attributes pass through public filters
79 // (fluentform/before_render_item) before reaching buildAttributes().
80 if (!is_array($attributes)) {
81 return $attributes;
82 }
83
84 if (!array_key_exists('autocomplete', $attributes)) {
85 return $attributes;
86 }
87
88 $token = static::sanitize($attributes['autocomplete']);
89
90 if ('' === $token) {
91 $type = strtolower((string) (isset($attributes['type']) ? $attributes['type'] : ''));
92 $token = static::resolveDefault(
93 isset(static::$typeDefaults[$type]) ? static::$typeDefaults[$type] : '',
94 ['input_type' => $type, 'sub_field' => '']
95 );
96 }
97
98 if ('' === $token || 'none' === $token) {
99 unset($attributes['autocomplete']);
100
101 return $attributes;
102 }
103
104 $attributes['autocomplete'] = $token;
105
106 return $attributes;
107 }
108
109 // One control covers every sub-field, so only a whole-field choice applies --
110 // and it overrides anything authored on a child.
111 public static function forSubField($name, $parentChoice = '', $childChoice = '')
112 {
113 // Both entry points must normalise identically: the save-side pass is
114 // skipped for unfiltered_html users, so ' OFF ' can arrive here raw.
115 $parentHasKey = null !== $parentChoice;
116 $parentChoice = static::sanitize($parentChoice);
117 $childChoice = static::sanitize($childChoice);
118
119 if (in_array($parentChoice, static::$wholeField, true)) {
120 return 'on' === $parentChoice && isset(static::$addressDefaults[$name])
121 ? static::resolveDefault(static::$addressDefaults[$name], ['input_type' => '', 'sub_field' => $name])
122 : $parentChoice;
123 }
124
125 if ($childChoice) {
126 return $childChoice;
127 }
128
129 return isset(static::$addressDefaults[$name]) || !$parentHasKey
130 ? ''
131 : static::resolveDefault(isset(static::$nameDefaults[$name]) ? static::$nameDefaults[$name] : '', ['input_type' => '', 'sub_field' => $name]);
132 }
133
134 // A site can rewrite any default through the filter, so whatever comes back
135 // is validated before it is allowed near the markup.
136 protected static function resolveDefault($default, $context)
137 {
138 /**
139 * @param string $token Derived token for a field left on Automatic, '' when none applies.
140 * @param array $context ['input_type' => e.g. 'email', 'sub_field' => e.g. 'first_name'].
141 */
142 return static::sanitize(apply_filters('fluentform/autocomplete_token', $default, $context));
143 }
144
145 public static function sanitize($value)
146 {
147 if (!is_scalar($value)) {
148 return '';
149 }
150
151 $value = preg_replace('/\s+/', ' ', strtolower(trim((string) $value)));
152
153 if (in_array($value, static::$wholeField, true)) {
154 return $value;
155 }
156
157 $parts = explode(' ', $value);
158 $fieldName = array_pop($parts);
159
160 // Filterable so a site can re-admit a spec token excluded here on policy
161 // (current-password, one-time-code) without patching the class.
162 $allowed = (array) apply_filters('fluentform/autocomplete_allowed_tokens', static::$allowed);
163
164 if (!in_array($fieldName, $allowed, true)) {
165 return '';
166 }
167
168 $prefix = static::validPrefix($parts, $fieldName);
169
170 return null === $prefix ? '' : trim($prefix . ' ' . $fieldName);
171 }
172
173 // Optional section, then an address scope, then a contact scope — in that
174 // order, and a contact scope only on a contact field. Null means invalid.
175 protected static function validPrefix(array $parts, $fieldName)
176 {
177 $prefix = [];
178
179 if ($parts && preg_match('/^section-[a-z0-9_-]{1,32}$/', $parts[0])) {
180 $prefix[] = array_shift($parts);
181 }
182
183 if ($parts && in_array($parts[0], ['shipping', 'billing'], true)) {
184 $prefix[] = array_shift($parts);
185 }
186
187 if ($parts && in_array($parts[0], ['home', 'work', 'mobile', 'fax', 'pager'], true)) {
188 if (0 !== strpos($fieldName, 'tel') && !in_array($fieldName, ['email', 'impp'], true)) {
189 return null;
190 }
191
192 $prefix[] = array_shift($parts);
193 }
194
195 return $parts ? null : implode(' ', $prefix);
196 }
197
198 public static function editorOptions()
199 {
200 $options = static::baseEditorOptions();
201
202 foreach (static::$allowed as $token) {
203 $options[] = ['value' => $token, 'label' => $token];
204 }
205
206 return $options;
207 }
208
209 // Address adds an opt-in choice the other composites do not need.
210 public static function addressEditorOptions()
211 {
212 $options = static::baseEditorOptions();
213
214 array_splice($options, 1, 0, [
215 ['value' => 'on', 'label' => __('On (autofill this address)', 'fluentform')],
216 ]);
217
218 return $options;
219 }
220
221 // The choices every control starts with, and all a composite can offer --
222 // see forSubField(). Note this is not $wholeField: the editor shows
223 // 'Automatic' (an empty stored value) and does not offer a bare 'on'.
224 public static function baseEditorOptions()
225 {
226 return [
227 ['value' => '', 'label' => __('Automatic', 'fluentform')],
228 ['value' => 'off', 'label' => 'off'],
229 ['value' => 'none', 'label' => __('None', 'fluentform')],
230 ];
231 }
232 }
233