array( 'name' => __( 'Forms', 'pubforms' ), 'singular_name' => __( 'Form', 'pubforms' ), 'add_new_item' => __( 'Add New Form', 'pubforms' ), 'edit_item' => __( 'Edit Form', 'pubforms' ), 'menu_name' => __( 'PubForms', 'pubforms' ), 'all_items' => __( 'All Forms', 'pubforms' ), ), 'public' => false, 'show_ui' => true, 'show_in_menu' => true, 'show_in_rest' => false, 'menu_icon' => 'dashicons-feedback', 'supports' => array( 'title' ), 'capability_type' => 'post', 'map_meta_cap' => true, ) ); } add_action( 'init', 'pubforms_register_post_type' ); /** * Cache-busting version for a bundled asset: the plugin version plus the file's modified time, so * browsers fetch a changed script or stylesheet even when the version number has not been bumped. */ function pubforms_asset_version( $file ) { $path = PUBFORMS_DIR . 'assets/' . $file; $mtime = file_exists( $path ) ? filemtime( $path ) : 0; return $mtime ? PUBFORMS_VERSION . '.' . $mtime : PUBFORMS_VERSION; } function pubforms_field_types() { return apply_filters( 'pubforms_field_types', array( 'text' => __( 'Single-line text', 'pubforms' ), 'textarea' => __( 'Paragraph text', 'pubforms' ), 'email' => __( 'Email address', 'pubforms' ), 'phone' => __( 'Phone number', 'pubforms' ), 'url' => __( 'Website URL', 'pubforms' ), 'number' => __( 'Number', 'pubforms' ), 'date' => __( 'Date', 'pubforms' ), 'time' => __( 'Time', 'pubforms' ), 'week' => __( 'Week', 'pubforms' ), 'month' => __( 'Month', 'pubforms' ), 'calculation' => __( 'Calculation', 'pubforms' ), 'select' => __( 'Dropdown', 'pubforms' ), 'radio' => __( 'Multiple choice', 'pubforms' ), 'quiz' => __( 'Quiz (graded)', 'pubforms' ), 'checkbox' => __( 'Checkbox', 'pubforms' ), 'checkbox_multi' => __( 'Checkboxes', 'pubforms' ), 'file' => __( 'File upload', 'pubforms' ), 'signature' => __( 'Signature', 'pubforms' ), 'hidden' => __( 'Hidden field', 'pubforms' ), 'section' => __( 'Section heading', 'pubforms' ), 'html' => __( 'HTML block', 'pubforms' ), ) ); } /** * Field types that take a list of choices (one per line in the builder). */ function pubforms_choice_types() { return array( 'select', 'radio', 'checkbox_multi', 'quiz' ); } /** * Default file types a file-upload field accepts, as a comma list of extensions. */ function pubforms_default_file_types() { return apply_filters( 'pubforms_default_file_types', 'jpg,jpeg,png,gif,webp,pdf,doc,docx,xls,xlsx,csv,txt,zip' ); } /** * Width presets offered to the builder, keyed by the value stored on each field. */ function pubforms_field_widths() { return array( '' => __( 'Full width', 'pubforms' ), 'half' => __( 'Half width', 'pubforms' ), 'third' => __( 'One third', 'pubforms' ), ); } /** * Clean the field list submitted from the builder (or read back from the database). * * Each field keeps a stable id, so renaming a label later does not orphan old entries. * * @param mixed $raw Rows with label, type, required, options, content, placeholder, default, help, width and id. * @return array[] id, label, type, required, options, content, placeholder, default, help, width. */ function pubforms_sanitize_fields( $raw ) { $types = pubforms_field_types(); $out = array(); $seen = array(); if ( ! is_array( $raw ) ) { return $out; } ksort( $raw ); foreach ( $raw as $row ) { if ( ! is_array( $row ) ) { continue; } $label = sanitize_text_field( $row['label'] ?? '' ); $type = sanitize_key( $row['type'] ?? 'text' ); $type = isset( $types[ $type ] ) ? $type : 'text'; // HTML blocks and hidden fields have no on-page label, so they may be left blank. if ( '' === $label && ! in_array( $type, array( 'html', 'hidden' ), true ) ) { continue; } $id = sanitize_key( $row['id'] ?? '' ); if ( '' === $id ) { $id = sanitize_title( $label ); } $id = '' !== $id ? $id : 'field'; $base = $id; $n = 2; while ( isset( $seen[ $id ] ) ) { $id = $base . '-' . $n; $n++; } $seen[ $id ] = true; $options = array(); if ( in_array( $type, pubforms_choice_types(), true ) ) { $lines = is_array( $row['options'] ?? null ) ? $row['options'] : preg_split( '/\r\n|\r|\n/', (string) ( $row['options'] ?? '' ) ); foreach ( $lines as $line ) { $line = sanitize_text_field( $line ); if ( '' !== $line && ! in_array( $line, $options, true ) ) { $options[] = $line; } } if ( ! $options ) { $type = 'text'; // A choice field with no choices is useless. } } // HTML blocks store raw markup, sanitised here so only safe content is kept. $content = 'html' === $type ? wp_kses_post( (string) ( $row['content'] ?? '' ) ) : ''; $help = sanitize_text_field( $row['help'] ?? '' ); $placeholder = sanitize_text_field( $row['placeholder'] ?? '' ); $default = sanitize_text_field( $row['default'] ?? '' ); $width = sanitize_key( $row['width'] ?? '' ); $width = in_array( $width, array( 'half', 'third' ), true ) ? $width : ''; // File uploads carry an allowed-types list and a per-field size cap (0 = the global default). $accept = ''; $max_size = 0; if ( 'file' === $type ) { $accept = trim( (string) ( $row['accept'] ?? '' ) ); $accept = '' !== $accept ? $accept : pubforms_default_file_types(); $accept = implode( ',', array_filter( array_map( 'sanitize_title', array_map( 'trim', explode( ',', $accept ) ) ) ) ); $max_size = max( 0, min( 100, (int) ( $row['max_size'] ?? 0 ) ) ); } $logic = array(); if ( isset( $row['logic'] ) && is_array( $row['logic'] ) ) { $logic_match = sanitize_key( $row['logic']['match'] ?? '' ); $logic_op = (string) ( $row['logic']['op'] ?? 'is' ); $logic_op = in_array( $logic_op, array( 'is', 'is_not', 'empty', 'not_empty' ), true ) ? $logic_op : 'is'; $logic_value = sanitize_text_field( $row['logic']['value'] ?? '' ); $logic = '' !== $logic_match ? array( 'match' => $logic_match, 'op' => $logic_op, 'value' => $logic_value ) : array(); } $page = max( 0, (int) ( $row['page'] ?? 0 ) ); // Calculation fields hold a safe formula over other number fields, e.g. {width} * {length}. $formula = ''; if ( 'calculation' === $type ) { $formula = preg_replace( '/[^0-9a-z_{}\+\-\*\/\(\)\. ]/i', '', sanitize_text_field( (string) ( $row['formula'] ?? '' ) ) ); } // Quiz fields mark one of their choices as the correct answer. $correct = ''; if ( 'quiz' === $type ) { $correct = sanitize_text_field( $row['correct'] ?? '' ); if ( ! in_array( $correct, $options, true ) ) { $correct = ''; } } $out[] = array( 'id' => $id, 'label' => $label, 'type' => $type, 'required' => ! empty( $row['required'] ), 'options' => $options, 'content' => $content, 'placeholder' => $placeholder, 'default' => $default, 'help' => $help, 'width' => $width, 'accept' => $accept, 'max_size' => $max_size, 'logic' => $logic, 'page' => $page, 'formula' => $formula, 'correct' => $correct ); if ( count( $out ) >= 50 ) { break; } } return $out; } function pubforms_default_settings() { return array( 'notify_email' => get_option( 'pubforms_default_notify_email', (string) get_option( 'admin_email' ) ), /* translators: %s: form title */ 'subject' => __( 'New submission: {form}', 'pubforms' ), 'success' => __( 'Thanks! Your message has been sent.', 'pubforms' ), 'button' => __( 'Submit', 'pubforms' ), 'confirmation_type' => 'message', 'redirect_to' => '', 'reply_enabled' => false, 'reply_subject' => __( 'Thanks for contacting us!', 'pubforms' ), 'reply_body' => __( 'Thanks, we received your message and will reply soon.', 'pubforms' ), 'spam_provider' => '', 'webhook_urls' => '', 'save_abandoned' => false, ); } function pubforms_sanitize_settings( $raw ) { $raw = is_array( $raw ) ? $raw : array(); $defaults = pubforms_default_settings(); $emails = array(); foreach ( explode( ',', (string) ( $raw['notify_email'] ?? '' ) ) as $email ) { $email = sanitize_email( trim( $email ) ); if ( is_email( $email ) && ! in_array( $email, $emails, true ) ) { $emails[] = $email; } } $subject = trim( preg_replace( '/[\r\n]+/', ' ', sanitize_text_field( $raw['subject'] ?? '' ) ) ); $success = sanitize_text_field( $raw['success'] ?? '' ); $button = sanitize_text_field( $raw['button'] ?? '' ); // Confirmation handling. $confirmation_type = (string) ( $raw['confirmation_type'] ?? 'message' ); $confirmation_type = in_array( $confirmation_type, array( 'message', 'redirect' ), true ) ? $confirmation_type : 'message'; $redirect_to = esc_url_raw( trim( (string) ( $raw['redirect_to'] ?? '' ) ) ); $reply_enabled = ! empty( $raw['reply_enabled'] ); $reply_subject = trim( preg_replace( '/[\r\n]+/', ' ', sanitize_text_field( $raw['reply_subject'] ?? '' ) ) ); $reply_body = trim( sanitize_textarea_field( $raw['reply_body'] ?? '' ) ); // '' follows the site-wide method; 'none' turns spam challenges off for this form. $spam_provider = in_array( (string) ( $raw['spam_provider'] ?? '' ), array( 'none', 'turnstile', 'hcaptcha', 'question' ), true ) ? sanitize_key( $raw['spam_provider'] ) : ''; $webhooks = array(); foreach ( preg_split( '/[\r\n,]+/', (string) ( $raw['webhook_urls'] ?? '' ) ) as $url ) { $url = trim( $url ); $lower = strtolower( $url ); if ( '' === $url || in_array( $url, $webhooks, true ) ) { continue; } if ( ( 0 === strpos( $lower, 'https://' ) || 0 === strpos( $lower, 'http://' ) ) && filter_var( $url, FILTER_VALIDATE_URL ) ) { $webhooks[] = $url; } if ( count( $webhooks ) >= 5 ) { break; } } return array( 'notify_email' => $emails ? implode( ', ', array_slice( $emails, 0, 5 ) ) : '', 'subject' => '' !== $subject ? $subject : $defaults['subject'], 'success' => '' !== $success ? $success : $defaults['success'], 'button' => '' !== $button ? $button : $defaults['button'], 'confirmation_type' => 'message' !== $confirmation_type ? $confirmation_type : $defaults['confirmation_type'], 'redirect_to' => 'redirect' === $confirmation_type ? $redirect_to : '', 'reply_enabled' => $reply_enabled, 'reply_subject' => '' !== $reply_subject ? $reply_subject : $defaults['reply_subject'], 'reply_body' => '' !== $reply_body ? $reply_body : $defaults['reply_body'], 'spam_provider' => $spam_provider, 'webhook_urls' => implode( "\n", $webhooks ), 'save_abandoned' => ! empty( $raw['save_abandoned'] ), ); } /** * @return array|null id, title, status, fields, settings. */ function pubforms_get_form( $form_id ) { $post = get_post( absint( $form_id ) ); if ( ! $post || 'pubforms_form' !== $post->post_type ) { return null; } $fields = pubforms_sanitize_fields( get_post_meta( $post->ID, '_pubforms_fields', true ) ); $stored = get_post_meta( $post->ID, '_pubforms_settings', true ); $settings = wp_parse_args( is_array( $stored ) ? array_filter( $stored, 'strlen' ) : array(), pubforms_default_settings() ); return array( 'id' => $post->ID, 'title' => get_the_title( $post ), 'status' => $post->post_status, 'fields' => $fields, 'settings' => $settings, ); }