__( 'All Forms', '2ranq-forms' ), self::PAGE_NEW => __( 'Add New', '2ranq-forms' ), self::PAGE_ENTRIES => __( 'Entries', '2ranq-forms' ), self::PAGE_SETTINGS => __( 'Settings', '2ranq-forms' ), ); foreach ( $pages as $slug => $title ) { add_submenu_page( self::PAGE, $title, $title, 'manage_options', $slug, array( __CLASS__, 'render' ) ); } } /** * Which of our screens is open, or an empty string. * * @return string */ private static function current_page() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- only recognising the screen, nothing is changed. $page = isset( $_GET['page'] ) ? sanitize_key( wp_unslash( $_GET['page'] ) ) : ''; $ours = array( self::PAGE, self::PAGE_NEW, self::PAGE_ENTRIES, self::PAGE_SETTINGS ); return in_array( $page, $ours, true ) ? $page : ''; } /** * The form id out of the address, or zero. * * @return int */ private static function requested_form_id() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- reading only. return isset( $_GET['form_id'] ) ? absint( wp_unslash( $_GET['form_id'] ) ) : 0; } /** * Whether we are in the builder. * * @return bool */ private static function is_builder() { return self::PAGE === self::current_page() && self::requested_form_id() > 0; } /** * The builder takes the whole screen, as theirs does, because that is how you * work without distraction. * * @param string $classes The existing classes. * * @return string */ public static function body_class( $classes ) { if ( self::is_builder() ) { return $classes . ' ranq-fullscreen'; } // The form list, the templates, the entries and the settings run edge to edge, // the same as the head of the builder. Without this the head floats in // WordPress's own padding and stops being the edge of the screen. $edge = array( self::PAGE, self::PAGE_NEW, self::PAGE_ENTRIES, self::PAGE_SETTINGS ); if ( in_array( self::current_page(), $edge, true ) ) { return $classes . ' ranq-edge'; } return $classes; } /** * The dashboard assets. Only on our screens. * * @return void */ public static function assets() { if ( ! self::current_page() ) { return; } // The small script of the settings screen: it shows only what the chosen // service asks for. It needs neither the builder nor the media library. wp_enqueue_script( 'ranq-forms-admin', RANQ_FORMS_URL . 'assets/js/admin.js', array(), RANQ_FORMS_VERSION, true ); wp_enqueue_style( 'ranq-forms-admin', RANQ_FORMS_URL . 'assets/css/admin.css', array(), RANQ_FORMS_VERSION ); if ( ! self::is_builder() ) { return; } // The same style sheet that goes to the site, so the preview is real and not near. wp_enqueue_style( 'ranq-forms', RANQ_FORMS_URL . 'assets/css/form.css', array(), RANQ_FORMS_VERSION ); // The builder ALWAYS needs the section style too, because the Full Widgets tab // shows a section. On the site it is sent only when a section really exists. wp_enqueue_style( 'ranq-forms-sections', RANQ_FORMS_URL . 'assets/css/sections.css', array( 'ranq-forms' ), RANQ_FORMS_VERSION ); // The Contact widget tab shows a real section, assembled out of ordinary // blocks. Without the block style the columns would fall one under the other // and the preview would lie about how the section really looks. wp_enqueue_style( 'wp-block-library' ); // The media library picker. Loaded only in the builder. wp_enqueue_media(); wp_enqueue_script( 'ranq-forms-builder', RANQ_FORMS_URL . 'assets/js/builder.js', array(), RANQ_FORMS_VERSION, true ); wp_localize_script( 'ranq-forms-builder', 'ranqFormsBuilder', Ranq_Forms_Builder::script_data( self::requested_form_id() ) ); } /** * The actions that change state: building a form from a template, deleting, * saving the settings. * * @return void */ public static function handle_actions() { if ( ! self::current_page() || ! current_user_can( 'manage_options' ) ) { return; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- the nonce is checked per action. $action = isset( $_REQUEST['ranq_action'] ) ? sanitize_key( wp_unslash( $_REQUEST['ranq_action'] ) ) : ''; if ( 'create' === $action ) { self::create_from_template(); } if ( 'delete' === $action ) { self::delete_form(); } if ( 'save_settings' === $action ) { self::save_settings(); } } /** * Builds a form from a template and goes straight into the builder. * * @return void */ private static function create_from_template() { $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, 'ranq_forms_create' ) ) { wp_die( esc_html__( 'That link has expired. Please try again.', '2ranq-forms' ) ); } $key = isset( $_GET['template'] ) ? sanitize_key( wp_unslash( $_GET['template'] ) ) : 'blank'; $template = Ranq_Forms_Templates::get( $key ); if ( ! $template ) { $template = Ranq_Forms_Templates::get( 'blank' ); } $form_id = wp_insert_post( array( 'post_type' => Ranq_Forms_Post_Type::POST_TYPE, 'post_title' => $template['name'], 'post_status' => 'publish', ) ); if ( is_wp_error( $form_id ) || ! $form_id ) { wp_die( esc_html__( 'The form could not be created.', '2ranq-forms' ) ); } update_post_meta( $form_id, '_ranq_forms_fields', $template['fields'] ); wp_safe_redirect( admin_url( 'admin.php?page=' . self::PAGE . '&form_id=' . (int) $form_id ) ); exit; } /** * Deletes a form. * * @return void */ private static function delete_form() { $form_id = self::requested_form_id(); $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : ''; if ( ! $form_id || ! wp_verify_nonce( $nonce, 'ranq_forms_delete_' . $form_id ) ) { wp_die( esc_html__( 'That link has expired. Please try again.', '2ranq-forms' ) ); } $form = get_post( $form_id ); if ( $form instanceof WP_Post && Ranq_Forms_Post_Type::POST_TYPE === $form->post_type ) { wp_delete_post( $form_id, true ); } wp_safe_redirect( admin_url( 'admin.php?page=' . self::PAGE . '&deleted=1' ) ); exit; } /** * Saves the plugin settings. * * @return void */ private static function save_settings() { check_admin_referer( 'ranq_forms_settings' ); $fields = array( 'ranq_forms_notify_email' => 'email', 'ranq_forms_store_ip' => 'bool', 'ranq_forms_remove_data_on_uninstall' => 'bool', 'ranq_forms_disable_styles' => 'bool', ); foreach ( $fields as $key => $type ) { if ( 'bool' === $type ) { update_option( $key, isset( $_POST[ $key ] ), false ); continue; } $value = isset( $_POST[ $key ] ) ? sanitize_email( wp_unslash( $_POST[ $key ] ) ) : ''; update_option( $key, $value, false ); } // The company details. The ready made sections are filled in from them, so // they are not typed again in every one. $company = array(); foreach ( array( 'name', 'street', 'city', 'phone', 'email', 'hours' ) as $key ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- cleaned on the line below, by `sanitize_email` or `sanitize_text_field` depending on the field. $raw = isset( $_POST[ 'ranq_company_' . $key ] ) ? wp_unslash( $_POST[ 'ranq_company_' . $key ] ) : ''; $company[ $key ] = 'email' === $key ? sanitize_email( $raw ) : sanitize_text_field( $raw ); } update_option( 'ranq_forms_company', $company, false ); // The keys of the outside spam checking service. // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- every field is cleaned by `Ranq_Forms_Spam::save()`, which alone knows what is valid for it. Ranq_Forms_Spam::save( array( 'provider' => isset( $_POST['ranq_spam_provider'] ) ? wp_unslash( $_POST['ranq_spam_provider'] ) : 'none', 'site_key' => isset( $_POST['ranq_spam_site_key'] ) ? wp_unslash( $_POST['ranq_spam_site_key'] ) : '', 'secret' => isset( $_POST['ranq_spam_secret'] ) ? wp_unslash( $_POST['ranq_spam_secret'] ) : '', 'rc_type' => isset( $_POST['ranq_spam_rc_type'] ) ? wp_unslash( $_POST['ranq_spam_rc_type'] ) : 'v2', 'threshold' => isset( $_POST['ranq_spam_threshold'] ) ? wp_unslash( $_POST['ranq_spam_threshold'] ) : '0.5', 'message' => isset( $_POST['ranq_spam_message'] ) ? wp_unslash( $_POST['ranq_spam_message'] ) : '', ) ); // The keys of the outside services. Ranq_Forms_Integrations::save_keys( wp_unslash( $_POST ) ); // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized wp_safe_redirect( admin_url( 'admin.php?page=' . self::PAGE_SETTINGS . '&saved=1' ) ); exit; } /** * Draws the requested screen. * * @return void */ public static function render() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You do not have permission to view this page.', '2ranq-forms' ) ); } $page = self::current_page(); if ( self::PAGE_NEW === $page ) { self::render_templates(); return; } if ( self::PAGE_ENTRIES === $page ) { Ranq_Forms_Entries_Screen::render(); return; } if ( self::PAGE_SETTINGS === $page ) { self::render_settings(); return; } $form_id = self::requested_form_id(); if ( $form_id ) { Ranq_Forms_Builder::render( $form_id ); return; } self::render_list(); } /** * The address for building a form from a template. * * @param string $template The template key. * * @return string */ public static function create_url( $template ) { return wp_nonce_url( admin_url( 'admin.php?page=' . self::PAGE_NEW . '&ranq_action=create&template=' . rawurlencode( $template ) ), 'ranq_forms_create' ); } /** * The template most sites really build, and the blank form. Both stand * OUTSIDE the wall of tiles, as the two choices offered first. The same keys * as in the Templates tab in the builder, so two screens doing the same job * start the same way. */ const TPL_FIRST = 'contact'; const TPL_BLANK = 'blank'; /** * The group subheading says the JOB those forms do, because a person first * knows what they need, and only then what it is called. * * @return array */ private static function template_notes() { return array( 'contact' => __( 'Someone wants to ask you something', '2ranq-forms' ), 'booking' => __( 'Someone wants a time slot', '2ranq-forms' ), 'quote' => __( 'Someone wants a price', '2ranq-forms' ), 'application' => __( 'Someone is applying', '2ranq-forms' ), 'verification' => __( 'Someone proves who they are, with files', '2ranq-forms' ), 'support' => __( 'Something is not working', '2ranq-forms' ), 'signup' => __( 'Someone leaves an email address', '2ranq-forms' ), 'feedback' => __( 'Someone tells you what they think', '2ranq-forms' ), ); } /** * Whether the template asks for a file upload. * * @param array $template The template. * * @return bool */ private static function template_has_file( $template ) { foreach ( $template['fields'] as $field ) { if ( isset( $field['type'] ) && 'file' === $field['type'] ) { return true; } } return false; } /** * The address of this screen with a given state. * * The state of the screen lives in the ADDRESS, not in a script: which group * is open, what is being searched for and which filter holds. That is why the * screen works without a single line of JavaScript, and every state has an * address that can be bookmarked and shared. * * @param array $args What changes: `open`, `s`, `show`. * @param string $hash The anchor at the end of the address, without the hash. * * @return string */ private static function templates_url( $args = array(), $hash = '' ) { $query = array( 'page' => self::PAGE_NEW ); foreach ( array( 'open', 's', 'show' ) as $name ) { if ( ! empty( $args[ $name ] ) ) { $query[ $name ] = $args[ $name ]; } } $url = admin_url( 'admin.php?' . http_build_query( $query ) ); return $hash ? $url . '#' . $hash : $url; } /** * The ladder: as many rungs as there are steps, the first one full because * that is where the visitor stands. The same mark sits next to the form name, * on the tile and at the top of the sketch, so it is learned once. * * @param int $steps The number of steps. * * @return void */ private static function render_steps( $steps ) { echo ''; } /** * The sketch is a small but truthful picture of the form: how long it is, * whether it has a long message box, whether two fields share a row and where * it breaks into steps. An icon would only repeat the name. * * @param array $template The template. * @param bool $big A large sketch, next to the card at the top. * * @return void */ private static function render_sketch( $template, $big = false ) { $class = 'ranq-nf__sketch' . ( $big ? ' is-big' : '' ); if ( empty( $template['fields'] ) ) { printf( '', esc_attr( $class ), esc_html__( 'Nothing yet', '2ranq-forms' ) ); return; } // The rows are worked out first and only then printed. A field half a row wide // joins the previous row, exactly as in a real form. $rows = array(); $pair = null; foreach ( $template['fields'] as $field ) { if ( count( $rows ) >= 4 ) { break; } $type = isset( $field['type'] ) ? $field['type'] : 'text'; if ( in_array( $type, array( 'pagebreak', 'divider', 'hidden' ), true ) ) { continue; } $half = isset( $field['size'] ) && in_array( $field['size'], array( 'half', 'third' ), true ); if ( $half && null !== $pair && count( $rows[ $pair ]['fields'] ) < 2 ) { $rows[ $pair ]['fields'][] = $field; continue; } $rows[] = array( 'pair' => $half, 'fields' => array( $field ), ); $pair = $half ? count( $rows ) - 1 : null; } $steps = Ranq_Forms_Templates::steps( $template ); ?> 1 ) : ?> ' . substr( $safe, $at, strlen( $mark ) ) . '' . substr( $safe, $at + strlen( $mark ) ); } /** * The template list, the "Add New" screen. * * The same job as the Templates tab in the builder, so the same look. A wall * of twenty nine tiles is not solved with smaller tiles but by not drawing a * tile until it is asked for: the screen is a list of JOBS, every job is one * row, and a closed row holds the names of all its forms. * * It all works without JavaScript, because the state sits in the address: * `open` is the open group, `s` is the searched word, `show` is the filter. * * @return void */ private static function render_templates() { $all = Ranq_Forms_Templates::all(); $groups = Ranq_Forms_Templates::groups(); $notes = self::template_notes(); // The numbers are read out of the registry, never typed in by hand. $total = count( $all ); $with_steps = 0; $with_file = 0; $by_group = array(); foreach ( $all as $key => $template ) { if ( Ranq_Forms_Templates::steps( $template ) > 1 ) { ++$with_steps; } if ( self::template_has_file( $template ) ) { ++$with_file; } $group = isset( $template['group'] ) ? $template['group'] : 'start'; if ( ! isset( $by_group[ $group ] ) ) { $by_group[ $group ] = array(); } $by_group[ $group ][ $key ] = $template; } // The same reckoning as in the Templates tab in the builder: every group with // at least one template counts, and "start" with them, because a blank form // and a recommendation are a job of their own even without a row. $jobs = 0; foreach ( $groups as $group => $label ) { if ( ! empty( $by_group[ $group ] ) ) { ++$jobs; } } $search_label = sprintf( /* translators: %d: number of ready forms. */ __( 'Search %d forms by name or by what they do', '2ranq-forms' ), (int) $total ); // With nothing at all in the address, Contact is open, because that is the job // most people come for. Closing a row sends `open=none`, which is not the name // of any group: without that, "I closed everything" would be no different from // "I have not touched anything yet" and the row would spring back to Contact. // phpcs:disable WordPress.Security.NonceVerification.Recommended -- display state only, nothing is changed. $search = isset( $_GET['s'] ) ? trim( sanitize_text_field( wp_unslash( $_GET['s'] ) ) ) : ''; $show = isset( $_GET['show'] ) ? sanitize_key( wp_unslash( $_GET['show'] ) ) : 'all'; $open = isset( $_GET['open'] ) ? sanitize_key( wp_unslash( $_GET['open'] ) ) : 'contact'; // phpcs:enable WordPress.Security.NonceVerification.Recommended if ( ! in_array( $show, array( 'all', 'steps', 'file' ), true ) ) { $show = 'all'; } if ( ! isset( $by_group[ $open ] ) || 'start' === $open ) { $open = ''; } $looking = '' !== $search || 'all' !== $show; ?>
$template ) { if ( 'steps' === $show && Ranq_Forms_Templates::steps( $template ) < 2 ) { continue; } if ( 'file' === $show && ! self::template_has_file( $template ) ) { continue; } if ( '' !== $search && false === stripos( $template['name'] . ' ' . $template['description'], $search ) ) { continue; } $found[ $key ] = $template; } ?>
$label ) : if ( 'start' === $group || empty( $by_group[ $group ] ) ) { continue; } $list = $by_group[ $group ]; $is_open = $open === $group; $anchor = 'ranq-job-' . $group; $all_steps = true; foreach ( $list as $one ) { if ( Ranq_Forms_Templates::steps( $one ) < 2 ) { $all_steps = false; } } ?>
$template ) : ?> 1 ) : ?>
$template ) : ?>
array( __( 'Company name', '2ranq-forms' ), get_bloginfo( 'name' ), '' ), 'street' => array( __( 'Street address', '2ranq-forms' ), '', '1200 Market Street, Suite 400' ), 'city' => array( __( 'City, state and ZIP', '2ranq-forms' ), '', 'San Francisco, CA 94103' ), 'phone' => array( __( 'Phone', '2ranq-forms' ), '', '(415) 555-0134' ), 'email' => array( __( 'Email', '2ranq-forms' ), get_option( 'admin_email' ), '' ), 'hours' => array( __( 'Opening hours', '2ranq-forms' ), '', 'Monday to Friday, 9am to 5pm' ), ); $company_filled = 0; foreach ( $company_fields as $company_key => $company_meta ) { $one = isset( $company[ $company_key ] ) ? $company[ $company_key ] : $company_meta[1]; if ( '' !== trim( (string) $one ) ) { ++$company_filled; } } // Which link is set up. The same figure serves the table of contents and the // mark next to the heading, so it is worked out once. $conn_keys = Ranq_Forms_Integrations::keys(); $conn_on = array(); foreach ( Ranq_Forms_Integrations::services() as $conn_key => $conn ) { foreach ( $conn['keys'] as $conn_field => $conn_meta ) { if ( '' !== (string) $conn_keys[ $conn_key ][ $conn_field ] ) { $conn_on[] = $conn['name']; break; } } } $spam = Ranq_Forms_Spam::get(); $providers = Ranq_Forms_Spam::providers(); $rc_types = Ranq_Forms_Spam::recaptcha_types(); $spam_name = isset( $providers[ $spam['provider'] ] ) ? $providers[ $spam['provider'] ]['name'] : __( 'Off', '2ranq-forms' ); if ( 'recaptcha' === $spam['provider'] && isset( $rc_types[ $spam['rc_type'] ] ) ) { $spam_name = $rc_types[ $spam['rc_type'] ]; } $counted = wp_count_posts( Ranq_Forms_Post_Type::POST_TYPE ); $how_many = (int) ( isset( $counted->publish ) ? $counted->publish : 0 ) + (int) ( isset( $counted->draft ) ? $counted->draft : 0 ); $page_url = admin_url( 'admin.php?page=' . self::PAGE_SETTINGS ); ?>

