post_type ? __( 'Form name, e.g. Contact our team', 'accessible-forms-by-accessible-org' ) : $text; }, 10, 2 ); } public static function assets() { wp_register_script( 'accessible-forms-admin-search', ACCESSIBLE_FORMS_URL . 'assets/admin-search.js', array(), \AccessibleForms\VERSION, true ); $templates = array(); foreach ( FormLibrary::templates() as $key => $label ) { $templates[ $key ] = FormLibrary::template( $key ); } wp_enqueue_script( 'accessible-forms-flow', ACCESSIBLE_FORMS_URL . 'assets/editor-flow.js', array( 'accessible-forms-admin', 'accessible-forms-appearance', 'accessible-forms-admin-search', 'wp-i18n' ), \AccessibleForms\VERSION, true ); wp_set_script_translations( 'accessible-forms-flow', 'accessible-forms-by-accessible-org' ); $post = get_post(); $errors = array(); if ( $post ) { // Read before admin_notices prints and clears them, so the editor can mark each control. $issues = Admin::pending_errors( $post->ID ); if ( ! $issues && current_user_can( 'edit_post', $post->ID ) ) { // Edits kept with problems, or a saved form that fails the current checks. $issues = \AccessibleForms\FormCheck::issues( $post->ID ); } foreach ( $issues as $issue ) { $issue['target'] = Admin::issue_target( $issue ); $errors[] = $issue; } } wp_localize_script( 'accessible-forms-flow', 'AccessibleFormsFlow', array( 'templates' => $templates, 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'postId' => $post ? (int) $post->ID : 0, 'siteId' => get_current_blog_id(), 'siteKey' => hash( 'sha256', site_url( '/' ) . ':' . get_current_blog_id() ), 'userId' => get_current_user_id(), 'serverModified' => $post ? (int) get_post_modified_time( 'U', true, $post ) : 0, 'serverNow' => time(), 'saveErrors' => $errors, ) ); } /** Transient holding locally saved edits the owner chose to restore, read once. */ const RESTORED = 'accessible_forms_restored_'; /** * Accept unsaved edits kept in this browser, so the next page load renders * them with the same server code that renders a rejected save. Nothing is * saved to the form itself. */ public static function restore_local() { check_ajax_referer( Admin::NONCE, Admin::NONCE . '_nonce' ); $id = absint( $_POST['post_ID'] ?? 0 ); if ( ! $id || 'accessible_form' !== get_post_type( $id ) || ! current_user_can( 'edit_post', $id ) ) { wp_send_json_error( array( 'message' => __( 'You cannot change this form.', 'accessible-forms-by-accessible-org' ) ), 403 ); } $raw = self::submitted(); if ( ! $raw ) { wp_send_json_error( array( 'message' => __( 'The saved changes could not be read.', 'accessible-forms-by-accessible-org' ) ), 400 ); } $key = self::RESTORED . $id . '_' . get_current_user_id(); set_transient( $key, $raw, 300 ); if ( get_transient( $key ) !== $raw ) { wp_send_json_error( array( 'message' => __( 'The changes could not be restored. Your browser recovery copy is kept; try again.', 'accessible-forms-by-accessible-org' ) ), 503 ); } wp_send_json_success(); } /** * Take locally saved edits chosen for restoring, if any. They apply once. * * @param int $id Form ID. * @return array|null Raw schema input, sanitized as text. */ public static function take_restored( $id ) { $key = self::RESTORED . (int) $id . '_' . get_current_user_id(); $restored = get_transient( $key ); if ( ! is_array( $restored ) ) { return null; } delete_transient( $key ); return $restored; } /** Native editor controls keep the pending inputs and redisplay the editor. */ public static function builder_action() { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Select the handler before checking its nonce. $action = is_string( $_POST['af_builder_action'] ?? null ) ? sanitize_key( wp_unslash( $_POST['af_builder_action'] ) ) : ''; if ( ! in_array( $action, array( 'subject', 'body', 'content' ), true ) ) { return; } check_admin_referer( Admin::NONCE, Admin::NONCE . '_nonce' ); $id = absint( $_POST['post_ID'] ?? 0 ); if ( 'accessible_form' !== get_post_type( $id ) || ! current_user_can( 'edit_post', $id ) ) { wp_die( esc_html__( 'You cannot change this form.', 'accessible-forms-by-accessible-org' ), '', array( 'response' => 403 ) ); } $raw = Admin::submitted_schema( $id ); if ( empty( $_POST['af_schema_complete'] ) || empty( $raw['fields'] ) || ! is_array( $raw['fields'] ) ) { wp_die( esc_html__( 'The full form did not reach the server. Go back and try again.', 'accessible-forms-by-accessible-org' ), '', array( 'response' => 400, 'back_link' => true ) ); } $step = 'delivery'; if ( 'content' === $action ) { $step = 'build'; if ( count( $raw['fields'] ) >= Schema::MAX_FIELDS ) { wp_die( esc_html__( 'This form already has the maximum number of fields. Go back to continue editing.', 'accessible-forms-by-accessible-org' ), '', array( 'response' => 400, 'back_link' => true ) ); } $raw['fields'][] = array( 'type' => 'content', 'content' => '' ); $focus = 'af-opt-af_schema-fields-' . array_key_last( $raw['fields'] ) . '-content'; } else { $key = 'subject' === $action ? 'subject_template' : 'body_template'; $tag = isset( $_POST[ 'af_tag_' . $action ] ) && is_string( $_POST[ 'af_tag_' . $action ] ) ? sanitize_text_field( wp_unslash( $_POST[ 'af_tag_' . $action ] ) ) : ''; $token = '{' . $tag . '}'; $field_names = array_column( array_filter( $raw['fields'], static function ( $field ) { return is_array( $field ) && Schema::is_answer_field( $field ); } ), 'name' ); if ( isset( MergeTags::labels()[ $token ] ) || in_array( preg_replace( '/^field:/', '', $tag ), $field_names, true ) ) { $value = (string) ( $raw[ $key ] ?? '' ); $raw[ $key ] = '' === $value ? $token : $value . ' ' . $token; if ( 'subject' === $action && ( MergeTags::legacy_conflict( $token ) || str_starts_with( $tag, 'field:' ) ) ) { $raw['merge_subject'] = true; } } $focus = 'subject' === $action ? 'af-subject_template' : 'af-opt-af_schema-body_template'; } $raw['_af_builder_title'] = sanitize_text_field( wp_unslash( $_POST['post_title'] ?? '' ) ); $raw['_af_builder_token'] = sanitize_text_field( wp_unslash( $_POST['af_edit_version'] ?? '' ) ); $key = self::RESTORED . $id . '_' . get_current_user_id(); set_transient( $key, $raw, 300 ); if ( get_transient( $key ) !== $raw ) { wp_die( esc_html__( 'The changes could not be kept. Go back and try again.', 'accessible-forms-by-accessible-org' ), '', array( 'response' => 503, 'back_link' => true ) ); } wp_safe_redirect( add_query_arg( 'af_editor_step', $step, get_edit_post_link( $id, 'raw' ) ) . '#' . $focus, 303 ); exit(); } public static function restore_builder_title( $post ) { if ( 'accessible_form' !== $post->post_type || ! current_user_can( 'edit_post', $post->ID ) ) { return; } $raw = get_transient( self::RESTORED . $post->ID . '_' . get_current_user_id() ); if ( is_string( $raw['_af_builder_title'] ?? null ) ) { $post->post_title = $raw['_af_builder_title']; } } /** Stable panel identities shared by the server response and its enhancement. */ public static function steps() { return array( 'build' => array( 'label' => __( 'Build', 'accessible-forms-by-accessible-org' ), 'title' => __( 'Build your form', 'accessible-forms-by-accessible-org' ) ), 'style' => array( 'label' => __( 'Style', 'accessible-forms-by-accessible-org' ), 'title' => __( 'Choose its appearance', 'accessible-forms-by-accessible-org' ) ), 'delivery' => array( 'label' => __( 'Email & confirmation', 'accessible-forms-by-accessible-org' ), 'title' => __( 'Set up email & confirmation', 'accessible-forms-by-accessible-org' ) ), 'review' => array( 'label' => __( 'Review & publish', 'accessible-forms-by-accessible-org' ), 'title' => __( 'Review & publish', 'accessible-forms-by-accessible-org' ) ), ); } /** A view preference only; it never controls what is saved or published. */ public static function current_step() { $step = isset( $_GET['af_editor_step'] ) && is_string( $_GET['af_editor_step'] ) ? sanitize_key( wp_unslash( $_GET['af_editor_step'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only panel selection. return isset( self::steps()[ $step ] ) ? $step : 'build'; } /** Panels stay readable without scripts; CSS selects the initial enhanced view. */ public static function open_panel( $key ) { $step = self::steps()[ $key ]; echo '

' . esc_html( $step['title'] ) . '

'; } public static function top( $post, array $schema ) { $state = Revisions::has_draft( $post->ID ) ? 'draft' : ( 'publish' === $post->post_status ? 'published' : 'unpublished' ); echo ''; echo '

' . esc_html( 'publish' === $post->post_status ? __( 'Edit a draft, check the preview, then publish your changes.', 'accessible-forms-by-accessible-org' ) : __( 'Build, choose delivery, then review and publish.', 'accessible-forms-by-accessible-org' ) ) . '

' . esc_html( Revisions::has_draft( $post->ID ) ? __( 'Draft changes', 'accessible-forms-by-accessible-org' ) : ( 'publish' === $post->post_status ? __( 'Published', 'accessible-forms-by-accessible-org' ) : __( 'Unpublished', 'accessible-forms-by-accessible-org' ) ) ) . ''; $saved = __( 'All changes saved', 'accessible-forms-by-accessible-org' ); if ( 'auto-draft' === $post->post_status ) { $saved = __( 'Form not yet saved', 'accessible-forms-by-accessible-org' ); } elseif ( $post->post_modified_gmt && '0000-00-00 00:00:00' !== $post->post_modified_gmt ) { $saved .= ' · ' . wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $post->post_modified_gmt . ' UTC' ) ); } echo '

' . esc_html( $saved ) . '

'; echo ''; } public static function starter( $post ) { if ( 'auto-draft' === $post->post_status ) { echo '
' . esc_html__( 'Starting with a contact form · Change template', 'accessible-forms-by-accessible-org' ) . '

' . esc_html__( 'Templates replace the fields below. Your form name and email recipients stay as you set them.', 'accessible-forms-by-accessible-org' ) . '

'; foreach ( FormLibrary::templates() as $key => $label ) { echo ''; } echo '
'; } } public static function review( $post, $schema = null, $with_footer = true ) { $schema = is_array( $schema ) ? $schema : ( Schema::get( $post->ID, true ) ?: Schema::defaults() ); echo '

' . esc_html__( 'Check your form', 'accessible-forms-by-accessible-org' ) . '

' . esc_html__( 'Review your fields and delivery. Publishing updates every page using this form.', 'accessible-forms-by-accessible-org' ) . '

' . esc_html__( 'Accessibility checks before publishing', 'accessible-forms-by-accessible-org' ) . '

' . esc_html__( 'Publishing repeats the automatic checks. Fix blocking problems and complete the author review below. These checks do not certify accessibility.', 'accessible-forms-by-accessible-org' ) . '
'; // Keep the required author confirmation visible beside the automatic checks. echo '

' . esc_html__( 'Required author review', 'accessible-forms-by-accessible-org' ) . '

'; echo '

' . esc_html__( 'Required before each publication: check that questions, choices, instructions and messages are clear. Test keyboard, zoom and screen reader use on the placed page too. This review is not WCAG certification.', 'accessible-forms-by-accessible-org' ) . '

'; echo '
' . esc_html__( 'How to review this form', 'accessible-forms-by-accessible-org' ) . '

' . esc_html__( 'The automatic checks catch missing labels, low contrast and broken structure. They cannot tell whether your questions make sense. These take a few minutes:', 'accessible-forms-by-accessible-org' ) . '

    '; foreach ( array( __( 'Read each label on its own. Does it say exactly what to enter, without the rest of the page?', 'accessible-forms-by-accessible-org' ), __( 'Open the keyboard test preview. Use Tab and Shift+Tab, arrow keys for radio choices, and Space for checkboxes. Reach every question and the send button, including conditional questions and each step.', 'accessible-forms-by-accessible-org' ), __( 'Switch the preview to “Validation errors”. Does each message say how to fix the answer?', 'accessible-forms-by-accessible-org' ), __( 'Try the test page at a narrow width and 200% zoom. Labels, instructions and errors must stay readable and controls must remain reachable.', 'accessible-forms-by-accessible-org' ), __( 'Check the test page with a screen reader, such as VoiceOver or NVDA. Questions, required states, choices, steps and errors should make sense when heard.', 'accessible-forms-by-accessible-org' ), __( 'After placing the form, open its page while logged in. The live check tests it with your theme and lists anything to fix.', 'accessible-forms-by-accessible-org' ), ) as $prompt ) { echo '
  • ' . esc_html( $prompt ) . '
  • '; } echo '
'; echo '

' . esc_html__( 'Preview and test', 'accessible-forms-by-accessible-org' ) . '

'; echo '
'; echo '

' . esc_html__( 'The appearance preview below cannot send messages. The keyboard test opens in a new tab to try questions, errors and confirmation without saving entries, files or emails.', 'accessible-forms-by-accessible-org' ) . '

' . esc_html__( 'Choose Refresh preview to see your current changes.', 'accessible-forms-by-accessible-org' ) . '

'; echo ''; $pages = TestPreview::page_options(); echo '
' . esc_html__( 'Test current edits with your site theme', 'accessible-forms-by-accessible-org' ) . '

' . esc_html__( 'Choose a page you can edit. A private snapshot replaces this form at its existing location; if it is not placed yet, it appears at the end of the page content. Neither the page nor the live form is changed. The preview opens in a new tab and expires after 30 minutes.', 'accessible-forms-by-accessible-org' ) . '

'; AccessibilityReview::panel( $post->ID, $schema ); echo '
'; $live_rows = array_filter( \AccessibleForms\LiveCheck::rows(), static function ( $row ) use ( $post ) { return (int) $post->ID === $row['form_id']; } ); if ( $live_rows ) { echo '

' . esc_html__( 'Checks on the pages using this form', 'accessible-forms-by-accessible-org' ) . '

    '; foreach ( $live_rows as $row ) { $state = $row['stale'] ? __( 'Out of date: check this page again.', 'accessible-forms-by-accessible-org' ) : ( $row['issues'] ? __( 'Accessibility problems detected. Fix the page or theme and check again before sharing it.', 'accessible-forms-by-accessible-org' ) : ( $row['unverified'] ? __( 'Some checks could not be completed. Manual review is required.', 'accessible-forms-by-accessible-org' ) : __( 'No problems detected by the live check. Manual review is still required.', 'accessible-forms-by-accessible-org' ) ) ); echo '
  • ' . esc_html( $row['path'] ) . ': ' . esc_html( $state ) . '
  • '; } echo '
'; } // What to do once the form is published, grouped apart from the preview. echo '

' . esc_html__( 'After publishing', 'accessible-forms-by-accessible-org' ) . '

' . esc_html__( 'Publish the form, then choose a page, post or block. Publishing the form alone does not add it to your site.', 'accessible-forms-by-accessible-org' ) . '

'; if ( 'publish' === $post->post_status ) { echo '

' . esc_html__( 'Choose where visitors see this form', 'accessible-forms-by-accessible-org' ) . '

'; $summary = Placement::summary( $post->ID ); if ( $summary ) { echo '

' . esc_html( $summary ) . '

'; } echo '

' . esc_html__( 'Choose where it appears', 'accessible-forms-by-accessible-org' ) . ' ' . esc_html__( 'See where this form is used', 'accessible-forms-by-accessible-org' ) . '

'; } if ( Capabilities::form( $post->ID ) && 'auto-draft' !== $post->post_status ) { $links = (array) \AccessibleForms\Extensions::filter( 'accessible_forms_review_links', array( EntriesTable::url( array( 'form_id' => $post->ID ) ) => __( 'View this form’s submissions', 'accessible-forms-by-accessible-org' ) ), $post->ID ); echo ''; } Experience::setup( $post, Schema::get( $post->ID ) ?: array() ); echo '
'; if ( $with_footer ) { self::footer( $post ); } } /** Real links retain step navigation while enhancement is unavailable. */ public static function footer( $post ) { $steps = self::steps(); $keys = array_keys( $steps ); $current = array_search( self::current_step(), $keys, true ); $previous = $keys[ max( 0, $current - 1 ) ]; $next = $keys[ min( count( $keys ) - 1, $current + 1 ) ]; echo ''; } /** A bounded page catalog, shared by the initial selector and its search. */ public static function page_options( $search = '', $page = 1 ) { $query = new \WP_Query( array( 'post_type' => 'page', 'post_status' => 'publish', 'has_password' => false, 'posts_per_page' => 20, 'paged' => max( 1, (int) $page ), 's' => wp_html_excerpt( sanitize_text_field( $search ), 200, '' ), 'orderby' => array( 'title' => 'ASC', 'ID' => 'ASC', ), ) ); return array( 'items' => array_map( array( self::class, 'page_option' ), $query->posts ), 'pages' => (int) $query->max_num_pages, ); } /** Include a unique identity and a safe preview link for similarly named pages. */ private static function page_option( $page ) { $title = $page->post_title ?: __( '(no title)', 'accessible-forms-by-accessible-org' ); return array( 'id' => (int) $page->ID, /* translators: 1: content title, 2: unique content ID. */ 'title' => sprintf( __( '%1$s · #%2$d', 'accessible-forms-by-accessible-org' ), $title, $page->ID ), 'url' => 'publish' === $page->post_status && '' === $page->post_password ? get_permalink( $page ) : '', ); } public static function page_picker( $selected ) { $catalog = self::page_options(); $options = array( 0 => __( 'Show the confirmation message here', 'accessible-forms-by-accessible-org' ) ); $urls = array(); foreach ( $catalog['items'] as $page ) { $options[ $page['id'] ] = $page['title']; $urls[ $page['id'] ] = $page['url']; } if ( $selected && ! isset( $options[ $selected ] ) ) { $page = get_post( $selected ); if ( $page && 'page' === $page->post_type && 'publish' === $page->post_status && '' === $page->post_password ) { $item = self::page_option( $page ); $options[ $selected ] = $item['title']; $urls[ $selected ] = $item['url']; } else { /* translators: %d: selected page ID. */ $options[ $selected ] = sprintf( __( 'Unavailable page · #%d: choose another page', 'accessible-forms-by-accessible-org' ), $selected ); } } echo '
' . esc_html__( 'Use a thank-you page (optional)', 'accessible-forms-by-accessible-org' ) . ''; Editor::select( 'af_schema[confirmation_page]', __( 'After submission', 'accessible-forms-by-accessible-org' ), $options, $selected ); echo '

' . esc_html__( 'Only public pages on this site are available. Search adds matching pages to After submission and keeps your current choice. Submitted answers stay out of the page address.', 'accessible-forms-by-accessible-org' ) . '

'; } public static function pages() { check_ajax_referer( Admin::NONCE, Admin::NONCE . '_nonce' ); if ( ! current_user_can( 'edit_accessible_forms' ) ) { wp_send_json_error( array(), 403 ); } wp_send_json_success( self::page_options( sanitize_text_field( wp_unslash( $_POST['search'] ?? '' ) ), max( 1, absint( $_POST['page'] ?? 1 ) ) ) ); } /** Decode one bounded JSON parameter so a large form does not hit max_input_vars. */ public static function submitted() { // phpcs:disable WordPress.Security.NonceVerification.Missing -- All callers verify the form nonce and edit capability before reading schema input. if ( isset( $_POST['af_schema_json'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw JSON text is size-bounded, decoded, then every decoded string is sanitized. $json = is_string( $_POST['af_schema_json'] ) ? wp_unslash( $_POST['af_schema_json'] ) : ''; $raw = strlen( $json ) <= 262144 ? json_decode( $json, true, 32 ) : null; return is_array( $raw ) ? Schema::clean_editor_input( $raw ) : array(); } $native = array(); if ( isset( $_POST['af_schema'] ) && is_array( $_POST['af_schema'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- The schema cleaner sanitizes every nested value and uses a restricted HTML allowlist for content blocks. $native = Schema::clean_editor_input( wp_unslash( $_POST['af_schema'] ) ); } // phpcs:enable WordPress.Security.NonceVerification.Missing return $native; } public static function preview() { check_ajax_referer( Admin::NONCE, Admin::NONCE . '_nonce' ); $id = absint( $_POST['post_ID'] ?? 0 ); if ( ! $id || 'accessible_form' !== get_post_type( $id ) || ! current_user_can( 'edit_post', $id ) ) { wp_send_json_error( array( 'message' => __( 'You cannot preview this form.', 'accessible-forms-by-accessible-org' ) ), 403 ); } [$schema, $issues] = Schema::validate( Admin::submitted_schema( $id ) ); $errors = Schema::messages( $issues ); $context = sanitize_key( wp_unslash( $_POST['af_preview_context'] ?? '' ) ); if ( 'style' === $context ) { wp_send_json_success( array( 'errors' => array(), 'html' => self::style_preview( $id, $schema, sanitize_key( wp_unslash( $_POST['af_style_state'] ?? 'normal' ) ) ), ) ); } if ( $errors ) { foreach ( $issues as $number => $issue ) { $issues[ $number ]['target'] = Admin::issue_target( $issue ); } wp_send_json_success( array( 'errors' => $errors, 'issues' => $issues, 'html' => '', ) ); } $state = array(); $mode = sanitize_key( wp_unslash( $_POST['af_preview_state'] ?? 'normal' ) ); if ( 'success' === $mode ) { $state['sent'] = true; } if ( 'errors' === $mode ) { foreach ( $schema['fields'] as $field ) { if ( Schema::is_answer_field( $field ) ) { $state['errors'][ $field['name'] ] = __( 'Enter an answer for this field.', 'accessible-forms-by-accessible-org' ); break; } } } $html = Renderer::render( $id, $state, true, $schema ); $html = str_replace( array( '
', ' autofocus' ), array( '
', '' ), $html ); $html = preg_replace( '/(<(?:input|textarea|select|button)\b)/', '$1 disabled', $html ); wp_send_json_success( array( 'errors' => array(), 'html' => $html, 'advice' => self::advice( $id, $schema ), ) ); } /** * The owner's own fields, rendered normally or with an explicit example error. * Keyboard focus is illustrated by the editor script. Nothing can be submitted. * * Fields that do not validate yet are left out; the Review step lists them. * * @param int $id Form ID. * @param array $schema Sanitized draft schema. * @param string $mode Normal, focus or error preview state. * @return string */ public static function style_preview( $id, array $schema, $mode = 'normal' ) { $state = array( 'errors' => array(), 'values' => array(), ); if ( 'error' === $mode ) { $types = Schema::field_types(); foreach ( $schema['fields'] as $field ) { if ( ! Schema::is_answer_field( $field ) || ! isset( $types[ $field['type'] ] ) ) { continue; } $state['errors'][ $field['name'] ] = __( 'Example: this is how an error message looks.', 'accessible-forms-by-accessible-org' ); break; } } $html = Renderer::render( $id, $state, true, $schema ); $html = str_replace( array( '', ' autofocus' ), array( '
', '' ), $html ); // A sample inside the editor adds no headings to the editor's outline. $html = preg_replace( '#<(h[1-6]) class="af-summary__heading">(.*?)#s', '

$2

', $html ); // Keep the real styles (not the disabled look) but make every control inert to typing. $html = preg_replace( '/(<(?:input|textarea|select)\b)/', '$1 readonly tabindex="-1"', $html ); // readonly does not stop clicks on choices or menus; the preview body blocks pointer input in CSS. $html = str_replace( 'type="submit"', 'type="button"', $html ); $html = preg_replace( '/(]*\btype=)/', '$1 type="button"', $html ); $html = preg_replace( '/( array() ); $ignored = get_post_meta( $id, self::IGNORED, true ); // The scan is shared by everyone; only name pages this person can edit. $visible = array_values( array_filter( $places['places'], static function ( $place ) { return current_user_can( 'edit_post', $place['id'] ); } ) ); $context = array( 'places' => $visible, 'more' => ! empty( $places['more'] ), 'ignored' => is_array( $ignored ) ? $ignored : array(), ); // Add-ons may add context of their own, such as correction counts. $context = (array) \AccessibleForms\Extensions::filter( 'accessible_forms_content_context', $context, (int) $id ); return \AccessibleForms\ContentCheck::run( $schema, $context ); } /** Ignore or restore one suggestion for a form. */ public static function ignore_advice() { check_ajax_referer( Admin::NONCE, Admin::NONCE . '_nonce' ); $id = absint( $_POST['post_ID'] ?? 0 ); if ( ! $id || 'accessible_form' !== get_post_type( $id ) || ! current_user_can( 'edit_post', $id ) ) { wp_send_json_error( array( 'message' => __( 'You cannot change this form.', 'accessible-forms-by-accessible-org' ) ), 403 ); } $key = isset( $_POST['key'] ) ? sanitize_text_field( wp_unslash( $_POST['key'] ) ) : ''; $restore = ! empty( $_POST['restore'] ); if ( ! preg_match( '/^[a-z_]{3,40}:[A-Za-z0-9_-]{0,64}$/', $key ) ) { wp_send_json_error( array( 'message' => __( 'Unknown suggestion.', 'accessible-forms-by-accessible-org' ) ), 400 ); } $ignored = get_post_meta( $id, self::IGNORED, true ); $ignored = is_array( $ignored ) ? $ignored : array(); $ignored = $restore ? array_values( array_diff( $ignored, array( $key ) ) ) : array_values( array_unique( array_merge( $ignored, array( $key ) ) ) ); $ignored = array_slice( $ignored, -200 ); update_post_meta( $id, self::IGNORED, $ignored ); wp_send_json_success( array( 'ignored' => $ignored ) ); } public static function preview_email() { check_ajax_referer( Admin::NONCE, Admin::NONCE . '_nonce' ); $id = absint( $_POST['post_ID'] ?? 0 ); if ( 'accessible_form' !== get_post_type( $id ) || ! current_user_can( 'edit_post', $id ) ) { wp_send_json_error( array(), 403 ); } [$schema, $errors] = Schema::sanitize( Admin::submitted_schema( $id ) ); if ( $errors ) { wp_send_json_error( array( 'message' => implode( ' ', $errors ) ), 400 ); } $values = array(); foreach ( $schema['fields'] as $field ) { if ( ! Schema::is_answer_field( $field ) ) { continue; } $value = $field['options'][0] ?? __( 'Sample answer', 'accessible-forms-by-accessible-org' ); if ( 'email' === $field['type'] ) { $value = 'visitor@example.com'; } $values[ $field['name'] ] = $value; } $entry = array( 'id' => 0, 'form_id' => $id, 'fields' => $values, 'snapshot' => array( 'schema' => $schema, 'title' => get_the_title( $id ), 'page' => array( 'title' => __( 'Sample page', 'accessible-forms-by-accessible-org' ), 'url' => home_url( '/sample-page/' ) ), ), ); $configs = \AccessibleForms\Notifications::configurations( $schema, $entry, false ); $html = ''; foreach ( $configs as $config ) { $mail = 'email' === \AccessibleForms\Notifications::channel( $config ) ? \AccessibleForms\Notifications::message( $entry, $config ) : null; if ( ! $mail ) { continue; } $html .= '

' . esc_html( $config['name'] ) . '

' . esc_html__( 'Recipients:', 'accessible-forms-by-accessible-org' ) . ' ' . esc_html( implode( ', ', $config['recipients'] ) ) . '

'; if ( ! empty( $config['cc'] ) && current_user_can( 'manage_options' ) ) { $html .= '

' . esc_html__( 'CC:', 'accessible-forms-by-accessible-org' ) . ' ' . esc_html( implode( ', ', $config['cc'] ) ) . '

'; } $html .= '

' . esc_html__( 'Subject:', 'accessible-forms-by-accessible-org' ) . ' ' . esc_html( $mail['subject'] ) . '

'; $html .= in_array( 'Content-Type: text/html; charset=UTF-8', $mail['headers'], true ) ? '
' . wp_kses( // Drop the document head (only a title), and show the email's own // heading as bold text so this screen's heading levels stay intact. preg_replace( array( '##is', '#]*>(.*?)#is' ), array( '', '

$1

' ), $mail['message'] ), array( 'p' => array(), 'br' => array(), 'strong' => array(), 'a' => array( 'href' => true ), 'table' => array(), 'caption' => array(), 'tbody' => array(), 'tr' => array(), 'th' => array( 'scope' => true ), 'td' => array(), ) ) . '
' : '
' . esc_html( $mail['message'] ) . '
'; $html .= '
'; } wp_send_json_success( array( 'html' => $html, 'message' => __( 'Sample only. No email was sent. Conditional notifications use the first choice in each field; review other routes in their settings. Test actual delivery from Settings → Email.', 'accessible-forms-by-accessible-org' ), ) ); } }