esc_html__('Alpha Styles', 'alpha-addons-for-gravity-forms'), 'aria-label' => esc_html__('Alpha Styles', 'alpha-addons-for-gravity-forms'), 'url' => self::get_editor_url($form_id), 'target' => '_blank', 'menu_class' => 'gf_form_toolbar_alphagf_styles', 'capabilities' => array('gravityforms_edit_forms'), 'priority' => 700, ); return $menu_items; } /** * Adds "Styles" to the form list row actions, between Duplicate and Trash. */ public function add_form_list_action($form_actions, $form_id) { $form_actions['alphagf_styles'] = array( 'label' => esc_html__('Styles', 'alpha-addons-for-gravity-forms'), 'aria-label' => esc_html__('Style this form', 'alpha-addons-for-gravity-forms'), 'url' => self::get_editor_url($form_id), 'target' => '_blank', 'menu_class' => 'gf_form_toolbar_alphagf_styles', 'capabilities' => array('gravityforms_edit_forms'), 'priority' => 550, ); return $form_actions; } public static function get_editor_url($form_id) { return add_query_arg(self::QUERY_VAR, absint($form_id), home_url('/')); } /** * Render the full page styler (or its preview iframe) for ?alpha-styles-preview={form_id}. */ public function maybe_render_editor() { // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only screens, guarded by capability checks. if (! isset($_GET[self::QUERY_VAR])) { return; } $form_id = absint(wp_unslash($_GET[self::QUERY_VAR])); $is_frame = ! empty($_GET[self::FRAME_VAR]); // phpcs:enable WordPress.Security.NonceVerification.Recommended // auth_redirect() validates the admin-only auth cookie, which isn't sent to frontend URLs. if (! is_user_logged_in()) { wp_safe_redirect(wp_login_url(self::get_editor_url($form_id))); exit; } if (! GFCommon::current_user_can_any('gravityforms_edit_forms')) { wp_die(esc_html__('Sorry, you are not allowed to style this form.', 'alpha-addons-for-gravity-forms'), '', array('response' => 403)); } $form = $form_id ? GFAPI::get_form($form_id) : false; if (! $form) { wp_die(esc_html__('The requested form could not be found.', 'alpha-addons-for-gravity-forms'), '', array('response' => 404)); } if (! defined('DONOTCACHEPAGE')) { define('DONOTCACHEPAGE', true); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- Standard constant read by caching plugins. } nocache_headers(); add_filter('show_admin_bar', '__return_false'); if ($is_frame) { $this->render_frame($form); } else { $this->render_editor($form); } exit; } private function render_editor($form) { $form_id = absint($form['id']); $saved = rgar($form, self::META_KEY); $forms = array(); foreach (GFFormsModel::get_forms(null, 'title') as $item) { $forms[] = array( 'id' => absint($item->id), 'title' => $item->title, ); } $data = array( 'formId' => $form_id, 'formTitle' => $form['title'], 'forms' => $forms, 'values' => (object) self::strip_desktop_layouts(is_array($saved) && is_array(rgar($saved, 'values')) ? $saved['values'] : array()), 'themes' => $this->get_themes(), 'presets' => self::get_presets(), 'schema' => self::get_schema(), 'fonts' => self::get_fonts(), /** * Filter which form features the editor shows sections for (a section's 'requires' key). * * @param array $features * @param array $form */ 'features' => apply_filters('alphagf_styles_features', array( 'steps' => self::form_has_steps($form), 'progressbar' => self::form_has_steps($form, 'percentage'), 'list' => self::form_has_list($form), 'repeater' => self::form_has_repeater($form), 'imagepicker' => self::form_has_image_picker($form), 'qrcode' => self::form_has_qrcode($form), 'emailverify' => self::form_has_email_verification($form), ), $form), 'choiceFields' => self::get_choice_fields($form), 'imagePickerFields' => self::get_image_picker_fields($form), 'imagePickerOptions' => self::get_image_picker_options(), 'frameUrl' => add_query_arg(array(self::QUERY_VAR => $form_id, self::FRAME_VAR => 1), home_url('/')), 'editorUrl' => self::get_editor_url(0), 'backUrl' => admin_url('admin.php?page=gf_edit_forms&id=' . $form_id), 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce(self::NONCE_ACTION), 'i18n' => self::get_i18n(), ); /** * Filter the data passed to the editor script (window.AlphaStylesData). * * Extensions can add 'icons' (name => SVG paths) and 'partTargets' (preview part => control key). * * @param array $data * @param array $form */ $data = apply_filters('alphagf_styles_editor_data', $data, $form); /** * Filter extra script URLs loaded in the editor before js/editor.js. * * @param string[] $scripts * @param array $form */ $extra_scripts = (array) apply_filters('alphagf_styles_editor_scripts', array(), $form); $assets_url = $this->get_base_url(); wp_register_style('alphagf-styles-editor-font', 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap', array(), null); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- Google Fonts URL. wp_register_style('alphagf-styles-editor', $assets_url . '/css/editor.css', array('alphagf-styles-editor-font'), $this->_version); // Data handle with no source: prints window.AlphaStylesData ahead of the extra scripts and editor.js. wp_register_script('alphagf-styles-editor-data', false, array(), $this->_version, false); wp_add_inline_script('alphagf-styles-editor-data', 'window.AlphaStylesData = ' . wp_json_encode($data, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ';'); $editor_deps = array('alphagf-styles-editor-data'); foreach (array_values($extra_scripts) as $index => $script_url) { $handle = 'alphagf-styles-editor-extra-' . $index; wp_register_script($handle, $script_url, array('alphagf-styles-editor-data'), null, false); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- Third-party URLs carry their own version. $editor_deps[] = $handle; } wp_register_script('alphagf-styles-editor', $assets_url . '/js/editor.js', $editor_deps, $this->_version, false); include __DIR__ . '/views/editor.php'; } /** * Whether the form is multi-page and shows the given progress indicator ("steps" or "percentage"). */ private static function form_has_steps($form, $indicator = 'steps') { if ($indicator !== rgars($form, 'pagination/type')) { return false; } foreach ((array) rgar($form, 'fields') as $field) { if ('page' === rgobj($field, 'type')) { return true; } } return false; } /** * Whether the form has a List field (List Field Pro styles it through the "List field" section). */ private static function form_has_list($form) { foreach ((array) rgar($form, 'fields') as $field) { if (is_object($field) && 'list' === $field->get_input_type()) { return true; } } return false; } /** * Whether the form has a Repeater field. */ private static function form_has_repeater($form) { foreach ((array) rgar($form, 'fields') as $field) { if (is_object($field) && 'repeater' === $field->type) { return true; } } return false; } /** * Whether Image Picker Pro is active and the form has a field with it enabled. */ private static function form_has_image_picker($form) { if (! class_exists('ImagePickerAddon') || ! defined('IMAGE_PICKER_PRO_ADDON_VERSION')) { return false; } $addon = ImagePickerAddon::get_instance(); return method_exists($addon, 'form_contains_image_picker_fields') && $addon->form_contains_image_picker_fields($form); } /** * Whether QR Code Pro is active and the form has a QR Code field or a field with the QR scanner on. */ private static function form_has_qrcode($form) { if (! class_exists('ALPHAGF_QRCode')) { return false; } foreach ((array) rgar($form, 'fields') as $field) { if (is_object($field) && ('alphagf_qrcode' === $field->type || ! empty($field->alphagfQrScanner))) { return true; } } return false; } /** * Whether Email Verification Pro is active and the form has an Email Verification field. */ private static function form_has_email_verification($form) { if (! class_exists('EVPGF_Addon')) { return false; } foreach ((array) rgar($form, 'fields') as $field) { if (is_object($field) && 'evpgf_verification' === $field->type) { return true; } } return false; } private function render_frame($form) { self::$is_frame = true; gravity_form_enqueue_scripts($form['id'], false); wp_enqueue_style('alphagf-styles-frame', $this->get_base_url() . '/css/frame.css', array(), $this->_version); wp_enqueue_script('alphagf-styles-frame', $this->get_base_url() . '/js/frame.js', array(), $this->_version, true); include __DIR__ . '/views/frame.php'; } /** * Prepend the form's saved styles to its markup on the frontend. */ public function print_form_styles($markup, $form) { if (self::$is_frame || ! is_array($form)) { return $markup; } $form_id = absint(rgar($form, 'id')); $styles = rgar($form, self::META_KEY); if (! $form_id || ! is_array($styles) || isset($this->printed[$form_id])) { return $markup; } // Horizontal choice layouts come first so saved tablet/mobile overrides can replace them. $css = self::get_horizontal_choice_css($form) . str_replace( array('{{wrapper}}', '{{confirmation}}', '{{qrmodal}}'), array('#gform_wrapper_' . $form_id, '#gform_confirmation_wrapper_' . $form_id, '.alphagf-qr-modal[data-form="' . $form_id . '"]'), self::sanitize_css((string) rgar($styles, 'css')) ); if ('' === $css) { return $markup; } $this->printed[$form_id] = true; $output = ''; $font_url = self::get_font_url(rgar($styles, 'font')); if ($font_url) { // Printed inline (not in the footer) so forms rendered over AJAX get their font too. $handle = 'alphagf-styles-font-' . $form_id; wp_register_style($handle, $font_url, array(), null); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- Google Fonts URL; a version arg would break the query. ob_start(); wp_print_styles($handle); $output .= ob_get_clean(); } $output .= ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Sanitized by sanitize_css(). return $output . $markup; } public function ajax_save_styles() { $this->verify_ajax_request(); $form = GFAPI::get_form(absint(rgpost('form_id'))); if (! $form) { wp_send_json_error(array('message' => esc_html__('The form could not be found.', 'alpha-addons-for-gravity-forms')), 404); } $values = self::sanitize_values(json_decode((string) rgpost('values'), true)); $font = sanitize_key((string) rgpost('font')); $css = self::sanitize_css((string) rgpost('css')); // Desktop choice layouts belong to the fields themselves (same settings as the form editor). $values = self::apply_choice_layouts($form, $values); $values = self::apply_image_picker_settings($form, $values); if (empty($values) && '' === $css && ! self::has_horizontal_choices($form)) { unset($form[self::META_KEY]); } else { $form[self::META_KEY] = array( 'values' => $values, 'css' => $css, 'font' => array_key_exists($font, self::get_fonts()) ? $font : '', 'updated' => time(), ); } $result = GFAPI::update_form($form); if (is_wp_error($result)) { wp_send_json_error(array('message' => $result->get_error_message()), 500); } wp_send_json_success(array( 'message' => esc_html__('Styles saved.', 'alpha-addons-for-gravity-forms'), 'choiceFields' => self::get_choice_fields($form), 'imagePickerFields' => self::get_image_picker_fields($form), )); } /** * Checkbox/radio fields that support Gravity Forms' "Choice Layout" setting. */ private static function get_choice_fields($form) { $list = array(); foreach ((array) rgar($form, 'fields') as $field) { if (! is_object($field) || ! in_array('display_choices_columns_setting', (array) $field->get_form_editor_field_settings(), true)) { continue; } $input_type = $field->get_input_type(); if ('multi_choice' === $input_type || 'multi_choice' === $field->type) { $input_type = 'single' === rgobj($field, 'choiceLimit') ? 'radio' : 'checkbox'; } if (! in_array($input_type, array('checkbox', 'radio'), true)) { continue; } if (method_exists($field, 'get_field_choice_alignment')) { $layout = $field->get_field_choice_alignment($field); } else { $layout = rgobj($field, 'choiceAlignment') ? $field->choiceAlignment : (rgobj($field, 'enableDisplayInColumns') ? 'columns' : 'vertical'); } $columns = absint(rgobj($field, 'displayColumns')); $label = wp_strip_all_tags(GFCommon::get_label($field)); $list[] = array( 'id' => absint($field->id), 'type' => $input_type, /* translators: %d: field ID. */ 'label' => '' !== trim($label) ? $label : sprintf(__('Field #%d', 'alpha-addons-for-gravity-forms'), $field->id), 'layout' => in_array($layout, array('vertical', 'horizontal', 'columns'), true) ? $layout : 'vertical', 'columns' => (string) ($columns >= 2 && $columns <= 5 ? $columns : 2), 'flow' => 'vertical' === rgobj($field, 'displayAlignment') ? 'down' : 'across', ); } return $list; } /** * Write "field.{id}.layout|columns|flow" values into the fields' Gravity Forms settings and * return the values without them. Tablet/mobile overrides ("…@tablet") stay as styles. */ private static function apply_choice_layouts(&$form, $values) { $changes = array(); foreach ($values as $key => $value) { if (preg_match('/^field\.(\d+)\.(layout|columns|flow)$/', $key, $match)) { $changes[absint($match[1])][$match[2]] = $value; unset($values[$key]); } } if (empty($changes)) { return $values; } foreach ((array) rgar($form, 'fields') as $field) { $id = absint($field->id); if (! isset($changes[$id])) { continue; } $change = $changes[$id]; if (isset($change['layout']) && in_array($change['layout'], array('vertical', 'horizontal', 'columns'), true)) { $field->choiceAlignment = $change['layout']; } if (isset($change['columns'])) { $columns = absint($change['columns']); if ($columns >= 2 && $columns <= 5) { $field->displayColumns = $columns; } } if (isset($change['flow'])) { $field->displayAlignment = 'down' === $change['flow'] ? 'vertical' : 'horizontal'; } if ('columns' === rgobj($field, 'choiceAlignment') && absint(rgobj($field, 'displayColumns')) < 2) { $field->displayColumns = 2; } } return $values; } /** * Image Picker Pro field settings the editor can change, per kind of field: * "imgpf.{id}.{option}" => Gravity Forms field property. */ private static function image_picker_props($kind) { $props = array( 'choice' => array( 'theme' => 'imagePicker_theme', 'imageStyle' => 'imagePicker_image_style', 'alignment' => 'imagePicker_alignment', 'columns' => 'imagePicker_column_large', 'columnsTablet' => 'imagePicker_column_medium', 'columnsMobile' => 'imagePicker_column_small', 'width' => 'imagePicker_column_large_fixed', 'widthTablet' => 'imagePicker_column_medium_fixed', 'widthMobile' => 'imagePicker_column_small_fixed', 'height' => 'imagePicker_height_large', 'heightTablet' => 'imagePicker_height_medium', 'heightMobile' => 'imagePicker_height_small', ), 'product' => array( 'theme' => 'imagePicker_product_theme', 'imageStyle' => 'imagePicker_product_image_style', 'height' => 'imagePicker_product_height', 'heightTablet' => 'imagePicker_product_height_medium', 'heightMobile' => 'imagePicker_product_height_small', 'cardWidth' => 'imagePicker_product_width', 'cardWidthTablet' => 'imagePicker_product_width_medium', 'cardWidthMobile' => 'imagePicker_product_width_small', 'quantityBtn' => 'imagePicker_product_quantity_btn', ), 'dropdown' => array( 'imageStyle' => 'imagePicker_dropdown_image_style', ), ); return isset($props[$kind]) ? $props[$kind] : array(); } /** * Allowed values of the select-type options ("form_setting" falls back to the form/global setting). */ private static function get_image_picker_options() { $columns = array(); foreach (range(1, 12) as $count) { $columns[] = array('value' => (string) $count, 'label' => (string) $count); } $columns[] = array('value' => 'fixed', 'label' => __('Fixed width', 'alpha-addons-for-gravity-forms')); $columns[] = array('value' => 'auto', 'label' => __('Auto', 'alpha-addons-for-gravity-forms')); return array( 'themes' => array( array('value' => 'simple', 'label' => __('Simple', 'alpha-addons-for-gravity-forms')), array('value' => 'card', 'label' => __('Card', 'alpha-addons-for-gravity-forms')), array('value' => 'polar', 'label' => __('Polar', 'alpha-addons-for-gravity-forms')), array('value' => 'box', 'label' => __('Box', 'alpha-addons-for-gravity-forms')), array('value' => 'cover', 'label' => __('Cover', 'alpha-addons-for-gravity-forms')), array('value' => 'circle', 'label' => __('Circle', 'alpha-addons-for-gravity-forms')), array('value' => 'luma', 'label' => __('Luma', 'alpha-addons-for-gravity-forms')), ), 'productThemes' => array( array('value' => 'simple', 'label' => __('Simple', 'alpha-addons-for-gravity-forms')), array('value' => 'card', 'label' => __('Card', 'alpha-addons-for-gravity-forms')), array('value' => 'cover', 'label' => __('Cover title', 'alpha-addons-for-gravity-forms')), array('value' => 'orbit', 'label' => __('Orbit', 'alpha-addons-for-gravity-forms')), array('value' => 'luma', 'label' => __('Luma', 'alpha-addons-for-gravity-forms')), array('value' => 'vivid', 'label' => __('Vivid', 'alpha-addons-for-gravity-forms')), ), 'imageStyles' => array( array('value' => 'cover', 'label' => __('Cover', 'alpha-addons-for-gravity-forms')), array('value' => 'contain', 'label' => __('Contained', 'alpha-addons-for-gravity-forms')), ), 'alignments' => array( array('value' => 'default', 'label' => __('Default', 'alpha-addons-for-gravity-forms')), array('value' => 'left', 'label' => __('Left', 'alpha-addons-for-gravity-forms')), array('value' => 'center', 'label' => __('Center', 'alpha-addons-for-gravity-forms')), array('value' => 'right', 'label' => __('Right', 'alpha-addons-for-gravity-forms')), ), 'columns' => $columns, ); } /** * Fields with Image Picker enabled, with their current settings and what "Use form setting" resolves to. */ private static function get_image_picker_fields($form) { $list = array(); if (! self::form_has_image_picker($form)) { return $list; } $addon = ImagePickerAddon::get_instance(); // An empty field makes Image Picker resolve the form setting, then the global one. $inherit = function ($setting, $field_key, $default) use ($addon, $form) { return (string) $addon->get_active_settings_value($form, array(), $setting, $field_key, $default); }; $inherited = array( 'choice' => array( 'theme' => $inherit('image_picker_theme', 'imagePicker_theme', 'simple'), 'imageStyle' => $inherit('image_picker_image_style', 'imagePicker_image_style', 'cover'), 'alignment' => $inherit('image_picker_alignment', 'imagePicker_alignment', 'default'), 'columns' => $inherit('image_picker_column_large', 'imagePicker_column_large', '6'), 'columnsTablet' => $inherit('image_picker_column_medium', 'imagePicker_column_medium', '4'), 'columnsMobile' => $inherit('image_picker_column_small', 'imagePicker_column_small', '2'), ), 'product' => array( 'theme' => $inherit('image_picker_theme', 'imagePicker_product_theme', 'simple'), 'imageStyle' => $inherit('image_picker_image_style', 'imagePicker_product_image_style', 'cover'), ), 'dropdown' => array( 'imageStyle' => $inherit('image_picker_image_style', 'imagePicker_dropdown_image_style', 'cover'), ), ); foreach ((array) rgar($form, 'fields') as $field) { if (! is_object($field) || ! $addon->is_image_picker_enabled($field)) { continue; } $input_type = $addon->get_input_type($field); if ('singleproduct' === $input_type) { $kind = 'product'; } elseif ($addon->is_dropdown_image_picker_enabled($field)) { $kind = 'dropdown'; } else { $kind = 'choice'; } $current = array(); foreach (self::image_picker_props($kind) as $option => $prop) { $value = rgobj($field, $prop); if ('quantityBtn' === $option) { $current[$option] = true === $value ? 'on' : 'off'; } elseif (self::is_image_picker_size($option)) { $number = floatval(preg_replace('/[^0-9.]/', '', (string) $value)); $current[$option] = $number > 0 ? $number : ''; } else { $current[$option] = is_string($value) && '' !== $value ? $value : 'form_setting'; } } $label = wp_strip_all_tags(GFCommon::get_label($field)); $list[] = array( 'id' => absint($field->id), 'kind' => $kind, /* translators: %d: field ID. */ 'label' => '' !== trim($label) ? $label : sprintf(__('Field #%d', 'alpha-addons-for-gravity-forms'), $field->id), 'values' => $current, 'inherit' => $inherited[$kind], ); } return $list; } private static function is_image_picker_size($option) { return (bool) preg_match('/^(width|height|cardWidth)/', $option); } /** * Write "imgpf.{id}.{option}" values into the Image Picker settings of the fields and * return the values without them. */ private static function apply_image_picker_settings(&$form, $values) { $changes = array(); foreach ($values as $key => $value) { if (preg_match('/^imgpf\.(\d+)\.([a-zA-Z]+)(@\w+)?$/', $key, $match)) { if (empty($match[3])) { $changes[absint($match[1])][$match[2]] = $value; } unset($values[$key]); } } if (empty($changes)) { return $values; } $fields = self::get_image_picker_fields($form); $options = self::get_image_picker_options(); $allowed = array( 'imageStyle' => wp_list_pluck($options['imageStyles'], 'value'), 'alignment' => wp_list_pluck($options['alignments'], 'value'), 'columns' => wp_list_pluck($options['columns'], 'value'), ); $kinds = wp_list_pluck($fields, 'kind', 'id'); foreach ((array) rgar($form, 'fields') as $field) { $id = absint($field->id); if (! isset($changes[$id], $kinds[$id])) { continue; } $kind = $kinds[$id]; $props = self::image_picker_props($kind); foreach ($changes[$id] as $option => $value) { if (! isset($props[$option])) { continue; } $value = (string) $value; if ('quantityBtn' === $option) { $field->{$props[$option]} = 'on' === $value; continue; } if (self::is_image_picker_size($option)) { $number = floatval($value); $field->{$props[$option]} = $number > 0 ? (string) $number : ''; continue; } if ('theme' === $option) { $valid = wp_list_pluck($options['product' === $kind ? 'productThemes' : 'themes'], 'value'); } else { $valid = $allowed[preg_replace('/(Tablet|Mobile)$/', '', $option)]; } if ('form_setting' === $value || in_array($value, $valid, true)) { $field->{$props[$option]} = $value; } } } return $values; } // Desktop layouts always come from the fields' current settings, never from stored styles. private static function strip_desktop_layouts($values) { foreach (array_keys($values) as $key) { if (preg_match('/^field\.\d+\.(layout|columns|flow)$/', $key) || 0 === strpos($key, 'imgpf.')) { unset($values[$key]); } } return $values; } private static function has_horizontal_choices($form) { foreach (self::get_choice_fields($form) as $field) { if ('horizontal' === $field['layout']) { return true; } } return false; } /** * Orbital only styles "Horizontal" for the newer Choice field, so lay out horizontal * checkbox/radio fields here, based on each field's current Gravity Forms setting. */ private static function get_horizontal_choice_css($form) { $css = ''; foreach (self::get_choice_fields($form) as $field) { if ('horizontal' !== $field['layout']) { continue; } $selector = '#gform_wrapper_' . absint($form['id']) . ' .gfield[id$="_' . $field['id'] . '"]'; $css .= $selector . ' .gfield_checkbox,' . $selector . ' .gfield_radio{display:flex;flex-direction:row;flex-wrap:wrap;gap:var(--gf-field-choice-align-x-gap-y, 8px) var(--gf-field-choice-align-x-gap-x, 24px);grid-template-columns:none;columns:auto}'; } return $css; } public function ajax_save_theme() { $this->verify_ajax_request(); $themes = get_option(self::THEMES_OPTION, array()); $themes = is_array($themes) ? $themes : array(); $id = sanitize_key((string) rgpost('id')); $name = mb_substr(sanitize_text_field((string) rgpost('name')), 0, 60); $raw = rgpost('values'); $values = null === $raw ? null : self::sanitize_values(json_decode((string) $raw, true)); if ($id && isset($themes[$id])) { if ('' !== $name) { $themes[$id]['name'] = $name; } if (null !== $values) { $themes[$id]['values'] = $values; } $themes[$id]['updated'] = time(); } else { if ('' === $name) { wp_send_json_error(array('message' => esc_html__('Please give your theme a name.', 'alpha-addons-for-gravity-forms')), 400); } $id = 'theme_' . str_replace('-', '', wp_generate_uuid4()); $themes[$id] = array( 'id' => $id, 'name' => $name, 'values' => $values ? $values : array(), 'created' => time(), 'updated' => time(), ); } update_option(self::THEMES_OPTION, $themes, false); wp_send_json_success(array( 'id' => $id, 'themes' => $this->get_themes(), )); } public function ajax_delete_theme() { $this->verify_ajax_request(); $themes = get_option(self::THEMES_OPTION, array()); $themes = is_array($themes) ? $themes : array(); $id = sanitize_key((string) rgpost('id')); unset($themes[$id]); update_option(self::THEMES_OPTION, $themes, false); wp_send_json_success(array('themes' => $this->get_themes())); } private function verify_ajax_request() { if (! check_ajax_referer(self::NONCE_ACTION, 'nonce', false)) { wp_send_json_error(array('message' => esc_html__('Your session has expired. Please reload the page.', 'alpha-addons-for-gravity-forms')), 403); } if (! GFCommon::current_user_can_any('gravityforms_edit_forms')) { wp_send_json_error(array('message' => esc_html__('Sorry, you are not allowed to do that.', 'alpha-addons-for-gravity-forms')), 403); } } private function get_themes() { $themes = get_option(self::THEMES_OPTION, array()); $list = array(); if (! is_array($themes)) { return $list; } foreach ($themes as $theme) { $list[] = array( 'id' => rgar($theme, 'id'), 'name' => rgar($theme, 'name'), 'values' => (object) (is_array(rgar($theme, 'values')) ? $theme['values'] : array()), ); } return $list; } /** * Keep only flat "section.property" => scalar pairs. */ private static function sanitize_values($values) { $clean = array(); if (! is_array($values)) { return $clean; } foreach (array_slice($values, 0, 300, true) as $key => $value) { // Responsive overrides use "section.property@tablet" / "@mobile". if (! is_string($key) || ! preg_match('/^[a-zA-Z0-9_.]{1,64}(@(tablet|mobile))?$/', $key) || ! is_scalar($value)) { continue; } if (is_bool($value)) { $clean[$key] = $value; } elseif (is_int($value) || is_float($value)) { $clean[$key] = $value + 0; } else { $value = mb_substr(sanitize_text_field($value), 0, 200); if ('' !== $value) { $clean[$key] = $value; } } } return $clean; } /** * The CSS is generated by the editor; strip anything that could break out of the