base && 'accessible_form' === $screen->post_type ) { $classes .= ' af-forms-list'; } elseif ( $screen && 'post' === $screen->base && 'accessible_form' === $screen->post_type ) { $classes .= ' af-guided-editor'; } return $classes; } /** Add decorative icons without changing WordPress view links or counts. */ public static function form_views( array $views ) { $icons = array( 'all' => 'grid', 'mine' => 'user', 'publish' => 'check', 'draft' => 'edit', 'pending' => 'clock', 'trash' => 'trash', ); foreach ( $views as $key => $view ) { $views[ $key ] = preg_replace( '/(]*>)/', '$1' . Navigation::icon( $icons[ $key ] ?? 'grid' ), $view, 1 ) ?? $view; } return $views; } /** * Enqueue admin assets on our screens only. * * @param string $hook Current admin page. */ public static function assets( $hook ) { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; $ours = $screen && 'accessible_form' === $screen->post_type; if ( ! $ours && false === strpos( (string) $hook, 'accessible-forms' ) ) { return; } // Native POST redirects and settings links must be interactive as soon as // the next document loads, including in browsers with scripting disabled. if ( ( $ours && 'post' === $screen->base ) || false !== strpos( (string) $hook, SettingsPage::SLUG ) ) { wp_dequeue_style( 'wp-view-transitions-admin' ); } wp_enqueue_style( 'accessible-forms-admin', ACCESSIBLE_FORMS_URL . 'assets/admin.css', array(), \AccessibleForms\VERSION ); wp_register_script( 'accessible-forms-admin-search', ACCESSIBLE_FORMS_URL . 'assets/admin-search.js', array(), \AccessibleForms\VERSION, true ); wp_enqueue_script( 'accessible-forms-form-picker', ACCESSIBLE_FORMS_URL . 'assets/form-picker.js', array( 'accessible-forms-admin-search', 'wp-i18n' ), \AccessibleForms\VERSION, true ); wp_set_script_translations( 'accessible-forms-form-picker', 'accessible-forms-by-accessible-org' ); if ( $ours && 'post' === $screen->base ) { wp_enqueue_script( 'accessible-forms-team', ACCESSIBLE_FORMS_URL . 'assets/team-access.js', array( 'wp-i18n', 'accessible-forms-admin-search' ), \AccessibleForms\VERSION, true ); wp_set_script_translations( 'accessible-forms-team', 'accessible-forms-by-accessible-org' ); wp_localize_script( 'accessible-forms-team', 'AccessibleFormsTeam', array( 'url' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'accessible_forms_team_members' ) ) ); wp_enqueue_style( 'accessible-forms' ); wp_enqueue_script( 'accessible-forms-appearance', ACCESSIBLE_FORMS_URL . 'assets/appearance.js', array( 'wp-i18n' ), \AccessibleForms\VERSION, true ); wp_set_script_translations( 'accessible-forms-appearance', 'accessible-forms-by-accessible-org' ); wp_localize_script( 'accessible-forms-appearance', 'AccessibleFormsAppearance', array( 'presets' => \AccessibleForms\Appearance::presets(), 'rules' => \AccessibleForms\Appearance::rules(), ) ); } if ( $ours || false !== strpos( (string) $hook, 'accessible-forms' ) ) { wp_enqueue_script( 'accessible-forms-admin', ACCESSIBLE_FORMS_URL . 'assets/admin.js', array( 'wp-i18n' ), \AccessibleForms\VERSION, true ); wp_set_script_translations( 'accessible-forms-admin', 'accessible-forms-by-accessible-org' ); wp_enqueue_script( 'accessible-forms-experience', ACCESSIBLE_FORMS_URL . 'assets/admin-experience.js', array( 'wp-i18n', 'accessible-forms-admin' ), \AccessibleForms\VERSION, true ); wp_set_script_translations( 'accessible-forms-experience', 'accessible-forms-by-accessible-org' ); wp_localize_script( 'accessible-forms-experience', 'AccessibleFormsExperience', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'storageKey' => 'af-list-' . hash( 'sha256', site_url() . ':' . get_current_blog_id() . ':' . get_current_user_id() ) ) ); if ( false !== strpos( (string) $hook, SettingsPage::SLUG ) || false !== strpos( (string) $hook, Management::SLUG ) || false !== strpos( (string) $hook, Placement::SLUG ) ) { wp_enqueue_script( 'accessible-forms-admin-tabs', ACCESSIBLE_FORMS_URL . 'assets/admin-tabs.js', array( 'wp-i18n', 'accessible-forms-admin', 'accessible-forms-form-picker' ), \AccessibleForms\VERSION, true ); wp_set_script_translations( 'accessible-forms-admin-tabs', 'accessible-forms-by-accessible-org' ); } if ( $ours && 'post' === $screen->base ) { Flow::assets(); } wp_localize_script( 'accessible-forms-admin', 'AccessibleFormsEditor', array( 'types' => Schema::field_types(), 'maxFields' => Schema::MAX_FIELDS, 'autofillHints' => Schema::autocomplete_hints(), ) ); } } /** * Register the schema metabox. */ public static function add_meta_boxes() { add_meta_box( 'accessible-forms-schema', __( 'Form settings', 'accessible-forms-by-accessible-org' ), array( self::class, 'render_meta_box' ), 'accessible_form', 'normal', 'high' ); } /** * Render WordPress's own publishing box inside Review from the first response. * Retain its callback, arguments and native actions; leave every other box alone. */ public static function publishing_meta_box() { global $wp_meta_boxes; foreach ( $wp_meta_boxes['accessible_form'] ?? array() as $context => $priorities ) { if ( 'af-publishing' === $context ) { continue; } foreach ( $priorities as $priority => $boxes ) { $box = $boxes['submitdiv'] ?? null; if ( ! is_array( $box ) ) { continue; } add_meta_box( 'submitdiv', $box['title'], $box['callback'], 'accessible_form', 'af-publishing', 'core' === $priority ? 'high' : $priority, $box['args'] ); return; } } } /** The outer native disclosure owns collapsing; its nested box must stay open. */ public static function publishing_meta_box_classes( $classes ) { return array_values( array_diff( $classes, array( 'closed' ) ) ); } /** Old saved sidebar ordering must not move the shared publishing box back. */ public static function meta_box_order( $order ) { if ( ! is_array( $order ) ) { return $order; } foreach ( $order as $context => $ids ) { if ( is_string( $ids ) ) { $order[ $context ] = implode( ',', array_diff( explode( ',', $ids ), array( 'submitdiv' ) ) ); } } return $order; } /** * Render the editor. * * @param \WP_Post $post Current form. */ /** Names of known add-ons, for notices about paused settings. */ public static function extension_label( $slug ) { $labels = array( 'pro' => __( 'Accessible Forms Pro', 'accessible-forms-by-accessible-org' ) ); return $labels[ $slug ] ?? $slug; } /** Explain, once per editor, that an inactive add-on's settings are kept but paused. */ private static function inactive_extensions_notice( array $schema ) { $slugs = Extensions::inactive( $schema['extensions'] ?? array() ); foreach ( $schema['fields'] as $field ) { $slugs = array_merge( $slugs, Extensions::inactive( $field['extensions'] ?? array() ) ); // A field of an inactive add-on's type; upload fields from before 2.0 belong to Pro. $addon = (string) ( $field['addon'] ?? ( 'file' === ( $field['type'] ?? '' ) ? 'pro' : '' ) ); if ( '' !== $addon && ! Extensions::registered( $addon ) ) { $slugs[] = sanitize_key( $addon ); } } $slugs = array_unique( $slugs ); if ( ! $slugs ) { return; } echo '

' . esc_html( sprintf( /* translators: %s: add-on name, such as Accessible Forms Pro. */ __( 'This form has settings from %s, which is not active. They are kept, but they will not work until it is active again.', 'accessible-forms-by-accessible-org' ), implode( ', ', array_map( array( self::class, 'extension_label' ), $slugs ) ) ) ) . '

'; } public static function render_meta_box( $post ) { $schema = Schema::get( $post->ID, true ) ?: Schema::defaults(); $legacy = empty( $schema['merge_subject'] ) && MergeTags::legacy_conflict( $schema['subject_template'] ); $pending = FormCheck::pending( $post->ID ); if ( $pending ) { // Edits saved with problems stay in the editor until they are fixed or discarded. $schema = self::overlay_kept( $schema, $pending['raw'] ); } $rejected = get_transient( 'accessible_forms_draft_' . $post->ID . '_' . get_current_user_id() ); if ( is_array( $rejected ) ) { $schema = self::overlay_kept( $schema, $rejected ); // Shown once, with the error summary that explains it. A later visit // shows the saved form; unsaved edits stay recoverable from this browser. delete_transient( 'accessible_forms_draft_' . $post->ID . '_' . get_current_user_id() ); } // Unsaved edits kept in this browser, which the owner chose to restore. $restored = Flow::take_restored( $post->ID ); if ( is_array( $restored ) ) { $schema = self::overlay_kept( $schema, $restored ); } $types = Schema::field_types(); wp_nonce_field( self::NONCE, self::NONCE . '_nonce' ); ?>

$field ) { if ( is_array( $field ) ) { self::field_row( $index, $field, $types ); } } ?>
'; Flow::open_panel( 'style' ); AppearanceControls::render( $schema['appearance'] ?? array(), $schema['layout'] ?? array() ); echo ''; Flow::open_panel( 'delivery' ); ?>

