PluginProbe
The Innovative Form Builder – IvyForms / 0.8
The Innovative Form Builder – IvyForms v0.8
1.4.1 1.4 trunk 0.1.2 0.2 0.2.1 0.3 0.3.1 0.4 0.5 0.6 0.6.1 0.6.1-backup 0.6.1.1 0.7 0.8 0.8.1 0.8.2 0.9 0.9.1 1.0 1.1 1.1.1 1.2 1.3
ivyforms / backend / src / Services / Shortcode / ShortcodeService.php

ShortcodeService.php in The Innovative Form Builder – IvyForms 0.8, at backend/src/Services/Shortcode/ShortcodeService.php

393 lines 11.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * @copyright © Melograno Venture Studio. All rights reserved.
5 * @licence See LICENCE.md for license details.
6 */
7
8 namespace IvyForms\Services\Shortcode;
9
10 // phpcs:disable PSR1.Files.SideEffects
11 if (!defined('ABSPATH')) {
12 exit; // Exit if accessed directly
13 }
14
15 use IvyForms\Common\Exceptions\ValidationException;
16 use IvyForms\Factory\Field\FieldFactory;
17 use IvyForms\Routes\Routes;
18 use IvyForms\Services\API\IvyFormsAPI;
19 use IvyForms\Services\Settings\SettingsService;
20 use IvyForms\Services\Translations\FrontendStrings;
21 use IvyForms\Factory\Security\SecurityServiceFactory;
22 use IvyForms\Services\Security\SecurityService;
23 use IvyForms\Repository\FieldOptions\FieldOptionsRepository;
24
25 /**
26 * Class ShortcodeService
27 *
28 * @package IvyForms\Services\Shortcode
29 */
30 class ShortcodeService
31 {
32 public static int $counter = 0;
33 /**
34 * Array keyed by unique render counters (string) -> payload array
35 *
36 * @var array<string, array<string, mixed>>
37 */
38 public static array $formList = [];
39
40 /**
41 * Shortcode handler
42 *
43 * @param mixed[] $atts
44 *
45 * @return string
46 * @throws ValidationException
47 */
48 public static function shortcodeHandler(array $atts): string
49 {
50 $atts = shortcode_atts(
51 [
52 'id' => '0',
53 'show_title' => null,
54 'show_description' => null,
55 ],
56 $atts
57 );
58
59 if (empty($atts['id'])) {
60 return '';
61 }
62
63 $formId = (int)$atts['id'];
64 $form = IvyFormsAPI::getForm($formId);
65
66 // Check if the form is published
67 if (is_wp_error($form) || !($form->isPublished())) {
68 return '';
69 }
70
71 // Generate a stable unique identifier for this render
72 $counter = $formId . '_' . uniqid();
73
74 // Build overrides array for show_title and show_description
75 $overrides = [];
76 if ($atts['show_title'] !== null) {
77 // Convert string 'true'/'false' or '1'/'0' to boolean
78 $overrides['showTitle'] = filter_var($atts['show_title'], FILTER_VALIDATE_BOOLEAN);
79 }
80 if ($atts['show_description'] !== null) {
81 $overrides['showDescription'] = filter_var($atts['show_description'], FILTER_VALIDATE_BOOLEAN);
82 }
83
84 self::getFormList($formId, $counter, $overrides);
85
86 self::enqueueScripts();
87
88 ob_start();
89 include IVYFORMS_PATH . '/view/frontend/view.php';
90 $html = ob_get_contents();
91 ob_end_clean();
92
93 return $html;
94 }
95
96 /**
97 * Get form list
98 *
99 * @param int $formId
100 * @param string $counter Unique counter identifier for this render
101 * @param array<string, mixed> $overrides Optional overrides for form settings (e.g., showTitle, showDescription)
102 *
103 * @return void
104 */
105 public static function getFormList(int $formId, string $counter, array $overrides = [])
106 {
107 $form = IvyFormsAPI::getForm($formId);
108
109 $fields = IvyFormsAPI::getFields($form->getId());
110
111 $confirmations = IvyFormsAPI::getConfirmations($form->getId());
112
113 $fieldsMap = self::prepareFieldsMap($fields);
114
115 $confirmationsMap = [];
116
117 foreach ($confirmations as $confirmation) {
118 $confirmationsMap[] = is_object($confirmation) && method_exists($confirmation, 'toArray')
119 ? $confirmation->toArray()
120 : $confirmation;
121 }
122
123 $form->setFields($fieldsMap);
124
125 $formArr = $form->toArray();
126
127 // Apply overrides for showTitle and showDescription (from Gutenberg block or shortcode)
128 if (array_key_exists('showTitle', $overrides)) {
129 $formArr['showTitle'] = $overrides['showTitle'];
130 }
131 if (array_key_exists('showDescription', $overrides)) {
132 $formArr['showDescription'] = $overrides['showDescription'];
133 }
134
135 self::$formList[$counter] = [
136 'form' => $formArr,
137 'fields' => $fieldsMap,
138 'id' => $form->getId(),
139 'counter' => $counter,
140 'nonce' => wp_create_nonce('ivyformsFrontSubmissionNonce_' . $form->getId()),
141 'confirmations' => $confirmationsMap,
142 ];
143 }
144
145 /**
146 * Prepare fields map for a form
147 *
148 * @param array<int, object> $fields
149 * @return array<int, array<string, mixed>>
150 */
151 private static function prepareFieldsMap(array $fields): array
152 {
153 $fieldsMap = [];
154 foreach ($fields as $field) {
155 $fieldArr = $field->toArray();
156 if (
157 isset($fieldArr['type']) && in_array(
158 $fieldArr['type'],
159 ['radio', 'checkbox', 'select', 'multi-select'],
160 true
161 )
162 ) {
163 $fieldOptions = IvyFormsAPI::getFieldOptions($fieldArr['id']);
164 $fieldArr['fieldOptions'] = [];
165 foreach ($fieldOptions as $option) {
166 if (is_object($option) && method_exists($option, 'toArray')) {
167 $fieldArr['fieldOptions'][] = $option->toArray();
168 }
169 }
170 }
171 $fieldsMap[] = $fieldArr;
172 }
173 return $fieldsMap;
174 }
175
176 /**
177 * Enqueue handler
178 * @throws ValidationException
179 */
180 public static function enqueueScripts(): void
181 {
182 $scriptId = IVYFORMS_DEV ? 'ivyforms_scripts_dev_vite' : 'ivyforms_script_index'; // @phpstan-ignore-line
183 $scriptSrc = IVYFORMS_DEV ? // @phpstan-ignore-line
184 'http://localhost:5173/src/assets/js/public/public.ts' :
185 IVYFORMS_URL . 'frontend/dist/public.js';
186
187 $styleSrc = IVYFORMS_DEV ? '' : IVYFORMS_URL . 'frontend/dist/index.css'; // @phpstan-ignore-line
188 $stylePublicSrc = IVYFORMS_DEV ? '' : IVYFORMS_URL . 'frontend/dist/public.css'; // @phpstan-ignore-line
189 $scriptVersion = IVYFORMS_DEV ? null : IVYFORMS_VERSION;// @phpstan-ignore-line
190
191 wp_enqueue_script(
192 $scriptId,
193 $scriptSrc,
194 [],
195 $scriptVersion,
196 true
197 );
198
199 // TODO: Improve this with merging styles
200 if ($styleSrc) { // @phpstan-ignore-line
201 wp_enqueue_style('ivyforms_style_index', $styleSrc, [], $scriptVersion);
202 wp_enqueue_style('ivyforms_style_public', $stylePublicSrc, [], $scriptVersion);
203
204 // Use helper for inline styles
205 wp_add_inline_style(
206 'ivyforms_style_index',
207 InlineStyleHelper::getFrontendInlineCss()
208 );
209 }
210 self::enqueueRecaptchaScript($scriptId);
211
212 wp_localize_script(
213 $scriptId,
214 'wpIvyFormDataList',
215 self::$formList
216 );
217
218 $frontendLabels = array_merge(
219 FrontendStrings::getFormRenderStrings()
220 );
221
222 /**
223 * Filter frontend labels before localizing
224 * Allows Pro version to merge additional strings
225 *
226 * @since 0.1.0
227 * @param array $frontendLabels Array of frontend label strings
228 */
229 $frontendLabels = apply_filters('ivyforms/shortcode/frontend_labels', $frontendLabels);
230
231 wp_localize_script($scriptId, 'wpIvyLabels', $frontendLabels);
232
233 wp_localize_script(
234 $scriptId,
235 'wpIvyUrls',
236 [
237 'pluginURL' => IVYFORMS_URL,
238 'siteURL' => esc_url_raw(site_url()),
239 ]
240 );
241
242 wp_localize_script(
243 $scriptId,
244 'wpIvyApiSettings',
245 [
246 'root' => esc_url_raw(rest_url()),
247 'nonce' => wp_create_nonce('wp_rest'),
248 'namespace' => Routes::$routeNamespace,
249 ]
250 );
251 wp_localize_script(
252 $scriptId,
253 'wpIvyDateFormat',
254 [
255 'dateFormat' => get_option('date_format'),
256 'timeFormat' => get_option('time_format'),
257 'dateTimeFormat' => get_option('date_format') . ' ' . get_option('time_format'),
258 'locale' => get_locale(),
259 ]
260 );
261
262 /**
263 * Allow external plugins/components to enqueue additional scripts/styles.
264 * @since 0.1.0
265 *
266 * Args:
267 * - string $scriptId The main script handle for the frontend (public) script
268 */
269 do_action('ivyforms/shortcode/enqueue_scripts', $scriptId);
270 }
271
272 /**
273 * Add script attribute for module type
274 *
275 * @param array<string, mixed> $attributes
276 *
277 * @return array<string, mixed>
278 */
279 public static function addScriptAttribute(array $attributes): array
280 {
281 $scriptArray = [
282 'ivyforms_scripts_dev_main-js',
283 'ivyforms_scripts_dev_vite-js',
284 'ivyforms_script_index-js',
285 'ivyforms_scripts_public-js',
286 ];
287
288 if (isset($attributes['id']) && in_array($attributes['id'], $scriptArray, true)) {
289 $attributes['type'] = 'module';
290 }
291
292 return $attributes;
293 }
294
295 /**
296 * Enqueue reCAPTCHA script if configured
297 *
298 * @param string $scriptId
299 * @throws ValidationException
300 */
301 public static function enqueueRecaptchaScript(string $scriptId): void
302 {
303 $securityService = self::getSecurityService();
304 $formFields = self::getAllFormFields();
305
306 // Get security configuration using our enhanced service
307 $securityConfig = $securityService->getFrontendSecurityConfig($formFields);
308
309 // Enqueue Google script if CAPTCHA is configured and has fields
310 if (self::shouldEnqueueCaptchaScript($securityConfig)) {
311 self::enqueueGoogleRecaptchaScript($securityConfig['captcha']);
312 }
313
314 wp_localize_script(
315 $scriptId,
316 'wpIvyRecaptchaConfig',
317 $securityConfig['captcha'] ?? []
318 );
319 }
320
321 /**
322 * Get security service instance
323 *
324 * @return SecurityService
325 */
326 private static function getSecurityService(): SecurityService
327 {
328 $settingsService = new SettingsService();
329 $securityFactory = new SecurityServiceFactory($settingsService);
330 return new SecurityService($securityFactory);
331 }
332
333 /**
334 * Get all form fields from currently loaded forms
335 *
336 * @return array<object> Array of form field objects
337 * @throws ValidationException
338 */
339 private static function getAllFormFields(): array
340 {
341 $allFields = [];
342
343 foreach (self::$formList as $formData) {
344 foreach ($formData['fields'] as $fieldData) {
345 // Create a simple object to match the expected interface
346 $field = FieldFactory::create($fieldData);
347
348 $allFields[] = $field;
349 }
350 }
351
352 return $allFields;
353 }
354
355 /**
356 * Check if CAPTCHA script should be enqueued
357 *
358 * @param array<string, mixed> $securityConfig
359 * @return bool
360 */
361 private static function shouldEnqueueCaptchaScript(array $securityConfig): bool
362 {
363 return isset($securityConfig['captcha']['recaptcha']) && $securityConfig['captcha']['enabled'];
364 }
365
366 /**
367 * Enqueue Google reCAPTCHA script using security service
368 *
369 * @param array<string, mixed> $captchaConfig
370 */
371 private static function enqueueGoogleRecaptchaScript(array $captchaConfig): void
372 {
373 if (!isset($captchaConfig['recaptcha'])) {
374 return;
375 }
376
377 $recaptchaConfig = $captchaConfig['recaptcha'];
378 $scriptUrl = $recaptchaConfig['scriptUrl'] ?? '';
379
380 if (empty($scriptUrl)) {
381 return;
382 }
383
384 wp_enqueue_script(
385 'google-recaptcha',
386 $scriptUrl,
387 [],
388 null, // No version for external script
389 true // Load in footer
390 );
391 }
392 }
393