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