$meta ) : ?>

$conn ) : $conn_howto = function ( $conn, $open ) { ?>

>

$conn_meta ) : $conn_id = 'ranq_conn_' . $conn_key . '_' . $conn_field; $conn_val = isset( $conn_keys[ $conn_key ][ $conn_field ] ) ? $conn_keys[ $conn_key ][ $conn_field ] : ''; $conn_pwd = 'password' === $conn_meta['type']; $conn_has = '' !== $conn_val; ?>
•••••••••••• />

$one ) : ?>
$name ) : ?>
$one ) : ?>
>

2ranq Forms
Ranq_Forms_Post_Type::POST_TYPE, 'post_status' => array( 'publish', 'draft' ), 'numberposts' => 100, 'orderby' => 'modified', 'order' => 'DESC', 'no_found_rows' => true, ) ); $new_url = admin_url( 'admin.php?page=' . self::PAGE_NEW ); // Everything is counted in ONE loop: the numbers in the filters, the largest // entry count that sets the bar width, and the state of every row. No new query. $rows = array(); $with_entries = 0; $no_fields = 0; $most = 0; foreach ( $forms as $form ) { $fields = count( Ranq_Forms_Fields::get_form_fields( $form->ID ) ); $entries = (int) Ranq_Forms_Entries::count_for_form( $form->ID ); if ( $entries > 0 ) { ++$with_entries; } if ( 0 === $fields ) { ++$no_fields; } if ( $entries > $most ) { $most = $entries; } $rows[] = array( 'form' => $form, 'fields' => $fields, 'entries' => $entries, ); } ?>

01

02

[2ranq_form id="1"]
03

ID . '"]'; $edit_url = admin_url( 'admin.php?page=' . self::PAGE . '&form_id=' . $form->ID ); $entries_url = admin_url( 'admin.php?page=' . self::PAGE_ENTRIES . '&form_id=' . $form->ID ); $delete_url = wp_nonce_url( admin_url( 'admin.php?page=' . self::PAGE . '&ranq_action=delete&form_id=' . $form->ID ), 'ranq_forms_delete_' . $form->ID ); $width = $most > 0 ? (int) round( $row['entries'] / $most * 100 ) : 0; ?>