$recipient ) { self::recipient_row( $recipient_index, $recipient ); } ?>

'; Flow::open_panel( 'review' ); Flow::review( $post, $schema, false ); Editor::options( $post, $schema, 'review' ); global $wp_meta_boxes; if ( ! empty( $wp_meta_boxes['accessible_form']['af-publishing'] ) ) { echo '
' . esc_html__( 'WordPress status & deletion', 'accessible-forms-by-accessible-org' ) . ''; do_meta_boxes( 'accessible_form', 'af-publishing', $post ); echo '
'; } echo ''; Flow::footer( $post ); ?>

aria-describedby="" required>

'text', 'label' => '', 'name' => '', 'help' => '', 'required' => false, 'autocomplete' => '', 'options' => array(), 'autocomplete_mode' => 'auto', ) ); $base = 'af_schema[fields][' . $index . ']'; $id = 'af-field-' . $index; $content = 'content' === $field['type']; if ( 'file' === $field['type'] && empty( $field['addon'] ) ) { // Upload fields saved before 2.0 belong to Accessible Forms Pro. $field['addon'] = 'pro'; } $paused = ! isset( $types[ $field['type'] ] ) && ! empty( $field['addon'] ); $config = $types[ $field['type'] ] ?? array( 'label' => $paused ? __( 'Paused add-on field', 'accessible-forms-by-accessible-org' ) : $types['text']['label'], 'autocomplete' => array(), ); $autofill = 'manual' === $field['autocomplete_mode'] ? $field['autocomplete'] : 'auto'; $options = is_array( $field['options'] ) ? implode( "\n", $field['options'] ) : $field['options']; ?>
open> > hidden> > __( 'Half width', 'accessible-forms-by-accessible-org' ), 'third' => __( 'Third width', 'accessible-forms-by-accessible-org' ), 'two-thirds' => __( 'Two-thirds width', 'accessible-forms-by-accessible-org' ) )[ $field['width'] ] ?? '' ) ); ?>
'; } if ( $paused ) : ?>

> >

'; } ?>

>

>

pattern="[a-z0-9_]+" aria-describedby="">

>

