PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.15
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.15
6.2.15 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 All 197 releases
← All changes | app/Modules/Ai/AiFormBuilder.php +68 -47 6.2.6 → 6.2.15 View file →
@@ -1,9 +1,9 @@
1 1 <?php
2 2
3 3 namespace FluentForm\App\Modules\Ai;
4 4
5 -defined('ABSPATH') or die;
5 +defined('ABSPATH') || die;
6 6
7 7 use Exception;
8 8 use FluentForm\App\Helpers\Helper;
9 9 use FluentForm\App\Models\Form;
@@ -46,8 +46,10 @@
46 46 }
47 47 }
48 48
49 49 /**
50 + * Map the AI-generated field list into a persisted form.
51 + *
50 52 * @param array $form
51 53 * @return Form|\FluentForm\Framework\Database\Query\Builder
52 54 * @throws Exception
53 55 */
@@ -87,8 +89,10 @@
87 89 return $this->saveForm($fluentFormFields, $title, $hasStep, $isConversational, $customCss);
88 90 }
89 91
90 92 /**
93 + * Send the prompt to the AI service and return the decoded form fields.
94 + *
91 95 * @param array $args
92 96 * @return array response form fields
93 97 * @throws Exception
94 98 */
@@ -107,9 +111,9 @@
107 111 'site_url' => site_url(),
108 112 'site_title' => get_bloginfo('name'),
109 113 'site_locale' => determine_locale(),
110 114 'has_pro' => Helper::hasPro(),
111 - 'has_payment' => $paymentSetting['status'] == 'yes',
115 + 'has_payment' => 'yes' == $paymentSetting['status'],
112 116 'request_id' => uniqid('ff_ai_'),
113 117 'save_usage' => apply_filters('fluentform/ai_save_usage', true),
114 118 ];
115 119
@@ -117,11 +121,14 @@
117 121
118 122 if (is_wp_error($result)) {
119 123 throw new Exception(esc_html($result->get_error_message()));
120 124 }
121 -
125 +
122 126 $response = trim(Arr::get($result, 'response', ''), '"');
123 - if (false !== preg_match('/```json(.*?)```/s', $response, $matches)) {
127 + // preg_match() returns 0 when there is no fence and false only on error,
128 + // so this must test for an actual match — otherwise an unfenced (and
129 + // perfectly valid) JSON reply is replaced with an empty string.
130 + if (1 === preg_match('/```json(.*?)```/s', $response, $matches)) {
124 131 $response = trim($matches[1]);
125 132 }
126 133
127 134 $decoded = json_decode($response, true);
@@ -129,22 +136,24 @@
129 136 throw new Exception(esc_html__('Invalid response: Please try again!', 'fluentform'));
130 137 }
131 138 return $this->applyPromptHints($decoded, $args);
132 139 }
133 -
140 +
134 141 protected function getDefaultFields()
135 142 {
136 143 if ($this->allDefaultFields) {
137 144 return $this->allDefaultFields;
138 145 }
139 - /**
140 - * @var \FluentForm\App\Services\FormBuilder\Components
141 - */
142 146 $components = $this->app->make('components');
143 147 $this->app->doAction('fluentform/editor_init', $components);
144 148 $editorComponents = $components->toArray();
145 - $general = Arr::get($editorComponents, 'general', []);
146 - $advanced = Arr::get($editorComponents, 'advanced', []);
149 + // Re-key by element name. The palette groups are keyed by element in
150 + // DefaultElements.php, but Components::sort() renumbers them 0..n for
151 + // the editor's JSON contract - so whether these arrive keyed or as a
152 + // list depends on whether anything rendered the palette earlier in the
153 + // request. resolveInput() matches on the key, so normalise here.
154 + $general = array_column(Arr::get($editorComponents, 'general', []), null, 'element');
155 + $advanced = array_column(Arr::get($editorComponents, 'advanced', []), null, 'element');
147 156 $container = Arr::get($editorComponents, 'container', []);
148 157
149 158 // Apply filter to get additional components
150 159 // The second parameter (true) is passed to prevent field loss when form ID is falsy loss some field
@@ -163,9 +172,9 @@
163 172 $payments = array_column($payments, null, 'element');
164 173 $this->allDefaultFields = array_merge($general, $payments, $advanced, ['container' => $container]);
165 174 return $this->allDefaultFields;
166 175 }
167 -
176 +
168 177 protected function processField($element, $field, $allFields)
169 178 {
170 179 if ('container' == $element) {
171 180 return $this->resolveContainerFields($field, $allFields);
@@ -188,9 +197,9 @@
188 197 if ($attributes = Arr::get($field, 'attributes')) {
189 198 $formatField['attributes'] = wp_parse_args($attributes, $matchedField['attributes']);
190 199 }
191 200
192 - $formatField['uniqElKey'] = "el_" . uniqid();
201 + $formatField['uniqElKey'] = 'el_' . uniqid();
193 202
194 203 if ('form_step' === $element) {
195 204 return $formatField;
196 205 }
@@ -226,12 +235,12 @@
226 235 $field['settings'] = wp_parse_args($targetSettings, $field['settings']);
227 236 }
228 237 }
229 238 }
230 -
239 +
231 240 return $formatField;
232 241 }
233 -
242 +
234 243 protected function resolveInput($field)
235 244 {
236 245 if (!is_array($field)) {
237 246 return false;
@@ -263,9 +272,9 @@
263 272 }
264 273 }
265 274 return false;
266 275 }
267 -
276 +
268 277 protected function getOptions($options = [])
269 278 {
270 279 $formattedOptions = [];
271 280 if (empty($options) || !is_array($options)) {
@@ -291,12 +300,12 @@
291 300 'label' => $label,
292 301 'value' => $value,
293 302 ];
294 303 }
295 -
304 +
296 305 return $formattedOptions;
297 306 }
298 -
307 +
299 308 protected function getBlankFormConfig()
300 309 {
301 310 $attributes = ['type' => 'form', 'predefined' => 'blank_form'];
302 311 $customForm = Form::resolvePredefinedForm($attributes);
@@ -304,9 +313,9 @@
304 313 $customForm['form_fields']['submitButton'] = $customForm['form']['submitButton'];
305 314 $customForm['form_fields'] = json_encode($customForm['form_fields']);
306 315 return $customForm;
307 316 }
308 -
317 +
309 318 protected function saveForm($formattedInputs, $title, $isStepForm = false, $isConversational = false, $customCss = '')
310 319 {
311 320 $customForm = $this->prepareCustomForm($formattedInputs, $isStepForm);
312 321 $data = Form::prepare($customForm);
@@ -311,11 +320,11 @@
311 320 $customForm = $this->prepareCustomForm($formattedInputs, $isStepForm);
312 321 $data = Form::prepare($customForm);
313 322
314 323 $form = $this->model->create($data);
315 - $form->title = $title ?: $form->title . ' (ChatGPT#' . $form->id . ')';
324 + $form->title = $title ? $title : $form->title . ' (ChatGPT#' . $form->id . ')';
316 325
317 - $formData = (object)$form->toArray();
326 + $formData = (object) $form->toArray();
318 327 if (FormFieldsParser::hasPaymentFields($formData)) {
319 328 $form->has_payment = 1;
320 329 }
321 330
@@ -333,9 +342,9 @@
333 342
334 343 if ($customCss = fluentformSanitizeCSS($customCss)) {
335 344 Helper::setFormMeta($form->id, '_custom_form_css', $customCss);
336 345 }
337 -
346 +
338 347 do_action('fluentform/inserted_new_form', $form->id, $data);
339 348 return $form;
340 349 }
341 350
@@ -388,8 +397,10 @@
388 397 return $matchedField;
389 398 }
390 399
391 400 /**
401 + * Build the step-wrapper skeleton used to wrap a multi-step form.
402 + *
392 403 * @return array
393 404 */
394 405 protected function getStepWrapper()
395 406 {
@@ -408,9 +419,9 @@
408 419 'enable_step_data_persistency' => 'no',
409 420 'enable_step_page_resume' => 'no',
410 421 ],
411 422 'editor_options' => [
412 - 'title' => 'Start Paging'
423 + 'title' => 'Start Paging',
413 424 ],
414 425 ],
415 426 'stepEnd' => [
416 427 'element' => 'step_end',
@@ -421,40 +432,50 @@
421 432 'settings' => [
422 433 'prev_btn' => [
423 434 'type' => 'default',
424 435 'text' => 'Previous',
425 - 'img_url' => ''
426 - ]
436 + 'img_url' => '',
437 + ],
427 438 ],
428 439 'editor_options' => [
429 - 'title' => 'End Paging'
440 + 'title' => 'End Paging',
430 441 ],
431 - ]
442 + ],
432 443 ];
433 444 }
434 -
445 +
435 446 private function getUserPrompt($args)
436 447 {
437 - $startingQuery = "Create a form for ";
448 + $startingQuery = 'Create a form for ';
438 449 $query = Sanitizer::sanitizeTextField(Arr::get($args, 'query'));
439 450 if (empty($query)) {
440 451 throw new Exception(esc_html__('Query is empty!', 'fluentform'));
441 452 }
442 -
443 - // Validate query length to prevent abuse (max 2000 characters)
444 - if (strlen($query) > 2000) {
445 - throw new Exception(esc_html__('Query is too long. Please limit your prompt to 2000 characters.', 'fluentform'));
453 +
454 + // Validate query length to prevent abuse (filterable; default 12000 characters)
455 + $maxQueryLength = (int) apply_filters('fluentform/ai_query_max_length', 12000);
456 + if (mb_strlen($query) > $maxQueryLength) {
457 + throw new Exception(esc_html(sprintf(
458 + /* translators: %d is the maximum allowed number of characters */
459 + __('Query is too long. Please limit your prompt to %d characters.', 'fluentform'),
460 + $maxQueryLength
461 + )));
446 462 }
447 -
463 +
448 464 $additionalQuery = Sanitizer::sanitizeTextField(Arr::get($args, 'additional_query'));
449 -
450 - // Validate additional query length (max 1000 characters)
451 - if ($additionalQuery && strlen($additionalQuery) > 1000) {
452 - throw new Exception(esc_html__('Additional query is too long. Please limit to 1000 characters.', 'fluentform'));
465 +
466 + // Validate additional query length (filterable; default 6000 characters)
467 + $maxAdditionalQueryLength = (int) apply_filters('fluentform/ai_additional_query_max_length', 6000);
468 + if ($additionalQuery && mb_strlen($additionalQuery) > $maxAdditionalQueryLength) {
469 + throw new Exception(esc_html(sprintf(
470 + /* translators: %d is the maximum allowed number of characters */
471 + __('Additional query is too long. Please limit to %d characters.', 'fluentform'),
472 + $maxAdditionalQueryLength
473 + )));
453 474 }
454 -
475 +
455 476 if ($additionalQuery) {
456 - $query .= "\n including questions for information like " . $additionalQuery . ".";
477 + $query .= "\n including questions for information like " . $additionalQuery . '.';
457 478 }
458 479 return $startingQuery . $query . $this->getPromptContractInstructions();
459 480 }
460 481
@@ -461,11 +482,11 @@
461 482 private function getPromptContractInstructions()
462 483 {
463 484 return "\n\nReturn strict JSON only. The user's instructions may be written in any language. "
464 485 . "Preserve labels and help text in the user's original language, but always return machine-readable field settings. "
465 - . "For every field, explicitly set whether it is required in settings.validation_rules.required.value when the prompt marks it as required or optional. "
466 - . "If the prompt lists allowed upload extensions, map them into settings.validation_rules.allowed_file_types.value. "
467 - . "If the prompt asks for a multi-step form with sections, include form_step elements between sections.";
486 + . 'For every field, explicitly set whether it is required in settings.validation_rules.required.value when the prompt marks it as required or optional. '
487 + . 'If the prompt lists allowed upload extensions, map them into settings.validation_rules.allowed_file_types.value. '
488 + . 'If the prompt asks for a multi-step form with sections, include form_step elements between sections.';
468 489 }
469 490
470 491 private function applyPromptHints(array $form, array $args)
471 492 {
@@ -509,9 +530,9 @@
509 530 continue;
510 531 }
511 532
512 533 $hint = Arr::get($hints, $hintIndex);
513 - $hintIndex++;
534 + ++$hintIndex;
514 535
515 536 if (!$hint) {
516 537 continue;
517 538 }
@@ -675,9 +696,9 @@
675 696 'verplicht',
676 697 'wymagane',
677 698 'zorunlu',
678 699 '必填',
679 - '必須'
700 + '必須',
680 701 ];
681 702 }
682 703
683 704 private function getOptionalKeywords()
@@ -690,9 +711,9 @@
690 711 'isteğe bağlı',
691 712 'opcional',
692 713 'opcionales',
693 714 '选填',
694 - '任意'
715 + '任意',
695 716 ];
696 717 }
697 718
698 719 private function getFileUploadKeywords()
@@ -710,9 +731,9 @@
710 731 'carregamento de arquivo',
711 732 'caricamento file',
712 733 'bestandsupload',
713 734 'yükleme',
714 - '上传'
735 + '上传',
715 736 ];
716 737 }
717 738
718 739 private function getFieldDescriptorKeywords()
@@ -746,8 +767,8 @@
746 767 'caricamento',
747 768 'ficheiro',
748 769 'bestand',
749 770 'yükleme',
750 - '上传'
771 + '上传',
751 772 ];
752 773 }
753 774 }