PluginProbe
The Innovative Form Builder – IvyForms / 1.5.1
The Innovative Form Builder – IvyForms v1.5.1
1.5.1 1.5 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 All 27 releases
← All changes | backend/src/Services/Shortcode/ShortcodeService.php +144 -259 0.3 → 1.5.1 View file →
@@ -1,26 +1,19 @@
1 1 <?php
2 2
3 3 /**
4 - * @copyright © TMS-Plugins. All rights reserved.
4 + * @copyright © Melograno Venture Studio. All rights reserved.
5 5 * @licence See LICENCE.md for license details.
6 6 */
7 7
8 8 namespace IvyForms\Services\Shortcode;
9 9
10 -// phpcs:disable PSR1.Files.SideEffects
11 -if (!defined('ABSPATH')) {
12 - exit; // Exit if accessed directly
13 -}
14 -
15 10 use IvyForms\Common\Exceptions\ValidationException;
16 -use IvyForms\Factory\Field\FieldFactory;
17 -use IvyForms\Routes\Routes;
11 +use IvyForms\Common\Helpers\FieldHelper;
18 12 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;
13 +use IvyForms\Services\Form\Style\FormStyleService;
14 +use IvyForms\Services\Placeholder\PlaceholderLocalizationHelper;
15 +use IvyForms\Services\Shortcode\Helpers\ShortcodeHelper;
23 16
24 17 /**
25 18 * Class ShortcodeService
26 19 *
@@ -29,9 +22,11 @@
29 22 class ShortcodeService
30 23 {
31 24 public static int $counter = 0;
32 25 /**
33 - * @var mixed[]
26 + * Array keyed by unique render counters (string) -> payload array
27 + *
28 + * @var array<string, array<string, mixed>>
34 29 */
35 30 public static array $formList = [];
36 31
37 32 /**
@@ -39,14 +34,17 @@
39 34 *
40 35 * @param mixed[] $atts
41 36 *
42 37 * @return string
38 + * @throws ValidationException
43 39 */
44 40 public static function shortcodeHandler(array $atts): string
45 41 {
46 42 $atts = shortcode_atts(
47 43 [
48 - 'id' => '0'
44 + 'id' => '0',
45 + 'show_title' => null,
46 + 'show_description' => null,
49 47 ],
50 48 $atts
51 49 );
52 50
@@ -61,15 +59,38 @@
61 59 if (is_wp_error($form) || !($form->isPublished())) {
62 60 return '';
63 61 }
64 62
65 - $counter = self::$counter++;
63 + // Generate a stable unique identifier for this render
64 + $counter = $formId . '_' . uniqid();
66 65
67 - self::getFormList($formId, $counter);
66 + // Build overrides array for show_title and show_description
67 + $overrides = [];
68 + if ($atts['show_title'] !== null) {
69 + // Convert string 'true'/'false' or '1'/'0' to boolean
70 + $overrides['showTitle'] = filter_var($atts['show_title'], FILTER_VALIDATE_BOOLEAN);
71 + }
72 + if ($atts['show_description'] !== null) {
73 + $overrides['showDescription'] = filter_var($atts['show_description'], FILTER_VALIDATE_BOOLEAN);
74 + }
68 75
76 + self::getFormList($formId, $counter, $overrides);
77 +
69 78 self::enqueueScripts();
70 79
80 + // Generate form-specific CSS
81 + $formCSS = self::generateFormCSS($formId);
82 +
71 83 ob_start();
84 +
85 + // Output CSS inline before the form
86 + if (!empty($formCSS)) {
87 + $styleId = 'ivyforms-form-styles-' . sanitize_key(
88 + str_replace('.', '-', $counter)
89 + );
90 + echo '<style id="' . esc_attr($styleId) . '">' . "\n" . $formCSS . "\n" . '</style>' . "\n";
91 + }
92 +
72 93 include IVYFORMS_PATH . '/view/frontend/view.php';
73 94 $html = ob_get_contents();
74 95 ob_end_clean();
75 96
@@ -79,13 +100,14 @@
79 100 /**
80 101 * Get form list
81 102 *
82 103 * @param int $formId
83 - * @param int $counter
104 + * @param string $counter Unique counter identifier for this render
105 + * @param array<string, mixed> $overrides Optional overrides for form settings (e.g., showTitle, showDescription)
84 106 *
85 107 * @return void
86 108 */
87 - public static function getFormList(int $formId, int $counter)
109 + public static function getFormList(int $formId, string $counter, array $overrides = [])
88 110 {
89 111 $form = IvyFormsAPI::getForm($formId);
90 112
91 113 $fields = IvyFormsAPI::getFields($form->getId());
@@ -91,14 +113,10 @@
91 113 $fields = IvyFormsAPI::getFields($form->getId());
92 114
93 115 $confirmations = IvyFormsAPI::getConfirmations($form->getId());
94 116
95 - $fieldsMap = [];
117 + $fieldsMap = self::prepareFieldsMap($fields);
96 118
97 - foreach ($fields as $field) {
98 - $fieldsMap[] = $field->toArray();
99 - }
100 -
101 119 $confirmationsMap = [];
102 120
103 121 foreach ($confirmations as $confirmation) {
104 122 $confirmationsMap[] = is_object($confirmation) && method_exists($confirmation, 'toArray')
@@ -109,8 +127,16 @@
109 127 $form->setFields($fieldsMap);
110 128
111 129 $formArr = $form->toArray();
112 130
131 + // Apply overrides for showTitle and showDescription (from Gutenberg block or shortcode)
132 + if (array_key_exists('showTitle', $overrides)) {
133 + $formArr['showTitle'] = $overrides['showTitle'];
134 + }
135 + if (array_key_exists('showDescription', $overrides)) {
136 + $formArr['showDescription'] = $overrides['showDescription'];
137 + }
138 +
113 139 self::$formList[$counter] = [
114 140 'form' => $formArr,
115 141 'fields' => $fieldsMap,
116 142 'id' => $form->getId(),
@@ -120,210 +146,118 @@
120 146 ];
121 147 }
122 148
123 149 /**
124 - * Enqueue handler
150 + * Prepare fields map for a form
151 + *
152 + * @param array<int, object> $fields
153 + * @return array<int, array<string, mixed>>
125 154 */
126 - public static function enqueueScripts(): void
155 + private static function prepareFieldsMap(array $fields): array
127 156 {
128 - $scriptId = IVYFORMS_DEV ? 'ivyforms_scripts_dev_vite' : 'ivyforms_script_index'; // @phpstan-ignore-line
129 - $scriptSrc = IVYFORMS_DEV ? // @phpstan-ignore-line
130 - 'http://localhost:5173/src/assets/js/public/public.ts' :
131 - IVYFORMS_URL . 'frontend/dist/public.js';
132 -
133 - $styleSrc = IVYFORMS_DEV ? '' : IVYFORMS_URL . 'frontend/dist/index.css'; // @phpstan-ignore-line
134 - $stylePublicSrc = IVYFORMS_DEV ? '' : IVYFORMS_URL . 'frontend/dist/public.css'; // @phpstan-ignore-line
135 - $scriptVersion = IVYFORMS_DEV ? null : IVYFORMS_VERSION;// @phpstan-ignore-line
136 -
137 - wp_enqueue_script(
138 - $scriptId,
139 - $scriptSrc,
140 - [],
141 - $scriptVersion,
142 - true
143 - );
144 -
145 - // TODO: Improve this with merging styles
146 - if ($styleSrc) { // @phpstan-ignore-line
147 - wp_enqueue_style(
148 - 'ivyforms_style_index',
149 - $styleSrc,
150 - [],
151 - $scriptVersion
152 - );
153 - wp_enqueue_style(
154 - 'ivyforms_style_public',
155 - $stylePublicSrc,
156 - [],
157 - $scriptVersion
158 - );
159 -
160 - // TODO Create a helper service for adding styles
161 - wp_add_inline_style(
162 - 'ivyforms_style_index',
163 - /** @lang CSS */
164 - '.ivyforms-skeleton {
165 - width: 100%;
166 - max-width: 720px;
167 - margin: 0 auto;
168 - }
169 -
170 - .ivyforms-skeleton-title {
171 - height: 28px;
172 - width: 40%;
173 - background: #dbdee3;
174 - margin-bottom: 12px;
175 - }
176 -
177 - .ivyforms-skeleton-field {
178 - margin-bottom: 16px;
179 - }
180 -
181 - .ivyforms-skeleton-label {
182 - height: 22px;
183 - width: 20%;
184 - background: #dbdee3;
185 - font-size: 14px;
186 - border-radius: 4px;
187 - margin-bottom: 4px;
188 - }
189 -
190 - .ivyforms-skeleton-input {
191 - line-height: 22px;
192 - font-size: 16px;
193 - background: #dbdee3;
194 - border-radius: 8px;
195 - width: 100%;
196 - }
197 -
198 - .ivyforms-skeleton-email,
199 - .ivyforms-skeleton-textarea,
200 - .ivyforms-skeleton-text,
201 - .ivyforms-skeleton-number,
202 - .ivyforms-skeleton-phone {
203 - height: 40px;
204 - }
205 -
206 - .ivyforms-skeleton-textarea {
207 - height: 60px;
208 - }
209 -
210 - .ivyforms-skeleton-button {
211 - height: 40px;
212 - width: 100px;
213 - background: #c0c5cb;
214 - border-radius: 6px;
215 - margin-top: 12px;
216 - }
217 -
218 - .ivyforms-skeleton-animate {
219 - animation: pulse 1.5s infinite ease-in-out;
220 - }
221 -
222 - @keyframes pulse {
223 - 0%, 100% {
224 - opacity: 1;
157 + $generalData = PlaceholderLocalizationHelper::getGeneralData();
158 + $fieldsMap = [];
159 + foreach ($fields as $field) {
160 + $fieldArr = $field->toArray();
161 + $fieldType = isset($fieldArr['type']) ? (string) $fieldArr['type'] : '';
162 + if (
163 + $fieldType !== ''
164 + && (FieldHelper::fieldTypeHasOptions($fieldType) || $fieldType === 'rating')
165 + ) {
166 + $fieldOptions = IvyFormsAPI::getFieldOptions($fieldArr['id']);
167 + $fieldArr['fieldOptions'] = [];
168 + foreach ($fieldOptions as $option) {
169 + if (is_object($option) && method_exists($option, 'toArray')) {
170 + $fieldArr['fieldOptions'][] = $option->toArray();
171 + }
225 172 }
226 - 50% {
227 - opacity: 0.4;
228 - }
229 173 }
174 + $fieldsMap[] = self::resolveFieldPlaceholders($fieldArr, $generalData);
175 + }
176 + return $fieldsMap;
177 + }
230 178
231 - .ivyforms-preview {
232 - width: 100%;
233 - max-width: 720px;
234 - margin: 0 auto;
235 - background: #fff;
236 - padding: 40px;
179 + /**
180 + * @param array<string, mixed> $fieldArr
181 + * @param array<string, string|int> $generalData
182 + * @return array<string, mixed>
183 + */
184 + private static function resolveFieldPlaceholders(array $fieldArr, array $generalData): array
185 + {
186 + foreach (['placeholder', 'defaultValue'] as $key) {
187 + if (!isset($fieldArr[$key]) || !is_string($fieldArr[$key])) {
188 + continue;
237 189 }
238 -
239 - .ivyforms-preview h1 {
240 - font-size: 50px !important;
241 - font-weight: normal !important;
242 - margin-top: 0 !important;
243 - margin-left: 0 !important;
244 - margin-bottom: 20px !important;
245 - padding: 0 !important;
246 - }'
190 + $fieldArr[$key] = PlaceholderLocalizationHelper::resolveFieldString(
191 + $fieldArr[$key],
192 + $generalData
247 193 );
248 194 }
249 - self::enqueueRecaptchaScript($scriptId);
250 195
251 - wp_localize_script(
252 - $scriptId,
253 - 'wpIvyFormDataList',
254 - self::$formList
255 - );
196 + return $fieldArr;
197 + }
256 198
257 - wp_localize_script(
258 - $scriptId,
259 - 'wpIvyLabels',
260 - array_merge(
261 - FrontendStrings::getFormRenderStrings(),
262 - )
263 - );
264 -
265 - wp_localize_script(
266 - $scriptId,
267 - 'wpIvyApiSettings',
268 - [
269 - 'root' => esc_url_raw(rest_url()),
270 - 'nonce' => wp_create_nonce('wp_rest'),
271 - 'namespace' => Routes::$routeNamespace,
272 - ]
273 - );
274 - wp_localize_script(
275 - $scriptId,
276 - 'wpIvyDateFormat',
277 - [
278 - 'dateFormat' => get_option('date_format'),
279 - 'timeFormat' => get_option('time_format'),
280 - 'dateTimeFormat' => get_option('date_format') . ' ' . get_option('time_format'),
281 - 'locale' => get_locale(),
282 - ]
283 - );
199 + /**
200 + * Enqueue handler
201 + */
202 + public static function enqueueScripts(): void
203 + {
204 + ShortcodeHelper::registerFrontendAssets(self::$formList);
284 205 }
285 206
286 207 /**
287 - * Add script attribute for module type
208 + * Generate CSS for a specific form
288 209 *
289 - * @param array<string, mixed> $attributes
290 - *
291 - * @return array<string, mixed>
210 + * @param int $formId
211 + * @return string
292 212 */
293 - public static function addScriptAttribute(array $attributes): array
213 + private static function generateFormCSS(int $formId): string
294 214 {
295 - $scriptArray = [
296 - 'ivyforms_scripts_dev_main-js',
297 - 'ivyforms_scripts_dev_vite-js',
298 - 'ivyforms_script_index-js',
299 - 'ivyforms_scripts_public-js',
300 - ];
215 + foreach (self::$formList as $formData) {
216 + if ($formData['id'] === $formId) {
217 + $formPayload = $formData['form'] ?? null;
218 + if (!is_array($formPayload)) {
219 + return '';
220 + }
301 221
302 - if (isset($attributes['id']) && in_array($attributes['id'], $scriptArray, true)) {
303 - $attributes['type'] = 'module';
222 + $styleSettings = $formPayload['styleSettings'] ?? null;
223 +
224 + if (!empty($styleSettings) && is_array($styleSettings)) {
225 + return FormStyleService::generateFormCSS(
226 + $formId,
227 + $styleSettings,
228 + isset($formPayload['formType']) ? (string)$formPayload['formType'] : null
229 + );
230 + }
231 + }
304 232 }
305 233
306 - return $attributes;
234 + return '';
307 235 }
308 236
309 237 /**
310 - * Enqueue reCAPTCHA script if configured
238 + * Enqueue CAPTCHA scripts (reCAPTCHA, Turnstile, etc.) if configured
311 239 *
312 240 * @param string $scriptId
313 - * @throws ValidationException
314 241 */
315 242 public static function enqueueRecaptchaScript(string $scriptId): void
316 243 {
317 - $securityService = self::getSecurityService();
318 - $formFields = self::getAllFormFields();
244 + // Delegate captcha discovery/enqueue to ShortcodeHelper while keeping BC wrapper here
245 + $securityService = ShortcodeHelper::getSecurityService();
246 + $formFields = ShortcodeHelper::getAllFormFields(self::$formList);
319 247
320 - // Get security configuration using our enhanced service
321 248 $securityConfig = $securityService->getFrontendSecurityConfig($formFields);
322 249
323 - // Enqueue Google script if CAPTCHA is configured and has fields
324 - if (self::shouldEnqueueCaptchaScript($securityConfig)) {
325 - self::enqueueGoogleRecaptchaScript($securityConfig['captcha']);
250 + if ($securityConfig['captcha']['enabled'] ?? false) {
251 + if (isset($securityConfig['captcha']['recaptcha'])) {
252 + ShortcodeHelper::enqueueGoogleRecaptchaScript($securityConfig['captcha']);
253 + }
254 + if (isset($securityConfig['captcha']['turnstile'])) {
255 + ShortcodeHelper::enqueueTurnstileScript($securityConfig['captcha']);
256 + }
257 + if (isset($securityConfig['captcha']['hcaptcha'])) {
258 + ShortcodeHelper::enqueueHCaptchaScript($securityConfig['captcha']);
259 + }
326 260 }
327 261
328 262 wp_localize_script(
329 263 $scriptId,
@@ -329,78 +263,29 @@
329 263 $scriptId,
330 264 'wpIvyRecaptchaConfig',
331 265 $securityConfig['captcha'] ?? []
332 266 );
333 - }
334 267
335 - /**
336 - * Get security service instance
337 - *
338 - * @return SecurityService
339 - */
340 - private static function getSecurityService(): SecurityService
341 - {
342 - $settingsService = new SettingsService();
343 - $securityFactory = new SecurityServiceFactory($settingsService);
344 - return new SecurityService($securityFactory);
345 - }
268 + wp_localize_script(
269 + $scriptId,
270 + 'wpIvyTurnstileConfig',
271 + $securityConfig['captcha'] ?? []
272 + );
346 273
347 - /**
348 - * Get all form fields from currently loaded forms
349 - *
350 - * @return array<object> Array of form field objects
351 - * @throws ValidationException
352 - */
353 - private static function getAllFormFields(): array
354 - {
355 - $allFields = [];
356 -
357 - foreach (self::$formList as $formData) {
358 - foreach ($formData['fields'] as $fieldData) {
359 - // Create a simple object to match the expected interface
360 - $field = FieldFactory::create($fieldData);
361 -
362 - $allFields[] = $field;
363 - }
364 - }
365 -
366 - return $allFields;
274 + wp_localize_script(
275 + $scriptId,
276 + 'wpIvyHCaptchaConfig',
277 + $securityConfig['captcha'] ?? []
278 + );
367 279 }
368 280
369 281 /**
370 - * Check if CAPTCHA script should be enqueued
282 + * Backwards-compatible wrapper for script attribute filter
371 283 *
372 - * @param array<string, mixed> $securityConfig
373 - * @return bool
284 + * @param array<string, mixed> $attributes
285 + * @return array<string, mixed>
374 286 */
375 - private static function shouldEnqueueCaptchaScript(array $securityConfig): bool
287 + public static function addScriptAttribute(array $attributes): array
376 288 {
377 - return isset($securityConfig['captcha']['recaptcha']) && $securityConfig['captcha']['enabled'];
378 - }
379 -
380 - /**
381 - * Enqueue Google reCAPTCHA script using security service
382 - *
383 - * @param array<string, mixed> $captchaConfig
384 - */
385 - private static function enqueueGoogleRecaptchaScript(array $captchaConfig): void
386 - {
387 - if (!isset($captchaConfig['recaptcha'])) {
388 - return;
389 - }
390 -
391 - $recaptchaConfig = $captchaConfig['recaptcha'];
392 - $scriptUrl = $recaptchaConfig['scriptUrl'] ?? '';
393 -
394 - if (empty($scriptUrl)) {
395 - return;
396 - }
397 -
398 - wp_enqueue_script(
399 - 'google-recaptcha',
400 - $scriptUrl,
401 - [],
402 - null, // No version for external script
403 - true // Load in footer
404 - );
289 + return ShortcodeHelper::addScriptAttribute($attributes);
405 290 }
406 291 }