PluginProbe
Forumax – AI Powered Advanced Community Forum Plugin / 2.5.0
Forumax – AI Powered Advanced Community Forum Plugin v2.5.0
2.5.0 2.4.4 2.4.3 2.4.2 2.4.1 2.4.0 trunk 1.0.8 1.1.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.4.1 2.0.0 All 30 releases
← All changes | includes/admin/Menu.php +397 -23 1.2.1 → 2.5.0 View file →
@@ -1,49 +1,423 @@
1 1 <?php
2 +
2 3 namespace admin;
3 4
5 +/**
6 + * Handle Forumax Admin Menu logic.
7 + */
4 8 class Menu {
5 - function __construct() {
6 - add_action( 'admin_menu', [ $this, 'bbpc_admin_menu' ] );
9 +
10 + public function __construct() {
11 + add_filter( 'bbp_register_forum_post_type', [ $this, 'hide_bbp_cpt_top_level_menu' ] );
12 + add_filter( 'bbp_register_topic_post_type', [ $this, 'hide_bbp_cpt_top_level_menu' ] );
13 + add_filter( 'bbp_register_reply_post_type', [ $this, 'hide_bbp_cpt_top_level_menu' ] );
14 +
15 + // Use priority 99 to ensure we run after other plugins (like bbPress) to hide their menus if needed.
16 + add_action( 'admin_menu', [ $this, 'forumax_admin_menu' ], 99 );
17 + add_action( 'admin_menu', [ $this, 'reorder_forumax_submenu' ], 999 );
18 + add_filter( 'parent_file', [ $this, 'fix_parent_active_state' ] );
19 + add_filter( 'submenu_file', [ $this, 'fix_submenu_active_state' ] );
20 +
21 + // Enqueue admin assets specifically for our menu styling
22 + add_filter( 'admin_body_class', [ $this, 'add_admin_body_class' ] );
23 +
24 + // Setup wizard AJAX save handler
25 + add_action( 'wp_ajax_forumax_save_wizard_settings', [ $this, 'ajax_save_wizard_settings' ] );
7 26 }
8 27
9 28 /**
10 - * Create Admin menu
29 + * Add a specific class to the admin body to target our sidebar styling reliably.
30 + */
31 + public function add_admin_body_class( $classes ) {
32 + $screen = get_current_screen();
33 + if ( $screen && ( 'forumax' === $screen->parent_base || strpos( $screen->id, 'forumax' ) !== false ) ) {
34 + $classes .= ' forumax-admin-page';
35 + }
36 + return $classes;
37 + }
38 +
39 + /**
40 + * Hide bbPress CPT top-level menus so Forumax can add them explicitly.
11 41 *
12 - * @return void
42 + * @param array $args Post type registration arguments.
43 + *
44 + * @return array
13 45 */
14 - public function bbpc_admin_menu() {
46 + public function hide_bbp_cpt_top_level_menu( $args ) {
47 + return $this->set_cpt_menu_parent( $args );
48 + }
49 +
50 + /**
51 + * Register the Forumax Admin Menu.
52 + */
53 + public function forumax_admin_menu() {
15 54 $capability = 'manage_options';
55 + $show_classic_post_types = (bool) forumax_get_opt( 'is_bbp_post_types_hidden', false );
16 56
17 - add_menu_page( __( 'BBP Core', 'bbp-core' ), __( 'BBP Core', 'bbp-core' ), $capability, 'bbp-core', [ $this, 'bbpc_plugin_page' ], 'dashicons-buddicons-bbpress-logo', 20 );
18 - add_submenu_page( 'bbp-core', __( 'Forums', 'bbp-core' ), __( 'Forums', 'bbp-core' ), $capability, 'bbp-core' );
19 - // add_submenu_page( 'bbp-core', __( 'BBP Core Dashboard', 'bbp-core' ), __( 'Dashboard', 'bbp-core' ), $capability, 'admin.php?page=bbp-core-dashboard', [ $this, 'bbpc_statistics_dashboard' ] );
57 + // 1. Root Menu
58 + add_menu_page(
59 + esc_html__( 'Forumax', 'forumax' ),
60 + esc_html__( 'Forumax', 'forumax' ),
61 + $capability,
62 + 'forumax',
63 + [ $this, 'forumax_dashboard_page' ],
64 + 'dashicons-groups',
65 + 6
66 + );
20 67
21 - // Remove menu items.
22 - $opt = get_option( 'bbp_core_settings' );
68 + // 2. Dashboard & Builder
69 + add_submenu_page(
70 + 'forumax',
71 + esc_html__( 'Dashboard', 'forumax' ),
72 + esc_html__( 'Dashboard', 'forumax' ),
73 + $capability,
74 + 'forumax',
75 + [ $this, 'forumax_dashboard_page' ]
76 + );
23 77
24 - if ( $opt['is_bbp_post_types_hidden'] ?? false ) {
25 - remove_menu_page( 'edit.php?post_type=forum' );
26 - remove_menu_page( 'edit.php?post_type=topic' );
27 - remove_menu_page( 'edit.php?post_type=reply' );
78 + add_submenu_page(
79 + 'forumax',
80 + esc_html__( 'Forum Builder', 'forumax' ),
81 + esc_html__( 'Forum Builder', 'forumax' ),
82 + $capability,
83 + 'forumax-builder',
84 + [ $this, 'forumax_plugin_page' ]
85 + );
86 +
87 + // CPT top-level entries are suppressed via the bbp_register_*_post_type filters
88 + // in __construct() (show_in_menu = false), so no removal call is needed here.
89 +
90 + if ( $show_classic_post_types ) {
91 + // --- SEPARATOR 1 (Before Content Management) ---
92 + $this->add_frmx_separator( '1' );
93 +
94 + // 3. Post Type utility links.
95 + $cpts = [
96 + 'forum' => esc_html__( 'Forums', 'forumax' ),
97 + 'topic' => esc_html__( 'Topics', 'forumax' ),
98 + 'reply' => esc_html__( 'Replies', 'forumax' ),
99 + ];
100 +
101 + foreach ( $cpts as $pt => $label ) {
102 + $post_type_object = get_post_type_object( $pt );
103 +
104 + if ( empty( $post_type_object ) ) {
105 + continue;
106 + }
107 +
108 + $edit_cap = $post_type_object->cap->edit_posts;
109 + $create_cap = isset( $post_type_object->cap->create_posts ) ? $post_type_object->cap->create_posts : $edit_cap;
110 +
111 + // Top item: the CPT list page (e.g. edit.php?post_type=forum).
112 + add_submenu_page(
113 + 'forumax',
114 + $label,
115 + $label,
116 + $edit_cap,
117 + "edit.php?post_type={$pt}"
118 + );
119 +
120 + // Indented child: Add New.
121 + add_submenu_page(
122 + 'forumax',
123 + esc_html__( 'Add New ', 'forumax' ) . $label,
124 + '&nbsp;&nbsp;&nbsp; ' . esc_html__( 'Add New', 'forumax' ),
125 + $create_cap,
126 + "post-new.php?post_type={$pt}&is_child=true"
127 + );
128 +
129 + // Indented children: taxonomy pages (e.g. topic tags).
130 + $taxs = get_object_taxonomies( $pt, 'objects' );
131 + foreach ( $taxs as $t_slug => $t_obj ) {
132 + if ( ! $t_obj->show_ui ) {
133 + continue;
134 + }
135 + add_submenu_page(
136 + 'forumax',
137 + esc_html( $t_obj->labels->name ),
138 + '&nbsp;&nbsp;&nbsp; ' . esc_html( $t_obj->labels->name ),
139 + $t_obj->cap->manage_terms,
140 + "edit-tags.php?taxonomy={$t_slug}&post_type={$pt}&is_child=true"
141 + );
142 + }
143 +
144 + // Separator after each CPT block except the last (Separator 2 closes the section).
145 + if ( 'reply' !== $pt ) {
146 + $this->add_frmx_separator( "after-{$pt}" );
147 + }
148 + }
149 +
150 + // --- SEPARATOR 2 (Before Utilities) ---
151 + $this->add_frmx_separator( '2' );
28 152 }
153 +
154 + // 4. Utility Pages
155 + add_submenu_page(
156 + 'forumax',
157 + esc_html__( 'Setup Wizard', 'forumax' ),
158 + esc_html__( 'Setup Wizard', 'forumax' ),
159 + $capability,
160 + 'forumax-setup',
161 + [ $this, 'forumax_setup_wizard_page' ]
162 + );
163 +
164 + remove_submenu_page( 'forumax', 'forumax-settings' );
165 +
166 + add_submenu_page(
167 + 'forumax',
168 + esc_html__( 'Settings', 'forumax' ),
169 + esc_html__( 'Settings', 'forumax' ),
170 + $capability,
171 + 'forumax-settings',
172 + [ $this, 'forumax_settings_page' ]
173 + );
174 +
175 + if ( ! $this->has_premium_analytics_page() ) {
176 + add_submenu_page(
177 + 'forumax',
178 + esc_html__( 'Analytics', 'forumax' ),
179 + esc_html__( 'Analytics', 'forumax' ),
180 + $capability,
181 + 'forumax-analytics',
182 + [ $this, 'forumax_analytics_page' ]
183 + );
184 + }
185 +
186 + // --- SEPARATOR 3 (Before Account Section added by others) ---
187 + $this->add_frmx_separator( '3' );
29 188 }
30 189
31 190 /**
32 - * Plugin page callback function.
33 - *
34 - * @return void
191 + * Reorder the Forumax submenu to ensure Settings and others are correctly placed.
35 192 */
36 - public function bbpc_plugin_page() {
37 - $opt = get_option( 'bbp_core_settings' );
193 + public function reorder_forumax_submenu() {
194 + global $submenu;
195 +
196 + if ( ! isset( $submenu['forumax'] ) || ! is_array( $submenu['forumax'] ) ) {
197 + return;
198 + }
199 +
200 + $current_menu = $submenu['forumax'];
201 +
202 + $weights = [
203 + 'forumax' => 1000,
204 + 'forumax-builder' => 2000,
205 + 'frmx-menu-sep-1' => 3000,
206 + ];
207 +
208 + $weighted_items = [];
209 +
210 + foreach ( $current_menu as $key => $item ) {
211 + $slug = isset( $item[2] ) ? $item[2] : '';
212 + $weight = 10000; // Default for "others"
213 +
214 + if ( isset( $weights[$slug] ) ) {
215 + $weight = $weights[$slug];
216 + } elseif ( strpos( $slug, 'post_type=forum' ) !== false ) {
217 + $weight = 4000;
218 + if ( strpos( $slug, 'post-new.php' ) !== false ) $weight = 4100;
219 + elseif ( strpos( $slug, 'edit-tags.php' ) !== false ) $weight = 4200;
220 + } elseif ( $slug === 'frmx-menu-sep-after-forum' ) {
221 + $weight = 4900;
222 + } elseif ( strpos( $slug, 'post_type=topic' ) !== false ) {
223 + $weight = 5000;
224 + if ( strpos( $slug, 'post-new.php' ) !== false ) $weight = 5100;
225 + elseif ( strpos( $slug, 'edit-tags.php' ) !== false ) $weight = 5200;
226 + } elseif ( $slug === 'frmx-menu-sep-after-topic' ) {
227 + $weight = 5900;
228 + } elseif ( strpos( $slug, 'post_type=reply' ) !== false ) {
229 + $weight = 6000;
230 + if ( strpos( $slug, 'post-new.php' ) !== false ) $weight = 6100;
231 + elseif ( strpos( $slug, 'edit-tags.php' ) !== false ) $weight = 6200;
232 + } else {
233 + // Other knowns
234 + if ( $slug === 'frmx-menu-sep-2' ) $weight = 7000;
235 + elseif ( $slug === 'forumax-setup' ) $weight = 8000;
236 + elseif ( $slug === 'forumax-settings' ) $weight = 9000;
237 + // others default to 10000
238 + elseif ( $slug === 'forumax-analytics' ) $weight = 11000;
239 + elseif ( $slug === 'frmx-menu-sep-3' ) $weight = 12000;
240 + }
241 +
242 + // Handle duplicate weights by incrementing
243 + while ( isset( $weighted_items[$weight] ) ) {
244 + $weight++;
245 + }
246 + $weighted_items[$weight] = $item;
247 + }
248 +
249 + ksort( $weighted_items );
250 + $submenu['forumax'] = $weighted_items;
251 + }
252 +
253 + /**
254 + * Matches sidebar logic for parent menu expansion.
255 + */
256 + public function fix_parent_active_state( $parent_file ) {
257 + global $current_screen;
258 + $bbp_types = [ 'forum', 'topic', 'reply' ];
259 + if ( $current_screen && in_array( $current_screen->post_type, $bbp_types, true ) ) {
260 + return 'forumax';
261 + }
262 + return $parent_file;
263 + }
264 +
265 + /**
266 + * Matches sidebar logic for specific submenu highlighting.
267 + */
268 + public function fix_submenu_active_state( $submenu_file ) {
269 + global $current_screen, $pagenow;
270 + $bbp_types = [ 'forum', 'topic', 'reply' ];
271 + $post_type = $current_screen ? $current_screen->post_type : '';
272 +
273 + if ( in_array( $post_type, $bbp_types, true ) ) {
274 + if ( 'post-new.php' === $pagenow ) {
275 + return "post-new.php?post_type={$post_type}&is_child=true";
276 + }
277 + if ( $current_screen->taxonomy ) {
278 + return "edit-tags.php?taxonomy={$current_screen->taxonomy}&post_type={$post_type}&is_child=true";
279 + }
280 + return "edit.php?post_type={$post_type}";
281 + }
282 + return $submenu_file;
283 + }
284 +
285 + /**
286 + * Helpers & Callbacks
287 + */
288 + private function set_cpt_menu_parent( array $args ) : array {
289 + // Prevent WordPress from auto-generating a top-level or parented menu entry
290 + // for this CPT. Forumax adds explicit submenu links in forumax_admin_menu().
291 + $args['show_in_menu'] = false;
292 +
293 + return $args;
294 + }
295 +
296 + private function add_frmx_separator( $id ) {
297 + add_submenu_page(
298 + 'forumax',
299 + '',
300 + '<span class="frmx-menu-separator" aria-hidden="true"></span>',
301 + 'manage_options',
302 + 'frmx-menu-sep-' . $id
303 + );
304 + }
305 +
306 + public function forumax_plugin_page() {
38 307 include plugin_dir_path( __FILE__ ) . '/menu/admin_ui.php';
39 308 }
40 309
310 + public function forumax_dashboard_page() {
311 + include plugin_dir_path( __FILE__ ) . '/menu/dashboard.php';
312 + }
313 +
314 + public function forumax_setup_wizard_page() {
315 + include plugin_dir_path( __FILE__ ) . '/menu/setup-wizard.php';
316 + }
317 +
41 318 /**
42 - * Dashboard Statistics.
319 + * AJAX handler — save all wizard settings.
43 320 *
44 - * @return void
321 + * Accepts POST data from the setup wizard JS, validates input,
322 + * and persists the values as WordPress options.
323 + *
324 + * Flow: nonce → capability → sanitize → save → respond.
45 325 */
46 - public function bbpc_statistics_dashboard() {
47 - include plugin_dir_path( __FILE__ ) . '/menu/statistics.php';
326 + public function ajax_save_wizard_settings() {
327 + if ( ! check_ajax_referer( 'forumax-wizard-nonce', 'nonce', false ) ) {
328 + wp_send_json_error( [ 'message' => __( 'Invalid security token.', 'forumax' ) ] );
329 + }
330 +
331 + if ( ! current_user_can( 'manage_options' ) ) {
332 + wp_send_json_error( [ 'message' => __( 'You do not have permission to save these settings.', 'forumax' ) ] );
333 + }
334 +
335 + // ── Sanitize & save each field ──────────────────────────────────
336 + $post = $_POST; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified above
337 +
338 + // Text fields: forum title + description.
339 + $text_fields = [ 'create_forum', 'forum_description' ];
340 + foreach ( $text_fields as $field ) {
341 + if ( isset( $post[ $field ] ) ) {
342 + update_option(
343 + 'forumax_wizard_' . $field,
344 + 'forum_description' === $field
345 + ? sanitize_textarea_field( $post[ $field ] )
346 + : sanitize_text_field( $post[ $field ] ),
347 + false
348 + );
349 + }
350 + }
351 +
352 + // Boolean fields: auto-create pages.
353 + $bool_fields = [ 'create_pages' ];
354 + foreach ( $bool_fields as $field ) {
355 + update_option( 'forumax_wizard_' . $field, isset( $post[ $field ] ) && '1' === $post[ $field ] ? '1' : '0', false );
356 + }
357 +
358 + // Radio fields with allowlists.
359 + $allowed_setup_types = [ 'single', 'sample', 'skip' ];
360 + $allowed_layout_styles = [ 'classic', 'modern', 'card' ];
361 +
362 + if ( isset( $post['setup_type'] ) && in_array( $post['setup_type'], $allowed_setup_types, true ) ) {
363 + update_option( 'forumax_wizard_setup_type', sanitize_key( $post['setup_type'] ), false );
364 + }
365 +
366 + if ( isset( $post['layout_style'] ) && in_array( $post['layout_style'], $allowed_layout_styles, true ) ) {
367 + update_option( 'forumax_wizard_layout_style', sanitize_key( $post['layout_style'] ), false );
368 + }
369 +
370 + // Brand color — save to wizard option AND the real CSF settings for immediate effect.
371 + if ( ! empty( $post['brand_color'] ) ) {
372 + $brand_color = sanitize_hex_color( $post['brand_color'] );
373 + if ( $brand_color ) {
374 + update_option( 'forumax_wizard_brand_color', $brand_color, false );
375 +
376 + // Write directly to CSF option so the frontend picks it up immediately.
377 + $csf_options = get_option( 'bbp_core_settings', [] );
378 + if ( ! is_array( $csf_options ) ) {
379 + $csf_options = [];
380 + }
381 + $csf_options['bbpc_brand_color'] = $brand_color;
382 + update_option( 'bbp_core_settings', $csf_options );
383 + }
384 + }
385 +
386 + // Mark wizard as completed so it can be skipped on future visits.
387 + update_option( 'forumax_wizard_completed', '1', false );
388 +
389 + wp_send_json_success( [ 'message' => __( 'Settings saved.', 'forumax' ) ] );
390 + }
391 +
392 + public function forumax_settings_page() {
393 + if ( ! class_exists( '\CSF_Options' ) || ! class_exists( '\CSF' ) ) {
394 + wp_die( esc_html__( 'Forumax settings could not be loaded.', 'forumax' ) );
395 + }
396 +
397 + if ( empty( \CSF::$args['admin_options']['bbp_core_settings'] ) || empty( \CSF::$args['sections']['bbp_core_settings'] ) ) {
398 + wp_die( esc_html__( 'Forumax settings are not registered.', 'forumax' ) );
399 + }
400 +
401 + static $settings_renderer = null;
402 +
403 + if ( null === $settings_renderer ) {
404 + $settings_renderer = \CSF_Options::instance(
405 + 'bbp_core_settings',
406 + [
407 + 'args' => \CSF::$args['admin_options']['bbp_core_settings'],
408 + 'sections' => \CSF::$args['sections']['bbp_core_settings'],
409 + ]
410 + );
411 + }
412 +
413 + $settings_renderer->add_options_html();
414 + }
415 +
416 + public function forumax_analytics_page() {
417 + include plugin_dir_path( __FILE__ ) . '/menu/analytics.php';
418 + }
419 +
420 + private function has_premium_analytics_page() {
421 + return defined( 'BBPCPRO_PATH' ) || class_exists( '\Forumax_Pro\Admin\Analytics' );
48 422 }
49 423 }