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/inc/cleantalk-admin.php b/inc/cleantalk-admin.php
index f52e3e67d..c245d2b31 100644
--- a/inc/cleantalk-admin.php
+++ b/inc/cleantalk-admin.php
@@ -1198,7 +1198,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-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/inc/cleantalk-settings.php b/inc/cleantalk-settings.php
index cea88f860..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')),
@@ -2179,7 +2194,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/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/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/Antispam/IntegrationsByClass/Woocommerce.php b/lib/Cleantalk/Antispam/IntegrationsByClass/Woocommerce.php
index bafa1e415..fe9ab1b81 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();
@@ -94,27 +111,7 @@ public function doAjaxWork()
public function doAdminWork()
{
- 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 () {
- ?>
-
-
-
- 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 +232,24 @@ 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();
+ 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' => '
';
+ }
+
+ /**
+ * URL of the page shown to the visitor whose order was blocked.
+ *
+ * @return string
+ * @psalm-suppress UndefinedFunction
+ */
+ private function getBlockedOrderRedirectUrl()
+ {
+ $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);
+ }
+
+ /**
+ * 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
@@ -596,6 +810,237 @@ 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']);
+
+ // 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.
+ *
+ * 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()
+ {
+ $list_table = new \Cleantalk\ApbctWP\WcSpamOrdersListTable($this->getOrdersListViews());
+ ?>
+
+
+
+ renderPageNotices(); ?>
+
+
+ $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/State.php b/lib/Cleantalk/ApbctWP/State.php
index e0d637726..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,
@@ -94,7 +95,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
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.'));
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 b5afb11d8..ca7a4b8b2 100644
--- a/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php
+++ b/lib/Cleantalk/ApbctWP/WcSpamOrdersListTable.php
@@ -11,16 +11,28 @@ class WcSpamOrdersListTable extends CleantalkListTable
protected $apbct;
protected $wc_active = false;
- 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();
@@ -32,10 +44,7 @@ public function __construct()
$this->prepare_items();
global $apbct;
- $this->apbct = $apbct;
- $this->page_title = 'WooCommerce spam orders';
-
- $this->generatePageHeader();
+ $this->apbct = $apbct;
}
/**
@@ -62,8 +71,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,
));
@@ -83,29 +97,40 @@ 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',
'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 +138,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 +151,79 @@ 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
+ {
+ if ( ! is_null($this->embedded_views) ) {
+ return $this->embedded_views;
+ }
+
+ $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 +352,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 +403,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 +505,69 @@ 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 ) {
+ $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) {
+ 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)
@@ -325,10 +591,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 '