Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/wpforo/admin/index.php
+++ b/wpforo/admin/index.php
@@ -12,7 +12,7 @@
}, 39 );
function wpforo_admin_menu(){
- if( wpforo_current_user_is( 'admin' ) || WPF()->usergroup->can( 'mf' ) || WPF()->usergroup->can( 'ms' ) || WPF()->usergroup->can( 'vm' ) || WPF()->usergroup->can( 'mp' ) || WPF()->usergroup->can( 'aum' ) || WPF()->usergroup->can( 'vmg' ) || WPF()->usergroup->can( 'mth' ) ) {
+ if( wpforo_current_user_is( 'admin' ) || WPF()->usergroup->can( 'mf' ) || WPF()->usergroup->can( 'ms' ) || WPF()->usergroup->can( 'mai' ) || WPF()->usergroup->can( 'vm' ) || WPF()->usergroup->can( 'mp' ) || WPF()->usergroup->can( 'aum' ) || WPF()->usergroup->can( 'vmg' ) || WPF()->usergroup->can( 'mth' ) ) {
$menu_position = apply_filters( 'wpforo_admin_menu_position', 23 );
$boards = WPF()->board->get_boards( [ 'status' => true ] );
$board = current( $boards );
@@ -60,6 +60,11 @@
require( WPFORO_DIR . '/admin/pages/settings.php' );
} );
}
+ if( WPF()->usergroup->can( 'mai' ) || wpforo_current_user_is( 'admin' ) ) {
+ add_submenu_page( $parent_slug, __( 'AI Features', 'wpforo' ), __( 'AI Features', 'wpforo' ), 'read', 'wpforo-ai', function() {
+ require( WPFORO_DIR . '/admin/pages/ai-features.php' );
+ } );
+ }
if( WPF()->usergroup->can( 'aum' ) || wpforo_current_user_is( 'admin' ) ) {
add_submenu_page( $parent_slug, __( 'Moderation', 'wpforo' ), __( 'Moderation', 'wpforo' ) . $mod_count, 'read', wpforo_prefix_slug( 'moderations' ), function() {
require( WPFORO_DIR . '/admin/pages/moderation.php' );
@@ -111,7 +116,7 @@
}
function wpforo_admin_menu_multiboard(){
- if( wpforo_current_user_is( 'admin' ) || WPF()->usergroup->can( 'mf' ) || WPF()->usergroup->can( 'ms' ) || WPF()->usergroup->can( 'vm' ) || WPF()->usergroup->can( 'mp' ) || WPF()->usergroup->can( 'aum' ) || WPF()->usergroup->can( 'vmg' ) || WPF()->usergroup->can( 'mth' ) ) {
+ if( wpforo_current_user_is( 'admin' ) || WPF()->usergroup->can( 'mf' ) || WPF()->usergroup->can( 'ms' ) || WPF()->usergroup->can( 'mai' ) || WPF()->usergroup->can( 'vm' ) || WPF()->usergroup->can( 'mp' ) || WPF()->usergroup->can( 'aum' ) || WPF()->usergroup->can( 'vmg' ) || WPF()->usergroup->can( 'mth' ) ) {
$menu_position = apply_filters( 'wpforo_admin_menu_position', 23 );
$parent_slug = 'wpforo-overview';
$attention_count = WPF()->member->get_count( [ 'p.status' => ['banned', 'inactive'] ] );
@@ -156,6 +161,11 @@
require( WPFORO_DIR . '/admin/pages/settings.php' );
} );
}
+ if( WPF()->usergroup->can( 'mai' ) || wpforo_current_user_is( 'admin' ) ) {
+ add_submenu_page( $parent_slug, __( 'AI Features', 'wpforo' ), __( 'AI Features', 'wpforo' ), 'read', 'wpforo-ai', function() {
+ require( WPFORO_DIR . '/admin/pages/ai-features.php' );
+ } );
+ }
if( WPF()->usergroup->can( 'mth' ) || wpforo_current_user_is( 'admin' ) ) {
add_submenu_page( $parent_slug, __( 'Themes', 'wpforo' ), __( 'Themes', 'wpforo' ), 'read', wpforo_prefix_slug( 'themes' ), function() {
require( WPFORO_DIR . '/admin/pages/themes.php' );
@@ -248,3 +258,6 @@
</div>
</div>';
} );
+
+// Feature Introduction Modal - shows once per admin after major updates
+new wpforoclassesFeatureIntro();
--- a/wpforo/admin/listtables/Moderations.php
+++ b/wpforo/admin/listtables/Moderations.php
@@ -13,6 +13,11 @@
public $wpfitems_count;
+ /**
+ * Cache for moderation data to avoid repeated queries
+ */
+ private $moderation_cache = [];
+
/** ************************************************************************
* REQUIRED. Set up a constructor that references the parent constructor. We
* use the parent reference to set some default configs.
@@ -59,8 +64,17 @@
return apply_filters( 'wpforo_admin_listtables_moderations_column_title', $item[ $column_name ], $item );
case 'userid':
$userdata = get_userdata( $item[ $column_name ] );
+ $display_name = ! empty( $userdata->user_nicename ) ? urldecode( $userdata->user_nicename ) : $item[ $column_name ];
+
+ // Check if user is banned
+ $member = WPF()->member->get_member( $item[ $column_name ] );
+ $is_banned = ( ! empty( $member ) && isset( $member['status'] ) && $member['status'] === 'banned' );
+
+ if( $is_banned ) {
+ return '<span style="color: #dc3545; font-weight: 500;" title="' . esc_attr__( 'Banned', 'wpforo' ) . '">' . esc_html( $display_name ) . '</span>';
+ }
- return ( ! empty( $userdata->user_nicename ) ? urldecode( $userdata->user_nicename ) : $item[ $column_name ] );
+ return esc_html( $display_name );
case 'is_first_post':
return ( $item[ $column_name ] ) ? __( 'TOPIC', 'wpforo' ) : __( 'REPLY', 'wpforo' );
case 'private':
@@ -99,9 +113,30 @@
$uhref = wp_nonce_url( admin_url( sprintf( 'admin.php?page=%1$s&wpfaction=%2$s&postid=%3$s', wpforo_prefix_slug( 'moderations' ), 'dashboard_post_unapprove', $item['postid'] ) ), 'wpforo-unapprove-post-' . $item['postid'] );
$actions['wpfunapprove'] = '<a href="' . $uhref . '">' . __( 'Unapprove', 'wpforo' ) . '</a>';
}
+
$dhref = wp_nonce_url( admin_url( sprintf( 'admin.php?page=%1$s&wpfaction=%2$s&postid=%3$s', wpforo_prefix_slug( 'moderations' ), 'dashboard_post_delete', $item['postid'] ) ), 'wpforo-delete-post-' . $item['postid'] );
$actions['delete'] = '<a onclick="return confirm('' . __( "Are you sure you want to DELETE this item?", 'wpforo' ) . '');" href="' . $dhref . '">' . __( 'Delete', 'wpforo' ) . '</a>';
+ // Ban/Unban User action - show based on user's current ban status
+ if( ! empty( $item['userid'] ) && $item['userid'] > 0 && WPF()->usergroup->can( 'bm' ) && intval( $item['userid'] ) !== WPF()->current_userid ) {
+ $member = WPF()->member->get_member( $item['userid'] );
+ $is_banned = ( ! empty( $member ) && isset( $member['status'] ) && $member['status'] === 'banned' );
+
+ if( $is_banned ) {
+ $unban_url = wp_nonce_url( admin_url( sprintf( 'admin.php?page=%1$s&wpfaction=%2$s&userid=%3$s', wpforo_prefix_slug( 'members' ), 'user_unban', $item['userid'] ) ), 'wpforo-user-unban-' . $item['userid'] );
+ $actions['ban_user'] = '<a style="white-space:nowrap; color:#006600;" onclick="return confirm('' . __( "Are you sure you want to UNBAN this user?", 'wpforo' ) . '');" href="' . esc_url( $unban_url ) . '">' . __( 'Unban User', 'wpforo' ) . '</a>';
+ } else {
+ $ban_url = wp_nonce_url( admin_url( sprintf( 'admin.php?page=%1$s&wpfaction=%2$s&userid=%3$s', wpforo_prefix_slug( 'members' ), 'user_ban', $item['userid'] ) ), 'wpforo-user-ban-' . $item['userid'] );
+ $actions['ban_user'] = '<a style="white-space:nowrap; color:orange;" onclick="return confirm('' . __( "Are you sure you want to BAN this user?", 'wpforo' ) . '');" href="' . esc_url( $ban_url ) . '">' . __( 'Ban User', 'wpforo' ) . '</a>';
+ }
+ }
+
+ // Delete User action - links to WordPress delete user page
+ if( ! empty( $item['userid'] ) && $item['userid'] > 0 && WPF()->usergroup->can( 'dm' ) && intval( $item['userid'] ) !== WPF()->current_userid ) {
+ $delete_user_url = wp_nonce_url( admin_url( 'users.php?action=delete&user=' . intval( $item['userid'] ) ), 'bulk-users' );
+ $actions['delete_user'] = '<a style="white-space:nowrap;" onclick="return confirm('' . __( "Are you sure you want to DELETE this USER? This will open the WordPress user deletion page where you can choose to delete or reassign their content.", 'wpforo' ) . '');" href="' . esc_url( $delete_user_url ) . '">' . __( 'Delete User', 'wpforo' ) . '</a>';
+ }
+
$actions = apply_filters( 'wpforo_admin_listtables_moderations_actions', $actions, $item );
//Return the title contents
@@ -365,4 +400,286 @@
</label>
<?php
}
+
+ /**
+ * Override single_row to add moderation report row after each post row
+ *
+ * @param array $item The current item
+ */
+ public function single_row( $item ) {
+ // Output the regular row
+ parent::single_row( $item );
+
+ // Only show moderation report row for unapproved posts
+ if( $this->get_filter_by_status_var() !== 1 ) {
+ return;
+ }
+
+ // Get moderation data for this post
+ $moderation_data = $this->get_moderation_data( $item );
+
+ // Always render the report row to maintain odd/even striping
+ // Empty rows will be hidden via CSS
+ $this->render_moderation_report_row( $item, $moderation_data );
+ }
+
+ /**
+ * Get moderation data for a post
+ *
+ * @param array $item Post item data
+ * @return array|null Moderation data or null
+ */
+ private function get_moderation_data( $item ) {
+ $postid = intval( $item['postid'] );
+
+ // Check cache first
+ if( isset( $this->moderation_cache[ $postid ] ) ) {
+ return $this->moderation_cache[ $postid ];
+ }
+
+ $result = [];
+
+ // Determine content type
+ $is_first_post = ! empty( $item['is_first_post'] );
+ $content_type = $is_first_post ? 'topic' : 'post';
+ $content_id = $is_first_post ? ( $item['topicid'] ?? $item['postid'] ) : $item['postid'];
+
+ // Get AI Moderation data (if AI Moderation is active)
+ if( class_exists( 'wpforoclassesAIContentModeration' ) && ! empty( WPF()->ai_content_moderation ) ) {
+ $ai_logs = WPF()->ai_content_moderation->get_moderation_logs( $content_type, (int) $content_id );
+ if( ! empty( $ai_logs ) ) {
+ $result['ai_moderation'] = $ai_logs;
+ }
+ }
+
+ // Check for wpForo built-in antispam (this is simple - just a flag that post was unapproved by antispam)
+ // Built-in antispam doesn't store detailed data, but we can check if it was likely antispam-based
+ // by looking at whether user is new or if spam patterns were detected
+ if( empty( $result['ai_moderation'] ) && ! empty( $item['userid'] ) ) {
+ $new_user_max_posts = wpforo_setting( 'antispam', 'new_user_max_posts' );
+ if( $new_user_max_posts ) {
+ $user_posts = WPF()->member->member_approved_posts( $item['userid'] );
+ if( $user_posts <= $new_user_max_posts ) {
+ // Likely caught by built-in antispam for new users
+ $result['builtin_antispam'] = [
+ 'reason' => 'new_user',
+ 'label' => __( 'New User Filter', 'wpforo' ),
+ 'description' => __( 'Post was automatically held for moderation because the author is a new user.', 'wpforo' ),
+ ];
+ }
+ }
+ }
+
+ // Cache the result
+ $this->moderation_cache[ $postid ] = $result;
+
+ return $result;
+ }
+
+ /**
+ * Render the moderation report row
+ *
+ * @param array $item Post item data
+ * @param array $moderation_data Moderation data (can be empty)
+ */
+ private function render_moderation_report_row( $item, $moderation_data ) {
+ $columns_count = count( $this->get_columns() );
+ $has_data = ! empty( $moderation_data );
+ $row_class = 'wpf-moderation-report-row' . ( $has_data ? '' : ' wpf-moderation-report-empty' );
+ ?>
+ <tr class="<?php echo esc_attr( $row_class ); ?>">
+ <?php if( $has_data ) : ?>
+ <td colspan="<?php echo esc_attr( $columns_count ); ?>" class="wpf-moderation-report-cell">
+ <div class="wpf-moderation-report-container">
+ <?php
+ // Display AI Moderation reports
+ if( ! empty( $moderation_data['ai_moderation'] ) ) {
+ foreach( $moderation_data['ai_moderation'] as $log ) {
+ $this->render_ai_moderation_report( $log );
+ }
+ }
+
+ // Display built-in antispam info
+ if( ! empty( $moderation_data['builtin_antispam'] ) ) {
+ $this->render_builtin_antispam_report( $moderation_data['builtin_antispam'] );
+ }
+ ?>
+ </div>
+ </td>
+ <?php else : ?>
+ <td colspan="<?php echo esc_attr( $columns_count ); ?>"></td>
+ <?php endif; ?>
+ </tr>
+ <?php
+ }
+
+ /**
+ * Render AI moderation report
+ *
+ * @param array $log Moderation log data
+ */
+ private function render_ai_moderation_report( $log ) {
+ $score = (int) ( $log['score'] ?? 0 );
+ $confidence = (float) ( $log['confidence'] ?? 0 );
+ $mod_type = $log['moderation_type'] ?? 'spam';
+ $action = $log['action_taken'] ?? 'none';
+ $summary = $log['analysis_summary'] ?? '';
+ $indicators = $log['indicators'] ?? [];
+ $quality = $log['quality_tier'] ?? 'fast';
+ $credits = (int) ( $log['credits_used'] ?? 0 );
+ $created = $log['created'] ?? '';
+ $context_used = ! empty( $log['context_used'] );
+ $is_ai = ( $quality !== 'rule_based' );
+
+ // Decode indicators if string
+ if( is_string( $indicators ) && ! empty( $indicators ) ) {
+ $indicators = json_decode( $indicators, true ) ?: [];
+ }
+
+ // Moderation type config
+ $type_config = [
+ 'spam' => [
+ 'label' => $is_ai ? __( 'Spam Detection', 'wpforo' ) : __( 'Auto Moderation', 'wpforo' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="4.93" y1="4.93" x2="19.07" y2="19.07"></line></svg>',
+ 'color' => $is_ai ? '#d63384' : '#0d6efd',
+ ],
+ 'toxicity' => [
+ 'label' => __( 'Toxicity Detection', 'wpforo' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"></path><line x1="12" y1="9" x2="12" y2="13"></line><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>',
+ 'color' => '#fd7e14',
+ ],
+ 'compliance' => [
+ 'label' => __( 'Policy Compliance', 'wpforo' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10"/></svg>',
+ 'color' => '#6f42c1',
+ ],
+ 'flood' => [
+ 'label' => __( 'Auto Moderation', 'wpforo' ),
+ 'icon' => '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>',
+ 'color' => '#0d6efd',
+ ],
+ ];
+
+ $config = $type_config[ $mod_type ] ?? $type_config['spam'];
+
+ // Status class and label
+ $status_class = 'clean';
+ $status_label = __( 'Clean', 'wpforo' );
+ if( $score >= 85 ) {
+ $status_class = 'detected';
+ $status_label = __( 'Detected', 'wpforo' );
+ } elseif( $score >= 70 ) {
+ $status_class = 'suspected';
+ $status_label = __( 'Suspected', 'wpforo' );
+ } elseif( $score >= 51 ) {
+ $status_class = 'uncertain';
+ $status_label = __( 'Uncertain', 'wpforo' );
+ }
+
+ // Action labels
+ $action_labels = [
+ 'none' => __( 'No action', 'wpforo' ),
+ 'approve' => __( 'Auto-approved', 'wpforo' ),
+ 'auto_approve' => __( 'Auto-approved', 'wpforo' ),
+ 'unapprove' => __( 'Unapproved', 'wpforo' ),
+ 'unapprove_ban' => __( 'Unapproved + Banned', 'wpforo' ),
+ 'delete_author' => __( 'Deleted + Banned', 'wpforo' ),
+ ];
+ $action_label = $action_labels[ $action ] ?? $action;
+
+ // Quality labels
+ $quality_labels = [
+ 'fast' => __( 'Fast', 'wpforo' ),
+ 'balanced' => __( 'Balanced', 'wpforo' ),
+ 'advanced' => __( 'Advanced', 'wpforo' ),
+ 'premium' => __( 'Premium', 'wpforo' ),
+ 'rule_based' => __( 'Rule-based', 'wpforo' ),
+ ];
+ $quality_label = $quality_labels[ $quality ] ?? $quality;
+ ?>
+ <div class="wpf-mod-report wpf-mod-report-ai wpf-mod-status-<?php echo esc_attr( $status_class ); ?>" data-type="<?php echo esc_attr( $mod_type ); ?>">
+ <div class="wpf-mod-report-type">
+ <span class="wpf-mod-type-icon" style="color: <?php echo esc_attr( $config['color'] ); ?>">
+ <?php echo $config['icon']; ?>
+ </span>
+ <span class="wpf-mod-type-label"><?php echo esc_html( $config['label'] ); ?></span>
+ </div>
+ <div class="wpf-mod-report-content">
+ <div class="wpf-mod-report-header">
+ <span class="wpf-mod-status wpf-mod-status-<?php echo esc_attr( $status_class ); ?>"><?php echo esc_html( $status_label ); ?></span>
+ <?php if( $is_ai ) : ?>
+ <span class="wpf-mod-score"><?php printf( __( 'Score: %d%%', 'wpforo' ), $score ); ?></span>
+ <span class="wpf-mod-confidence"><?php printf( __( 'Confidence: %d%%', 'wpforo' ), round( $confidence * 100 ) ); ?></span>
+ <?php else : ?>
+ <span class="wpf-mod-score"><?php _e( 'Score: -', 'wpforo' ); ?></span>
+ <?php endif; ?>
+ <span class="wpf-mod-action"><?php printf( __( 'Action: %s', 'wpforo' ), $action_label ); ?></span>
+ </div>
+
+ <?php if( ! empty( $summary ) ) : ?>
+ <div class="wpf-mod-report-summary">
+ <strong><?php _e( 'Summary:', 'wpforo' ); ?></strong> <?php echo esc_html( $summary ); ?>
+ </div>
+ <?php endif; ?>
+
+ <?php if( ! empty( $indicators ) && is_array( $indicators ) ) : ?>
+ <div class="wpf-mod-report-indicators">
+ <strong><?php _e( 'Indicators:', 'wpforo' ); ?></strong>
+ <ul class="wpf-mod-indicator-list">
+ <?php foreach( $indicators as $indicator ) : ?>
+ <li class="wpf-mod-indicator wpf-mod-severity-<?php echo esc_attr( strtolower( $indicator['severity'] ?? 'medium' ) ); ?>">
+ <span class="wpf-mod-indicator-category"><?php echo esc_html( $indicator['category'] ?? '' ); ?></span>
+ <?php if( ! empty( $indicator['description'] ) ) : ?>
+ <span class="wpf-mod-indicator-desc"><?php echo esc_html( $indicator['description'] ); ?></span>
+ <?php endif; ?>
+ </li>
+ <?php endforeach; ?>
+ </ul>
+ </div>
+ <?php endif; ?>
+
+ <div class="wpf-mod-report-meta">
+ <?php if( $is_ai ) : ?>
+ <span class="wpf-mod-quality"><?php printf( __( 'AI executor: %s', 'wpforo' ), $quality_label ); ?></span>
+ <?php if( $credits > 0 ) : ?>
+ <span class="wpf-mod-credits"><?php printf( _n( '%d credit', '%d credits', $credits, 'wpforo' ), $credits ); ?></span>
+ <?php endif; ?>
+ <?php if( $context_used ) : ?>
+ <span class="wpf-mod-context"><?php _e( 'Context-aware', 'wpforo' ); ?></span>
+ <?php endif; ?>
+ <?php endif; ?>
+ <?php if( ! empty( $created ) ) : ?>
+ <span class="wpf-mod-date"><?php echo esc_html( date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $created ) ) ); ?></span>
+ <?php endif; ?>
+ </div>
+ </div>
+ </div>
+ <?php
+ }
+
+ /**
+ * Render built-in antispam report
+ *
+ * @param array $data Antispam data
+ */
+ private function render_builtin_antispam_report( $data ) {
+ ?>
+ <div class="wpf-mod-report wpf-mod-report-builtin">
+ <div class="wpf-mod-report-type">
+ <span class="wpf-mod-type-icon" style="color: #0d6efd;">
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="8" x2="12" y2="12"></line><line x1="12" y1="16" x2="12.01" y2="16"></line></svg>
+ </span>
+ <span class="wpf-mod-type-label"><?php _e( 'wpForo Antispam', 'wpforo' ); ?></span>
+ </div>
+ <div class="wpf-mod-report-content">
+ <div class="wpf-mod-report-header">
+ <span class="wpf-mod-status wpf-mod-status-builtin"><?php echo esc_html( $data['label'] ); ?></span>
+ </div>
+ <div class="wpf-mod-report-summary">
+ <?php echo esc_html( $data['description'] ); ?>
+ </div>
+ </div>
+ </div>
+ <?php
+ }
}
--- a/wpforo/admin/pages/ai-features.php
+++ b/wpforo/admin/pages/ai-features.php
@@ -0,0 +1,333 @@
+<?php
+/**
+ * wpForo AI Features - Admin Page
+ *
+ * Main administration page for managing AI features integration, subscription,
+ * and usage monitoring.
+ *
+ * @since 3.0.0
+ */
+
+// Security: Check permissions
+if ( ! wpforo_current_user_is( 'admin' ) && ! WPF()->usergroup->can( 'mai' ) ) {
+ wp_die( __( 'You do not have sufficient permissions to access this page.', 'wpforo' ) );
+}
+
+// Include helper functions and tab content files FIRST (before using them)
+require_once __DIR__ . '/tabs/ai-features-helpers.php';
+require_once __DIR__ . '/tabs/ai-features-tab-overview.php';
+require_once __DIR__ . '/tabs/ai-features-tab-rag-indexing.php';
+require_once __DIR__ . '/tabs/ai-features-tab-ai-tasks.php';
+require_once __DIR__ . '/tabs/ai-features-tab-analytics.php';
+require_once __DIR__ . '/tabs/ai-features-tab-ai-logs.php';
+
+// Determine current tab early (needed for conditional script loading)
+$current_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'overview';
+
+// Enqueue AI Features scripts and styles
+wp_enqueue_style( 'wpforo-ai-features', WPFORO_URL . '/admin/assets/css/ai-features.css', [], WPFORO_VERSION );
+
+// Chart.js is only needed on the analytics tab - don't load it elsewhere
+$ai_features_deps = [ 'jquery', 'suggest' ];
+if ( $current_tab === 'analytics' ) {
+ wp_enqueue_script( 'wpforo-chart-js', WPFORO_URL . '/admin/assets/js/chart.min.js', [], '4.4.1', true );
+ $ai_features_deps[] = 'wpforo-chart-js';
+}
+wp_enqueue_script( 'wpforo-ai-features', WPFORO_URL . '/admin/assets/js/ai-features.js', $ai_features_deps, WPFORO_VERSION, false );
+
+// Localize script with AJAX URL and nonce
+wp_localize_script( 'wpforo-ai-features', 'wpforoAIAdmin', [
+ 'ajaxUrl' => admin_url( 'admin-ajax.php' ),
+ 'nonce' => wp_create_nonce( 'wpforo_ai_features_nonce' ),
+ 'adminNonce' => wp_create_nonce( 'wpforo_admin_ajax' ), // For WordPress content indexing AJAX calls
+ 'debugMode' => (bool) wpforo_setting( 'general', 'debug_mode' ),
+] );
+
+// Localize i18n strings for AI logs (wpforoAI is used by WpForoAILogs module)
+wp_localize_script( 'wpforo-ai-features', 'wpforoAI', [
+ 'i18n' => [
+ 'confirmDelete' => __( 'Are you sure you want to delete this log?', 'wpforo' ),
+ 'confirmDeleteSelected' => __( 'Are you sure you want to delete the selected logs?', 'wpforo' ),
+ 'deleteAllLogs' => __( 'Delete All Logs', 'wpforo' ),
+ 'noLogs' => __( 'No logs found.', 'wpforo' ),
+ ],
+] );
+
+// Initialize AI Client
+if ( ! isset( WPF()->ai_client ) ) {
+ WPF()->ai_client = new wpforoclassesAIClient();
+}
+
+// Pricing is now static - no cache refresh needed
+
+// Handle form submissions
+$notice = wpforo_ai_handle_form_actions();
+
+// Clear WordPress object cache for options (in case of external cache plugins)
+// This ensures fresh values from DB after form submissions
+wp_cache_delete( 'alloptions', 'options' );
+wp_cache_delete( 'notoptions', 'options' );
+
+// Note: Status transient is only cleared by specific actions (connect, disconnect, refresh)
+// NOT on every page load - the 5-minute cache in get_tenant_status() prevents excessive API calls
+
+// Get current connection status (fresh from database)
+// Use global options (shared across all boards) to check connection
+$api_key = WPF()->ai_client->get_api_key();
+$tenant_id = WPF()->ai_client->get_tenant_id();
+$is_connected = ! empty( $api_key ) && ! empty( $tenant_id );
+
+// Check if returning from Freemius purchase (for status badge update)
+$is_post_purchase = (isset( $_GET['upgraded'] ) && $_GET['upgraded'] == '1') || (isset( $_GET['credits_purchased'] ) && $_GET['credits_purchased'] == '1');
+$purchased_plan = isset( $_GET['plan'] ) ? sanitize_key( $_GET['plan'] ) : '';
+
+// Get tenant status if connected
+$status = null;
+if ( $is_connected ) {
+ $status = WPF()->ai_client->get_tenant_status();
+ if ( is_wp_error( $status ) ) {
+ // Connection exists but status fetch failed - show error state
+ $connection_error = $status;
+ $is_connected = false;
+ }
+}
+
+// Determine current state
+$current_state = wpforo_ai_get_current_state( $is_connected, $status );
+
+// If pending_approval and status is missing/error, construct from transient
+if ( $current_state === 'pending_approval' && ( ! $status || is_wp_error( $status ) ) ) {
+ $pending = get_transient( 'wpforo_ai_pending_approval' );
+ if ( $pending ) {
+ $status = [
+ 'subscription' => [
+ 'status' => 'pending_approval',
+ 'plan' => 'free_trial',
+ 'credits_total' => wpfval( $pending, 'credits_total' ) ?: 500,
+ ],
+ ];
+ }
+}
+
+?>
+
+<div class="wrap wpforo-ai-wrap">
+ <h1 class="wpforo-ai-title">
+ <svg width="50px" height="50px" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <title>ai</title>
+ <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+ <g id="icon" fill="#000000" transform="translate(64.000000, 64.000000)">
+ <path d="M320,64 L320,320 L64,320 L64,64 L320,64 Z M171.749388,128 L146.817842,128 L99.4840387,256 L121.976629,256 L130.913039,230.977 L187.575039,230.977 L196.319607,256 L220.167172,256 L171.749388,128 Z M260.093778,128 L237.691519,128 L237.691519,256 L260.093778,256 L260.093778,128 Z M159.094727,149.47526 L181.409039,213.333 L137.135039,213.333 L159.094727,149.47526 Z M341.333333,256 L384,256 L384,298.666667 L341.333333,298.666667 L341.333333,256 Z M85.3333333,341.333333 L128,341.333333 L128,384 L85.3333333,384 L85.3333333,341.333333 Z M170.666667,341.333333 L213.333333,341.333333 L213.333333,384 L170.666667,384 L170.666667,341.333333 Z M85.3333333,0 L128,0 L128,42.6666667 L85.3333333,42.6666667 L85.3333333,0 Z M256,341.333333 L298.666667,341.333333 L298.666667,384 L256,384 L256,341.333333 Z M170.666667,0 L213.333333,0 L213.333333,42.6666667 L170.666667,42.6666667 L170.666667,0 Z M256,0 L298.666667,0 L298.666667,42.6666667 L256,42.6666667 L256,0 Z M341.333333,170.666667 L384,170.666667 L384,213.333333 L341.333333,213.333333 L341.333333,170.666667 Z M0,256 L42.6666667,256 L42.6666667,298.666667 L0,298.666667 L0,256 Z M341.333333,85.3333333 L384,85.3333333 L384,128 L341.333333,128 L341.333333,85.3333333 Z M0,170.666667 L42.6666667,170.666667 L42.6666667,213.333333 L0,213.333333 L0,170.666667 Z M0,85.3333333 L42.6666667,85.3333333 L42.6666667,128 L0,128 L0,85.3333333 Z" id="Combined-Shape">
+
+ </path>
+ </g>
+ </g>
+ </svg>
+ <?php _e( 'wpForo AI Features', 'wpforo' ); ?>
+ </h1>
+
+ <?php
+ // Display notices
+ if ( $notice ) {
+ wpforo_ai_display_notice( $notice );
+ }
+ ?>
+
+ <!-- Tab Navigation -->
+ <?php
+ // $current_tab is set at top of file (for conditional script loading)
+ $tabs = array(
+ 'overview' => __( 'Overview', 'wpforo' ),
+ );
+
+ // Only show AI feature tabs when subscription is active or trial
+ // For expired, inactive, pending_approval, not_connected, or error states - only show Overview
+ if ( in_array( $current_state, [ 'free_trial', 'paid_plan' ], true ) ) {
+ $tabs['rag_indexing'] = __( 'AI Content Indexing', 'wpforo' );
+ $tabs['ai_tasks'] = __( 'AI Tasks', 'wpforo' );
+ $tabs['analytics'] = __( 'AI Analytics', 'wpforo' );
+ $tabs['ai_logs'] = __( 'AI Logs', 'wpforo' );
+ }
+
+ // Force redirect to overview tab if user tries to access restricted tab
+ if ( ! isset( $tabs[ $current_tab ] ) ) {
+ $current_tab = 'overview';
+ }
+ ?>
+ <?php
+ // Get all active boards for AI Settings tab
+ $all_boards = WPF()->board->get_boards( [ 'status' => true ] );
+ $is_multiboard = count( $all_boards ) > 1;
+ ?>
+ <nav class="nav-tab-wrapper wpforo-ai-tabs">
+ <?php foreach ( $tabs as $tab_key => $tab_label ) : ?>
+ <?php
+ $tab_url = add_query_arg( array(
+ 'page' => 'wpforo-ai',
+ 'tab' => $tab_key,
+ ), admin_url( 'admin.php' ) );
+ $active_class = ( $current_tab === $tab_key ) ? 'nav-tab-active' : '';
+ ?>
+ <a href="<?php echo esc_url( $tab_url ); ?>" class="nav-tab <?php echo esc_attr( $active_class ); ?>">
+ <?php echo esc_html( $tab_label ); ?>
+ </a>
+ <?php endforeach; ?>
+
+ <?php
+ // AI Settings tab - links to board settings pages
+ // Only show when subscription is active (not expired, inactive, etc.)
+ $show_settings_tab = $is_connected && in_array( $current_state, [ 'free_trial', 'paid_plan' ], true );
+ ?>
+ <?php if ( $show_settings_tab && $is_multiboard ) : ?>
+ <span class="nav-tab wpforo-ai-settings-tab">
+ <span style="color: #000;"><?php _e( 'AI Settings', 'wpforo' ); ?></span>
+ [ <?php
+ $board_links = [];
+ foreach ( $all_boards as $board ) {
+ $boardid = (int) $board['boardid'];
+ // Build settings page URL: wpforo-settings for board 0, wpforo-{id}-settings for others
+ $settings_page = ( $boardid === 0 ) ? 'wpforo-settings' : 'wpforo-' . $boardid . '-settings';
+ $settings_url = admin_url( 'admin.php?page=' . $settings_page . '&wpf_tab=ai#wpf-settings-tab');
+ $board_links[] = sprintf(
+ '<a href="%s">%s</a>',
+ esc_url( $settings_url ),
+ esc_html( $board['title'] )
+ );
+ }
+ echo implode( ' | ', $board_links );
+ ?> ]
+ </span>
+ <?php elseif ( $show_settings_tab ) : ?>
+ <?php
+ // Single board - direct link to settings
+ $settings_url = admin_url( 'admin.php?page=wpforo-settings&wpf_tab=ai#wpf-settings-tab' );
+ ?>
+ <a href="<?php echo esc_url( $settings_url ); ?>" class="nav-tab wpforo-ai-settings-tab" style="cursor: pointer; display: flex; justify-content: center; align-items: center; gap: 7px;">
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M12,8a4,4,0,1,0,4,4A4,4,0,0,0,12,8Zm0,6a2,2,0,1,1,2-2A2,2,0,0,1,12,14Z"></path><path d="M21.294,13.9l-.444-.256a9.1,9.1,0,0,0,0-3.29l.444-.256a3,3,0,1,0-3-5.2l-.445.257A8.977,8.977,0,0,0,15,3.513V3A3,3,0,0,0,9,3v.513A8.977,8.977,0,0,0,6.152,5.159L5.705,4.9a3,3,0,0,0-3,5.2l.444.256a9.1,9.1,0,0,0,0,3.29l-.444.256a3,3,0,1,0,3,5.2l.445-.257A8.977,8.977,0,0,0,9,20.487V21a3,3,0,0,0,6,0v-.513a8.977,8.977,0,0,0,2.848-1.646l.447.258a3,3,0,0,0,3-5.2Zm-2.548-3.776a7.048,7.048,0,0,1,0,3.75,1,1,0,0,0,.464,1.133l1.084.626a1,1,0,0,1-1,1.733l-1.086-.628a1,1,0,0,0-1.215.165,6.984,6.984,0,0,1-3.243,1.875,1,1,0,0,0-.751.969V21a1,1,0,0,1-2,0V19.748a1,1,0,0,0-.751-.969A6.984,6.984,0,0,1,7.006,16.9a1,1,0,0,0-1.215-.165l-1.084.627a1,1,0,1,1-1-1.732l1.084-.626a1,1,0,0,0,.464-1.133,7.048,7.048,0,0,1,0-3.75A1,1,0,0,0,4.79,8.992L3.706,8.366a1,1,0,0,1,1-1.733l1.086.628A1,1,0,0,0,7.006,7.1a6.984,6.984,0,0,1,3.243-1.875A1,1,0,0,0,11,4.252V3a1,1,0,0,1,2,0V4.252a1,1,0,0,0,.751.969A6.984,6.984,0,0,1,16.994,7.1a1,1,0,0,0,1.215.165l1.084-.627a1,1,0,1,1,1,1.732l-1.084.626A1,1,0,0,0,18.746,10.125Z"></path></svg>
+ <?php _e( 'AI Settings', 'wpforo' ); ?>
+ </a>
+ <?php endif; ?>
+ </nav>
+
+ <!-- Tab Content -->
+ <div class="wpforo-ai-tab-content">
+ <?php
+ switch ( $current_tab ) {
+ case 'rag_indexing':
+ wpforo_ai_render_rag_indexing_tab( $is_connected, $status );
+ break;
+
+ case 'ai_tasks':
+ wpforo_ai_render_ai_tasks_tab( $is_connected, $status );
+ break;
+
+ case 'analytics':
+ wpforo_ai_render_analytics_tab( $is_connected, $status );
+ break;
+
+ case 'ai_logs':
+ wpforo_ai_render_ai_logs_tab( $is_connected, $status );
+ break;
+
+ case 'overview':
+ default:
+ // Display appropriate state
+ switch ( $current_state ) {
+ case 'not_connected':
+ wpforo_ai_render_not_connected_state();
+ break;
+
+ case 'pending_approval':
+ wpforo_ai_render_pending_approval_state( $status );
+ break;
+
+ case 'inactive':
+ wpforo_ai_render_inactive_state( $status );
+ break;
+
+ case 'free_trial':
+ wpforo_ai_render_free_trial_state( $status, $is_post_purchase );
+ break;
+
+ case 'paid_plan':
+ wpforo_ai_render_paid_plan_state( $status, $is_post_purchase );
+ break;
+
+ case 'expired':
+ wpforo_ai_render_expired_state( $status );
+ break;
+
+ case 'cancelled':
+ wpforo_ai_render_cancelled_state( $status );
+ break;
+
+ case 'error':
+ wpforo_ai_render_error_state( $connection_error ?? $status );
+ break;
+ }
+ break;
+ }
+ ?>
+ </div>
+
+</div>
+
+<div style="margin-bottom: 150px;"> </div>
+
+<!-- Global JavaScript for all tabs -->
+<script type="text/javascript">
+ jQuery(document).ready(function($) {
+ // Set AJAX nonce (ai-features.js will call init() automatically)
+ if (typeof window.WpForoAI !== 'undefined') {
+ WpForoAI.ajaxNonce = '<?php echo wp_create_nonce( 'wpforo_ai_features_nonce' ); ?>';
+ }
+
+ // Post-purchase notifications (works on all tabs)
+ <?php
+ $upgraded = isset( $_GET['upgraded'] ) && $_GET['upgraded'] == '1';
+ $credits_purchased = isset( $_GET['credits_purchased'] ) && $_GET['credits_purchased'] == '1';
+ $plan = isset( $_GET['plan'] ) ? sanitize_key( $_GET['plan'] ) : '';
+ $pack = isset( $_GET['pack'] ) ? sanitize_key( $_GET['pack'] ) : '';
+
+ if ( $upgraded || $credits_purchased ) {
+ // Clear status cache to force fresh fetch
+ delete_transient( 'wpforo_ai_tenant_status' );
+ }
+ ?>
+
+ <?php if ( $upgraded ) : ?>
+ // Purchase detected - show processing message
+ // The checkPostPurchaseRefresh() function will auto-refresh after 60 seconds
+ if (typeof WpForoAI !== 'undefined' && typeof WpForoAI.showNotice === 'function') {
+ WpForoAI.showNotice('Processing your <?php echo esc_js( ucfirst( $plan ) ); ?> plan purchase... Page will refresh in 60 seconds.', 'info');
+ }
+ <?php endif; ?>
+
+ <?php if ( $credits_purchased ) : ?>
+ // Credit pack purchase detected
+ if (typeof WpForoAI !== 'undefined' && typeof WpForoAI.showNotice === 'function') {
+ WpForoAI.showNotice('Processing your <?php echo esc_js( $pack ); ?> credits purchase... Your credits will be added shortly.', 'info');
+ }
+ <?php endif; ?>
+
+ <?php
+ // Show success messages after plan activation or credits added
+ $plan_activated = isset( $_GET['plan_activated'] ) && $_GET['plan_activated'] == '1';
+ $credits_added = isset( $_GET['credits_added'] ) && $_GET['credits_added'] == '1';
+ ?>
+
+ <?php if ( $plan_activated ) : ?>
+ // Plan successfully activated
+ if (typeof WpForoAI !== 'undefined' && typeof WpForoAI.showNotice === 'function') {
+ WpForoAI.showNotice('✓ Your subscription plan has been successfully activated!', 'success');
+ }
+ <?php endif; ?>
+
+ <?php if ( $credits_added ) : ?>
+ // Credits successfully added
+ if (typeof WpForoAI !== 'undefined' && typeof WpForoAI.showNotice === 'function') {
+ WpForoAI.showNotice('✓ Credits have been successfully added to your account!', 'success');
+ }
+ <?php endif; ?>
+ });
+</script>
--- a/wpforo/admin/pages/dashboard.php
+++ b/wpforo/admin/pages/dashboard.php
@@ -4,7 +4,7 @@
?>
<div id="wpf-admin-wrap" class="wrap">
- <h1 style="padding:30px 10px 10px;"><?php _e( 'Forum Dashboard', 'wpforo' ); ?></h1>
+ <h1 style="padding:10px 10px 0;"><?php _e( 'Forum Dashboard', 'wpforo' ); ?></h1>
<?php WPF()->notice->show() ?>
<div id="dashboard-widgets-wrap">
--- a/wpforo/admin/pages/forum.php
+++ b/wpforo/admin/pages/forum.php
@@ -120,6 +120,7 @@
}
if( ! empty( $_GET['parentid'] ) ) $selected_forumid = $_GET['parentid'];
$color = wpfval( $data, 'color' ) ? $data['color'] : wpforo_random_colors();
+ $is_boxed_layout = (int) wpfval( $data, 'layout' ) === 5;
?>
<style type="text/css">
#forum_layout .wpf-fl-box a:not(.wpf-lightbox) {
@@ -164,6 +165,8 @@
border: 1px solid #cccccc;
padding: 5px;
text-align: center;
+ display: flex;
+ flex-direction: column;
}
.wpf-fl-box h4 {
@@ -228,12 +231,20 @@
$('#wpfl-' + this.value).addClass('wpf-fl-active');
});
$('#use_us_cat').on('change', function () {
+ var isBoxedLayout = <?php echo $is_boxed_layout ? 'true' : 'false'; ?>;
if (!$(this).is(':checked')) {
- $('#wpf_forum_cover_field').hide();
+ if (!isBoxedLayout) {
+ $('#wpf_forum_cover_field').hide();
+ }
+ // Show private topics option for non-category forums
+ $('#wpf-private-topics-option').show();
<?php if( wpfkey( $data, 'is_cat' ) && ! wpfval( $data, 'is_cat' ) ): ?>
$('.wpf-fl-box').removeClass('wpf-fl-active');
var wpf_layout = $('#wpf-current-layout').val();
$('#wpfl-' + wpf_layout).addClass('wpf-fl-active');
+ <?php elseif( $is_boxed_layout ): ?>
+ $('.wpf-fl-box').removeClass('wpf-fl-active');
+ $('#wpfl-5').addClass('wpf-fl-active');
<?php else: ?>
$('.wpf-fl-box').removeClass('wpf-fl-active');
$('#forum_layout').hide();
@@ -244,6 +255,9 @@
var wpf_layout = $('#layout').find('option:selected').val();
$('#wpfl-' + wpf_layout).addClass('wpf-fl-active');
$('#forum_layout').show();
+ // Hide private topics option for categories (and uncheck it)
+ $('#wpf-private-topics-option').hide();
+ $('#make_topics_private').prop('checked', false);
}
});
});
@@ -376,6 +390,17 @@
</div>
<div style="clear: both"></div>
</div>
+ <div id="wpf-private-topics-option" class="wpf-forum-additional-options" style="margin-top: 15px;<?php echo ( isset( $data['is_cat'] ) && $data['is_cat'] == 1 ) ? ' display: none;' : ''; ?>">
+ <p class="form-field" style="margin: 0;">
+ <label for="make_topics_private" style="cursor: pointer;">
+ <?php _e( 'Make All Topics Private', 'wpforo' ); ?>
+ <input id="make_topics_private" type="checkbox" name="forum[type]" value="ticket_forum" <?php echo ( wpfval( $data, 'type' ) === 'ticket_forum' ) ? 'checked' : ''; ?> />
+ </label>
+ </p>
+ <p class="description" style="margin: 5px 0 0 0; font-style: italic; color: #666;">
+ <?php _e( 'When enabled, all topics in this forum will be created as private by default. Users cannot make topics public.', 'wpforo' ); ?>
+ </p>
+ </div>
</div>
</div>
@@ -386,7 +411,7 @@
<div id="postbox-container-2" class="postbox-container">
<div id="normal-sortables" class="meta-box-sortables ui-sortable">
- <div id="forum_layout" class="postbox" <?php if( ! wpfkey( $data, 'is_cat' ) ) echo 'style="display: none"'; ?>>
+ <div id="forum_layout" class="postbox" <?php if( ! wpfkey( $data, 'is_cat' ) && ! $is_boxed_layout ) echo 'style="display: none"'; ?>>
<h3 class="wpf-box-header">
<span>
<?php if( wpfkey( $data, 'is_cat' ) ): ?>
@@ -415,6 +440,7 @@
style="height: 100px;"/></a>
<a href="#_" class="wpf-lightbox" id="img1<?php echo intval( $layout['id'] ); ?>"><img
src="<?php echo WPFORO_URL ?>/themes/<?php echo esc_attr( $theme ) ?>/layouts/<?php echo intval( $layout['id'] ); ?>/view-forums.png"/></a>
+ <hr style="height: 1px; background-color: #666; width: 100%;" />
<a href="#img2<?php echo intval( $layout['id'] ); ?>"><img
src="<?php echo WPFORO_URL ?>/themes/<?php echo esc_attr( $theme ) ?>/layouts/<?php echo intval( $layout['id'] ); ?>/view-posts.png"
style="height: 100px;"/></a>
@@ -433,7 +459,7 @@
); ?></p>
- <?php $show_cover_field = ! (int) wpfval( $data, 'forumid' ) || (int) wpfval( $data, 'is_cat' ); ?>
+ <?php $show_cover_field = ! (int) wpfval( $data, 'forumid' ) || (int) wpfval( $data, 'is_cat' ) || $is_boxed_layout; ?>
<div id="wpf_forum_cover_field" style="padding: 10px; <?php echo( $show_cover_field ? 'display: block' : 'display: none' ) ?>">
<h3 style="font-size: 15px;"><?php _e( 'Category Cover Image', 'wpforo' ) ?></h3>
<div id="wpf-forum-cover" style="background-image: url('<?php echo esc_url_raw( (string) wpfval( $data, 'cover_url' ) ) ?>')">
--- a/wpforo/admin/pages/legal/privacy-policy.php
+++ b/wpforo/admin/pages/legal/privacy-policy.php
@@ -0,0 +1,564 @@
+<?php
+/**
+ * wpForo AI Features - Privacy Policy
+ *
+ * This document describes how we collect, use, and protect your data.
+ * Last updated: March 2026
+ *
+ * @since 3.0.0
+ */
+
+if( ! defined( 'ABSPATH' ) ) exit;
+?>
+
+<div class="wpforo-ai-legal-document">
+ <h1>wpForo AI Features - Privacy Policy</h1>
+ <p class="wpforo-ai-legal-updated"><strong>Last Updated:</strong> March 23, 2026</p>
+ <p class="wpforo-ai-legal-updated"><strong>Effective Date:</strong> March 23, 2026</p>
+
+ <div class="wpforo-ai-legal-toc">
+ <h3>Table of Contents</h3>
+ <ol>
+ <li><a href="#introduction">Introduction</a></li>
+ <li><a href="#definitions">Definitions</a></li>
+ <li><a href="#data-controller">Data Controller and Processor Roles</a></li>
+ <li><a href="#local-data">Data Stored Locally (Your Server)</a></li>
+ <li><a href="#cloud-data">Data Processed in the Cloud</a></li>
+ <li><a href="#how-we-use">How We Use Your Data</a></li>
+ <li><a href="#ai-processing">AI and Machine Learning Processing</a></li>
+ <li><a href="#data-sharing">Data Sharing and Third Parties</a></li>
+ <li><a href="#data-retention">Data Retention</a></li>
+ <li><a href="#data-security">Data Security</a></li>
+ <li><a href="#your-rights">Your Rights</a></li>
+ <li><a href="#international">International Data Transfers</a></li>
+ <li><a href="#children">Children's Privacy</a></li>
+ <li><a href="#changes">Changes to This Policy</a></li>
+ <li><a href="#contact">Contact Us</a></li>
+ </ol>
+ </div>
+
+ <hr>
+
+ <h2 id="introduction">1. Introduction</h2>
+ <p>wpForo AI features are provided by <a href="https://v3.wpforo.com/gvectors-ai/" target="_blank">gVectors AI</a>. This Privacy Policy explains how gVectors Team ("we", "us", "our") collects, uses, stores, and protects information when you use the wpForo AI Features service ("Service").</p>
+ <p>By using the Service, you agree to the collection and use of information as described in this Privacy Policy. This policy should be read in conjunction with our Terms of Service.</p>
+ <p><strong>Important:</strong> This Privacy Policy covers data processed by our Service. As a forum operator, you are responsible for your own privacy policy that governs how you collect and process your forum users' data.</p>
+
+ <h2 id="definitions">2. Definitions</h2>
+ <ul>
+ <li><strong>"Personal Data"</strong> means any information relating to an identified or identifiable natural person.</li>
+ <li><strong>"Forum Content"</strong> means topics, posts, replies, and other content created by users on your forum.</li>
+ <li><strong>"Vector Embeddings"</strong> means mathematical representations of text content used for semantic search.</li>
+ <li><strong>"Customer"</strong> means the forum administrator who registers for and uses the Service.</li>
+ <li><strong>"End Users"</strong> means visitors and registered users of your forum.</li>
+ <li><strong>"Processing"</strong> means any operation performed on data, including collection, storage, and deletion.</li>
+ </ul>
+
+ <h2 id="data-controller">3. Data Controller and Processor Roles</h2>
+
+ <h3>3.1 Your Role as Data Controller</h3>
+ <p>As the forum operator, <strong>you are the Data Controller</strong> for:</p>
+ <ul>
+ <li>Your forum users' personal data</li>
+ <li>Forum content created by your users</li>
+ <li>Decisions about what data to index and process</li>
+ </ul>
+ <p>You are responsible for:</p>
+ <ul>
+ <li>Obtaining necessary consents from your forum users</li>
+ <li>Updating your own privacy policy to disclose use of AI services</li>
+ <li>Responding to your users' data subject requests</li>
+ <li>Ensuring lawful basis for processing forum content</li>
+ </ul>
+
+ <h3>3.2 Our Role as Data Processor</h3>
+ <p>gVectors Team (through <a href="https://v3.wpforo.com/gvectors-ai/" target="_blank">gVectors AI</a>) acts as a <strong>Data Processor</strong> for forum content you submit to the Service. We:</p>
+ <ul>
+ <li>Process data only according to your instructions (indexing requests)</li>
+ <li>Implement appropriate security measures</li>
+ <li>Assist you in responding to data subject requests</li>
+ <li>Delete data upon your request or service termination</li>
+ </ul>
+
+ <h3>3.3 Our Role as Data Controller</h3>
+ <p>We are the <strong>Data Controller</strong> for:</p>
+ <ul>
+ <li>Your account information (email, site URL)</li>
+ <li>Billing and subscription data</li>
+ <li>Service usage analytics</li>
+ <li>Technical logs and operational data</li>
+ </ul>
+
+ <h2 id="local-data">4. Data Stored Locally (Your Server)</h2>
+ <p><strong>The following data is stored only on your WordPress server and is NOT transmitted to our cloud services:</strong></p>
+
+ <h3>4.1 Plugin Configuration</h3>
+ <ul>
+ <li>Feature enable/disable settings</li>
+ <li>Display preferences and customizations</li>
+ <li>Search result formatting options</li>
+ </ul>
+
+ <h3>4.2 Credentials (Encrypted)</h3>
+ <ul>
+ <li>API key (stored encrypted in WordPress database)</li>
+ <li>Tenant ID</li>
+ <li>Connection status</li>
+ </ul>
+
+ <h3>4.3 Cache Data</h3>
+ <ul>
+ <li>Temporary search results cache</li>
+ <li>Performance optimization data</li>
+ </ul>
+
+ <h3>4.4 Your Control Over Local Data</h3>
+ <p>You have complete control over locally stored data:</p>
+ <ul>
+ <li>Delete via plugin settings at any time</li>
+ <li>Included in standard WordPress backup procedures</li>
+ <li>Subject to your server's security measures</li>
+ <li>Never shared with us without explicit API calls</li>
+ </ul>
+
+ <h2 id="cloud-data">5. Data Processed in the Cloud</h2>
+ <p><strong>IMPORTANT: The following data IS transmitted to and processed by our cloud infrastructure when you use the Service.</strong></p>
+
+ <h3>5.1 Forum Content Data</h3>
+ <table class="wpforo-ai-legal-table">
+ <thead>
+ <tr>
+ <th>Data Type</th>
+ <th>Examples</th>
+ <th>Purpose</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>Topic Content</td>
+ <td>Titles, descriptions, body text</td>
+ <td>Indexing for semantic search</td>
+ </tr>
+ <tr>
+ <td>Replies</td>
+ <td>User responses, comments</td>
+ <td>Indexing for semantic search</td>
+ </tr>
+ <tr>
+ <td>Metadata</td>
+ <td>Post IDs, timestamps, forum categories</td>
+ <td>Result organization and filtering</td>
+ </tr>
+ <tr>
+ <td>Status Flags</td>
+ <td>Solved, best answer, closed</td>
+ <td>Search result ranking</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <h3>5.2 Account Information</h3>
+ <table class="wpforo-ai-legal-table">
+ <thead>
+ <tr>
+ <th>Data Type</th>
+ <th>Examples</th>
+ <th>Purpose</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>Administrator Email</td>
+ <td>admin@yourforum.com</td>
+ <td>Account identification, notifications</td>
+ </tr>
+ <tr>
+ <td>Site URL</td>
+ <td>https://yourforum.com</td>
+ <td>Origin validation, tenant identification</td>
+ </tr>
+ <tr>
+ <td>Software Versions</td>
+ <td>WordPress 6+, wpForo 3+</td>
+ <td>Compatibility and support</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <h3>5.3 Search Query Data</h3>
+ <table class="wpforo-ai-legal-table">
+ <thead>
+ <tr>
+ <th>Data Type</th>
+ <th>Examples</th>
+ <th>Purpose</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>Search Queries</td>
+ <td>"how to configure email notifications"</td>
+ <td>Processing search requests</td>
+ </tr>
+ <tr>
+ <td>Query Metadata</td>
+ <td>Timestamp, results count</td>
+ <td>Analytics and billing</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <h3>5.4 Usage and Billing Data</h3>
+ <table class="wpforo-ai-legal-table">
+ <thead>
+ <tr>
+ <th>Data Type</th>
+ <th>Examples</th>
+ <th>Purpose</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>Credit Usage</td>
+ <td>Credits consumed per operation</td>
+ <td>Billing and limits enforcement</td>
+ </tr>
+ <tr>
+ <td>API Calls</td>
+ <td>Request counts, endpoints accessed</td>
+ <td>Usage analytics, troubleshooting</td>
+ </tr>
+ <tr>
+ <td>Subscription Status</td>
+ <td>Plan type, expiration date</td>
+ <td>Service access control</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <h2 id="how-we-use">6. How We Use Your Data</h2>
+
+ <h3>6.1 Primary Purposes</h3>
+ <ul>
+ <li><strong>Providing the Service:</strong> Processing forum content to enable semantic search</li>
+ <li><strong>AI Features:</strong> Powering topic suggestions, related content, and search</li>
+ <li><strong>Account Management:</strong> Managing your subscription and authentication</li>
+ <li><strong>Billing:</strong> Tracking usage and processing payments</li>
+ </ul>
+
+ <h3>6.2 Operational Purposes</h3>
+ <ul>
+ <li><strong>Service Improvement:</strong> Analyzing aggregated usage patterns</li>
+ <li><strong>Technical Support:</strong> Diagnosing issues when you contact support</li>
+ <li><strong>Security:</strong> Detecting and preventing abuse or unauthorized access</li>
+ <li><strong>Legal Compliance:</strong> Meeting legal obligations</li>
+ </ul>
+
+ <h3>6.3 What We Do NOT Do</h3>
+ <ul>
+ <li style="color: #1e7e34">Sell your data to third parties</li>
+ <li style="color: #1e7e34">Use your forum content for advertising</li>
+ <li style="color: #1e7e34">Share your data with other customers</li>
+ <li style="color: #1e7e34">Train our own AI models on your specific content (without explicit consent)</li>
+ <li style="color: #1e7e34">Access your content except as needed to provide the Service</li>
+ </ul>
+
+ <h2 id="ai-processing">7. AI and Machine Learning Processing</h2>
+
+ <h3>7.1 AI Services Used</h3>
+ <p>Your forum content is processed using the following AI technologies:</p>
+
+ <h4>Amazon Bedrock</h4>
+ <ul>
+ <li><strong>Purpose:</strong> Managed AI/ML service for text embeddings</li>
+ <li><strong>Data Handling:</strong> Content processed in real-time; not stored by Amazon Bedrock</li>
+ <li><strong>Privacy:</strong> Subject to <a href="https://aws.amazon.com/bedrock/faqs/" target="_blank" rel="noopener">AWS Bedrock privacy practices</a></li>
+ </ul>
+
+ <h4>Amazon Nova Models</h4>
+ <ul>
+ <li><strong>Purpose:</strong> Foundation models for semantic understanding</li>
+ <li><strong>Data Handling:</strong> Content processed temporarily for embedding generation</li>
+ <li><strong>Training:</strong> Your content is NOT used to train Nova models</li>
+ </ul>
+
+ <h4>Anthropic Claude (via Bedrock)</h4>
+ <ul>
+ <li><strong>Purpose:</strong> Advanced language understanding for AI features</li>
+ <li><strong>Data Handling:</strong> Content processed temporarily; not stored by Anthropic</li>
+ <li><strong>Training:</strong> Your content is NOT used to train Claude models (API usage)</li>
+ </ul>
+
+ <h3>7.2 Vector Embeddings</h3>
+ <p>When we process your forum content:</p>
+ <ol>
+ <li>Text is converted into numerical vectors (by Amazon embedding models)</li>
+ <li>Original text is NOT stored in the vector database</li>
+ <li>Vectors capture semantic meaning but cannot be reversed to original text</li>
+ <li>Metadata (IDs, timestamps) is stored alongside vectors for result retrieval</li>
+ </ol>
+
+ <h3>7.3 AI Processing Limitations</h3>
+ <p>We commit to:</p>
+ <ul>
+ <li style="color: #1e7e34">Using AI only for providing Service features</li>
+ <li style="color: #1e7e34">Not using your content to train proprietary models</li>
+ <li style="color: #1e7e34">Not sharing your content with AI providers beyond processing needs</li>
+ <li style="color: #1e7e34">Providing transparency about AI processing methods</li>
+ </ul>
+
+ <h2 id="data-sharing">8. Data Sharing and Third Parties</h2>
+
+ <h3>8.1 Service Providers (Sub-processors)</h3>
+ <p>We share data with the following service providers who process data on our behalf:</p>
+
+ <table class="wpforo-ai-legal-table">
+ <thead>
+ <tr>
+ <th>Provider</th>
+ <th>Purpose</th>
+ <th>Data Shared</th>
+ <th>Location</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>Amazon Web Services (AWS)</td>
+ <td>Cloud infrastructure, AI processing</td>
+ <td>All cloud-processed data</td>
+ <td>United States (us-east-1) / Can be changed for customers with Enterprise Subscription Plan</td>
+ </tr>
+ <tr>
+ <td>Amazon Bedrock</td>
+ <td>AI embedding generation</td>
+ <td>Forum content (temporary)</td>
+ <td>United States</td>
+ </tr>
+ <tr>
+ <td>Freemius</td>
+ <td>Payment processing</td>
+ <td>Email, site URL, billing info</td>
+ <td>Israel/United States</td>
+ </tr>
+ <tr>
+ <td>Paddle</td>
+ <td>Payment processing (Merchant of Record)</td>
+ <td>Email, site URL, billing info</td>
+ <td>United Kingdom/United States</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <h3>8.2 Legal Requirements</h3>
+ <p>We may disclose data when required by:</p>
+ <ul>
+ <li>Valid legal process (court order, subpoena)</li>
+ <li>Law enforcement requests with proper authority</li>
+ <li>Protection of our rights, property, or safety</li>
+ <li>Emergency situations involving potential harm</li>
+ </ul>
+
+ <h3>8.3 Business Transfers</h3>
+ <p>In case of merger, acquisition, or sale of assets:</p>
+ <ul>
+ <li>Your data may be transferred to the acquiring entity</li>
+ <li>You will be notified of any such transfer</li>
+ <li>This Privacy Policy will continue to apply until you are notified otherwise</li>
+ </ul>
+
+ <h3>8.4 No Sale of Personal Data</h3>
+ <p style="color: #1e7e34">We do NOT sell, rent, or trade your personal data or forum content to any third parties for their marketing purposes.</p>
+
+ <h2 id="data-retention">9. Data Retention</h2>
+
+ <h3>9.1 Retention Periods</h3>
+ <table class="wpforo-ai-legal-table">
+ <thead>
+ <tr>
+ <th>Data Type</th>
+ <th>Retention Period</th>
+ <th>Deletion Method</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr>
+ <td>Vector Embeddings</td>
+ <td>Until deletion request or account termination</td>
+ <td>Automatic on disconnect + 30 days</td>
+ </tr>
+ <tr>
+ <td>Account Information</td>
+ <td>Duration of subscription + 30 days</td>
+ <td>Automatic after grace period</td>
+ </tr>
+ <tr>
+ <td>API Logs</td>
+ <td>90 days</td>
+ <td>Automatic TTL deletion</td>
+ </tr>
+ <tr>
+ <td>Usage Analytics</td>
+ <td>90 days</td>
+ <td>Automatic TTL deletion</td>
+ </tr>
+ <tr>
+ <td>Billing Records</td>
+ <td>7 years (legal requirement)</td>
+ <td>Manual after legal period</td>
+ </tr>
+ <tr>
+ <td>Support Tickets</td>
+ <td>2 years after resolution</td>
+ <td>Manual review and deletion</td>
+ </tr>
+ </tbody>
+ </table>
+
+ <h3>9.2 Deletion Upon Request</h3>
+ <p>You can request immediate deletion of your data:</p>
+ <ul>
+ <li><strong>Forum Content:</strong> Use "Clear All Indexed Data" in plugin settings</li>
+ <li><strong>Account Data:</strong> Disconnect and request full deletion via support</li>
+ <li><strong>All Data:</strong> Contact support for complete data erasure</li>
+ </ul>
+
+ <h3>9.3 Post-Termination</h3>
+ <ul>
+ <li>30-day grace period for reactivation</li>
+ <li>After 30 days: Permanent deletion of all cloud data</li>
+ <li>Billing records retained as legally required</li>
+ <li>Aggregated, anonymized statistics may be retained</li>
+ </ul>
+
+ <h2 id="data-security">10. Data Security</h2>
+
+ <h3>10.1 Technical Measures</h3>
+ <ul>
+ <li><strong>Encryption in Transit:</strong> TLS 1.2+ for all communications</li>
+ <li><strong>Encryption at Rest:</strong> AES-256 encryption for all stored data</li>
+ <li><strong>Access Control:</strong> IAM-based least-privilege access</li>
+ <li><strong>Network Security:</strong> AWS VPC isolation, security groups</li>
+ <li><strong>API