PluginProbe
MxChat – AI Chatbot & Content Generation for WordPress / 3.1.3
MxChat – AI Chatbot & Content Generation for WordPress v3.1.3
3.2.21 3.2.20 3.2.19 3.2.18 3.2.17 3.2.16 3.2.15 3.2.14 3.2.12 3.2.13 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 3.2.6 3.2.5 3.2.4 3.2.3 3.2.2 3.2.1 2.0.3 2.0.4 2.0.5 2.0.6 All 152 releases
← All changes | admin/class-knowledge-manager.php +120 -634 3.2.23.1.3 View file →
@@ -56,11 +56,12 @@
56 56 add_action('wp_ajax_mxchat_get_recent_entries', array($this, 'ajax_mxchat_get_recent_entries'));
57 57 add_action('wp_ajax_mxchat_detect_sitemaps', array($this, 'ajax_mxchat_detect_sitemaps'));
58 58 add_action('wp_ajax_mxchat_refresh_pinecone_entries', array($this, 'ajax_mxchat_refresh_pinecone_entries'));
59 59 add_action('wp_ajax_mxchat_paginate_entries', array($this, 'ajax_mxchat_paginate_entries'));
60 - add_action('wp_ajax_mxchat_get_entry_content', array($this, 'ajax_mxchat_get_entry_content'));
61 - add_action('wp_ajax_mxchat_save_entry_content', array($this, 'ajax_mxchat_save_entry_content'));
62 60
61 + // Hook for content deletion
62 + add_action('mxchat_delete_content', array($this, 'mxchat_delete_from_pinecone_by_url'), 10, 1);
63 +
63 64 // WordPress post management hooks
64 65 add_action('pre_post_update', array($this, 'mxchat_store_pre_update_status'), 10, 2);
65 66 add_action('post_updated', array($this, 'mxchat_handle_post_update'), 10, 3);
66 67 add_action('before_delete_post', array($this, 'mxchat_handle_post_delete'));
@@ -160,13 +161,9 @@
160 161 public function mxchat_is_pdf_url($url, $response) {
161 162 $content_type = wp_remote_retrieve_header($response, 'content-type');
162 163 $file_extension = strtolower(pathinfo($url, PATHINFO_EXTENSION));
163 164
164 - // Check Content-Disposition header for .pdf filename (Google Drive sends this)
165 - $disposition = wp_remote_retrieve_header($response, 'content-disposition');
166 - $has_pdf_disposition = ! empty($disposition) && stripos($disposition, '.pdf') !== false;
167 -
168 - return strpos($content_type, 'pdf') !== false || $file_extension === 'pdf' || $has_pdf_disposition;
165 + return strpos($content_type, 'pdf') !== false || $file_extension === 'pdf';
169 166 }
170 167
171 168
172 169 public function mxchat_handle_pdf_for_knowledge_base($pdf_url, $response, $bot_id = 'default') {
@@ -623,269 +620,8 @@
623 620 }
624 621 }
625 622
626 623
627 -/**
628 - * AJAX: Get full content for editing — reassembles chunks if needed.
629 - * Works for both WordPress DB and Pinecone entries.
630 - */
631 -public function ajax_mxchat_get_entry_content() {
632 - check_ajax_referer('mxchat_edit_entry_nonce', 'nonce');
633 -
634 - if ( ! current_user_can('manage_options') ) {
635 - wp_send_json_error( array( 'message' => 'Permission denied.' ) );
636 - }
637 -
638 - $source_url = isset($_POST['source_url']) ? sanitize_text_field( wp_unslash($_POST['source_url']) ) : '';
639 - $entry_id = isset($_POST['entry_id']) ? absint($_POST['entry_id']) : 0;
640 - $data_source = isset($_POST['data_source']) ? sanitize_key($_POST['data_source']) : 'wordpress';
641 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
642 -
643 - if ( $data_source === 'pinecone' ) {
644 - // Pinecone: fetch vectors by source_url, reassemble chunks
645 - $content = $this->get_pinecone_entry_content( $source_url, $entry_id, $bot_id );
646 - } else {
647 - // WordPress DB
648 - $content = $this->get_wordpress_entry_content( $source_url, $entry_id );
649 - }
650 -
651 - if ( is_wp_error( $content ) ) {
652 - wp_send_json_error( array( 'message' => $content->get_error_message() ) );
653 - }
654 -
655 - wp_send_json_success( $content );
656 -}
657 -
658 -/**
659 - * Get content from WordPress DB — reassembles chunks by source_url.
660 - */
661 -private function get_wordpress_entry_content( $source_url, $entry_id ) {
662 - global $wpdb;
663 - $table = $wpdb->prefix . 'mxchat_system_prompt_content';
664 -
665 - // If we have a source_url, check for chunks
666 - if ( ! empty( $source_url ) && strpos( $source_url, 'mxchat://' ) !== 0 ) {
667 - $rows = $wpdb->get_results( $wpdb->prepare(
668 - "SELECT id, article_content, source_url, content_type FROM {$table} WHERE source_url = %s ORDER BY id ASC",
669 - $source_url
670 - ) );
671 -
672 - if ( $rows && count( $rows ) > 1 ) {
673 - // Multiple rows = chunked. Reassemble.
674 - $chunks = array();
675 - foreach ( $rows as $row ) {
676 - $parsed = MxChat_Chunker::parse_stored_chunk( $row->article_content );
677 - $index = isset( $parsed['metadata']['chunk_index'] ) ? intval( $parsed['metadata']['chunk_index'] ) : count( $chunks );
678 - $chunks[ $index ] = $parsed['text'];
679 - }
680 - ksort( $chunks );
681 - return array(
682 - 'content' => implode( "\n\n", $chunks ),
683 - 'source_url' => $source_url,
684 - 'is_chunked' => true,
685 - 'chunk_count' => count( $chunks ),
686 - 'content_type' => $rows[0]->content_type,
687 - );
688 - } elseif ( $rows && count( $rows ) === 1 ) {
689 - $parsed = MxChat_Chunker::parse_stored_chunk( $rows[0]->article_content );
690 - return array(
691 - 'content' => $parsed['text'],
692 - 'source_url' => $source_url,
693 - 'entry_id' => $rows[0]->id,
694 - 'is_chunked' => false,
695 - 'content_type' => $rows[0]->content_type,
696 - );
697 - }
698 - }
699 -
700 - // Fallback: fetch by ID
701 - if ( $entry_id > 0 ) {
702 - $row = $wpdb->get_row( $wpdb->prepare(
703 - "SELECT id, article_content, source_url, content_type FROM {$table} WHERE id = %d",
704 - $entry_id
705 - ) );
706 - if ( $row ) {
707 - $parsed = MxChat_Chunker::parse_stored_chunk( $row->article_content );
708 - return array(
709 - 'content' => $parsed['text'],
710 - 'source_url' => $row->source_url,
711 - 'entry_id' => $row->id,
712 - 'is_chunked' => false,
713 - 'content_type' => $row->content_type,
714 - );
715 - }
716 - }
717 -
718 - return new WP_Error( 'not_found', 'Entry not found.' );
719 -}
720 -
721 -/**
722 - * Get content from Pinecone — fetches vectors by source_url, reassembles chunks.
723 - */
724 -private function get_pinecone_entry_content( $source_url, $entry_id, $bot_id ) {
725 - if ( ! class_exists('MxChat_Pinecone_Manager') ) {
726 - return new WP_Error( 'pinecone_unavailable', 'Pinecone manager not available.' );
727 - }
728 -
729 - // Get Pinecone config
730 - if ( $bot_id === 'default' || ! class_exists('MxChat_Multi_Bot_Manager') ) {
731 - $pinecone_options = get_option('mxchat_pinecone_addon_options');
732 - $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
733 - $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
734 - $namespace = $pinecone_options['mxchat_pinecone_namespace'] ?? '';
735 - } else {
736 - $bot_config = apply_filters('mxchat_get_bot_pinecone_config', array(), $bot_id);
737 - $api_key = $bot_config['api_key'] ?? '';
738 - $host = $bot_config['host'] ?? '';
739 - $namespace = $bot_config['namespace'] ?? '';
740 - }
741 -
742 - if ( empty($host) || empty($api_key) ) {
743 - return new WP_Error( 'pinecone_config', 'Pinecone not configured.' );
744 - }
745 -
746 - // List vectors with the source_url prefix
747 - $base_id = md5( $source_url );
748 - $vector_ids = array( $base_id );
749 -
750 - // Find chunk vectors
751 - $list_url = "https://{$host}/vectors/list";
752 - $list_body = array( 'prefix' => $base_id . '_chunk_', 'limit' => 100 );
753 - if ( ! empty($namespace) ) {
754 - $list_body['namespace'] = $namespace;
755 - }
756 -
757 - $list_resp = wp_remote_post( $list_url, array(
758 - 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ),
759 - 'body' => wp_json_encode( $list_body ),
760 - 'timeout' => 15,
761 - ) );
762 -
763 - if ( ! is_wp_error($list_resp) ) {
764 - $list_data = json_decode( wp_remote_retrieve_body($list_resp), true );
765 - if ( ! empty($list_data['vectors']) ) {
766 - foreach ( $list_data['vectors'] as $v ) {
767 - $vector_ids[] = $v['id'];
768 - }
769 - }
770 - }
771 -
772 - // Fetch vectors with metadata
773 - $fetch_url = "https://{$host}/vectors/fetch";
774 - $fetch_body = array( 'ids' => $vector_ids );
775 - if ( ! empty($namespace) ) {
776 - $fetch_body['namespace'] = $namespace;
777 - }
778 -
779 - $fetch_resp = wp_remote_post( $fetch_url, array(
780 - 'headers' => array( 'Api-Key' => $api_key, 'Content-Type' => 'application/json' ),
781 - 'body' => wp_json_encode( $fetch_body ),
782 - 'timeout' => 15,
783 - ) );
784 -
785 - if ( is_wp_error($fetch_resp) ) {
786 - return new WP_Error( 'pinecone_fetch', 'Failed to fetch from Pinecone.' );
787 - }
788 -
789 - $fetch_data = json_decode( wp_remote_retrieve_body($fetch_resp), true );
790 - $vectors = $fetch_data['vectors'] ?? array();
791 -
792 - if ( empty($vectors) ) {
793 - return new WP_Error( 'not_found', 'Entry not found in Pinecone.' );
794 - }
795 -
796 - // Reassemble chunks
797 - $chunks = array();
798 - $content_type = 'content';
799 - foreach ( $vectors as $vid => $vector ) {
800 - $meta = $vector['metadata'] ?? array();
801 - $text = $meta['text'] ?? '';
802 - $index = $meta['chunk_index'] ?? 0;
803 - $content_type = $meta['type'] ?? 'content';
804 - $chunks[ intval($index) ] = $text;
805 - }
806 - ksort( $chunks );
807 -
808 - return array(
809 - 'content' => implode( "\n\n", $chunks ),
810 - 'source_url' => $source_url,
811 - 'is_chunked' => count($chunks) > 1,
812 - 'chunk_count' => count($chunks),
813 - 'content_type' => $content_type,
814 - );
815 -}
816 -
817 -/**
818 - * AJAX: Save edited content — re-chunks and re-embeds as needed.
819 - * Works for both WordPress DB and Pinecone entries.
820 - */
821 -public function ajax_mxchat_save_entry_content() {
822 - check_ajax_referer('mxchat_edit_entry_nonce', 'nonce');
823 -
824 - if ( ! current_user_can('manage_options') ) {
825 - wp_send_json_error( array( 'message' => 'Permission denied.' ) );
826 - }
827 -
828 - $source_url = isset($_POST['source_url']) ? sanitize_text_field( wp_unslash($_POST['source_url']) ) : '';
829 - $entry_id = isset($_POST['entry_id']) ? absint($_POST['entry_id']) : 0;
830 - $content = isset($_POST['content']) ? wp_kses_post( wp_unslash($_POST['content']) ) : '';
831 - $data_source = isset($_POST['data_source']) ? sanitize_key($_POST['data_source']) : 'wordpress';
832 - $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
833 - $content_type = isset($_POST['content_type']) ? sanitize_key($_POST['content_type']) : 'content';
834 -
835 - if ( empty($content) ) {
836 - wp_send_json_error( array( 'message' => 'Content cannot be empty.' ) );
837 - }
838 -
839 - // Get the embedding API key
840 - $options = get_option('mxchat_options', array());
841 - $api_key = '';
842 -
843 - if ( $bot_id !== 'default' && class_exists('MxChat_Multi_Bot_Manager') ) {
844 - $bot_options = apply_filters('mxchat_get_bot_options', array(), $bot_id);
845 - $api_key = $bot_options['api_key'] ?? '';
846 - }
847 - if ( empty($api_key) ) {
848 - $api_key = $options['api_key'] ?? '';
849 - }
850 -
851 - global $wpdb;
852 - $table = $wpdb->prefix . 'mxchat_system_prompt_content';
853 -
854 - // If source_url is empty but we have an entry_id, look it up
855 - if ( empty($source_url) && $entry_id > 0 && $data_source === 'wordpress' ) {
856 - $row = $wpdb->get_row( $wpdb->prepare( "SELECT source_url FROM {$table} WHERE id = %d", $entry_id ) );
857 - if ( $row && ! empty($row->source_url) ) {
858 - $source_url = $row->source_url;
859 - }
860 - }
861 -
862 - // For manual entries (no source_url or mxchat:// prefix), delete the old entry by ID first
863 - // so submit_content_to_db creates a replacement instead of a duplicate
864 - // Also treat legacy mxchat.ai source URLs as manual — old bug assigned the site URL to manual entries
865 - $is_legacy_manual = !empty($source_url) && strpos($source_url, 'mxchat.ai') !== false && strpos($source_url, 'mxchat://') !== 0;
866 - if ( $entry_id > 0 && (empty($source_url) || strpos($source_url, 'mxchat://') === 0 || $is_legacy_manual) ) {
867 - $wpdb->delete( $table, array( 'id' => $entry_id ), array( '%d' ) );
868 - // Clear legacy URL so submit_content_to_db generates a unique mxchat:// identifier
869 - // instead of reusing the shared URL (which would mass-delete other entries with the same URL)
870 - if ( $is_legacy_manual ) {
871 - $source_url = '';
872 - }
873 - }
874 -
875 - // Use the existing submit_content_to_db which handles chunking, Pinecone, and WP DB
876 - $vector_id = ! empty($source_url) ? md5($source_url) : md5('mxchat_manual_' . $entry_id);
877 -
878 - // submit_content_to_db already handles: delete old chunks → re-chunk → re-embed → store
879 - $result = MxChat_Utils::submit_content_to_db( $content, $source_url, $api_key, $vector_id, $bot_id, $content_type );
880 -
881 - if ( is_wp_error($result) ) {
882 - wp_send_json_error( array( 'message' => $result->get_error_message() ) );
883 - }
884 -
885 - wp_send_json_success( array( 'message' => 'Content saved and re-embedded successfully.' ) );
886 -}
887 -
888 624 public function mxchat_get_pdf_processing_status($pdf_url) {
889 625 $pdf_url = esc_url_raw($pdf_url);
890 626 $status = get_transient(sanitize_key('mxchat_pdf_status_' . md5($pdf_url)));
891 627
@@ -947,22 +683,9 @@
947 683 exit;
948 684 }
949 685
950 686 $submitted_url = esc_url_raw($_POST['sitemap_url']);
951 -
952 - // Convert Google Drive sharing URLs to direct download URLs
953 - if ( strpos($submitted_url, 'drive.google.com') !== false ) {
954 - $file_id = '';
955 - if ( preg_match('/[?&]id=([a-zA-Z0-9_-]+)/', $submitted_url, $m) ) {
956 - $file_id = $m[1];
957 - } elseif ( preg_match('#/file/d/([a-zA-Z0-9_-]+)#', $submitted_url, $m) ) {
958 - $file_id = $m[1];
959 - }
960 - if ( ! empty($file_id) ) {
961 - $submitted_url = 'https://drive.google.com/uc?export=download&id=' . $file_id;
962 - }
963 - }
964 -
687 +
965 688 // Get bot_id from form submission
966 689 $bot_id = isset($_POST['bot_id']) ? sanitize_key($_POST['bot_id']) : 'default';
967 690
968 691 // Get bot-specific options and validate API key
@@ -990,18 +713,10 @@
990 713 wp_safe_redirect(esc_url(admin_url('admin.php?page=mxchat-prompts')));
991 714 exit;
992 715 }
993 716
994 - // Fetch URL — use browser-like headers so servers with bot protection don't block us
995 - $response = wp_remote_get($submitted_url, array(
996 - 'timeout' => 30,
997 - 'sslverify' => false,
998 - 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
999 - 'headers' => array(
1000 - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
1001 - 'Accept-Language' => 'en-US,en;q=0.9',
1002 - ),
1003 - ));
717 + // Fetch URL
718 + $response = wp_remote_get($submitted_url, array('timeout' => 30));
1004 719
1005 720 if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
1006 721 $error_message = is_wp_error($response) ? $response->get_error_message() : 'HTTP Status: ' . wp_remote_retrieve_response_code($response);
1007 722 set_transient('mxchat_admin_notice_error',
@@ -1908,10 +1623,8 @@
1908 1623 'preview_length' => $preview_length,
1909 1624 'source_url' => $entry->source_url,
1910 1625 'has_link' => !empty($entry->source_url) && strpos($entry->source_url, 'mxchat://') !== 0,
1911 1626 'chunk_metadata' => $chunk_metadata,
1912 - 'bot_id' => $entry->bot_id ?? 'default',
1913 - 'edit_nonce' => wp_create_nonce('mxchat_edit_entry_nonce'),
1914 1627 'delete_nonce' => wp_create_nonce('mxchat_delete_prompt_nonce')
1915 1628 );
1916 1629 }
1917 1630
@@ -2117,22 +1830,10 @@
2117 1830 <?php else : ?>
2118 1831 <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual Content', 'mxchat'); ?></span>
2119 1832 <?php endif; ?>
2120 1833 </td>
2121 - <td class="mxchat-actions-cell" style="padding: 12px 16px; white-space: nowrap;">
2122 - <?php if ($data_source !== 'pinecone') : ?>
1834 + <td class="mxchat-actions-cell" style="padding: 12px 16px;">
2123 1835 <button type="button"
2124 - class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn"
2125 - data-source-url="<?php echo esc_attr($source_url); ?>"
2126 - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
2127 - data-data-source="<?php echo esc_attr($data_source); ?>"
2128 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2129 - data-nonce="<?php echo wp_create_nonce('mxchat_edit_entry_nonce'); ?>"
2130 - title="<?php esc_attr_e('Edit content', 'mxchat'); ?>">
2131 - <span class="dashicons dashicons-edit" style="font-size: 14px;"></span>
2132 - </button>
2133 - <?php endif; ?>
2134 - <button type="button"
2135 1836 class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-group"
2136 1837 data-source-url="<?php echo esc_attr($source_url); ?>"
2137 1838 data-chunk-count="<?php echo esc_attr($chunk_count); ?>"
2138 1839 data-data-source="<?php echo esc_attr($data_source); ?>"
@@ -2585,22 +2286,10 @@
2585 2286 <?php else : ?>
2586 2287 <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual Content', 'mxchat'); ?></span>
2587 2288 <?php endif; ?>
2588 2289 </td>
2589 - <td class="mxchat-actions-cell" style="padding: 12px 16px; white-space: nowrap;">
2590 - <?php if ($data_source !== 'pinecone') : ?>
2290 + <td class="mxchat-actions-cell" style="padding: 12px 16px;">
2591 2291 <button type="button"
2592 - class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn"
2593 - data-source-url="<?php echo esc_attr($source_url); ?>"
2594 - data-entry-id="<?php echo esc_attr($first_prompt->id); ?>"
2595 - data-data-source="<?php echo esc_attr($data_source); ?>"
2596 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2597 - data-nonce="<?php echo wp_create_nonce('mxchat_edit_entry_nonce'); ?>"
2598 - title="<?php esc_attr_e('Edit content', 'mxchat'); ?>">
2599 - <span class="dashicons dashicons-edit" style="font-size: 14px;"></span>
2600 - </button>
2601 - <?php endif; ?>
2602 - <button type="button"
2603 2292 class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-group"
2604 2293 data-source-url="<?php echo esc_attr($source_url); ?>"
2605 2294 data-chunk-count="<?php echo esc_attr($chunk_count); ?>"
2606 2295 data-data-source="<?php echo esc_attr($data_source); ?>"
@@ -2730,19 +2419,9 @@
2730 2419 <?php else : ?>
2731 2420 <span style="color: var(--mxch-text-muted);"><?php esc_html_e('Manual', 'mxchat'); ?></span>
2732 2421 <?php endif; ?>
2733 2422 </td>
2734 - <td style="padding: 12px 16px; white-space: nowrap;">
2735 - <button type="button"
2736 - class="mxch-btn mxch-btn-ghost mxch-btn-sm mxchat-edit-entry-btn"
2737 - data-source-url="<?php echo esc_attr($prompt->source_url ?? ''); ?>"
2738 - data-entry-id="<?php echo esc_attr($prompt->id); ?>"
2739 - data-data-source="<?php echo esc_attr($data_source); ?>"
2740 - data-bot-id="<?php echo esc_attr($current_bot_id); ?>"
2741 - data-nonce="<?php echo wp_create_nonce('mxchat_edit_entry_nonce'); ?>"
2742 - title="<?php esc_attr_e('Edit content', 'mxchat'); ?>">
2743 - <span class="dashicons dashicons-edit" style="font-size: 14px;"></span>
2744 - </button>
2423 + <td style="padding: 12px 16px;">
2745 2424 <button type="button" class="mxch-btn mxch-btn-ghost mxch-btn-sm delete-button-wordpress" data-entry-id="<?php echo esc_attr($prompt->id); ?>" data-bot-id="<?php echo esc_attr($current_bot_id); ?>" data-nonce="<?php echo wp_create_nonce('mxchat_delete_wordpress_prompt_nonce'); ?>" style="color: var(--mxch-error);">
2746 2425 <span class="dashicons dashicons-trash" style="font-size: 14px;"></span>
2747 2426 </button>
2748 2427 </td>
@@ -2835,12 +2514,11 @@
2835 2514 foreach ($primary_indexes as $path => $source) {
2836 2515 $url = trailingslashit($site_url) . $path;
2837 2516
2838 2517 $response = wp_remote_head($url, array(
2839 - 'timeout' => 10,
2518 + 'timeout' => 3, // Short timeout
2840 2519 'sslverify' => false,
2841 - 'redirection' => 1,
2842 - 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
2520 + 'redirection' => 1
2843 2521 ));
2844 2522
2845 2523 if (!is_wp_error($response) && wp_remote_retrieve_response_code($response) === 200) {
2846 2524 // Found a sitemap index - parse it to get sub-sitemaps
@@ -2898,15 +2576,10 @@
2898 2576 private function parse_sitemap_index($url) {
2899 2577 $sub_sitemaps = array();
2900 2578
2901 2579 $response = wp_remote_get($url, array(
2902 - 'timeout' => 30,
2903 - 'sslverify' => false,
2904 - 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
2905 - 'headers' => array(
2906 - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
2907 - 'Accept-Language' => 'en-US,en;q=0.9',
2908 - ),
2580 + 'timeout' => 5,
2581 + 'sslverify' => false
2909 2582 ));
2910 2583
2911 2584 if (is_wp_error($response)) {
2912 2585 return $sub_sitemaps;
@@ -2957,15 +2630,10 @@
2957 2630 * Get URL count from a sitemap
2958 2631 */
2959 2632 private function get_sitemap_url_count($url) {
2960 2633 $response = wp_remote_get($url, array(
2961 - 'timeout' => 30,
2962 - 'sslverify' => false,
2963 - 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
2964 - 'headers' => array(
2965 - 'Accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
2966 - 'Accept-Language' => 'en-US,en;q=0.9',
2967 - ),
2634 + 'timeout' => 10,
2635 + 'sslverify' => false
2968 2636 ));
2969 2637
2970 2638 if (is_wp_error($response)) {
2971 2639 return 0;
@@ -2988,11 +2656,10 @@
2988 2656 $sitemaps = array();
2989 2657 $robots_url = trailingslashit($site_url) . 'robots.txt';
2990 2658
2991 2659 $response = wp_remote_get($robots_url, array(
2992 - 'timeout' => 15,
2993 - 'sslverify' => false,
2994 - 'user-agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36',
2660 + 'timeout' => 5,
2661 + 'sslverify' => false
2995 2662 ));
2996 2663
2997 2664 if (is_wp_error($response)) {
2998 2665 return $sitemaps;
@@ -5181,33 +4848,40 @@
5181 4848 // If the post was previously published but is now not published, remove from knowledge base
5182 4849 if ($previous_status === 'publish' && $post->post_status !== 'publish') {
5183 4850 // Use the stored URL from when it was published, or fall back to current permalink
5184 4851 $source_url = $previous_url ?: get_permalink($post_id);
4852 +
4853 + if ($source_url) {
4854 + // Check if Pinecone is enabled
4855 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
4856 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5185 4857
5186 - if ($source_url) {
5187 - // Chunk-aware deletion (routes to Pinecone or WP DB and removes base + all chunks)
5188 - MxChat_Utils::delete_chunks_for_url($source_url, 'default');
4858 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
4859 + // Delete from Pinecone
4860 + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
4861 + } else {
4862 + // Delete from WordPress DB
4863 + global $wpdb;
4864 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
4865 +
4866 + $result = $wpdb->delete(
4867 + $table_name,
4868 + array('source_url' => $source_url),
4869 + array('%s')
4870 + );
4871 + }
5189 4872 }
5190 -
4873 +
5191 4874 // Clean up the transients and exit early
5192 4875 delete_transient($previous_status_key);
5193 4876 delete_transient($previous_url_key);
5194 4877 return;
5195 4878 }
5196 -
5197 - // Slug/permalink rename while still published: delete the old vectors before upserting new ones.
5198 - // Without this, md5(old_url) vectors (base + chunks) would be orphaned under the stale URL.
5199 - if ($post->post_status === 'publish' && !empty($previous_url)) {
5200 - $current_url = get_permalink($post_id);
5201 - if ($current_url && $current_url !== $previous_url) {
5202 - MxChat_Utils::delete_chunks_for_url($previous_url, 'default');
5203 - }
5204 - }
5205 -
4879 +
5206 4880 // Store the current status for next time (if this is an update)
5207 4881 if ($update) {
5208 4882 set_transient($previous_status_key, $post->post_status, DAY_IN_SECONDS);
5209 -
4883 +
5210 4884 // If the post is currently published, also store its URL
5211 4885 if ($post->post_status === 'publish') {
5212 4886 $current_url = get_permalink($post_id);
5213 4887 set_transient($previous_url_key, $current_url, DAY_IN_SECONDS);
@@ -5437,14 +5111,12 @@
5437 5111 if (!$should_sync) {
5438 5112 return;
5439 5113 }
5440 5114
5441 - // Resolve the pre-trash URL. wp_trash_post renames the slug with "__trashed" before firing
5442 - // this hook, so get_permalink() here would return the trashed URL and md5() would miss the
5443 - // real vector IDs stored under the original URL.
5444 - $source_url = $this->mxchat_resolve_pre_trash_url($post_id);
5115 + // Get the URL before post is deleted
5116 + $source_url = get_permalink($post_id);
5445 5117 if (!$source_url) {
5446 - //error_log('MXChat: Failed to resolve source URL for post ' . $post_id);
5118 + //error_log('MXChat: Failed to get permalink for post ' . $post_id);
5447 5119 return;
5448 5120 }
5449 5121
5450 5122 // Use chunk-aware deletion (handles both chunked and non-chunked content)
@@ -5452,36 +5124,56 @@
5452 5124
5453 5125 if (is_wp_error($delete_result)) {
5454 5126 //error_log('MXChat: Chunk-aware deletion failed for URL: ' . $source_url . ' - ' . $delete_result->get_error_message());
5455 5127 }
5128 +}
5456 5129
5457 - delete_transient('mxchat_prev_url_' . $post_id);
5458 - delete_transient('mxchat_prev_status_' . $post_id);
5459 -}
5460 5130
5461 -/**
5462 - * Resolve the source URL for a post being trashed/deleted.
5463 - *
5464 - * Why: wp_trash_post appends "__trashed" to the slug before the wp_trash_post action fires, so
5465 - * get_permalink() returns a URL whose md5() won't match the vector IDs stored in Pinecone or
5466 - * the source_url rows in the WP DB. Prefer the URL captured by mxchat_store_pre_update_status
5467 - * (runs on pre_post_update, before the rename); fall back to stripping the __trashed suffix.
5468 - */
5469 -private function mxchat_resolve_pre_trash_url($post_id) {
5470 - $previous_url = get_transient('mxchat_prev_url_' . $post_id);
5471 - if (!empty($previous_url)) {
5472 - return $previous_url;
5473 - }
5131 + /**
5132 + * Deletes data from Pinecone using a source URL
5133 + */
5134 + public function mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options) {
5135 + $host = $pinecone_options['mxchat_pinecone_host'] ?? '';
5136 + $api_key = $pinecone_options['mxchat_pinecone_api_key'] ?? '';
5474 5137
5475 - $current = get_permalink($post_id);
5476 - if (!$current) {
5477 - return '';
5478 - }
5479 - return preg_replace('#__trashed(/?)$#', '$1', $current);
5480 -}
5138 + if (empty($host) || empty($api_key)) {
5139 + //error_log('MXChat: Pinecone deletion failed - missing configuration');
5140 + return false;
5141 + }
5481 5142
5143 + $api_endpoint = "https://{$host}/vectors/delete";
5144 + $vector_id = md5($source_url);
5482 5145
5146 + $request_body = array(
5147 + 'ids' => array($vector_id)
5148 + );
5483 5149
5150 + $response = wp_remote_post($api_endpoint, array(
5151 + 'headers' => array(
5152 + 'Api-Key' => $api_key,
5153 + 'accept' => 'application/json',
5154 + 'content-type' => 'application/json'
5155 + ),
5156 + 'body' => wp_json_encode($request_body),
5157 + 'timeout' => 30
5158 + ));
5159 +
5160 + if (is_wp_error($response)) {
5161 + //error_log('MXChat: Pinecone deletion error - ' . $response->get_error_message());
5162 + return false;
5163 + }
5164 +
5165 + $response_code = wp_remote_retrieve_response_code($response);
5166 + if ($response_code !== 200) {
5167 + //error_log('MXChat: Pinecone deletion failed with status ' . $response_code);
5168 + return false;
5169 + }
5170 +
5171 + return true;
5172 + }
5173 +
5174 +
5175 +
5484 5176 public function mxchat_handle_product_change($post_id, $post, $update) {
5485 5177 if ($post->post_type !== 'product') {
5486 5178 return;
5487 5179 }
@@ -5633,18 +5325,28 @@
5633 5325 if (get_post_type($post_id) !== 'product') {
5634 5326 return;
5635 5327 }
5636 5328
5637 - $source_url = $this->mxchat_resolve_pre_trash_url($post_id);
5638 - if (!$source_url) {
5639 - return;
5640 - }
5329 + $source_url = get_permalink($post_id);
5641 5330
5642 - // Chunk-aware deletion (routes to Pinecone or WP DB and removes base + all chunks)
5643 - MxChat_Utils::delete_chunks_for_url($source_url, 'default');
5331 + // Check if Pinecone is enabled
5332 + $pinecone_options = get_option('mxchat_pinecone_addon_options', array());
5333 + $use_pinecone = ($pinecone_options['mxchat_use_pinecone'] ?? '0') === '1';
5644 5334
5645 - delete_transient('mxchat_prev_url_' . $post_id);
5646 - delete_transient('mxchat_prev_status_' . $post_id);
5335 + if ($use_pinecone && !empty($pinecone_options['mxchat_pinecone_api_key'])) {
5336 + // Delete from Pinecone
5337 + $this->mxchat_delete_from_pinecone_by_url($source_url, $pinecone_options);
5338 + } else {
5339 + // Delete from WordPress DB
5340 + global $wpdb;
5341 + $table_name = $wpdb->prefix . 'mxchat_system_prompt_content';
5342 +
5343 + $wpdb->delete(
5344 + $table_name,
5345 + array('source_url' => $source_url),
5346 + array('%s')
5347 + );
5348 + }
5647 5349 }
5648 5350
5649 5351 /**
5650 5352 * Handle individual Pinecone content deletion
@@ -7055,27 +6757,13 @@
7055 6757
7056 6758 $result = false;
7057 6759 $error_message = '';
7058 6760
7059 - // Read item directly from DB to get queue_id and preserve special chars in item_data
7060 - // (POST round-trip through JS mangles characters like apostrophes in URLs)
7061 - $db_item = $wpdb->get_row($wpdb->prepare(
7062 - "SELECT queue_id, item_data FROM $table_name WHERE id = %d",
7063 - $item_id
7064 - ));
7065 - $item_queue_id = $db_item ? $db_item->queue_id : '';
7066 - if ($db_item && !empty($db_item->item_data)) {
7067 - $db_data = json_decode($db_item->item_data, true);
7068 - if (is_array($db_data)) {
7069 - $item_data = $db_data;
7070 - }
7071 - }
7072 -
7073 6761 switch ($item_type) {
7074 6762 case 'url':
7075 - $result = $this->mxchat_process_queue_url($item_data, $bot_id, $item_queue_id);
6763 + $result = $this->mxchat_process_queue_url($item_data, $bot_id);
7076 6764 break;
7077 -
6765 +
7078 6766 case 'pdf_page':
7079 6767 $result = $this->mxchat_process_queue_pdf_page($item_data, $bot_id);
7080 6768 break;
7081 6769
@@ -7083,37 +6771,11 @@
7083 6771 throw new Exception('Unknown item type: ' . $item_type);
7084 6772 }
7085 6773
7086 6774 if (is_wp_error($result)) {
7087 - $error_code = $result->get_error_code();
7088 - // Content errors (empty page, sanitization) are permanent — retrying won't help
7089 - $permanent_codes = array('empty_page', 'empty_after_sanitization', 'no_api_key', 'page_not_found');
7090 - if (in_array($error_code, $permanent_codes)) {
7091 - // Mark as permanently failed — set attempts = max_attempts so it won't be retried
7092 - $current_item = $wpdb->get_row($wpdb->prepare(
7093 - "SELECT max_attempts FROM $table_name WHERE id = %d", $item_id
7094 - ));
7095 - $wpdb->update(
7096 - $table_name,
7097 - array(
7098 - 'status' => 'failed',
7099 - 'error_message' => $result->get_error_message(),
7100 - 'attempts' => $current_item ? $current_item->max_attempts : 3
7101 - ),
7102 - array('id' => $item_id),
7103 - array('%s', '%s', '%d'),
7104 - array('%d')
7105 - );
7106 - wp_send_json_error(array(
7107 - 'message' => $result->get_error_message(),
7108 - 'permanent_failure' => true,
7109 - 'item_id' => $item_id
7110 - ));
7111 - return;
7112 - }
7113 6775 throw new Exception($result->get_error_message());
7114 6776 }
7115 -
6777 +
7116 6778 if ($result === false) {
7117 6779 throw new Exception('Processing returned false - item may be empty or invalid');
7118 6780 }
7119 6781
@@ -7189,9 +6851,9 @@
7189 6851
7190 6852 /**
7191 6853 * Process a URL from the queue
7192 6854 */
7193 -private function mxchat_process_queue_url($item_data, $bot_id = 'default', $queue_id = '') {
6855 +private function mxchat_process_queue_url($item_data, $bot_id = 'default') {
7194 6856 $url = isset($item_data['url']) ? $item_data['url'] : '';
7195 6857
7196 6858 if (empty($url)) {
7197 6859 return new WP_Error('invalid_url', 'URL is empty');
@@ -7237,11 +6899,10 @@
7237 6899 // If WooCommerce extraction failed, fall through to HTML extraction
7238 6900 }
7239 6901
7240 6902 // Fetch URL content (fallback for non-products or when WooCommerce extraction fails)
7241 - $is_likely_pdf = (strtolower(pathinfo(parse_url($url, PHP_URL_PATH) ?: '', PATHINFO_EXTENSION)) === 'pdf');
7242 6903 $response = wp_remote_get($url, array(
7243 - 'timeout' => $is_likely_pdf ? 120 : 30,
6904 + 'timeout' => 30,
7244 6905 'redirection' => 5,
7245 6906 'user-agent' => 'MxChat/1.0'
7246 6907 ));
7247 6908
@@ -7250,16 +6911,11 @@
7250 6911 }
7251 6912
7252 6913 $response_code = wp_remote_retrieve_response_code($response);
7253 6914 if ($response_code !== 200) {
7254 - return new WP_Error('http_error', 'HTTP ' . $response_code . ' error for: ' . $url);
6915 + return new WP_Error('http_error', 'HTTP ' . $response_code . ' error');
7255 6916 }
7256 6917
7257 - // Check if URL is a PDF — expand into per-page queue items using the standard PDF pipeline
7258 - if ($this->mxchat_is_pdf_url($url, $response)) {
7259 - return $this->mxchat_expand_pdf_to_queue($url, $response, $bot_id, $queue_id);
7260 - }
7261 -
7262 6918 $html = wp_remote_retrieve_body($response);
7263 6919
7264 6920 if (empty($html)) {
7265 6921 return new WP_Error('empty_response', 'Empty response body');
@@ -7287,180 +6943,8 @@
7287 6943 return $result;
7288 6944 }
7289 6945
7290 6946 /**
7291 - * Expand a PDF URL into per-page queue items using the standard PDF pipeline.
7292 - * Called when a sitemap URL turns out to be a PDF — downloads, parses page count,
7293 - * and adds pdf_page items to the same queue so they process with full progress tracking.
7294 - */
7295 -private function mxchat_expand_pdf_to_queue($pdf_url, $response, $bot_id = 'default', $queue_id = '') {
7296 - set_time_limit(120); // PDFs need extra time for download + parsing
7297 -
7298 - $upload_dir = wp_upload_dir();
7299 - $pdf_filename = sanitize_file_name('mxchat_kb_' . md5($pdf_url) . '.pdf');
7300 - $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
7301 -
7302 - $response_body = wp_remote_retrieve_body($response);
7303 - if (empty($response_body)) {
7304 - return new WP_Error('empty_pdf', 'Empty PDF response for: ' . $pdf_url);
7305 - }
7306 -
7307 - if (!wp_mkdir_p(dirname($pdf_path))) {
7308 - return new WP_Error('dir_error', 'Failed to create upload directory');
7309 - }
7310 -
7311 - file_put_contents($pdf_path, $response_body);
7312 -
7313 - if (!file_exists($pdf_path)) {
7314 - return new WP_Error('save_error', 'Failed to save PDF file');
7315 - }
7316 -
7317 - try {
7318 - $total_pages = $this->mxchat_validate_and_count_pdf_pages($pdf_path);
7319 -
7320 - if ($total_pages === false || $total_pages < 1) {
7321 - wp_delete_file($pdf_path);
7322 - return new WP_Error('no_pages', 'PDF has no pages: ' . $pdf_url);
7323 - }
7324 -
7325 - // Build per-page items identical to mxchat_handle_pdf_for_knowledge_base
7326 - $pages = array();
7327 - for ($i = 1; $i <= $total_pages; $i++) {
7328 - $pages[] = array(
7329 - 'pdf_path' => $pdf_path,
7330 - 'pdf_url' => $pdf_url,
7331 - 'page_number' => $i,
7332 - 'total_pages' => $total_pages
7333 - );
7334 - }
7335 -
7336 - // Add pdf_page items to the SAME queue so the JS picks them up automatically
7337 - if (!empty($queue_id)) {
7338 - $queued_count = $this->mxchat_add_to_queue($queue_id, 'pdf_page', $pages, $bot_id);
7339 - } else {
7340 - // Fallback: create a new PDF queue (shouldn't happen in sitemap flow)
7341 - $new_queue_id = 'pdf_' . md5($pdf_url . time());
7342 - $queued_count = $this->mxchat_add_to_queue($new_queue_id, 'pdf_page', $pages, $bot_id);
7343 - $this->mxchat_set_queue_meta($new_queue_id, 'source_url', $pdf_url);
7344 - $this->mxchat_set_queue_meta($new_queue_id, 'queue_type', 'pdf');
7345 - $this->mxchat_set_queue_meta($new_queue_id, 'total_items', $total_pages);
7346 - $this->mxchat_set_queue_meta($new_queue_id, 'bot_id', $bot_id);
7347 - $this->mxchat_set_queue_meta($new_queue_id, 'pdf_path', $pdf_path);
7348 - $this->mxchat_set_queue_meta($new_queue_id, 'created_at', current_time('mysql'));
7349 - }
7350 -
7351 - if ($queued_count === 0) {
7352 - wp_delete_file($pdf_path);
7353 - return new WP_Error('queue_error', 'Failed to add PDF pages to queue');
7354 - }
7355 -
7356 - // Return true so the original URL item is marked complete
7357 - // The new pdf_page items will be processed in subsequent batches
7358 - return true;
7359 -
7360 - } catch (Exception $e) {
7361 - if (file_exists($pdf_path)) {
7362 - wp_delete_file($pdf_path);
7363 - }
7364 - return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage());
7365 - }
7366 -}
7367 -
7368 -/**
7369 - * Legacy: Process a PDF URL inline during sitemap queue processing.
7370 - * @deprecated Use mxchat_expand_pdf_to_queue instead — kept for reference only.
7371 - */
7372 -private function mxchat_process_pdf_url_inline($pdf_url, $response, $api_key, $bot_id = 'default') {
7373 - set_time_limit(120); // PDFs need more time — downloading + parsing all pages
7374 -
7375 - $upload_dir = wp_upload_dir();
7376 - $pdf_filename = sanitize_file_name('mxchat_kb_' . md5($pdf_url) . '.pdf');
7377 - $pdf_path = trailingslashit($upload_dir['path']) . $pdf_filename;
7378 -
7379 - $response_body = wp_remote_retrieve_body($response);
7380 - if (empty($response_body)) {
7381 - return new WP_Error('empty_pdf', 'Empty PDF response');
7382 - }
7383 -
7384 - if (!wp_mkdir_p(dirname($pdf_path))) {
7385 - return new WP_Error('dir_error', 'Failed to create upload directory');
7386 - }
7387 -
7388 - file_put_contents($pdf_path, $response_body);
7389 -
7390 - if (!file_exists($pdf_path)) {
7391 - return new WP_Error('save_error', 'Failed to save PDF file');
7392 - }
7393 -
7394 - try {
7395 - mxchat_load_pdf_parser();
7396 - $parser = new \Smalot\PdfParser\Parser();
7397 - $pdf = $parser->parseFile($pdf_path);
7398 - $pages = $pdf->getPages();
7399 - $total_pages = count($pages);
7400 -
7401 - if ($total_pages < 1) {
7402 - wp_delete_file($pdf_path);
7403 - return new WP_Error('no_pages', 'PDF has no pages');
7404 - }
7405 -
7406 - $processed = 0;
7407 - $skipped_pages = array();
7408 -
7409 - for ($i = 0; $i < $total_pages; $i++) {
7410 - $page_num = $i + 1;
7411 - $text = $pages[$i]->getText();
7412 - if (empty($text)) {
7413 - $skipped_pages[] = 'Page ' . $page_num . ': No text could be extracted — page may contain only images, links, or non-standard encoding';
7414 - continue;
7415 - }
7416 -
7417 - $sanitized = $this->mxchat_sanitize_content_for_api($text);
7418 - if (empty($sanitized)) {
7419 - $skipped_pages[] = 'Page ' . $page_num . ': Text was extracted but contained only special characters, control codes, or unsupported content';
7420 - continue;
7421 - }
7422 -
7423 - $metadata = array(
7424 - 'document_type' => 'pdf',
7425 - 'total_pages' => $total_pages,
7426 - 'current_page' => $page_num,
7427 - 'source_url' => $pdf_url,
7428 - );
7429 -
7430 - $content_with_metadata = wp_json_encode($metadata) . "\n---\n" . $sanitized;
7431 - $page_url = esc_url($pdf_url . '#page=' . $page_num);
7432 -
7433 - MxChat_Utils::submit_content_to_db(
7434 - $content_with_metadata,
7435 - $page_url,
7436 - $api_key,
7437 - null,
7438 - $bot_id,
7439 - 'pdf'
7440 - );
7441 -
7442 - $processed++;
7443 - }
7444 -
7445 - // Clean up the temp PDF file
7446 - wp_delete_file($pdf_path);
7447 -
7448 - if (!empty($skipped_pages)) {
7449 - error_log('MxChat PDF: Skipped ' . count($skipped_pages) . ' of ' . $total_pages . ' pages: ' . implode('; ', $skipped_pages));
7450 - }
7451 -
7452 - return $processed > 0 ? true : false;
7453 -
7454 - } catch (Exception $e) {
7455 - if (file_exists($pdf_path)) {
7456 - wp_delete_file($pdf_path);
7457 - }
7458 - return new WP_Error('pdf_parse_error', 'Error parsing PDF: ' . $e->getMessage());
7459 - }
7460 -}
7461 -
7462 -/**
7463 6947 * Extract WooCommerce product content including pricing
7464 6948 *
7465 6949 * @param string $url The product URL
7466 6950 * @return string|false Product content with pricing, or false if not found
@@ -7614,17 +7098,18 @@
7614 7098 return new WP_Error('page_not_found', 'Page ' . $page_number . ' not found in PDF');
7615 7099 }
7616 7100
7617 7101 $text = $pages[$page_number - 1]->getText();
7618 -
7102 +
7619 7103 if (empty($text)) {
7620 - return new WP_Error('empty_page', 'Page ' . $page_number . ': No text could be extracted — page may contain only images, links, or non-standard encoding');
7104 + // Not an error - just an empty page
7105 + return false;
7621 7106 }
7622 -
7107 +
7623 7108 $sanitized = $this->mxchat_sanitize_content_for_api($text);
7624 -
7109 +
7625 7110 if (empty($sanitized)) {
7626 - return new WP_Error('empty_after_sanitization', 'Page ' . $page_number . ': Text was extracted but contained only special characters, control codes, or unsupported content that was removed during cleanup');
7111 + return false;
7627 7112 }
7628 7113
7629 7114 // Create metadata
7630 7115 $metadata = array(
@@ -7728,16 +7213,17 @@
7728 7213
7729 7214 // Calculate percentage
7730 7215 $percentage = $total > 0 ? round((($completed + $failed) / $total) * 100) : 0;
7731 7216
7732 - // Get failed items details (include all failed items, not just those that exhausted retries)
7217 + // Get failed items details
7733 7218 $failed_items = array();
7734 7219 if ($failed > 0) {
7735 7220 $failed_items = $wpdb->get_results($wpdb->prepare(
7736 - "SELECT item_type, item_data, error_message, attempts
7737 - FROM $table_name
7738 - WHERE queue_id = %s
7221 + "SELECT item_type, item_data, error_message, attempts
7222 + FROM $table_name
7223 + WHERE queue_id = %s
7739 7224 AND status = 'failed'
7225 + AND attempts >= max_attempts
7740 7226 ORDER BY id DESC
7741 7227 LIMIT 50",
7742 7228 $queue_id
7743 7229 ));