'', 1 => __( 'Form updated.', 'accessible-forms-by-accessible-org' ), 2 => __( 'Form updated.', 'accessible-forms-by-accessible-org' ), 3 => __( 'Form updated.', 'accessible-forms-by-accessible-org' ), 4 => __( 'Form updated.', 'accessible-forms-by-accessible-org' ), 5 => __( 'Form restored.', 'accessible-forms-by-accessible-org' ), 6 => __( 'Form published. Choose where it appears below to add it to your site.', 'accessible-forms-by-accessible-org' ), 7 => __( 'Form saved.', 'accessible-forms-by-accessible-org' ), 8 => __( 'Form submitted.', 'accessible-forms-by-accessible-org' ), 9 => __( 'Form scheduled.', 'accessible-forms-by-accessible-org' ), 10 => __( 'Form draft updated.', 'accessible-forms-by-accessible-org' ), ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only success-message selection. if ( 'draft' === sanitize_key( wp_unslash( $_GET['af_saved'] ?? '' ) ) ) { $messages['accessible_form'][1] = __( 'Draft changes saved. Publish changes when you are ready to update the live form.', 'accessible-forms-by-accessible-org' ); } return $messages; } /** * Save the schema, rejecting anything inaccessible. * * @param int $post_id Form post ID. * @param \WP_Post $post Post object. */ public static function save( $post_id, $post ) { if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; } if ( ! isset( $_POST[ self::NONCE . '_nonce' ] ) ) { return; } $nonce = sanitize_text_field( wp_unslash( $_POST[ self::NONCE . '_nonce' ] ) ); if ( ! wp_verify_nonce( $nonce, self::NONCE ) ) { return; } if ( ! current_user_can( 'edit_post', $post_id ) ) { return; } // The owner chose to discard edits that were saved with problems. if ( ! empty( $_POST['af_discard_pending'] ) ) { FormCheck::forget( $post_id ); self::clear_rejection( $post_id ); return; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized field by field in Schema::sanitize(). $raw = self::submitted_schema( $post_id ); $access_rule = \AccessibleForms\Access::posted_rule( $post_id ); if ( is_wp_error( $access_rule ) ) { self::reject( $post_id, array( Schema::issue( $access_rule->get_error_message(), 'entry_access' ) ), $raw ); return; } $expected = sanitize_text_field( wp_unslash( $_POST['af_edit_version'] ?? '' ) ); // An add-on may handle the save itself, such as restoring a stored version. It returns // true when done, a WP_Error to reject, or null to let the normal save continue. $handled = Extensions::filter( 'accessible_forms_editor_save', null, $post_id, $raw, $expected ); if ( is_wp_error( $handled ) ) { self::reject( $post_id, array( Schema::issue( $handled->get_error_message(), (string) ( $handled->get_error_data()['setting'] ?? '' ) ?: null ) ), $raw ); return; } if ( true === $handled ) { // Such as a restored version, which replaces any edits kept with problems. self::clear_rejection( $post_id ); FormCheck::forget( $post_id ); return; } if ( isset( $_POST['af_editor_format'] ) && empty( $_POST['af_schema_complete'] ) ) { // Partial input must never be kept: saving it would drop the fields that did not arrive. self::reject( $post_id, array( Schema::issue( __( 'The server received an incomplete form. Your saved version was kept. Enable JavaScript or ask your host to raise max_input_vars.', 'accessible-forms-by-accessible-org' ) ) ), $raw ); return; } list( $schema, $errors ) = Schema::validate( $raw ); if ( $errors ) { // The edits are kept on the server, so no work is lost, but nothing // that breaks a rule reaches the draft or published versions. FormCheck::keep( $post_id, $raw, $errors ); self::clear_rejection( $post_id ); return; } $reviewed = isset( $_POST['af_accessibility_reviewed'] ) && '1' === $_POST['af_accessibility_reviewed']; if ( self::publishing_requested( $post_id, $post->post_status ) && ! $reviewed ) { FormCheck::keep( $post_id, $raw, array( self::review_issue() ) ); self::clear_rejection( $post_id ); return; } $publish = self::publishing_requested( $post_id, $post->post_status ); // A first publication has no working copy; save its validated schema directly. if ( ! Schema::get_live( $post_id ) ) { $publish = in_array( $post->post_status, array( 'publish', 'future' ), true ); } $result = Revisions::save( $post_id, $schema, $expected, $publish, $reviewed ); if ( is_wp_error( $result ) ) { if ( 'publish' === get_post_status( $post_id ) && ! Schema::get_live( $post_id ) ) { // WordPress updates status before this hook; a failed first publication stays a draft. remove_action( 'save_post_accessible_form', array( self::class, 'save' ), 10 ); try { wp_update_post( array( 'ID' => $post_id, 'post_status' => 'draft' ) ); } finally { add_action( 'save_post_accessible_form', array( self::class, 'save' ), 10, 2 ); } } self::reject( $post_id, array( Schema::issue( $result->get_error_message(), $result->get_error_data()['setting'] ?? null ) ), $raw ); return; } self::clear_rejection( $post_id ); FormCheck::forget( $post_id ); // Add-ons save settings that live outside the form configuration, such as access lists. if ( ! Extensions::notify( 'accessible_forms_editor_saved', (int) $post_id ) ) { self::reject( $post_id, array( Schema::issue( __( 'The form was saved, but some add-on settings could not be saved. Review those settings and try again.', 'accessible-forms-by-accessible-org' ) ) ), $raw ); return; } if ( isset( $_POST['af_record_review'] ) && '1' === $_POST['af_record_review'] ) { $report = isset( $_POST['af_manual_review'] ) && is_array( $_POST['af_manual_review'] ) ? map_deep( wp_unslash( $_POST['af_manual_review'] ), 'sanitize_textarea_field' ) : array(); $result = AccessibilityReview::record( $post_id, $schema, $report ); if ( is_wp_error( $result ) ) { set_transient( AccessibilityReview::META . '_' . $post_id . '_' . get_current_user_id(), $report, 600 ); self::reject( $post_id, array( Schema::issue( $result->get_error_message(), 'manual_review' ) ), $raw ); } } } /** * Show kept edits (a rejected save, or changes restored from this browser) * over the stored form. * * Kept edits are raw input: a settings group such as the webhook lacks any * checkbox that was left unticked, and any list with nothing chosen. Those * missing keys mean "off" or "none", so they are filled that way; any other * missing key takes its default. Lists, such as fields, are replaced whole. * * @param array $schema Stored schema. * @param array $kept Raw kept input. * @return array */ private static function overlay_kept( array $schema, array $kept ) { $defaults = Schema::defaults(); foreach ( $kept as $key => $value ) { $default = $defaults[ $key ] ?? null; if ( is_array( $default ) && $default && array_values( $default ) !== $default && is_array( $value ) ) { foreach ( $default as $inner => $inner_default ) { if ( ! array_key_exists( $inner, $value ) ) { if ( is_bool( $inner_default ) ) { $value[ $inner ] = false; } elseif ( is_array( $inner_default ) && array_values( $inner_default ) === $inner_default ) { $value[ $inner ] = array(); } else { $value[ $inner ] = $inner_default; } } } } $schema[ $key ] = $value; } return $schema; } /** * Keep the rejected edits and the reasons, for the next page load. * * @param int $post_id Form post ID. * @param array $issues Structured issues from Schema::validate() or Schema::issue(). * @param array $raw Submitted schema input. */ private static function reject( $post_id, array $issues, array $raw ) { $suffix = $post_id . '_' . get_current_user_id(); set_transient( 'accessible_forms_errors_' . $suffix, $issues, 600 ); set_transient( 'accessible_forms_draft_' . $suffix, Schema::clean_editor_input( $raw ), 600 ); } /** * Forget a previous rejection after a successful save. * * @param int $post_id Form post ID. */ private static function clear_rejection( $post_id ) { $suffix = $post_id . '_' . get_current_user_id(); delete_transient( 'accessible_forms_errors_' . $suffix ); delete_transient( 'accessible_forms_draft_' . $suffix ); $blocked = get_transient( self::PUBLICATION_BLOCKS . get_current_user_id() ); if ( is_array( $blocked ) && isset( $blocked[ $post_id ] ) ) { unset( $blocked[ $post_id ] ); if ( $blocked ) { set_transient( self::PUBLICATION_BLOCKS . get_current_user_id(), $blocked, 600 ); } else { delete_transient( self::PUBLICATION_BLOCKS . get_current_user_id() ); } } } /** * Structured save errors waiting to be shown to the current user. * * Older transients hold plain strings; they become general issues. * * @param int $post_id Form post ID. * @return array[] */ public static function pending_errors( $post_id ) { $stored = $post_id ? get_transient( 'accessible_forms_errors_' . $post_id . '_' . get_current_user_id() ) : false; if ( ! is_array( $stored ) ) { return array(); } $issues = array(); foreach ( $stored as $issue ) { if ( is_array( $issue ) && isset( $issue['message'] ) ) { $issues[] = Schema::issue( $issue['message'], $issue['setting'] ?? null, $issue['field'] ?? null, $issue['control'] ?? null ); } elseif ( is_string( $issue ) && '' !== $issue ) { $issues[] = Schema::issue( $issue ); } } return $issues; } /** * The element ID an issue refers to, used for links in the error summary. * * These IDs match the server-rendered editor, so the links work without * JavaScript; the editor script then opens the right step and panel. * * @param array $issue Structured issue. * @return string Empty when the issue has no single control. */ public static function issue_target( array $issue ) { if ( isset( $issue['field'] ) && null !== $issue['field'] ) { $base = 'af-field-' . (int) $issue['field']; $control = (string) ( $issue['control'] ?? 'label' ); if ( in_array( $control, array( 'label', 'type', 'name', 'autocomplete' ), true ) ) { return $base . '-' . $control; } if ( 'choices' === $control ) { return $base . '-choice-0'; } // Add-on controls are named after their input, such as extensions-pro-step. if ( in_array( $control, array( 'min_length', 'min_choices', 'min', 'content', 'email_domain_mode', 'free_email_domains', 'email_domains' ), true ) || 0 === strpos( $control, 'extensions-' ) ) { return 'af-opt-af_schema-fields-' . (int) $issue['field'] . '-' . $control; } return $base . '-label'; } $settings = array( 'heading' => 'af-heading', 'success_message' => 'af-success_message', 'submit_label' => 'af-submit_label', 'closed_message' => 'af-opt-af_schema-closed_message', 'recipients' => 'af-recipient-0', 'cc' => 'af-opt-af_schema-cc', 'appearance' => 'af-appearance-title', 'fields' => 'af-fields-title', 'confirmation_page' => 'af-opt-af_schema-confirmation_page', 'reply_to' => 'af-opt-af_schema-reply_to', 'retention_days' => 'af-retention-form-days', 'history' => 'af-publishing', 'accessibility_review' => 'af-accessibility-reviewed', 'manual_review' => 'af-manual-test-record', 'entry_access' => 'af-opt-af_entry_access_mode', ); // Add-ons name the element for their own settings. $settings = (array) Extensions::filter( 'accessible_forms_issue_targets', $settings ); return $settings[ (string) ( $issue['setting'] ?? '' ) ] ?? ''; } public static function submitted_schema( $id ) { // Add-ons add submitted values kept outside the schema inputs, such as secrets. $submitted = Flow::submitted(); $raw = Extensions::filter( 'accessible_forms_submitted_schema', $submitted, (int) $id ); $raw = is_array( $raw ) ? $raw : $submitted; $saved = Schema::get( $id, true ) ?: array(); if ( ! current_user_can( 'manage_options' ) ) { $raw['cc'] = $saved['cc'] ?? array(); } if ( ! empty( $saved['merge_subject'] ) || ! MergeTags::legacy_conflict( $saved['subject_template'] ?? '' ) ) { $subject = is_string( $raw['subject_template'] ?? null ) ? $raw['subject_template'] : ''; $raw['merge_subject'] = MergeTags::legacy_conflict( $subject ) || str_contains( $subject, '{field:' ); } return $raw; } /** * Print save rejections as an error summary. Each linked message moves to * the control that needs attention. * * @param array[] $errors Structured issues. */ /** * List problems that stop the form from being saved or published, each * linked to its control. * * @param array[] $errors Structured issues. * @param string $mode `rejected` when nothing was saved, `kept` when the edits * were saved with problems, `rules` when the saved form * does not meet the current checks. */ public static function error_summary( array $errors, $mode = 'rejected' ) { $count = count( $errors ); if ( 'kept' === $mode ) { /* translators: %d: number of problems. */ $title = _n( '%d problem to fix before this form can be published', '%d problems to fix before this form can be published', $count, 'accessible-forms-by-accessible-org' ); $intro = __( 'Your edits are saved, but visitors still see the published version. Fix these problems, then publish. Choose a problem to go to it.', 'accessible-forms-by-accessible-org' ); } elseif ( 'rules' === $mode ) { /* translators: %d: number of problems. */ $title = _n( '%d problem to fix before this form can be published again', '%d problems to fix before this form can be published again', $count, 'accessible-forms-by-accessible-org' ); $intro = __( 'The accessibility checks have become stricter since this form was saved. Visitors still see the published form. Fix these problems, then publish your changes. Choose a problem to go to it.', 'accessible-forms-by-accessible-org' ); } else { /* translators: %d: number of problems found when saving. */ $title = _n( '%d change needs attention before this form can be saved', '%d changes need attention before this form can be saved', $count, 'accessible-forms-by-accessible-org' ); $intro = __( 'Your previous form is unchanged; your edits are shown below. Choose a problem to go to it.', 'accessible-forms-by-accessible-org' ); } echo '
'; echo '

