PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.14
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.14
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 / Modules / MCP / Tools / FormTools.php

FormTools.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder 6.2.14, at app/Modules/MCP/Tools/FormTools.php

285 lines 13.0 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\Modules\MCP\Tools;
4
5 defined('ABSPATH') || exit;
6
7 use FluentForm\App\Models\Form;
8 use FluentForm\App\Modules\MCP\Support\ErrorCodes;
9 use FluentForm\App\Modules\MCP\Support\FormAccess;
10 use FluentForm\App\Modules\MCP\Support\FormCreator;
11 use FluentForm\App\Modules\MCP\Support\MCPHelper;
12 use FluentForm\App\Modules\MCP\Support\Mutation;
13 use FluentForm\App\Modules\MCP\Support\WriteGuard;
14 use FluentForm\App\Services\Form\FormService;
15
16 /**
17 * Form tools (read).
18 *
19 * The list-forms tool is the catalogue; get-form loads one form's field schema
20 * so the agent knows the exact field keys before reading entries. Both respect the
21 * user's form scope: a "specific forms" manager never sees a form outside their
22 * assignment, even by id.
23 */
24 class FormTools
25 {
26 public static function definitions()
27 {
28 return [
29 'fluentform/list-forms' => [
30 'label' => __('List Forms', 'fluentform'),
31 'group' => __('Forms', 'fluentform'),
32 'description' => __('Find and filter forms with compact rows (id, title, status, type, entry count). Use this to discover the form_id you need for list-submissions and get-form-stats.', 'fluentform'),
33 'input_schema' => [
34 'type' => 'object',
35 'properties' => [
36 'search' => ['type' => 'string', 'description' => 'Matches form title.'],
37 'status' => ['type' => 'string', 'enum' => ['published', 'unpublished']],
38 'sort_by' => ['type' => 'string', 'enum' => ['ASC', 'DESC'], 'default' => 'DESC'],
39 'page' => ['type' => 'integer', 'default' => 1],
40 'per_page' => ['type' => 'integer', 'default' => 15, 'description' => 'Max 100.'],
41 ],
42 ],
43 'execute_callback' => [self::class, 'listForms'],
44 'capability' => ['fluentform_forms_manager', 'fluentform_dashboard_access'],
45 'annotations' => ['readonly' => true],
46 ],
47
48 'fluentform/get-form' => [
49 'label' => __('Get Form', 'fluentform'),
50 'group' => __('Forms', 'fluentform'),
51 'description' => __('Full detail for one form: status, type, timestamps, and the field schema (each input key, label, and element type) so you know the exact keys that appear in submission responses.', 'fluentform'),
52 'input_schema' => [
53 'type' => 'object',
54 'properties' => [
55 'form_id' => ['type' => 'integer', 'description' => 'The form id (from list-forms).'],
56 ],
57 'required' => ['form_id'],
58 ],
59 'execute_callback' => [self::class, 'getForm'],
60 'capability' => ['fluentform_forms_manager', 'fluentform_dashboard_access'],
61 'annotations' => ['readonly' => true],
62 ],
63
64 'fluentform/create-form' => [
65 'label' => __('Create Form', 'fluentform'),
66 'group' => __('Forms', 'fluentform'),
67 'description' => __('Create a new form from a title and a list of fields. Each field needs a type (text, email, textarea, name, phone, number, url, dropdown, checkbox, radio, date) and a label. Omit fields to create a basic contact form (name, email, message). Call once with dry_run:true to preview the form that would be created and get a confirm_token, then call again with the same title and fields plus confirm_token to execute. Returns the new form id and editor URL.', 'fluentform'),
68 'input_schema' => [
69 'type' => 'object',
70 'properties' => array_merge([
71 'title' => ['type' => 'string', 'description' => 'The form title.'],
72 'fields' => [
73 'type' => 'array',
74 'description' => 'Fields to add, in order. Omit for a basic contact form.',
75 'items' => [
76 'type' => 'object',
77 'properties' => [
78 'type' => ['type' => 'string', 'description' => 'Field type, e.g. text, email, textarea, name, phone, number, url, dropdown, checkbox, radio, date.'],
79 'label' => ['type' => 'string', 'description' => 'Field label shown to the user.'],
80 ],
81 'required' => ['type'],
82 ],
83 ],
84 'is_conversational' => ['type' => 'boolean', 'description' => 'Create as a conversational form. Default false.'],
85 ], WriteGuard::schemaProps()),
86 'required' => ['title'],
87 ],
88 'execute_callback' => [self::class, 'createForm'],
89 'capability' => 'fluentform_forms_manager',
90 ],
91 ];
92 }
93
94 public static function createForm($params = [])
95 {
96 $title = isset($params['title']) ? sanitize_text_field($params['title']) : '';
97 if ('' === $title) {
98 return MCPHelper::error(ErrorCodes::MISSING_PARAM, __('title is required.', 'fluentform'), ['fields' => ['title']]);
99 }
100
101 $specFields = [];
102 $fieldsIn = (isset($params['fields']) && is_array($params['fields'])) ? $params['fields'] : [];
103 foreach ($fieldsIn as $field) {
104 if (!is_array($field)) {
105 continue;
106 }
107 $type = isset($field['type']) ? sanitize_text_field($field['type']) : '';
108 if ('' === $type) {
109 continue;
110 }
111 $label = isset($field['label']) ? sanitize_text_field($field['label']) : '';
112 $settings = [];
113 if ('' !== $label) {
114 $settings['label'] = $label;
115 $settings['admin_field_label'] = $label;
116 }
117 $specFields[] = ['type' => $type, 'settings' => $settings];
118 }
119
120 // No usable fields supplied -> sensible default so the form is never empty.
121 if (!$specFields) {
122 $specFields = [
123 ['type' => 'name', 'settings' => ['label' => __('Name', 'fluentform')]],
124 ['type' => 'email', 'settings' => ['label' => __('Email', 'fluentform')]],
125 ['type' => 'textarea', 'settings' => ['label' => __('Message', 'fluentform')]],
126 ];
127 }
128
129 $isConversational = !empty($params['is_conversational']);
130
131 // Nothing exists yet to fingerprint, so the token binds to the spec
132 // itself: the form previewed to the operator is the only form it can
133 // create. A different title or field list needs its own dry run.
134 $specHash = md5(wp_json_encode([$title, $specFields, $isConversational]));
135
136 return Mutation::runGuarded(
137 'fluentform/create-form',
138 $params,
139 'form:new:' . $specHash,
140 'spec:' . $specHash,
141 function () use ($title, $specFields, $isConversational) {
142 return [
143 'title' => $title,
144 'is_conversational' => $isConversational,
145 'field_count' => count($specFields),
146 'fields' => array_map(function ($field) {
147 return [
148 'type' => isset($field['type']) ? $field['type'] : null,
149 'label' => isset($field['settings']['label']) ? $field['settings']['label'] : null,
150 ];
151 }, $specFields),
152 ];
153 },
154 function () use ($title, $specFields, $isConversational) {
155 try {
156 $creator = new FormCreator();
157
158 // Submission responses are keyed by attributes.name; assign each
159 // field a unique one before saving so repeated field types never
160 // share a storage key (create() re-checks this, idempotently).
161 $specFields = $creator->assignStorageNames($specFields);
162
163 $form = $creator->create([
164 'title' => $title,
165 'fields' => $specFields,
166 'is_conversational' => $isConversational,
167 ]);
168 } catch (\Throwable $e) {
169 return MCPHelper::error(ErrorCodes::CREATE_FAILED, $e->getMessage(), ['retryable' => false]);
170 }
171
172 return MCPHelper::envelope(
173 sprintf(
174 /* translators: 1: form title, 2: form id */
175 __('Form "%1$s" created (#%2$d).', 'fluentform'),
176 $form->title,
177 (int) $form->id
178 ),
179 [
180 'id' => (int) $form->id,
181 'title' => $form->title,
182 'status' => $form->status,
183 'fields' => count($specFields),
184 'edit_url' => admin_url('admin.php?page=fluent_forms&form_id=' . (int) $form->id . '&route=editor'),
185 ]
186 );
187 },
188 function ($result) {
189 return (is_array($result) && isset($result['data']['id'])) ? ['form_id' => (int) $result['data']['id']] : [];
190 }
191 );
192 }
193
194 public static function listForms($params = [])
195 {
196 $paging = MCPHelper::pagination($params, 15);
197
198 $query = Form::query()->orderBy('id', strtoupper(isset($params['sort_by']) && 'ASC' === strtoupper($params['sort_by']) ? 'ASC' : 'DESC'));
199
200 FormAccess::applyScope($query, 'id');
201 if (!empty($params['search'])) {
202 $query->where('title', 'LIKE', '%' . sanitize_text_field($params['search']) . '%');
203 }
204 if (!empty($params['status'])) {
205 $query->where('status', sanitize_text_field($params['status']));
206 }
207
208 $paginator = $query->paginate($paging['per_page'], ['*'], 'page', $paging['page']);
209 $total = MCPHelper::paginatorTotal($paginator);
210
211 $items = MCPHelper::paginatorItems($paginator);
212 $counts = FormAccess::entryCounts(array_map(function ($form) {
213 return (int) $form->id;
214 }, is_array($items) ? $items : iterator_to_array($items)));
215
216 $rows = [];
217 foreach ($items as $form) {
218 $formId = (int) $form->id;
219 $rows[] = [
220 'id' => $formId,
221 'title' => $form->title,
222 'status' => $form->status,
223 'type' => $form->type,
224 'entries' => isset($counts[$formId]) ? $counts[$formId] : null,
225 'created_at' => MCPHelper::toIso8601($form->created_at),
226 ];
227 }
228
229 return MCPHelper::envelope(
230 sprintf(
231 /* translators: %d: number of matching forms */
232 _n('%d form found.', '%d forms found.', $total, 'fluentform'),
233 $total
234 ),
235 ['forms' => $rows],
236 MCPHelper::pagingMeta($paginator)
237 );
238 }
239
240 public static function getForm($params = [])
241 {
242 $form = FormAccess::resolveForm($params);
243 if (is_wp_error($form)) {
244 return $form;
245 }
246 $formId = (int) $form->id;
247
248 $fields = [];
249 try {
250 $service = new FormService();
251 $schema = $service->getInputsAndLabels($formId);
252 $inputs = isset($schema['inputs']) ? $schema['inputs'] : [];
253 $labels = isset($schema['labels']) ? $schema['labels'] : [];
254 foreach ($inputs as $key => $input) {
255 $fields[] = [
256 'key' => $key,
257 'label' => isset($labels[$key]) ? $labels[$key] : (isset($input['admin_label']) ? $input['admin_label'] : $key),
258 'element' => isset($input['element']) ? $input['element'] : null,
259 ];
260 }
261 } catch (\Throwable $e) {
262 $fields = [];
263 }
264
265 return MCPHelper::envelope(
266 sprintf(
267 /* translators: 1: form title, 2: field count */
268 __('Form "%1$s" has %2$d fields.', 'fluentform'),
269 $form->title,
270 count($fields)
271 ),
272 [
273 'id' => (int) $form->id,
274 'title' => $form->title,
275 'status' => $form->status,
276 'type' => $form->type,
277 'entries' => FormAccess::entryCount($form->id),
278 'created_at' => MCPHelper::toIso8601($form->created_at),
279 'updated_at' => MCPHelper::toIso8601($form->updated_at),
280 'fields' => $fields,
281 ]
282 );
283 }
284 }
285