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 +70 -48 6.2.5 → 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,10 +111,11 @@
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',
112 - 'request_id' => uniqid('ff_ai_')
115 + 'has_payment' => 'yes' == $paymentSetting['status'],
116 + 'request_id' => uniqid('ff_ai_'),
117 + 'save_usage' => apply_filters('fluentform/ai_save_usage', true),
113 118 ];
114 119
115 120 $result = (new FluentFormAIAPI())->makeRequest($queryArgs);
116 121
@@ -116,11 +121,14 @@
116 121
117 122 if (is_wp_error($result)) {
118 123 throw new Exception(esc_html($result->get_error_message()));
119 124 }
120 -
125 +
121 126 $response = trim(Arr::get($result, 'response', ''), '"');
122 - 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)) {
123 131 $response = trim($matches[1]);
124 132 }
125 133
126 134 $decoded = json_decode($response, true);
@@ -128,22 +136,24 @@
128 136 throw new Exception(esc_html__('Invalid response: Please try again!', 'fluentform'));
129 137 }
130 138 return $this->applyPromptHints($decoded, $args);
131 139 }
132 -
140 +
133 141 protected function getDefaultFields()
134 142 {
135 143 if ($this->allDefaultFields) {
136 144 return $this->allDefaultFields;
137 145 }
138 - /**
139 - * @var \FluentForm\App\Services\FormBuilder\Components
140 - */
141 146 $components = $this->app->make('components');
142 147 $this->app->doAction('fluentform/editor_init', $components);
143 148 $editorComponents = $components->toArray();
144 - $general = Arr::get($editorComponents, 'general', []);
145 - $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');
146 156 $container = Arr::get($editorComponents, 'container', []);
147 157
148 158 // Apply filter to get additional components
149 159 // The second parameter (true) is passed to prevent field loss when form ID is falsy loss some field
@@ -162,9 +172,9 @@
162 172 $payments = array_column($payments, null, 'element');
163 173 $this->allDefaultFields = array_merge($general, $payments, $advanced, ['container' => $container]);
164 174 return $this->allDefaultFields;
165 175 }
166 -
176 +
167 177 protected function processField($element, $field, $allFields)
168 178 {
169 179 if ('container' == $element) {
170 180 return $this->resolveContainerFields($field, $allFields);
@@ -187,9 +197,9 @@
187 197 if ($attributes = Arr::get($field, 'attributes')) {
188 198 $formatField['attributes'] = wp_parse_args($attributes, $matchedField['attributes']);
189 199 }
190 200
191 - $formatField['uniqElKey'] = "el_" . uniqid();
201 + $formatField['uniqElKey'] = 'el_' . uniqid();
192 202
193 203 if ('form_step' === $element) {
194 204 return $formatField;
195 205 }
@@ -225,12 +235,12 @@
225 235 $field['settings'] = wp_parse_args($targetSettings, $field['settings']);
226 236 }
227 237 }
228 238 }
229 -
239 +
230 240 return $formatField;
231 241 }
232 -
242 +
233 243 protected function resolveInput($field)
234 244 {
235 245 if (!is_array($field)) {
236 246 return false;
@@ -262,9 +272,9 @@
262 272 }
263 273 }
264 274 return false;
265 275 }
266 -
276 +
267 277 protected function getOptions($options = [])
268 278 {
269 279 $formattedOptions = [];
270 280 if (empty($options) || !is_array($options)) {
@@ -290,12 +300,12 @@
290 300 'label' => $label,
291 301 'value' => $value,
292 302 ];
293 303 }
294 -
304 +
295 305 return $formattedOptions;
296 306 }
297 -
307 +
298 308 protected function getBlankFormConfig()
299 309 {
300 310 $attributes = ['type' => 'form', 'predefined' => 'blank_form'];
301 311 $customForm = Form::resolvePredefinedForm($attributes);
@@ -303,9 +313,9 @@
303 313 $customForm['form_fields']['submitButton'] = $customForm['form']['submitButton'];
304 314 $customForm['form_fields'] = json_encode($customForm['form_fields']);
305 315 return $customForm;
306 316 }
307 -
317 +
308 318 protected function saveForm($formattedInputs, $title, $isStepForm = false, $isConversational = false, $customCss = '')
309 319 {
310 320 $customForm = $this->prepareCustomForm($formattedInputs, $isStepForm);
311 321 $data = Form::prepare($customForm);
@@ -310,11 +320,11 @@
310 320 $customForm = $this->prepareCustomForm($formattedInputs, $isStepForm);
311 321 $data = Form::prepare($customForm);
312 322
313 323 $form = $this->model->create($data);
314 - $form->title = $title ?: $form->title . ' (ChatGPT#' . $form->id . ')';
324 + $form->title = $title ? $title : $form->title . ' (ChatGPT#' . $form->id . ')';
315 325
316 - $formData = (object)$form->toArray();
326 + $formData = (object) $form->toArray();
317 327 if (FormFieldsParser::hasPaymentFields($formData)) {
318 328 $form->has_payment = 1;
319 329 }
320 330
@@ -332,9 +342,9 @@
332 342
333 343 if ($customCss = fluentformSanitizeCSS($customCss)) {
334 344 Helper::setFormMeta($form->id, '_custom_form_css', $customCss);
335 345 }
336 -
346 +
337 347 do_action('fluentform/inserted_new_form', $form->id, $data);
338 348 return $form;
339 349 }
340 350
@@ -387,8 +397,10 @@
387 397 return $matchedField;
388 398 }
389 399
390 400 /**
401 + * Build the step-wrapper skeleton used to wrap a multi-step form.
402 + *
391 403 * @return array
392 404 */
393 405 protected function getStepWrapper()
394 406 {
@@ -407,9 +419,9 @@
407 419 'enable_step_data_persistency' => 'no',
408 420 'enable_step_page_resume' => 'no',
409 421 ],
410 422 'editor_options' => [
411 - 'title' => 'Start Paging'
423 + 'title' => 'Start Paging',
412 424 ],
413 425 ],
414 426 'stepEnd' => [
415 427 'element' => 'step_end',
@@ -420,40 +432,50 @@
420 432 'settings' => [
421 433 'prev_btn' => [
422 434 'type' => 'default',
423 435 'text' => 'Previous',
424 - 'img_url' => ''
425 - ]
436 + 'img_url' => '',
437 + ],
426 438 ],
427 439 'editor_options' => [
428 - 'title' => 'End Paging'
440 + 'title' => 'End Paging',
429 441 ],
430 - ]
442 + ],
431 443 ];
432 444 }
433 -
445 +
434 446 private function getUserPrompt($args)
435 447 {
436 - $startingQuery = "Create a form for ";
448 + $startingQuery = 'Create a form for ';
437 449 $query = Sanitizer::sanitizeTextField(Arr::get($args, 'query'));
438 450 if (empty($query)) {
439 451 throw new Exception(esc_html__('Query is empty!', 'fluentform'));
440 452 }
441 -
442 - // Validate query length to prevent abuse (max 2000 characters)
443 - if (strlen($query) > 2000) {
444 - 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 + )));
445 462 }
446 -
463 +
447 464 $additionalQuery = Sanitizer::sanitizeTextField(Arr::get($args, 'additional_query'));
448 -
449 - // Validate additional query length (max 1000 characters)
450 - if ($additionalQuery && strlen($additionalQuery) > 1000) {
451 - 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 + )));
452 474 }
453 -
475 +
454 476 if ($additionalQuery) {
455 - $query .= "\n including questions for information like " . $additionalQuery . ".";
477 + $query .= "\n including questions for information like " . $additionalQuery . '.';
456 478 }
457 479 return $startingQuery . $query . $this->getPromptContractInstructions();
458 480 }
459 481
@@ -460,11 +482,11 @@
460 482 private function getPromptContractInstructions()
461 483 {
462 484 return "\n\nReturn strict JSON only. The user's instructions may be written in any language. "
463 485 . "Preserve labels and help text in the user's original language, but always return machine-readable field settings. "
464 - . "For every field, explicitly set whether it is required in settings.validation_rules.required.value when the prompt marks it as required or optional. "
465 - . "If the prompt lists allowed upload extensions, map them into settings.validation_rules.allowed_file_types.value. "
466 - . "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.';
467 489 }
468 490
469 491 private function applyPromptHints(array $form, array $args)
470 492 {
@@ -508,9 +530,9 @@
508 530 continue;
509 531 }
510 532
511 533 $hint = Arr::get($hints, $hintIndex);
512 - $hintIndex++;
534 + ++$hintIndex;
513 535
514 536 if (!$hint) {
515 537 continue;
516 538 }
@@ -674,9 +696,9 @@
674 696 'verplicht',
675 697 'wymagane',
676 698 'zorunlu',
677 699 '必填',
678 - '必須'
700 + '必須',
679 701 ];
680 702 }
681 703
682 704 private function getOptionalKeywords()
@@ -689,9 +711,9 @@
689 711 'isteğe bağlı',
690 712 'opcional',
691 713 'opcionales',
692 714 '选填',
693 - '任意'
715 + '任意',
694 716 ];
695 717 }
696 718
697 719 private function getFileUploadKeywords()
@@ -709,9 +731,9 @@
709 731 'carregamento de arquivo',
710 732 'caricamento file',
711 733 'bestandsupload',
712 734 'yükleme',
713 - '上传'
735 + '上传',
714 736 ];
715 737 }
716 738
717 739 private function getFieldDescriptorKeywords()
@@ -745,8 +767,8 @@
745 767 'caricamento',
746 768 'ficheiro',
747 769 'bestand',
748 770 'yükleme',
749 - '上传'
771 + '上传',
750 772 ];
751 773 }
752 774 }