'3S', 'Serie' => '000000000-999999999', 'CustomerCode' => $customer_code, 'CustomerNumber' => $customer_num, 'Range' => $range, ), $endpoint ); $response = wp_remote_get( $url, array( 'timeout' => 15, 'headers' => array( 'apikey' => $api_key, 'NewKey' => Settings::get_instance()->get_new_key_header_value(), 'accept' => 'application/json', 'Content-Type' => 'application/json', 'SourceSystem' => '35', ), ) ); $logger = Main::get_logger(); if ( $logger ) { $logger->write( 'PostNL new API key validation request (V1 barcode).' ); } $reason = self::classify( $response ); return self::REASON_VALID === $reason ? true : self::error( $reason ); } /** * Classify a Barcode API response into a reason. * * The HTTP status code decides first; the body is consulted only to tell a * genuine rejection (a fault or Errors payload returned on a 2xx) apart from * a real barcode. Reading the body first is what made a 429 or 503 carrying * an Apigee fault look like a bad key, so status wins here. * * @param array|\WP_Error $response Result of wp_remote_get(). * * @return string One of the REASON_* slugs. */ protected static function classify( $response ) { if ( is_wp_error( $response ) ) { return self::REASON_UNREACHABLE; } $code = (int) wp_remote_retrieve_response_code( $response ); $data = json_decode( wp_remote_retrieve_body( $response ), true ); if ( 401 === $code || 403 === $code ) { return self::REASON_INVALID; } if ( $code >= 200 && $code < 300 ) { if ( is_array( $data ) && ( ! empty( $data['fault'] ) || ! empty( $data['Errors'] ) || ! empty( $data['Error'] ) ) ) { return self::REASON_INVALID; } if ( is_array( $data ) && isset( $data['Barcode'] ) ) { return self::REASON_VALID; } // A 2xx with no barcode means PostNL never actually minted one, so we // have no proof the key works — treat it as "could not check", not bad. return self::REASON_UNREACHABLE; } if ( 429 === $code || $code >= 500 ) { return self::REASON_UNREACHABLE; } // Any other 4xx: PostNL understood the request and refused it, most often // because the Customer Code or Number does not go with this key. return self::REASON_REJECTED; } /** * Build the WP_Error for a non-valid reason, carrying the reason as its code * and the merchant-facing sentence as its message. * * @param string $reason One of the REASON_* slugs. * * @return \WP_Error */ protected static function error( $reason ) { return new \WP_Error( $reason, self::reason_message( $reason ) ); } /** * Merchant-facing sentence for a validation reason. Kept as the single source * so the save-time notice and the on-blur endpoint word the same outcome the * same way. * * @param string $reason One of the REASON_* slugs. * * @return string */ public static function reason_message( $reason ) { switch ( $reason ) { case self::REASON_MISSING: return __( 'Fill in your Customer Code and Customer Number first, then check the key again.', 'postnl-for-woocommerce' ); case self::REASON_REJECTED: return __( 'PostNL could not process the check. This usually means the Customer Code or Customer Number does not match this key.', 'postnl-for-woocommerce' ); case self::REASON_UNREACHABLE: return __( 'We could not reach PostNL to check the key. Please try again in a few minutes.', 'postnl-for-woocommerce' ); case self::REASON_INVALID: default: return __( 'The newly entered API key is invalid. Please check the key and enter it again.', 'postnl-for-woocommerce' ); } } }