From 5b78a8dcac9cb9a269659271032cdb14b4bcec06 Mon Sep 17 00:00:00 2001 From: datorik Date: Thu, 13 Aug 2026 18:23:50 +0300 Subject: [PATCH 01/14] Udp.Code.WooCommerce integration --- .../IntegrationsByClass/Woocommerce.php | 93 +++++++++++++++---- lib/Cleantalk/ApbctWP/State.php | 2 +- 2 files changed, 75 insertions(+), 20 deletions(-) diff --git a/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php b/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php index bafa1e415..ce9c2b06c 100644 --- a/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php +++ b/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php @@ -177,6 +177,11 @@ public function checkoutCheck($_data, $errors) { global $apbct, $cleantalk_executed; + if ( ! $apbct->settings['data__wc_store_blocked_orders'] ) { + // The checkout is left to WooCommerce as is: no check, no blocked order to store. + return; + } + if ( count($errors->errors) ) { return; } @@ -230,14 +235,10 @@ public function checkoutCheck($_data, $errors) ct_hash($ct_result->id); if ( $ct_result->allow == 0 ) { - if ( $apbct->settings['data__wc_store_blocked_orders'] ) { - $this->storeBlockedOrder(); - } + $this->handleBlockedOrder(); wp_send_json(array( - 'result' => 'failure', - 'messages' => "", - 'refresh' => 'false', - 'reload' => 'false' + 'result' => 'success', + 'redirect' => $this->getBlockedOrderRedirectUrl(), )); } } @@ -252,6 +253,11 @@ public function checkoutCheckFromRest($order) { global $apbct, $cleantalk_executed; + if ( ! $apbct->settings['data__wc_store_blocked_orders'] ) { + // The checkout is left to WooCommerce as is: no check, no blocked order to store. + return; + } + if ( is_null($order) || ! ($order instanceof \WC_Order) ) { return; } @@ -286,11 +292,15 @@ public function checkoutCheckFromRest($order) ct_hash($ct_result->id); if ( $ct_result->allow == 0 ) { - if ( $apbct->settings['data__wc_store_blocked_orders'] ) { - $this->storeBlockedOrder(); - } + $response = $this->getStoreApiPassedResponse($order); + + $this->handleBlockedOrder(); if ( $order->get_status() === 'pending' || $order->get_status() === 'checkout-draft' ) { + if ( function_exists('wc_release_stock_for_order') ) { + wc_release_stock_for_order($order); + } + try { $order->delete(true); } catch (\Exception $e) { @@ -298,22 +308,67 @@ public function checkoutCheckFromRest($order) } } - $response = [ - 'code' => 'woocommerce_store_api_checkout_order_processed', - 'message' => $ct_result->comment, - 'data' => [ - 'status' => 403 - ] - ]; - if ( ! headers_sent() ) { - http_response_code(403); + header('Content-Type: application/json; charset=utf-8'); } die(json_encode($response)); } } } + /** + * Common actions for an order blocked as spam. + * + * @return void + * @psalm-suppress UndefinedFunction + */ + private function handleBlockedOrder() + { + $this->storeBlockedOrder(); + + if ( function_exists('wc') && ! is_null(wc()->cart) ) { + wc()->cart->empty_cart(); + } + } + + /** + * URL of the page shown to the visitor whose order was blocked. + * + * @return string + * @psalm-suppress UndefinedFunction + */ + private function getBlockedOrderRedirectUrl() + { + return wc_get_endpoint_url('order-received', '', wc_get_checkout_url()); + } + + /** + * Response for the Store API checkout route imitating a passed checkout. + * + * @param \WC_Order $order + * + * @return array + * @psalm-suppress UndefinedClass, UndefinedFunction + */ + private function getStoreApiPassedResponse($order) + { + return array( + 'order_id' => $order->get_id(), + 'status' => $order->get_status(), + 'order_key' => $order->get_order_key(), + 'customer_note' => $order->get_customer_note(), + 'customer_id' => $order->get_customer_id(), + 'billing_address' => $order->get_address('billing'), + 'shipping_address' => $order->get_address('shipping'), + 'payment_method' => $order->get_payment_method(), + 'payment_result' => array( + 'payment_status' => 'success', + 'payment_details' => array(), + 'redirect_url' => $this->getBlockedOrderRedirectUrl(), + ), + ); + } + /** * @return void * @psalm-suppress UndefinedFunction diff --git a/lib/Cleantalk/ApbctWP/State.php b/lib/Cleantalk/ApbctWP/State.php index e0d637726..9e8ed2041 100644 --- a/lib/Cleantalk/ApbctWP/State.php +++ b/lib/Cleantalk/ApbctWP/State.php @@ -94,7 +94,7 @@ class State extends \Cleantalk\Common\State 'data__email_decoder_obfuscation_custom_text' => '', 'data__email_decoder_encode_phone_numbers' => 0, 'data__email_decoder_encode_email_addresses' => 1, - 'data__wc_store_blocked_orders' => 0, + 'data__wc_store_blocked_orders' => 1, // Exclusions // Send to the cloud some excepted requests From bccf9f8ac6a58504f0334c243515e141fb66c980 Mon Sep 17 00:00:00 2001 From: datorik Date: Fri, 14 Aug 2026 17:22:35 +0300 Subject: [PATCH 02/14] Udp.Code.Security WooCommerce UI --- css/cleantalk-admin.min.css | 2 +- css/src/cleantalk-admin.css | 45 +++ .../ApbctWP/WcSpamOrdersListTable.php | 285 ++++++++++++++++-- 3 files changed, 308 insertions(+), 24 deletions(-) diff --git a/css/cleantalk-admin.min.css b/css/cleantalk-admin.min.css index daf049497..c2c7a6d27 100644 --- a/css/cleantalk-admin.min.css +++ b/css/cleantalk-admin.min.css @@ -1 +1 @@ -:disabled{cursor:not-allowed!important}.apbct_color--gray{color:gray}.apbct_display--none{display:none}.apbct_bottom_links--left{margin-right:2pc}.apbct_bottom_links--other{margin-right:2pc;margin-left:2pc}.ct_translate_links{color:#969614}.ct_support_links{color:#961414}.ct_faq_links{color:#149614}.ct_setting_links{color:#141496}.ct_translate_links:hover{color:#d2d214!important}.ct_support_links:hover{color:#fa1414!important}.ct_faq_links:hover{color:#14fa14!important}.ct_setting_links:hover{color:#1414fa!important}.ct_link_new_tab img{float:none!important;margin:0 2px;border:0}#negative_reports_table tr td{padding:7px 5px!important}#wp-admin-bar-cleantalk_admin_bar__parent_node{margin-right:5px}#wp-admin-bar-cleantalk_admin_bar__parent_node span{display:inline-block}#wp-admin-bar-cleantalk_admin_bar__parent_node .apbct-icon-attention-alt{background:#d63638;color:#fff;border-radius:50%;font-size:12px}#wp-admin-bar-cleantalk_admin_bar__parent_node img.cleantalk_admin_bar__spbc_icon{width:14px;height:17px;margin-top:7px}#wp-admin-bar-cleantalk_admin_bar__parent_node img.cleantalk_admin_bar__apbct_icon{width:18px;height:18px;margin-top:7px}#wp-admin-bar-cleantalk_admin_bar__parent_node div.cleantalk_admin_bar__sum_counter{color:#999;display:inline;padding:2px 5px!important}.cleantalk_admin_bar__blocked div{cursor:not-allowed!important}.cleantalk_admin_bar__blocked div a{color:#777!important}.cleantalk_admin_bar__title{vertical-align:top}.cleantalk_admin_bar__separator{height:0!important}.cleantalk-admin_bar--list_wrapper .ab-sub-wrapper ul:last-child{margin-bottom:5px!important}.apbct-plugin-errors{margin-left:0;margin-bottom:20px}#cleantalk_notice_review .caption{margin:0 0 15px;color:gray}#cleantalk_notice_review .button{margin-bottom:20px}.apbct-details-spam-order-button,.apbct-restore-spam-order-button{cursor:pointer}.apbct-details-spam-order-button{color:gray}.ct-modal-buttons{display:flex;align-items:center;padding:20px 0;justify-content:space-between}.ct-modal-message{font-weight:700;font-size:16px;line-height:2rem}.apbct-popup-fade:before{content:'';background:#000;position:fixed;left:0;top:0;width:100%;height:100%;opacity:.7;z-index:9999}.apbct-popup{position:fixed;top:20%;left:50%;padding:20px;width:360px;margin-left:-200px;background:#fff;border:1px solid;border-radius:4px;z-index:99999;opacity:1}.apbct-table-actions-wrapper{background:#fcfcfc;background:#fcfcfc;border-radius:2px;padding:5px!important;border:1px solid #d3d3d3;margin:0 5px 5px 0!important}.apbct_wc_details__table-container_header{text-align:center}.apbct_wc_details__wrapper{display:flex;flex-direction:column;scroll-behavior:smooth;overflow-y:auto;max-height:400px;min-width:50pc;position:relative}.apbct_wc_details__table-wrapper-inner{margin-bottom:25px;background:#f9f9f9;border-radius:8px;box-shadow:0 2px 4px rgba(0,0,0,.08);position:relative;padding:0 15px}.apbct_wc_details__table-wrapper-inner h4{margin:0 0 12px 0;color:#333;font-size:16px;font-weight:600;padding:10px 0 8px 0;border-bottom:2px solid #e0e0e0;position:sticky;top:0;background:#f9f9f9;z-index:10}.apbct_wc_details__table{width:100%;border-collapse:collapse;background:#fff;border-radius:6px;overflow:hidden}.apbct_wc_details__table-key-cell{border:1px solid #e8e8e8;padding:10px 12px;font-weight:600;background:#f5f7fa;color:#2c3e50;width:35%;font-size:13px}.apbct_wc_details__table-value-cell{border:1px solid #e8e8e8;padding:10px 12px;color:#34495e;font-size:13px;word-break:break-word}.apbct_wc_details__table tbody tr:hover{background:#fafbfc}.apbct_wc_details__table tbody tr:last-child td{border-bottom:none}.apbct_wc_details__table-value-cell--json{font-family:monospace;font-size:12px;white-space:pre-wrap}@supports (position:sticky){.apbct_wc_details__table th,.apbct_wc_details__table-wrapper-inner h4{position:sticky}}@media screen and (max-width:1120px){.apbct-tablenav{display:flex;flex-direction:column;flex-wrap:nowrap;height:100%;max-width:50%}}.apbct-notice.apbct-banner-success{padding:16px 32px!important;background-color:#fff!important;box-shadow:0 4px 15px #84848440!important;border-left-color:#00bb5d!important}.apbct-banner-content{display:flex;flex-wrap:wrap;justify-content:space-between;row-gap:16px}.apbct-banner-content-wrapper{display:flex;flex-wrap:wrap;gap:16px;justify-content:space-between}.apbct-banner-text-wrapper{margin-top:16px;margin-left:32px}.apbct-banner-button{display:block;padding:17px 53px;color:#fff!important;border-radius:8px;font-size:16px;font-weight:500;text-decoration:none!important;text-align:center;align-content:center}.apbct-banner-button-wrapper{display:grid;gap:12px;align-content:center;text-align:center;margin-right:24px;margin-left:32px}.apbct-banner-title{font-size:24px;line-height:29px;font-weight:500;color:#000}.apbct-banner-subtitle{font-size:14px;line-height:22px;font-weight:400;color:#444;margin-top:8px}.apbct-banner-big-subtitle{font-size:16px}.apbct-banner-link{font-size:16px;line-height:24px;font-weight:400;color:#026e88!important}.apbct-banner-dismiss-link{font-weight:400;font-size:14px;line-height:22px;color:#646464!important}.apbct-notice.apbct-banner-error{padding:16px 32px!important;background-color:#fff!important;box-shadow:0 4px 15px #84848440!important;border-left-color:#bb3323!important}.apbct-notice.apbct-banner-error .notice-dismiss::before{color:#646464}.apbct-banner-button-green{background-color:#00bb5d!important}.apbct-banner-button-green:hover{background-color:#00a34f!important}.apbct-banner-button-red{background-color:#bb3323!important}.apbct-banner-button-red:hover{background-color:#a82e20!important} \ No newline at end of file +:disabled{cursor:not-allowed!important}.apbct_color--gray{color:gray}.apbct_display--none{display:none}.apbct_bottom_links--left{margin-right:2pc}.apbct_bottom_links--other{margin-right:2pc;margin-left:2pc}.ct_translate_links{color:#969614}.ct_support_links{color:#961414}.ct_faq_links{color:#149614}.ct_setting_links{color:#141496}.ct_translate_links:hover{color:#d2d214!important}.ct_support_links:hover{color:#fa1414!important}.ct_faq_links:hover{color:#14fa14!important}.ct_setting_links:hover{color:#1414fa!important}.ct_link_new_tab img{float:none!important;margin:0 2px;border:0}#negative_reports_table tr td{padding:7px 5px!important}#wp-admin-bar-cleantalk_admin_bar__parent_node{margin-right:5px}#wp-admin-bar-cleantalk_admin_bar__parent_node span{display:inline-block}#wp-admin-bar-cleantalk_admin_bar__parent_node .apbct-icon-attention-alt{background:#d63638;color:#fff;border-radius:50%;font-size:12px}#wp-admin-bar-cleantalk_admin_bar__parent_node img.cleantalk_admin_bar__spbc_icon{width:14px;height:17px;margin-top:7px}#wp-admin-bar-cleantalk_admin_bar__parent_node img.cleantalk_admin_bar__apbct_icon{width:18px;height:18px;margin-top:7px}#wp-admin-bar-cleantalk_admin_bar__parent_node div.cleantalk_admin_bar__sum_counter{color:#999;display:inline;padding:2px 5px!important}.cleantalk_admin_bar__blocked div{cursor:not-allowed!important}.cleantalk_admin_bar__blocked div a{color:#777!important}.cleantalk_admin_bar__title{vertical-align:top}.cleantalk_admin_bar__separator{height:0!important}.cleantalk-admin_bar--list_wrapper .ab-sub-wrapper ul:last-child{margin-bottom:5px!important}.apbct-plugin-errors{margin-left:0;margin-bottom:20px}#cleantalk_notice_review .caption{margin:0 0 15px;color:gray}#cleantalk_notice_review .button{margin-bottom:20px}.apbct-details-spam-order-button,.apbct-restore-spam-order-button{cursor:pointer}.apbct-details-spam-order-button{color:gray}.wp-list-table.wc_spam_orders .column-ct_order_date,.wp-list-table.wc_spam_orders .column-ct_status,.wp-list-table.wc_spam_orders .column-ct_total{width:15%}.wp-list-table.wc_spam_orders td{vertical-align:middle}.wp-list-table.wc_spam_orders .apbct-order-view{color:#2271b1;text-decoration:none}.wp-list-table.wc_spam_orders .apbct-order-view:hover{color:#135e96}.apbct-order-status{display:inline-flex;line-height:2.5em;color:#454545;background:#e5e5e5;border-radius:4px;border-bottom:1px solid rgba(0,0,0,.05);margin:-.25em 0;cursor:inherit!important;white-space:nowrap;max-width:100%}.apbct-order-status>span{margin:0 1em;overflow:hidden;text-overflow:ellipsis}.apbct-order-status--spam{background:#eba3a3;color:#570000}.ct-modal-buttons{display:flex;align-items:center;padding:20px 0;justify-content:space-between}.ct-modal-message{font-weight:700;font-size:16px;line-height:2rem}.apbct-popup-fade:before{content:'';background:#000;position:fixed;left:0;top:0;width:100%;height:100%;opacity:.7;z-index:9999}.apbct-popup{position:fixed;top:20%;left:50%;padding:20px;width:360px;margin-left:-200px;background:#fff;border:1px solid;border-radius:4px;z-index:99999;opacity:1}.apbct-table-actions-wrapper{background:#fcfcfc;background:#fcfcfc;border-radius:2px;padding:5px!important;border:1px solid #d3d3d3;margin:0 5px 5px 0!important}.apbct_wc_details__table-container_header{text-align:center}.apbct_wc_details__wrapper{display:flex;flex-direction:column;scroll-behavior:smooth;overflow-y:auto;max-height:400px;min-width:50pc;position:relative}.apbct_wc_details__table-wrapper-inner{margin-bottom:25px;background:#f9f9f9;border-radius:8px;box-shadow:0 2px 4px rgba(0,0,0,.08);position:relative;padding:0 15px}.apbct_wc_details__table-wrapper-inner h4{margin:0 0 12px 0;color:#333;font-size:16px;font-weight:600;padding:10px 0 8px 0;border-bottom:2px solid #e0e0e0;position:sticky;top:0;background:#f9f9f9;z-index:10}.apbct_wc_details__table{width:100%;border-collapse:collapse;background:#fff;border-radius:6px;overflow:hidden}.apbct_wc_details__table-key-cell{border:1px solid #e8e8e8;padding:10px 12px;font-weight:600;background:#f5f7fa;color:#2c3e50;width:35%;font-size:13px}.apbct_wc_details__table-value-cell{border:1px solid #e8e8e8;padding:10px 12px;color:#34495e;font-size:13px;word-break:break-word}.apbct_wc_details__table tbody tr:hover{background:#fafbfc}.apbct_wc_details__table tbody tr:last-child td{border-bottom:none}.apbct_wc_details__table-value-cell--json{font-family:monospace;font-size:12px;white-space:pre-wrap}@supports (position:sticky){.apbct_wc_details__table th,.apbct_wc_details__table-wrapper-inner h4{position:sticky}}@media screen and (max-width:1120px){.apbct-tablenav{display:flex;flex-direction:column;flex-wrap:nowrap;height:100%;max-width:50%}}.apbct-notice.apbct-banner-success{padding:16px 32px!important;background-color:#fff!important;box-shadow:0 4px 15px #84848440!important;border-left-color:#00bb5d!important}.apbct-banner-content{display:flex;flex-wrap:wrap;justify-content:space-between;row-gap:16px}.apbct-banner-content-wrapper{display:flex;flex-wrap:wrap;gap:16px;justify-content:space-between}.apbct-banner-text-wrapper{margin-top:16px;margin-left:32px}.apbct-banner-button{display:block;padding:17px 53px;color:#fff!important;border-radius:8px;font-size:16px;font-weight:500;text-decoration:none!important;text-align:center;align-content:center}.apbct-banner-button-wrapper{display:grid;gap:12px;align-content:center;text-align:center;margin-right:24px;margin-left:32px}.apbct-banner-title{font-size:24px;line-height:29px;font-weight:500;color:#000}.apbct-banner-subtitle{font-size:14px;line-height:22px;font-weight:400;color:#444;margin-top:8px}.apbct-banner-big-subtitle{font-size:16px}.apbct-banner-link{font-size:16px;line-height:24px;font-weight:400;color:#026e88!important}.apbct-banner-dismiss-link{font-weight:400;font-size:14px;line-height:22px;color:#646464!important}.apbct-notice.apbct-banner-error{padding:16px 32px!important;background-color:#fff!important;box-shadow:0 4px 15px #84848440!important;border-left-color:#bb3323!important}.apbct-notice.apbct-banner-error .notice-dismiss::before{color:#646464}.apbct-banner-button-green{background-color:#00bb5d!important}.apbct-banner-button-green:hover{background-color:#00a34f!important}.apbct-banner-button-red{background-color:#bb3323!important}.apbct-banner-button-red:hover{background-color:#a82e20!important} \ No newline at end of file diff --git a/css/src/cleantalk-admin.css b/css/src/cleantalk-admin.css index 815b371aa..09ff7c189 100644 --- a/css/src/cleantalk-admin.css +++ b/css/src/cleantalk-admin.css @@ -105,6 +105,51 @@ .apbct-details-spam-order-button { color: gray; } + +/* WooCommerce orders list alike view of the spam orders table. + The order column has no width, so it takes all the space left. */ +.wp-list-table.wc_spam_orders .column-ct_order_date, +.wp-list-table.wc_spam_orders .column-ct_status, +.wp-list-table.wc_spam_orders .column-ct_total { + width: 15%; +} + +.wp-list-table.wc_spam_orders td { + vertical-align: middle; +} + +.wp-list-table.wc_spam_orders .apbct-order-view { + color: #2271b1; + text-decoration: none; +} + +.wp-list-table.wc_spam_orders .apbct-order-view:hover { + color: #135e96; +} + +.apbct-order-status { + display: inline-flex; + line-height: 2.5em; + color: #454545; + background: #e5e5e5; + border-radius: 4px; + border-bottom: 1px solid rgba(0, 0, 0, .05); + margin: -.25em 0; + cursor: inherit !important; + white-space: nowrap; + max-width: 100%; +} + +.apbct-order-status > span { + margin: 0 1em; + overflow: hidden; + text-overflow: ellipsis; +} + +.apbct-order-status--spam { + background: #eba3a3; + color: #570000; +} .ct-modal-buttons { display: flex; align-items: center; diff --git a/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php b/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php index b5afb11d8..3a25aa965 100644 --- a/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php +++ b/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php @@ -62,8 +62,13 @@ public function prepare_items() // phpcs:ignore PSR1.Methods.CamelCapsMethodNam $wc_spam_orders = $this->getWcSpamOrders(); $this->wc_spam_orders_count = count($wc_spam_orders); + // Blocked orders are never put on hold, so the view is always empty + if ( $this->getCurrentStatus() === 'on-hold' ) { + $wc_spam_orders = array(); + } + $this->set_pagination_args(array( - 'total_items' => $this->wc_spam_orders_count, + 'total_items' => count($wc_spam_orders), 'per_page' => $per_page, )); @@ -94,18 +99,22 @@ public function prepare_items() // phpcs:ignore PSR1.Methods.CamelCapsMethodNam 'details' => '' . esc_html__('See details', 'cleantalk-spam-protect') . '', ); - $order_id_column = sprintf('%1$s %2$s', $wc_spam_order->id, $this->row_actions($actions)); + $order_column = sprintf( + '%1$s %2$s', + $this->renderOrderColumn($wc_spam_order->id, $wc_spam_order->customer_details), + $this->row_actions($actions) + ); - $order_details_column = $this->renderOrderDetailsColumn($wc_spam_order->order_details); - $customer_details_column = $this->renderCustomerDetailsColumn($wc_spam_order->customer_details); - $order_date_column = $this->renderOrderDateColumn($wc_spam_order->order_date); + $order_date_column = $this->renderOrderDateColumn($wc_spam_order->order_date); + $status_column = $this->renderStatusColumn(); + $total_column = $this->renderTotalColumn($wc_spam_order->order_details); $this->items[] = array( - 'cb' => $wc_spam_order->id, - 'ct_order_id' => $order_id_column, - 'ct_order_details' => $order_details_column, - 'ct_customer_details' => $customer_details_column, - 'ct_order_date' => $order_date_column, + 'cb' => $wc_spam_order->id, + 'ct_order' => $order_column, + 'ct_order_date' => $order_date_column, + 'ct_status' => $status_column, + 'ct_total' => $total_column, ); } } @@ -113,11 +122,11 @@ public function prepare_items() // phpcs:ignore PSR1.Methods.CamelCapsMethodNam public function get_columns() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps { $columns = array( - 'cb' => '', - 'ct_order_id' => esc_html__('ID', 'cleantalk-spam-protect'), - 'ct_order_details' => esc_html__('Order details', 'cleantalk-spam-protect'), - 'ct_customer_details' => esc_html__('Customer details', 'cleantalk-spam-protect'), - 'ct_order_date' => esc_html__('Order date', 'cleantalk-spam-protect'), + 'cb' => '', + 'ct_order' => esc_html__('Order', 'cleantalk-spam-protect'), + 'ct_order_date' => esc_html__('Date', 'cleantalk-spam-protect'), + 'ct_status' => esc_html__('Status', 'cleantalk-spam-protect'), + 'ct_total' => esc_html__('Total', 'cleantalk-spam-protect'), ); return $columns; @@ -126,8 +135,75 @@ public function get_columns() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.N protected function get_sortable_columns() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps { return array( + 'ct_order' => array('id', false), 'ct_order_date' => array('order_date', false), + 'ct_total' => array('total', false), + ); + } + + /** + * Statuses row above the table, the same one as the WooCommerce orders list has. + * Every stored order is a blocked spam one, so the "On hold" view is always empty. + * + * @return array + */ + protected function get_views() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps + { + $current_status = $this->getCurrentStatus(); + + $statuses = array( + 'all' => array(esc_html__('All', 'cleantalk-spam-protect'), $this->wc_spam_orders_count), + 'on-hold' => array(esc_html__('On hold', 'cleantalk-spam-protect'), 0), + 'spam' => array(esc_html__('Spam', 'cleantalk-spam-protect'), $this->wc_spam_orders_count), ); + + $views = array(); + + foreach ( $statuses as $status => $status_data ) { + list($title, $count) = $status_data; + + $url = admin_url('admin.php?page=' . Get::getString('page')); + if ( $status !== 'all' ) { + $url = add_query_arg('status', $status, $url); + } + + $views[$status] = sprintf( + '%3$s (%4$d)', + esc_url($url), + $status === $current_status ? ' class="current" aria-current="page"' : '', + $title, + $count + ); + } + + return $views; + } + + /** + * Currently selected status view. + * + * @return string all|on-hold|spam + */ + private function getCurrentStatus() + { + $status = Get::getString('status'); + + return in_array($status, array('on-hold', 'spam'), true) ? $status : 'all'; + } + + /** + * @inheritDoc + */ + public function display() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps + { + $this->views(); + + parent::display(); + } + + public function no_items() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps + { + esc_html_e('No orders found.', 'cleantalk-spam-protect'); } public function get_bulk_actions() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps @@ -256,6 +332,45 @@ private function renderCustomerDetailsColumn($customer_details) return $result; } + /** + * Order number and the customer name, the same way as WooCommerce orders list does it. + * + * @param int|string $spam_order_id + * @param string $customer_details + * + * @return string + */ + private function renderOrderColumn($spam_order_id, $customer_details) + { + $customer_details = json_decode($customer_details, true); + + $customer_name = ''; + + if ( is_array($customer_details) ) { + $customer_name = trim( + TT::getArrayValueAsString($customer_details, 'billing_first_name') + . ' ' + . TT::getArrayValueAsString($customer_details, 'billing_last_name') + ); + + if ( $customer_name === '' ) { + $customer_name = TT::getArrayValueAsString($customer_details, 'billing_email'); + } + } + + if ( $customer_name === '' ) { + $customer_name = esc_html__('Guest', 'cleantalk-spam-protect'); + } else { + $customer_name = esc_html($customer_name); + } + + return sprintf( + '#%1$s %2$s', + esc_attr(TT::toString($spam_order_id)), + $customer_name + ); + } + private function renderOrderDateColumn($order_date) { if ( ! $order_date ) { @@ -268,14 +383,96 @@ private function renderOrderDateColumn($order_date) return '-'; } + // Fresh orders are shown as "5 minutes ago", the older ones as a date. Same as WooCommerce does. + $diff = time() - $timestamp; + if ( $diff >= 0 && $diff < DAY_IN_SECONDS ) { + /* translators: %s: human-readable time difference */ + $show_date = sprintf(__('%s ago', 'cleantalk-spam-protect'), human_time_diff($timestamp, time())); + } else { + $show_date = date_i18n('M j, Y', $timestamp); // Feb 15, 2023 + } + return sprintf( '', esc_attr(date_i18n('c', $timestamp)), // 2023-02-15T20:25:06+00:00 - esc_html(date_i18n('d.m.Y H:i', $timestamp)), // 15.02.2023 20:25 - esc_html(date_i18n('M d, Y', $timestamp)) // Feb 15, 2023 + esc_attr(date_i18n('d.m.Y H:i', $timestamp)), // 15.02.2023 20:25 + esc_html($show_date) ); } + /** + * Every stored order is a blocked spam one, so the status is always the same. + * + * @return string + */ + private function renderStatusColumn() + { + return '' + . esc_html__('Spam', 'cleantalk-spam-protect') + . ''; + } + + /** + * @param string $order_details + * + * @return string Formatted order total or a dash if it can not be calculated. + */ + private function renderTotalColumn($order_details) + { + $total = $this->calcOrderTotal($order_details); + + if ( is_null($total) ) { + return '-'; + } + + /** @psalm-suppress UndefinedFunction */ + return function_exists('wc_price') + ? wc_price($total) + : esc_html(number_format_i18n($total, 2)); + } + + /** + * Sums up the stored cart items. Cart data keeps the calculated line totals, + * the product price is used as a fallback only. + * + * @param string $order_details + * + * @return float|null Null if the order details can not be decoded. + * + * @psalm-suppress UndefinedFunction + */ + private function calcOrderTotal($order_details) + { + $order_details = json_decode($order_details, true); + + if ( ! is_array($order_details) ) { + return null; + } + + $total = 0; + + foreach ( $order_details as $order_detail ) { + if ( ! is_array($order_detail) ) { + continue; + } + + if ( isset($order_detail['line_total']) ) { + $total += (float) $order_detail['line_total'] + (float) ($order_detail['line_tax'] ?? 0); + continue; + } + + if ( isset($order_detail['product_id']) && function_exists('wc_get_product') && class_exists('\WC_Product') ) { + $wc_product = wc_get_product($order_detail['product_id']); + $wc_product_class = '\WC_Product'; + if ( $wc_product instanceof $wc_product_class ) { + $total += (float) $wc_product->get_price() * (float) ($order_detail['quantity'] ?? 1); + } + } + } + + return (float) $total; + } + /** * @return array */ @@ -288,20 +485,62 @@ private function getWcSpamOrders() $sql = 'SELECT * FROM ' . APBCT_TBL_WC_SPAM_ORDERS; - if ($orderby) { - $sql .= ' ORDER BY ' . $orderby . ' ' . $order; - } + // The newest spam orders are shown first by default, the same way as WooCommerce orders list does it. + $sql .= ' ORDER BY ' . ($orderby ? $orderby : 'order_date') . ' ' . $order; $result = $wpdb->get_results($sql, OBJECT); - return is_array($result) ? $result : array(); + $result = is_array($result) ? $result : array(); + + if ( Get::getString('orderby') === 'total' ) { + $result = $this->sortByTotal($result, $order); + } + + return $result; + } + + /** + * The order total is not stored as a column, so it has to be sorted after the fetch. + * + * @param array $wc_spam_orders + * @param string $order ASC|DESC + * + * @return array + */ + private function sortByTotal($wc_spam_orders, $order) + { + $totals = array(); + + foreach ( $wc_spam_orders as $key => $wc_spam_order ) { + $totals[$key] = is_string($wc_spam_order->order_details) + ? (float) $this->calcOrderTotal($wc_spam_order->order_details) + : 0; + } + + uasort($totals, static function ($a, $b) { + if ( $a === $b ) { + return 0; + } + return $a < $b ? -1 : 1; + }); + + if ( $order === 'DESC' ) { + $totals = array_reverse($totals, true); + } + + $sorted = array(); + foreach ( array_keys($totals) as $key ) { + $sorted[] = $wc_spam_orders[$key]; + } + + return $sorted; } private function getSqlOrderBy() { $order_by = Get::getString('orderby'); - $allowed_order_by = array_keys($this->get_sortable_columns()); - return in_array('ct_' . $order_by, $allowed_order_by) ? $order_by : ''; + $allowed_order_by = array('id', 'order_date'); + return in_array($order_by, $allowed_order_by, true) ? $order_by : ''; } private function removeSpam($ids) From 871476a122dba6d3f45ad22ffbb0399187393db3 Mon Sep 17 00:00:00 2001 From: datorik Date: Tue, 18 Aug 2026 13:59:47 +0300 Subject: [PATCH 03/14] Udp.Code.Security WooCommerce UI --- .../IntegrationsByClass/Woocommerce.php | 210 ++++++++++++++++++ .../ApbctWP/WcSpamOrdersFunctions.php | 16 ++ .../ApbctWP/WcSpamOrdersListTable.php | 39 +++- 3 files changed, 259 insertions(+), 6 deletions(-) diff --git a/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php b/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php index ce9c2b06c..eb00731d7 100644 --- a/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php +++ b/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php @@ -94,6 +94,8 @@ public function doAjaxWork() public function doAdminWork() { + add_action('current_screen', [$this, 'addOrdersListStatusViews']); + add_action('admin_menu', function () { add_submenu_page( 'woocommerce', @@ -651,6 +653,214 @@ public function addOrdersSpamStatusHideFromList($query) } } + /** + * Enable the always visible status links on the HPOS orders list + * + * @param \WP_Screen $current_screen + * + * @return void + * @psalm-suppress PossiblyUnusedMethod + */ + public function addOrdersListStatusViews($current_screen) + { + if ( ! isset($current_screen->id) || ! function_exists('wc_get_page_screen_id') ) { + return; + } + + $orders_screen_id = wc_get_page_screen_id('shop_order'); + + // The legacy storage renders the orders on the posts list screen, the links there lead to another URL + if ( empty($orders_screen_id) || $orders_screen_id === 'shop_order' || $current_screen->id !== $orders_screen_id ) { + return; + } + + add_filter('views_' . $orders_screen_id, [$this, 'addOrdersListStatusLinks']); + + // The blocked orders are stored apart from the WooCommerce ones, so the 'Spam' view is rendered by the plugin + if ( Get::getString('status') === 'wc-spamorder' ) { + $this->replaceOrdersListRenderer($orders_screen_id); + } + } + + /** + * Hand the orders page over to the spam orders table. + * + * The page content is printed by the WooCommerce page controller hooked to the page hook, + * so that callback is taken off and replaced. If it can not be found (the WooCommerce + * internals have changed), nothing is replaced to avoid rendering two tables at once. + * + * @param string $page_hook + * + * @return void + */ + private function replaceOrdersListRenderer($page_hook) + { + global $wp_filter; + + if ( ! isset($wp_filter[$page_hook]) || ! isset($wp_filter[$page_hook]->callbacks) ) { + return; + } + + $removed = false; + + foreach ( $wp_filter[$page_hook]->callbacks as $priority => $callbacks ) { + foreach ( $callbacks as $callback ) { + if ( ! isset($callback['function'][0]) || ! is_object($callback['function'][0]) ) { + continue; + } + + if ( strpos(get_class($callback['function'][0]), 'Admin\\Orders\\PageController') === false ) { + continue; + } + + $removed = remove_action($page_hook, $callback['function'], $priority) || $removed; + } + } + + if ( $removed ) { + add_action($page_hook, [$this, 'renderSpamOrdersPage']); + } + } + + /** + * The spam orders table shown in place of the WooCommerce orders list, + * keeping the page markup and the status links of the original page. + * + * @return void + * @psalm-suppress PossiblyUnusedMethod + */ + public function renderSpamOrdersPage() + { + $post_type = get_post_type_object('shop_order'); + $title = $post_type && isset($post_type->labels->name) + ? $post_type->labels->name + : __('Orders', 'cleantalk-spam-protect'); + ?> +
+

+
+
+ getOrdersListViews()); + $list_table->display(); + ?> +
+
+ $label ) { + $count = wc_orders_count($status, 'shop_order'); + + $status_object = get_post_status_object($status); + if ( $status_object && ! empty($status_object->show_in_admin_all_list) ) { + $all_count += $count; + } + + if ( $count > 0 ) { + $views[$status] = $this->getOrdersListStatusLink($status, $label, false); + } + } + + return array_merge( + array('all' => $this->getOrdersListStatusLink('', __('All', 'cleantalk-spam-protect'), false, $all_count)), + $views + ); + } + + /** + * The orders list shows the statuses having orders only, so the spam workflow statuses + * are unreachable until an order gets marked as spam. Both of them are added back: + * 'Spam' itself and 'On hold' the unmarked orders are moved to. + * + * @param array $views + * + * @return array + * @psalm-suppress PossiblyUnusedMethod, PossiblyUnusedReturnValue + */ + public function addOrdersListStatusLinks($views) + { + if ( ! is_array($views) || ! function_exists('wc_get_order_statuses') ) { + return $views; + } + + $order_statuses = wc_get_order_statuses(); + $current_status = Get::getString('status'); + + foreach ( array('wc-on-hold', 'wc-spamorder') as $status ) { + if ( isset($views[$status]) || ! isset($order_statuses[$status]) ) { + continue; + } + + $views[$status] = $this->getOrdersListStatusLink( + $status, + $order_statuses[$status], + $current_status === $status + ); + } + + return $views; + } + + /** + * @param string $status Empty for the 'All' link + * @param string $label + * @param bool $is_current + * @param int|null $count Known count, counted by the status when not given + * + * @return string + */ + private function getOrdersListStatusLink($status, $label, $is_current, $count = null) + { + if ( is_null($count) ) { + $count = $this->getOrdersListStatusCount($status); + } + + $url = admin_url('admin.php?page=wc-orders'); + if ( ! empty($status) ) { + $url = add_query_arg('status', $status, $url); + } + + return sprintf( + '%s (%s)', + esc_url($url), + $is_current ? ' class="current"' : '', + esc_html($label), + number_format_i18n($count) + ); + } + + /** + * @param string $status + * + * @return int + */ + private function getOrdersListStatusCount($status) + { + // The blocked orders never become WooCommerce ones, they are counted in the plugin table + if ( $status === 'wc-spamorder' ) { + return WcSpamOrdersFunctions::getSpamOrdersCount(); + } + + return function_exists('wc_orders_count') ? wc_orders_count($status, 'shop_order') : 0; + } + /** * Add bulk actions: 'Mark as spam' and 'Unmark as spam' */ diff --git a/lib/Cleantalk/ApbctWP/WcSpamOrdersFunctions.php b/lib/Cleantalk/ApbctWP/WcSpamOrdersFunctions.php index 327f28559..b75eabc35 100644 --- a/lib/Cleantalk/ApbctWP/WcSpamOrdersFunctions.php +++ b/lib/Cleantalk/ApbctWP/WcSpamOrdersFunctions.php @@ -6,6 +6,22 @@ class WcSpamOrdersFunctions { + /** + * Count of the stored blocked orders, used for the 'Spam' status counter of the orders list. + * + * @return int + */ + public static function getSpamOrdersCount() + { + global $wpdb; + + if ( ! defined('APBCT_TBL_WC_SPAM_ORDERS') ) { + return 0; + } + + return (int)$wpdb->get_var('SELECT COUNT(*) FROM ' . APBCT_TBL_WC_SPAM_ORDERS . ';'); + } + public static function restoreOrderAction() { AJAXService::checkNonceRestrictingNonAdmins(); diff --git a/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php b/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php index 3a25aa965..20adbc7a4 100644 --- a/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php +++ b/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php @@ -14,13 +14,26 @@ class WcSpamOrdersListTable extends CleantalkListTable protected $page_title = ''; protected $wc_spam_orders_count = 0; - public function __construct() + /** + * Status links of the hosting page when the table is embedded into it. + * Null means the table is rendered on its own page and builds the links itself. + * + * @var array|null + */ + protected $embedded_views = null; + + /** + * @param array|null $embedded_views Status links of the hosting page, see $embedded_views + */ + public function __construct($embedded_views = null) { parent::__construct(array( 'singular' => 'wc_spam_orders', 'plural' => 'wc_spam_orders' )); + $this->embedded_views = is_array($embedded_views) ? $embedded_views : null; + $this->bulk_actions_handler(); $this->row_actions_handler(); @@ -35,7 +48,10 @@ public function __construct() $this->apbct = $apbct; $this->page_title = 'WooCommerce spam orders'; - $this->generatePageHeader(); + // The hosting page renders its own header + if ( is_null($this->embedded_views) ) { + $this->generatePageHeader(); + } } /** @@ -88,11 +104,18 @@ public function prepare_items() // phpcs:ignore PSR1.Methods.CamelCapsMethodNam continue; } - $delete_url = wp_nonce_url( - admin_url('admin.php?page=' . Get::getString('page') . '&action=delete&spam=' . $wc_spam_order->id), - 'apbct_wc_spam_orders_row', - '_wpnonce' + // The status has to be kept, the hosting page is chosen by it + $current_status = Get::getString('status'); + $delete_url = admin_url('admin.php?page=' . Get::getString('page')); + $delete_url = add_query_arg( + array_filter(array( + 'status' => $current_status, + 'action' => 'delete', + 'spam' => $wc_spam_order->id, + )), + $delete_url ); + $delete_url = wp_nonce_url($delete_url, 'apbct_wc_spam_orders_row', '_wpnonce'); $actions = array( 'restore' => '' . esc_html__('Restore', 'cleantalk-spam-protect') . '', 'delete' => 'Delete', @@ -149,6 +172,10 @@ protected function get_sortable_columns() // phpcs:ignore PSR1.Methods.CamelCaps */ protected function get_views() // phpcs:ignore PSR1.Methods.CamelCapsMethodName.NotCamelCaps { + if ( ! is_null($this->embedded_views) ) { + return $this->embedded_views; + } + $current_status = $this->getCurrentStatus(); $statuses = array( From d7932127d368db99d7fe93a03b10df7b5c69b73a Mon Sep 17 00:00:00 2001 From: datorik Date: Thu, 20 Aug 2026 18:38:22 +0300 Subject: [PATCH 04/14] Udp.Code.Security WooCommerce UI --- inc/cleantalk-admin.php | 2 +- inc/cleantalk-settings.php | 2 +- .../IntegrationsByClass/Woocommerce.php | 35 +-------- .../ApbctWP/WcSpamOrdersListTable.php | 75 ++++++++----------- 4 files changed, 39 insertions(+), 75 deletions(-) diff --git a/inc/cleantalk-admin.php b/inc/cleantalk-admin.php index 9cbb6f880..3c849464f 100644 --- a/inc/cleantalk-admin.php +++ b/inc/cleantalk-admin.php @@ -1197,7 +1197,7 @@ function apbct_admin__admin_bar__add_child_nodes($wp_admin_bar) array( 'parent' => 'apbct__parent_node', 'id' => 'ct_settings_bulk_orders', - 'title' => '' + 'title' => '' . __('WooCommerce spam orders', 'cleantalk-spam-protect') . '', ) ); diff --git a/inc/cleantalk-settings.php b/inc/cleantalk-settings.php index 244d612b5..33e48f9c5 100644 --- a/inc/cleantalk-settings.php +++ b/inc/cleantalk-settings.php @@ -2179,7 +2179,7 @@ function apbct_settings__field__action_buttons() if ( apbct_is_plugin_active('woocommerce/woocommerce.php') ) { add_filter('apbct_settings_action_buttons', function ($buttons_array) { $buttons_array[] = - '' + '' . __('WooCommerce spam orders', 'cleantalk-spam-protect') . ''; return $buttons_array; diff --git a/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php b/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php index eb00731d7..7dcb87ee9 100644 --- a/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php +++ b/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php @@ -95,28 +95,6 @@ public function doAjaxWork() public function doAdminWork() { add_action('current_screen', [$this, 'addOrdersListStatusViews']); - - add_action('admin_menu', function () { - add_submenu_page( - 'woocommerce', - __("WooCommerce spam orders", 'cleantalk-spam-protect'), - __("WooCommerce spam orders", 'cleantalk-spam-protect'), - 'activate_plugins', - 'apbct_wc_spam_orders', - function () { - ?> -
-
- display(); - ?> -
-
- labels->name) - ? $post_type->labels->name - : __('Orders', 'cleantalk-spam-protect'); + $list_table = new \Cleantalk\ApbctWP\WcSpamOrdersListTable($this->getOrdersListViews()); ?>
-

