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