array( 'name' => __( 'Accessible Forms', 'accessible-forms-by-accessible-org' ), 'singular_name' => __( 'Form', 'accessible-forms-by-accessible-org' ), 'add_new_item' => __( 'Add New Form', 'accessible-forms-by-accessible-org' ), 'edit_item' => __( 'Edit Form', 'accessible-forms-by-accessible-org' ), 'new_item' => __( 'New Form', 'accessible-forms-by-accessible-org' ), 'search_items' => __( 'Search Forms', 'accessible-forms-by-accessible-org' ), 'not_found' => __( 'No forms found.', 'accessible-forms-by-accessible-org' ), 'all_items' => __( 'Forms', 'accessible-forms-by-accessible-org' ), 'not_found_in_trash' => __( 'No forms found in Trash.', 'accessible-forms-by-accessible-org' ), 'filter_items_list' => __( 'Filter forms list', 'accessible-forms-by-accessible-org' ), 'items_list' => __( 'Forms list', 'accessible-forms-by-accessible-org' ), 'items_list_navigation' => __( 'Forms list navigation', 'accessible-forms-by-accessible-org' ), 'menu_name' => __( 'Accessible Forms', 'accessible-forms-by-accessible-org' ), ), 'public' => false, 'show_ui' => true, 'show_in_menu' => true, 'show_in_rest' => false, 'exclude_from_search' => true, 'publicly_queryable' => false, 'has_archive' => false, 'rewrite' => false, 'menu_icon' => 'dashicons-feedback', 'menu_position' => 26, 'supports' => array( 'title' ), 'capability_type' => array( 'accessible_form', 'accessible_forms' ), 'map_meta_cap' => true, ) ); } add_action( 'init', __NAMESPACE__ . '\\register_post_type_form' ); // Publication validation also applies to scheduled and programmatic status changes. add_filter( 'wp_insert_post_data', array( Admin\Admin::class, 'guard_publish' ), 10, 2 ); // Core's cron callback calls wp_publish_post(), which bypasses the insert filter. // The guarded worker delegates all other post types to WordPress unchanged. remove_action( 'publish_future_post', 'check_and_publish_future_post' ); add_action( 'publish_future_post', array( Admin\Admin::class, 'publish_scheduled' ) ); add_action( 'save_post', array( Admin\Placement::class, 'invalidate' ) ); add_action( 'before_delete_post', array( Admin\Placement::class, 'invalidate' ) ); add_action( 'before_delete_post', array( Admin\Experience::class, 'delete_setup' ) ); /** * Register the shortcode and the server-rendered block. */ function register_renderers() { add_shortcode( 'accessible_form', __NAMESPACE__ . '\\shortcode' ); register_frontend_assets(); wp_register_style( 'accessible-forms-block-editor', ACCESSIBLE_FORMS_URL . 'assets/block-editor.css', array( 'accessible-forms' ), VERSION ); wp_register_script( 'accessible-forms-block', ACCESSIBLE_FORMS_URL . 'assets/block.js', array( 'wp-blocks', 'wp-element', 'wp-components', 'wp-block-editor', 'wp-api-fetch', 'wp-i18n' ), VERSION, true ); wp_set_script_translations( 'accessible-forms-block', 'accessible-forms-by-accessible-org' ); register_block_type( ACCESSIBLE_FORMS_DIR . 'assets/form-block', array( 'render_callback' => __NAMESPACE__ . '\\render_block_form', ) ); } add_action( 'init', __NAMESPACE__ . '\\register_renderers' ); add_action( 'rest_api_init', array( FormCatalog::class, 'register' ) ); add_action( 'rest_api_init', array( ProofOfWork::class, 'register' ) ); add_action( 'accessible_forms_purge_batch', array( Audit::class, 'purge' ) ); add_action( 'accessible_forms_purge', array( Locks::class, 'purge' ) ); add_action( 'accessible_forms_purge_batch', array( Locks::class, 'purge' ) ); /** * Shortcode handler. * * @param array $atts Shortcode attributes. * @return string */ function shortcode( $atts ) { $atts = shortcode_atts( array( 'id' => 0, 'key' => '', ), $atts, 'accessible_form' ); return Renderer::render( FormKey::resolve( $atts['id'], $atts['key'] ) ) ?: unavailable_notice(); } /** * The message shown where a form cannot be displayed, in the same styled box * as a form so its text keeps the checked colours and sizes. * * @return string */ function unavailable_notice() { wp_enqueue_style( 'accessible-forms' ); return '