+


+ renderPageNotices(); ?>
- getOrdersListViews()); - $list_table->display(); - ?> + display(); ?>
prepare_items(); global $apbct; - $this->apbct = $apbct; - $this->page_title = 'WooCommerce spam orders'; - - // The hosting page renders its own header - if ( is_null($this->embedded_views) ) { - $this->generatePageHeader(); - } + $this->apbct = $apbct; } /** @@ -591,10 +584,16 @@ private function removeSpam($ids) ); } - private function generatePageHeader() + /** + * Notices shown above the table: the stored orders count and the warnings about the data. + * + * @return void + * @psalm-suppress PossiblyUnusedMethod + */ + public function renderPageNotices() { if ( ! apbct_api_key__is_correct() ) { - if ( 1 == $this->spam_checker->getApbct()->moderate_ip ) { + if ( 1 == $this->apbct->moderate_ip ) { echo '

' . sprintf( __( @@ -612,39 +611,31 @@ private function generatePageHeader() } ?> -
-

- apbct->data["wl_mode_enabled"]) { - echo $this->apbct->data["wl_brandname"]; - } else { - echo 'CleanTalk logo' . $this->apbct->plugin_name; - } - ?> -

- -
-

page_title; ?>

-

Total count of spam orders: wc_spam_orders_count ?>

