init_components(); $this->setup_hooks(); } /** @ignore */ private function __clone() {} /** @ignore */ public function __wakeup() {} // ------------------------------------------------------------------------- // Initialisation // ------------------------------------------------------------------------- /** * Instantiate all plugin sub-components. * * @since 1.0.0 */ private function init_components(): void { $this->settings_manager = AAMO_Notification_Groups_Settings_Manager::get_instance(); $this->notice_collector = AAMO_Notification_Groups_Notice_Collector::get_instance(); } /** * Register all WordPress hooks. * * Note: the activation hook is registered in the main plugin file * (aamo-notification-groups.php) rather than here, because it must be * called at file-load time — before plugins_loaded fires — for correct * behaviour during a first activation. * * @since 1.0.0 */ private function setup_hooks(): void { add_action( 'admin_menu', array( $this, 'register_admin_menu' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); add_action( 'admin_notices', array( $this, 'maybe_render_demo_notices' ) ); add_action( 'admin_notices', array( $this, 'maybe_render_settings_saved_notice' ) ); add_action( 'admin_post_' . AAMO_Notification_Groups_Constants::SAVE_SETTINGS_ACTION, array( $this, 'handle_save_settings' ) ); } // ------------------------------------------------------------------------- // Admin page // ------------------------------------------------------------------------- /** * Register the plugin's admin page as a submenu under Settings. * * @since 1.0.0 */ public function register_admin_menu(): void { $capability = AAMO_Notification_Groups_Constants::CAP_MANAGE_NOTIFICATIONS; $this->main_hook = add_submenu_page( 'options-general.php', __( 'Notification Groups', 'aamo-notification-groups' ), __( 'Notification Groups', 'aamo-notification-groups' ), $capability, AAMO_Notification_Groups_Constants::PAGE_MAIN, array( $this, 'render_main_page' ) ); } /** * Render the main admin page. * * @since 1.0.0 */ public function render_main_page(): void { if ( ! current_user_can( AAMO_Notification_Groups_Constants::CAP_MANAGE_NOTIFICATIONS ) ) { wp_die( esc_html__( 'You do not have permission to access this page.', 'aamo-notification-groups' ) ); } include AAMO_NOTIFICATION_GROUPS_DIR . 'templates/admin-page.php'; } // ------------------------------------------------------------------------- // Demo notices // ------------------------------------------------------------------------- /** * Output a one-off batch of fake notices covering every severity tab, * but only when this plugin's own "Generate Demo Notifications" link * was just clicked — verified via nonce, and scoped to this plugin's * own admin page. Nothing is persisted; a normal visit to this page * never shows these. * * @since 1.0.0 */ public function maybe_render_demo_notices(): void { if ( empty( $_GET[ AAMO_Notification_Groups_Constants::DEMO_QUERY_ARG ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- presence check only; verified below. return; } if ( ! $this->main_hook || ! get_current_screen() || get_current_screen()->id !== $this->main_hook ) { return; } if ( ! current_user_can( AAMO_Notification_Groups_Constants::CAP_MANAGE_NOTIFICATIONS ) ) { return; } check_admin_referer( AAMO_Notification_Groups_Constants::DEMO_NONCE_ACTION ); $this->render_demo_notices(); } /** * Echo the demo notice battery: two each of error/warning/success/info * (one success notice deliberately uses the legacy `.updated` class * instead of `notice-success`), plus one bare `.notice` with no * severity class so the Misc tab has something to show too. * * @since 1.0.0 */ private function render_demo_notices(): void { $demo_notices = array( array( 'class' => 'notice notice-error is-dismissible', 'message' => __( 'Demo: payment gateway failed to connect.', 'aamo-notification-groups' ), ), array( 'class' => 'notice notice-error', 'message' => __( 'Demo: a required field is missing on the last saved record.', 'aamo-notification-groups' ), ), array( 'class' => 'notice notice-warning', 'message' => __( 'Demo: a scheduled task did not run on time.', 'aamo-notification-groups' ), ), array( 'class' => 'notice notice-warning', 'message' => __( 'Demo: storage usage is approaching its limit.', 'aamo-notification-groups' ), ), array( 'class' => 'notice notice-success is-dismissible', 'message' => __( 'Demo: settings saved successfully.', 'aamo-notification-groups' ), ), array( 'class' => 'updated', 'message' => __( 'Demo: import completed (legacy .updated notice style).', 'aamo-notification-groups' ), ), array( 'class' => 'notice notice-info', 'message' => __( 'Demo: a new version of a dependency is available.', 'aamo-notification-groups' ), ), array( 'class' => 'notice notice-info', 'message' => __( 'Demo: this feature is in beta.', 'aamo-notification-groups' ), ), array( 'class' => 'notice', 'message' => __( 'Demo: uncategorized notice with no severity class — lands in Misc.', 'aamo-notification-groups' ), ), ); foreach ( $demo_notices as $notice ) { printf( '

