PluginProbe
Forumax – AI Powered Advanced Community Forum Plugin / trunk
Forumax – AI Powered Advanced Community Forum Plugin vtrunk
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 trunk, at includes/functions.php

972 lines 28.0 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 * 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 /**
70 * Check If the Page is Forum page
71 */
72 function forumax_is_forum_page() {
73 if ( in_array( 'bbpress', get_body_class(), true ) ) {
74 return true;
75 }
76 return false;
77 }
78
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 /**
286 * Check if the pro plugin and plan is active
287 *
288 * @return bool|void
289 */
290 function forumax_is_premium() {
291 if ( class_exists('Forumax_Pro') && bc_fs()->can_use_premium_code() ) {
292 return true;
293 }
294 }
295
296 /**
297 * Check if the promax plan is active
298 *
299 * @return bool|void
300 */
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 ) {
321 return true;
322 }
323 } elseif ( 'settings' === $admin ) {
324 if ( admin_url( 'admin.php?page=forumax-settings' ) === $current_url ) {
325 return true;
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 }
339 }
340
341 return false;
342 }
343
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()) {
353 return true;
354 }
355 }
356
357
358 /**
359 * Posts Arraty
360 * @param object Post Type
361 */
362 function forumax_get_posts( $post_type = 'forum' ) {
363 $posts = get_pages(
364 [
365 'post_type' => $post_type,
366 'parent' => 0,
367 ]
368 );
369
370 $posts_array = [];
371
372 if ( $posts ) {
373 foreach ( $posts as $post ) {
374 $posts_array[ $post->ID ] = $post->post_title;
375 }
376 }
377
378 return $posts_array;
379 }
380
381 /**
382 * Limit letter
383 * @param $string
384 * @param $limit_length
385 * @param string $suffix
386 */
387 function forumax_limit_letter( $string, $limit_length, $suffix = '...' ) {
388 if ( strlen( $string ) > $limit_length ) {
389 echo esc_html ( strip_shortcodes( substr( $string, 0, $limit_length ) . $suffix ) );
390 } else {
391 echo esc_html( $string );
392 }
393 }
394
395 /**
396 * Return the topic view count.
397 *
398 * @param int $topic_id Optional. Topic id
399 *
400 * @return int The view count
401 * @uses get_post_meta() To get the view count meta
402 * @uses bbp_get_topic_id() To get the topic id
403 */
404 function forumax_get_topic_view_count( $topic_id = 0 ) {
405 $topic_id = bbp_get_topic_id( $topic_id );
406
407 if ( empty( $topic_id ) ) {
408 return 0;
409 }
410
411 $views = (int) get_post_meta( $topic_id, '_btv_view_count', true );
412
413 return $views;
414 }
415
416 /**
417 * Output the topic view count.
418 *
419 * @param int $topic_id Optional. Topic id
420 *
421 * @uses bbp_get_topic_id() To get the topic id
422 * @uses btv_get_topic_view_count() To get the view count for the topic
423 */
424 function forumax_topic_view_count( $topic_id = 0 ) {
425 $topic_id = bbp_get_topic_id( $topic_id );
426 $view_count = forumax_get_topic_view_count( $topic_id );
427 return $view_count;
428 }
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 );
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
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 /**
497 * Get forum title
498 * @return string
499 */
500 function forumax_forum_title(){
501 $forum_id = bbp_get_forum_id();
502 $forum_title = get_the_title( $forum_id );
503 return $forum_title;
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' );
972