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' );