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 +120 -197 0.7 → 1.5 View file →
@@ -6,22 +6,14 @@
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 /**
@@ -46,9 +40,11 @@
46 40 public static function shortcodeHandler(array $atts): string
47 41 {
48 42 $atts = shortcode_atts(
49 43 [
50 - 'id' => '0'
44 + 'id' => '0',
45 + 'show_title' => null,
46 + 'show_description' => null,
51 47 ],
52 48 $atts
53 49 );
54 50
@@ -63,15 +59,38 @@
63 59 if (is_wp_error($form) || !($form->isPublished())) {
64 60 return '';
65 61 }
66 62
67 - $counter = self::$counter++;
63 + // Generate a stable unique identifier for this render
64 + $counter = $formId . '_' . uniqid();
68 65
69 - 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 + }
70 75
76 + self::getFormList($formId, $counter, $overrides);
77 +
71 78 self::enqueueScripts();
72 79
80 + // Generate form-specific CSS
81 + $formCSS = self::generateFormCSS($formId);
82 +
73 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 +
74 93 include IVYFORMS_PATH . '/view/frontend/view.php';
75 94 $html = ob_get_contents();
76 95 ob_end_clean();
77 96
@@ -81,13 +100,14 @@
81 100 /**
82 101 * Get form list
83 102 *
84 103 * @param int $formId
85 - * @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)
86 106 *
87 107 * @return void
88 108 */
89 - public static function getFormList(int $formId, int $counter)
109 + public static function getFormList(int $formId, string $counter, array $overrides = [])
90 110 {
91 111 $form = IvyFormsAPI::getForm($formId);
92 112
93 113 $fields = IvyFormsAPI::getFields($form->getId());
@@ -107,8 +127,16 @@
107 127 $form->setFields($fieldsMap);
108 128
109 129 $formArr = $form->toArray();
110 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 +
111 139 self::$formList[$counter] = [
112 140 'form' => $formArr,
113 141 'fields' => $fieldsMap,
114 142 'id' => $form->getId(),
@@ -125,17 +153,16 @@
125 153 * @return array<int, array<string, mixed>>
126 154 */
127 155 private static function prepareFieldsMap(array $fields): array
128 156 {
157 + $generalData = PlaceholderLocalizationHelper::getGeneralData();
129 158 $fieldsMap = [];
130 159 foreach ($fields as $field) {
131 160 $fieldArr = $field->toArray();
161 + $fieldType = isset($fieldArr['type']) ? (string) $fieldArr['type'] : '';
132 162 if (
133 - isset($fieldArr['type']) && in_array(
134 - $fieldArr['type'],
135 - ['radio', 'checkbox', 'select', 'multi-select'],
136 - true
137 - )
163 + $fieldType !== ''
164 + && (FieldHelper::fieldTypeHasOptions($fieldType) || $fieldType === 'rating')
138 165 ) {
139 166 $fieldOptions = IvyFormsAPI::getFieldOptions($fieldArr['id']);
140 167 $fieldArr['fieldOptions'] = [];
141 168 foreach ($fieldOptions as $option) {
@@ -143,149 +170,94 @@
143 170 $fieldArr['fieldOptions'][] = $option->toArray();
144 171 }
145 172 }
146 173 }
147 - $fieldsMap[] = $fieldArr;
174 + $fieldsMap[] = self::resolveFieldPlaceholders($fieldArr, $generalData);
148 175 }
149 176 return $fieldsMap;
150 177 }
151 178
152 179 /**
153 - * Enqueue handler
154 - * @throws ValidationException
180 + * @param array<string, mixed> $fieldArr
181 + * @param array<string, string|int> $generalData
182 + * @return array<string, mixed>
155 183 */
156 - public static function enqueueScripts(): void
184 + private static function resolveFieldPlaceholders(array $fieldArr, array $generalData): array
157 185 {
158 - $scriptId = IVYFORMS_DEV ? 'ivyforms_scripts_dev_vite' : 'ivyforms_script_index'; // @phpstan-ignore-line
159 - $scriptSrc = IVYFORMS_DEV ? // @phpstan-ignore-line
160 - 'http://localhost:5173/src/assets/js/public/public.ts' :
161 - IVYFORMS_URL . 'frontend/dist/public.js';
162 -
163 - $styleSrc = IVYFORMS_DEV ? '' : IVYFORMS_URL . 'frontend/dist/index.css'; // @phpstan-ignore-line
164 - $stylePublicSrc = IVYFORMS_DEV ? '' : IVYFORMS_URL . 'frontend/dist/public.css'; // @phpstan-ignore-line
165 - $scriptVersion = IVYFORMS_DEV ? null : IVYFORMS_VERSION;// @phpstan-ignore-line
166 -
167 - wp_enqueue_script(
168 - $scriptId,
169 - $scriptSrc,
170 - [],
171 - $scriptVersion,
172 - true
173 - );
174 -
175 - // TODO: Improve this with merging styles
176 - if ($styleSrc) { // @phpstan-ignore-line
177 - wp_enqueue_style('ivyforms_style_index', $styleSrc, [], $scriptVersion);
178 - wp_enqueue_style('ivyforms_style_public', $stylePublicSrc, [], $scriptVersion);
179 -
180 - // Use helper for inline styles
181 - wp_add_inline_style(
182 - 'ivyforms_style_index',
183 - InlineStyleHelper::getFrontendInlineCss()
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
184 193 );
185 194 }
186 - self::enqueueRecaptchaScript($scriptId);
187 195
188 - wp_localize_script(
189 - $scriptId,
190 - 'wpIvyFormDataList',
191 - self::$formList
192 - );
196 + return $fieldArr;
197 + }
193 198
194 - $frontendLabels = array_merge(
195 - FrontendStrings::getFormRenderStrings()
196 - );
197 -
198 - /**
199 - * Filter frontend labels before localizing
200 - * Allows Pro version to merge additional strings
201 - *
202 - * @since 0.1.0
203 - * @param array $frontendLabels Array of frontend label strings
204 - */
205 - $frontendLabels = apply_filters('ivyforms/shortcode/frontend_labels', $frontendLabels);
206 -
207 - wp_localize_script($scriptId, 'wpIvyLabels', $frontendLabels);
208 -
209 - wp_localize_script(
210 - $scriptId,
211 - 'wpIvyUrls',
212 - [
213 - 'pluginURL' => IVYFORMS_URL,
214 - 'siteURL' => esc_url_raw(site_url()),
215 - ]
216 - );
217 -
218 - wp_localize_script(
219 - $scriptId,
220 - 'wpIvyApiSettings',
221 - [
222 - 'root' => esc_url_raw(rest_url()),
223 - 'nonce' => wp_create_nonce('wp_rest'),
224 - 'namespace' => Routes::$routeNamespace,
225 - ]
226 - );
227 - wp_localize_script(
228 - $scriptId,
229 - 'wpIvyDateFormat',
230 - [
231 - 'dateFormat' => get_option('date_format'),
232 - 'timeFormat' => get_option('time_format'),
233 - 'dateTimeFormat' => get_option('date_format') . ' ' . get_option('time_format'),
234 - 'locale' => get_locale(),
235 - ]
236 - );
237 -
238 - /**
239 - * Allow external plugins/components to enqueue additional scripts/styles.
240 - * @since 0.1.0
241 - *
242 - * Args:
243 - * - string $scriptId The main script handle for the frontend (public) script
244 - */
245 - do_action('ivyforms/shortcode/enqueue_scripts', $scriptId);
199 + /**
200 + * Enqueue handler
201 + */
202 + public static function enqueueScripts(): void
203 + {
204 + ShortcodeHelper::registerFrontendAssets(self::$formList);
246 205 }
247 206
248 207 /**
249 - * Add script attribute for module type
208 + * Generate CSS for a specific form
250 209 *
251 - * @param array<string, mixed> $attributes
252 - *
253 - * @return array<string, mixed>
210 + * @param int $formId
211 + * @return string
254 212 */
255 - public static function addScriptAttribute(array $attributes): array
213 + private static function generateFormCSS(int $formId): string
256 214 {
257 - $scriptArray = [
258 - 'ivyforms_scripts_dev_main-js',
259 - 'ivyforms_scripts_dev_vite-js',
260 - 'ivyforms_script_index-js',
261 - 'ivyforms_scripts_public-js',
262 - ];
215 + foreach (self::$formList as $formData) {
216 + if ($formData['id'] === $formId) {
217 + $formPayload = $formData['form'] ?? null;
218 + if (!is_array($formPayload)) {
219 + return '';
220 + }
263 221
264 - if (isset($attributes['id']) && in_array($attributes['id'], $scriptArray, true)) {
265 - $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 + }
266 232 }
267 233
268 - return $attributes;
234 + return '';
269 235 }
270 236
271 237 /**
272 - * Enqueue reCAPTCHA script if configured
238 + * Enqueue CAPTCHA scripts (reCAPTCHA, Turnstile, etc.) if configured
273 239 *
274 240 * @param string $scriptId
275 - * @throws ValidationException
276 241 */
277 242 public static function enqueueRecaptchaScript(string $scriptId): void
278 243 {
279 - $securityService = self::getSecurityService();
280 - $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);
281 247
282 - // Get security configuration using our enhanced service
283 248 $securityConfig = $securityService->getFrontendSecurityConfig($formFields);
284 249
285 - // Enqueue Google script if CAPTCHA is configured and has fields
286 - if (self::shouldEnqueueCaptchaScript($securityConfig)) {
287 - 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 + }
288 260 }
289 261
290 262 wp_localize_script(
291 263 $scriptId,
@@ -291,78 +263,29 @@
291 263 $scriptId,
292 264 'wpIvyRecaptchaConfig',
293 265 $securityConfig['captcha'] ?? []
294 266 );
295 - }
296 267
297 - /**
298 - * Get security service instance
299 - *
300 - * @return SecurityService
301 - */
302 - private static function getSecurityService(): SecurityService
303 - {
304 - $settingsService = new SettingsService();
305 - $securityFactory = new SecurityServiceFactory($settingsService);
306 - return new SecurityService($securityFactory);
307 - }
268 + wp_localize_script(
269 + $scriptId,
270 + 'wpIvyTurnstileConfig',
271 + $securityConfig['captcha'] ?? []
272 + );
308 273
309 - /**
310 - * Get all form fields from currently loaded forms
311 - *
312 - * @return array<object> Array of form field objects
313 - * @throws ValidationException
314 - */
315 - private static function getAllFormFields(): array
316 - {
317 - $allFields = [];
318 -
319 - foreach (self::$formList as $formData) {
320 - foreach ($formData['fields'] as $fieldData) {
321 - // Create a simple object to match the expected interface
322 - $field = FieldFactory::create($fieldData);
323 -
324 - $allFields[] = $field;
325 - }
326 - }
327 -
328 - return $allFields;
274 + wp_localize_script(
275 + $scriptId,
276 + 'wpIvyHCaptchaConfig',
277 + $securityConfig['captcha'] ?? []
278 + );
329 279 }
330 280
331 281 /**
332 - * Check if CAPTCHA script should be enqueued
282 + * Backwards-compatible wrapper for script attribute filter
333 283 *
334 - * @param array<string, mixed> $securityConfig
335 - * @return bool
284 + * @param array<string, mixed> $attributes
285 + * @return array<string, mixed>
336 286 */
337 - private static function shouldEnqueueCaptchaScript(array $securityConfig): bool
287 + public static function addScriptAttribute(array $attributes): array
338 288 {
339 - return isset($securityConfig['captcha']['recaptcha']) && $securityConfig['captcha']['enabled'];
340 - }
341 -
342 - /**
343 - * Enqueue Google reCAPTCHA script using security service
344 - *
345 - * @param array<string, mixed> $captchaConfig
346 - */
347 - private static function enqueueGoogleRecaptchaScript(array $captchaConfig): void
348 - {
349 - if (!isset($captchaConfig['recaptcha'])) {
350 - return;
351 - }
352 -
353 - $recaptchaConfig = $captchaConfig['recaptcha'];
354 - $scriptUrl = $recaptchaConfig['scriptUrl'] ?? '';
355 -
356 - if (empty($scriptUrl)) {
357 - return;
358 - }
359 -
360 - wp_enqueue_script(
361 - 'google-recaptcha',
362 - $scriptUrl,
363 - [],
364 - null, // No version for external script
365 - true // Load in footer
366 - );
289 + return ShortcodeHelper::addScriptAttribute($attributes);
367 290 }
368 291 }