PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / trunk
WCPOS – Point of Sale (POS) plugin for WooCommerce vtrunk
1.10.13 1.10.14 1.10.12 1.10.11 1.10.10 1.10.9 1.10.8 untagged-3d9b7ccddc54df87c672 1.10.7 1.10.6 1.10.5 1.10.3 1.10.4 1.10.2 1.10.1 1.10.0 1.9.17 1.9.15 1.9.16 1.9.14 1.9.13 1.9.12 1.9.11 1.9.10 1.9.9 All 158 releases
woocommerce-pos / includes / API / V2 / Ping.php

Ping.php in WCPOS – Point of Sale (POS) plugin for WooCommerce trunk, at includes/API/V2/Ping.php

167 lines 6.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Public ping REST API controller and bootstrap fast path.
4 *
5 * @package WCPOS\WooCommercePOS\API\V2
6 */
7
8 namespace WCPOS\WooCommercePOS\API\V2;
9
10 use WP_REST_Response;
11 use const WCPOS\WooCommercePOS\VERSION;
12 /**
13 * Serves the lightweight public status response.
14 *
15 * Loaded cross-plugin: Pro bundles a copy of this file and, on a site running both
16 * plugins, whichever plugin loads first declares the class for BOTH. So the version
17 * of this class in memory may be older or newer than the bootstrap calling into it,
18 * and that call happens during the plugin include phase where a "Call to undefined
19 * method" is an unrecoverable site-wide fatal. Treat the public static entry points
20 * as a frozen ABI: add methods, never rename or change the signature of an existing
21 * one.
22 */
23 final class Ping {
24 private const ROUTE = '/wcpos/v2/ping';
25 private const PRETTY_ROUTE = '/wp-json/wcpos/v2/ping';
26
27 /**
28 * Request-scoped host pressure bucket.
29 *
30 * @var string|null
31 */
32 private static $host_pressure_bucket = null;
33
34 /**
35 * Whether host pressure has been read this request.
36 *
37 * @var bool
38 */
39 private static $host_pressure_checked = false;
40 // phpcs:disable Squiz.Commenting.FunctionComment.MissingParamTag, Squiz.Commenting.FunctionComment.Missing -- Typed signatures keep this bootstrap path within its strict size budget.
41 /** Detect an exact raw ping request. */
42 public static function matches_request( string $method, string $request_uri, ?string $rest_route ): bool {
43 if ( 'GET' !== $method && 'HEAD' !== $method ) {
44 return false;
45 }
46 $path = explode( '?', $request_uri, 2 )[0];
47
48 return self::ROUTE === $rest_route || ( \strlen( $path ) >= \strlen( self::PRETTY_ROUTE ) && self::PRETTY_ROUTE === substr( $path, -\strlen( self::PRETTY_ROUTE ) ) );
49 }
50
51 /** Serve a matching request before the remaining plugins load. */
52 public static function maybe_serve(): void {
53 $method = isset( $_SERVER['REQUEST_METHOD'] ) && \is_string( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
54 if ( 'GET' !== $method && 'HEAD' !== $method ) {
55 return;
56 }
57 $request_uri = isset( $_SERVER['REQUEST_URI'] ) && \is_string( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
58 if ( false === strpos( $request_uri, 'wcpos' ) ) {
59 return;
60 }
61 $rest_route = isset( $_GET['rest_route'] ) && \is_string( $_GET['rest_route'] ) ? sanitize_text_field( wp_unslash( $_GET['rest_route'] ) ) : null;
62 if ( ! self::matches_request( $method, $request_uri, $rest_route ) ) {
63 return;
64 }
65 $data = self::payload();
66 http_response_code( 200 );
67 header( 'Content-Type: application/json; charset=UTF-8' );
68 header( 'Access-Control-Allow-Origin: *' );
69 // Deliberately just the one header this fast path can emit, not the
70 // full Rest_Cors::EXPOSE_HEADERS set: this short-circuits before the
71 // autoloader and WP REST exist. The OPTIONS preflight for this route
72 // is answered by Rest_Cors on the normal REST lane.
73 header( 'Access-Control-Expose-Headers: X-WCPOS-Pressure' );
74 if ( isset( $data['pressure'] ) ) {
75 header( 'X-WCPOS-Pressure: ' . $data['pressure'] );
76 }
77 if ( 'HEAD' !== $method ) {
78 echo wp_json_encode( $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON HTTP response.
79 }
80 // Ship the response before exit's shutdown handlers run: the OTel
81 // wordpress instrumentation reads conditional tags (is_404) at shutdown,
82 // and with no query having run, WP_DEBUG_DISPLAY sites would append a
83 // _doing_it_wrong notice after the JSON body (#1582). Under FPM, closing
84 // the request first makes late output unreachable; elsewhere, mute
85 // display so shutdown notices cannot corrupt the payload.
86 if ( \function_exists( 'fastcgi_finish_request' ) ) {
87 fastcgi_finish_request();
88 } else {
89 @ini_set( 'display_errors', '0' ); // phpcs:ignore WordPress.PHP.IniSet.display_errors_Disallowed, WordPress.PHP.NoSilencedErrors.Discouraged -- last-resort mute on non-FPM SAPIs; the response is already emitted.
90 }
91 exit;
92 }
93
94 /** Register the canonical REST fallback. */
95 public function register_routes(): void {
96 register_rest_route(
97 'wcpos/v2',
98 '/ping',
99 array(
100 'methods' => 'GET, HEAD',
101 'callback' => array( $this, 'get_ping' ),
102 'permission_callback' => '__return_true',
103 )
104 );
105 }
106
107 /** @return array<string, string[]> */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- compact typed classification.
108 public function wcpos_route_classifications(): array {
109 return array( 'public' => array( self::ROUTE ) );
110 }
111
112 /** Return the canonical REST response. */
113 public function get_ping(): WP_REST_Response {
114 $data = self::payload();
115 $response = new WP_REST_Response( $data, 200 );
116 if ( isset( $data['pressure'] ) ) {
117 $response->header( 'X-WCPOS-Pressure', $data['pressure'] );
118 }
119
120 return $response;
121 }
122
123 /** Convert normalized load to a pressure bucket, or read host load when omitted (memoized per request so body and header always agree). */
124 public static function pressure_bucket( ?float $load = null ): ?string {
125 if ( null === $load ) {
126 if ( ! self::$host_pressure_checked ) {
127 self::$host_pressure_checked = true;
128 self::$host_pressure_bucket = self::read_host_pressure_bucket();
129 }
130
131 return self::$host_pressure_bucket;
132 }
133 if ( $load < 0.9 ) {
134 return 'low';
135 }
136
137 return $load <= 1.8 ? 'elevated' : 'high';
138 }
139
140 /** Read the host load average and convert it to a bucket, or null when unavailable. */
141 private static function read_host_pressure_bucket(): ?string {
142 if ( ! \function_exists( 'sys_getloadavg' ) || ! \is_array( $average = @sys_getloadavg() ) || ! isset( $average[0] ) ) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure -- call only after availability check.
143 return null;
144 }
145 /** @var int|null $cpus */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- local static type.
146 static $cpus = null;
147 if ( null === $cpus ) {
148 $cpuinfo = @file_get_contents( '/proc/cpuinfo' );
149 $found = \is_string( $cpuinfo ) ? preg_match_all( '/^processor\s*:/m', $cpuinfo ) : false;
150 $cpus = \is_int( $found ) && $found > 0 ? $found : 1;
151 }
152
153 return self::pressure_bucket( (float) $average[0] / $cpus );
154 }
155
156 /** @return array<string, bool|int|string> */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- compact typed payload.
157 private static function payload(): array {
158 $data = array( 'ok' => true, 'ts' => time(), 'v' => VERSION ); // phpcs:ignore WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound -- fixed four-field maximum.
159 $pressure = self::pressure_bucket();
160 if ( null !== $pressure ) {
161 $data['pressure'] = $pressure;
162 }
163
164 return $data;
165 }
166 }
167