PluginProbe
Forumax – AI Powered Advanced Community Forum Plugin / 2.4.2
Forumax – AI Powered Advanced Community Forum Plugin v2.4.2
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 2.1.0 All 29 releases
bbp-core / includes / functions.php

functions.php in Forumax – AI Powered Advanced Community Forum Plugin 2.4.2, at includes/functions.php

950 lines 27.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Get the value of a settings field.
4 *
5 * @param string $option settings field name
6 * @param string $section the section name this field belongs to
7 * @param string $default default text if it's not found
8 *
9 * @return mixed
10 */
11 function forumax_get_opt( $option, $default = '' ) {
12 $options = get_option( 'bbp_core_settings' );
13
14 if ( isset( $options[ $option ] ) ) {
15 return $options[ $option ];
16 }
17
18 return $default;
19 }
20
21 /**
22 * Check if a plugin has been installed for specific number of days
23 *
24 * @param string $plugin_path The plugin path (e.g. 'woocommerce/woocommerce.php')
25 * @param int $days Number of days to check against
26 * @return bool True if plugin is installed for specified days, false otherwise
27 */
28 function forumax_is_plugin_installed_for_days( $days, $plugin_slug = 'bbpc' ) {
29 // Get the installation timestamp of the plugin
30 $installed_time = get_option( $plugin_slug . '_installed' );
31
32 // Ensure it's a valid timestamp
33 if ( ! is_numeric( $installed_time ) || $installed_time <= 0 ) {
34 return false;
35 }
36
37 // Convert days to seconds
38 $required_time = (int) $days * DAY_IN_SECONDS;
39
40 // Get the current UTC time
41 $current_time = time();
42
43 // Check if the plugin has been installed for the required duration
44 return ( $current_time - $installed_time ) >= $required_time;
45 }
46
47 /**
48 * Check If the Page is Forum page
49 */
50 function forumax_is_forum_page() {
51 if ( in_array( 'bbpress', get_body_class(), true ) ) {
52 return true;
53 }
54 return false;
55 }
56
57 /**
58 * Add a stable body class for all Forumax/bbPress related pages.
59 *
60 * This helps themes (e.g. Hello Biz) and custom CSS target Forumax pages
61 * reliably, even when the page uses shortcodes/blocks instead of bbPress
62 * native endpoints.
63 *
64 * @param array $classes Existing body classes.
65 * @return array
66 */
67 function forumax_add_forumax_body_class( $classes ) {
68 if ( is_admin() ) {
69 return $classes;
70 }
71
72 $should_add = false;
73
74 // Native bbPress pages (forums, topics, replies, search, user profiles, etc.).
75 if ( function_exists( 'is_bbpress' ) && is_bbpress() ) {
76 $should_add = true;
77 }
78
79 // Some installs load forum content via shortcodes/blocks on normal pages.
80 if ( ! $should_add ) {
81 $post = get_post();
82 if ( $post instanceof WP_Post ) {
83 $shortcodes = array(
84 // bbPress shortcodes.
85 'bbp-forum-index',
86 'bbp-forum-form',
87 'bbp-single-forum',
88 'bbp-topic-index',
89 'bbp-topic-form',
90 'bbp-single-topic',
91 'bbp-reply-form',
92 'bbp-single-reply',
93 'bbp-single-view',
94 'bbp-search-form',
95 'bbp-search',
96 'bbp-login',
97 'bbp-register',
98 'bbp-lost-pass',
99
100 // Forumax shortcodes.
101 'forumax_login_form',
102 'forumax_chat',
103 );
104
105 foreach ( $shortcodes as $shortcode ) {
106 if ( has_shortcode( $post->post_content, $shortcode ) ) {
107 $should_add = true;
108 break;
109 }
110 }
111
112 // Also check for bbPress/Forumax blocks.
113 if ( ! $should_add && function_exists( 'has_block' ) ) {
114 $blocks = array(
115 'bbpress/forum-index',
116 'bbpress/topic-index',
117 'forumax/forums',
118 );
119
120 foreach ( $blocks as $block ) {
121 if ( has_block( $block, $post ) ) {
122 $should_add = true;
123 break;
124 }
125 }
126 }
127 }
128 }
129
130 if ( $should_add && ! in_array( 'forumax-body', $classes, true ) ) {
131 $classes[] = 'forumax-body';
132 }
133
134 return $classes;
135 }
136 add_filter( 'body_class', 'forumax_add_forumax_body_class', 20 );
137
138 /**
139 * Get hashed IP address for anonymous user identification.
140 *
141 * This function creates a one-way hash of the user's IP address,
142 * making it impossible to recover the original IP while still
143 * allowing duplicate detection (same IP = same hash).
144 *
145 * GDPR Compliant: Real IP addresses are never stored in the database.
146 *
147 * @since 2.3.1
148 *
149 * @return string Hashed IP address (64 characters)
150 */
151 function forumax_get_hashed_ip() {
152 $ip = '';
153
154 // Get the real IP address (handles proxies and load balancers)
155 if ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) {
156 $ip = $_SERVER['HTTP_CLIENT_IP'];
157 } elseif ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
158 // Can contain multiple IPs, get the first one
159 $ip_list = explode( ',', $_SERVER['HTTP_X_FORWARDED_FOR'] );
160 $ip = trim( $ip_list[0] );
161 } elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
162 $ip = $_SERVER['REMOTE_ADDR'];
163 }
164
165 // Sanitize the IP
166 $ip = filter_var( $ip, FILTER_VALIDATE_IP ) ? $ip : 'unknown';
167
168 // Use WordPress salt for extra security (unique per site)
169 $salt = defined( 'NONCE_SALT' ) ? NONCE_SALT : 'forumax_default_salt_key';
170
171 // Create one-way hash - cannot be reversed to get original IP
172 return hash( 'sha256', $ip . $salt );
173 }
174
175 /**
176 * Get raw IP address for backward compatibility check only.
177 *
178 * WARNING: This function is used ONLY to check for legacy vote entries.
179 * The raw IP is NEVER stored - it's immediately compared against legacy
180 * data and then discarded. New votes always use forumax_get_hashed_ip().
181 *
182 * @since 2.3.1
183 * @access private
184 *
185 * @return string|false Raw IP address or false if invalid
186 */
187 function forumax_get_raw_ip() {
188 $ip = '';
189
190 if ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) {
191 $ip = $_SERVER['HTTP_CLIENT_IP'];
192 } elseif ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) {
193 $ip_list = explode( ',', $_SERVER['HTTP_X_FORWARDED_FOR'] );
194 $ip = trim( $ip_list[0] );
195 } elseif ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
196 $ip = $_SERVER['REMOTE_ADDR'];
197 }
198
199 return filter_var( $ip, FILTER_VALIDATE_IP ) ? $ip : false;
200 }
201
202 /**
203 * Turn on the WordPress visual editor for bbPress
204 *
205 * @param array $args
206 * @return array
207 */
208 function forumax_bbp_enable_visual_editor( $args = [] ) {
209 $args['tinymce'] = true;
210 $args['default_editor'] = 'tinymce';
211 return $args;
212 }
213 add_filter( 'bbp_after_get_the_content_parse_args', 'forumax_bbp_enable_visual_editor' );
214
215 /**
216 * Sanitize topic and reply content to prevent raw HTML display.
217 *
218 * When topics/replies are submitted from the frontend, bbPress runs
219 * `bbp_encode_bad` which entity-encodes HTML tags (e.g. `<p>` becomes
220 * `&lt;p&gt;`). This causes HTML tags to display as visible plain text
221 * instead of being rendered by the browser.
222 *
223 * This filter detects entity-encoded HTML, decodes it back to real HTML,
224 * and sanitizes the output through `wp_kses_post()` to allow only safe
225 * post-level HTML tags (p, strong, em, a, br, etc.) while stripping
226 * dangerous elements like script or iframe.
227 *
228 * @since 2.2.2
229 *
230 * @param string $content The topic or reply content.
231 * @return string Sanitized content with proper HTML rendering.
232 */
233 function forumax_sanitize_block_content( $content ) {
234 // Bail early if content is empty.
235 if ( empty( $content ) ) {
236 return $content;
237 }
238
239 // Check if content contains entity-encoded HTML tags (e.g. &lt;p&gt;).
240 if ( strpos( $content, '&lt;' ) !== false ) {
241 // Decode entity-encoded HTML back to real HTML tags.
242 $content = html_entity_decode( $content, ENT_QUOTES, 'UTF-8' );
243
244 // Sanitize with wp_kses_post — allows safe HTML (p, strong, a, br, etc.)
245 // but strips dangerous tags (script, iframe, etc.).
246 $content = wp_kses_post( $content );
247 }
248
249 // Strip inline style attributes from all HTML tags.
250 $content = preg_replace( '/\s+style="[^"]*"/i', '', $content );
251
252 // Strip block-editor class attributes (e.g. wp-block-paragraph).
253 $content = preg_replace( '/\s+class="wp-block-[^"]*"/i', '', $content );
254
255 // Clean up any remaining empty class attributes.
256 $content = preg_replace( '/\s+class=""/i', '', $content );
257
258 return $content;
259 }
260 add_filter( 'bbp_get_topic_content', 'forumax_sanitize_block_content', 4 );
261 add_filter( 'bbp_get_reply_content', 'forumax_sanitize_block_content', 4 );
262
263 /**
264 * Check if the pro plugin and plan is active
265 *
266 * @return bool|void
267 */
268 function forumax_is_premium() {
269 if ( class_exists('Forumax_Pro') && bc_fs()->can_use_premium_code() ) {
270 return true;
271 }
272 }
273
274 /**
275 * Check if the promax plan is active
276 *
277 * @return bool|void
278 */
279 function forumax_is_promax() {
280 if ( class_exists('Forumax_Pro') && bc_fs()->can_use_premium_code() && bc_fs()->is_plan('promax') ) {
281 return true;
282 }
283 }
284
285 /**
286 * Forumax Admin pages
287 *
288 * Checks if the current admin page matches the specified Forumax page type.
289 *
290 * @param string $admin The admin page type to check ('admin', 'settings', 'dashboard').
291 *
292 * @return bool True if on the specified admin page, false otherwise.
293 */
294 function forumax_admin_pages( $admin ) {
295 $current_url = ! empty( $_GET['page'] ) ? admin_url( 'admin.php?page=' ) . sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
296
297 if ( 'admin' === $admin ) {
298 if ( admin_url( 'admin.php?page=forumax-builder' ) === $current_url ) {
299 return true;
300 }
301 } elseif ( 'settings' === $admin ) {
302 if ( admin_url( 'admin.php?page=forumax-settings' ) === $current_url ) {
303 return true;
304 }
305 } elseif ( 'dashboard' === $admin ) {
306 if ( admin_url( 'admin.php?page=forumax' ) === $current_url ) {
307 return true;
308 }
309 } elseif ( 'setup' === $admin ) {
310 if ( admin_url( 'admin.php?page=forumax-setup' ) === $current_url ) {
311 return true;
312 }
313 } elseif ( 'analytics' === $admin ) {
314 if ( admin_url( 'admin.php?page=forumax-analytics' ) === $current_url ) {
315 return true;
316 }
317 }
318
319 return false;
320 }
321
322
323 /**
324 * BBP Forum Assets
325 * Checks if the current page is a single forum or a single topic.
326 *
327 * @return bool True if the current page is a single forum or topic, false otherwise.
328 */
329 function forumax_forum_and_topic_page(){
330 if ( bbp_is_single_forum() || bbp_is_single_topic() || bbp_is_reply_edit()) {
331 return true;
332 }
333 }
334
335
336 /**
337 * Posts Arraty
338 * @param object Post Type
339 */
340 function forumax_get_posts( $post_type = 'forum' ) {
341 $posts = get_pages(
342 [
343 'post_type' => $post_type,
344 'parent' => 0,
345 ]
346 );
347
348 $posts_array = [];
349
350 if ( $posts ) {
351 foreach ( $posts as $post ) {
352 $posts_array[ $post->ID ] = $post->post_title;
353 }
354 }
355
356 return $posts_array;
357 }
358
359 /**
360 * Limit letter
361 * @param $string
362 * @param $limit_length
363 * @param string $suffix
364 */
365 function forumax_limit_letter( $string, $limit_length, $suffix = '...' ) {
366 if ( strlen( $string ) > $limit_length ) {
367 echo esc_html ( strip_shortcodes( substr( $string, 0, $limit_length ) . $suffix ) );
368 } else {
369 echo esc_html( $string );
370 }
371 }
372
373 /**
374 * Return the topic view count.
375 *
376 * @param int $topic_id Optional. Topic id
377 *
378 * @return int The view count
379 * @uses get_post_meta() To get the view count meta
380 * @uses bbp_get_topic_id() To get the topic id
381 */
382 function forumax_get_topic_view_count( $topic_id = 0 ) {
383 $topic_id = bbp_get_topic_id( $topic_id );
384
385 if ( empty( $topic_id ) ) {
386 return 0;
387 }
388
389 $views = (int) get_post_meta( $topic_id, '_btv_view_count', true );
390
391 return $views;
392 }
393
394 /**
395 * Output the topic view count.
396 *
397 * @param int $topic_id Optional. Topic id
398 *
399 * @uses bbp_get_topic_id() To get the topic id
400 * @uses btv_get_topic_view_count() To get the view count for the topic
401 */
402 function forumax_topic_view_count( $topic_id = 0 ) {
403 $topic_id = bbp_get_topic_id( $topic_id );
404 $view_count = forumax_get_topic_view_count( $topic_id );
405 return $view_count;
406 }
407
408 /**
409 * Increment the topic view count.
410 *
411 * Increments the view count when a visitor views a topic.
412 * Uses cookies to prevent duplicate counts within a session (1 hour).
413 *
414 * @param int $topic_id Topic ID.
415 * @return bool True if view was counted, false otherwise.
416 */
417 function forumax_increment_topic_view_count( $topic_id = 0 ) {
418 $topic_id = bbp_get_topic_id( $topic_id );
419
420 if ( empty( $topic_id ) ) {
421 return false;
422 }
423
424 // Optionally: Don't count views for logged-in admins/moderators
425 // Uncomment the following to exclude admin views from being counted
426 // if ( current_user_can( 'moderate' ) ) {
427 // return false;
428 // }
429
430 // Use a cookie to prevent duplicate counts within the same session
431 $cookie_name = 'forumax_viewed_' . $topic_id;
432
433 // Check if this topic was already viewed in this session
434 if ( isset( $_COOKIE[ $cookie_name ] ) ) {
435 return false;
436 }
437
438 // Get current view count
439 $current_views = (int) get_post_meta( $topic_id, '_btv_view_count', true );
440
441 // Increment the view count
442 $new_views = $current_views + 1;
443
444 // Update the view count
445 update_post_meta( $topic_id, '_btv_view_count', $new_views );
446
447 // Set cookie to prevent duplicate counting (expires in 1 hour)
448 setcookie( $cookie_name, '1', time() + HOUR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), true );
449
450 return true;
451 }
452
453 /**
454 * Track topic views on template redirect.
455 *
456 * Hooks into bbPress template redirect to track topic views
457 * when a single topic page is loaded.
458 */
459 function forumax_track_topic_views() {
460 // Only track on single topic pages
461 if ( ! function_exists( 'bbp_is_single_topic' ) || ! bbp_is_single_topic() ) {
462 return;
463 }
464
465 // Get the current topic ID
466 $topic_id = bbp_get_topic_id();
467
468 if ( ! empty( $topic_id ) ) {
469 forumax_increment_topic_view_count( $topic_id );
470 }
471 }
472 add_action( 'bbp_template_redirect', 'forumax_track_topic_views', 20 );
473
474 /**
475 * Get forum title
476 * @return string
477 */
478 function forumax_forum_title(){
479 $forum_id = bbp_get_forum_id();
480 $forum_title = get_the_title( $forum_id );
481 return $forum_title;
482 }
483
484 /**
485 * Customizer section hide from customizer
486 */
487 add_action( 'customize_register', function( $wp_customize ) {
488 // Unset the section you want to hide
489 $wp_customize->remove_section( 'design_fields' );
490 }, 20 );
491
492 /**
493 * Get all the registered menus
494 */
495 function forumax_get_registered_nav_menus() {
496 $menus = get_registered_nav_menus();
497 $menu_locations = [];
498 $empty = [ '' => esc_html__('Select Menu Location', 'forumax') ];
499 foreach ( $menus as $location => $description ) {
500 $menu_locations[ $location ] = $description;
501 }
502
503 return $empty + $menu_locations;
504 }
505
506 /**
507 * Fix Gutenberg editor support for bbPress forum and topic post types.
508 *
509 * Filters the post type registration arguments to enable full REST API
510 * support, ensure thumbnail support, and force correct labels so that
511 * Gutenberg displays the proper post type name instead of "Document".
512 *
513 * @param array $args Array of arguments for registering a post type.
514 * @param string $post_type Post type key.
515 * @return array Modified arguments.
516 */
517 function forumax_fix_post_type_args( $args, $post_type ) {
518 $post_types_config = array(
519 'forum' => array(
520 'rest_base' => 'forums',
521 'labels' => array(
522 'name' => __( 'Forums', 'bbpress' ),
523 'singular_name' => __( 'Forum', 'bbpress' ),
524 ),
525 ),
526 'topic' => array(
527 'rest_base' => 'topics',
528 'labels' => array(
529 'name' => __( 'Topics', 'bbpress' ),
530 'singular_name' => __( 'Topic', 'bbpress' ),
531 ),
532 ),
533 );
534
535 if ( ! isset( $post_types_config[ $post_type ] ) ) {
536 return $args;
537 }
538
539 $config = $post_types_config[ $post_type ];
540
541 // Force full REST API support for Gutenberg.
542 $args['show_in_rest'] = true;
543 $args['rest_base'] = $config['rest_base'];
544 $args['rest_controller_class'] = 'WP_REST_Posts_Controller';
545
546 // Set explicit label property (singular string).
547 $args['label'] = $config['labels']['name'];
548
549 // Ensure thumbnail support is included.
550 if ( ! empty( $args['supports'] ) && is_array( $args['supports'] ) ) {
551 if ( ! in_array( 'thumbnail', $args['supports'], true ) ) {
552 $args['supports'][] = 'thumbnail';
553 }
554 } else {
555 $args['supports'] = array( 'title', 'editor', 'revisions', 'thumbnail' );
556 }
557
558 // Merge labels to prevent Gutenberg "Document" fallback.
559 if ( ! empty( $args['labels'] ) && is_array( $args['labels'] ) ) {
560 $args['labels'] = array_merge( $args['labels'], $config['labels'] );
561 } else {
562 $args['labels'] = $config['labels'];
563 }
564
565 return $args;
566 }
567 add_filter( 'register_post_type_args', 'forumax_fix_post_type_args', 99, 2 );
568
569 /**
570 * Grant bbPress post type capabilities to WordPress administrators.
571 *
572 * Gutenberg requests the post type REST endpoint with context=edit, which
573 * checks custom capabilities like edit_forums and edit_topics. WordPress
574 * administrators may not have these caps unless bbPress has explicitly
575 * assigned them. This filter dynamically grants the required caps to
576 * any user who can manage_options (i.e. administrators).
577 *
578 * @param array $allcaps All capabilities for the user.
579 * @param array $caps Required capabilities being checked.
580 * @param array $args Additional arguments passed to the check.
581 * @return array Modified capabilities array.
582 */
583 function forumax_grant_bbpress_caps_to_admins( $allcaps, $caps, $args ) {
584 // Only grant to users who can manage options (administrators).
585 if ( empty( $allcaps['manage_options'] ) ) {
586 return $allcaps;
587 }
588
589 // bbPress forum and topic capabilities needed for REST API access.
590 $bbpress_caps = array(
591 'edit_forums',
592 'edit_others_forums',
593 'publish_forums',
594 'read_private_forums',
595 'read_hidden_forums',
596 'delete_forums',
597 'delete_others_forums',
598 'edit_topics',
599 'edit_others_topics',
600 'publish_topics',
601 'read_private_topics',
602 'delete_topics',
603 'delete_others_topics',
604 );
605
606 foreach ( $bbpress_caps as $cap ) {
607 $allcaps[ $cap ] = true;
608 }
609
610 return $allcaps;
611 }
612 add_filter( 'user_has_cap', 'forumax_grant_bbpress_caps_to_admins', 10, 3 );
613
614 /**
615 * Mutual Deactivation of old and new plugin paths
616 */
617 add_action( 'activated_plugin', function( $plugin ) {
618 if ( ! function_exists( 'is_plugin_active' ) ) {
619 require_once ABSPATH . 'wp-admin/includes/plugin.php';
620 }
621
622 $free_plugins_new = [ 'forumax/forumax.php' ];
623 $free_plugins_old = [ 'bbp-core/bbp-core.php', 'bbp-core/forumax.php' ];
624
625 $pro_plugins_new = [ 'forumax-pro/forumax.php' ];
626 $pro_plugins_old = [ 'bbp-core-pro/bbp-core.php', 'forumax-premium/bbp-core.php', 'forumax-premium/forumax.php' ];
627
628 // If activating NEW free, deactivate OLD free
629 if ( in_array( $plugin, $free_plugins_new, true ) ) {
630 deactivate_plugins( $free_plugins_old );
631 }
632
633 // If activating OLD free, deactivate NEW free
634 if ( in_array( $plugin, $free_plugins_old, true ) ) {
635 deactivate_plugins( $free_plugins_new );
636 }
637
638 // If activating NEW pro, deactivate OLD pro
639 if ( in_array( $plugin, $pro_plugins_new, true ) ) {
640 deactivate_plugins( $pro_plugins_old );
641 }
642
643 // If activating OLD pro, deactivate NEW pro
644 if ( in_array( $plugin, $pro_plugins_old, true ) ) {
645 deactivate_plugins( $pro_plugins_new );
646 }
647 } );
648
649 /**
650 * Register Forum Sidebar widget area
651 * This makes the Forum Sidebar available for any theme, not just theme-specific implementations
652 */
653 add_action( 'widgets_init', function () {
654 global $wp_registered_sidebars;
655
656 // Check if the sidebar is already registered by the theme (e.g., Docy)
657 if ( isset( $wp_registered_sidebars['forum_archive_sidebar'] ) ) {
658 return;
659 }
660
661 register_sidebar( [
662 'name' => esc_html__( 'Forumax Sidebar', 'forumax' ),
663 'description' => esc_html__( 'Add widgets here for the Forumax Sidebar area', 'forumax' ),
664 'id' => 'forum_archive_sidebar',
665 'before_widget' => '<div id="%1$s" class="widget sidebar_widget %2$s">',
666 'after_widget' => '</div>',
667 'before_title' => '<h3 class="widget-title">',
668 'after_title' => '</h3>'
669 ] );
670 }, 20 ); // Priority 20 to run after theme's widgets_init
671
672
673 /**
674 * Add title and thumbnail support for bbPress forum and topic post types.
675 *
676 * Ensures the title field and featured image meta box are available
677 * in the WordPress admin editor for both post types.
678 */
679 add_action( 'init', function () {
680 add_post_type_support( 'forum', 'title' );
681 add_post_type_support( 'forum', 'thumbnail' );
682 add_post_type_support( 'topic', 'title' );
683 add_post_type_support( 'topic', 'thumbnail' );
684 }, 25 );
685
686
687 /**
688 * Get moderator and keymaster users for assistant selection.
689 *
690 * @return array Array of user ID => display name.
691 */
692 if ( ! function_exists( 'frmx_get_moderator_users' ) ) {
693 function frmx_get_moderator_users() {
694 $users = [];
695
696 // Get users with bbPress moderator or keymaster roles.
697 $args = [
698 'role__in' => [ 'bbp_moderator', 'bbp_keymaster', 'administrator' ],
699 'orderby' => 'display_name',
700 'order' => 'ASC',
701 'number' => 100,
702 ];
703
704 $user_query = new WP_User_Query( $args );
705
706 if ( ! empty( $user_query->get_results() ) ) {
707 foreach ( $user_query->get_results() as $user ) {
708
709 $role_display = '';
710
711 if ( in_array( 'bbp_keymaster', (array) $user->roles, true ) ) {
712 $role_display = __( 'Keymaster', 'forumax' );
713 } elseif ( in_array( 'bbp_moderator', (array) $user->roles, true ) ) {
714 $role_display = __( 'Moderator', 'forumax' );
715 } elseif ( in_array( 'administrator', (array) $user->roles, true ) ) {
716 $role_display = __( 'Admin', 'forumax' );
717 }
718
719 $users[$user->ID] = sprintf(
720 '%s (%s)',
721 $user->display_name,
722 $role_display
723 );
724 }
725 }
726
727 return $users;
728 }
729 }
730
731 /**
732 * Remove all admin notices on Forumax admin pages.
733 *
734 * Cleans up the admin interface by hiding third-party notices
735 * on all Forumax-related admin pages.
736 *
737 * @since 1.0.0
738 * @return void
739 */
740 function forumax_remove_admin_notices() {
741 // Check if we're on any Forumax admin page.
742 $is_forumax_page = false;
743
744 // Check using forumax_admin_pages() for known page types.
745 if ( function_exists( 'forumax_admin_pages' ) ) {
746 $is_forumax_page = forumax_admin_pages( 'admin' )
747 || forumax_admin_pages( 'settings' )
748 || forumax_admin_pages( 'dashboard' );
749 }
750
751 // Also check for Analytics and other forumax-* pages.
752 $current_page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : '';
753 if ( strpos( $current_page, 'forumax' ) === 0 ) {
754 $is_forumax_page = true;
755 }
756
757 if ( $is_forumax_page ) {
758 remove_all_actions( 'admin_notices' );
759 remove_all_actions( 'all_admin_notices' );
760 }
761 }
762 add_action( 'admin_head', 'forumax_remove_admin_notices' );
763
764 /**
765 * Hide theme sidebars on bbPress pages
766 *
767 * This ensures only the Forumax sidebar displays on forum pages,
768 * preventing conflicts with theme sidebars (Divi, Astra, etc.).
769 *
770 * @param array $sidebars_widgets Array of sidebar widgets.
771 * @return array Modified array with theme sidebars emptied on bbPress pages.
772 */
773 function forumax_hide_theme_sidebars( $sidebars_widgets ) {
774 // Only modify on frontend bbPress pages
775 if ( is_admin() || ! function_exists( 'is_bbpress' ) || ! is_bbpress() ) {
776 return $sidebars_widgets;
777 }
778
779 // Keep only the Forumax sidebar, hide all other theme sidebars
780 foreach ( $sidebars_widgets as $sidebar_id => $widgets ) {
781 // Skip wp_inactive_widgets and our own forum sidebar
782 // We also skip footer sidebars to ensure footer widgets are visible
783 if ( 'wp_inactive_widgets' === $sidebar_id || 'forum_archive_sidebar' === $sidebar_id || strpos( $sidebar_id, 'footer' ) !== false ) {
784 continue;
785 }
786
787 // Empty other sidebars (this prevents them from rendering)
788 $sidebars_widgets[ $sidebar_id ] = [];
789 }
790
791 return $sidebars_widgets;
792 }
793 add_filter( 'sidebars_widgets', 'forumax_hide_theme_sidebars' );
794
795 /**
796 * Get topic count by status.
797 *
798 * @param string $status The post status to count (e.g., 'publish', 'closed').
799 * @param int|bool $parent_id Optional. The parent forum ID to filter by.
800 *
801 * @return int The number of topics with the specified status.
802 */
803 function forumax_get_topic_count_by_status( $status = 'publish', $parent_id = false ) {
804 global $wpdb;
805
806 // Sanitize parameters for cache key
807 $cache_status = sanitize_key( $status );
808 $cache_parent = ( false !== $parent_id && is_numeric( $parent_id ) ) ? (int) $parent_id : 0;
809
810 // Check transient cache
811 $cache_key = 'frmx_topic_count_' . $cache_status . '_' . $cache_parent;
812 $count = get_transient( $cache_key );
813
814 if ( false !== $count ) {
815 return (int) $count;
816 }
817
818 $status = esc_sql( $status );
819 $where = "WHERE post_type = 'topic' AND post_status = '{$status}'";
820
821 if ( false !== $parent_id && is_numeric( $parent_id ) ) {
822 $where .= $wpdb->prepare( " AND post_parent = %d", $parent_id );
823 }
824
825 $count = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->posts} {$where}" );
826
827 // Set transient cache for 1 hour
828 set_transient( $cache_key, (int) $count, HOUR_IN_SECONDS );
829
830 return (int) $count;
831 }
832
833 /**
834 * Get unanswered topics count.
835 *
836 * @return int The number of unanswered topics.
837 */
838 function forumax_get_unanswered_topics_count() {
839 $cache_key = 'frmx_unanswered_topics_count';
840 $count = get_transient( $cache_key );
841
842 if ( false !== $count ) {
843 return $count;
844 }
845
846 global $wpdb;
847 $count = $wpdb->get_var(
848 "SELECT COUNT(*) FROM {$wpdb->posts} p
849 LEFT JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id AND pm.meta_key = '_bbp_reply_count'
850 WHERE p.post_type = 'topic'
851 AND p.post_status = 'publish'
852 AND (pm.meta_value IS NULL OR pm.meta_value = '0')"
853 );
854
855 set_transient( $cache_key, $count, HOUR_IN_SECONDS );
856 return absint( $count );
857 }
858
859 /**
860 * Get recent unanswered topics.
861 *
862 * @param int $limit Number of topics to retrieve.
863 * @return array Array of WP_Post objects.
864 */
865 function forumax_get_recent_unanswered_topics( $limit = 5 ) {
866 $args = [
867 'post_type' => 'topic',
868 'post_status' => 'publish',
869 'posts_per_page' => $limit,
870 'orderby' => 'date',
871 'order' => 'DESC',
872 'meta_query' => [
873 'relation' => 'OR',
874 [
875 'key' => '_bbp_reply_count',
876 'value' => '0',
877 'compare' => '=',
878 ],
879 [
880 'key' => '_bbp_reply_count',
881 'compare' => 'NOT EXISTS',
882 ],
883 ],
884 ];
885
886 return get_posts( $args );
887 }
888
889 /**
890 * Invalidate unanswered topics cache.
891 */
892 function forumax_invalidate_unanswered_topics_cache() {
893 delete_transient( 'frmx_unanswered_topics_count' );
894 }
895 add_action( 'bbp_new_reply', 'forumax_invalidate_unanswered_topics_cache' );
896 add_action( 'bbp_deleted_reply', 'forumax_invalidate_unanswered_topics_cache' );
897 add_action( 'bbp_trash_reply', 'forumax_invalidate_unanswered_topics_cache' );
898 add_action( 'bbp_untrash_reply', 'forumax_invalidate_unanswered_topics_cache' );
899 add_action( 'bbp_spam_reply', 'forumax_invalidate_unanswered_topics_cache' );
900 add_action( 'bbp_unspam_reply', 'forumax_invalidate_unanswered_topics_cache' );
901 add_action( 'bbp_new_topic', 'forumax_invalidate_unanswered_topics_cache' );
902 add_action( 'bbp_deleted_topic', 'forumax_invalidate_unanswered_topics_cache' );
903 add_action( 'bbp_trash_topic', 'forumax_invalidate_unanswered_topics_cache' );
904 add_action( 'bbp_untrash_topic', 'forumax_invalidate_unanswered_topics_cache' );
905 add_action( 'bbp_spam_topic', 'forumax_invalidate_unanswered_topics_cache' );
906 add_action( 'bbp_unspam_topic', 'forumax_invalidate_unanswered_topics_cache' );
907
908 /**
909 * Filter topics by unanswered status in admin dashboard.
910 *
911 * @param WP_Query $query The WP_Query instance (modified in place).
912 */
913 function forumax_filter_unanswered_topics_admin( $query ) {
914 if ( ! is_admin() || ! $query->is_main_query() ) {
915 return;
916 }
917
918 if ( 'topic' !== $query->get( 'post_type' ) ) {
919 return;
920 }
921
922 $forumax_filter = '';
923 if ( isset( $_GET['forumax_filter'] ) ) {
924 $forumax_filter = sanitize_key( wp_unslash( $_GET['forumax_filter'] ) );
925 }
926
927 if ( 'unanswered' === $forumax_filter ) {
928 $meta_query = $query->get( 'meta_query' );
929 if ( ! is_array( $meta_query ) ) {
930 $meta_query = [];
931 }
932
933 $meta_query[] = [
934 'relation' => 'OR',
935 [
936 'key' => '_bbp_reply_count',
937 'value' => '0',
938 'compare' => '=',
939 ],
940 [
941 'key' => '_bbp_reply_count',
942 'compare' => 'NOT EXISTS',
943 ],
944 ];
945
946 $query->set( 'meta_query', $meta_query );
947 }
948 }
949 add_action( 'pre_get_posts', 'forumax_filter_unanswered_topics_admin' );
950