-

Please do backup of WordPress database before delete any orders!

-

Results are based on the decision of our spam checking system and do not give a complete guarantee that - these orders are spam.

- apbct->settings['data__wc_store_blocked_orders'] != 1) { - echo '

' - . __( - 'To store WooCommerce spam orders, enable the "Store blocked WooCommerce orders" option in CleanTalk settings.', - 'cleantalk-spam-protect' - ) - . '

'; - } - ?> -
+

wc_spam_orders_count)) + ); ?>

+

+

apbct->settings['data__wc_store_blocked_orders'] != 1) { + echo '

' + . esc_html__( + 'To store WooCommerce spam orders, enable the "Store blocked WooCommerce orders" option in CleanTalk settings.', + 'cleantalk-spam-protect' + ) + . '

'; + } } private function deleteFromDb($spam_ids) From 5304c09bff5bb6886a18803d7701ef6df608abbe Mon Sep 17 00:00:00 2001 From: datorik Date: Thu, 20 Aug 2026 23:33:10 +0300 Subject: [PATCH 05/14] Udp.Code.Security WooCommerce UI --- lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php b/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php index 6efb37daa..5ee5d9344 100644 --- a/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php +++ b/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php @@ -611,18 +611,21 @@ public function renderPageNotices() } ?> +

wc_spam_orders_count)) - ); ?>