' . esc_html__( 'This form is currently unavailable.', 'accessible-forms-by-accessible-org' ) . '

'; } /** * Block render callback. * * @param array $attributes Block attributes. * @return string */ function render_block_form( $attributes ) { $form_id = FormKey::resolve( $attributes['formId'] ?? 0, $attributes['formKey'] ?? '' ); if ( ! $form_id ) { return is_admin() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ? '

' . esc_html__( 'Select a form.', 'accessible-forms-by-accessible-org' ) . '

' : ''; } $html = Renderer::render( $form_id ) ?: unavailable_notice(); // The standard block wrapper carries alignment (wide/full) and block classes. return '
' . $html . '
'; } /** * Register the block editor script. Plain JS, no build step. */ function register_block_editor_assets() { wp_localize_script( 'accessible-forms-block', 'AccessibleFormsBlock', array( 'createUrl' => current_user_can( 'edit_accessible_forms' ) ? admin_url( 'post-new.php?post_type=accessible_form' ) : '', 'manageUrl' => current_user_can( 'edit_accessible_forms' ) ? admin_url( 'edit.php?post_type=accessible_form' ) : '', ) ); } add_action( 'enqueue_block_editor_assets', __NAMESPACE__ . '\\register_block_editor_assets' ); /** * Front-end assets. Only enqueued when a form is actually rendered. */ function register_frontend_assets() { wp_register_style( 'accessible-forms', ACCESSIBLE_FORMS_URL . 'assets/form.css', array(), VERSION ); wp_register_script( 'accessible-forms', ACCESSIBLE_FORMS_URL . 'assets/form.js', array( 'wp-i18n' ), VERSION, true ); wp_set_script_translations( 'accessible-forms', 'accessible-forms-by-accessible-org' ); } add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\\enqueue_embedded_assets' ); add_filter( 'jetpack_boost_async_style', array( FrontendAssets::class, 'stylesheet_loading' ), PHP_INT_MAX, 2 ); /** Load styles before the head for direct and reusable form placements. */ function enqueue_embedded_assets() { FrontendAssets::enqueue(); } /** * Handle submissions before any output is sent, so a redirect is still possible. */ add_action( 'template_redirect', array( Submission::class, 'maybe_handle' ), 1 ); add_action( 'admin_post_accessible_forms_complete', array( Standalone::class, 'render' ) ); add_action( 'admin_post_nopriv_accessible_forms_complete', array( Standalone::class, 'render' ) ); /** * Daily retention purge. */ add_action( 'accessible_forms_purge', array( Entries::class, 'purge' ) ); add_action( 'accessible_forms_purge_batch', array( Entries::class, 'purge' ) ); add_action( 'accessible_forms_purge', array( Audit::class, 'purge' ) ); add_action( 'accessible_forms_notifications', array( Notifications::class, 'run' ) ); add_action( 'accessible_forms_purge', array( ProofOfWork::class, 'purge' ) ); add_action( 'accessible_forms_purge_proofs', array( ProofOfWork::class, 'purge' ) ); /** * Boot the admin. */ add_action( 'plugins_loaded', static function () { Admin\TestPreview::init(); if ( is_admin() ) { Admin\Admin::init(); } // Mail delivery. Adds no hooks unless SMTP or a From address is // configured, so other mail plugins keep working untouched. Mailer::init(); Entries::register_privacy_hooks(); TimeMigration::register(); // Editor-only accessibility check on published pages. Visitors get nothing. LiveCheck::init(); // Run a table upgrade if the plugin was updated without reactivation. A // failed upgrade is retried every ten minutes, not on every request. if ( (int) get_option( 'accessible_forms_db_version' ) !== DB_VERSION && ! get_transient( 'accessible_forms_install_retry' ) ) { add_action( 'init', __NAMESPACE__ . '\\activate', 1 ); } // The daily cleanup is scheduled on activation. Deactivating another copy // of this plugin (such as the one installed under the earlier directory // name) clears it, so it is scheduled again here when missing. if ( ! wp_next_scheduled( 'accessible_forms_purge' ) ) { wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'accessible_forms_purge' ); } } ); // Register Free-owned team controls after earlier add-ons have finished booting. add_action( 'plugins_loaded', array( Access::class, 'init' ), 30 ); /** * Activation: create the table, seed options, schedule the purge. */ function activate() { $stored_version = (int) get_option( 'accessible_forms_db_version' ); Entries::install(); Audit::install(); Notifications::install(); Capabilities::install(); Migration::run( $stored_version ); global $wpdb; $ready = true; foreach ( array( Entries::table_name(), Audit::table(), Notifications::table(), ) as $table ) { if ( ! $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $wpdb->esc_like( $table ) ) ) ) { $ready = false; } } if ( $ready ) { $columns = $wpdb->get_col( $wpdb->prepare( 'DESCRIBE %i', Entries::table_name() ) ); if ( array_diff( array( 'snapshot', 'request_key', 'payload_hash', 'workflow', 'assignee', 'notes', 'expires_at', 'retention_days', 'resolved_at', 'first_reply_at', 'time_version', 'time_original', ), $columns ) ) { $ready = false; } } if ( $ready ) { update_option( 'accessible_forms_db_version', DB_VERSION ); delete_transient( 'accessible_forms_install_retry' ); } else { // Health reports the missing tables; repeating the work on every request would not change it. set_transient( 'accessible_forms_install_retry', 1, 10 * MINUTE_IN_SECONDS ); } add_option( Settings::OPTION, Settings::defaults() ); if ( ! wp_next_scheduled( 'accessible_forms_purge' ) ) { wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'accessible_forms_purge' ); } } register_activation_hook( __FILE__, __NAMESPACE__ . '\\activate' ); // Each existing network site upgrades on its first request. This avoids an // unbounded network activation scan; new sites can be initialized immediately. add_action( 'wp_initialize_site', static function ( $site ) { if ( ! isset( get_site_option( 'active_sitewide_plugins', array() )[ plugin_basename( ACCESSIBLE_FORMS_FILE ) ] ) ) { return; } switch_to_blog( $site->blog_id ); try { activate(); } finally { restore_current_blog(); } }, 200 ); /** * Whether another installed copy of this plugin is active, such as the one in * the directory WordPress.org assigned. Both share the same options, tables and * schedules, so the copy being switched off must leave them alone. * * @return bool */ function other_copy_active() { $this_copy = plugin_basename( ACCESSIBLE_FORMS_FILE ); $active = array_merge( (array) get_option( 'active_plugins', array() ), is_multisite() ? array_keys( (array) get_site_option( 'active_sitewide_plugins', array() ) ) : array() ); foreach ( $active as $plugin ) { if ( is_string( $plugin ) && $plugin !== $this_copy && 'accessible-forms.php' === basename( $plugin ) && is_file( WP_PLUGIN_DIR . '/' . $plugin ) ) { return true; } } return false; } /** * Deactivation: unschedule the purge. Data is left untouched. */ function deactivate( $network_wide = false ) { if ( other_copy_active() ) { return; } if ( $network_wide && is_multisite() ) { $offset = 0; do { $sites = get_sites( array( 'fields' => 'ids', 'number' => 100, 'offset' => $offset ) ); foreach ( $sites as $site ) { switch_to_blog( $site ); try { deactivate( false ); } finally { restore_current_blog(); } } $offset += 100; } while ( count( $sites ) === 100 ); return; } wp_clear_scheduled_hook( 'accessible_forms_purge' ); wp_clear_scheduled_hook( 'accessible_forms_purge_proofs' ); wp_clear_scheduled_hook( 'accessible_forms_purge_batch' ); wp_clear_scheduled_hook( 'accessible_forms_notifications' ); wp_clear_scheduled_hook( 'accessible_forms_search_index' ); wp_clear_scheduled_hook( TimeMigration::HOOK ); } register_deactivation_hook( __FILE__, __NAMESPACE__ . '\\deactivate' );