PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.9.15
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.9.15
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
woocommerce-pos / includes / API / Traits / WCPOS_REST_API.php

WCPOS_REST_API.php in WCPOS – Point of Sale (POS) plugin for WooCommerce 1.9.15, at includes/API/Traits/WCPOS_REST_API.php

417 lines 13.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WCPOS_REST_API.
4 *
5 * @package WCPOS\WooCommercePOS
6 */
7
8 namespace WCPOS\WooCommercePOS\API\Traits;
9
10 use Automattic\WooCommerce\Utilities\OrderUtil;
11 use WC_Data;
12 use WCPOS\WooCommercePOS\Logger;
13 use WP_REST_Response;
14 use Exception;
15
16 /**
17 * Shared helpers for all WCPOS REST API controllers.
18 */
19 trait WCPOS_REST_API {
20 /**
21 * Formats the response for all fetched posts into associative arrays.
22 *
23 * @param array $results The raw results from the database query.
24 *
25 * @return array An array of associative arrays with post information.
26 */
27 public function wcpos_format_all_posts_response( $results ) {
28 /**
29 * Performance notes:
30 * - Using a generator is faster than array_map when dealing with large datasets.
31 * - If date is in the format 'Y-m-d H:i:s' we just do preg_replace to 'Y-m-d\TH:i:s', rather than using wc_rest_prepare_date_response
32 *
33 * This resulted in execution time of 10% of the original time.
34 */
35 return iterator_to_array(
36 ( function () use ( $results ) {
37 foreach ( $results as $result ) {
38 $result['id'] = (int) $result['id'];
39
40 if ( isset( $result['date_modified_gmt'] ) ) {
41 if ( preg_match( '/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/', $result['date_modified_gmt'] ) ) {
42 $result['date_modified_gmt'] = preg_replace( '/(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})/', '$1T$2', $result['date_modified_gmt'] );
43 } else {
44 $result['date_modified_gmt'] = wc_rest_prepare_date_response( $result['date_modified_gmt'] );
45 }
46 }
47
48 yield $result;
49 }
50 } )()
51 );
52 }
53
54 /**
55 * BUG FIX: some servers are not returning the correct meta_data if it is left as WC_Meta_Data objects
56 * NOTE: it only seems to effect some versions of PHP, or some plugins are adding weird meta_data types
57 * The result is mata_data: [{}, {}, {}] ie: empty objects, I think json_encode can't handle the WC_Meta_Data objects.
58 *
59 * @param WC_Data $object The WC_Data object to parse meta from.
60 *
61 * @return array
62 */
63 public function wcpos_parse_meta_data( WC_Data $object ): array {
64 $raw_meta = $object->get_meta_data();
65 $meta_data = array_map(
66 function ( $meta_data ) {
67 return $meta_data->get_data();
68 },
69 $raw_meta
70 );
71
72 // Monitor meta count and log if thresholds exceeded.
73 $this->wcpos_monitor_meta_count( $object, $raw_meta );
74
75 return $meta_data;
76 }
77
78 /**
79 * Monitor meta_data count and log warnings/errors when thresholds are exceeded.
80 *
81 * Uses a static array to throttle logging: one log per object per request lifecycle.
82 *
83 * @param WC_Data $object The WC_Data object.
84 * @param array $raw_meta Array of WC_Meta_Data objects.
85 */
86 private function wcpos_monitor_meta_count( WC_Data $object, array $raw_meta ): void {
87 static $logged_ids = array();
88
89 $count = \count( $raw_meta );
90 $id = $object->get_id();
91
92 // Throttle: one log per object per request.
93 $key = \get_class( $object ) . '_' . $id;
94 if ( isset( $logged_ids[ $key ] ) ) {
95 return;
96 }
97
98 $warning_threshold = (int) apply_filters( 'woocommerce_pos_meta_data_warning_threshold', 50 );
99 $error_threshold = (int) apply_filters( 'woocommerce_pos_meta_data_error_threshold', 500 );
100 $include_top_keys = (bool) apply_filters( 'woocommerce_pos_meta_data_log_top_keys', false, $object, $count );
101 $context = $include_top_keys ? 'Top meta keys: ' . $this->wcpos_get_top_meta_keys( $raw_meta ) : null;
102
103 if ( $count >= $error_threshold ) {
104 $logged_ids[ $key ] = true;
105 $type = $this->wcpos_get_object_type_label( $object );
106 Logger::error(
107 "{$type} #{$id} has {$count} meta_data entries (threshold: {$error_threshold}). This is likely causing performance issues.",
108 $context
109 );
110 } elseif ( $count >= $warning_threshold ) {
111 $logged_ids[ $key ] = true;
112 $type = $this->wcpos_get_object_type_label( $object );
113 Logger::warning(
114 "{$type} #{$id} has {$count} meta_data entries (threshold: {$warning_threshold}). This may indicate plugin meta bloat.",
115 $context
116 );
117 }
118 }
119
120 /**
121 * Get a human-readable label for a WC_Data object type.
122 *
123 * @param WC_Data $object The WC_Data object.
124 *
125 * @return string
126 */
127 private function wcpos_get_object_type_label( WC_Data $object ): string {
128 if ( $object instanceof \WC_Order ) {
129 return 'Order';
130 }
131 if ( $object instanceof \WC_Product_Variation ) {
132 return 'Variation';
133 }
134 if ( $object instanceof \WC_Product ) {
135 return 'Product';
136 }
137 if ( $object instanceof \WC_Customer ) {
138 return 'Customer';
139 }
140
141 return 'Object';
142 }
143
144 /**
145 * Get a string of the top 10 most common meta keys and their counts.
146 *
147 * @param array $raw_meta Array of WC_Meta_Data objects.
148 *
149 * @return string Formatted string like "_yoast_seo (12), _elementor_data (8), ..."
150 */
151 private function wcpos_get_top_meta_keys( array $raw_meta ): string {
152 $counts = array();
153 foreach ( $raw_meta as $meta ) {
154 $meta_key = $meta->key;
155 if ( ! isset( $counts[ $meta_key ] ) ) {
156 $counts[ $meta_key ] = 0;
157 }
158 ++$counts[ $meta_key ];
159 }
160 arsort( $counts );
161 $top = \array_slice( $counts, 0, 10, true );
162
163 $parts = array();
164 foreach ( $top as $meta_key => $cnt ) {
165 $parts[] = "{$meta_key} ({$cnt})";
166 }
167
168 return implode( ', ', $parts );
169 }
170
171 /**
172 * Estimate the response size and log if it exceeds thresholds.
173 *
174 * Uses a lightweight calculation instead of serialize() to avoid doubling memory usage.
175 *
176 * @param array $data The response data array.
177 * @param int $id The object ID.
178 * @param string $type The object type label (e.g. 'Product', 'Order').
179 */
180 public function wcpos_estimate_response_size( array $data, int $id, string $type ): void {
181 static $logged_ids = array();
182
183 $key = $type . '_' . $id;
184 if ( isset( $logged_ids[ $key ] ) ) {
185 return;
186 }
187
188 // Estimate: meta_count * 200 bytes + string field lengths.
189 $meta_count = isset( $data['meta_data'] ) ? \count( $data['meta_data'] ) : 0;
190 $estimated_size = $meta_count * 200;
191
192 // Add string field sizes.
193 $string_fields = array( 'description', 'short_description', 'content' );
194 foreach ( $string_fields as $field ) {
195 if ( isset( $data[ $field ] ) && \is_string( $data[ $field ] ) ) {
196 $estimated_size += \strlen( $data[ $field ] );
197 }
198 }
199
200 $warning_threshold = (int) apply_filters( 'woocommerce_pos_response_size_warning_threshold', 100000 );
201 $error_threshold = (int) apply_filters( 'woocommerce_pos_response_size_error_threshold', 500000 );
202
203 if ( $estimated_size >= $error_threshold ) {
204 $logged_ids[ $key ] = true;
205 $size_kb = round( $estimated_size / 1024, 1 );
206 $threshold_kb = round( $error_threshold / 1024, 1 );
207 Logger::error( "{$type} #{$id} estimated response size {$size_kb}KB exceeds {$threshold_kb}KB threshold." );
208 } elseif ( $estimated_size >= $warning_threshold ) {
209 $logged_ids[ $key ] = true;
210 $size_kb = round( $estimated_size / 1024, 1 );
211 $threshold_kb = round( $warning_threshold / 1024, 1 );
212 Logger::warning( "{$type} #{$id} estimated response size {$size_kb}KB exceeds {$threshold_kb}KB threshold." );
213 }
214 }
215
216 /**
217 * Pre-flight check: count meta entries for an object before WC loads it.
218 *
219 * Runs a cheap SELECT COUNT(*) query. Callers should check the return value
220 * and bypass WC's response pipeline if the count exceeds the error threshold.
221 *
222 * @param int $object_id The object ID.
223 * @param string $object_type One of 'post', 'order', 'user'.
224 *
225 * @return int The meta count.
226 */
227 public function wcpos_preflight_meta_count( int $object_id, string $object_type = 'post' ): int {
228 global $wpdb;
229
230 switch ( $object_type ) {
231 case 'order':
232 if ( class_exists( OrderUtil::class ) && OrderUtil::custom_orders_table_usage_is_enabled() ) {
233 $table = "{$wpdb->prefix}wc_orders_meta";
234 $column = 'order_id';
235 } else {
236 $table = $wpdb->postmeta;
237 $column = 'post_id';
238 }
239 break;
240
241 case 'user':
242 $table = $wpdb->usermeta;
243 $column = 'user_id';
244 break;
245
246 default:
247 $table = $wpdb->postmeta;
248 $column = 'post_id';
249 break;
250 }
251
252 $count = (int) $wpdb->get_var(
253 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table/column names are safe hardcoded values.
254 $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE {$column} = %d", $object_id )
255 );
256
257 return $count;
258 }
259
260 /**
261 * Get only the essential POS meta keys for an object when the full meta load would OOM.
262 *
263 * @param int $object_id The object ID.
264 * @param string $object_type One of 'post', 'order', 'user'.
265 * @param array $extra_keys Additional meta keys to include.
266 *
267 * @return array Array of meta entries in WC REST format [{id, key, value}, ...].
268 */
269 public function wcpos_get_essential_meta( int $object_id, string $object_type = 'post', array $extra_keys = array() ): array {
270 global $wpdb;
271
272 // Base essential key present for all object types.
273 $keys = array( '_woocommerce_pos_uuid' );
274 $keys = array_merge( $keys, $extra_keys );
275
276 // Build LIKE patterns for wildcard pro keys (products/variations only).
277 $like_patterns = array();
278
279 switch ( $object_type ) {
280 case 'order':
281 if ( class_exists( OrderUtil::class ) && OrderUtil::custom_orders_table_usage_is_enabled() ) {
282 $table = "{$wpdb->prefix}wc_orders_meta";
283 $id_col = 'order_id';
284 $meta_id = 'id';
285 } else {
286 $table = $wpdb->postmeta;
287 $id_col = 'post_id';
288 $meta_id = 'meta_id';
289 }
290 $keys = array_merge(
291 $keys,
292 array(
293 '_pos_user',
294 '_pos_store',
295 '_pos_cash_amount_tendered',
296 '_pos_cash_change',
297 '_pos_card_cashback',
298 '_woocommerce_pos_tax_based_on',
299 )
300 );
301 break;
302
303 case 'user':
304 $table = $wpdb->usermeta;
305 $id_col = 'user_id';
306 $meta_id = 'umeta_id';
307 break;
308
309 default: // post (products, variations).
310 $table = $wpdb->postmeta;
311 $id_col = 'post_id';
312 $meta_id = 'meta_id';
313
314 // Add barcode field if it's a custom meta key.
315 $barcode_field = woocommerce_pos_get_settings( 'general', 'barcode_field' );
316 if ( \is_string( $barcode_field ) && ! empty( $barcode_field )
317 && '_sku' !== $barcode_field && '_global_unique_id' !== $barcode_field ) {
318 $keys[] = $barcode_field;
319 }
320 $keys[] = '_woocommerce_pos_variable_prices';
321
322 // Pro store-specific pricing keys use wildcard patterns.
323 $like_patterns = array(
324 '_pos_price%',
325 '_pos_regular_price%',
326 '_pos_sale_price%',
327 '_pos_tax_status%',
328 '_pos_tax_class%',
329 '_pos_price_fields%',
330 '_pos_tax_fields%',
331 );
332 break;
333 }
334
335 $keys = array_unique( $keys );
336
337 // Build the WHERE clause.
338 $placeholders = implode( ', ', array_fill( 0, \count( $keys ), '%s' ) );
339 $prepare_args = array_merge( array( $object_id ), $keys );
340
341 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table/column names are safe hardcoded values.
342 $where = $wpdb->prepare( "{$id_col} = %d AND meta_key IN ({$placeholders})", $prepare_args );
343
344 // Add LIKE patterns for wildcard keys.
345 foreach ( $like_patterns as $pattern ) {
346 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- table/column names are safe hardcoded values.
347 $where .= $wpdb->prepare( " OR ({$id_col} = %d AND meta_key LIKE %s)", $object_id, $pattern );
348 }
349
350 // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared -- built safely above.
351 $results = $wpdb->get_results( "SELECT {$meta_id} as meta_id, meta_key, meta_value FROM {$table} WHERE {$where}" );
352
353 if ( ! $results ) {
354 return array();
355 }
356
357 return array_map(
358 function ( $row ) {
359 return array(
360 'id' => (int) $row->meta_id,
361 'key' => $row->meta_key,
362 'value' => maybe_unserialize( $row->meta_value ),
363 );
364 },
365 $results
366 );
367 }
368
369 /**
370 * Get barcode field from settings.
371 *
372 * @return bool
373 */
374 public function wcpos_allow_decimal_quantities() {
375 $allow_decimal_quantities = woocommerce_pos_get_settings( 'general', 'decimal_qty' );
376
377 // Check for WP_Error.
378 if ( is_wp_error( $allow_decimal_quantities ) ) {
379 Logger::log( 'Error retrieving decimal_qty: ' . $allow_decimal_quantities->get_error_message() );
380
381 return false;
382 }
383
384 // make sure it's true, just in case there's a corrupt setting.
385 return true === $allow_decimal_quantities;
386 }
387
388 /**
389 * Get server load average.
390 *
391 * @return array The load average.
392 */
393 public function get_server_load() {
394 try {
395 if ( stristr( PHP_OS, 'win' ) ) {
396 // Use WMIC to get load percentage from Windows.
397 $load = @shell_exec( 'wmic cpu get loadpercentage /all' ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
398 if ( $load ) {
399 $load = explode( "\n", $load );
400 if ( isset( $load[1] ) ) {
401 $load = intval( $load[1] );
402 return array( $load, $load, $load ); // Mimic the array structure of sys_getloadavg().
403 }
404 }
405 } elseif ( function_exists( 'sys_getloadavg' ) ) {
406 return sys_getloadavg();
407 }
408 } catch ( Exception $e ) {
409 // Log the error for debugging purposes.
410 Logger::log( 'Error getting server load: ' . $e->getMessage() );
411 }
412
413 // Fallback if no method is available or an error occurs.
414 return array( 0, 0, 0 );
415 }
416 }
417