| @@ -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 | } |