' . esc_html( sprintf( $title, $count ) ) . '

' . esc_html( $intro ) . '

'; if ( 'kept' === $mode ) { // Submits the editor's own form, which carries its nonce. echo '

'; } echo '
'; } /** * Surface save rejections and configuration warnings. */ /** * Accessible Forms Pro names this plugin's directory as its dependency, so * WordPress refuses to activate Pro beside a copy installed under the * directory used before the name change. Say why on the Plugins screen, * where WordPress only reports the dependency as missing. */ public static function directory_notice() { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || 'plugins' !== $screen->id || ! current_user_can( 'activate_plugins' ) ) { return; } $directory = dirname( plugin_basename( ACCESSIBLE_FORMS_FILE ) ); if ( 'accessible-forms-by-accessible-org' === $directory || Extensions::registered( 'pro' ) ) { return; } if ( ! file_exists( WP_PLUGIN_DIR . '/accessible-forms-pro/accessible-forms-pro.php' ) ) { return; } wp_admin_notice( sprintf( /* translators: 1: current plugin directory name, 2: expected directory name. */ __( 'Accessible Forms Pro cannot be activated while Accessible Forms runs from the folder “%1$s”. Install the current package, which uses the folder “%2$s”, and activate it; then deactivate and delete this copy. Forms, submissions and settings are shared and stay in place while another copy is active.', 'accessible-forms-by-accessible-org' ), $directory, 'accessible-forms-by-accessible-org' ), array( 'type' => 'warning' ) ); } public static function notices() { $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || 'accessible_form' !== $screen->post_type ) { return; } if ( 'post' === $screen->base ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $post_id = isset( $_GET['post'] ) ? (int) $_GET['post'] : 0; $errors = self::pending_errors( $post_id ); if ( $errors ) { self::error_summary( $errors ); delete_transient( 'accessible_forms_errors_' . $post_id . '_' . get_current_user_id() ); } elseif ( $post_id && current_user_can( 'edit_post', $post_id ) ) { // Shown on every visit until fixed: edits kept with problems, or a // saved form that no longer meets the current checks. $issues = FormCheck::issues( $post_id ); if ( $issues ) { self::error_summary( $issues, FormCheck::pending( $post_id ) ? 'kept' : 'rules' ); } } } // Failed notifications from the last 7 days; dismissible per person. Shown // on the overview screens only: the form editor stays focused on editing, // and an entry's own screen already explains its failure. $overview = 'edit' === $screen->base || str_ends_with( $screen->id, EntriesTable::SLUG ) || str_ends_with( $screen->id, SettingsPage::SLUG ); if ( $overview ) { Inbox::failed_mail_notice(); } if ( 'edit' === $screen->base ) { self::publication_blocks_notice(); self::live_problems_notice(); } } /** Explain rejected native bulk/status requests beside the affected forms. */ private static function publication_blocks_notice() { $blocked = get_transient( self::PUBLICATION_BLOCKS . get_current_user_id() ); $ids = is_array( $blocked ) ? array_filter( array_keys( $blocked ), static function ( $id ) { return 'accessible_form' === get_post_type( $id ) && current_user_can( 'edit_post', $id ); } ) : array(); if ( ! $ids ) { return; } echo ''; } /** * Published forms that do not meet the current accessibility checks. They * stay online, since removing a form would break the pages that use it, * but every editor is told until they are fixed. */ private static function live_problems_notice() { if ( ! current_user_can( 'edit_accessible_forms' ) ) { return; } $forms = array_filter( FormCheck::forms(), static function ( $form ) { return $form['live'] > 0; } ); if ( ! $forms ) { return; } echo '

' . esc_html( sprintf( /* translators: %d: number of forms. */ _n( '%d published form does not meet the current accessibility checks. Visitors still see it. Open it to see what to fix, then publish the fix.', '%d published forms do not meet the current accessibility checks. Visitors still see them. Open each one to see what to fix, then publish the fix.', count( $forms ), 'accessible-forms-by-accessible-org' ), count( $forms ) ) ) . '

'; } /** * Add shortcode and entry-count columns to the forms list. * * @param array $columns Existing columns. * @return array */ public static function columns( $columns ) { $new = array(); foreach ( $columns as $key => $label ) { $new[ $key ] = $label; if ( 'title' === $key ) { $new['af_fields'] = __( 'Fields', 'accessible-forms-by-accessible-org' ); $new['af_shortcode'] = __( 'Placement', 'accessible-forms-by-accessible-org' ); $new['af_entries'] = __( 'Entries', 'accessible-forms-by-accessible-org' ); } } return $new; } /** * Render custom columns. * * @param string $column Column key. * @param int $post_id Form post ID. */ public static function column( $column, $post_id ) { if ( 'af_fields' === $column ) { $schema = Schema::get( $post_id, true ); echo '' . esc_html( number_format_i18n( count( $schema['fields'] ?? array() ) ) ) . ''; } if ( 'af_shortcode' === $column ) { if ( 'publish' !== get_post_status( $post_id ) ) { echo '' . esc_html__( 'Publish to embed', 'accessible-forms-by-accessible-org' ) . ''; return; } echo '

' . Navigation::label( 'pin', __( 'Place form', 'accessible-forms-by-accessible-org' ) ) . '

'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Navigation::label() escapes text and emits only fixed decorative SVG. $summary = Placement::summary( $post_id ); if ( $summary ) { echo '

' . esc_html( $summary ) . '

'; } echo '
' . Navigation::label( 'code', __( 'Shortcode', 'accessible-forms-by-accessible-org' ) ) . ' ' . esc_html( sprintf( /* translators: %s: form title. */ __( 'for %s', 'accessible-forms-by-accessible-org' ), get_the_title( $post_id ) ?: __( '(no title)', 'accessible-forms-by-accessible-org' ) ) ) . '' . Navigation::icon( 'chevron' ) . '
' . esc_html( Placement::shortcode( $post_id ) ) . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Navigation escapes its labels and returns fixed SVG; dynamic titles, IDs and shortcode are escaped here. echo '
'; } if ( 'af_entries' === $column ) { if ( ! Capabilities::form( $post_id ) ) { echo esc_html__( 'Not available', 'accessible-forms-by-accessible-org' ); return; } $counts = Entries::overview( $post_id ); $received = (int) ( $counts['received'] ?? 0 ); /* translators: %s: number of received submissions. */ $received_label = sprintf( _n( '%s received submission', '%s received submissions', $received, 'accessible-forms-by-accessible-org' ), number_format_i18n( $received ) ); echo '' . Navigation::icon( 'inbox' ) . // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Fixed decorative SVG from Navigation::icon(). esc_html( sprintf( /* translators: %s: number of received submissions. */ _n( '%s received', '%s received', $received, 'accessible-forms-by-accessible-org' ), number_format_i18n( $received ) ) ) . ''; // Only what needs attention, as short tags; zero counts are left out. $tags = array( /* translators: %s: number of unread submissions. */ 'unread' => array( 'info', array( 'status' => 'unread' ), _n_noop( '%s unread', '%s unread', 'accessible-forms-by-accessible-org' ) ), /* translators: %s: number of submissions not yet resolved. */ 'unresolved' => array( 'neutral', null, _n_noop( '%s unresolved', '%s unresolved', 'accessible-forms-by-accessible-org' ) ), /* translators: %s: number of submissions held for review. */ 'review' => array( 'warn', array( 'status' => 'review' ), _n_noop( '%s needs review', '%s need review', 'accessible-forms-by-accessible-org' ) ), /* translators: %s: number of submissions marked as spam. */ 'spam' => array( 'neutral', array( 'status' => 'spam' ), _n_noop( '%s spam', '%s spam', 'accessible-forms-by-accessible-org' ) ), /* translators: %s: number of submissions whose notification email failed. */ 'mail_issues' => array( 'error', array( 'mail_status' => 'failed' ), _n_noop( '%s email failed', '%s emails failed', 'accessible-forms-by-accessible-org' ) ), ); $shown = ''; // Progress is extension-owned; retained workflow data is not an // actionable state while the corresponding entry controls are absent. if ( ! in_array( 'workflow', (array) Extensions::filter( 'accessible_forms_entry_filter_keys', array() ), true ) ) { unset( $tags['unresolved'] ); } foreach ( $tags as $key => [$tone, $filter, $noop] ) { $count = (int) ( $counts[ $key ] ?? 0 ); if ( $count < 1 ) { continue; } $text = sprintf( translate_nooped_plural( $noop, $count, 'accessible-forms-by-accessible-org' ), number_format_i18n( $count ) ); $tag = '' . esc_html( $text ) . ''; $shown .= $filter ? '
  • ' . $tag . '
  • ' : '
  • ' . $tag . '
  • '; } if ( $shown ) { echo wp_kses( '', array( 'ul' => array( 'class' => true ), 'li' => array(), 'a' => array( 'href' => true, 'aria-label' => true ), 'span' => array( 'class' => true ), ) ); } } } /** Important warnings belong with the form name, including when Fields is hidden. */ public static function form_states( $states, $post ) { if ( 'accessible_form' !== $post->post_type ) { return $states; } $live = count( FormCheck::live_issues( $post->ID ) ); if ( $live ) { /* translators: %s: number of problems. */ $text = sprintf( _n( '%s accessibility problem', '%s accessibility problems', $live, 'accessible-forms-by-accessible-org' ), number_format_i18n( $live ) ); $states['af_accessibility'] = '' . esc_html( $text ) . ''; } elseif ( FormCheck::pending( $post->ID ) ) { $states['af_accessibility'] = '' . esc_html__( 'Unpublished edits need fixes', 'accessible-forms-by-accessible-org' ) . ''; } return $states; } /** A nonce-protected action for a saved form. */ private static function action_url( $action, $post_id ) { return wp_nonce_url( add_query_arg( array( 'action' => 'accessible_forms_' . $action, 'form_id' => $post_id, ), admin_url( 'admin-post.php' ) ), 'accessible_forms_' . $action . '_' . $post_id ); } /** Name an action independently of WordPress's version-dependent table headers. */ private static function form_action_label( $action, $post_id ) { $title = wp_strip_all_tags( get_the_title( $post_id ) ) ?: __( '(no title)', 'accessible-forms-by-accessible-org' ); /* translators: 1: visible action label, 2: form or field name. */ return sprintf( __( '%1$s for %2$s', 'accessible-forms-by-accessible-org' ), $action, $title ); } /** Add useful management actions to the native WordPress forms list. */ public static function form_actions( $actions, $post ) { if ( 'accessible_form' !== $post->post_type || 'trash' === $post->post_status || ! current_user_can( 'edit_post', $post->ID ) ) { return $actions; } $actions['af_duplicate'] = '' . esc_html__( 'Duplicate', 'accessible-forms-by-accessible-org' ) . ''; $actions['af_preview'] = '' . esc_html__( 'Preview (new tab)', 'accessible-forms-by-accessible-org' ) . ''; if ( Capabilities::form( $post->ID ) ) { $actions['af_entries'] = '' . esc_html__( 'Entries', 'accessible-forms-by-accessible-org' ) . ''; } if ( 'publish' === $post->post_status ) { $actions['af_place'] = '' . esc_html__( 'Place form', 'accessible-forms-by-accessible-org' ) . ''; } return $actions; } /** Native disclosure keeps secondary actions available to keyboard and no-script users. */ public static function group_actions( $actions, $post ) { if ( 'accessible_form' !== $post->post_type ) { return $actions; } $icons = array( 'edit' => 'edit', 'inline' => 'edit', 'trash' => 'trash', 'delete' => 'trash', 'untrash' => 'restore', 'view' => 'eye', 'af_preview' => 'eye', 'af_entries' => 'inbox', 'af_place' => 'pin', 'af_duplicate' => 'copy' ); foreach ( $actions as $key => $markup ) { $action = explode( ' ', $key )[0]; if ( isset( $icons[ $action ] ) ) { $actions[ $key ] = Navigation::action_icon( $markup, $icons[ $action ] ); } } $primary = array_intersect_key( $actions, array_flip( array( 'edit', 'af_entries', 'af_preview' ) ) ); $secondary = array_diff_key( $actions, $primary ); if ( ! $secondary ) { return $primary; } $title = get_the_title( $post ) ?: __( '(no title)', 'accessible-forms-by-accessible-org' ); /* translators: %s: form title. */ $more = '
    ' . Navigation::label( 'more', __( 'More actions', 'accessible-forms-by-accessible-org' ) ) . ' ' . esc_html( sprintf( __( 'for %s', 'accessible-forms-by-accessible-org' ), $title ) ) . '' . Navigation::icon( 'chevron' ) . '
    '; foreach ( $secondary as $key => $markup ) { $more .= '' . $markup . ''; } // Action markup comes from WordPress and the same trusted row-action filters as the original table. $primary['af_more'] = $more . '
    '; return $primary; } /** Validate the form identity, capability and nonce for a management action. */ private static function action_form( $action ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Verified below using the form-specific nonce. $id = isset( $_GET['form_id'] ) ? absint( $_GET['form_id'] ) : 0; check_admin_referer( 'accessible_forms_' . $action . '_' . $id ); $post = get_post( $id ); if ( ! $post || 'accessible_form' !== $post->post_type || ! current_user_can( 'edit_post', $id ) ) { wp_die( esc_html__( 'You do not have permission to manage this form.', 'accessible-forms-by-accessible-org' ), '', array( 'response' => 403 ) ); } return $post; } /** Copy configuration only. Entries stay attached to their original form. */ public static function duplicate_form() { $post = self::action_form( 'duplicate' ); if ( ! current_user_can( 'edit_accessible_forms' ) ) { wp_die( esc_html__( 'You do not have permission to create forms.', 'accessible-forms-by-accessible-org' ) ); } $id = wp_insert_post( wp_slash( array( 'post_type' => 'accessible_form', 'post_status' => 'draft', /* translators: %s: original form title. */ 'post_title' => sprintf( __( '%s (copy)', 'accessible-forms-by-accessible-org' ), $post->post_title ), 'post_author' => get_current_user_id(), ) ), true ); if ( is_wp_error( $id ) ) { wp_die( esc_html( $id->get_error_message() ) ); } $schema = Schema::get( $post->ID, true ) ?: Schema::defaults(); unset( $schema['cc'] ); Schema::save( $id, $schema ); wp_safe_redirect( get_edit_post_link( $id, 'raw' ) ); exit; } /** Preview drafts privately without loading the optional external spam service. */ public static function preview_form() { $post = self::action_form( 'preview' ); nocache_headers(); header( 'Content-Type: text/html; charset=' . get_bloginfo( 'charset' ) ); \AccessibleForms\register_frontend_assets(); wp_enqueue_style( 'accessible-forms-preview', ACCESSIBLE_FORMS_URL . 'assets/preview.css', array( 'accessible-forms' ), \AccessibleForms\VERSION ); ?> > <?php echo esc_html( $post->post_title ); ?>

    post_title ); ?>

    ID, array(), true ), \AccessibleForms\Renderer::allowed_html() ); ?>
    post_type || 'future' !== $post->post_status ) { check_and_publish_future_post( $id ); return; } $schema = Schema::get_live( $post->ID ); $issues = is_array( $schema ) ? Schema::validate( $schema )[1] : array( Schema::issue( __( 'The saved form configuration is missing. Open the form editor and review it before publishing.', 'accessible-forms-by-accessible-org' ) ) ); $pending = FormCheck::pending( $post->ID ); if ( $pending ) { $issues[] = Schema::issue( __( 'This form has unpublished edits that need attention. Open its form editor, fix the listed problems, and use Review & publish.', 'accessible-forms-by-accessible-org' ) ); } if ( $issues ) { FormCheck::keep( $post->ID, $pending['raw'] ?? ( $schema ?: Schema::defaults() ), $issues ); wp_update_post( array( 'ID' => $post->ID, 'post_status' => 'draft' ) ); wp_clear_scheduled_hook( 'publish_future_post', array( $post->ID ) ); // Never delegate on failure, even if the status write could not complete. return; } check_and_publish_future_post( $post->ID ); } /** Validate editor and insertion routes, including Quick Edit, bulk edits and scheduling. */ public static function guard_publish( $data, $postarr ) { if ( 'accessible_form' !== ( $data['post_type'] ?? '' ) ) { return $data; } $id = absint( $postarr['ID'] ?? 0 ); $original = $id ? get_post( $id ) : null; $editor = isset( $_POST[ self::NONCE . '_nonce' ] ) && is_string( $_POST[ self::NONCE . '_nonce' ] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST[ self::NONCE . '_nonce' ] ) ), self::NONCE ); if ( ! $editor ) { if ( ! in_array( $data['post_status'] ?? '', array( 'publish', 'future' ), true ) ) { return $data; } // Only a change of status is a publication. Re-saving an already published // or scheduled form (a Quick Edit of its title, a bulk author change) keeps // the reviewed version visitors already see. if ( $original && $original->post_status === ( $data['post_status'] ?? '' ) ) { return $data; } $schema = $id ? Schema::get_live( $id ) : Schema::defaults(); $issues = is_array( $schema ) ? Schema::validate( $schema )[1] : array( self::review_issue() ); if ( $id && FormCheck::pending( $id ) ) { $issues[] = Schema::issue( __( 'This form has unpublished edits that need attention. Open its form editor, fix the listed problems, and use Review & publish.', 'accessible-forms-by-accessible-org' ) ); } // Interactive status changes must go through the form editor. Trusted // server-side callers still receive the deterministic schema checks. if ( is_admin() && ( ! $original || ! in_array( $original->post_status, array( 'publish', 'future' ), true ) ) ) { $issues[] = self::review_issue(); } if ( $issues ) { $data['post_status'] = $original && 'publish' === $original->post_status ? 'publish' : 'draft'; if ( $id && current_user_can( 'edit_post', $id ) ) { self::reject( $id, $issues, FormCheck::pending( $id )['raw'] ?? ( Schema::get( $id, true ) ?: Schema::defaults() ) ); if ( is_admin() ) { $key = self::PUBLICATION_BLOCKS . get_current_user_id(); $blocked = get_transient( $key ); $blocked = is_array( $blocked ) ? $blocked : array(); $blocked[ $id ] = true; set_transient( $key, array_slice( $blocked, -200, null, true ), 600 ); } } // WordPress has already checked its inline-edit nonce and permissions. // Return its expected plain error response so it announces the problem // and keeps the unsaved Quick Edit controls available for correction. // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only action detection on a rejected write; cannot authorize publication. $action = isset( $_POST['action'] ) && is_string( $_POST['action'] ) ? sanitize_key( wp_unslash( $_POST['action'] ) ) : ''; if ( wp_doing_ajax() && 'inline-save' === $action ) { wp_die( esc_html( implode( ' ', Schema::messages( $issues ) ) ) ); } } return $data; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Opaque hash must exactly match Revisions::token; a changed value fails closed. $expected = isset( $_POST['af_edit_version'] ) && is_string( $_POST['af_edit_version'] ) ? sanitize_text_field( wp_unslash( $_POST['af_edit_version'] ) ) : ''; if ( $original && ! hash_equals( Revisions::token( $id ), $expected ) ) { $data['post_title'] = $original->post_title; $data['post_status'] = $original->post_status; return $data; } // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized by Schema::sanitize. $raw = self::submitted_schema( (int) ( $postarr['ID'] ?? 0 ) ); list( , $errors ) = Schema::validate( $raw ); if ( isset( $_POST['af_editor_format'] ) && empty( $_POST['af_schema_complete'] ) ) { $errors[] = __( 'The complete form settings were not received. Try saving again.', 'accessible-forms-by-accessible-org' ); } if ( self::publishing_requested( $id, $data['post_status'] ?? '' ) && ( ! isset( $_POST['af_accessibility_reviewed'] ) || '1' !== $_POST['af_accessibility_reviewed'] ) ) { $errors[] = self::review_issue(); } if ( $errors && ( empty( $postarr['ID'] ) || 'publish' !== get_post_status( $postarr['ID'] ) ) ) { $data['post_status'] = 'draft'; } return $data; } /** Avoid a competing WordPress success notice after a rejected schema. */ public static function save_redirect( $location, $post_id ) { if ( get_transient( 'accessible_forms_errors_' . $post_id . '_' . get_current_user_id() ) || FormCheck::pending( $post_id ) ) { return remove_query_arg( 'message', $location ); } // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only navigation after the native WordPress save handler. $step = sanitize_key( wp_unslash( $_POST['af_editor_step'] ?? '' ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Read-only navigation after the native WordPress save handler. if ( ! empty( $_POST['publish'] ) || ! empty( $_POST['af_publish_changes'] ) ) { $step = 'review'; } if ( Revisions::has_draft( $post_id ) ) { $location = add_query_arg( 'af_saved', 'draft', $location ); } return in_array( $step, array( 'build', 'style', 'delivery', 'review' ), true ) ? preg_replace( '/#.*$/', '', $location ) . '#af-step-' . $step : $location; } /** Help site owners describe their actual data handling in WordPress's policy guide. */ public static function privacy_guide() { if ( ! function_exists( 'wp_add_privacy_policy_content' ) ) { return; } $content = '

    ' . __( 'When you submit a form, we store the information you provide, the page path, submission time, browser information, and a salted hash of your IP address. We use this information to respond to your request and prevent spam. Notification emails may contain your answers.', 'accessible-forms-by-accessible-org' ) . '

    '; $content .= '

    ' . __( 'Site owner: specify your retention period and who receives notifications. If you send email through an SMTP service, disclose your email provider. Email copies and backups have separate retention policies.', 'accessible-forms-by-accessible-org' ) . '

    '; wp_add_privacy_policy_content( __( 'Accessible Forms', 'accessible-forms-by-accessible-org' ), wp_kses_post( $content ) ); } /** * Register the entries, entry and settings screens. */ /** * WordPress treats a screen registered without a parent as its own menu * section, which overrides the parent_file filter. Once access has been * checked, drop this plugin's hidden screens from that list so the Forms * menu stays open while they are shown. */ public static function unlist_hidden_screens() { global $submenu; foreach ( $submenu[''] ?? array() as $position => $item ) { if ( in_array( $item[2] ?? '', array( EntryView::SLUG, Placement::SLUG ), true ) ) { unset( $submenu[''][ $position ] ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Removes only this plugin's own hidden screens, after the access check. } } } public static function menus() { $placement_hook = add_submenu_page( '', __( 'Where this form appears', 'accessible-forms-by-accessible-org' ), '', 'read', Placement::SLUG, array( Placement::class, 'render' ) ); add_action( 'load-' . $placement_hook, static function () { // Hidden screens need an explicit document title before the admin header. $GLOBALS['title'] = __( 'Where this form appears', 'accessible-forms-by-accessible-org' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Only on this plugin's own hidden screen, which otherwise has no title. // Keep the plugin menu open with Forms highlighted. add_filter( 'parent_file', static function () { return 'edit.php?post_type=accessible_form'; } ); add_filter( 'submenu_file', static function () { return 'edit.php?post_type=accessible_form'; } ); add_action( 'admin_head', array( self::class, 'unlist_hidden_screens' ) ); } ); // Unread plus held-for-review submissions, on both menu levels. $label = __( 'Entries', 'accessible-forms-by-accessible-org' ) . Inbox::menu_badge(); Inbox::badge_top_level_menu(); add_submenu_page( 'edit.php?post_type=accessible_form', __( 'Entries', 'accessible-forms-by-accessible-org' ), $label, 'read_accessible_form_entries', EntriesTable::SLUG, array( EntriesTable::class, 'render_page' ) ); // Registered without a menu entry so the single-entry screen has a home. $entry_hook = add_submenu_page( '', __( 'Entry', 'accessible-forms-by-accessible-org' ), '', 'read_accessible_form_entries', EntryView::SLUG, array( EntryView::class, 'render' ) ); add_action( 'load-' . $entry_hook, static function () { $GLOBALS['title'] = __( 'Entry', 'accessible-forms-by-accessible-org' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Only on this plugin's own hidden screen, which otherwise has no title. // Keep the plugin menu open with Entries highlighted while reading one entry. add_filter( 'parent_file', static function () { return 'edit.php?post_type=accessible_form'; } ); add_filter( 'submenu_file', static function () { return EntriesTable::SLUG; } ); add_action( 'admin_head', array( self::class, 'unlist_hidden_screens' ) ); } ); add_submenu_page( 'edit.php?post_type=accessible_form', __( 'Settings', 'accessible-forms-by-accessible-org' ), __( 'Settings', 'accessible-forms-by-accessible-org' ), 'manage_accessible_forms_settings', SettingsPage::SLUG, array( SettingsPage::class, 'render' ) ); } }