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