'frontglut_manage_settings', 'json' => array( 'form' ) ) ); Ajax::register( 'form_preview', array( $this, 'ajax_preview' ), array( 'cap' => 'frontglut_manage_settings', 'json' => array( 'form' ) ) ); Ajax::register( 'form_submit', array( $this, 'ajax_submit' ), array( 'public' => true ) ); add_shortcode( 'frontglut_form', array( $this, 'shortcode' ) ); add_filter( 'frontglut_field_render_taxonomy', array( __CLASS__, 'render_taxonomy' ), 10, 5 ); add_filter( 'frontglut_field_sanitize_taxonomy', array( __CLASS__, 'sanitize_taxonomy' ), 10, 3 ); add_filter( 'frontglut_register_field_types', function ( $t ) { $t['taxonomy'] = __( 'Taxonomy terms', 'frontglut' ); return $t; } ); add_filter( 'frontglut_form_handle_custom', array( $this, 'handle_custom' ), 10, 3 ); } /* ---------------- storage ---------------- */ public static function table() { global $wpdb; return $wpdb->prefix . 'frontglut_forms'; } private static function hydrate( $row ) { if ( ! $row ) { return null; } $row['id'] = (int) $row['id']; $row['settings'] = json_decode( (string) $row['settings'], true ); $row['settings'] = is_array( $row['settings'] ) ? $row['settings'] : array(); $row['fields'] = json_decode( (string) $row['fields'], true ); $row['fields'] = is_array( $row['fields'] ) ? $row['fields'] : array(); return $row; } public static function get( $id ) { global $wpdb; $table = self::table(); return self::hydrate( $wpdb->get_row( $wpdb->prepare( "SELECT * FROM `{$table}` WHERE id = %d", $id ), ARRAY_A ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- table name is the plugin's own prefixed constant. } public static function all( $type = null ) { global $wpdb; $table = self::table(); if ( $type ) { $rows = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM `{$table}` WHERE type = %s ORDER BY id ASC", $type ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- plugin-owned table name. } else { $rows = $wpdb->get_results( "SELECT * FROM `{$table}` ORDER BY id ASC", ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- plugin-owned table name, no user input. } return array_map( array( __CLASS__, 'hydrate' ), (array) $rows ); } /** @return int id */ public static function save( array $form ) { global $wpdb; $data = array( 'type' => in_array( $form['type'], array( 'post', 'profile', 'register', 'custom' ), true ) ? $form['type'] : 'custom', 'title' => sanitize_text_field( $form['title'] ), 'settings' => wp_json_encode( isset( $form['settings'] ) ? $form['settings'] : array() ), 'fields' => wp_json_encode( isset( $form['fields'] ) ? $form['fields'] : array() ), 'status' => 'active', ); if ( ! empty( $form['id'] ) ) { $wpdb->update( self::table(), $data, array( 'id' => (int) $form['id'] ) ); return (int) $form['id']; } $data['created'] = current_time( 'mysql', true ); $wpdb->insert( self::table(), $data ); return (int) $wpdb->insert_id; } public static function delete( $id ) { global $wpdb; $defaults = (array) get_option( 'frontglut_default_forms', array() ); if ( in_array( (int) $id, array_map( 'intval', $defaults ), true ) && ! apply_filters( 'frontglut_allow_delete_default_form', false ) ) { return false; } return (bool) $wpdb->delete( self::table(), array( 'id' => (int) $id ) ); } /** The seeded default form of a type (register, profile) or named key. */ public static function default_form( $key ) { $d = (array) get_option( 'frontglut_default_forms', array() ); $id = isset( $d[ $key ] ) ? (int) $d[ $key ] : 0; $f = $id ? self::get( $id ) : null; if ( ! $f ) { self::seed(); $d = (array) get_option( 'frontglut_default_forms', array() ); $f = isset( $d[ $key ] ) ? self::get( (int) $d[ $key ] ) : null; } return $f; } public static function maybe_seed() { if ( ! get_option( 'frontglut_default_forms' ) ) { self::seed(); } } public static function seed() { global $wpdb; $d = (array) get_option( 'frontglut_default_forms', array() ); $make = function ( $key, $form ) use ( &$d ) { if ( ! empty( $d[ $key ] ) && self::get( (int) $d[ $key ] ) ) { return; } $d[ $key ] = self::save( $form ); }; $t = self::templates(); $make( 'register', array( 'type' => 'register', 'title' => __( 'Registration form', 'frontglut' ), 'settings' => array( 'success' => __( 'Account created.', 'frontglut' ) ), 'fields' => $t['register']['fields'] ) ); $make( 'profile', array( 'type' => 'profile', 'title' => __( 'Profile form', 'frontglut' ), 'settings' => array(), 'fields' => $t['profile']['fields'] ) ); $make( 'post_guest', array( 'type' => 'post', 'title' => __( 'Guest post form', 'frontglut' ), 'settings' => array_merge( $t['article']['settings'], array( 'guest' => 1, 'captcha' => 1 ) ), 'fields' => $t['article']['fields'] ) ); $make( 'post_member', array( 'type' => 'post', 'title' => __( 'Member post form', 'frontglut' ), 'settings' => array_merge( $t['article']['settings'], array( 'guest' => 0 ) ), 'fields' => $t['article']['fields'] ) ); update_option( 'frontglut_default_forms', $d, false ); } /* ---------------- templates ---------------- */ public static function f( $key, $type, $label, $extra = array() ) { return array_merge( Fields::defaults(), array( 'key' => $key, 'type' => $type, 'label' => $label ), $extra ); } public static function post_settings( $extra = array() ) { return array_merge( array( 'post_type' => 'post', 'status' => 'pending', 'status_roles' => '', 'author' => 0, 'guest' => 0, 'captcha' => 0, 'editor' => 'classic', 'success' => __( 'Thank you! Your submission was received.', 'frontglut' ), 'redirect' => '', 'notify_admin' => 1, 'notify_user' => 1, 'per_hour' => 5, 'login_msg' => '', 'limit_title' => 150 ), $extra ); } public static function templates() { $f = array( __CLASS__, 'f' ); $std = array( self::f( 'title', 'text', __( 'Title', 'frontglut' ), array( 'required' => 1, 'storage' => 'post_title', 'maxlength' => 150 ) ), self::f( 'content', 'editor', __( 'Content', 'frontglut' ), array( 'required' => 1, 'storage' => 'post_content' ) ), self::f( 'featured', 'image', __( 'Featured image', 'frontglut' ), array( 'storage' => 'featured' ) ), self::f( 'category', 'taxonomy', __( 'Category', 'frontglut' ), array( 'storage' => 'tax', 'taxonomy' => 'category', 'ui' => 'select' ) ), self::f( 'tags', 'taxonomy', __( 'Tags', 'frontglut' ), array( 'storage' => 'tax', 'taxonomy' => 'post_tag', 'ui' => 'tags', 'help' => __( 'Separate with commas.', 'frontglut' ) ) ), ); return apply_filters( 'frontglut_form_templates', array( 'register' => array( 'label' => 'Registration', 'type' => 'register', 'settings' => array(), 'fields' => array( self::f( 'user_login', 'text', __( 'Username', 'frontglut' ), array( 'required' => 1, 'storage' => 'user_login', 'width' => '50' ) ), self::f( 'user_email', 'email', __( 'Email', 'frontglut' ), array( 'required' => 1, 'storage' => 'user_email', 'width' => '50' ) ), self::f( 'user_pass', 'password', __( 'Password', 'frontglut' ), array( 'required' => 1, 'storage' => 'user_pass', 'minlength' => 8, 'width' => '50' ) ), self::f( 'user_pass2', 'password', __( 'Confirm password', 'frontglut' ), array( 'required' => 1, 'storage' => 'user_pass2', 'width' => '50' ) ), ) ), 'profile' => array( 'label' => 'Profile', 'type' => 'profile', 'settings' => array(), 'fields' => array( self::f( 'first_name', 'text', __( 'First name', 'frontglut' ), array( 'storage' => 'first_name', 'width' => '50' ) ), self::f( 'last_name', 'text', __( 'Last name', 'frontglut' ), array( 'storage' => 'last_name', 'width' => '50' ) ), self::f( 'display_name', 'text', __( 'Display name', 'frontglut' ), array( 'storage' => 'display_name', 'required' => 1, 'width' => '50' ) ), self::f( 'user_email', 'email', __( 'Email', 'frontglut' ), array( 'storage' => 'user_email', 'required' => 1, 'width' => '50' ) ), self::f( 'user_url', 'url', __( 'Website', 'frontglut' ), array( 'storage' => 'user_url' ) ), self::f( 'description', 'textarea', __( 'Bio', 'frontglut' ), array( 'storage' => 'description' ) ), self::f( 'frontglut_avatar', 'image', __( 'Profile picture', 'frontglut' ), array( 'storage' => 'meta' ) ), ) ), 'article' => array( 'label' => __( 'Article / blog post', 'frontglut' ), 'type' => 'post', 'settings' => self::post_settings(), 'fields' => $std ), 'news_tip' => array( 'label' => __( 'News tip (short)', 'frontglut' ), 'type' => 'post', 'settings' => self::post_settings( array( 'editor' => 'plain', 'guest' => 1, 'captcha' => 1 ) ), 'fields' => array( self::f( 'title', 'text', __( 'Headline', 'frontglut' ), array( 'required' => 1, 'storage' => 'post_title' ) ), self::f( 'content', 'textarea', __( 'What happened?', 'frontglut' ), array( 'required' => 1, 'storage' => 'post_content', 'maxlength' => 1500 ) ), self::f( 'source', 'url', __( 'Source link', 'frontglut' ), array( 'storage' => 'meta' ) ), self::f( 'photos', 'gallery', __( 'Photos', 'frontglut' ), array( 'storage' => 'gallery' ) ), ) ), 'recipe' => array( 'label' => __( 'Recipe', 'frontglut' ), 'type' => 'post', 'settings' => self::post_settings(), 'fields' => array( self::f( 'title', 'text', __( 'Recipe name', 'frontglut' ), array( 'required' => 1, 'storage' => 'post_title' ) ), self::f( 'excerpt', 'textarea', __( 'Short description', 'frontglut' ), array( 'storage' => 'post_excerpt', 'maxlength' => 250 ) ), self::f( 'prep', 'number', __( 'Prep time (min)', 'frontglut' ), array( 'storage' => 'meta', 'width' => '50' ) ), self::f( 'servings', 'number', __( 'Servings', 'frontglut' ), array( 'storage' => 'meta', 'width' => '50' ) ), self::f( 'ingredients', 'table', __( 'Ingredients', 'frontglut' ), array( 'storage' => 'meta', 'columns' => 'Ingredient,Amount' ) ), self::f( 'content', 'editor', __( 'Method', 'frontglut' ), array( 'required' => 1, 'storage' => 'post_content' ) ), self::f( 'featured', 'image', __( 'Photo', 'frontglut' ), array( 'storage' => 'featured' ) ), self::f( 'category', 'taxonomy', __( 'Category', 'frontglut' ), array( 'storage' => 'tax', 'taxonomy' => 'category', 'ui' => 'checkboxes' ) ), ) ), 'classified' => array( 'label' => __( 'Classified ad', 'frontglut' ), 'type' => 'post', 'settings' => self::post_settings( array( 'editor' => 'lite' ) ), 'fields' => array( self::f( 'title', 'text', __( 'Ad title', 'frontglut' ), array( 'required' => 1, 'storage' => 'post_title' ) ), self::f( 'kind', 'radio', __( 'Type', 'frontglut' ), array( 'storage' => 'meta', 'options' => "sell|For sale\nbuy|Wanted\nfree|Free", 'default' => 'sell' ) ), self::f( 'price', 'number', __( 'Price', 'frontglut' ), array( 'storage' => 'meta', 'min' => 0, 'width' => '50', 'logic' => array( 'field' => 'kind', 'op' => 'neq', 'value' => 'free' ) ) ), self::f( 'phone', 'tel', __( 'Phone', 'frontglut' ), array( 'storage' => 'meta', 'width' => '50' ) ), self::f( 'content', 'editor', __( 'Description', 'frontglut' ), array( 'required' => 1, 'storage' => 'post_content' ) ), self::f( 'photos', 'gallery', __( 'Photos', 'frontglut' ), array( 'storage' => 'gallery' ) ), ) ), 'photo' => array( 'label' => __( 'Photo story', 'frontglut' ), 'type' => 'post', 'settings' => self::post_settings( array( 'editor' => 'lite' ) ), 'fields' => array( self::f( 'title', 'text', __( 'Title', 'frontglut' ), array( 'required' => 1, 'storage' => 'post_title' ) ), self::f( 'featured', 'image', __( 'Cover photo', 'frontglut' ), array( 'required' => 1, 'storage' => 'featured' ) ), self::f( 'photos', 'gallery', __( 'More photos', 'frontglut' ), array( 'storage' => 'gallery' ) ), self::f( 'content', 'editor', __( 'Story', 'frontglut' ), array( 'storage' => 'post_content' ) ), self::f( 'credit', 'text', __( 'Photo credit', 'frontglut' ), array( 'storage' => 'meta' ) ), ) ), 'contact' => array( 'label' => __( 'Contact form (entries)', 'frontglut' ), 'type' => 'custom', 'settings' => array( 'success' => __( 'Thanks, we will be in touch.', 'frontglut' ), 'notify_admin' => 1, 'captcha' => 0, 'guest' => 1, 'per_hour' => 10 ), 'fields' => array( self::f( 'name', 'text', __( 'Your name', 'frontglut' ), array( 'required' => 1, 'width' => '50' ) ), self::f( 'email', 'email', __( 'Email', 'frontglut' ), array( 'required' => 1, 'width' => '50' ) ), self::f( 'subject', 'text', __( 'Subject', 'frontglut' ) ), self::f( 'message', 'textarea', __( 'Message', 'frontglut' ), array( 'required' => 1 ) ), ) ), ) ); } /* ---------------- taxonomy field ---------------- */ public static function render_taxonomy( $o, $f, $v, $name, $id ) { $tax = isset( $f['taxonomy'] ) ? $f['taxonomy'] : 'category'; if ( ! taxonomy_exists( $tax ) ) { return '' . esc_html__( 'Unknown taxonomy', 'frontglut' ) . ''; } $ui = isset( $f['ui'] ) ? $f['ui'] : 'select'; if ( 'tags' === $ui ) { $names = ''; if ( is_array( $v ) && $v ) { $names = implode( ', ', array_map( function ( $t ) { return is_numeric( $t ) ? ( get_term( (int) $t ) ? get_term( (int) $t )->name : '' ) : $t; }, $v ) ); } elseif ( is_string( $v ) ) { $names = $v; } return ''; } $terms = get_terms( array( 'taxonomy' => $tax, 'hide_empty' => false, 'number' => 300 ) ); if ( is_wp_error( $terms ) ) { return ''; } $cur = array_map( 'intval', (array) $v ); if ( 'checkboxes' === $ui ) { $o = '
'; foreach ( $terms as $t ) { $o .= ''; } return $o . '
'; } $multi = 'multiselect' === $ui; $o = ''; } /** Returns array of term ids (or for tags UI: array of names). */ public static function sanitize_taxonomy( $pre, $f, $raw ) { $ui = isset( $f['ui'] ) ? $f['ui'] : 'select'; if ( 'tags' === $ui ) { $names = array_filter( array_map( 'sanitize_text_field', explode( ',', is_array( $raw ) ? implode( ',', $raw ) : (string) $raw ) ) ); return array_slice( array_values( array_unique( $names ) ), 0, 20 ); } $tax = isset( $f['taxonomy'] ) ? $f['taxonomy'] : 'category'; $ids = array_filter( array_map( 'absint', (array) $raw ) ); $out = array(); foreach ( $ids as $id ) { if ( term_exists( $id, $tax ) ) { $out[] = $id; } } if ( 'multiselect' !== $ui && 'checkboxes' !== $ui ) { $out = array_slice( $out, 0, 1 ); } return $out; } /* ---------------- rendering ---------------- */ /** * Render a list of fields. $roles filters by field 'roles' visibility. */ public static function render_fields( array $fields, array $values = array(), $prefix = 'frontglut_f', $form_id = 0 ) { $user = wp_get_current_user(); $roles = (array) $user->roles; $out = ''; foreach ( $fields as $f ) { if ( empty( $f['key'] ) && ! in_array( $f['type'], array( 'heading', 'label' ), true ) ) { continue; } if ( ! empty( $f['roles'] ) && ! array_intersect( (array) $f['roles'], $roles ) && ! current_user_can( 'manage_options' ) ) { continue; } if ( $form_id && in_array( $f['type'], array( 'file', 'image', 'gallery' ), true ) ) { $f['guest_form'] = $form_id; } $val = isset( $values[ $f['key'] ] ) ? $values[ $f['key'] ] : ( isset( $f['default'] ) ? $f['default'] : '' ); $out .= Fields::render( $f, $val, $prefix . '[' . $f['key'] . ']', array( 'prefix' => $prefix ) ); } return $out; } public static function honeypot() { return ''; } public static function check_honeypot( array $req ) { return empty( $req['frontglut_hp'] ); } public function shortcode( $atts ) { $a = shortcode_atts( array( 'id' => 0 ), $atts ); $form = self::get( (int) $a['id'] ); if ( ! $form ) { return ''; } \FrontGlut\Core\Assets::need(); if ( 'post' === $form['type'] ) { return do_shortcode( '[frontglut_post_form id="' . $form['id'] . '"]' ); } if ( 'custom' !== $form['type'] ) { return ''; } $s = $form['settings']; if ( empty( $s['guest'] ) && ! is_user_logged_in() ) { return '
' . Ui::notice( esc_html__( 'Please log in to use this form.', 'frontglut' ), 'warn' ) . '
'; } $h = '
' . Ui::form_open( 'form_submit', array( 'after' => 'reset' ) ) . Ui::hidden( 'form_id', $form['id'] ) . self::honeypot(); $h .= self::render_fields( $form['fields'], array(), 'frontglut_f', $form['id'] ); if ( ! empty( $s['captcha'] ) && Captcha::enabled() ) { $h .= Captcha::render(); } return $h . Ui::form_close( __( 'Send', 'frontglut' ) ) . '
'; } /* ---------------- generic submit ---------------- */ public function ajax_submit( $req ) { $form = self::get( isset( $req['form_id'] ) ? (int) $req['form_id'] : 0 ); if ( ! $form ) { Ajax::fail( __( 'Form not found.', 'frontglut' ), 404 ); } $s = $form['settings']; if ( empty( $s['guest'] ) && ! is_user_logged_in() ) { Ajax::fail( __( 'Please log in first.', 'frontglut' ), 401 ); } if ( ! self::check_honeypot( $req ) ) { Ajax::fail( __( 'Spam detected.', 'frontglut' ) ); } if ( ! empty( $s['captcha'] ) ) { $c = Captcha::verify( $req ); if ( is_wp_error( $c ) ) { Ajax::fail( $c->get_error_message() ); } } if ( ! RateLimiter::hit( 'form' . $form['id'], max( 1, (int) ( isset( $s['per_hour'] ) ? $s['per_hour'] : 10 ) ), HOUR_IN_SECONDS, is_user_logged_in() ? 'u' . get_current_user_id() : '' ) ) { Ajax::fail( __( 'Too many submissions. Please try again later.', 'frontglut' ), 429 ); } $res = apply_filters( 'frontglut_form_handle_' . $form['type'], null, $form, $req ); if ( null === $res ) { Ajax::fail( __( 'This form cannot be submitted.', 'frontglut' ) ); } if ( is_wp_error( $res ) ) { Ajax::fail( $res->get_error_message(), 400, array( 'errors' => (array) $res->get_error_data() ) ); } return $res; } public function handle_custom( $pre, $form, $req ) { if ( null !== $pre ) { return $pre; // an earlier filter (add-on) already decided (e.g. a limit error). } $post = isset( $req['frontglut_f'] ) && is_array( $req['frontglut_f'] ) ? $req['frontglut_f'] : array(); list( $values, $errors ) = Fields::process( $form['fields'], $post ); if ( $errors ) { return new \WP_Error( 'invalid', __( 'Please fix the highlighted fields.', 'frontglut' ), $errors ); } foreach ( $form['fields'] as $f ) { if ( in_array( $f['type'], array( 'file', 'image', 'gallery' ), true ) && ! empty( $values[ $f['key'] ] ) ) { $values[ $f['key'] ] = \FrontGlut\Core\Uploads::filter_owned( (array) $values[ $f['key'] ] ); } } $id = wp_insert_post( array( 'post_type' => 'frontglut_entry', 'post_status' => 'private', 'post_title' => sprintf( '%s #%s', $form['title'], gmdate( 'ymd-His' ) ), 'post_content' => wp_json_encode( $values ), 'post_author' => get_current_user_id() ), true ); if ( is_wp_error( $id ) ) { return $id; } update_post_meta( $id, '_frontglut_form', $form['id'] ); foreach ( $values as $v ) { if ( is_array( $v ) ) { \FrontGlut\Core\Uploads::claim( array_filter( array_map( 'absint', $v ) ), $id ); } } if ( ! empty( $form['settings']['notify_admin'] ) ) { $lines = array(); foreach ( $form['fields'] as $f ) { if ( isset( $values[ $f['key'] ] ) ) { $lines[] = $f['label'] . ': ' . ( is_array( $values[ $f['key'] ] ) ? wp_json_encode( $values[ $f['key'] ] ) : $values[ $f['key'] ] ); } } /* translators: %s: value. */ Mailer::send( get_option( 'admin_email' ), sprintf( __( 'New entry: %s', 'frontglut' ), $form['title'] ), implode( "\n", $lines ) ); } return array( 'message' => ! empty( $form['settings']['success'] ) ? $form['settings']['success'] : __( 'Thanks!', 'frontglut' ), 'redirect' => ! empty( $form['settings']['redirect'] ) ? esc_url_raw( $form['settings']['redirect'] ) : '' ); } public function register_entry_cpt() { register_post_type( 'frontglut_entry', array( 'labels' => array( 'name' => __( 'Form entries', 'frontglut' ) ), 'public' => false, 'show_ui' => true, 'show_in_menu' => 'frontglut', 'supports' => array( 'title', 'editor' ), 'capabilities' => array( 'create_posts' => 'do_not_allow' ), 'map_meta_cap' => true ) ); } /* ---------------- admin: builder ---------------- */ public function admin_tabs( $tabs ) { $tabs['forms'] = array( 'label' => __( 'Forms', 'frontglut' ), 'order' => 12, 'cb' => array( $this, 'tab_forms' ) ); return $tabs; } public function builder_assets( $hook ) { if ( false === strpos( (string) $hook, 'frontglut' ) || ( Request::route( 'tab', 'key', '' ) ) !== 'forms' ) { return; } wp_enqueue_script( 'frontglut-builder', FRONTGLUT_URL . 'assets/js/builder.js', array( 'frontglut-admin', 'jquery-ui-sortable' ), FRONTGLUT_VERSION, true ); } private function nonce_url( $action, $args ) { return wp_nonce_url( add_query_arg( array_merge( array( 'action' => $action ), $args ), admin_url( 'admin-post.php' ) ), $action ); } public function tab_forms() { $edit = Request::route( 'edit', 'int', 0 ); if ( $edit && ( $form = self::get( $edit ) ) ) { return $this->builder( $form ); } echo '

' . esc_html__( 'Build forms for posts, registration, profiles and generic entries. Embed with a shortcode or the FrontGlut block.', 'frontglut' ) . '

'; echo '
'; wp_nonce_field( 'frontglut_form_new' ); echo ' '; submit_button( __( 'Create form', 'frontglut' ), 'primary', 'submit', false ); echo '

'; echo ''; foreach ( self::all() as $f ) { $sc = in_array( $f['type'], array( 'post', 'custom' ), true ) ? '[frontglut_form id="' . $f['id'] . '"]' : '—'; echo ''; } echo '
ID' . esc_html__( 'Title', 'frontglut' ) . '' . esc_html__( 'Type', 'frontglut' ) . '' . esc_html__( 'Shortcode', 'frontglut' ) . '
' . (int) $f['id'] . '' . esc_html( $f['title'] ) . '' . esc_html( $f['type'] ) . '' . esc_html( $sc ) . ''; echo '' . esc_html__( 'Edit', 'frontglut' ) . ' | ' . esc_html__( 'Duplicate', 'frontglut' ) . ' | ' . esc_html__( 'Export', 'frontglut' ) . ' | ' . esc_html__( 'Delete', 'frontglut' ) . '

' . esc_html__( 'Import form (JSON)', 'frontglut' ) . '

'; wp_nonce_field( 'frontglut_form_import' ); echo ''; submit_button( __( 'Import', 'frontglut' ), 'secondary' ); echo '
'; } private function builder( array $form ) { $taxes = array(); foreach ( get_taxonomies( array( 'public' => true ), 'objects' ) as $t ) { $taxes[ $t->name ] = $t->label; } $roles = array(); foreach ( wp_roles()->roles as $k => $r ) { $roles[ $k ] = translate_user_role( $r['name'] ); } $pts = array(); foreach ( get_post_types( array( 'show_ui' => true ), 'objects' ) as $p ) { if ( ! in_array( $p->name, array( 'attachment', 'frontglut_entry', 'wp_block', 'wp_template', 'wp_template_part', 'wp_navigation', 'wp_global_styles', 'shop_order', 'shop_coupon' ), true ) ) { $pts[ $p->name ] = $p->label; } } $conf = array( 'form' => $form, 'types' => Fields::types(), 'taxes' => $taxes, 'roles' => $roles, 'pts' => $pts, 'postStatuses' => array( 'publish' => 'Publish', 'pending' => 'Pending review', 'draft' => 'Draft' ), 'nonce' => Ajax::nonce(), 'back' => Admin::url( 'forms' ), // Add-on hooks: per-type extra field options and extra form settings (key,label,type,options?). 'extra' => apply_filters( 'frontglut_builder_field_options', array( 'table' => array( array( 'key' => 'columns', 'label' => 'Columns (comma separated)', 'type' => 'text' ) ) ) ), 'formExtra' => apply_filters( 'frontglut_builder_form_options', array(), $form ), ); echo '

← ' . esc_html__( 'All forms', 'frontglut' ) . '

'; if ( in_array( $form['type'], array( 'post', 'custom' ), true ) ) { echo '

' . esc_html__( 'Shortcode:', 'frontglut' ) . ' [frontglut_form id="' . (int) $form['id'] . '"]

'; } } public function ajax_save( $req ) { $data = isset( $req['form'] ) ? $req['form'] : array(); // Decoded and sanitised by Ajax::request(); clean_form() validates every key. if ( ! is_array( $data ) || empty( $data['id'] ) || ! self::get( (int) $data['id'] ) ) { Ajax::fail( __( 'Invalid form data.', 'frontglut' ) ); } $existing = self::get( (int) $data['id'] ); $clean = self::clean_form( $data, $existing['type'] ); $clean['id'] = $existing['id']; $clean['type'] = $existing['type']; self::save( $clean ); Audit::log( 'form_saved', (string) $existing['id'] ); return array( 'message' => __( 'Form saved.', 'frontglut' ) ); } public function ajax_preview( $req ) { $data = isset( $req['form'] ) ? $req['form'] : array(); // Decoded and sanitised by Ajax::request(). $clean = self::clean_form( is_array( $data ) ? $data : array(), 'custom' ); return array( 'html' => '
' . self::render_fields( $clean['fields'], array(), 'frontglut_pv' ) . '
' ); } /** Sanitise a whole form definition coming from the builder/import. */ public static function clean_form( array $data, $type ) { $types = array_keys( Fields::types() ); $fields = array(); $seen = array(); foreach ( (array) ( isset( $data['fields'] ) ? $data['fields'] : array() ) as $f ) { if ( ! is_array( $f ) ) { continue; } $c = Fields::defaults(); $c['type'] = isset( $f['type'] ) && in_array( $f['type'], $types, true ) ? $f['type'] : 'text'; $c['key'] = isset( $f['key'] ) ? preg_replace( '/[^a-z0-9_]/', '', strtolower( str_replace( array( ' ', '-' ), '_', (string) $f['key'] ) ) ) : ''; if ( '' === $c['key'] && ! in_array( $c['type'], array( 'heading', 'label' ), true ) ) { $c['key'] = 'field_' . ( count( $fields ) + 1 ); } if ( isset( $seen[ $c['key'] ] ) && '' !== $c['key'] ) { $c['key'] .= '_' . ( count( $fields ) + 1 ); } $seen[ $c['key'] ] = 1; $c['label'] = isset( $f['label'] ) ? sanitize_text_field( $f['label'] ) : ''; $c['required'] = ! empty( $f['required'] ) ? 1 : 0; $c['placeholder'] = isset( $f['placeholder'] ) ? sanitize_text_field( $f['placeholder'] ) : ''; $c['help'] = isset( $f['help'] ) ? wp_kses_post( $f['help'] ) : ''; $c['options'] = isset( $f['options'] ) ? sanitize_textarea_field( is_array( $f['options'] ) ? '' : $f['options'] ) : ''; $c['default'] = isset( $f['default'] ) && is_scalar( $f['default'] ) ? sanitize_text_field( $f['default'] ) : ''; $c['width'] = isset( $f['width'] ) && in_array( (string) $f['width'], array( '25', '33', '50', '66', '75', '100' ), true ) ? (string) $f['width'] : '100'; $c['class'] = isset( $f['class'] ) ? sanitize_html_class( $f['class'] ) : ''; $c['storage'] = isset( $f['storage'] ) ? preg_replace( '/[^a-z0-9_:\-]/i', '', $f['storage'] ) : 'meta'; foreach ( array( 'min', 'max', 'maxlength', 'minlength' ) as $n ) { $c[ $n ] = isset( $f[ $n ] ) && is_numeric( $f[ $n ] ) ? $f[ $n ] + 0 : ''; } $c['multiple'] = ! empty( $f['multiple'] ) ? 1 : 0; $c['roles'] = isset( $f['roles'] ) ? array_values( array_intersect( array_map( 'sanitize_key', (array) $f['roles'] ), array_keys( wp_roles()->roles ) ) ) : array(); if ( ! empty( $f['logic']['field'] ) ) { $c['logic'] = array( 'field' => sanitize_key( $f['logic']['field'] ), 'op' => in_array( $f['logic']['op'] ?? 'eq', array( 'eq', 'neq', 'contains', 'empty', 'notempty' ), true ) ? $f['logic']['op'] : 'eq', 'value' => sanitize_text_field( $f['logic']['value'] ?? '' ) ); } else { $c['logic'] = array(); } if ( 'taxonomy' === $c['type'] ) { $c['taxonomy'] = isset( $f['taxonomy'] ) && taxonomy_exists( $f['taxonomy'] ) ? $f['taxonomy'] : 'category'; $c['ui'] = isset( $f['ui'] ) && in_array( $f['ui'], array( 'select', 'multiselect', 'checkboxes', 'tags' ), true ) ? $f['ui'] : 'select'; $c['storage'] = 'tax'; } if ( 'table' === $c['type'] ) { $c['columns'] = isset( $f['columns'] ) ? sanitize_text_field( $f['columns'] ) : 'Column 1,Column 2'; } $c = apply_filters( 'frontglut_clean_field', $c, $f ); if ( 'editor' === $c['type'] ) { $c['editor'] = ''; } $fields[] = $c; } $in = isset( $data['settings'] ) && is_array( $data['settings'] ) ? $data['settings'] : array(); $s = array(); $s['success'] = isset( $in['success'] ) ? sanitize_text_field( $in['success'] ) : ''; $s['redirect'] = isset( $in['redirect'] ) ? esc_url_raw( $in['redirect'] ) : ''; $s['guest'] = ! empty( $in['guest'] ) ? 1 : 0; $s['captcha'] = ! empty( $in['captcha'] ) ? 1 : 0; $s['per_hour'] = isset( $in['per_hour'] ) ? max( 1, (int) $in['per_hour'] ) : 5; $s['notify_admin'] = ! empty( $in['notify_admin'] ) ? 1 : 0; $s['notify_user'] = ! empty( $in['notify_user'] ) ? 1 : 0; if ( 'post' === $type ) { $s['post_type'] = isset( $in['post_type'] ) && post_type_exists( $in['post_type'] ) ? $in['post_type'] : 'post'; $s['status'] = isset( $in['status'] ) && in_array( $in['status'], array( 'publish', 'pending', 'draft' ), true ) ? $in['status'] : 'pending'; $s['status_roles'] = isset( $in['status_roles'] ) ? sanitize_textarea_field( $in['status_roles'] ) : ''; $s['author'] = isset( $in['author'] ) ? absint( $in['author'] ) : 0; $s['editor'] = isset( $in['editor'] ) && in_array( $in['editor'], array( 'classic', 'lite', 'plain' ), true ) ? $in['editor'] : 'classic'; $s['login_msg'] = isset( $in['login_msg'] ) ? sanitize_text_field( $in['login_msg'] ) : ''; } $s = apply_filters( 'frontglut_clean_form_settings', $s, $in, $type ); return array( 'title' => isset( $data['title'] ) ? sanitize_text_field( $data['title'] ) : 'Form', 'settings' => $s, 'fields' => $fields ); } public function new_action() { check_admin_referer( 'frontglut_form_new' ); $this->guard(); $tpl = self::templates(); $k = isset( $_POST['template'] ) ? sanitize_key( $_POST['template'] ) : 'article'; $t = isset( $tpl[ $k ] ) ? $tpl[ $k ] : $tpl['article']; $id = self::save( array( 'type' => $t['type'], 'title' => isset( $_POST['title'] ) ? sanitize_text_field( wp_unslash( $_POST['title'] ) ) : 'Form', 'settings' => $t['settings'], 'fields' => $t['fields'] ) ); wp_safe_redirect( Admin::url( 'forms', array( 'edit' => $id ) ) ); exit; } public function delete_action() { check_admin_referer( 'frontglut_form_delete' ); $this->guard(); self::delete( isset( $_GET['id'] ) ? (int) $_GET['id'] : 0 ); wp_safe_redirect( Admin::url( 'forms', Admin::flash( __( 'Form deleted.', 'frontglut' ) ) ) ); exit; } public function dup_action() { check_admin_referer( 'frontglut_form_dup' ); $this->guard(); $f = self::get( isset( $_GET['id'] ) ? (int) $_GET['id'] : 0 ); if ( $f ) { unset( $f['id'] ); $f['title'] .= ' (copy)'; self::save( $f ); } wp_safe_redirect( Admin::url( 'forms', Admin::flash( __( 'Form duplicated.', 'frontglut' ) ) ) ); exit; } public function export() { check_admin_referer( 'frontglut_form_export' ); $this->guard(); $f = self::get( isset( $_GET['id'] ) ? (int) $_GET['id'] : 0 ); if ( ! $f ) { wp_die( 'Not found' ); } nocache_headers(); header( 'Content-Type: application/json; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="frontglut-form-' . (int) $f['id'] . '.json"' ); echo wp_json_encode( array( 'frontglut_form' => 1, 'type' => $f['type'], 'title' => $f['title'], 'settings' => $f['settings'], 'fields' => $f['fields'] ), JSON_PRETTY_PRINT ); exit; } public function import_action() { check_admin_referer( 'frontglut_form_import' ); $this->guard(); $d = \FrontGlut\Support\Sanitize::post_json( 'json', 'html' ); // Decoded and sanitised; clean_form() then validates every field definition. $msg = __( 'Invalid JSON.', 'frontglut' ); if ( is_array( $d ) && ! empty( $d['frontglut_form'] ) ) { $type = in_array( $d['type'] ?? '', array( 'post', 'custom' ), true ) ? $d['type'] : 'custom'; $clean = self::clean_form( $d, $type ); $clean['type'] = $type; self::save( $clean ); $msg = __( 'Form imported.', 'frontglut' ); } wp_safe_redirect( Admin::url( 'forms', Admin::flash( $msg ) ) ); exit; } private function guard() { if ( ! current_user_can( 'frontglut_manage_settings' ) && ! current_user_can( 'manage_options' ) ) { wp_die( 'Forbidden', 403 ); } } }