-

-

Date: Fri, 21 Aug 2026 12:45:45 +0300 Subject: [PATCH 06/14] Udp.Code.Security WooCommerce UI --- .../ApbctWP/WcSpamOrdersListTable.php | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php b/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php index 5ee5d9344..50ae0d113 100644 --- a/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php +++ b/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php @@ -611,16 +611,12 @@ public function renderPageNotices() } ?> -

wc_spam_orders_count)) - ); - echo ' '; + ); ?>

+

apbct->settings['data__wc_store_blocked_orders'] != 1) { + if ($this->apbct->settings['forms__wc_checkout_test'] != 1) { echo '

' - . esc_html__( - 'To store WooCommerce spam orders, enable the "Store blocked WooCommerce orders" option in CleanTalk settings.', - 'cleantalk-spam-protect' + . sprintf( + esc_html__( + 'To store Spam orders, enable the "Store blocked WooCommerce orders" option in %1$sCleanTalk settings%2$s.', + 'cleantalk-spam-protect' + ), + '', + '' ) . '

'; } From f311aa3614286dbd3da5f462f693a8a8b6a5ed15 Mon Sep 17 00:00:00 2001 From: datorik Date: Fri, 21 Aug 2026 19:08:52 +0300 Subject: [PATCH 07/14] Udp.Code.Security WooCommerce UI --- lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php b/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php index 50ae0d113..7e2d3dd43 100644 --- a/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php +++ b/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php @@ -611,11 +611,6 @@ public function renderPageNotices() } ?> -

wc_spam_orders_count)) - ); ?>

