PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.10.18
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.10.18
1.10.19 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 All 163 releases
← All changes | includes/API/V2/Ping.php +71 -8 1.10.11.10.18 View file →
@@ -10,8 +10,16 @@
10 10 use WP_REST_Response;
11 11 use const WCPOS\WooCommercePOS\VERSION;
12 12 /**
13 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.
14 22 */
15 23 final class Ping {
16 24 private const ROUTE = '/wcpos/v2/ping';
17 25 private const PRETTY_ROUTE = '/wp-json/wcpos/v2/ping';
@@ -28,8 +36,22 @@
28 36 *
29 37 * @var bool
30 38 */
31 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;
32 54 // phpcs:disable Squiz.Commenting.FunctionComment.MissingParamTag, Squiz.Commenting.FunctionComment.Missing -- Typed signatures keep this bootstrap path within its strict size budget.
33 55 /** Detect an exact raw ping request. */
34 56 public static function matches_request( string $method, string $request_uri, ?string $rest_route ): bool {
35 57 if ( 'GET' !== $method && 'HEAD' !== $method ) {
@@ -39,8 +61,34 @@
39 61
40 62 return self::ROUTE === $rest_route || ( \strlen( $path ) >= \strlen( self::PRETTY_ROUTE ) && self::PRETTY_ROUTE === substr( $path, -\strlen( self::PRETTY_ROUTE ) ) );
41 63 }
42 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 +
43 91 /** Serve a matching request before the remaining plugins load. */
44 92 public static function maybe_serve(): void {
45 93 $method = isset( $_SERVER['REQUEST_METHOD'] ) && \is_string( $_SERVER['REQUEST_METHOD'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) : '';
46 94 if ( 'GET' !== $method && 'HEAD' !== $method ) {
@@ -54,10 +102,14 @@
54 102 if ( ! self::matches_request( $method, $request_uri, $rest_route ) ) {
55 103 return;
56 104 }
57 105 $data = self::payload();
106 + self::forbid_page_cache();
58 107 http_response_code( 200 );
59 108 header( 'Content-Type: application/json; charset=UTF-8' );
109 + foreach ( self::cache_defeating_headers() as $name => $value ) {
110 + header( $name . ': ' . $value );
111 + }
60 112 header( 'Access-Control-Allow-Origin: *' );
61 113 // Deliberately just the one header this fast path can emit, not the
62 114 // full Rest_Cors::EXPOSE_HEADERS set: this short-circuits before the
63 115 // autoloader and WP REST exist. The OPTIONS preflight for this route
@@ -128,22 +180,33 @@
128 180
129 181 return $load <= 1.8 ? 'elevated' : 'high';
130 182 }
131 183
132 - /** Read the host load average and convert it to a bucket, or null when unavailable. */
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 + */
133 198 private static function read_host_pressure_bucket(): ?string {
134 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.
135 200 return null;
136 201 }
137 - /** @var int|null $cpus */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- local static type.
138 - static $cpus = null;
139 - if ( null === $cpus ) {
140 - $cpuinfo = @file_get_contents( '/proc/cpuinfo' );
141 - $found = \is_string( $cpuinfo ) ? preg_match_all( '/^processor\s*:/m', $cpuinfo ) : false;
142 - $cpus = \is_int( $found ) && $found > 0 ? $found : 1;
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;
143 206 }
144 207
145 - return self::pressure_bucket( (float) $average[0] / $cpus );
208 + return null === self::$host_cpu_count ? null : self::pressure_bucket( (float) $average[0] / self::$host_cpu_count );
146 209 }
147 210
148 211 /** @return array<string, bool|int|string> */ // phpcs:ignore Generic.Commenting.DocComment.MissingShort -- compact typed payload.
149 212 private static function payload(): array {