submissions_page = new SubmissionsPage(); $this->settings_page = new SettingsPage(); $this->logs_page = new LogsPage(); $this->tools_page = new ToolsPage(); $this->integrations_page = new IntegrationsPage( IntegrationRegistry::get_instance() ); $this->dashboard_page = new DashboardPage(); } /** * Initialize admin functionality. * * @since 1.0.0 */ public function init(): void { $this->hooks(); ( new PluginInstaller() )->hooks(); // Initialize page controllers. $this->submissions_page->init(); // Initialize usage limit banner. UsageLimitNotice::init(); // Initialize API connection bar. ConnectionBar::hooks(); // Handle the one-click Connect return on admin_init (before output). ConnectFlow::hooks(); // Full-screen welcome screen shown after first activation. WelcomeScreen::hooks(); } /** * Initialize admin hooks. * * @since 1.0.0 */ public function hooks(): void { add_action( 'admin_init', [ $this, 'maybe_redirect_after_activation' ] ); add_action( 'admin_init', [ $this->settings_page, 'maybe_redirect_legacy_integration' ] ); add_action( 'admin_menu', [ $this, 'add_menu_pages' ] ); add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_assets' ] ); // Priority 100 keeps us behind anything else on the hook; in_admin_header // fires before #screen-meta, so the panel can no longer displace the header. add_action( 'in_admin_header', [ $this, 'maybe_render_admin_header' ], 100 ); add_action( 'admin_print_scripts', [ $this, 'hide_unrelated_notices' ] ); add_action( 'admin_notices', [ SubmissionsTable::class, 'display_bulk_action_notices' ] ); add_action( 'wp_ajax_activelayer_verify_api_key', [ $this->settings_page, 'ajax_verify_api_key' ] ); add_action( 'wp_ajax_activelayer_save_integration_settings', [ $this->integrations_page, 'ajax_save_settings' ] ); add_action( 'wp_ajax_activelayer_dismiss_onboarding', [ $this, 'ajax_dismiss_onboarding' ] ); add_action( 'wp_ajax_activelayer_dismiss_opt_out_announce', [ $this, 'ajax_dismiss_opt_out_announce' ] ); add_action( 'admin_enqueue_scripts', [ $this->dashboard_page, 'enqueue_chart_assets' ] ); add_filter( 'plugin_action_links_' . plugin_basename( ACTIVELAYER_PLUGIN_FILE ), [ $this, 'add_plugin_action_links' ] ); } /** * Redirect to dashboard page after first-time plugin activation. * * @since 1.1.0 * @since 1.6.0 Land on the full-screen welcome screen when no API key is connected yet; AJAX/cron requests no longer consume the transient. */ public function maybe_redirect_after_activation(): void { // Bail before touching the transient: admin-ajax/cron also fire admin_init, // and consuming the transient there would steal the redirect from the // user's first real admin page load (e.g. a Heartbeat tick). if ( wp_doing_ajax() || wp_doing_cron() ) { return; } if ( ! get_transient( 'activelayer_activation_redirect' ) ) { return; } delete_transient( 'activelayer_activation_redirect' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['activate-multi'] ) || is_network_admin() ) { return; } if ( ! current_user_can( 'manage_activelayer' ) ) { return; } // Not connected yet: land on the full-screen welcome screen instead. $target = SettingsHelper::has_api_key() ? 'activelayer-dashboard' : WelcomeScreen::PAGE_SLUG; wp_safe_redirect( admin_url( 'admin.php?page=' . $target ) ); exit; } /** * Add action links on the Plugins list page. * * @since 1.1.0 * @since 1.2.0 Build register URL via AppUrlHelper and normalize UTM shape. * @since 1.3.0 Account link now builds a one-click Connect URL. * * @param array $links Existing plugin action links. * * @return array Modified plugin action links. */ public function add_plugin_action_links( array $links ): array { if ( ! current_user_can( 'manage_activelayer' ) ) { return $links; } $settings_url = admin_url( 'admin.php?page=activelayer-settings' ); $docs_url = 'https://activelayer.com/docs/wordpress-plugin/?utm_source=plugin&utm_medium=plugins-page&utm_campaign=action-links'; if ( SettingsHelper::has_api_key() ) { $account_link = sprintf( '%2$s', esc_url( 'https://app.activelayer.com/?utm_source=plugin&utm_medium=plugins-page&utm_campaign=action-links' ), esc_html__( 'Manage Account', 'activelayer-anti-spam-spam-protection-for-forms-comments' ) ); } else { $account_link = sprintf( '%3$s', esc_url( ( new ConnectFlow() )->start( 'plugins-page', 'create_account' ) ), esc_attr__( 'Create your ActiveLayer account', 'activelayer-anti-spam-spam-protection-for-forms-comments' ), esc_html__( 'Get ActiveLayer Account', 'activelayer-anti-spam-spam-protection-for-forms-comments' ) ); } $custom_links = [ 'account' => $account_link, 'settings' => sprintf( '%2$s', esc_url( $settings_url ), esc_html__( 'Settings', 'activelayer-anti-spam-spam-protection-for-forms-comments' ) ), 'docs' => sprintf( '%2$s', esc_url( $docs_url ), esc_html__( 'Docs', 'activelayer-anti-spam-spam-protection-for-forms-comments' ) ), ]; return array_merge( $custom_links, $links ); } /** * Render the branded admin header on ActiveLayer screens. * * Hooked to in_admin_header rather than printed inside each page callback: * WordPress outputs #screen-meta at the top of #wpbody-content, so a header * printed by the page was a later sibling and got pushed down the moment the * Screen Options panel expanded. * * @since 1.7.0 * * @return void */ public function maybe_render_admin_header(): void { $screen = get_current_screen(); if ( ! $screen || strpos( $screen->id, 'activelayer' ) === false ) { return; } self::render_header(); } /** * Render admin header. * * @since 1.0.0 */ public static function render_header(): void { $logo_url = ACTIVELAYER_PLUGIN_URL . 'assets/images/logo.svg'; ?>
' . esc_html__( 'Upgrade to Pro', 'activelayer-anti-spam-spam-protection-for-forms-comments' ) . '', 'manage_activelayer', $upgrade_url, ]; } } /** * Enqueue admin assets. * * @since 1.0.0 * * @param string $hook Current admin page. */ public function enqueue_assets( string $hook ): void { $is_activelayer_page = strpos( $hook, 'activelayer' ) !== false; // Use minified files in production, regular files in debug mode. $suffix = ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ? '' : '.min'; // Load styles only on ActiveLayer admin pages. if ( $is_activelayer_page ) { wp_enqueue_style( 'activelayer-admin', ACTIVELAYER_PLUGIN_URL . 'assets/css/admin' . $suffix . '.css', [], ACTIVELAYER_PLUGIN_VERSION ); } // Only load full JS on our pages, but provide AJAX data globally for banner dismiss. if ( $is_activelayer_page ) { wp_enqueue_script( 'activelayer-admin', ACTIVELAYER_PLUGIN_URL . 'assets/js/admin' . $suffix . '.js', [ 'jquery' ], ACTIVELAYER_PLUGIN_VERSION, true ); } else { wp_enqueue_script( 'activelayer-admin-global', ACTIVELAYER_PLUGIN_URL . 'assets/js/admin' . $suffix . '.js', [ 'jquery' ], ACTIVELAYER_PLUGIN_VERSION, true ); } $script_handle = $is_activelayer_page ? 'activelayer-admin' : 'activelayer-admin-global'; wp_localize_script( $script_handle, 'activelayerAdmin', [ 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'activelayer_admin' ), 'verifyKeyNonce' => wp_create_nonce( 'activelayer_verify_api_key' ), 'installPluginNonce' => wp_create_nonce( 'activelayer_install_plugin' ), 'strings' => [ 'emptyApiKey' => __( 'Please enter an API key', 'activelayer-anti-spam-spam-protection-for-forms-comments' ), 'verifying' => __( 'Verifying...', 'activelayer-anti-spam-spam-protection-for-forms-comments' ), 'verifyKey' => __( 'Verify Key', 'activelayer-anti-spam-spam-protection-for-forms-comments' ), 'connectionError' => __( 'Connection error. Please try again.', 'activelayer-anti-spam-spam-protection-for-forms-comments' ), 'apiKeyValidTitle' => __( 'API key is valid', 'activelayer-anti-spam-spam-protection-for-forms-comments' ), /* translators: %s: number of selected submissions. */ 'bulkDeleteConfirm' => __( 'Are you sure you want to delete %s submission(s)? This action cannot be undone.', 'activelayer-anti-spam-spam-protection-for-forms-comments' ), 'singleDeleteConfirm' => __( 'Are you sure you want to delete this submission? This action cannot be undone.', 'activelayer-anti-spam-spam-protection-for-forms-comments' ), 'dismissOnboardingConfirm' => __( 'Are you sure you want to skip the setup guide?', 'activelayer-anti-spam-spam-protection-for-forms-comments' ), 'installing' => __( 'Installing...', 'activelayer-anti-spam-spam-protection-for-forms-comments' ), 'activating' => __( 'Activating...', 'activelayer-anti-spam-spam-protection-for-forms-comments' ), 'installError' => __( 'Installation failed. Please try again.', 'activelayer-anti-spam-spam-protection-for-forms-comments' ), 'copied' => __( 'Copied!', 'activelayer-anti-spam-spam-protection-for-forms-comments' ), ], ] ); // Enqueue integrations.js only on the integrations page. if ( strpos( $hook, 'activelayer-integrations' ) !== false ) { wp_enqueue_script( 'activelayer-integrations', ACTIVELAYER_PLUGIN_URL . 'assets/js/integrations' . $suffix . '.js', [], ACTIVELAYER_PLUGIN_VERSION, true ); wp_localize_script( 'activelayer-integrations', 'activelayerIntegrations', [ 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'activelayer_integration_settings' ), 'strings' => [ 'saving' => __( 'Saving...', 'activelayer-anti-spam-spam-protection-for-forms-comments' ), 'saved' => __( 'Settings saved.', 'activelayer-anti-spam-spam-protection-for-forms-comments' ), 'saveError' => __( 'Save failed. Please try again.', 'activelayer-anti-spam-spam-protection-for-forms-comments' ), 'saveButton' => __( 'Save Settings', 'activelayer-anti-spam-spam-protection-for-forms-comments' ), 'active' => __( 'Active', 'activelayer-anti-spam-spam-protection-for-forms-comments' ), 'paused' => __( 'Paused', 'activelayer-anti-spam-spam-protection-for-forms-comments' ), 'selectAll' => __( 'Select All', 'activelayer-anti-spam-spam-protection-for-forms-comments' ), 'deselectAll' => __( 'Deselect All', 'activelayer-anti-spam-spam-protection-for-forms-comments' ), ], ] ); } } /** * Hide unrelated admin notices on ActiveLayer pages. * * Removes third-party plugin notices from ActiveLayer admin screens * to keep the UI clean and focused. * * @since 1.1.0 */ public function hide_unrelated_notices(): void { $screen = get_current_screen(); if ( ! $screen || strpos( $screen->id, 'activelayer' ) === false ) { return; } $notice_hooks = [ 'user_admin_notices', 'admin_notices', 'all_admin_notices', 'network_admin_notices', ]; foreach ( $notice_hooks as $hook ) { $this->remove_hook_callbacks( $hook, [ $this, 'is_activelayer_callback' ], true ); } // Remove delayed admin notices from footer. $this->remove_hook_callbacks( 'admin_footer', static function ( $callback ) { return ( $callback['function'] ?? null ) === 'render_delayed_admin_notices'; } ); } /** * Remove callbacks from a hook based on a matcher function. * * When $invert is true, callbacks matching the matcher are kept (all others removed). * When $invert is false (default), callbacks matching the matcher are removed. * * @since 1.1.0 * * @param string $hook Hook name. * @param callable $matcher Receives a callback array, returns bool. * @param bool $invert When true, keep matches and remove non-matches. */ private function remove_hook_callbacks( string $hook, callable $matcher, bool $invert = false ): void { global $wp_filter; if ( ! isset( $wp_filter[ $hook ] ) || empty( $wp_filter[ $hook ]->callbacks ) ) { return; } foreach ( $wp_filter[ $hook ]->callbacks as $priority => $callbacks ) { foreach ( $callbacks as $name => $arr ) { $matched = $matcher( $arr ); if ( $invert ? $matched : ! $matched ) { continue; } unset( $wp_filter[ $hook ]->callbacks[ $priority ][ $name ] ); } } } /** * Check if a callback belongs to ActiveLayer. * * @since 1.1.0 * * @param array $callback Callback array from $wp_filter. * * @return bool */ private function is_activelayer_callback( array $callback ): bool { $function = $callback['function'] ?? null; if ( ! $function ) { return false; } // Instance method call: [ $object, 'method' ]. if ( is_array( $function ) && isset( $function[0] ) && is_object( $function[0] ) ) { return stripos( get_class( $function[0] ), 'activelayer' ) !== false; } // Static method call: [ 'ClassName', 'method' ]. if ( is_array( $function ) && isset( $function[0] ) && is_string( $function[0] ) ) { return stripos( $function[0], 'activelayer' ) !== false; } // Static class method string: 'ClassName::method'. if ( is_string( $function ) && stripos( $function, 'activelayer' ) !== false ) { return true; } return false; } /** * Redirect main menu to dashboard page. * * Ensures the top-level ActiveLayer menu loads the Dashboard screen by default. * * @since 1.1.0 */ public function redirect_to_dashboard_page(): void { wp_safe_redirect( admin_url( 'admin.php?page=activelayer-dashboard' ) ); exit; } /** * Handle AJAX request to dismiss the onboarding banner. * * @since 1.1.0 */ public function ajax_dismiss_onboarding(): void { check_ajax_referer( 'activelayer_admin', 'nonce' ); if ( ! current_user_can( 'manage_activelayer' ) ) { wp_send_json_error( [ 'message' => esc_html__( 'Permission denied.', 'activelayer-anti-spam-spam-protection-for-forms-comments' ) ], 403 ); } $manager = new OnboardingManager(); $manager->dismiss(); wp_send_json_success(); } /** * Handle AJAX request to dismiss the opt-out announce notice. * * @since 1.3.0 */ public function ajax_dismiss_opt_out_announce(): void { check_ajax_referer( 'activelayer_admin', 'nonce' ); if ( ! current_user_can( 'manage_activelayer' ) ) { wp_send_json_error( [ 'message' => esc_html__( 'Permission denied.', 'activelayer-anti-spam-spam-protection-for-forms-comments' ) ], 403 ); } delete_option( UpgradeRunner::OPTION_ANNOUNCE_REQUIRED ); wp_send_json_success(); } /** * Render dashboard page. * * @since 1.1.0 */ public function render_dashboard_page(): void { $this->dashboard_page->render(); } /** * Render integrations page. * * @since 1.1.0 */ public function render_integrations_page(): void { $this->integrations_page->render(); } /** * Render submissions page. * * @since 1.0.0 */ public function render_submissions_page(): void { $this->submissions_page->render(); } /** * Render settings page. * * @since 1.0.0 */ public function render_settings_page(): void { $this->settings_page->render(); } /** * Render logs page. * * @since 1.0.0 */ public function render_logs_page(): void { $this->logs_page->render(); } /** * Render tools page. * * @since 1.0.0 */ public function render_tools_page(): void { $this->tools_page->render(); } }