PluginProbe
WCPOS – Point of Sale (POS) plugin for WooCommerce / 1.9.16
WCPOS – Point of Sale (POS) plugin for WooCommerce v1.9.16
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.16, at includes/API/Traits/WCPOS_REST_API.php

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