post_content, $shortcode ); } return $has_form; } /** In the head of a page with a form, so the no-JavaScript styles never flash for visitors who have it. */ public function enqueue_js_flag() { if ( self::page_has_form() ) { wp_enqueue_script( 'holmesforms-js-flag' ); } } public static function no_page_cache() { if ( ! defined( 'DONOTCACHEPAGE' ) ) { define( 'DONOTCACHEPAGE', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- the page cache plugins' own constant } self::tell_litespeed_no_cache(); if ( ! headers_sent() ) { nocache_headers(); } } public static function add_litespeed_no_cache_header( $headers ) { $headers['X-LiteSpeed-Cache-Control'] = 'no-cache'; return $headers; } public static function tell_litespeed_no_cache() { do_action( 'litespeed_control_set_nocache', 'HolmesPortfolio Forms' ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- LiteSpeed Cache's own hook } public function send_fresh_token() { if ( ! defined( 'DONOTCACHEPAGE' ) ) { define( 'DONOTCACHEPAGE', true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- the page cache plugins' own constant } nocache_headers(); $reply = array( 'nonce' => wp_create_nonce( 'wp_rest' ) ); $token = isset( $_GET['token'] ) ? sanitize_text_field( wp_unslash( $_GET['token'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read only, the draft token is checked against the stored one below if ( '' !== $token ) { require_once HOLMESFORMS_DIR . 'includes/class-hp-forms-draft.php'; require_once HOLMESFORMS_DIR . 'includes/class-hp-forms-spam.php'; $form_id = absint( strtok( $token, '.' ) ); if ( $form_id && HOLMESFORMS_POST_TYPE === get_post_type( $form_id ) && 'publish' === get_post_status( $form_id ) && HP_Forms_Draft::needs_new_token( $token ) ) { $draft = HP_Forms_Draft::create( $form_id ); $reply['token'] = $draft['token']; $reply['ts'] = HP_Forms_Spam::make_time_token( $form_id ); } } wp_send_json( $reply ); } /* Activation and deactivation */ public static function on_activate() { require_once HOLMESFORMS_DIR . 'includes/class-hp-forms-storage.php'; require_once HOLMESFORMS_DIR . 'includes/class-hp-forms-components.php'; require_once HOLMESFORMS_DIR . 'includes/class-hp-forms-form.php'; HP_Forms_Storage::install(); self::instance()->register_post_type(); flush_rewrite_rules(); if ( ! wp_next_scheduled( HOLMESFORMS_PRUNE_HOOK ) ) { wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', HOLMESFORMS_PRUNE_HOOK ); } self::seed_default_forms(); self::seed_sample_entry(); require_once HOLMESFORMS_DIR . 'includes/class-hp-forms-guide.php'; HP_Forms_Guide::publish_page(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read only, skips the welcome redirect on bulk activation if ( ! is_network_admin() && ! isset( $_GET['activate-multi'] ) ) { set_transient( 'holmesforms_activation_redirect', 1, self::ACTIVATION_REDIRECT_SECONDS ); } } public function maybe_activation_redirect() { if ( ! get_transient( 'holmesforms_activation_redirect' ) ) { return; } delete_transient( 'holmesforms_activation_redirect' ); if ( wp_doing_ajax() || ! current_user_can( 'manage_options' ) ) { return; } require_once HOLMESFORMS_DIR . 'includes/class-hp-forms-setup.php'; if ( ! HP_Forms_Setup::is_done() ) { wp_safe_redirect( HP_Forms_Setup::url() ); exit; } wp_safe_redirect( admin_url( 'admin.php?page=' . HP_Forms_Admin::MENU_SLUG . '&holmesforms_welcome=1' ) ); exit; } public static function seed_default_forms() { $existing = get_posts( array( 'post_type' => HOLMESFORMS_POST_TYPE, 'post_status' => 'any', 'posts_per_page' => 1, 'fields' => 'ids', 'no_found_rows' => true, ) ); if ( $existing || get_option( 'holmesforms_defaults_seeded' ) || get_option( 'holmesforms_starter_seeded' ) ) { return; } require_once HOLMESFORMS_DIR . 'includes/class-hp-forms-starters.php'; self::insert_form( self::expand_email_all_fields( HP_Forms_Starters::enquiry_form() ) ); update_option( 'holmesforms_defaults_seeded', 1, false ); update_option( 'holmesforms_starter_seeded', 1, false ); } public static function seed_sample_entry() { if ( get_option( 'holmesforms_sample_entry_seeded' ) ) { return; } update_option( 'holmesforms_sample_entry_seeded', 1, false ); require_once HOLMESFORMS_DIR . 'includes/class-hp-forms-storage.php'; require_once HOLMESFORMS_DIR . 'includes/class-hp-forms-form.php'; if ( HP_Forms_Storage::count() > 0 ) { return; } $form_ids = get_posts( array( 'post_type' => HOLMESFORMS_POST_TYPE, 'post_status' => 'publish', 'posts_per_page' => 1, 'orderby' => 'ID', 'order' => 'ASC', 'fields' => 'ids', 'no_found_rows' => true, ) ); if ( ! $form_ids ) { return; } $form = HP_Forms_Form::get( (int) $form_ids[0] ); if ( ! $form ) { return; } $sample_answers = array( 'email' => 'sample@example.com', 'tel' => '01632 960000', 'url' => 'https://example.com', 'number' => '2', 'textarea' => __( 'This is a sample entry added when the plugin was set up. Delete it whenever you like.', 'holmesportfolio-forms' ), 'text' => __( 'Sample Person', 'holmesportfolio-forms' ), 'date' => array( 'day' => '01', 'month' => '01', 'year' => (string) (int) gmdate( 'Y' ), ), ); $answers = array(); foreach ( $form->all_fields() as $field ) { if ( empty( $field['key'] ) || in_array( $field['type'], array( 'container', 'button', 'email_action', 'submit_message', 'cya', 'alert', 'hidden', 'attachments', 'slider' ), true ) ) { continue; } if ( 'text' === $field['type'] ) { $mode = isset( $field['config']['mode'] ) ? $field['config']['mode'] : 'text'; $answers[ $field['key'] ] = isset( $sample_answers[ $mode ] ) ? $sample_answers[ $mode ] : $sample_answers['text']; } elseif ( isset( $sample_answers[ $field['type'] ] ) ) { $answers[ $field['key'] ] = $sample_answers[ $field['type'] ]; } elseif ( in_array( $field['type'], array( 'radio', 'select', 'checkbox' ), true ) && ! empty( $field['options'][0] ) ) { $first_option = $field['options'][0]; $answers[ $field['key'] ] = isset( $first_option['value'] ) && '' !== $first_option['value'] ? $first_option['value'] : ( $first_option['label'] ?? '' ); } } HP_Forms_Storage::insert( array( 'form_id' => (int) $form_ids[0], 'status' => 'unread', 'data' => array( 'reference' => 'SAMPLE-1', 'fields' => $answers, 'variables' => array(), 'hidden' => array(), ), 'context' => array( 'form_title' => get_the_title( (int) $form_ids[0] ), 'sample' => true, ), ) ); } /** @param int $form_id */ public static function is_bundled_form( $form_id ) { $title = strtolower( trim( (string) get_the_title( (int) $form_id ) ) ); if ( '' === $title ) { return false; } foreach ( self::starter_form_definitions() as $definition ) { if ( strtolower( trim( (string) $definition['title'] ) ) === $title ) { return true; } } return false; } /** @return array */ private static function bundled_title_map() { $bundled_titles = array(); foreach ( self::starter_form_definitions() as $definition ) { $bundled_titles[ strtolower( trim( (string) $definition['title'] ) ) ] = true; } $form_ids = array(); foreach ( get_posts( array( 'post_type' => HOLMESFORMS_POST_TYPE, 'post_status' => 'any', 'numberposts' => -1, 'no_found_rows' => true, 'orderby' => 'modified', 'order' => 'DESC', 'fields' => 'ids', ) ) as $form_id ) { $key = strtolower( trim( (string) get_the_title( $form_id ) ) ); if ( isset( $bundled_titles[ $key ] ) && ! isset( $form_ids[ $key ] ) ) { $form_ids[ $key ] = (int) $form_id; } } return $form_ids; } private static function strip_ids( $value ) { if ( is_string( $value ) ) { return preg_replace( '/data-hpf-group="[^"]*"/', 'data-hpf-group=""', $value ); } if ( ! is_array( $value ) ) { return $value; } unset( $value['id'] ); foreach ( $value as $key => $item ) { $value[ $key ] = self::strip_ids( $item ); } return $value; } private static function pages_print( $pages ) { return md5( (string) wp_json_encode( self::strip_ids( $pages ) ) ); } private static function is_open_in_builder( $form_id ) { $lock = explode( ':', (string) get_post_meta( (int) $form_id, '_edit_lock', true ) ); return isset( $lock[1] ) && (int) $lock[0] > time() - (int) apply_filters( 'wp_check_post_lock_window', self::EDIT_LOCK_SECONDS ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress core filter } /** @return array[] { title, group, id, state (missing|unchanged|older|changed|locked), entries } */ public static function starter_forms_status() { $form_ids = self::bundled_title_map(); $rows = array(); foreach ( self::starter_form_definitions() as $definition ) { $key = strtolower( trim( (string) $definition['title'] ) ); $form_id = isset( $form_ids[ $key ] ) ? (int) $form_ids[ $key ] : 0; $row = array( 'title' => $definition['title'], 'group' => isset( $definition['group'] ) ? $definition['group'] : '', 'suits' => isset( $definition['suits'] ) ? $definition['suits'] : '', 'id' => $form_id, 'state' => 'missing', 'entries' => 0, ); if ( $form_id ) { $stored = get_post_meta( $form_id, '_holmesforms_pages', true ); if ( self::is_open_in_builder( $form_id ) ) { $row['state'] = 'locked'; } else { $stored_variables = get_post_meta( $form_id, '_holmesforms_variables', true ); $same_pages = json_decode( wp_json_encode( self::strip_ids( $stored ) ), true ) == json_decode( wp_json_encode( self::strip_ids( $definition['pages'] ) ), true ); // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- loose on purpose, numbers may come back as strings $same_variables = json_decode( wp_json_encode( is_array( $stored_variables ) ? $stored_variables : array() ), true ) == json_decode( wp_json_encode( isset( $definition['variables'] ) ? $definition['variables'] : array() ), true ); // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- loose on purpose, numbers may come back as strings $is_unchanged = $same_pages && $same_variables; $is_untouched = ! $is_unchanged && self::pages_print( $stored ) === get_post_meta( $form_id, self::STARTER_PRINT_META, true ); $row['state'] = $is_unchanged ? 'unchanged' : ( $is_untouched ? 'older' : 'changed' ); } if ( class_exists( 'HP_Forms_Storage' ) ) { $row['entries'] = (int) HP_Forms_Storage::count( array( 'form_id' => $form_id ) ); } } $rows[] = $row; } return $rows; } /** * @param string[]|null $only Exact bundled titles; null for all. * @return array { add: missing, overwrite: existing and forced, locked: open in the builder } */ public static function restore_forms_preview( $only = null ) { $form_ids = self::bundled_title_map(); $add = array(); $overwrite = array(); $locked = array(); foreach ( self::starter_form_definitions() as $definition ) { if ( is_array( $only ) && ! in_array( $definition['title'], $only, true ) ) { continue; } $key = strtolower( trim( (string) $definition['title'] ) ); if ( ! isset( $form_ids[ $key ] ) ) { $add[] = $definition['title']; } elseif ( self::is_open_in_builder( $form_ids[ $key ] ) ) { $locked[] = $definition['title']; } else { $overwrite[] = $definition['title']; } } return array( 'add' => $add, 'overwrite' => $overwrite, 'locked' => $locked, ); } /** * @param string[] $posted Titles ticked. * @param bool $is_reset_confirmed The reset box was ticked. * @param bool $adds_anyway With resets unconfirmed, still add the missing ones (the wizard). * @return array { done, added, overwritten, skipped_locked, backup } */ public static function restore_picked( array $posted, $is_reset_confirmed, $adds_anyway = false ) { $picked = array(); $reset = array(); foreach ( self::starter_forms_status() as $row ) { if ( ! in_array( $row['title'], $posted, true ) || in_array( $row['state'], array( 'locked', 'unchanged' ), true ) ) { continue; } $picked[] = $row['title']; if ( 'missing' !== $row['state'] ) { $reset[] = $row['title']; } } $nothing_done = array( 'added' => array(), 'overwritten' => array(), 'skipped_locked' => array(), ); if ( ! $picked ) { return $nothing_done + array( 'done' => true ); } if ( $reset && ! $is_reset_confirmed ) { if ( ! $adds_anyway ) { return $nothing_done + array( 'done' => false ); } $picked = array_values( array_diff( $picked, $reset ) ); $reset = array(); if ( ! $picked ) { return $nothing_done + array( 'done' => false ); } } return self::restore_default_forms( $reset, $picked ); } /** * @param bool|string[] $force True to overwrite all, or the titles to overwrite. * @param string[]|null $only Exact bundled titles; null for all. * @return array { done, added, overwritten, skipped_locked } */ public static function restore_default_forms( $force = false, $only = null ) { $preview = self::restore_forms_preview( $only ); $may_overwrite = static function ( $title ) use ( $force ) { return is_array( $force ) ? in_array( $title, $force, true ) : (bool) $force; }; $backup = ''; $form_ids = self::bundled_title_map(); $added = array(); $overwritten = array(); foreach ( self::starter_form_definitions() as $definition ) { if ( is_array( $only ) && ! in_array( $definition['title'], $only, true ) ) { continue; } $key = strtolower( trim( (string) $definition['title'] ) ); if ( ! isset( $form_ids[ $key ] ) ) { $form_id = self::insert_form( $definition ); if ( $form_id && ! is_wp_error( $form_id ) ) { $added[] = $definition['title']; } continue; } if ( ! $may_overwrite( $definition['title'] ) || in_array( $definition['title'], $preview['locked'], true ) ) { continue; } if ( '' === $backup && class_exists( 'HP_Forms_Backup' ) ) { $backup_path = HP_Forms_Backup::run_backup( 'manual' ); $backup = is_wp_error( $backup_path ) ? '' : basename( (string) $backup_path ); } self::save_starter_meta( $form_ids[ $key ], $definition ); $overwritten[] = $definition['title']; } return array( 'backup' => $backup, 'done' => ! ( false === $force && $preview['overwrite'] ), 'added' => $added, 'overwritten' => $overwritten, 'skipped_locked' => $preview['locked'], ); } public static function insert_form( array $definition ) { $form_id = wp_insert_post( array( 'post_type' => HOLMESFORMS_POST_TYPE, 'post_status' => 'publish', 'post_title' => $definition['title'], ) ); if ( $form_id && ! is_wp_error( $form_id ) ) { self::save_starter_meta( $form_id, $definition ); update_post_meta( $form_id, '_holmesforms_seeded', 1 ); } return $form_id; } private static function save_starter_meta( $form_id, array $definition ) { require_once HOLMESFORMS_DIR . 'includes/class-hp-forms-form.php'; update_post_meta( $form_id, '_holmesforms_pages', wp_slash( $definition['pages'] ) ); update_post_meta( $form_id, self::STARTER_PRINT_META, wp_slash( self::pages_print( get_post_meta( $form_id, '_holmesforms_pages', true ) ) ) ); update_post_meta( $form_id, '_holmesforms_variables', wp_slash( isset( $definition['variables'] ) ? $definition['variables'] : array() ) ); if ( ! empty( $definition['ref_prefix'] ) ) { update_post_meta( $form_id, HP_Forms_Form::META_REF_PREFIX, wp_slash( $definition['ref_prefix'] ) ); } if ( ! empty( $definition['bh_region'] ) ) { update_post_meta( $form_id, HP_Forms_Form::META_BH_REGION, $definition['bh_region'] ); } if ( ! empty( $definition['group'] ) ) { wp_set_object_terms( $form_id, array( $definition['group'] ), HOLMESFORMS_FORM_GROUP_TAX, false ); } } /** @return array group title => bundled form titles. */ public static function starter_form_groups() { $groups = array(); foreach ( self::starter_form_definitions() as $definition ) { if ( empty( $definition['group'] ) ) { continue; } $groups[ $definition['group'] ][] = $definition['title']; } return $groups; } public static function starter_form_definitions() { require_once HOLMESFORMS_DIR . 'includes/class-hp-forms-starters.php'; return array_map( array( __CLASS__, 'expand_email_all_fields' ), HP_Forms_Starters::all() ); } /** * @param array $definition One bundled form definition. * @return array */ public static function expand_email_all_fields( array $definition ) { $summary_html = self::answer_summary_html( (array) $definition['pages'] ); $shown_keys = array(); $answer_lines = ''; foreach ( (array) $definition['pages'] as $page ) { $answer_lines .= self::field_answer_lines( (array) ( isset( $page['fields'] ) ? $page['fields'] : array() ), $shown_keys ); } $answer_lines .= self::variable_answer_lines( (array) ( isset( $definition['variables'] ) ? $definition['variables'] : array() ), $shown_keys ); $answer_lines = '' !== trim( $answer_lines ) ? "
\n" . rtrim( $answer_lines ) . "\n
\n" : ''; foreach ( (array) $definition['pages'] as $page_index => $page ) { if ( isset( $page['fields'] ) ) { $definition['pages'][ $page_index ]['fields'] = self::fill_all_fields_bodies( (array) $page['fields'], $answer_lines, $summary_html ); } } return $definition; } /** @return string Email lines for each answered question, children first; notes the keys read-only fields show. */ private static function field_answer_lines( array $fields, array &$shown_keys ) { $lines = ''; foreach ( $fields as $field ) { if ( isset( $field['config']['children'] ) && is_array( $field['config']['children'] ) ) { $lines .= self::field_answer_lines( $field['config']['children'], $shown_keys ); } if ( empty( $field['key'] ) || in_array( isset( $field['type'] ) ? $field['type'] : '', self::NO_ANSWER_LINE_TYPES, true ) || '' !== (string) ( $field['config']['match'] ?? '' ) ) { continue; } if ( ! empty( $field['config']['readonly'] ) && isset( $field['config']['default'] ) && preg_match( '/^\[([a-z0-9_]+)\]$/', (string) $field['config']['default'], $matches ) ) { $shown_keys[ $matches[1] ] = true; } $label = HP_Forms_Components::answer_label( ( isset( $field['label'] ) && '' !== trim( (string) $field['label'] ) ) ? $field['label'] : $field['key'] ); $lines .= self::answer_line( $field['key'], $label ); } return $lines; } /** @return string Email lines for the variables no read-only field already shows. */ private static function variable_answer_lines( array $variables, array $shown_keys ) { $lines = ''; foreach ( $variables as $variable ) { if ( ! empty( $variable['key'] ) && empty( $shown_keys[ $variable['key'] ] ) ) { $name = ( isset( $variable['name'] ) && '' !== trim( (string) $variable['name'] ) ) ? $variable['name'] : $variable['key']; $lines .= self::answer_line( $variable['key'], $name ); } } return $lines; } /** @return array[] The fields with [all-fields] filled in: the answers summary on Check your answers, answer lines in emails. */ private static function fill_all_fields_bodies( array $fields, $answer_lines, $summary_html ) { foreach ( $fields as $i => $field ) { if ( isset( $field['config']['children'] ) && is_array( $field['config']['children'] ) ) { $field['config']['children'] = self::fill_all_fields_bodies( $field['config']['children'], $answer_lines, $summary_html ); } if ( isset( $field['type'], $field['config']['body'] ) && 'cya' === $field['type'] && '[all-fields]' === trim( (string) $field['config']['body'] ) ) { $field['config']['body'] = $summary_html; } if ( isset( $field['type'], $field['config']['body'] ) && 'email_action' === $field['type'] && false !== strpos( $field['config']['body'], '[all-fields]' ) ) { $field['config']['body'] = str_replace( '[all-fields]', $answer_lines, $field['config']['body'] ); } $fields[ $i ] = $field; } return $fields; } /** @return string One email line that shows the answer only when there is one. */ private static function answer_line( $key, $label ) { return '\n
" . $label . "
\n
[" . $key . "]
\n
\n\n"; } /** @return array The pages before the one holding the field; all pages when it isn't found. */ public static function pages_before_field( array $pages, $field_id ) { $holds_field = static function ( array $fields ) use ( &$holds_field, $field_id ) { foreach ( $fields as $field ) { if ( (string) ( $field['id'] ?? '' ) === $field_id ) { return true; } if ( ! empty( $field['config']['children'] ) && is_array( $field['config']['children'] ) && $holds_field( $field['config']['children'] ) ) { return true; } } return false; }; if ( '' === (string) $field_id ) { return $pages; } foreach ( array_values( $pages ) as $index => $page ) { if ( $holds_field( (array) ( $page['fields'] ?? array() ) ) ) { return array_slice( array_values( $pages ), 0, $index ); } } return $pages; } /** @return string Answers summary HTML: a section per page, a line per answer, a Go to button per page. */ public static function answer_summary_html( array $pages ) { $sections = array(); $page_number = 0; foreach ( $pages as $page ) { ++$page_number; $title = '' !== trim( (string) ( $page['title'] ?? '' ) ) ? (string) $page['title'] /* translators: %d: page number */ : sprintf( __( 'page %d', 'holmesportfolio-forms' ), $page_number ); $heading = '' !== trim( (string) ( $page['title'] ?? '' ) ) ? (string) $page['title'] /* translators: %d: page number, as a heading */ : sprintf( __( 'Page %d', 'holmesportfolio-forms' ), $page_number ); $rows = self::wrap_summary_rows( self::answer_summary_rows( (array) ( $page['fields'] ?? array() ), $title ) ); if ( '' !== $rows ) { $sections[] = array( (string) ( $page['id'] ?? '' ), $title, $rows, $heading ); } } $html = ''; foreach ( $sections as $section ) { $html .= '
\n" . '

' . esc_html( $section[3] ) . "

\n" . $section[2]; if ( count( $pages ) > 1 ) { /* translators: %s: page title */ $html .= '\n"; } $html .= "
\n\n"; } return $html; } public static function wrap_summary_rows( $rows_html ) { return (string) preg_replace( '#((?:
\n$1\n", $rows_html ); } private static function answer_summary_rows( array $fields, $page_title ) { $html = ''; $is_after_group = false; foreach ( $fields as $field ) { if ( 'container' === ( $field['type'] ?? '' ) && ! HP_Forms_Components::is_repeatable( $field ) ) { $inner = self::answer_summary_rows( (array) ( $field['config']['children'] ?? array() ), $page_title ); $title = trim( (string) ( $field['label'] ?? '' ) ); if ( '' !== $inner && '' !== $title && 0 !== strcasecmp( $title, trim( (string) $page_title ) ) ) { $html .= '

' . esc_html( $title ) . "

\n"; $is_after_group = true; } $html .= $inner; continue; } $row = self::answer_summary_row( $field ); if ( '' !== $row && $is_after_group ) { $html .= "
\n"; $is_after_group = false; } $html .= '' !== $row ? $row . "\n" : ''; } return $html; } private static function answer_summary_row( array $field ) { $type = (string) ( $field['type'] ?? '' ); $key = (string) ( $field['key'] ?? '' ); if ( '' === $key || ( 'text' === $type && '' !== (string) ( $field['config']['match'] ?? '' ) ) ) { return ''; } $label = '' !== (string) ( $field['label'] ?? '' ) ? (string) $field['label'] : $key; $field_attribute = ' data-hpf-field="' . esc_attr( $key ) . '"'; if ( HP_Forms_Components::is_repeatable( $field ) ) { return '
' . esc_html( HP_Forms_Components::answer_label( $label ) ) . '
[' . $key . ']
'; } if ( 'attachments' === $type ) { return '
' . esc_html( HP_Forms_Components::answer_label( $label ) ) . '
' . esc_html__( 'Not answered', 'holmesportfolio-forms' ) . '
'; } if ( 'alert' === $type ) { if ( empty( $field['config']['confirm'] ) ) { return ''; } $label = HP_Forms_Components::answer_name( $field ); } elseif ( in_array( $type, array( 'hidden', 'button', 'email_action', 'submit_message', 'container', 'cya' ), true ) || HP_Forms_Components::stores_no_answer( $type ) ) { return ''; } return '
' . esc_html( HP_Forms_Components::answer_label( $label ) ) . '
[' . $key . ']
'; } public static function on_deactivate() { // The guide's address must go with the plugin; the stored rules still hold it, so WordPress rebuilds them on the next load. delete_option( 'holmesforms_guide_built' ); delete_option( 'rewrite_rules' ); $timestamp = wp_next_scheduled( HOLMESFORMS_PRUNE_HOOK ); if ( $timestamp ) { wp_unschedule_event( $timestamp, HOLMESFORMS_PRUNE_HOOK ); } if ( class_exists( 'HP_Forms_Backup' ) ) { HP_Forms_Backup::unschedule(); } wp_unschedule_hook( 'holmesforms_bank_holidays_refresh' ); if ( class_exists( 'HP_Forms_Hold' ) ) { HP_Forms_Hold::unschedule(); } if ( class_exists( 'HP_Forms_Jobs' ) ) { HP_Forms_Jobs::unschedule(); } flush_rewrite_rules(); } public function schedule_prune() { if ( ! wp_next_scheduled( HOLMESFORMS_PRUNE_HOOK ) ) { wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', HOLMESFORMS_PRUNE_HOOK ); } } /* Post type */ public function register_post_type() { register_post_type( HOLMESFORMS_POST_TYPE, array( 'labels' => array( 'name' => __( 'Forms', 'holmesportfolio-forms' ), 'singular_name' => __( 'Form', 'holmesportfolio-forms' ), 'add_new_item' => __( 'Add New Form', 'holmesportfolio-forms' ), 'edit_item' => __( 'Edit Form', 'holmesportfolio-forms' ), 'search_items' => __( 'Search Forms', 'holmesportfolio-forms' ), 'not_found' => __( 'No forms found.', 'holmesportfolio-forms' ), ), 'public' => false, 'show_ui' => false, 'show_in_menu' => false, 'show_in_rest' => false, 'exclude_from_search' => true, 'publicly_queryable' => false, 'has_archive' => false, 'rewrite' => false, 'query_var' => false, 'capability_type' => 'page', 'capabilities' => array( 'create_posts' => 'holmesforms_manage_forms' ), 'map_meta_cap' => true, 'supports' => array( 'title', 'revisions' ), ) ); register_taxonomy( HOLMESFORMS_FORM_GROUP_TAX, HOLMESFORMS_POST_TYPE, array( 'labels' => array( 'name' => __( 'Form groups', 'holmesportfolio-forms' ), 'singular_name' => __( 'Form group', 'holmesportfolio-forms' ), ), 'public' => false, 'hierarchical' => false, 'show_ui' => false, 'show_in_menu' => false, 'show_in_rest' => false, 'show_admin_column' => false, 'rewrite' => false, 'query_var' => false, ) ); } public function map_form_caps( $caps, $cap, $user_id, $args ) { if ( ! in_array( $cap, array( 'edit_post', 'delete_post', 'publish_post' ), true ) || empty( $args[0] ) ) { return $caps; } if ( HOLMESFORMS_POST_TYPE !== get_post_type( (int) $args[0] ) ) { return $caps; } if ( ! user_can( $user_id, 'holmesforms_manage_forms' ) && ! user_can( $user_id, 'manage_options' ) ) { return array( 'do_not_allow' ); } return $caps; } public function guard_form_delete( $check, $post ) { if ( null !== $check || ! $post || HOLMESFORMS_POST_TYPE !== $post->post_type || defined( 'WP_UNINSTALL_PLUGIN' ) ) { return $check; } if ( is_user_logged_in() && ! current_user_can( 'holmesforms_manage_forms' ) && ! current_user_can( 'manage_options' ) ) { return false; } if ( HP_Forms_Storage::export_count( array( (int) $post->ID ) ) > 0 && ! apply_filters( 'holmesforms_entry_delete_allowed', true ) ) { return false; } return $check; } public function on_form_deleted( $post_id ) { if ( HOLMESFORMS_POST_TYPE === get_post_type( $post_id ) ) { HP_Forms_Storage::delete_for_form( (int) $post_id ); do_action( 'holmesforms_form_deleted', (int) $post_id ); } } /* Assets */ public static function enqueue_style_mode() { $plugin = self::instance(); if ( ! wp_style_is( 'holmesforms-form', 'registered' ) ) { $plugin->register_assets(); } wp_enqueue_style( 'holmesforms-form' ); $mode = self::style_mode(); if ( 'gds' === $mode ) { wp_enqueue_style( 'holmesforms-gds' ); } elseif ( 'bootstrap' === $mode ) { wp_enqueue_style( 'holmesforms-bootstrap-skin' ); } } /** @return string gds, bootstrap, plain or none. */ public static function style_mode() { $settings = get_option( 'holmesforms_settings', array() ); $mode = isset( $settings['style_mode'] ) ? sanitize_key( $settings['style_mode'] ) : 'gds'; return in_array( $mode, array( 'gds', 'bootstrap', 'plain', 'none' ), true ) ? $mode : 'gds'; } public function keep_textareas_as_typed( $tags ) { $tags[] = 'textarea'; return $tags; } public function register_assets() { wp_register_style( 'holmesforms-govuk', HOLMESFORMS_URL . 'assets/css/vendor/govuk-frontend.min.css', array(), holmesforms_asset_version( 'assets/css/vendor/govuk-frontend.min.css' ) ); wp_register_style( 'holmesforms-gds', HOLMESFORMS_URL . 'assets/css/gds.css', array( 'holmesforms-govuk' ), holmesforms_asset_version( 'assets/css/gds.css' ) ); wp_register_style( 'holmesforms-bootstrap', HOLMESFORMS_URL . 'assets/css/vendor/bootstrap.min.css', array(), '5.3.3' ); wp_register_style( 'holmesforms-bootstrap-skin', HOLMESFORMS_URL . 'assets/css/bootstrap.css', array( 'holmesforms-bootstrap' ), holmesforms_asset_version( 'assets/css/bootstrap.css' ) ); wp_register_style( 'holmesforms-form', HOLMESFORMS_URL . 'assets/css/form.css', array(), holmesforms_asset_version( 'assets/css/form.css' ) ); wp_register_script( 'holmesforms-form', HOLMESFORMS_URL . 'assets/js/form.js', array(), holmesforms_asset_version( 'assets/js/form.js' ), true ); wp_set_script_translations( 'holmesforms-form', 'holmesportfolio-forms', HOLMESFORMS_DIR . 'languages' ); if ( ! wp_script_is( 'holmesforms-js-flag', 'registered' ) ) { wp_register_script( 'holmesforms-js-flag', false, array(), HOLMESFORMS_VERSION, false ); wp_add_inline_script( 'holmesforms-js-flag', 'document.documentElement.classList.add("hpf-js");' ); } } /** Enqueues what a form needs, once per request. */ public function enqueue_for_form( $form ) { $form_id = $form->id(); if ( isset( $this->queued[ $form_id ] ) ) { return; } $this->queued[ $form_id ] = true; if ( ! wp_style_is( 'holmesforms-form', 'registered' ) ) { $this->register_assets(); } $mode = $form->style_mode(); wp_enqueue_style( 'holmesforms-form' ); wp_enqueue_script( 'holmesforms-js-flag' ); if ( 'gds' === $mode ) { wp_enqueue_style( 'holmesforms-gds' ); $style_handle = 'holmesforms-gds'; } elseif ( 'bootstrap' === $mode ) { wp_enqueue_style( 'holmesforms-bootstrap-skin' ); $style_handle = 'holmesforms-bootstrap-skin'; } else { $style_handle = 'holmesforms-form'; } if ( 'plain' === $mode && '' !== $form->accent_css() ) { wp_add_inline_style( $style_handle, $form->accent_css() ); } $custom_css = $form->custom_css(); if ( '' !== $custom_css ) { wp_add_inline_style( $style_handle, $custom_css ); } wp_enqueue_script( 'holmesforms-form' ); do_action( 'holmesforms_form_assets', array_values( array_unique( wp_list_pluck( $form->all_fields(), 'type' ) ) ), $form ); wp_localize_script( 'holmesforms-form', 'holmesformsRuntime', array( 'rest' => esc_url_raw( rest_url( HOLMESFORMS_REST_NS . '/' ) ), 'nonce' => wp_create_nonce( 'wp_rest' ), 'ajaxUrl' => esc_url_raw( holmesforms_front_url() ), 'strings' => array( 'genericError' => __( 'Sorry, something went wrong. Try again.', 'holmesportfolio-forms' ), 'sendUnsure' => __( 'We could not confirm that your form was sent. It may have gone through, so check for a confirmation email before sending it again.', 'holmesportfolio-forms' ), 'answersChanged' => __( 'Your answers have changed since this page was last shown. Check them, then use the button below.', 'holmesportfolio-forms' ), 'submitting' => __( 'Submitting…', 'holmesportfolio-forms' ), /* translators: 1: current step number, 2: total steps */ 'stepOf' => __( 'Step %1$d of %2$d', 'holmesportfolio-forms' ), /* translators: browser tab title after a form is sent, used when the thank-you message has no heading. The page's own title follows it. */ 'sentTitle' => __( 'Form sent', 'holmesportfolio-forms' ), /* translators: %d: number of words still allowed */ 'wordsRemaining' => __( 'You have %d words remaining.', 'holmesportfolio-forms' ), /* translators: %d: number of words over the limit */ 'wordsOver' => __( 'You have %d words too many.', 'holmesportfolio-forms' ), /* translators: %d: number of characters still allowed */ 'charsRemaining' => __( 'You have %d characters remaining.', 'holmesportfolio-forms' ), /* translators: %d: number of characters over the limit */ 'charsOver' => __( 'You have %d characters too many.', 'holmesportfolio-forms' ), /* translators: %d: the number 1 */ 'wordsRemainingOne' => _n( 'You have %d word remaining.', 'You have %d words remaining.', 1, 'holmesportfolio-forms' ), /* translators: %d: the number 1 */ 'wordsOverOne' => _n( 'You have %d word too many.', 'You have %d words too many.', 1, 'holmesportfolio-forms' ), /* translators: %d: the number 1 */ 'charsRemainingOne' => _n( 'You have %d character remaining.', 'You have %d characters remaining.', 1, 'holmesportfolio-forms' ), /* translators: %d: the number 1 */ 'charsOverOne' => _n( 'You have %d character too many.', 'You have %d characters too many.', 1, 'holmesportfolio-forms' ), /* translators: %s: the affected field's label */ 'optionCleared' => __( 'Your answer to %s was cleared because you changed an earlier answer. Choose again.', 'holmesportfolio-forms' ), /* translators: 1: first box name, like Feet; 2: second box name, like Inches */ 'measureTwo' => __( 'Now showing two boxes: %1$s and %2$s.', 'holmesportfolio-forms' ), /* translators: %s: box name, like Centimetres */ 'measureOne' => __( 'Now showing one box: %s.', 'holmesportfolio-forms' ), 'errorTitle' => __( 'Error: ', 'holmesportfolio-forms' ), /* translators: %s: unit name, like Kilograms */ 'measureUnit' => __( 'Unit changed to %s.', 'holmesportfolio-forms' ), 'thisField' => __( 'this field', 'holmesportfolio-forms' ), 'comboboxHint' => __( 'Type to search the list.', 'holmesportfolio-forms' ), 'comboboxNone' => __( 'No results found', 'holmesportfolio-forms' ), 'comboboxOne' => __( '1 result available.', 'holmesportfolio-forms' ), /* translators: %d: number of matching choices, 2 or more */ 'comboboxMany' => __( '%d results available.', 'holmesportfolio-forms' ), 'errorPrefix' => __( 'Error:', 'holmesportfolio-forms' ), 'goToPayment' => __( 'Continue to payment', 'holmesportfolio-forms' ), /* translators: %s: rejected file name */ 'attachType' => __( '%s is not an accepted file type.', 'holmesportfolio-forms' ), /* translators: %s: rejected file name */ 'attachTooBig' => __( '%s is larger than the size limit.', 'holmesportfolio-forms' ), 'attachTooManyOne' => __( 'You can attach 1 file.', 'holmesportfolio-forms' ), /* translators: %s: the email address the visitor probably meant */ 'emailSuggest' => __( 'Did you mean %s?', 'holmesportfolio-forms' ), /* translators: %s: the suggested email address */ 'emailSuggestUse' => __( 'Use %s', 'holmesportfolio-forms' ), /* translators: %d: maximum number of files, 2 or more */ 'attachTooMany' => __( 'You can attach up to %d files.', 'holmesportfolio-forms' ), 'attachTotal' => __( 'Your files add up to more than the limit. Remove one or attach smaller files.', 'holmesportfolio-forms' ), 'attachRequired' => __( 'Upload at least one file.', 'holmesportfolio-forms' ), /* translators: %s: file name */ 'attachRemove' => __( 'Remove %s', 'holmesportfolio-forms' ), /* translators: %s: file name */ 'attachRemoved' => __( '%s removed.', 'holmesportfolio-forms' ), /* translators: %s: the names of the files just attached, separated by semicolons */ 'attachAdded' => __( '%s added.', 'holmesportfolio-forms' ), 'addrLine1' => __( 'Enter the first line of your address.', 'holmesportfolio-forms' ), 'addrTown' => __( 'Enter your town or city.', 'holmesportfolio-forms' ), 'addrPostcode' => __( 'Enter your postcode.', 'holmesportfolio-forms' ), 'addrPostcodeFormat' => __( 'Enter a real UK postcode, like SW1A 1AA.', 'holmesportfolio-forms' ), 'addrSaved' => __( 'Address saved. Use Change address to edit it.', 'holmesportfolio-forms' ), 'addrEditing' => __( 'Editing your address.', 'holmesportfolio-forms' ), /* translators: %s: labels of the questions or notices now shown */ 'condNowShowing' => __( 'Now showing: %s', 'holmesportfolio-forms' ), /* translators: %s: labels of the questions or notices just removed */ 'condNoLonger' => __( 'Now hidden: %s', 'holmesportfolio-forms' ), /* translators: 1: first three labels, 2: how many more */ 'condMore' => __( '%1$s and %2$d more', 'holmesportfolio-forms' ), /* translators: 1: current page number, 2: total pages */ 'condPage' => __( 'Page %1$d of %2$d', 'holmesportfolio-forms' ), ), ) ); } /* Rendering entry points */ /** [holmesforms_form id="123"] shortcode. */ public function render_shortcode( $atts ) { $atts = shortcode_atts( array( 'id' => 0, 'title' => '', ), $atts, self::SHORTCODE ); $form_id = absint( $atts['id'] ); if ( ! $form_id && '' !== $atts['title'] ) { $found_ids = get_posts( array( 'post_type' => HOLMESFORMS_POST_TYPE, 'post_status' => array( 'publish', 'draft' ), 'title' => sanitize_text_field( $atts['title'] ), 'numberposts' => 1, 'fields' => 'ids', 'no_found_rows' => true, 'suppress_filters' => false, ) ); if ( $found_ids ) { $form_id = (int) $found_ids[0]; } } return $this->render_form( $form_id ); } /** Shared renderer for the shortcode and the block. */ public function render_form( $form_id ) { $form_id = absint( $form_id ); if ( ! $form_id || HOLMESFORMS_POST_TYPE !== get_post_type( $form_id ) || 'publish' !== get_post_status( $form_id ) ) { if ( current_user_can( 'edit_posts' ) ) { return '

' . esc_html__( 'HolmesPortfolio Forms: no published form with that id.', 'holmesportfolio-forms' ) . '

'; } return ''; } require_once HOLMESFORMS_DIR . 'includes/class-hp-forms-components.php'; require_once HOLMESFORMS_DIR . 'includes/class-hp-forms-form.php'; require_once HOLMESFORMS_DIR . 'includes/class-hp-forms-logic.php'; require_once HOLMESFORMS_DIR . 'includes/class-hp-forms-draft.php'; require_once HOLMESFORMS_DIR . 'includes/class-hp-forms-spam.php'; require_once HOLMESFORMS_DIR . 'includes/class-hp-forms-render.php'; $form = HP_Forms_Form::get( $form_id ); if ( ! $form ) { return ''; } self::no_page_cache(); $this->enqueue_for_form( $form ); $renderer = new HP_Forms_Render( $form ); return $renderer->form_html(); } /* Block */ public function register_block() { if ( ! function_exists( 'register_block_type' ) ) { return; } wp_register_script( 'hpf-block-edit', HOLMESFORMS_URL . 'block/edit.js', array( 'wp-blocks', 'wp-element', 'wp-block-editor', 'wp-components', 'wp-server-side-render', 'wp-i18n' ), holmesforms_asset_version( 'block/edit.js' ), true ); wp_set_script_translations( 'hpf-block-edit', 'holmesportfolio-forms', HOLMESFORMS_DIR . 'languages' ); if ( is_admin() ) { $forms = get_posts( array( 'post_type' => HOLMESFORMS_POST_TYPE, 'post_status' => 'publish', 'numberposts' => self::MAX_BLOCK_FORMS, 'orderby' => 'title', 'order' => 'ASC', ) ); wp_localize_script( 'hpf-block-edit', 'holmesformsBlock', array( 'forms' => array_map( static function ( $form ) { return array( 'id' => (int) $form->ID, 'title' => get_the_title( $form ) ? get_the_title( $form ) : ( '#' . $form->ID ), ); }, $forms ), ) ); } register_block_type( HOLMESFORMS_DIR . 'block', array( 'render_callback' => array( $this, 'render_block' ), ) ); } public function render_block( $attributes ) { $form_id = isset( $attributes['formId'] ) ? absint( $attributes['formId'] ) : 0; if ( ! $form_id ) { return ''; } return $this->render_form( $form_id ); } }