%2$s

', esc_attr( $notice['class'] ), esc_html( $notice['message'] ) ); } } // ------------------------------------------------------------------------- // Settings (display style + minimum group count) // ------------------------------------------------------------------------- /** * Handle the settings form submission (display style + minimum group * count). Capability + nonce checked, saves both posted values, then * redirects back to the settings page with a flag that shows a real * success notice — standard WordPress settings-saved pattern. * * @since 1.0.0 */ public function handle_save_settings(): void { if ( ! current_user_can( AAMO_Notification_Groups_Constants::CAP_MANAGE_NOTIFICATIONS ) ) { wp_die( esc_html__( 'You do not have permission to do that.', 'aamo-notification-groups' ) ); } check_admin_referer( AAMO_Notification_Groups_Constants::SAVE_SETTINGS_ACTION ); // Cast to string before sanitize_key(): defensive only, not a fix for // an actual error — a capability-checked, nonce-verified admin could // still hand-craft this field as an array, which sanitize_key() // cannot accept. absint() handles the same edge case for the count // field on its own (and is what marks it "sanitized" to WPCS). $style = isset( $_POST['aamo_ng_display_style'] ) ? sanitize_key( (string) wp_unslash( $_POST['aamo_ng_display_style'] ) ) : ''; $this->settings_manager->save_display_style( $style ); $min_group_count = isset( $_POST['aamo_ng_min_group_count'] ) ? absint( wp_unslash( $_POST['aamo_ng_min_group_count'] ) ) : 0; $this->settings_manager->save_min_group_count( $min_group_count ); $collapsed_by_default = ! empty( $_POST['aamo_ng_collapsed_by_default'] ); $this->settings_manager->save_collapsed_by_default( $collapsed_by_default ); // Built directly rather than via menu_page_url(): that helper reads // the $_parent_pages global that add_submenu_page() populates on the // admin_menu hook — but admin_menu never fires during an // admin-post.php request, so menu_page_url() would return an empty // string here and add_query_arg() would silently fall back to the // current request URI (admin-post.php itself) as the base, // redirecting back into admin-post.php instead of the settings page. $redirect = add_query_arg( array( 'page' => AAMO_Notification_Groups_Constants::PAGE_MAIN, AAMO_Notification_Groups_Constants::SETTINGS_SAVED_QUERY_ARG => '1', ), admin_url( 'options-general.php' ) ); wp_safe_redirect( $redirect ); exit; } /** * Show a real success notice after a settings save — no nonce needed * here, it's a read-only flag on the already-verified save handler's * redirect target, the same pattern WordPress core's own "Settings * saved." notices use. * * Deliberately carries the `inline` class so neither WordPress core's * own notice-relocation shim nor our own Notice Collector sweep it up * — both already skip `.inline` notices by convention (see * NOTICE_EXCLUDE_CLASSES). Saving a setting shouldn't itself look like * a demo of the collector card; that's what the explicit Demo button * is for. * * @since 1.0.0 */ public function maybe_render_settings_saved_notice(): void { if ( empty( $_GET[ AAMO_Notification_Groups_Constants::SETTINGS_SAVED_QUERY_ARG ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display only, nothing is mutated here. return; } if ( ! $this->main_hook || ! get_current_screen() || get_current_screen()->id !== $this->main_hook ) { return; } if ( ! current_user_can( AAMO_Notification_Groups_Constants::CAP_MANAGE_NOTIFICATIONS ) ) { return; } printf( '

%s

', esc_html__( 'Settings saved.', 'aamo-notification-groups' ) ); } // ------------------------------------------------------------------------- // Assets // ------------------------------------------------------------------------- /** * Enqueue admin assets, scoped to the plugin's own page only. * * @since 1.0.0 * @param string $hook_suffix Current admin page hook suffix. */ public function enqueue_assets( string $hook_suffix ): void { if ( ! $this->main_hook || $hook_suffix !== $this->main_hook ) { return; } wp_enqueue_style( AAMO_Notification_Groups_Constants::ASSET_HANDLE_CSS, AAMO_NOTIFICATION_GROUPS_URL . 'assets/css/admin.css', array(), AAMO_NOTIFICATION_GROUPS_VERSION ); } // ------------------------------------------------------------------------- // Utilities // ------------------------------------------------------------------------- /** * Return the plugin version string. * * @since 1.0.0 * @return string */ public function get_version(): string { return AAMO_NOTIFICATION_GROUPS_VERSION; } }