PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.17
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.17
1.10.18 1.10.17 1.10.16 1.10.15 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 All 162 releases
woocommerce-pos / includes / API / V2 / Ping.php

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

222 lines 8.5 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
41 /**
42 * Host CPU count, or null when unavailable.
43 *
44 * @var int|null
45 */
46 private static $host_cpu_count = null;
47
48 /**
49 * Whether the host CPU count has been resolved.
50 *
51 * @var bool
52 */
53 private static $host_cpu_count_resolved = false;
54 // phpcs:disable Squiz.Commenting.FunctionComment.MissingParamTag, Squiz.Commenting.FunctionComment.Missing -- Typed signatures keep this bootstrap path within its strict size budget.
55 /** Detect an exact raw ping request. */
56 public static function matches_request( string $method, string $request_uri, ?string $rest_route ): bool {
57 if ( 'GET' !== $method && 'HEAD' !== $method ) {
58 return false;
59 }
60 $path = explode( '?', $request_uri, 2 )[0];
61
62 return self::ROUTE === $rest_route || ( \strlen( $path ) >= \strlen( self::PRETTY_ROUTE ) && self::PRETTY_ROUTE === substr( $path, -\strlen( self::PRETTY_ROUTE ) ) );
63 }
64
65 /**
66 * Response headers that keep the ping out of proxy and server caches.
67 *
68 * The fast path answers before WP REST exists, so Rest_Cors never adds
69 * its cache-defeating headers here; without these an origin page cache
70 * served one host's ping (timestamp and pressure bucket) frozen for its
71 * whole TTL (measured 2026-09-16). Same Cache-Control value as Rest_Cors.
72 *
73 * @return array<string, string>
74 */
75 public static function cache_defeating_headers(): array {
76 return array(
77 'Cache-Control' => 'private, no-store',
78 'X-LiteSpeed-Cache-Control' => 'no-cache',
79 );
80 }
81
82 /** Belt and braces for drop-in page caches that finalise at shutdown and read constants, not headers. */
83 private static function forbid_page_cache(): void {
84 foreach ( array( 'DONOTCACHEPAGE', 'LSCACHE_NO_CACHE' ) as $constant ) {
85 if ( ! \defined( $constant ) ) {
86 \define( $constant, true ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedConstantFound -- third-party constant.
87 }
88 }
89 }
90
91 /** Serve a matching request before the remaining plugins load. */
92 public static function maybe_serve(): void {
93 $method = isset( $_SERVER['REQUEST_METHOD'] ) && \is_string( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
94 if ( 'GET' !== $method && 'HEAD' !== $method ) {
95 return;
96 }
97 $request_uri = isset( $_SERVER['REQUEST_URI'] ) && \is_string( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
98 if ( false === strpos( $request_uri, 'wcpos' ) ) {
99 return;
100 }
101 $rest_route = isset( $_GET['rest_route'] ) && \is_string( $_GET['rest_route'] ) ? sanitize_text_field( wp_unslash( $_GET['rest_route'] ) ) : null;
102 if ( ! self::matches_request( $method, $request_uri, $rest_route ) ) {
103 return;
104 }
105 $data = self::payload();
106 self::forbid_page_cache();
107 http_response_code( 200 );
108 header( 'Content-Type: application/json; charset=UTF-8' );
109 foreach ( self::cache_defeating_headers() as $name => $value ) {
110 header( $name . ': ' . $value );
111 }
112 header( 'Access-Control-Allow-Origin: *' );
113 // Deliberately just the one header this fast path can emit, not the
114 // full Rest_Cors::EXPOSE_HEADERS set: this short-circuits before the
115 // autoloader and WP REST exist. The OPTIONS preflight for this route
116 // is answered by Rest_Cors on the normal REST lane.
117 header( 'Access-Control-Expose-Headers: X-WCPOS-Pressure' );
118 if ( isset( $data['pressure'] ) ) {
119 header( 'X-WCPOS-Pressure: ' . $data['pressure'] );
120 }
121 if ( 'HEAD' !== $method ) {
122 echo wp_json_encode( $data ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON HTTP response.
123 }
124 // Ship the response before exit's shutdown handlers run: the OTel
125 // wordpress instrumentation reads conditional tags (is_404) at shutdown,
126 // and with no query having run, WP_DEBUG_DISPLAY sites would append a
127 // _doing_it_wrong notice after the JSON body (#1582). Under FPM, closing
128 // the request first makes late output unreachable; elsewhere, mute
129 // display so shutdown notices cannot corrupt the payload.
130 if ( \function_exists( 'fastcgi_finish_request' ) ) {
131 fastcgi_finish_request();
132 } else {
133 @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.
134 }
135 exit;
136 }
137
138 /** Register the canonical REST fallback. */
139 public function register_routes(): void {
140 register_rest_route(
141 'wcpos/v2',
142 '/ping',
143 array(
144 'methods' => 'GET, HEAD',
145 'callback' => array( $this, 'get_ping' ),
146 'permission_callback' => '__return_true',
147 )
148 );
149 }
150
151 /** @return array<string, string[]> */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- compact typed classification.
152 public function wcpos_route_classifications(): array {
153 return array( 'public' => array( self::ROUTE ) );
154 }
155
156 /** Return the canonical REST response. */
157 public function get_ping(): WP_REST_Response {
158 $data = self::payload();
159 $response = new WP_REST_Response( $data, 200 );
160 if ( isset( $data['pressure'] ) ) {
161 $response->header( 'X-WCPOS-Pressure', $data['pressure'] );
162 }
163
164 return $response;
165 }
166
167 /** Convert normalized load to a pressure bucket, or read host load when omitted (memoized per request so body and header always agree). */
168 public static function pressure_bucket( ?float $load = null ): ?string {
169 if ( null === $load ) {
170 if ( ! self::$host_pressure_checked ) {
171 self::$host_pressure_checked = true;
172 self::$host_pressure_bucket = self::read_host_pressure_bucket();
173 }
174
175 return self::$host_pressure_bucket;
176 }
177 if ( $load < 0.9 ) {
178 return 'low';
179 }
180
181 return $load <= 1.8 ? 'elevated' : 'high';
182 }
183
184 /**
185 * Use only /proc/cpuinfo because sys_getloadavg() reads host-wide /proc/loadavg,
186 * so its CPU divisor must share the host namespace rather than a container quota.
187 */
188 public static function cpu_count_from_cpuinfo( ?string $cpuinfo ): ?int {
189 $found = null !== $cpuinfo ? preg_match_all( '/^processor\s*:/m', $cpuinfo ) : false;
190
191 return \is_int( $found ) && $found > 0 ? $found : null;
192 }
193
194 /**
195 * Normalize host load using the /proc/cpuinfo CPU count.
196 * Unknown counts yield null (no header), rather than misleading pressure from a guessed divisor.
197 */
198 private static function read_host_pressure_bucket(): ?string {
199 if ( ! \function_exists( 'sys_getloadavg' ) || ! \is_array( $average = @sys_getloadavg() ) || ! isset( $average[0] ) ) { // phpcs:ignore Squiz.PHP.DisallowMultipleAssignments.FoundInControlStructure -- call only after availability check.
200 return null;
201 }
202 if ( ! self::$host_cpu_count_resolved ) {
203 $cpuinfo = @file_get_contents( '/proc/cpuinfo' );
204 self::$host_cpu_count = self::cpu_count_from_cpuinfo( false === $cpuinfo ? null : $cpuinfo );
205 self::$host_cpu_count_resolved = true;
206 }
207
208 return null === self::$host_cpu_count ? null : self::pressure_bucket( (float) $average[0] / self::$host_cpu_count );
209 }
210
211 /** @return array<string, bool|int|string> */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- compact typed payload.
212 private static function payload(): array {
213 $data = array( 'ok' => true, 'ts' => time(), 'v' => VERSION ); // phpcs:ignore WordPress.Arrays.ArrayDeclarationSpacing.AssociativeArrayFound -- fixed four-field maximum.
214 $pressure = self::pressure_bucket();
215 if ( null !== $pressure ) {
216 $data['pressure'] = $pressure;
217 }
218
219 return $data;
220 }
221 }
222