'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 '
';
echo '| ID | ' . esc_html__( 'Title', 'frontglut' ) . ' | ' . esc_html__( 'Type', 'frontglut' ) . ' | ' . esc_html__( 'Shortcode', 'frontglut' ) . ' | |
';
foreach ( self::all() as $f ) {
$sc = in_array( $f['type'], array( 'post', 'custom' ), true ) ? '[frontglut_form id="' . $f['id'] . '"]' : '—';
echo '| ' . (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' ) . ' |
';
}
echo '
' . esc_html__( 'Import form (JSON)', 'frontglut' ) . '
';
}
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 );
}
}
}