Date: Mon, 24 Aug 2026 13:50:02 +0300 Subject: [PATCH 08/14] Code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php b/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php index 7e2d3dd43..c9342cf4a 100644 --- a/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php +++ b/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php @@ -622,7 +622,7 @@ public function renderPageNotices() 'cleantalk-spam-protect' ); ?>

apbct->settings['forms__wc_checkout_test'] != 1) { + if ( empty($this->apbct->settings['data__wc_store_blocked_orders']) ) { echo '

' . sprintf( esc_html__( From 06c8bedfa23de7e4314cee94faff6bcc662ec289 Mon Sep 17 00:00:00 2001 From: Aleksandr Banins Date: Mon, 24 Aug 2026 13:59:40 +0300 Subject: [PATCH 09/14] Code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php b/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php index c9342cf4a..e244050f6 100644 --- a/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php +++ b/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php @@ -532,9 +532,16 @@ private function sortByTotal($wc_spam_orders, $order) $totals = array(); foreach ( $wc_spam_orders as $key => $wc_spam_order ) { - $totals[$key] = is_string($wc_spam_order->order_details) - ? (float) $this->calcOrderTotal($wc_spam_order->order_details) - : 0; + $calculated_total = is_string($wc_spam_order->order_details) + ? $this->calcOrderTotal($wc_spam_order->order_details) + : null; + + if ( is_null($calculated_total) ) { + // Keep undecodable totals consistently at the end for both ASC and DESC. + $calculated_total = $order === 'DESC' ? -PHP_FLOAT_MAX : PHP_FLOAT_MAX; + } + + $totals[$key] = (float) $calculated_total; } uasort($totals, static function ($a, $b) { From 1400e41e9e727958918321f0ab22aa86923ae690 Mon Sep 17 00:00:00 2001 From: AntonV1211 Date: Mon, 24 Aug 2026 14:01:07 +0700 Subject: [PATCH 10/14] Fix errors psalm --- inc/cleantalk-ajax.php | 1 - inc/cleantalk-public-integrations.php | 3 --- lib/Cleantalk/Antispam/Integrations/ChatyContactForm.php | 1 - .../Antispam/Integrations/ElementorUltimateAddonsRegister.php | 1 - lib/Cleantalk/Antispam/Integrations/HivePress.php | 1 - lib/Cleantalk/Antispam/Integrations/HivePressRegistration.php | 1 - lib/Cleantalk/Antispam/Integrations/MailPoet.php | 1 - lib/Cleantalk/Antispam/Integrations/SureForms.php | 1 - lib/Cleantalk/Antispam/Integrations/UserRegistrationPro.php | 3 --- lib/Cleantalk/ApbctWP/Variables/AltSessions.php | 3 --- 10 files changed, 16 deletions(-) diff --git a/inc/cleantalk-ajax.php b/inc/cleantalk-ajax.php index 48d12a792..e8b9be86a 100644 --- a/inc/cleantalk-ajax.php +++ b/inc/cleantalk-ajax.php @@ -894,7 +894,6 @@ function ct_ajax_hook($message_obj = null) 'message' => $ct_result->comment ) ); - die(); } // Plugin Name: eForm - WordPress Form Builder; ajax action ipt_fsqm_save_form diff --git a/inc/cleantalk-public-integrations.php b/inc/cleantalk-public-integrations.php index 5fd280d9c..b22c6df36 100644 --- a/inc/cleantalk-public-integrations.php +++ b/inc/cleantalk-public-integrations.php @@ -2448,9 +2448,6 @@ function apbct_form__the7_contact_form() ); wp_send_json($response); - - // IMPORTANT: don't forget to "exit" @todo AG: Why? Exit does not terminate connection, but I can't see how it is applicable - exit; } } } diff --git a/lib/Cleantalk/Antispam/Integrations/ChatyContactForm.php b/lib/Cleantalk/Antispam/Integrations/ChatyContactForm.php index 7836148e3..12599885b 100644 --- a/lib/Cleantalk/Antispam/Integrations/ChatyContactForm.php +++ b/lib/Cleantalk/Antispam/Integrations/ChatyContactForm.php @@ -41,6 +41,5 @@ public function doBlock($message) 'message' => $message, ]; wp_send_json($response); - exit; } } diff --git a/lib/Cleantalk/Antispam/Integrations/ElementorUltimateAddonsRegister.php b/lib/Cleantalk/Antispam/Integrations/ElementorUltimateAddonsRegister.php index feaa89b16..f5a839cc0 100644 --- a/lib/Cleantalk/Antispam/Integrations/ElementorUltimateAddonsRegister.php +++ b/lib/Cleantalk/Antispam/Integrations/ElementorUltimateAddonsRegister.php @@ -24,7 +24,6 @@ public function doBlock($message) ) ) ); - return false; } public function allow() diff --git a/lib/Cleantalk/Antispam/Integrations/HivePress.php b/lib/Cleantalk/Antispam/Integrations/HivePress.php index 50a1eda3c..c5f227594 100644 --- a/lib/Cleantalk/Antispam/Integrations/HivePress.php +++ b/lib/Cleantalk/Antispam/Integrations/HivePress.php @@ -39,6 +39,5 @@ public function doBlock($message) 'error' => ['message' => $message], ] ); - die(); } } diff --git a/lib/Cleantalk/Antispam/Integrations/HivePressRegistration.php b/lib/Cleantalk/Antispam/Integrations/HivePressRegistration.php index fc5044faa..0279910a2 100644 --- a/lib/Cleantalk/Antispam/Integrations/HivePressRegistration.php +++ b/lib/Cleantalk/Antispam/Integrations/HivePressRegistration.php @@ -35,6 +35,5 @@ public function doBlock($message) 'error' => ['message' => $message], ] ); - die(); } } diff --git a/lib/Cleantalk/Antispam/Integrations/MailPoet.php b/lib/Cleantalk/Antispam/Integrations/MailPoet.php index 5343bf60b..df1bb14e0 100644 --- a/lib/Cleantalk/Antispam/Integrations/MailPoet.php +++ b/lib/Cleantalk/Antispam/Integrations/MailPoet.php @@ -58,6 +58,5 @@ public function doBlock($message) ), 403 ); - die(); } } diff --git a/lib/Cleantalk/Antispam/Integrations/SureForms.php b/lib/Cleantalk/Antispam/Integrations/SureForms.php index 8e9a83d5f..eeb997515 100644 --- a/lib/Cleantalk/Antispam/Integrations/SureForms.php +++ b/lib/Cleantalk/Antispam/Integrations/SureForms.php @@ -29,6 +29,5 @@ public function doBlock($message) 'message' => $message ) ); - die(); } } diff --git a/lib/Cleantalk/Antispam/Integrations/UserRegistrationPro.php b/lib/Cleantalk/Antispam/Integrations/UserRegistrationPro.php index d6011c211..d4719fecd 100644 --- a/lib/Cleantalk/Antispam/Integrations/UserRegistrationPro.php +++ b/lib/Cleantalk/Antispam/Integrations/UserRegistrationPro.php @@ -38,8 +38,6 @@ public function getDataForChecking($argument) /** * @param $message - * - * @psalm-suppress UnusedVariable */ public function doBlock($message) { @@ -48,6 +46,5 @@ public function doBlock($message) 'message' => $message ) ); - die(); } } diff --git a/lib/Cleantalk/ApbctWP/Variables/AltSessions.php b/lib/Cleantalk/ApbctWP/Variables/AltSessions.php index 82e41d3ea..3694d7e62 100644 --- a/lib/Cleantalk/ApbctWP/Variables/AltSessions.php +++ b/lib/Cleantalk/ApbctWP/Variables/AltSessions.php @@ -192,7 +192,6 @@ public static function setFromRemote($request = null) 'error' => 'AltSessions: No cookies data provided.' ) ); - die(); // Need to prevent psalm further processing checking } if ( ! wp_verify_nonce($nonce, $action) ) { @@ -211,7 +210,6 @@ public static function setFromRemote($request = null) try { $cookies_array = json_decode($cookies_to_set, true); } catch ( \Exception $e ) { - $cookies_array = array(); unset($e); wp_send_json(array( 'success' => false, @@ -234,7 +232,6 @@ public static function setFromRemote($request = null) //other versions json errors if json_decode returns null if ( is_null($cookies_array) ) { - $cookies_array = array(); wp_send_json(array( 'success' => false, 'error' => 'AltSessions: Internal JSON error: $cookies_array is null.')); From 8b6b2d05d4c60675f09a2d1b43f791b3a83deee9 Mon Sep 17 00:00:00 2001 From: datorik Date: Tue, 8 Sep 2026 23:45:56 +0300 Subject: [PATCH 11/14] Udp.Code.Security WooCommerce UI. Task 1 --- .../IntegrationsByClass/Woocommerce.php | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php b/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php index 7dcb87ee9..e05b1f7fc 100644 --- a/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php +++ b/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php @@ -654,12 +654,40 @@ public function addOrdersListStatusViews($current_screen) add_filter('views_' . $orders_screen_id, [$this, 'addOrdersListStatusLinks']); + // WooCommerce replaces the whole list with a notice while the store has no orders of its own. + // The status links go away with it, so the 'Spam' view becomes unreachable - keep the list on screen. + add_filter( + 'woocommerce_shop_order_list_table_should_render_blank_state', + [$this, 'keepOrdersListWhenSpamOrdersExist'] + ); + // The blocked orders are stored apart from the WooCommerce ones, so the 'Spam' view is rendered by the plugin if ( Get::getString('status') === 'wc-spamorder' ) { $this->replaceOrdersListRenderer($orders_screen_id); } } + /** + * Keep the orders list on screen when the store has no orders of its own but spam ones exist. + * + * A brand new store has nothing in the WooCommerce tables, so the list is replaced with the + * "When you receive a new order, it will appear here." notice. The blocked orders live in a table + * of the plugin and are not counted there, so the shop owner loses the only way to reach them. + * + * @param bool|null $should_render_blank_state Null keeps the WooCommerce own decision + * + * @return bool|null + * @psalm-suppress PossiblyUnusedMethod + */ + public function keepOrdersListWhenSpamOrdersExist($should_render_blank_state) + { + if ( WcSpamOrdersFunctions::getSpamOrdersCount() > 0 ) { + return false; + } + + return $should_render_blank_state; + } + /** * Hand the orders page over to the spam orders table. * From 1303148c791b69119608fd24559729c4ebe9502d Mon Sep 17 00:00:00 2001 From: datorik Date: Wed, 9 Sep 2026 12:22:42 +0300 Subject: [PATCH 12/14] Udp.Code.Security WooCommerce UI. Task 2 --- lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php b/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php index e244050f6..ca7a4b8b2 100644 --- a/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php +++ b/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php @@ -627,6 +627,12 @@ public function renderPageNotices() esc_html_e( 'Results are based on the decision of our spam checking system and do not give a complete guarantee that these orders are spam.', 'cleantalk-spam-protect' + ); + echo ' '; + printf( + '%2$s', + esc_url(TT::toString($this->apbct->settings_link)), + esc_html__('Anti-Spam by CleanTalk', 'cleantalk-spam-protect') ); ?>

apbct->settings['data__wc_store_blocked_orders']) ) { From 2abe12c5c4a0e7057e47b28b0d4f3e974e3dae54 Mon Sep 17 00:00:00 2001 From: datorik Date: Wed, 9 Sep 2026 15:32:30 +0300 Subject: [PATCH 13/14] Udp.Code.Security WooCommerce UI. Task 3 --- .../IntegrationsByClass/Woocommerce.php | 155 +++++++++++++++++- 1 file changed, 151 insertions(+), 4 deletions(-) diff --git a/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php b/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php index e05b1f7fc..c9e3709b1 100644 --- a/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php +++ b/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php @@ -34,8 +34,19 @@ */ class Woocommerce extends IntegrationByClassBase { + /** + * Prefix of the transient keeping the details of a blocked order. + */ + const BLOCKED_ORDER_TRANSIENT = 'apbct_blocked_order_'; + private $event_token = null; + /** + * Key of the transient holding the details of the order blocked in this request. + * @var string + */ + private $blocked_order_key = ''; + /** * @return void * @psalm-suppress PossiblyUnusedMethod @@ -47,6 +58,12 @@ public function doPublicWork() // honeypot add_filter('woocommerce_checkout_fields', [$this, 'addHoneypotField']); + // The blocked visitor gets a thank you page with no order behind it - fill in the details. + // Only the redirect of a blocked order carries the key, so the rest of the pages are left alone. + if ( Get::getString('key') !== '' ) { + add_action('woocommerce_after_template_part', [$this, 'renderBlockedOrderOverview'], 10, 4); + } + // add to cart hooks if cart works with non-ajax requests $this->addCartActions(); @@ -272,9 +289,10 @@ public function checkoutCheckFromRest($order) ct_hash($ct_result->id); if ( $ct_result->allow == 0 ) { - $response = $this->getStoreApiPassedResponse($order); + // The details must be stored before the response carrying their key is built + $this->handleBlockedOrder($order); - $this->handleBlockedOrder(); + $response = $this->getStoreApiPassedResponse($order); if ( $order->get_status() === 'pending' || $order->get_status() === 'checkout-draft' ) { if ( function_exists('wc_release_stock_for_order') ) { @@ -302,15 +320,139 @@ public function checkoutCheckFromRest($order) * @return void * @psalm-suppress UndefinedFunction */ - private function handleBlockedOrder() + private function handleBlockedOrder($order = null) { $this->storeBlockedOrder(); + $this->rememberBlockedOrderOverview($order); + if ( function_exists('wc') && ! is_null(wc()->cart) ) { wc()->cart->empty_cart(); } } + /** + * Store the details of the blocked order to show them on the thank you page. + * + * The blocked order never becomes a WooCommerce one, so the thank you page has nothing to + * render and gives the spammer a hint that the order went wrong. The details are kept in a + * transient addressed by the key of the redirect URL and printed by renderBlockedOrderOverview(). + * + * @param \WC_Order|null $order Order created by the Store API checkout, absent for the classic one + * + * @return void + * @psalm-suppress UndefinedClass, UndefinedFunction + */ + private function rememberBlockedOrderOverview($order = null) + { + $overview = array( + 'date' => date_i18n(get_option('date_format')), + 'total' => $this->getBlockedOrderTotal($order), + 'payment_method' => $this->getBlockedOrderPaymentMethod($order), + ); + + $this->blocked_order_key = 'wc_order_' . wp_generate_password(13, false); + + set_transient(self::BLOCKED_ORDER_TRANSIENT . $this->blocked_order_key, $overview, HOUR_IN_SECONDS); + } + + /** + * @param \WC_Order|null $order + * + * @return string Formatted total, the cart one when there is no order + * @psalm-suppress UndefinedClass, UndefinedFunction + */ + private function getBlockedOrderTotal($order = null) + { + if ( $order instanceof \WC_Order ) { + return $order->get_formatted_order_total(); + } + + return ( function_exists('wc') && ! is_null(wc()->cart) ) ? wc()->cart->get_total() : ''; + } + + /** + * @param \WC_Order|null $order + * + * @return string Title of the chosen gateway, empty when it can not be resolved + * @psalm-suppress UndefinedClass, UndefinedFunction + */ + private function getBlockedOrderPaymentMethod($order = null) + { + if ( $order instanceof \WC_Order ) { + return $order->get_payment_method_title(); + } + + $chosen_method = Post::getString('payment_method'); + + if ( $chosen_method === '' || ! function_exists('WC') ) { + return ''; + } + + $gateways = WC()->payment_gateways() ? WC()->payment_gateways()->payment_gateways() : array(); + + return isset($gateways[$chosen_method]) ? $gateways[$chosen_method]->get_title() : ''; + } + + /** + * Print the order details on the thank you page shown to the blocked visitor. + * + * WooCommerce renders the details itself when an order stands behind the page. There is none + * for a blocked order, so the same markup is printed right after the template that says + * the order has been received. + * + * @param string $template_name + * @param string $template_path + * @param string $located + * @param array $args + * + * @return void + * @psalm-suppress PossiblyUnusedMethod, UndefinedFunction + */ + public function renderBlockedOrderOverview($template_name, $template_path, $located, $args) + { + if ( $template_name !== 'checkout/order-received.php' || ! empty($args['order']) ) { + return; + } + + $blocked_order_key = Get::getString('key'); + + if ( $blocked_order_key === '' ) { + return; + } + + $overview = get_transient(self::BLOCKED_ORDER_TRANSIENT . $blocked_order_key); + + if ( ! is_array($overview) || empty($overview['date']) ) { + return; + } + + $rows = array( + 'date' => array(__('Date:', 'cleantalk-spam-protect'), $overview['date']), + 'total' => array(__('Total:', 'cleantalk-spam-protect'), $overview['total']), + ); + + if ( ! empty($overview['payment_method']) ) { + $rows['method'] = array( + __('Payment method:', 'cleantalk-spam-protect'), + $overview['payment_method'] + ); + } + + echo '
    '; + + foreach ( $rows as $key => $row ) { + printf( + '
  • %2$s %3$s
  • ', + esc_attr($key), + esc_html($row[0]), + wp_kses_post($row[1]) + ); + } + + echo '
'; + } + /** * URL of the page shown to the visitor whose order was blocked. * @@ -319,7 +461,12 @@ private function handleBlockedOrder() */ private function getBlockedOrderRedirectUrl() { - return wc_get_endpoint_url('order-received', '', wc_get_checkout_url()); + $url = wc_get_endpoint_url('order-received', '', wc_get_checkout_url()); + + // The key makes the page look like the usual one and points at the stored details + return $this->blocked_order_key === '' + ? $url + : add_query_arg('key', $this->blocked_order_key, $url); } /** From b81c0dbf66ac91ecfd4f0a54629fb759d92f1b06 Mon Sep 17 00:00:00 2001 From: datorik Date: Wed, 9 Sep 2026 19:01:58 +0300 Subject: [PATCH 14/14] Udp.Code.Security WooCommerce settings --- inc/cleantalk-settings.php | 17 ++++++++- inc/cleantalk-updater.php | 8 ++++ .../IntegrationsByClass/Woocommerce.php | 38 +++++++++++++++++-- lib/Cleantalk/ApbctWP/State.php | 1 + 4 files changed, 60 insertions(+), 4 deletions(-) diff --git a/inc/cleantalk-settings.php b/inc/cleantalk-settings.php index 4dc05f059..9b7b70e8c 100644 --- a/inc/cleantalk-settings.php +++ b/inc/cleantalk-settings.php @@ -364,9 +364,24 @@ function apbct_settings__set_fields() array('val' => 0, 'label' => __('Off')), ), ), + 'forms__wc_show_rejection_message' => array( + 'title' => __('Show rejection message to customers', 'cleantalk-spam-protect'), + 'description' => __( + 'This message tells the customer why their order was filtered, allowing them to fix the issue that caused it. However, this may result in multiple orders from the same customer because all rejected orders are saved in the Spam folder. By default, this option is OFF.', + 'cleantalk-spam-protect' + ), + 'class' => 'apbct_settings-field_wrapper--sub', + 'options' => array( + array('val' => 1, 'label' => __('On')), + array('val' => 0, 'label' => __('Off')), + ), + ), 'data__wc_store_blocked_orders' => array( 'title' => __('Store blocked orders', 'cleantalk-spam-protect'), - 'description' => __('The orders which was blocked by the Anti-Spam will be stored and could be restored manually later if its needed.', 'cleantalk-spam-protect'), + 'description' => __( + 'Orders blocked by Anti-Spam will be stored and can be restored manually later if needed. If the option is OFF, the customer must fix the issue on their end to pass the anti-spam check and place an order. By default, this option is ON.', + 'cleantalk-spam-protect' + ), 'class' => 'apbct_settings-field_wrapper--sub', 'options' => array( array('val' => 1, 'label' => __('On')), diff --git a/inc/cleantalk-updater.php b/inc/cleantalk-updater.php index d4c865378..943e5447d 100644 --- a/inc/cleantalk-updater.php +++ b/inc/cleantalk-updater.php @@ -1378,6 +1378,14 @@ function apbct_update_to_6_60_0() } } +function apbct_update_to_6_88_0() +{ + global $apbct; + + $apbct->settings['forms__wc_show_rejection_message'] = 1; + $apbct->save('settings'); +} + function apbct_update_to_6_76_0() { global $apbct; diff --git a/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php b/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php index c9e3709b1..fe9ab1b81 100644 --- a/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php +++ b/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php @@ -232,6 +232,20 @@ public function checkoutCheck($_data, $errors) ct_hash($ct_result->id); if ( $ct_result->allow == 0 ) { + if ( $apbct->settings['forms__wc_show_rejection_message'] ) { + // The customer is told why the order was rejected and is left on the checkout + if ( $apbct->settings['data__wc_store_blocked_orders'] ) { + $this->storeBlockedOrder(); + } + + wp_send_json(array( + 'result' => 'failure', + 'messages' => '
  • ' . $ct_result->comment . '
', + 'refresh' => 'false', + 'reload' => 'false' + )); + } + $this->handleBlockedOrder(); wp_send_json(array( 'result' => 'success', @@ -289,10 +303,25 @@ public function checkoutCheckFromRest($order) ct_hash($ct_result->id); if ( $ct_result->allow == 0 ) { - // The details must be stored before the response carrying their key is built - $this->handleBlockedOrder($order); + $show_rejection = (bool)$apbct->settings['forms__wc_show_rejection_message']; + + if ( $show_rejection ) { + // The customer is told why the order was rejected and is left on the checkout + if ( $apbct->settings['data__wc_store_blocked_orders'] ) { + $this->storeBlockedOrder(); + } - $response = $this->getStoreApiPassedResponse($order); + $response = array( + 'code' => 'woocommerce_store_api_checkout_order_processed', + 'message' => $ct_result->comment, + 'data' => array('status' => 403), + ); + } else { + // The details must be stored before the response carrying their key is built + $this->handleBlockedOrder($order); + + $response = $this->getStoreApiPassedResponse($order); + } if ( $order->get_status() === 'pending' || $order->get_status() === 'checkout-draft' ) { if ( function_exists('wc_release_stock_for_order') ) { @@ -307,6 +336,9 @@ public function checkoutCheckFromRest($order) } if ( ! headers_sent() ) { + if ( $show_rejection ) { + http_response_code(403); + } header('Content-Type: application/json; charset=utf-8'); } die(json_encode($response)); diff --git a/lib/Cleantalk/ApbctWP/State.php b/lib/Cleantalk/ApbctWP/State.php index 9e8ed2041..ff2220536 100644 --- a/lib/Cleantalk/ApbctWP/State.php +++ b/lib/Cleantalk/ApbctWP/State.php @@ -58,6 +58,7 @@ class State extends \Cleantalk\Common\State 'forms__wc_checkout_test' => 1, // WooCommerce checkout default test 'forms__wc_register_from_order' => 1, // Woocommerce registration during checkout 'forms__wc_add_to_cart' => 0, // Woocommerce add to cart + 'forms__wc_show_rejection_message' => 0, // Tell the customer why the order was rejected 'forms__search_test' => 1, // Test default WordPress form 'forms__check_external' => 0, 'forms__check_external__capture_buffer' => 0,