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 +151 -154 0.2.1 → 1.5 View file →
@@ -1,21 +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 -use IvyForms\Routes\Routes;
10 +use IvyForms\Common\Exceptions\ValidationException;
11 +use IvyForms\Common\Helpers\FieldHelper;
16 12 use IvyForms\Services\API\IvyFormsAPI;
17 -use IvyForms\Services\Translations\FrontendStrings;
13 +use IvyForms\Services\Form\Style\FormStyleService;
14 +use IvyForms\Services\Placeholder\PlaceholderLocalizationHelper;
15 +use IvyForms\Services\Shortcode\Helpers\ShortcodeHelper;
18 16
19 17 /**
20 18 * Class ShortcodeService
21 19 *
@@ -24,9 +22,11 @@
24 22 class ShortcodeService
25 23 {
26 24 public static int $counter = 0;
27 25 /**
28 - * @var mixed[]
26 + * Array keyed by unique render counters (string) -> payload array
27 + *
28 + * @var array<string, array<string, mixed>>
29 29 */
30 30 public static array $formList = [];
31 31
32 32 /**
@@ -34,14 +34,17 @@
34 34 *
35 35 * @param mixed[] $atts
36 36 *
37 37 * @return string
38 + * @throws ValidationException
38 39 */
39 40 public static function shortcodeHandler(array $atts): string
40 41 {
41 42 $atts = shortcode_atts(
42 43 [
43 - 'id' => '0'
44 + 'id' => '0',
45 + 'show_title' => null,
46 + 'show_description' => null,
44 47 ],
45 48 $atts
46 49 );
47 50
@@ -56,15 +59,38 @@
56 59 if (is_wp_error($form) || !($form->isPublished())) {
57 60 return '';
58 61 }
59 62
60 - $counter = self::$counter++;
63 + // Generate a stable unique identifier for this render
64 + $counter = $formId . '_' . uniqid();
61 65
62 - 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 + }
63 75
76 + self::getFormList($formId, $counter, $overrides);
77 +
64 78 self::enqueueScripts();
65 79
80 + // Generate form-specific CSS
81 + $formCSS = self::generateFormCSS($formId);
82 +
66 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 +
67 93 include IVYFORMS_PATH . '/view/frontend/view.php';
68 94 $html = ob_get_contents();
69 95 ob_end_clean();
70 96
@@ -74,13 +100,14 @@
74 100 /**
75 101 * Get form list
76 102 *
77 103 * @param int $formId
78 - * @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)
79 106 *
80 107 * @return void
81 108 */
82 - public static function getFormList(int $formId, int $counter)
109 + public static function getFormList(int $formId, string $counter, array $overrides = [])
83 110 {
84 111 $form = IvyFormsAPI::getForm($formId);
85 112
86 113 $fields = IvyFormsAPI::getFields($form->getId());
@@ -86,14 +113,10 @@
86 113 $fields = IvyFormsAPI::getFields($form->getId());
87 114
88 115 $confirmations = IvyFormsAPI::getConfirmations($form->getId());
89 116
90 - $fieldsMap = [];
117 + $fieldsMap = self::prepareFieldsMap($fields);
91 118
92 - foreach ($fields as $field) {
93 - $fieldsMap[] = $field->toArray();
94 - }
95 -
96 119 $confirmationsMap = [];
97 120
98 121 foreach ($confirmations as $confirmation) {
99 122 $confirmationsMap[] = is_object($confirmation) && method_exists($confirmation, 'toArray')
@@ -104,8 +127,16 @@
104 127 $form->setFields($fieldsMap);
105 128
106 129 $formArr = $form->toArray();
107 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 +
108 139 self::$formList[$counter] = [
109 140 'form' => $formArr,
110 141 'fields' => $fieldsMap,
111 142 'id' => $form->getId(),
@@ -115,180 +146,146 @@
115 146 ];
116 147 }
117 148
118 149 /**
119 - * Enqueue handler
150 + * Prepare fields map for a form
151 + *
152 + * @param array<int, object> $fields
153 + * @return array<int, array<string, mixed>>
120 154 */
121 - public static function enqueueScripts(): void
155 + private static function prepareFieldsMap(array $fields): array
122 156 {
123 - $scriptId = IVYFORMS_DEV ? 'ivyforms_scripts_dev_vite' : 'ivyforms_script_index'; // @phpstan-ignore-line
124 - $scriptSrc = IVYFORMS_DEV ? // @phpstan-ignore-line
125 - 'http://localhost:5173/src/assets/js/public/public.ts' :
126 - IVYFORMS_URL . 'frontend/dist/public.js';
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 + }
172 + }
173 + }
174 + $fieldsMap[] = self::resolveFieldPlaceholders($fieldArr, $generalData);
175 + }
176 + return $fieldsMap;
177 + }
127 178
128 - $styleSrc = IVYFORMS_DEV ? '' : IVYFORMS_URL . 'frontend/dist/index.css';// @phpstan-ignore-line
129 - $scriptVersion = IVYFORMS_DEV ? null : IVYFORMS_VERSION;// @phpstan-ignore-line
130 -
131 - wp_enqueue_script(
132 - $scriptId,
133 - $scriptSrc,
134 - [],
135 - $scriptVersion,
136 - true
137 - );
138 -
139 - if ($styleSrc) { // @phpstan-ignore-line
140 - wp_enqueue_style(
141 - 'ivyforms_style_index',
142 - $styleSrc,
143 - [],
144 - $scriptVersion
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;
189 + }
190 + $fieldArr[$key] = PlaceholderLocalizationHelper::resolveFieldString(
191 + $fieldArr[$key],
192 + $generalData
145 193 );
194 + }
146 195
147 - // TODO Create a helper service for adding styles
148 - wp_add_inline_style(
149 - 'ivyforms_style_index',
150 - /** @lang CSS */
151 - '.ivyforms-skeleton {
152 - width: 100%;
153 - max-width: 720px;
154 - margin: 0 auto;
155 - }
196 + return $fieldArr;
197 + }
156 198
157 - .ivyforms-skeleton-title {
158 - height: 28px;
159 - width: 40%;
160 - background: #dbdee3;
161 - margin-bottom: 12px;
162 - }
199 + /**
200 + * Enqueue handler
201 + */
202 + public static function enqueueScripts(): void
203 + {
204 + ShortcodeHelper::registerFrontendAssets(self::$formList);
205 + }
163 206
164 - .ivyforms-skeleton-field {
165 - margin-bottom: 16px;
166 - }
207 + /**
208 + * Generate CSS for a specific form
209 + *
210 + * @param int $formId
211 + * @return string
212 + */
213 + private static function generateFormCSS(int $formId): string
214 + {
215 + foreach (self::$formList as $formData) {
216 + if ($formData['id'] === $formId) {
217 + $formPayload = $formData['form'] ?? null;
218 + if (!is_array($formPayload)) {
219 + return '';
220 + }
167 221
168 - .ivyforms-skeleton-label {
169 - height: 22px;
170 - width: 20%;
171 - background: #dbdee3;
172 - font-size: 14px;
173 - border-radius: 4px;
174 - margin-bottom: 4px;
175 - }
222 + $styleSettings = $formPayload['styleSettings'] ?? null;
176 223
177 - .ivyforms-skeleton-input {
178 - line-height: 22px;
179 - font-size: 16px;
180 - background: #dbdee3;
181 - border-radius: 8px;
182 - width: 100%;
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 + }
183 231 }
232 + }
184 233
185 - .ivyforms-skeleton-email,
186 - .ivyforms-skeleton-textarea,
187 - .ivyforms-skeleton-text,
188 - .ivyforms-skeleton-number,
189 - .ivyforms-skeleton-phone {
190 - height: 40px;
191 - }
234 + return '';
235 + }
192 236
193 - .ivyforms-skeleton-textarea {
194 - height: 60px;
195 - }
237 + /**
238 + * Enqueue CAPTCHA scripts (reCAPTCHA, Turnstile, etc.) if configured
239 + *
240 + * @param string $scriptId
241 + */
242 + public static function enqueueRecaptchaScript(string $scriptId): void
243 + {
244 + // Delegate captcha discovery/enqueue to ShortcodeHelper while keeping BC wrapper here
245 + $securityService = ShortcodeHelper::getSecurityService();
246 + $formFields = ShortcodeHelper::getAllFormFields(self::$formList);
196 247
197 - .ivyforms-skeleton-button {
198 - height: 40px;
199 - width: 100px;
200 - background: #c0c5cb;
201 - border-radius: 6px;
202 - margin-top: 12px;
203 - }
248 + $securityConfig = $securityService->getFrontendSecurityConfig($formFields);
204 249
205 - .ivyforms-skeleton-animate {
206 - animation: pulse 1.5s infinite ease-in-out;
250 + if ($securityConfig['captcha']['enabled'] ?? false) {
251 + if (isset($securityConfig['captcha']['recaptcha'])) {
252 + ShortcodeHelper::enqueueGoogleRecaptchaScript($securityConfig['captcha']);
207 253 }
208 -
209 - @keyframes pulse {
210 - 0%, 100% {
211 - opacity: 1;
212 - }
213 - 50% {
214 - opacity: 0.4;
215 - }
254 + if (isset($securityConfig['captcha']['turnstile'])) {
255 + ShortcodeHelper::enqueueTurnstileScript($securityConfig['captcha']);
216 256 }
217 -
218 - .ivyforms-preview {
219 - width: 100%;
220 - max-width: 720px;
221 - margin: 0 auto;
222 - background: #fff;
223 - padding: 40px;
257 + if (isset($securityConfig['captcha']['hcaptcha'])) {
258 + ShortcodeHelper::enqueueHCaptchaScript($securityConfig['captcha']);
224 259 }
225 -
226 - .ivyforms-preview h1 {
227 - font-size: 50px !important;
228 - font-weight: normal !important;
229 - margin-top: 0 !important;
230 - margin-left: 0 !important;
231 - margin-bottom: 20px !important;
232 - padding: 0 !important;
233 - }'
234 - );
235 260 }
236 261
237 262 wp_localize_script(
238 263 $scriptId,
239 - 'wpIvyFormDataList',
240 - self::$formList
264 + 'wpIvyRecaptchaConfig',
265 + $securityConfig['captcha'] ?? []
241 266 );
242 267
243 268 wp_localize_script(
244 269 $scriptId,
245 - 'wpIvyLabels',
246 - array_merge(
247 - FrontendStrings::getFormRenderStrings(),
248 - )
270 + 'wpIvyTurnstileConfig',
271 + $securityConfig['captcha'] ?? []
249 272 );
250 273
251 274 wp_localize_script(
252 275 $scriptId,
253 - 'wpIvyApiSettings',
254 - [
255 - 'root' => esc_url_raw(rest_url()),
256 - 'nonce' => wp_create_nonce('wp_rest'),
257 - 'namespace' => Routes::$routeNamespace,
258 - ]
276 + 'wpIvyHCaptchaConfig',
277 + $securityConfig['captcha'] ?? []
259 278 );
260 - wp_localize_script(
261 - $scriptId,
262 - 'wpIvyDateFormat',
263 - [
264 - 'dateFormat' => get_option('date_format'),
265 - 'timeFormat' => get_option('time_format'),
266 - 'dateTimeFormat' => get_option('date_format') . ' ' . get_option('time_format'),
267 - 'locale' => get_locale(),
268 - ]
269 - );
270 279 }
271 280
272 281 /**
273 - * Add script attribute for module type
282 + * Backwards-compatible wrapper for script attribute filter
274 283 *
275 284 * @param array<string, mixed> $attributes
276 - *
277 285 * @return array<string, mixed>
278 286 */
279 287 public static function addScriptAttribute(array $attributes): array
280 288 {
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;
289 + return ShortcodeHelper::addScriptAttribute($attributes);
293 290 }
294 291 }