payload array * * @var array> */ public static array $formList = []; /** * Shortcode handler * * @param mixed[] $atts * * @return string * @throws ValidationException */ public static function shortcodeHandler(array $atts): string { $atts = shortcode_atts( [ 'id' => '0', 'show_title' => null, 'show_description' => null, ], $atts ); if (empty($atts['id'])) { return ''; } $formId = (int)$atts['id']; $form = IvyFormsAPI::getForm($formId); // Check if the form is published if (is_wp_error($form) || !($form->isPublished())) { return ''; } // Generate a stable unique identifier for this render $counter = $formId . '_' . uniqid(); // Build overrides array for show_title and show_description $overrides = []; if ($atts['show_title'] !== null) { // Convert string 'true'/'false' or '1'/'0' to boolean $overrides['showTitle'] = filter_var($atts['show_title'], FILTER_VALIDATE_BOOLEAN); } if ($atts['show_description'] !== null) { $overrides['showDescription'] = filter_var($atts['show_description'], FILTER_VALIDATE_BOOLEAN); } self::getFormList($formId, $counter, $overrides); self::enqueueScripts(); ob_start(); include IVYFORMS_PATH . '/view/frontend/view.php'; $html = ob_get_contents(); ob_end_clean(); return $html; } /** * Get form list * * @param int $formId * @param string $counter Unique counter identifier for this render * @param array $overrides Optional overrides for form settings (e.g., showTitle, showDescription) * * @return void */ public static function getFormList(int $formId, string $counter, array $overrides = []) { $form = IvyFormsAPI::getForm($formId); $fields = IvyFormsAPI::getFields($form->getId()); $confirmations = IvyFormsAPI::getConfirmations($form->getId()); $fieldsMap = self::prepareFieldsMap($fields); $confirmationsMap = []; foreach ($confirmations as $confirmation) { $confirmationsMap[] = is_object($confirmation) && method_exists($confirmation, 'toArray') ? $confirmation->toArray() : $confirmation; } $form->setFields($fieldsMap); $formArr = $form->toArray(); // Apply overrides for showTitle and showDescription (from Gutenberg block or shortcode) if (array_key_exists('showTitle', $overrides)) { $formArr['showTitle'] = $overrides['showTitle']; } if (array_key_exists('showDescription', $overrides)) { $formArr['showDescription'] = $overrides['showDescription']; } self::$formList[$counter] = [ 'form' => $formArr, 'fields' => $fieldsMap, 'id' => $form->getId(), 'counter' => $counter, 'nonce' => wp_create_nonce('ivyformsFrontSubmissionNonce_' . $form->getId()), 'confirmations' => $confirmationsMap, ]; } /** * Prepare fields map for a form * * @param array $fields * @return array> */ private static function prepareFieldsMap(array $fields): array { $fieldsMap = []; foreach ($fields as $field) { $fieldArr = $field->toArray(); if ( isset($fieldArr['type']) && in_array( $fieldArr['type'], ['radio', 'checkbox', 'select', 'multi-select'], true ) ) { $fieldOptions = IvyFormsAPI::getFieldOptions($fieldArr['id']); $fieldArr['fieldOptions'] = []; foreach ($fieldOptions as $option) { if (is_object($option) && method_exists($option, 'toArray')) { $fieldArr['fieldOptions'][] = $option->toArray(); } } } $fieldsMap[] = $fieldArr; } return $fieldsMap; } /** * Enqueue handler * @throws ValidationException */ public static function enqueueScripts(): void { $scriptId = IVYFORMS_DEV ? 'ivyforms_scripts_dev_vite' : 'ivyforms_script_index'; // @phpstan-ignore-line $scriptSrc = IVYFORMS_DEV ? // @phpstan-ignore-line 'http://localhost:5173/src/assets/js/public/public.ts' : IVYFORMS_URL . 'frontend/dist/public.js'; $styleSrc = IVYFORMS_DEV ? '' : IVYFORMS_URL . 'frontend/dist/index.css'; // @phpstan-ignore-line $stylePublicSrc = IVYFORMS_DEV ? '' : IVYFORMS_URL . 'frontend/dist/public.css'; // @phpstan-ignore-line $scriptVersion = IVYFORMS_DEV ? null : IVYFORMS_VERSION;// @phpstan-ignore-line wp_enqueue_script( $scriptId, $scriptSrc, [], $scriptVersion, true ); // TODO: Improve this with merging styles if ($styleSrc) { // @phpstan-ignore-line wp_enqueue_style('ivyforms_style_index', $styleSrc, [], $scriptVersion); wp_enqueue_style('ivyforms_style_public', $stylePublicSrc, [], $scriptVersion); // Use helper for inline styles wp_add_inline_style( 'ivyforms_style_index', InlineStyleHelper::getFrontendInlineCss() ); } self::enqueueRecaptchaScript($scriptId); wp_localize_script( $scriptId, 'wpIvyFormDataList', self::$formList ); $frontendLabels = array_merge( FrontendStrings::getFormRenderStrings() ); /** * Filter frontend labels before localizing * Allows Pro version to merge additional strings * * @since 0.1.0 * @param array $frontendLabels Array of frontend label strings */ $frontendLabels = apply_filters('ivyforms/shortcode/frontend_labels', $frontendLabels); wp_localize_script($scriptId, 'wpIvyLabels', $frontendLabels); wp_localize_script( $scriptId, 'wpIvyUrls', [ 'pluginURL' => IVYFORMS_URL, 'siteURL' => esc_url_raw(site_url()), ] ); wp_localize_script( $scriptId, 'wpIvyApiSettings', [ 'root' => esc_url_raw(rest_url()), 'nonce' => wp_create_nonce('wp_rest'), 'namespace' => Routes::$routeNamespace, ] ); wp_localize_script( $scriptId, 'wpIvyDateFormat', [ 'dateFormat' => get_option('date_format'), 'timeFormat' => get_option('time_format'), 'dateTimeFormat' => get_option('date_format') . ' ' . get_option('time_format'), 'locale' => get_locale(), ] ); /** * Allow external plugins/components to enqueue additional scripts/styles. * @since 0.1.0 * * Args: * - string $scriptId The main script handle for the frontend (public) script */ do_action('ivyforms/shortcode/enqueue_scripts', $scriptId); } /** * Add script attribute for module type * * @param array $attributes * * @return array */ public static function addScriptAttribute(array $attributes): array { $scriptArray = [ 'ivyforms_scripts_dev_main-js', 'ivyforms_scripts_dev_vite-js', 'ivyforms_script_index-js', 'ivyforms_scripts_public-js', ]; if (isset($attributes['id']) && in_array($attributes['id'], $scriptArray, true)) { $attributes['type'] = 'module'; } return $attributes; } /** * Enqueue reCAPTCHA script if configured * * @param string $scriptId * @throws ValidationException */ public static function enqueueRecaptchaScript(string $scriptId): void { $securityService = self::getSecurityService(); $formFields = self::getAllFormFields(); // Get security configuration using our enhanced service $securityConfig = $securityService->getFrontendSecurityConfig($formFields); // Enqueue Google script if CAPTCHA is configured and has fields if (self::shouldEnqueueCaptchaScript($securityConfig)) { self::enqueueGoogleRecaptchaScript($securityConfig['captcha']); } wp_localize_script( $scriptId, 'wpIvyRecaptchaConfig', $securityConfig['captcha'] ?? [] ); } /** * Get security service instance * * @return SecurityService */ private static function getSecurityService(): SecurityService { $settingsService = new SettingsService(); $securityFactory = new SecurityServiceFactory($settingsService); return new SecurityService($securityFactory); } /** * Get all form fields from currently loaded forms * * @return array Array of form field objects * @throws ValidationException */ private static function getAllFormFields(): array { $allFields = []; foreach (self::$formList as $formData) { foreach ($formData['fields'] as $fieldData) { // Create a simple object to match the expected interface $field = FieldFactory::create($fieldData); $allFields[] = $field; } } return $allFields; } /** * Check if CAPTCHA script should be enqueued * * @param array $securityConfig * @return bool */ private static function shouldEnqueueCaptchaScript(array $securityConfig): bool { return isset($securityConfig['captcha']['recaptcha']) && $securityConfig['captcha']['enabled']; } /** * Enqueue Google reCAPTCHA script using security service * * @param array $captchaConfig */ private static function enqueueGoogleRecaptchaScript(array $captchaConfig): void { if (!isset($captchaConfig['recaptcha'])) { return; } $recaptchaConfig = $captchaConfig['recaptcha']; $scriptUrl = $recaptchaConfig['scriptUrl'] ?? ''; if (empty($scriptUrl)) { return; } wp_enqueue_script( 'google-recaptcha', $scriptUrl, [], null, // No version for external script true // Load in footer ); } }