PluginProbe
PostNL for WooCommerce / trunk
PostNL for WooCommerce vtrunk
5.9.12 5.9.11 5.9.10 5.9.9 5.9.8 5.9.7 5.9.6 trunk 2.5.0 2.5.1 2.5.2 2.5.3 2.5.4 2.5.5 3.1.4 3.1.5 3.1.6 3.1.7 4.0.0 4.0.1 4.0.2 4.3.2 4.3.3 4.4.0 4.4.1 All 72 releases
woo-postnl / src / Admin / Api_Key_Check.php

Api_Key_Check.php in PostNL for WooCommerce trunk, at src/Admin/Api_Key_Check.php

122 lines 4.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Class Admin\Api_Key_Check file.
4 *
5 * Read-only AJAX endpoint that checks a new API key while the merchant is still
6 * typing it, so the status row under the field can go green (or red) before they
7 * save. It writes no option and stores no validated hash — that only happens on
8 * save — and it never logs the key.
9 *
10 * A successful check mints a real barcode from the merchant's range, so the
11 * endpoint is rate limited per user; the browser also memoises checked values
12 * and aborts in-flight requests so a quick blur/focus does not fan out calls.
13 *
14 * @package PostNLWooCommerce\Admin
15 */
16
17 namespace PostNLWooCommerce\Admin;
18
19 use PostNLWooCommerce\Rest_API\Barcode\Key_Validator;
20 use PostNLWooCommerce\Shipping_Method\Settings;
21
22 if ( ! defined( 'ABSPATH' ) ) {
23 exit;
24 }
25
26 /**
27 * Class Api_Key_Check
28 */
29 class Api_Key_Check {
30
31 const NONCE_ACTION = 'postnl_check_new_api_key';
32 const AJAX_ACTION = 'postnl_check_new_api_key';
33
34 /**
35 * Most checks a single user may run inside a rolling RATE_LIMIT_WINDOW-second
36 * window. Each check that reaches PostNL mints a barcode, so this bounds the
37 * volume; the counter's TTL is refreshed on every hit, so sustained checking
38 * stays capped rather than resetting.
39 */
40 const RATE_LIMIT_MAX = 10;
41 const RATE_LIMIT_WINDOW = 60;
42
43 /**
44 * Register the AJAX handler.
45 */
46 public function __construct() {
47 add_action( 'wp_ajax_' . self::AJAX_ACTION, array( $this, 'handle' ) );
48 }
49
50 /**
51 * Validate the posted key and return the status-row payload for it.
52 */
53 public function handle() {
54 if ( ! current_user_can( 'manage_woocommerce' ) ) {
55 wp_send_json_error( array( 'message' => __( 'You are not allowed to do this.', 'postnl-for-woocommerce' ) ), 403 );
56 }
57
58 check_ajax_referer( self::NONCE_ACTION, 'nonce' );
59
60 $settings = Settings::get_instance();
61 $is_sandbox = isset( $_POST['environment'] ) && 'sandbox' === sanitize_key( wp_unslash( $_POST['environment'] ) );
62
63 // The key and customer details are taken from the request, not storage, so
64 // a merchant filling in all three fields at once is checked against what
65 // they just typed rather than the empty saved values.
66 $new_key = isset( $_POST['api_key'] ) ? trim( (string) wp_unslash( $_POST['api_key'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- API key is not free text; trimmed and sent as a header only.
67 $customer_code = isset( $_POST['customer_code'] ) ? sanitize_text_field( wp_unslash( $_POST['customer_code'] ) ) : '';
68 $customer_num = isset( $_POST['customer_num'] ) ? sanitize_text_field( wp_unslash( $_POST['customer_num'] ) ) : '';
69
70 $original = $is_sandbox
71 ? trim( (string) $settings->get_api_key_sandbox() )
72 : trim( (string) $settings->get_api_key() );
73
74 // Empty and same-as-old are decidable without touching PostNL. The browser
75 // already skips those, but guard here too so a crafted request cannot spend
76 // a barcode on them.
77 if ( '' === $new_key || $new_key === $original ) {
78 wp_send_json_success( $settings->build_new_key_status( $new_key, $original, false, $is_sandbox, false ) );
79 }
80
81 // This exact key already passed validation and is saved, so report the
82 // green "Valid" state without spending another barcode. Without this, a
83 // merchant who just focuses and blurs the pre-filled field would mint a
84 // barcode and see the row drop to amber (or red during an outage).
85 if ( $settings->is_api_key_new_validated_value( $new_key, $is_sandbox ) ) {
86 wp_send_json_success( $settings->build_new_key_status( $new_key, $original, true, $is_sandbox, true ) );
87 }
88
89 if ( ! $this->within_rate_limit() ) {
90 wp_send_json_error(
91 array( 'message' => __( 'Too many checks in a short time. Please wait a moment and try again.', 'postnl-for-woocommerce' ) ),
92 429
93 );
94 }
95
96 $result = Key_Validator::validate( $new_key, $customer_code, $customer_num, $is_sandbox );
97 $valid = ( true === $result );
98 $reason = $valid ? Key_Validator::REASON_VALID : $result->get_error_code();
99
100 wp_send_json_success( $settings->build_new_key_status( $new_key, $original, $valid, $is_sandbox, false, $reason ) );
101 }
102
103 /**
104 * Whether the current user is under the per-window check limit. Increments the
105 * counter as a side effect when it is.
106 *
107 * @return bool
108 */
109 protected function within_rate_limit() {
110 $transient = 'postnl_key_check_' . get_current_user_id();
111 $count = (int) get_transient( $transient );
112
113 if ( $count >= self::RATE_LIMIT_MAX ) {
114 return false;
115 }
116
117 set_transient( $transient, $count + 1, self::RATE_LIMIT_WINDOW );
118
119 return true;
120 }
121 }
122