PluginProbe
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder / V3.3.2
Bit Form – Contact Form, Payment Forms, Multi Step Forms, Calculator & Custom Form Builder vV3.3.2
V3.3.2 3.3.1 V-3.3.0 3.2.2 3.2.1 3.2.0 3.1.4 3.1.3 3.1.2 3.1.1 3.1.0 V3.0.3 V3.0.2 -3.0.1 V_3.0.0 1.1.1 1.1.8 1.2 1.3 1.4 1.4.18 1.5.2 1.9 2.0 2.10.0 All 139 releases
← All changes | includes/Frontend/Form/View/FormViewer.php +258 -34 2.0 → V3.3.2 View file →
@@ -2,15 +2,20 @@
2 2
3 3 namespace BitCode\BitForm\Frontend\Form\View;
4 4
5 5 use BitCode\BitForm\Admin\Form\Helpers;
6 +use BitCode\BitForm\Core\Util\Translation\TranslationManager;
6 7 use BitCode\BitForm\Frontend\Form\FrontendFormManager;
8 +use BitCode\BitForm\Frontend\Form\View\Conversational\ConversationalHelpers;
9 +use BitCode\BitForm\Frontend\Form\View\Conversational\DefaultConversationalTheme;
7 10
8 -class FormViewer {
11 +class FormViewer
12 +{
9 13 private $_theme = 'Default';
10 14 private $_themeDetails;
11 15 public $_fields;
12 16 private $_layout;
17 + public $_nestedLayout;
13 18 private $_buttons;
14 19 public $_form;
15 20 public $_error;
16 21 private $_previousValue;
@@ -15,22 +20,26 @@
15 20 public $_error;
16 21 private $_previousValue;
17 22 public $_formAtomicClsMap;
18 23 public $_tokens;
24 + public $_formContents;
19 25
20 - public function __construct(FrontendFormManager $formManager, $form_contents, $formAtomicClsMap, $errorMessages = null, $previousValue = null) {
26 + public function __construct(FrontendFormManager $formManager, $form_contents, $formAtomicClsMap, $errorMessages = null, $previousValue = null)
27 + {
21 28 $this->_tokens = Helpers::csrfEecrypted();
22 29 $this->_fields = $form_contents->fields;
23 30 $this->_layout = $form_contents->layout;
31 + $this->_nestedLayout = isset($form_contents->nestedLayout) ? $form_contents->nestedLayout : '';
24 32 $this->_buttons = isset($form_contents->buttons) ? $form_contents->buttons : '';
25 33 $this->_form = $formManager;
26 34 $this->_error = $errorMessages;
27 - $this->form_contents = $form_contents;
35 + $this->_formContents = $form_contents;
28 36 $this->_previousValue = $previousValue;
29 37 $this->_formAtomicClsMap = $formAtomicClsMap;
30 38 }
31 39
32 - public function getView($hasFile) {
40 + public function getView($hasFile, $msg = null)
41 + {
33 42 $name = str_replace(' ', '', $this->_theme);
34 43 $file = false !== strpos($name, 'Theme') ? $name . '.php' : $name . 'Theme.php';
35 44 if (file_exists(__DIR__ . '/' . $file)) {
36 45 $className = __NAMESPACE__ . '\\Theme\\' . $name . 'Theme';
@@ -38,35 +47,71 @@
38 47 } else {
39 48 $className = __NAMESPACE__ . '\\Theme\\' . 'DefaultTheme';
40 49 $this->_themeDetails = new $className();
41 50 }
42 - return $this->setView($hasFile);
51 + return $this->setView($hasFile, $msg);
43 52 }
44 53
45 - public function honeypotField() {
54 + public function honeypotField()
55 + {
46 56 $time = time();
47 57 $honeypodFldName = Helpers::honeypotEncryptedToken("_bitforms_{$this->getFormID()}_{$time}_");
48 58 $honeypotInput = '';
49 - if (Helpers::property_exists_nested($this->form_contents, 'additional->enabled->honeypot', true)) {
59 + if (Helpers::property_exists_nested($this->_formContents, 'additional->enabled->honeypot', true)) {
50 60 $honeypotInput =
51 - <<<HTMLa
52 - <input type="text" class="d-none" name="b_h_t" value="{$honeypodFldName}">
53 - <input type="text" class="d-none" name="{$honeypodFldName}" required>
54 -HTMLa;
61 + '
62 + <input type="text" class="d-none" name="b_h_t" value="' . $honeypodFldName . '">
63 + <input type="text" class="d-none" name="' . $honeypodFldName . '" required>
64 + ';
55 65 return $honeypotInput;
56 66 }
57 67 return $honeypotInput;
58 68 }
59 69
60 - public function setTheme($theme = 'Default') {
70 + /**
71 + * Opt-out markers for HTML-layer translators (Google/GTranslate, TranslatePress)
72 + * when a form disables translation. Empty otherwise, so markup is untouched.
73 + *
74 + * @return array{attrs:string, cls:string}
75 + */
76 + private function noTranslateMarkup()
77 + {
78 + // Pass the row we already hold; otherwise the lookup re-queries form_content per render.
79 + $rawContent = $this->_form->isExist() ? $this->_form->getFieldsContent() : null;
80 + $enabled = TranslationManager::isFormTranslationEnabled($this->getFormID(), $rawContent);
81 + if ($enabled) {
82 + return ['attrs' => '', 'cls' => ''];
83 + }
84 + return ['attrs' => ' translate="no" data-no-translation', 'cls' => ' notranslate'];
85 + }
86 +
87 + /**
88 + * Carries the page language into the AJAX submission so validation and
89 + * confirmation strings match it. Empty when no provider resolves one.
90 + *
91 + * @return string
92 + */
93 + private function languageField()
94 + {
95 + $lang = (string) apply_filters('bitform_current_language', '', $this->getFormID());
96 + if ('' === $lang) {
97 + return '';
98 + }
99 + return '<input type="text" class="d-none" name="bf_lang" value="' . esc_attr($lang) . '">';
100 + }
101 +
102 + public function setTheme($theme = 'Default')
103 + {
61 104 $this->_theme = $theme;
62 105 }
63 106
64 - public function getError($field_name) {
107 + public function getError($field_name)
108 + {
65 109 return isset($this->_error[$field_name]) ? $this->_error[$field_name] : null;
66 110 }
67 111
68 - public function getFieldName($rowId) {
112 + public function getFieldName($rowId)
113 + {
69 114 $formIdentifier = $this->_form->getFormIdentifier();
70 115 $field_name = $rowId;
71 116 if ('button' === $this->_fields->{$rowId}->typ && 'submit' === $this->_fields->{$rowId}->btnTyp) {
72 117 $field_name = $formIdentifier;
@@ -74,13 +119,15 @@
74 119 $field_name .= isset($this->_fields->{$rowId}->lbl) ? preg_replace('/[\`\~\!\@\#\$\'\.\s\?\+\-\*\&\|\/\\\!]/', '_', $this->_fields->{$rowId}->lbl) : '';
75 120 return $field_name;
76 121 }
77 122
78 - public function getAtomicClaMap() {
123 + public function getAtomicClaMap()
124 + {
79 125 return $this->_formAtomicClsMap;
80 126 }
81 127
82 - public function getValue($field_name, $rowId) {
128 + public function getValue($field_name, $rowId)
129 + {
83 130 $valueToEsc = isset($this->_previousValue[$field_name]) ?
84 131 $this->_previousValue[$field_name] : (isset($this->_fields->{$rowId}->val) ? $this->_fields->{$rowId}->val : null);
85 132 if (empty($valueToEsc)) {
86 133 $value = null;
@@ -101,30 +148,190 @@
101 148 }
102 149 return $value;
103 150 }
104 151
105 - public function getFormID() {
152 + public function getFormID()
153 + {
106 154 return $this->_form->getFormID();
107 155 }
108 156
109 - public function fields($rowID) {
110 - return $this->_fields->{$rowID};
157 + public function fields($rowID)
158 + {
159 + return $this->_fields->{$rowID} ?? null;
111 160 }
112 161
113 - private function setView($hasFile) {
162 + public function getNestedLayout($rowID)
163 + {
164 + // return null;
165 + return isset($this->_nestedLayout->{$rowID}) ? $this->_nestedLayout->{$rowID} : null;
166 + }
167 +
168 + public function getFormInfo()
169 + {
170 + return $this->_formContents->formInfo;
171 + }
172 +
173 + private function getLayoutHtml($lay)
174 + {
175 + $html = '';
176 + foreach ($lay->lg as $row) {
177 + $html .= $this->_themeDetails->inputWrapper($this, $row->i);
178 + }
179 + return $html;
180 + }
181 +
182 + public function getConversationalView($hasFile, $msg = null)
183 + {
184 + $noTranslate = $this->noTranslateMarkup();
185 + $conversationSettings = $this->getFormInfo()->conversationalSettings;
186 + $this->_themeDetails = new DefaultConversationalTheme($conversationSettings);
114 187 $file_upload_tag = null;
188 + $restrictionMsg = '';
115 189 $formID = $this->_form->getFormID();
116 190 if ($hasFile) {
117 191 $file_upload_tag = 'enctype="multipart/form-data"';
118 - wp_enqueue_script('bitforms-frontend-file');
119 192 }
120 193 $formIdentifier = $this->_form->getFormIdentifier();
121 194 $fieldHtml = '';
122 - foreach ($this->_layout->lg as $row) {
123 - $fieldHtml .= $this->_themeDetails->inputWrapper($this, $row->i);
195 + $layouts = $this->_layout;
196 + $isMultiStep = is_array($layouts) && count($layouts) > 1;
197 + $layoutIsArray = !$isMultiStep && is_array($layouts);
198 + if ($layoutIsArray) {
199 + $layout = $layouts[0];
200 + $layouts = isset($layout->layout) ? $layout->layout : $layout;
201 + $this->_layout = $layouts;
124 202 }
203 + $conversationalHelper = new ConversationalHelpers($this->getFormID(), $conversationSettings);
204 + $welcomePageHtml = $conversationalHelper->getWelcomePageView();
205 + if (!$isMultiStep) {
206 + $tempLayout = $conversationalHelper->filterConversationalAllowedFields($layouts, $this->_fields);
207 + $fieldHtml .= $this->getLayoutHtml($tempLayout);
208 + } else {
209 + foreach ($layouts as $lay) {
210 + $tempLayout = $conversationalHelper->filterConversationalAllowedFields($lay->layout, $this->_fields);
211 + $fieldHtml .= $this->getLayoutHtml($tempLayout);
212 + }
213 + }
214 + $conversationNavigationHtml = $conversationalHelper->getNavigationView();
125 215
216 + // $confMsg = $this->_form->getSuccessMessageMarkups();
217 + $abandonmentMsg = $this->_form->getFormAbandonmentMessage();
218 +
219 + // if (!empty($this->_buttons)) {
220 + // $this->_buttons->name = $formIdentifier;
221 + // $buttonClass = "button-{$formIdentifier}";
222 + // $subBtn = $this->_themeDetails->getField($this->_buttons, $buttonClass, '');
223 + // }
224 +
225 + if (!empty($msg)) {
226 + $restrictionMsg = $this->restrictionOverlay($msg, $formID);
227 + }
228 + $formHTML =
229 + '
230 + <div id="' . $formIdentifier . '"' . $noTranslate['attrs'] . ' class="b' . $formID . '-bit-form ' . $this->_form->getAtomicCls("_frm-bg-b{$formID}") . ' bit-form bf-form-wrapper' . $noTranslate['cls'] . '">
231 + ' . $restrictionMsg . '
232 + <form novalidate id="form-' . $formIdentifier . '" class="_frm-bc' . $formID . ' bf-form" ' . $file_upload_tag . ' method=\'post\'>
233 + <input type="text" class="d-none" name="csrf" value="' . $this->_tokens['csrf'] . '">
234 + <input type="text" class="d-none" name="t_identity" value="' . $this->_tokens['t_identity'] . '">
235 + ' . $this->honeypotField() . '
236 + <input type="text" class="d-none" name="bitforms_id" value="bitforms_' . $this->_form->getFormID() . '">
237 + ' . $this->languageField() . '
238 + <div class="bc' . $formID . '-steps-container">
239 + ' . $welcomePageHtml . '
240 + ' . $fieldHtml . '
241 + ' . $conversationNavigationHtml . '
242 + </div>
243 + </form>
244 + <div id=\'bc-form-msg-wrp-' . $formIdentifier . '\' class="bc-form-msg-wrp"></div>
245 + ' . $abandonmentMsg . '
246 + </div>
247 +';
248 +
249 + return $formHTML;
250 + }
251 +
252 + /**
253 + * Restriction overlay; message may hold the visitor IP, so it is kses-filtered.
254 + *
255 + * @param string $msg Restriction message
256 + * @param int $formID Form ID
257 + *
258 + * @return string Overlay markup
259 + */
260 + private function restrictionOverlay($msg, $formID)
261 + {
262 + return '
263 + <div class="' . $this->_form->getAtomicCls("_frm-ovrly-b{$formID}") . '">
264 + <p class="' . $this->_form->getAtomicCls("_frm-ovrly-msg-b{$formID}") . '">
265 + ' . wp_kses_post($msg) . '
266 + </p>
267 + </div>
268 +';
269 + }
270 +
271 + private function setView($hasFile, $msg)
272 + {
273 + $noTranslate = $this->noTranslateMarkup();
274 + $file_upload_tag = null;
275 + $restrictionMsg = '';
276 + $formID = $this->_form->getFormID();
277 + if ($hasFile) {
278 + $file_upload_tag = 'enctype="multipart/form-data"';
279 + }
280 + $formIdentifier = $this->_form->getFormIdentifier();
281 + $fieldHtml = '';
282 + $layouts = $this->_layout;
283 + $isMultiStep = is_array($layouts) && count($layouts) > 1;
284 + $layoutIsArray = !$isMultiStep && is_array($layouts);
285 + if ($layoutIsArray) {
286 + $layout = $layouts[0];
287 + $layouts = isset($layout->layout) ? $layout->layout : $layout;
288 + $this->_layout = $layouts;
289 + }
290 + if (!$isMultiStep) {
291 + $fieldHtml .= $this->getLayoutHtml($layouts);
292 + } else {
293 + $formViewHelper = new FormViewHelper($this->_form, $this->_formContents);
294 +
295 + $fieldHtml .=
296 + '
297 + <div class=\'' . $this->_form->getAtomicCls("_frm-b{$formID}-stp-cntnr") . ' _frm-b-stp-cntnr\'>
298 + ' . $formViewHelper->getStepHeaderHtml() . '
299 + <div class=\'' . $this->_form->getAtomicCls("_frm-b{$formID}-stp-wrpr") . ' _frm-b-stp-wrpr\'>
300 + ' . $formViewHelper->getProgressBarMarkup() . '
301 + <div class=\'' . $this->_form->getAtomicCls("_frm-b{$formID}-stp-cntnt-wrpr") . ' _frm-b-stp-cntnt-wrpr\'>
302 +';
303 + foreach ($layouts as $key => $lay) {
304 + $hideOtherSteps = $key > 0 ? 'deactive' : '';
305 + $step = $key + 1;
306 + $ariaHidden = $key > 0 ? 'aria-hidden="true"' : '';
307 + $fieldHtml .=
308 + '
309 + <div
310 + class="' . $this->_form->getAtomicCls("_frm-b{$formID}-stp-cntnt") . ' _frm-b-stp-cntnt ' . $hideOtherSteps . '"
311 + data-step="' . $step . '"
312 + role="tabpanel"
313 + id="step-' . $formID . '-' . $step . '-panel"
314 + aria-labelledby="step-' . $formID . '-' . $step . '-tab"
315 + ' . $ariaHidden . '
316 + >
317 + <div class="_frm-b' . $formID . ' _frm-b">
318 + ' . $this->getLayoutHtml($lay->layout) . '
319 + </div>
320 + ' . $formViewHelper->getStepButtonMarkup() . '
321 + </div>
322 +';
323 + }
324 + $fieldHtml .=
325 + '
326 + </div>
327 + </div>
328 + </div>
329 +';
330 + }
331 +
126 332 $confMsg = $this->_form->getSuccessMessageMarkups();
333 + $abandonmentMsg = $this->_form->getFormAbandonmentMessage();
127 334
128 335 if (!empty($this->_buttons)) {
129 336 $this->_buttons->name = $formIdentifier;
130 337 $buttonClass = "button-{$formIdentifier}";
@@ -129,21 +336,38 @@
129 336 $this->_buttons->name = $formIdentifier;
130 337 $buttonClass = "button-{$formIdentifier}";
131 338 $subBtn = $this->_themeDetails->getField($this->_buttons, $buttonClass, '');
132 339 }
340 +
341 + if (!empty($msg)) {
342 + $restrictionMsg = $this->restrictionOverlay($msg, $formID);
343 + }
133 344 $formHTML =
134 - <<<HTMLa
135 - <div id="{$formIdentifier}" class="{$this->_form->getAtomicCls("_frm-bg-b{$formID}")}">
136 - <form novalidate id="form-{$formIdentifier}" class="{$this->_form->getAtomicCls("_frm-b{$formID}")}" $file_upload_tag method='post'>
137 - <input type="text" class="d-none" name="csrf" value="{$this->_tokens['csrf']}">
138 - <input type="text" class="d-none" name="t_identy" value="{$this->_tokens['t_identy']}">
139 - {$this->honeypotField()}
140 - <input type="text" class="d-none" name="bitforms_id" value="bitforms_{$this->_form->getFormID()}">
141 - $fieldHtml
345 + '
346 + <div id="' . $formIdentifier . '"' . $noTranslate['attrs'] . ' class="b' . $formID . '-bit-form ' . $this->_form->getAtomicCls("_frm-bg-b{$formID}") . ' bit-form bf-form-wrapper' . $noTranslate['cls'] . '">
347 + ' . $restrictionMsg . '
348 + <form novalidate id="form-' . $formIdentifier . '" class="' . $this->_form->getAtomicCls("_frm-b{$formID}") . ' bf-form" ' . $file_upload_tag . ' method=\'post\'>
349 + <input type="text" class="d-none" name="csrf" value="' . $this->_tokens['csrf'] . '">
350 + <input type="text" class="d-none" name="t_identity" value="' . $this->_tokens['t_identity'] . '">
351 + ' . $this->honeypotField() . '
352 + <input type="text" class="d-none" name="bitforms_id" value="bitforms_' . $this->_form->getFormID() . '">
353 + ' . $this->languageField() . '
354 + ' . $fieldHtml . '
142 355 </form>
143 - <div id='bf-form-msg-wrp-{$formIdentifier}'></div>
144 - $confMsg
356 + ' . $abandonmentMsg . '
357 + <div id=\'bf-form-msg-wrp-' . $formIdentifier . '\'></div>
358 + <!--
359 + Live Region for Screen Reader Announcements (Accessibility)
360 + -->
361 + <div
362 + class="bf-live-region"
363 + role="status"
364 + aria-live="polite"
365 + aria-atomic="true"
366 + style="position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;"
367 + ></div>
368 + ' . $confMsg . '
145 369 </div>
146 -HTMLa;
370 +';
147 371 return $formHTML;
148 372 }
149 373 }