Below is a differential between the unpatched vulnerable code and the patched update, for reference.
--- a/wpforo/admin/pages/ai-features.php
+++ b/wpforo/admin/pages/ai-features.php
@@ -18,6 +18,7 @@
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-wp-indexing.php';
+require_once __DIR__ . '/tabs/ai-features-tab-ai-tools.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';
@@ -42,6 +43,13 @@
wp_enqueue_script( 'wpforo-ai-wp-indexing', WPFORO_URL . '/admin/assets/js/ai-features-wp-indexing.js', [ 'jquery' ], WPFORO_VERSION, true );
}
+// File Indexing tab - load isolated scripts/styles and media library
+if ( $current_tab === 'ai_tools' ) {
+ wp_enqueue_media();
+ wp_enqueue_style( 'wpforo-ai-tools', WPFORO_URL . '/admin/assets/css/ai-features-tools.css', [ 'wpforo-ai-features' ], WPFORO_VERSION );
+ wp_enqueue_script( 'wpforo-ai-tools', WPFORO_URL . '/admin/assets/js/ai-features-tools.js', [ 'jquery', 'wpforo-ai-features' ], WPFORO_VERSION, true );
+}
+
// Localize script with AJAX URL and nonce
wp_localize_script( 'wpforo-ai-features', 'wpforoAIAdmin', [
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
@@ -163,6 +171,11 @@
$tabs['wp_indexing'] = __( 'WordPress Indexing', 'wpforo' );
}
+ // File Indexing tab - only show if custom_knowledge feature is available (Business+ plan + cloud storage)
+ if ( isset( WPF()->ai_client ) && WPF()->ai_client->is_feature_available( 'custom_knowledge' ) ) {
+ $tabs['ai_tools'] = __( 'File Indexing', 'wpforo' );
+ }
+
$tabs['ai_tasks'] = __( 'AI Tasks', 'wpforo' );
$tabs['analytics'] = __( 'AI Analytics', 'wpforo' );
$tabs['ai_logs'] = __( 'AI Logs', 'wpforo' );
@@ -240,6 +253,10 @@
wpforo_ai_render_wp_indexing_tab( $is_connected, $status );
break;
+ case 'ai_tools':
+ wpforo_ai_render_ai_tools_tab( $is_connected, $status );
+ break;
+
case 'ai_tasks':
wpforo_ai_render_ai_tasks_tab( $is_connected, $status );
break;
--- a/wpforo/admin/pages/board.php
+++ b/wpforo/admin/pages/board.php
@@ -153,7 +153,7 @@
<div class="wpf-board-box">
<div class="wpf-board-is_standalone">
<label>
- <?php _e( 'Turn WordPress to this forum board', 'wpforo' ) ?> <a href="https://wpforo.com/docs/wpforo-v2/getting-started/forum-page/turn-wordpress-to-wpforo/" title="<?php _e( 'Read the documentation', 'wpforo' ) ?>" target="_blank"><i class="far fa-question-circle"></i></a>
+ <?php _e( 'Turn WordPress to this forum board', 'wpforo' ) ?> <a href="https://wpforo.com/docs/wpforo-v3/getting-started/forum-page/turn-wordpress-to-wpforo/" title="<?php _e( 'Read the documentation', 'wpforo' ) ?>" target="_blank"><i class="far fa-question-circle"></i></a>
<p class="wpf-info"><?php _e( 'This option will disable WordPress on front-end. Only forum pages and excluded post/pages will be available. wpForo will look like as a stand-alone forum.', 'wpforo' ) ?></p>
</label>
<div class="wpf-switch-field">
--- a/wpforo/admin/pages/forum.php
+++ b/wpforo/admin/pages/forum.php
@@ -67,7 +67,7 @@
<div class="wpf-info-bar"
style="line-height: 1em; clear:both; padding: 5px 50px; box-sizing: border-box; font-size:15px; display:block; box-shadow:none; margin: 20px 0 10px 0; font-style: italic; background: #FFFFFF; width:100%; position: relative;">
- <a href="https://wpforo.com/docs/wpforo-v2/categories-and-forums/forum-manager/" title="<?php _e( 'Read the documentation', 'wpforo' ) ?>" target="_blank"
+ <a href="https://wpforo.com/docs/wpforo-v3/categories-and-forums/forum-manager/" title="<?php _e( 'Read the documentation', 'wpforo' ) ?>" target="_blank"
style="font-size: 16px; position: absolute; right: 15px; top: 15px;"><i class="far fa-question-circle"></i></a>
<ul style="list-style-type: disc; line-height:18px;">
<li style="list-style:none; margin-left:-17px; font-style:normal; font-weight:bold; padding-bottom: 5px;"><i class="fas fa-info-circle" aria-hidden="true"></i> <?php _e(
@@ -308,7 +308,7 @@
<div id="side-sortables" class="meta-box-sortables ui-sortable">
<div id="forum_cat" class="postbox">
<h3 class="wpf-box-header"><span><?php _e( 'Forum Options', 'wpforo' ); ?> <a
- href="https://wpforo.com/docs/wpforo-v2/categories-and-forums/forum-manager/add-new-forum/#forum-options"
+ href="https://wpforo.com/docs/wpforo-v3/categories-and-forums/forum-manager/add-new-forum/#forum-options"
title="<?php _e( 'Read the documentation', 'wpforo' ) ?>" target="_blank" style="font-size: 14px;"><i class="far fa-question-circle"></i></a></span>
</h3>
<div class="inside">
@@ -369,7 +369,7 @@
<div id="forum_permissions" class="postbox">
<h3 class="wpf-box-header"><span><?php _e( 'Forum Permissions', 'wpforo' ); ?> <a
- href="https://wpforo.com/docs/wpforo-v2/categories-and-forums/forum-manager/add-new-forum/#forum-permissions"
+ href="https://wpforo.com/docs/wpforo-v3/categories-and-forums/forum-manager/add-new-forum/#forum-permissions"
title="<?php _e( 'Read the documentation', 'wpforo' ) ?>" target="_blank" style="font-size: 14px;"><i class="far fa-question-circle"></i></a></span>
</h3>
<div class="inside">
@@ -423,7 +423,7 @@
<?php else: ?>
<?php _e( 'Category layout and cover image', 'wpforo' ); ?>
<?php endif; ?>
- <a href="https://wpforo.com/docs/wpforo-v2/categories-and-forums/forum-layouts/" title="<?php _e( 'Read the documentation', 'wpforo' ) ?>"
+ <a href="https://wpforo.com/docs/wpforo-v3/categories-and-forums/forum-layouts/" title="<?php _e( 'Read the documentation', 'wpforo' ) ?>"
target="_blank" style="font-size: 14px;"><i class="far fa-question-circle"></i></a>
</span>
</h3>
@@ -519,7 +519,7 @@
<div id="forum_slug" class="postbox">
<h3 class="wpf-box-header"><span><?php _e( 'Forum Slug', 'wpforo' ); ?> <a
- href="https://wpforo.com/docs/wpforo-v2/categories-and-forums/forum-manager/add-new-forum/#forum-slug"
+ href="https://wpforo.com/docs/wpforo-v3/categories-and-forums/forum-manager/add-new-forum/#forum-slug"
title="<?php _e( 'Read the documentation', 'wpforo' ) ?>" target="_blank" style="font-size: 14px;"><i class="far fa-question-circle"></i></a></span>
</h3>
<div class="inside">
@@ -531,7 +531,7 @@
<div id="forum_icon" class="postbox">
<h3 class="wpf-box-header"><span><?php _e( 'Forum Icon', 'wpforo' ); ?> <a
- href="https://wpforo.com/docs/wpforo-v2/categories-and-forums/forum-manager/add-new-forum/#forum-icon"
+ href="https://wpforo.com/docs/wpforo-v3/categories-and-forums/forum-manager/add-new-forum/#forum-icon"
title="<?php _e( 'Read the documentation', 'wpforo' ) ?>" target="_blank" style="font-size: 14px;"><i class="far fa-question-circle"></i></a></span>
</h3>
<div class="inside" style="padding-top:10px;">
--- a/wpforo/admin/pages/phrase.php
+++ b/wpforo/admin/pages/phrase.php
@@ -165,7 +165,7 @@
<tr>
<td style="padding-bottom: 10px;">
<label for="langid" style="font-weight: bold;"><?php _e( 'Manage Phrases using XML File', 'wpforo' ); ?> <a
- href="https://wpforo.com/docs/wpforo-v2/wpforo-settings/general-settings/#xml-language" title="<?php _e( 'Read the documentation', 'wpforo' ) ?>" target="_blank"><i
+ href="https://wpforo.com/docs/wpforo-v3/wpforo-settings/general-settings/#xml-language" title="<?php _e( 'Read the documentation', 'wpforo' ) ?>" target="_blank"><i
class="far fa-question-circle"></i></a></label>
<p class="wpf-info"><?php _e(
'This option is only related to XML language files. You should upload a translation XML file to have a new language option in this drop-down. If you are using PO/MO translation files you should change WordPress Language in Dashboard > Settings admin page to load according translation for wpForo.',
--- a/wpforo/admin/pages/settings.php
+++ b/wpforo/admin/pages/settings.php
@@ -17,7 +17,7 @@
<?php //esc_html_e("wpForo", "wpforo") ?>
</div>
<div class="wpf-head-info">
- <span><a href="https://wpforo.com/docs/wpforo-v2/" target="_blank"><?php esc_html_e( "Documentation", "wpforo" ); ?></a></span>
+ <span><a href="https://wpforo.com/docs/wpforo-v3/" target="_blank"><?php esc_html_e( "Documentation", "wpforo" ); ?></a></span>
<span><a href="https://wpforo.com/community/" target="_blank"><?php esc_html_e( "Support", "wpforo" ); ?></a></span>
<span><a href="https://gvectors.com/product-category/wpforo/" target="_blank"><?php esc_html_e( "Addons", "wpforo" ); ?></a></span>
</div>
--- a/wpforo/admin/pages/tabs/ai-features-tab-ai-tools.php
+++ b/wpforo/admin/pages/tabs/ai-features-tab-ai-tools.php
@@ -0,0 +1,430 @@
+<?php
+/**
+ * AI Features - AI Tools Tab (Custom Knowledge)
+ *
+ * Allows Business+ tenants to upload custom knowledge files (JSON/Markdown/Text)
+ * and configure priority settings for AI features.
+ *
+ * @package wpForo
+ * @subpackage Admin
+ * @since 3.0.0
+ */
+
+if ( ! defined( 'ABSPATH' ) ) {
+ exit;
+}
+
+/**
+ * Render AI Tools tab content
+ *
+ * @param bool $is_connected Whether tenant is connected to AI service
+ * @param array $status Tenant status data from API
+ */
+function wpforo_ai_render_ai_tools_tab( $is_connected, $status ) {
+ if ( ! $is_connected ) {
+ ?>
+ <div class="wpforo-ai-box wpforo-ai-not-connected-notice">
+ <div class="wpforo-ai-box-body">
+ <div class="wpforo-ai-status-badge status-warning">
+ <span class="dashicons dashicons-warning"></span>
+ <?php _e( 'Not Connected', 'wpforo' ); ?>
+ </div>
+ <p><?php _e( 'Please connect to wpForo AI API first in the Overview tab to enable AI Tools.', 'wpforo' ); ?></p>
+ <a href="<?php echo esc_url( admin_url( 'admin.php?page=wpforo-ai&tab=overview' ) ); ?>" class="button button-primary">
+ <?php _e( 'Go to Overview', 'wpforo' ); ?>
+ </a>
+ </div>
+ </div>
+ <?php
+ return;
+ }
+
+ // Feature gate check - Business+ plan required
+ $custom_knowledge_available = isset( WPF()->ai_client ) && WPF()->ai_client->is_feature_available( 'custom_knowledge' );
+ if ( ! $custom_knowledge_available ) {
+ ?>
+ <div class="wpforo-ai-box wpforo-ai-upgrade-notice">
+ <div class="wpforo-ai-box-body">
+ <div class="wpforo-ai-status-badge status-warning">
+ <span class="dashicons dashicons-lock"></span>
+ <?php _e( 'Business Plan Required', 'wpforo' ); ?>
+ </div>
+ <p><?php _e( 'Custom Knowledge is available on Business and Enterprise plans. Upgrade to upload your own knowledge files for AI-powered search, chatbot, and bot replies.', 'wpforo' ); ?></p>
+ <a href="<?php echo esc_url( admin_url( 'admin.php?page=wpforo-ai&tab=overview' ) ); ?>" class="button button-primary">
+ <?php _e( 'View Plans', 'wpforo' ); ?>
+ </a>
+ </div>
+ </div>
+ <?php
+ return;
+ }
+
+ // Check storage mode - custom knowledge requires cloud mode
+ $storage_manager = WPF()->vector_storage->for_board( 0 );
+ $storage_mode = $storage_manager->get_storage_mode();
+
+ if ( $storage_mode !== 'cloud' ) {
+ ?>
+ <div class="wpforo-ai-box wpforo-ai-upgrade-notice">
+ <div class="wpforo-ai-box-body">
+ <div class="wpforo-ai-status-badge status-warning">
+ <span class="dashicons dashicons-cloud"></span>
+ <?php _e( 'Cloud Storage Required', 'wpforo' ); ?>
+ </div>
+ <p><?php _e( 'Custom Knowledge requires Cloud storage mode. Please switch to Cloud storage in the Forum Indexing tab to use this feature.', 'wpforo' ); ?></p>
+ <a href="<?php echo esc_url( admin_url( 'admin.php?page=wpforo-ai&tab=rag_indexing' ) ); ?>" class="button button-primary">
+ <?php _e( 'Go to Forum Indexing', 'wpforo' ); ?>
+ </a>
+ </div>
+ </div>
+ <?php
+ return;
+ }
+
+ // Get remaining credits
+ $remaining_credits = 0;
+ if ( isset( $status['subscription']['credits_remaining'] ) ) {
+ $remaining_credits = (int) $status['subscription']['credits_remaining'];
+ }
+
+ // Get all boards for board settings
+ $all_boards = WPF()->board->get_boards( [ 'status' => true ] );
+ $is_multiboard = count( $all_boards ) > 1;
+ ?>
+
+ <div class="wpforo-ai-tools-tab">
+
+ <!-- Add Custom Knowledge Box -->
+ <div class="wpforo-ai-box wpforo-ai-knowledge-upload-box">
+ <div class="wpforo-ai-box-header">
+ <h2>
+ <span class="dashicons dashicons-database-add"></span>
+ <?php _e( 'Add Custom Knowledge', 'wpforo' ); ?>
+ </h2>
+ </div>
+ <div class="wpforo-ai-box-body">
+ <p class="wpforo-ai-section-desc kind-text">
+ <span class="dashicons dashicons-media-text desc-icon"></span>
+ <?php _e( 'Add JSON, Markdown, or Text files to enhance AI search, chatbot, and bot replies with your custom content.', 'wpforo' ); ?>
+ </p>
+
+ <form id="wpforo-ai-knowledge-upload-form" class="wpforo-ai-knowledge-form kind-text">
+ <div class="wpforo-ai-url-input-group">
+ <input type="url"
+ id="knowledge-file-url"
+ name="file_url"
+ placeholder="<?php esc_attr_e( 'Select a TXT, MD, or JSON file from Media Library or paste URL', 'wpforo' ); ?>"
+ required>
+ <button type="button" class="button" id="knowledge-media-btn">
+ <span class="dashicons dashicons-admin-media"></span>
+ <?php _e( 'Media Library', 'wpforo' ); ?>
+ </button>
+ <button type="submit" class="button button-primary" id="knowledge-upload-btn" style="width: 20%; text-align: center; display: inline-block;">
+ <?php _e( 'Index Text File', 'wpforo' ); ?>
+ </button>
+ </div>
+
+ <!-- Hidden fields for auto-detected values -->
+ <input type="hidden" id="knowledge-file-type" name="file_type" value="text">
+ <input type="hidden" id="knowledge-file-name" name="name" value="">
+
+ <div class="wpforo-ai-upload-info">
+ <div class="wpforo-ai-file-detected" id="knowledge-file-detected" style="display: none;">
+ <span class="file-icon"></span>
+ <span class="file-name"></span>
+ <span class="file-type-badge"></span>
+ </div>
+ <div class="wpforo-ai-credits-info">
+ <span class="dashicons dashicons-database"></span>
+ <?php printf(
+ __( 'Credits: %s remaining', 'wpforo' ),
+ '<strong>' . number_format( $remaining_credits ) . '</strong>'
+ ); ?>
+ <span class="wpforo-ai-credit-rate"><?php _e( '(1 credit per 100KB or 1 file with small size, max 20MB)', 'wpforo' ); ?></span>
+ </div>
+ </div>
+
+ <div class="wpforo-ai-upload-progress" id="knowledge-upload-progress" style="display: none;">
+ <div class="wpforo-ai-progress-bar">
+ <div class="wpforo-ai-progress-fill"></div>
+ </div>
+ <div class="wpforo-ai-progress-text"></div>
+ </div>
+ </form>
+
+ <p class="wpforo-ai-section-desc kind-pdf" style="margin-top: 24px;">
+ <span class="dashicons dashicons-media-document desc-icon"></span>
+ <?php _e( 'Add PDF files to enhance AI search, chatbot, and bot replies with your custom content.', 'wpforo' ); ?>
+ </p>
+
+ <form id="wpforo-ai-knowledge-pdf-upload-form" class="wpforo-ai-knowledge-form kind-pdf">
+ <div class="wpforo-ai-url-input-group">
+ <input type="url"
+ id="knowledge-pdf-file-url"
+ name="file_url"
+ placeholder="<?php esc_attr_e( 'Select a PDF from Media Library or paste URL', 'wpforo' ); ?>"
+ required>
+ <button type="button" class="button" id="knowledge-pdf-media-btn">
+ <span class="dashicons dashicons-admin-media"></span>
+ <?php _e( 'Media Library', 'wpforo' ); ?>
+ </button>
+ <button type="submit" class="button button-primary" id="knowledge-pdf-upload-btn" style="width: 20%; text-align: center; display: inline-block;">
+ <?php _e( 'Index PDF File', 'wpforo' ); ?>
+ </button>
+ </div>
+
+ <input type="hidden" id="knowledge-pdf-file-type" name="file_type" value="pdf">
+ <input type="hidden" id="knowledge-pdf-file-name" name="name" value="">
+
+ <div class="wpforo-ai-upload-info">
+ <div class="wpforo-ai-file-detected" id="knowledge-pdf-file-detected" style="display: none;">
+ <span class="file-icon"></span>
+ <span class="file-name"></span>
+ <span class="file-type-badge"></span>
+ </div>
+ <div class="wpforo-ai-credits-info">
+ <span class="dashicons dashicons-database"></span>
+ <?php printf(
+ __( 'Credits: %s remaining', 'wpforo' ),
+ '<strong>' . number_format( $remaining_credits ) . '</strong>'
+ ); ?>
+ <span class="wpforo-ai-credit-rate"><?php _e( '(1 credit per 1 page, max 50MB)', 'wpforo' ); ?></span>
+ </div>
+ </div>
+
+ <div class="wpforo-ai-upload-progress" id="knowledge-pdf-upload-progress" style="display: none;">
+ <div class="wpforo-ai-progress-bar">
+ <div class="wpforo-ai-progress-fill"></div>
+ </div>
+ <div class="wpforo-ai-progress-text"></div>
+ </div>
+ </form>
+
+ <details class="wpforo-ai-file-format-help">
+ <summary><?php _e( 'Supported file formats', 'wpforo' ); ?></summary>
+ <div class="wpforo-ai-format-list">
+ <div class="format-item">
+ <strong>.json</strong> - <?php _e( 'Array of objects with "title" and "content" fields (recommended)', 'wpforo' ); ?>
+ </div>
+ <div class="format-item">
+ <strong>.md</strong> - <?php _e( 'Markdown with ## headings to split into chunks', 'wpforo' ); ?>
+ </div>
+ <div class="format-item">
+ <strong>.txt</strong> - <?php _e( 'Plain text, split by "---" separators or paragraphs', 'wpforo' ); ?>
+ </div>
+ <div class="format-item">
+ <strong>.pdf</strong> - <?php _e( 'PDF documents — each page becomes a section. Scanned/image-only PDFs are auto-OCR'd when text is too sparse.', 'wpforo' ); ?>
+ </div>
+ </div>
+ </details>
+ </div>
+ </div>
+
+ <!-- Indexed Knowledge Files Box -->
+ <div class="wpforo-ai-box wpforo-ai-knowledge-files-box">
+ <div class="wpforo-ai-box-header">
+ <h2>
+ <span class="dashicons dashicons-media-document"></span>
+ <?php _e( 'Indexed Knowledge Files', 'wpforo' ); ?>
+ <span class="wpforo-ai-file-count" id="knowledge-file-count"></span>
+ </h2>
+ <div class="wpforo-ai-header-actions">
+ <button type="button" class="button button-small" id="knowledge-refresh-files">
+ <span class="dashicons dashicons-update"></span>
+ <?php _e( 'Refresh', 'wpforo' ); ?>
+ </button>
+ </div>
+ </div>
+ <div class="wpforo-ai-box-body">
+ <div id="knowledge-files-loading" class="wpforo-ai-loading">
+ <span class="spinner is-active"></span>
+ <?php _e( 'Loading files...', 'wpforo' ); ?>
+ </div>
+
+ <div id="knowledge-files-empty" class="wpforo-ai-empty-state" style="display: none;">
+ <span class="dashicons dashicons-portfolio"></span>
+ <p><?php _e( 'No knowledge files indexed yet. Upload your first file above.', 'wpforo' ); ?></p>
+ </div>
+
+ <table id="knowledge-files-table" class="wp-list-table widefat fixed striped" style="display: none;">
+ <thead>
+ <tr>
+ <th class="column-name"><?php _e( 'Name', 'wpforo' ); ?></th>
+ <th class="column-type"><?php _e( 'Type', 'wpforo' ); ?></th>
+ <th class="column-size"><?php _e( 'Size', 'wpforo' ); ?></th>
+ <th class="column-chunks"><?php _e( 'Chunks', 'wpforo' ); ?></th>
+ <th class="column-credits"><?php _e( 'Credits', 'wpforo' ); ?></th>
+ <th class="column-status"><?php _e( 'Status', 'wpforo' ); ?></th>
+ <th class="column-actions"><?php _e( 'Actions', 'wpforo' ); ?></th>
+ </tr>
+ </thead>
+ <tbody id="knowledge-files-tbody">
+ </tbody>
+ <tfoot>
+ <tr class="wpforo-ai-files-totals">
+ <td class="column-name"><strong><?php _e( 'Total', 'wpforo' ); ?></strong></td>
+ <td class="column-type"></td>
+ <td class="column-size" id="knowledge-total-size">-</td>
+ <td class="column-chunks" id="knowledge-total-chunks">0</td>
+ <td class="column-credits" id="knowledge-total-credits">0</td>
+ <td class="column-status"></td>
+ <td class="column-actions"></td>
+ </tr>
+ </tfoot>
+ </table>
+ </div>
+ </div>
+
+ <!-- Board Settings Box -->
+ <div class="wpforo-ai-box wpforo-ai-knowledge-settings-box">
+ <div class="wpforo-ai-box-header">
+ <h2>
+ <span class="dashicons dashicons-admin-settings"></span>
+ <?php _e( 'Board Settings', 'wpforo' ); ?>
+ </h2>
+ </div>
+ <div class="wpforo-ai-box-body">
+ <?php if ( $is_multiboard ) : ?>
+ <div class="wpforo-ai-board-selector">
+ <label for="knowledge-board-select"><?php _e( 'Select Board:', 'wpforo' ); ?></label>
+ <select id="knowledge-board-select">
+ <?php foreach ( $all_boards as $board ) : ?>
+ <option value="<?php echo esc_attr( $board['boardid'] ); ?>">
+ <?php echo esc_html( $board['title'] ); ?>
+ </option>
+ <?php endforeach; ?>
+ </select>
+ </div>
+ <?php else : ?>
+ <input type="hidden" id="knowledge-board-select" value="0">
+ <?php endif; ?>
+
+ <form id="wpforo-ai-knowledge-settings-form" class="wpforo-ai-settings-form">
+ <input type="hidden" name="board_id" id="knowledge-settings-board-id" value="0">
+
+ <div id="knowledge-settings-loading" class="wpforo-ai-loading">
+ <span class="spinner is-active"></span>
+ <?php _e( 'Loading settings...', 'wpforo' ); ?>
+ </div>
+
+ <div id="knowledge-settings-content" style="display: none;">
+ <!-- Enable/Disable Toggle -->
+ <div class="wpforo-ai-setting-row wpforo-ai-enable-row">
+ <label class="wpforo-ai-toggle-label">
+ <span class="wpforo-ai-toggle">
+ <input type="checkbox" name="enabled" id="knowledge-enabled" value="1">
+ <span class="wpforo-ai-toggle-slider"></span>
+ </span>
+ <span class="toggle-text"><?php _e( 'Enable Custom Knowledge for this board', 'wpforo' ); ?></span>
+ </label>
+ <p class="description"><?php _e( 'When enabled, AI Search, Chatbot, and Bot Reply will include your custom knowledge files.', 'wpforo' ); ?></p>
+ </div>
+
+ <!-- Priority Settings (hidden by default, shown when enabled) -->
+ <div id="knowledge-priority-section" style="display: none;">
+ <h4><?php _e( 'Source Priority', 'wpforo' ); ?></h4>
+ <p class="description"><?php _e( 'Higher priority sources appear first and influence AI responses more.', 'wpforo' ); ?></p>
+
+ <div class="wpforo-ai-priority-row">
+ <label><?php _e( 'AI Search', 'wpforo' ); ?></label>
+ <div class="wpforo-ai-priority-selects">
+ <select name="search_priority[]" class="priority-select" data-feature="search">
+ <option value="forum"><?php _e( 'Forum', 'wpforo' ); ?></option>
+ <option value="wordpress"><?php _e( 'WordPress', 'wpforo' ); ?></option>
+ <option value="custom_knowledge"><?php _e( 'Custom Knowledge', 'wpforo' ); ?></option>
+ </select>
+ <span class="priority-arrow">→</span>
+ <select name="search_priority[]" class="priority-select" data-feature="search">
+ <option value="wordpress"><?php _e( 'WordPress', 'wpforo' ); ?></option>
+ <option value="forum"><?php _e( 'Forum', 'wpforo' ); ?></option>
+ <option value="custom_knowledge"><?php _e( 'Custom Knowledge', 'wpforo' ); ?></option>
+ </select>
+ <span class="priority-arrow">→</span>
+ <select name="search_priority[]" class="priority-select" data-feature="search">
+ <option value="custom_knowledge"><?php _e( 'Custom Knowledge', 'wpforo' ); ?></option>
+ <option value="forum"><?php _e( 'Forum', 'wpforo' ); ?></option>
+ <option value="wordpress"><?php _e( 'WordPress', 'wpforo' ); ?></option>
+ </select>
+ </div>
+ </div>
+
+ <div class="wpforo-ai-priority-row">
+ <label><?php _e( 'AI Chatbot', 'wpforo' ); ?></label>
+ <div class="wpforo-ai-priority-selects">
+ <select name="chat_priority[]" class="priority-select" data-feature="chat">
+ <option value="custom_knowledge"><?php _e( 'Custom Knowledge', 'wpforo' ); ?></option>
+ <option value="forum"><?php _e( 'Forum', 'wpforo' ); ?></option>
+ <option value="wordpress"><?php _e( 'WordPress', 'wpforo' ); ?></option>
+ </select>
+ <span class="priority-arrow">→</span>
+ <select name="chat_priority[]" class="priority-select" data-feature="chat">
+ <option value="forum"><?php _e( 'Forum', 'wpforo' ); ?></option>
+ <option value="custom_knowledge"><?php _e( 'Custom Knowledge', 'wpforo' ); ?></option>
+ <option value="wordpress"><?php _e( 'WordPress', 'wpforo' ); ?></option>
+ </select>
+ <span class="priority-arrow">→</span>
+ <select name="chat_priority[]" class="priority-select" data-feature="chat">
+ <option value="wordpress"><?php _e( 'WordPress', 'wpforo' ); ?></option>
+ <option value="forum"><?php _e( 'Forum', 'wpforo' ); ?></option>
+ <option value="custom_knowledge"><?php _e( 'Custom Knowledge', 'wpforo' ); ?></option>
+ </select>
+ </div>
+ </div>
+
+ <div class="wpforo-ai-priority-row">
+ <label><?php _e( 'Bot Reply', 'wpforo' ); ?></label>
+ <div class="wpforo-ai-priority-selects">
+ <select name="bot_reply_priority[]" class="priority-select" data-feature="bot_reply">
+ <option value="forum"><?php _e( 'Forum', 'wpforo' ); ?></option>
+ <option value="custom_knowledge"><?php _e( 'Custom Knowledge', 'wpforo' ); ?></option>
+ <option value="wordpress"><?php _e( 'WordPress', 'wpforo' ); ?></option>
+ </select>
+ <span class="priority-arrow">→</span>
+ <select name="bot_reply_priority[]" class="priority-select" data-feature="bot_reply">
+ <option value="custom_knowledge"><?php _e( 'Custom Knowledge', 'wpforo' ); ?></option>
+ <option value="forum"><?php _e( 'Forum', 'wpforo' ); ?></option>
+ <option value="wordpress"><?php _e( 'WordPress', 'wpforo' ); ?></option>
+ </select>
+ <span class="priority-arrow">→</span>
+ <select name="bot_reply_priority[]" class="priority-select" data-feature="bot_reply">
+ <option value="wordpress"><?php _e( 'WordPress', 'wpforo' ); ?></option>
+ <option value="forum"><?php _e( 'Forum', 'wpforo' ); ?></option>
+ <option value="custom_knowledge"><?php _e( 'Custom Knowledge', 'wpforo' ); ?></option>
+ </select>
+ </div>
+ </div>
+ </div>
+
+ <div class="wpforo-ai-form-actions">
+ <button type="submit" class="button button-primary" id="knowledge-save-settings">
+ <?php _e( 'Save Settings', 'wpforo' ); ?>
+ </button>
+ <span class="wpforo-ai-save-status" id="settings-save-status"></span>
+ </div>
+ </div>
+ </form>
+ </div>
+ </div>
+
+ </div>
+
+ <!-- Delete Confirmation Modal -->
+ <div id="knowledge-delete-modal" class="wpforo-ai-modal" style="display: none;">
+ <div class="wpforo-ai-modal-content">
+ <div class="wpforo-ai-modal-header">
+ <h3><?php _e( 'Delete Knowledge File', 'wpforo' ); ?></h3>
+ <button type="button" class="wpforo-ai-modal-close">×</button>
+ </div>
+ <div class="wpforo-ai-modal-body">
+ <p><?php _e( 'Are you sure you want to delete this knowledge file? This will remove all indexed vectors and cannot be undone.', 'wpforo' ); ?></p>
+ <p class="wpforo-ai-delete-file-name"></p>
+ </div>
+ <div class="wpforo-ai-modal-footer">
+ <button type="button" class="button" id="knowledge-delete-cancel"><?php _e( 'Cancel', 'wpforo' ); ?></button>
+ <button type="button" class="button button-danger" id="knowledge-delete-confirm"><?php _e( 'Delete', 'wpforo' ); ?></button>
+ </div>
+ </div>
+ </div>
+
+ <?php
+}
--- a/wpforo/admin/pages/tabs/ai-features-tab-analytics.php
+++ b/wpforo/admin/pages/tabs/ai-features-tab-analytics.php
@@ -581,6 +581,7 @@
'content_indexing' => __( 'Content Indexing', 'wpforo' ),
'multimodal_image_indexing' => __( 'Image Indexing', 'wpforo' ),
'document_indexing' => __( 'Document Indexing', 'wpforo' ),
+ 'custom_knowledge_indexing' => __( 'Custom Knowledge Indexing', 'wpforo' ),
'ai_topic_generator' => __( 'Topic Generator', 'wpforo' ),
'ai_reply_generator' => __( 'Reply Generator', 'wpforo' ),
'auto_tag_generation' => __( 'Tag Maintenance', 'wpforo' ),
@@ -612,6 +613,7 @@
'content_indexing' => '#9C27B0',
'multimodal_image_indexing' => '#CE93D8',
'document_indexing' => '#AB47BC',
+ 'custom_knowledge_indexing' => '#7B1FA2',
'ai_topic_generator' => '#FF9800',
'ai_reply_generator' => '#FF5722',
'auto_tag_generation' => '#795548',
--- a/wpforo/admin/pages/tabs/ai-features-tab-overview.php
+++ b/wpforo/admin/pages/tabs/ai-features-tab-overview.php
@@ -1349,6 +1349,18 @@
'plan' => 'business',
'preview' => false,
],
+ 'custom_knowledge_indexing' => [
+ 'name' => __( 'Custom Knowledge Indexing', 'wpforo' ),
+ 'description' => __( 'Upload and index custom knowledge files (JSON, Markdown, Text) to enhance AI responses with your expert documentation, FAQs, and specialized content. Perfect for product manuals, internal guides, and domain-specific knowledge bases.', 'wpforo' ),
+ 'plan' => 'business',
+ 'preview' => false,
+ ],
+ 'file_indexing' => [
+ 'name' => __( 'File Indexing (TXT, MD, JSON, PDF)', 'wpforo' ),
+ 'description' => __( 'Index plain text, Markdown, JSON, and PDF files into the AI knowledge base for richer answers from custom documentation and reference material. Scanned PDFs are auto-OCR'd when the text layer is too sparse.', 'wpforo' ),
+ 'plan' => 'business',
+ 'preview' => false,
+ ],
'developer_features' => [
'name' => __( 'Developer Features', 'wpforo' ),
'description' => __( 'Advanced developer tools and API access for custom integrations.', 'wpforo' ),
@@ -1433,6 +1445,8 @@
'wordpress_content_indexing' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>',
'custom_post_types_indexing' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="m12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83Z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/></svg>',
'woocommerce_products_indexing' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="8" cy="21" r="1"/><circle cx="19" cy="21" r="1"/><path d="M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12"/></svg>',
+ 'custom_knowledge_indexing' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H19a1 1 0 0 1 1 1v18a1 1 0 0 1-1 1H6.5a1 1 0 0 1 0-5H20"/><path d="M8 11h8"/><path d="M8 7h6"/><circle cx="12" cy="15" r="1"/></svg>',
+ 'file_indexing' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M8 13h8"/><path d="M8 17h5"/></svg>',
// Enterprise Features
'developer_features' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
'rest_api_access' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M18 20V10"/><path d="M12 20V4"/><path d="M6 20v-6"/><circle cx="18" cy="7" r="3"/><circle cx="6" cy="11" r="3"/></svg>',
@@ -1969,6 +1983,20 @@
<td><span class="crossmark">✗</span></td>
<td><span class="crossmark">✗</span></td>
<td><span class="checkmark">✓</span></td>
+ <td><span class="checkmark">✓</span></td>
+ </tr>
+ <tr>
+ <td><strong><?php _e( 'Custom Knowledge Indexing', 'wpforo' ); ?></strong></td>
+ <td><span class="crossmark">✗</span></td>
+ <td><span class="crossmark">✗</span></td>
+ <td><span class="checkmark">✓</span></td>
+ <td><span class="checkmark">✓</span></td>
+ </tr>
+ <tr class="feature-item">
+ <td><?php _e( 'File Indexing (TXT, MD, JSON, PDF)', 'wpforo' ); ?></td>
+ <td><span class="crossmark">✗</span></td>
+ <td><span class="crossmark">✗</span></td>
+ <td><span class="checkmark">✓</span></td>
<td><span class="checkmark">✓</span></td>
</tr>
--- a/wpforo/admin/pages/tabs/ai-features-tab-rag-indexing.php
+++ b/wpforo/admin/pages/tabs/ai-features-tab-rag-indexing.php
@@ -433,52 +433,15 @@
</div>
- <?php
- // Get indexing status breakdown
- $status_breakdown = $storage_manager->get_indexing_status_breakdown();
- $has_excluded_topics = ( $status_breakdown['private'] > 0 || $status_breakdown['unapproved'] > 0 );
- ?>
- <?php if ( $has_excluded_topics ) : ?>
- <div class="wpforo-ai-indexing-breakdown">
- <details class="wpforo-ai-breakdown-details">
- <summary class="wpforo-ai-breakdown-summary">
- <span class="dashicons dashicons-info-outline"></span>
- <?php
- $excluded_count = $status_breakdown['private'] + $status_breakdown['unapproved'];
- printf(
- __( '%s topics are excluded from indexing', 'wpforo' ),
- '<strong>' . number_format( $excluded_count ) . '</strong>'
- );
- ?>
- <span class="dashicons dashicons-arrow-down-alt2 wpforo-ai-breakdown-arrow"></span>
- </summary>
- <div class="wpforo-ai-breakdown-content">
- <p class="wpforo-ai-breakdown-intro">
- <?php _e( 'The following topics are automatically excluded from AI indexing:', 'wpforo' ); ?>
- </p>
- <ul class="wpforo-ai-breakdown-list">
- <?php if ( $status_breakdown['private'] > 0 ) : ?>
- <li>
- <span class="dashicons dashicons-lock"></span>
- <strong><?php echo number_format( $status_breakdown['private'] ); ?></strong>
- <?php _e( 'private topics - these are only visible to their authors', 'wpforo' ); ?>
- </li>
- <?php endif; ?>
- <?php if ( $status_breakdown['unapproved'] > 0 ) : ?>
- <li>
- <span class="dashicons dashicons-clock"></span>
- <strong><?php echo number_format( $status_breakdown['unapproved'] ); ?></strong>
- <?php _e( 'unapproved topics - these will be indexed once approved by moderators', 'wpforo' ); ?>
- </li>
- <?php endif; ?>
- </ul>
- <p class="wpforo-ai-breakdown-note">
- <em><?php _e( 'Private topics are never indexed to protect user privacy. Unapproved topics will be automatically indexed when approved.', 'wpforo' ); ?></em>
- </p>
- </div>
- </details>
- </div>
- <?php endif; ?>
+ <!-- Indexing breakdown loaded via AJAX (cached 1 day) -->
+ <div id="wpforo-ai-indexing-breakdown-container"
+ data-loading-text="<?php esc_attr_e( 'Loading...', 'wpforo' ); ?>"
+ data-excluded-text="<?php esc_attr_e( '%s topics are excluded from indexing', 'wpforo' ); ?>"
+ data-intro-text="<?php esc_attr_e( 'The following topics are automatically excluded from AI indexing:', 'wpforo' ); ?>"
+ data-private-text="<?php esc_attr_e( 'private topics - these are only visible to their authors', 'wpforo' ); ?>"
+ data-unapproved-text="<?php esc_attr_e( 'unapproved topics - these will be indexed once approved by moderators', 'wpforo' ); ?>"
+ data-note-text="<?php esc_attr_e( 'Private topics are never indexed to protect user privacy. Unapproved topics will be automatically indexed when approved.', 'wpforo' ); ?>">
+ </div>
</div>
<div class="wpforo-ai-section-divider">
--- a/wpforo/admin/pages/tools.php
+++ b/wpforo/admin/pages/tools.php
@@ -7,9 +7,11 @@
<div id="icon-users" class="icon32"><br></div>
<?php
$tabs = [
- 'debug' => __( 'Debug', 'wpforo' ),
- 'tables' => __( 'Database Tables', 'wpforo' ),
- 'misc' => __( 'Admin Note', 'wpforo' )
+ 'debug' => __( 'Debug', 'wpforo' ),
+ 'tables' => __( 'Database Tables', 'wpforo' ),
+ 'misc' => __( 'Admin Note', 'wpforo' ),
+ 'email_queue' => __( 'Email Queue', 'wpforo' ),
+ 'cron_jobs' => __( 'Cron Jobs', 'wpforo' ),
];
wpforo_admin_tools_tabs( $tabs, ( isset( $_GET['tab'] ) ? $_GET['tab'] : 'debug' ) );
?>
@@ -24,6 +26,12 @@
case 'tables':
$includefile = WPFORO_DIR . '/admin/tools-tabs/tables.php';
break;
+ case 'email_queue':
+ $includefile = WPFORO_DIR . '/admin/tools-tabs/email-queue.php';
+ break;
+ case 'cron_jobs':
+ $includefile = WPFORO_DIR . '/admin/tools-tabs/cron-jobs.php';
+ break;
}
}
include_once( $includefile );
--- a/wpforo/admin/pages/usergroup.php
+++ b/wpforo/admin/pages/usergroup.php
@@ -34,7 +34,7 @@
</th>
<?php endif; ?>
<th scope="col" id="title" class="manage-column column-title" style="padding:10px; font-size:14px; padding-left:15px; font-weight:bold;"><span><?php _e( 'Usergroup', 'wpforo' ) ?> <a
- href="https://wpforo.com/docs/wpforo-v2/members/usergroups-and-permissions/" title="<?php _e( 'Read the documentation', 'wpforo' ) ?>" target="_blank"
+ href="https://wpforo.com/docs/wpforo-v3/members/usergroups-and-permissions/" title="<?php _e( 'Read the documentation', 'wpforo' ) ?>" target="_blank"
style="font-size: 14px;"><i class="far fa-question-circle"></i></a></span></th>
<th scope="col" id="count" class="manage-column column-title" style="padding:10px; font-size:14px; padding-left:15px; font-weight:bold;"><span><?php _e(
'Members',
@@ -402,7 +402,7 @@
<div class="wpf-label-big">
<?php _e( 'Usergroup Name', 'wpforo' );
if( $group['groupid'] === 4 ) echo '<span>: ' . __( 'Guest', 'wpforo' ) . '</span>'; ?>
- <a href="https://wpforo.com/docs/wpforo-v2/members/usergroups-and-permissions/" title="<?php _e( 'Read the documentation', 'wpforo' ) ?>" target="_blank"
+ <a href="https://wpforo.com/docs/wpforo-v3/members/usergroups-and-permissions/" title="<?php _e( 'Read the documentation', 'wpforo' ) ?>" target="_blank"
style="font-size: 14px;"><i class="far fa-question-circle"></i></a><br>
</div>
<input name="usergroup[name]" <?php echo $group['groupid'] === 4 ? 'type="hidden"' : 'type="text"'; ?> value="<?php echo esc_attr( $group['name'] ) ?>" required
--- a/wpforo/admin/settings/board.php
+++ b/wpforo/admin/settings/board.php
@@ -13,7 +13,7 @@
<div class="wpf-subtitle">
<span class="dashicons dashicons-admin-links"></span> <?php _e( 'Permalinks', 'wpforo' ) ?>
<div class="wpf-opt-doc" style="float: right; font-size: 16px; margin-right: -8px;">
- <a href="https://wpforo.com/docs/wpforo-v2/settings/board-settings/#permalinks" title="<?php _e('Read the documentation', 'wpforo') ?>" target="_blank"><i class="far fa-question-circle"></i></a>
+ <a href="https://wpforo.com/docs/wpforo-v3/settings/board-settings/#permalinks" title="<?php _e('Read the documentation', 'wpforo') ?>" target="_blank"><i class="far fa-question-circle"></i></a>
</div>
</div>
--- a/wpforo/admin/settings/email.php
+++ b/wpforo/admin/settings/email.php
@@ -9,6 +9,7 @@
WPF()->settings->form_field( 'email', 'admin_emails' );
WPF()->settings->form_field( 'email', 'new_topic_notify' );
WPF()->settings->form_field( 'email', 'new_reply_notify' );
+WPF()->settings->form_field( 'email', 'async_notifications' );
?>
<div class="wpf-subtitle">
--- a/wpforo/admin/settings/general.php
+++ b/wpforo/admin/settings/general.php
@@ -21,7 +21,7 @@
<div class="wpf-subtitle">
<span class="dashicons dashicons-admin-links"></span> <?php _e( 'Permalinks', 'wpforo' ) ?>
<div class="wpf-opt-doc" style="float: right; font-size: 16px; margin-right: -8px;">
- <a href="https://wpforo.com/docs/wpforo-v2/settings/general-settings/#permalinks" title="<?php _e('Read the documentation', 'wpforo') ?>" target="_blank"><i class="far fa-question-circle"></i></a>
+ <a href="https://wpforo.com/docs/wpforo-v3/settings/general-settings/#permalinks" title="<?php _e('Read the documentation', 'wpforo') ?>" target="_blank"><i class="far fa-question-circle"></i></a>
</div>
</div>
--- a/wpforo/admin/settings/legal.php
+++ b/wpforo/admin/settings/legal.php
@@ -10,7 +10,7 @@
</svg>
<div>
<h3 style="font-weight:600; padding:0 0 5px 0; margin:0; color:#666666; font-size: 18px;">
- <?php _e( 'Forum Privacy Policy and GDPR compliant', 'wpforo' ) ?> | <a href="https://wpforo.com/docs/wpforo-v2/gdpr/" rel="noreferrer"
+ <?php _e( 'Forum Privacy Policy and GDPR compliant', 'wpforo' ) ?> | <a href="https://wpforo.com/docs/wpforo-v3/gdpr/" rel="noreferrer"
style="text-decoration: none; font-weight: normal;" target="_blank"><?php _e(
'Documentation',
'wpforo'
--- a/wpforo/admin/settings/styles.php
+++ b/wpforo/admin/settings/styles.php
@@ -9,7 +9,7 @@
$colorids = apply_filters( 'wpforo_manageable_colorids', [ 1, 3, 9, 11, 12, 14, 15, 18 ] );
?>
-<h3 style="margin:20px 0 0; padding:10px 0; border-bottom:3px solid #F5F5F5; font-size: 15px;" data-wpf-opt="style"><?php _e( 'Forum Styles', 'wpforo' ); ?> <a href="https://wpforo.com/docs/wpforo-v2/wpforo-settings/style-settings/" title="<?php _e( 'Read the documentation', 'wpforo' ) ?>" target="_blank" style="font-size: 14px;"><i class="far fa-question-circle"></i></a> | <a href="https://wpforo.com/docs/wpforo-v2/forum-themes/theme-styles/" target="_blank"
+<h3 style="margin:20px 0 0; padding:10px 0; border-bottom:3px solid #F5F5F5; font-size: 15px;" data-wpf-opt="style"><?php _e( 'Forum Styles', 'wpforo' ); ?> <a href="https://wpforo.com/docs/wpforo-v3/wpforo-settings/style-settings/" title="<?php _e( 'Read the documentation', 'wpforo' ) ?>" target="_blank" style="font-size: 14px;"><i class="far fa-question-circle"></i></a> | <a href="https://wpforo.com/docs/wpforo-v3/forum-themes/theme-styles/" target="_blank"
style="font-size:13px; text-decoration:none;"><?php _e( 'Colors Documentation', 'wpforo' ); ?> »</a>
</h3>
<table style="width:95%; border:none; padding:5px; margin-left:10px; margin-top:15px;">
--- a/wpforo/admin/tools-tabs/cron-jobs.php
+++ b/wpforo/admin/tools-tabs/cron-jobs.php
@@ -0,0 +1,635 @@
+<?php
+/**
+ * wpForo Tools — Cron Jobs tab
+ *
+ * Lists every WP-Cron event registered in this WordPress install with
+ * wpForo events ordered first. Each row exposes Run Now and Delete
+ * buttons. Helps diagnose `wp_options.cron` bloat and verify whether
+ * AI / Email Queue / other wpForo crons are scheduled correctly.
+ */
+
+// Exit if accessed directly
+if ( ! defined( 'ABSPATH' ) ) exit;
+if ( ! current_user_can( 'administrator' ) ) exit;
+
+$wpf_cron_nonce_action = 'wpforo_cron_jobs';
+
+// -----------------------------------------------------------------------
+// POST handler — run / delete / run-all-wpforo
+// Tools page emits HTML before including this tab file, so a redirect
+// here would land after "headers already sent". We process inline,
+// add notices via WPF()->notice, and fall through to render — matching
+// the existing Email Queue tab pattern. Actions are idempotent
+// enough (re-running a cron just fires it again; re-deleting is a
+// silent no-op) that a browser refresh re-submitting is safe.
+// -----------------------------------------------------------------------
+if (
+ 'POST' === ( $_SERVER['REQUEST_METHOD'] ?? '' )
+ && isset( $_POST['wpforo_cron_action'] )
+ && wp_verify_nonce( wpfval( $_POST, '_wpnonce' ), $wpf_cron_nonce_action )
+) {
+ $action = sanitize_text_field( wp_unslash( $_POST['wpforo_cron_action'] ) );
+ $timestamp = absint( wpfval( $_POST, 'timestamp' ) );
+ $hook = sanitize_text_field( wpfval( $_POST, 'hook' ) );
+ $md5 = sanitize_text_field( wpfval( $_POST, 'md5' ) );
+
+ $events = function_exists( '_get_cron_array' ) ? _get_cron_array() : [];
+ if ( ! is_array( $events ) ) $events = [];
+
+ switch ( $action ) {
+
+ case 'run':
+ if ( $timestamp && $hook && $md5 && isset( $events[ $timestamp ][ $hook ][ $md5 ] ) ) {
+ $entry = $events[ $timestamp ][ $hook ][ $md5 ];
+ $args = isset( $entry['args'] ) ? (array) $entry['args'] : [];
+ $schedule = $entry['schedule'] ?? false;
+
+ // For single events: unschedule before firing so the queued one
+ // doesn't run a second time. For recurring events: leave the
+ // schedule alone — the regular cadence resumes after we fire.
+ if ( false === $schedule ) {
+ wp_unschedule_event( $timestamp, $hook, $args );
+ }
+
+ try {
+ do_action_ref_array( $hook, $args );
+ WPF()->notice->add(
+ sprintf(
+ /* translators: %s = cron hook name */
+ __( 'Cron event "%s" executed.', 'wpforo' ),
+ $hook
+ ),
+ 'success'
+ );
+ } catch ( Throwable $e ) {
+ WPF()->notice->add(
+ sprintf(
+ /* translators: 1: hook name, 2: error message */
+ __( 'Cron event "%1$s" raised an exception: %2$s', 'wpforo' ),
+ $hook,
+ $e->getMessage()
+ ),
+ 'error'
+ );
+ }
+ } else {
+ WPF()->notice->add(
+ __( 'Cron event no longer exists (likely already executed or rescheduled).', 'wpforo' ),
+ 'warning'
+ );
+ }
+ break;
+
+ case 'delete':
+ if ( $timestamp && $hook && $md5 && isset( $events[ $timestamp ][ $hook ][ $md5 ] ) ) {
+ $args = (array) ( $events[ $timestamp ][ $hook ][ $md5 ]['args'] ?? [] );
+ $schedule = $events[ $timestamp ][ $hook ][ $md5 ]['schedule'] ?? false;
+
+ wp_unschedule_event( $timestamp, $hook, $args );
+
+ // Recurring event: also clear any future occurrences with the
+ // same args so the row truly goes away from the listing.
+ if ( false !== $schedule ) {
+ wp_clear_scheduled_hook( $hook, $args );
+ }
+
+ WPF()->notice->add(
+ sprintf(
+ /* translators: %s = cron hook name */
+ __( 'Cron event "%s" deleted.', 'wpforo' ),
+ $hook
+ ),
+ 'success'
+ );
+ }
+ break;
+
+ case 'run_all_wpforo':
+ $ran = 0;
+ foreach ( $events as $ts => $hooks ) {
+ if ( ! is_array( $hooks ) ) continue;
+ foreach ( $hooks as $h => $items ) {
+ if ( strpos( $h, 'wpforo_' ) !== 0 ) continue;
+ if ( ! is_array( $items ) ) continue;
+ foreach ( $items as $item ) {
+ $args = isset( $item['args'] ) ? (array) $item['args'] : [];
+ $schedule = $item['schedule'] ?? false;
+ if ( false === $schedule ) {
+ wp_unschedule_event( $ts, $h, $args );
+ }
+ try {
+ do_action_ref_array( $h, $args );
+ $ran++;
+ } catch ( Throwable $e ) {
+ WPF()->notice->add(
+ sprintf(
+ /* translators: 1: hook name, 2: error message */
+ __( 'Cron event "%1$s" raised an exception: %2$s', 'wpforo' ),
+ $h,
+ $e->getMessage()
+ ),
+ 'error'
+ );
+ }
+ }
+ }
+ }
+ WPF()->notice->add(
+ sprintf(
+ /* translators: %d = number of cron events executed */
+ _n( '%d wpForo cron event executed.', '%d wpForo cron events executed.', $ran, 'wpforo' ),
+ $ran
+ ),
+ 'success'
+ );
+ break;
+ }
+}
+
+// -----------------------------------------------------------------------
+// Load + normalize + sort cron events
+// -----------------------------------------------------------------------
+$cron_array = function_exists( '_get_cron_array' ) ? _get_cron_array() : [];
+if ( ! is_array( $cron_array ) ) $cron_array = [];
+
+$rows = [];
+$wpforo_count = 0;
+
+foreach ( $cron_array as $timestamp => $hooks ) {
+ if ( ! is_array( $hooks ) ) continue;
+ foreach ( $hooks as $hook => $items ) {
+ if ( ! is_array( $items ) ) continue;
+ foreach ( $items as $md5 => $entry ) {
+ $is_wpforo = ( strpos( $hook, 'wpforo_' ) === 0 );
+ if ( $is_wpforo ) $wpforo_count++;
+ $rows[] = [
+ 'timestamp' => (int) $timestamp,
+ 'hook' => (string) $hook,
+ 'md5' => (string) $md5,
+ 'args' => isset( $entry['args'] ) ? (array) $entry['args'] : [],
+ 'schedule' => $entry['schedule'] ?? false,
+ 'interval' => isset( $entry['interval'] ) ? (int) $entry['interval'] : 0,
+ 'is_wpforo' => $is_wpforo,
+ ];
+ }
+ }
+}
+
+// Sort: wpForo first (alphabetical by hook then by timestamp), then everything
+// else by next-run time.
+usort( $rows, function ( $a, $b ) {
+ if ( $a['is_wpforo'] !== $b['is_wpforo'] ) return $a['is_wpforo'] ? -1 : 1;
+ if ( $a['is_wpforo'] ) {
+ $h = strcmp( $a['hook'], $b['hook'] );
+ if ( $h !== 0 ) return $h;
+ return $a['timestamp'] <=> $b['timestamp'];
+ }
+ return $a['timestamp'] <=> $b['timestamp'];
+} );
+
+$now = time();
+$total_count = count( $rows );
+$cron_disabled = defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON;
+$cron_alt = defined( 'ALTERNATE_WP_CRON' ) && ALTERNATE_WP_CRON;
+$schedules = function_exists( 'wp_get_schedules' ) ? wp_get_schedules() : [];
+
+// Pretty-print interval helper
+$wpf_fmt_interval = function ( $seconds ) {
+ $seconds = (int) $seconds;
+ if ( $seconds <= 0 ) return '—';
+ if ( $seconds < HOUR_IN_SECONDS ) {
+ $n = max( 1, (int) round( $seconds / MINUTE_IN_SECONDS ) );
+ /* translators: %d is the number of minutes */
+ return sprintf( _n( 'every %d minute', 'every %d minutes', $n, 'wpforo' ), $n );
+ }
+ if ( $seconds < DAY_IN_SECONDS ) {
+ $n = max( 1, (int) round( $seconds / HOUR_IN_SECONDS ) );
+ /* translators: %d is the number of hours */
+ return sprintf( _n( 'every %d hour', 'every %d hours', $n, 'wpforo' ), $n );
+ }
+ if ( $seconds < WEEK_IN_SECONDS ) {
+ $n = max( 1, (int) round( $seconds / DAY_IN_SECONDS ) );
+ /* translators: %d is the number of days */
+ return sprintf( _n( 'every %d day', 'every %d days', $n, 'wpforo' ), $n );
+ }
+ $n = max( 1, (int) round( $seconds / WEEK_IN_SECONDS ) );
+ /* translators: %d is the number of weeks */
+ return sprintf( _n( 'every %d week', 'every %d weeks', $n, 'wpforo' ), $n );
+};
+
+// Pretty-print relative time helper
+$wpf_fmt_when = function ( $ts ) use ( $now ) {
+ $diff = $ts - $now;
+ if ( $diff < 0 ) return '<span style="color:#dc3232;font-weight:600;">' . esc_html__( 'past due', 'wpforo' ) . '</span>';
+ if ( $diff < MINUTE_IN_SECONDS ) return esc_html__( 'in <1 min', 'wpforo' );
+ if ( $diff < HOUR_IN_SECONDS ) {
+ $n = (int) round( $diff / MINUTE_IN_SECONDS );
+ /* translators: %d is the number of minutes — matches wpforo_ai_format_next_run_time() in ai-features-helpers.php */
+ return esc_html( sprintf( _n( 'in %d min', 'in %d mins', $n, 'wpforo' ), $n ) );
+ }
+ if ( $diff < DAY_IN_SECONDS ) {
+ $n = (int) round( $diff / HOUR_IN_SECONDS );
+ /* translators: %d is the number of hours */
+ return esc_html( sprintf( _n( 'in %d hour', 'in %d hours', $n, 'wpforo' ), $n ) );
+ }
+ $n = (int) round( $diff / DAY_IN_SECONDS );
+ /* translators: %d is the number of days */
+ return esc_html( sprintf( _n( 'in %d day', 'in %d days', $n, 'wpforo' ), $n ) );
+};
+?>
+
+<style>
+.wpf-cron-summary {
+ display: flex;
+ gap: 15px;
+ margin: 15px 0;
+ flex-wrap: wrap;
+ align-items: center;
+}
+.wpf-cron-summary .wpf-cron-counter {
+ background: #fff;
+ border: 1px solid #ccd0d4;
+ border-radius: 4px;
+ padding: 8px 18px;
+ font-size: 13px;
+}
+.wpf-cron-summary .wpf-cron-counter strong { font-size: 18px; color: #1d2327; }
+.wpf-cron-summary .wpf-cron-counter.wpforo strong { color: #0073aa; }
+
+.wpf-cron-banner {
+ padding: 10px 15px;
+ border-radius: 4px;
+ margin: 10px 0;
+}
+.wpf-cron-banner.warning { background: #fff3cd; border: 1px solid #ffc107; color: #856404; }
+.wpf-cron-banner.ok { background: #d4edda; border: 1px solid #c3e6cb; color: #155724; }
+
+.wpf-cron-table { width: 100%; border-collapse: collapse; background: #fff; border: 1px solid #ccd0d4; }
+.wpf-cron-table th, .wpf-cron-table td { padding: 9px 12px; text-align: left; border-bottom: 1px solid #eee; vertical-align: top; font-size: 13px; }
+.wpf-cron-table th { background: #f6f7f7; font-weight: 600; }
+.wpf-cron-table tr.wpforo-row td { background: #f6fbff; }
+.wpf-cron-table tr:hover td { background: #f0f6fc; }
+
+.wpf-cron-hook { font-family: Consolas, Monaco, monospace; word-break: break-all; }
+.wpf-cron-hook .wpf-cron-badge { background: #0073aa; color: #fff; font-size: 10px; padding: 2px 6px; border-radius: 3px; margin-right: 6px; font-family: -apple-system, sans-serif; font-weight: 600; vertical-align: middle; }
+
+.wpf-cron-row-actions { display: inline-flex; gap: 6px; flex-wrap: wrap; }
+.wpf-cron-row-actions form { margin: 0; display: inline-block; }
+.wpf-cron-row-actions .button { font-size: 11px; height: auto; padding: 3px 9px; line-height: 1.4; }
+.wpf-cron-row-actions .button.delete { color: #b32d2e; border-color: #b32d2e; }
+.wpf-cron-row-actions .button.delete:hover { background: #b32d2e; color: #fff; }
+.wpf-cron-row-actions .button.details { color: #2271b1; border-color: #2271b1; }
+.wpf-cron-row-actions .button.details:hover { background: #2271b1; color: #fff; }
+
+/* Details modal */
+.wpf-cron-modal-overlay {
+ display: none;
+ position: fixed;
+ inset: 0;
+ background: rgba(0,0,0,0.55);
+ z-index: 99999;
+ align-items: flex-start;
+ justify-content: center;
+ overflow-y: auto;
+ padding: 60px 20px;
+}
+.wpf-cron-modal-overlay.is-open { display: flex; }
+.wpf-cron-modal {
+ background: #fff;
+ border-radius: 6px;
+ max-width: 720px;
+ width: 100%;
+ box-shadow: 0 12px 32px rgba(0,0,0,0.25);
+ max-height: calc(100vh - 120px);
+ display: flex;
+ flex-direction: column;
+}
+.wpf-cron-modal-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 14px 18px;
+ border-bottom: 1px solid #e5e5e5;
+}
+.wpf-cron-modal-header h3 { margin: 0; font-size: 16px; }
+.wpf-cron-modal-close {
+ background: transparent;
+ border: 0;
+ font-size: 22px;
+ line-height: 1;
+ cursor: pointer;
+ color: #555;
+ padding: 4px 8px;
+}
+.wpf-cron-modal-close:hover { color: #000; }
+.wpf-cron-modal-body { padding: 16px 18px; overflow-y: auto; }
+.wpf-cron-modal-body dl { margin: 0; }
+.wpf-cron-modal-body dt {
+ font-weight: 600;
+ color: #1d2327;
+ margin-top: 12px;
+ margin-bottom: 4px;
+ font-size: 12px;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+}
+.wpf-cron-modal-body dt:first-child { margin-top: 0; }
+.wpf-cron-modal-body dd { margin: 0;