';
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 '
' . esc_html__( 'Publication was blocked. Open the affected form editor, fix the listed problems, and complete Review & publish. Other valid bulk edits may have been saved.', 'accessible-forms-by-accessible-org' ) . '
';
}
/**
* 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 )
)
) .
'
';
foreach ( array_keys( $forms ) as $id ) {
if ( current_user_can( 'edit_post', $id ) ) {
echo '
';
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 );
?>
>
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' )
);
}
}