PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.1.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.1.0
2.3.0 2.2.0 2.1.1 2.1.0 2.0.0 1.10.0 1.9.1 1.9.0 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.6.0 All 59 releases
storeengine / includes / utils / helper.php

helper.php in StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More 2.1.0, at includes/utils/helper.php

2,201 lines 67.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php /** @noinspection PhpUnused */
2
3 namespace StoreEngine\Utils;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit;
7 }
8
9 use StoreEngine;
10 use StoreEngine\Classes\Countries;
11 use StoreEngine\Classes\Exceptions\StoreEngineException;
12 use StoreEngine\Classes\Logger;
13 use StoreEngine\Models\Cart;
14 use StoreEngine\Shipping\Methods\ShippingMethod;
15 use StoreEngine\Utils\traits\{Attribute,
16 Currency,
17 Customer,
18 DownloadPermission,
19 Gateway,
20 Integration,
21 Order,
22 Pages,
23 Product,
24 ThemePlugin};
25 use WP_Error;
26 use WP_Post;
27
28 class Helper extends Template {
29
30 use Customer, Pages, Order, Currency, Gateway,
31 Product, Integration, Attribute, DownloadPermission,
32 ThemePlugin, StoreEngine\Utils\Traits\Repository;
33
34 const PRODUCT_POST_TYPE = STOREENGINE_PLUGIN_SLUG . '_product';
35
36 const PRODUCT_CATEGORY_TAXONOMY = self::PRODUCT_POST_TYPE . '_category';
37
38 const PRODUCT_ATTRIBUTE_TAXONOMY = self::PRODUCT_POST_TYPE . '_attribute';
39
40 const PRODUCT_TAG_TAXONOMY = self::PRODUCT_POST_TYPE . '_tag';
41
42 const COUPON_POST_TYPE = STOREENGINE_PLUGIN_SLUG . '_coupon';
43
44 const DB_PREFIX = STOREENGINE_PLUGIN_SLUG . '_';
45
46 /**
47 * @var null|bool|int
48 */
49 protected static $dashboard_index = null;
50
51 public static function get_time() {
52 return time() + ( get_option( 'gmt_offset' ) * HOUR_IN_SECONDS );
53 }
54
55 public static function is_fse_theme() {
56 if ( function_exists( 'wp_is_block_theme' ) ) {
57 return wp_is_block_theme();
58 }
59 if ( function_exists( 'gutenberg_is_fse_theme' ) ) {
60 return \gutenberg_is_fse_theme();
61 }
62
63 return false;
64 }
65
66 public static function remove_line_break( string $content ): string {
67 $content = preg_replace( '/\s+/', ' ', $content );
68
69 return trim( $content );
70 }
71
72 public static function remove_tag_space( string $content ): string {
73 return preg_replace( '/>\s+</', '><', $content );
74 }
75
76 public static function add_string_quote( $value ) {
77 if ( in_array( gettype( $value ), array( 'integer', 'double', 'float' ), true ) ) {
78 return $value;
79 } elseif ( 'boolean' === gettype( $value ) ) {
80 return (int) $value;
81 }
82
83 return "'" . $value . "'";
84 }
85
86 public static function array_diff_recursive( $array1, $array2 ): array {
87 $difference = [];
88
89 foreach ( $array1 as $key => $value ) {
90 if ( is_array( $value ) ) {
91 if ( ! isset( $array2[ $key ] ) || ! is_array( $array2[ $key ] ) ) {
92 $difference[ $key ] = $value;
93 } else {
94 $newDiff = self::array_diff_recursive( $value, $array2[ $key ] );
95 if ( ! empty( $newDiff ) ) {
96 $difference[ $key ] = $newDiff;
97 }
98 }
99 } elseif ( ! array_key_exists( $key, $array2 ) || ( $array2[ $key ] !== $value ) ) {
100 $difference[ $key ] = $value;
101 }
102 }
103
104 return $difference;
105 }
106
107 public static function get_country_name( string $key ) {
108 $countries = Countries::init()->get_countries();
109
110 return $countries[ $key ] ?? '';
111 }
112
113 public static function cart(): ?\StoreEngine\Classes\Cart {
114 return StoreEngine::init()->get_cart();
115 }
116
117 public static function get_price_duration( $price, $duration, $duration_type ): string {
118 if ( 1 === $duration ) {
119 /* translators: 1. Price 2, duration */
120 return sprintf( __( '%1$s Every %2$s', 'storeengine' ), Formatting::price( $price ), ucfirst( $duration_type ) );
121 } else {
122 return ( Formatting::price( $price ) . ' / ' . $duration . '-' . $duration_type . 's' );
123 }
124 }
125
126 /**
127 * @deprecated
128 */
129 public static function get_enabled_payment_methods(): array {
130 $payments_settings = self::get_payments_settings();
131 $enabled_payment_methods = [];
132 if ( is_array( $payments_settings ) ) {
133 foreach ( $payments_settings as $payment_settings ) {
134 if ( $payment_settings['is_enabled'] ) {
135 $enabled_payment_methods[] = array(
136 'type' => $payment_settings['type'],
137 'title' => $payment_settings['title'] ?? $payment_settings['type'],
138 'instructions' => $payment_settings['instructions'] ?? null,
139 );
140 }
141 }
142 }
143
144 return $enabled_payment_methods;
145 }
146
147 /**
148 * @deprecated
149 */
150 public static function get_payments_settings( $payment_method = '', $default = null ) {
151 $payments_settings = \StoreEngine\Admin\Settings\Payments::get_settings_saved_data();
152
153 if ( is_array( $payments_settings ) ) {
154 if ( ! $payment_method ) {
155 return $payments_settings;
156 }
157
158 foreach ( $payments_settings as $payment_settings ) {
159 if ( $payment_settings['type'] === $payment_method ) {
160 return $payment_settings;
161 }
162 }
163
164 return [];
165 }
166
167 return $default;
168 }
169
170 /**
171 * @param string $page
172 * @param ?string $fallback
173 *
174 * @return string
175 */
176 public static function get_page_permalink( string $page, string $fallback = null ): string {
177 $page_id = self::get_settings( $page );
178 $permalink = 0 < $page_id ? get_permalink( $page_id ) : '';
179
180 if ( ! $permalink ) {
181 $permalink = is_null( $fallback ) ? get_home_url() : $fallback;
182 }
183
184 $permalink = apply_filters( "storeengine/get_{$page}_permalink", $permalink, $page_id, $fallback );
185
186 return apply_filters( 'storeengine/get_page_permalink', $permalink, $page, $page_id, $fallback );
187 }
188
189 public static function get_dashboard_url(): string {
190 return self::get_page_permalink( 'dashboard_page' );
191 }
192
193 public static function get_preloader_html() {
194 ob_start();
195 ?>
196 <div class="storeengine-initial-preloader"><?php esc_html_e( 'Loading...', 'storeengine' ); ?></div>
197 <?php
198 return ob_get_clean();
199 }
200
201
202 /**
203 * Get endpoint URL.
204 *
205 * Gets the URL for an endpoint, which varies depending on permalink settings.
206 *
207 * @param string $endpoint
208 * @param string|int|float $value
209 * @param string|false $permalink
210 *
211 * @return string
212 */
213 public static function get_endpoint_url( string $endpoint, $value = '', $permalink = '' ): string {
214 global $wp_query;
215
216 if ( ! $permalink ) {
217 $permalink = get_permalink();
218 }
219
220 // Map endpoint to options.
221 $query_vars = $wp_query->query_vars;
222 $orig_endpoint = $endpoint;
223 $endpoint = ! empty( $query_vars[ $endpoint ] ) ? $query_vars[ $endpoint ] : $endpoint;
224
225 if ( get_option( 'permalink_structure' ) ) {
226 if ( strstr( $permalink, '?' ) ) {
227 $query_string = '?' . wp_parse_url( $permalink, PHP_URL_QUERY );
228 $permalink = current( explode( '?', $permalink ) );
229 } else {
230 $query_string = '';
231 }
232
233 // Cleanup trailing slash.
234 $url = trailingslashit( untrailingslashit( $permalink ) );
235
236 if ( $value ) {
237 $url .= trailingslashit( untrailingslashit( $endpoint ) ) . user_trailingslashit( $value );
238 } else {
239 $url .= user_trailingslashit( $endpoint );
240 }
241
242 $url .= $query_string;
243 } elseif ( 'order-pay' === $orig_endpoint ) {
244 // The registered query vars for this endpoint are `order_pay` +
245 // `order_id` (see PermalinkRewrite::register_query_vars()), not a
246 // single `order-pay=<id>` pair — plain permalinks need both set
247 // explicitly or nothing on the page (is_valid_order_pay_page(),
248 // is_available(), etc.) will ever recognise the order-pay context.
249 $url = add_query_arg( [
250 'order_pay' => 'true',
251 'order_id' => $value,
252 ], $permalink );
253 } else {
254 $url = add_query_arg( $endpoint, $value, $permalink );
255 }
256
257 $url = apply_filters( "storeengine/get_{$endpoint}_endpoint_url", $url, $value, $permalink );
258
259 return apply_filters( 'storeengine/get_endpoint_url', $url, $endpoint, $value, $permalink );
260 }
261
262 /**
263 * @return ?string
264 */
265 public static function get_current_dashboard_endpoint(): ?string {
266 global $wp;
267
268 return $wp->query_vars['storeengine_dashboard_page'] ?? null;
269 }
270
271 public static function get_logout_redirect_url(): string {
272 return apply_filters( 'storeengine/logout_default_redirect_url', Helper::get_dashboard_url() );
273 }
274
275 public static function get_logout_url( string $redirect = '' ): string {
276 $redirect = $redirect ?: self::get_logout_redirect_url();
277 $args = [
278 'redirect_to' => $redirect,
279 'action' => 'logout',
280 ];
281 $logout_url = self::get_endpoint_url( 'customer-logout', '', self::get_dashboard_url() );
282 $logout_url = wp_nonce_url( add_query_arg( $args, $logout_url ), 'customer-logout' );
283
284 return apply_filters( 'storeengine/logout_url', $logout_url, $redirect );
285 }
286
287 /**
288 * Get account endpoint URL.
289 *
290 * @param string $endpoint Endpoint.
291 *
292 * @return string
293 */
294 public static function get_account_endpoint_url( string $endpoint, $value = '' ): ?string {
295 if ( 'dashboard' === $endpoint || 'myaccount' === $endpoint || 'index' === $endpoint ) {
296 return self::get_dashboard_url();
297 }
298
299 if ( 'customer-logout' === $endpoint ) {
300 return self::get_logout_url();
301 }
302
303 $url = self::get_endpoint_url( $endpoint, $value, self::get_dashboard_url() );
304
305 $url = apply_filters( "storeengine/dashboard/get_{$endpoint}_endpoint_url", $url, $value );
306
307 return apply_filters( 'storeengine/dashboard/get_endpoint_url', $url, $endpoint, $value );
308 }
309
310 public static function get_current_dashboard_endpoint_url( string $endpoint = null, $value = '' ): string {
311 return self::get_account_endpoint_url( $endpoint ?? self::get_current_dashboard_endpoint() ?? '', $value );
312 }
313
314 /**
315 * Get the link to the edit account details page.
316 *
317 * @return string
318 */
319 public static function customer_edit_account_url(): string {
320 $edit_account_url = self::get_endpoint_url( 'edit-account', '', self::get_dashboard_url() );
321
322 return apply_filters( 'storeengine/customer/edit_account_url', $edit_account_url );
323 }
324
325 /**
326 * add-filter to lostpassword_url
327 *
328 * @param $default_url
329 * @param $redirect
330 *
331 * @return mixed|string
332 */
333 public static function get_lost_password_url( $default_url = '', $redirect = '' ) {
334 // Avoid loading too early.
335 if ( ! did_action( 'init' ) ) {
336 return $default_url;
337 }
338
339 // Don't change the admin form.
340 if ( did_action( 'login_form_login' ) ) {
341 return $default_url;
342 }
343
344 // Don't redirect to the StoreEngine endpoint on global network admin lost passwords.
345 if ( is_multisite() && isset( $_GET['redirect_to'] ) && false !== strpos( wp_unslash( $_GET['redirect_to'] ), network_admin_url() ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
346 return $default_url;
347 }
348
349 $permalink = self::get_page_permalink( 'password_reset_page' );
350
351 if ( ! $permalink ) {
352 return $default_url;
353 }
354
355 if ( ! empty( $redirect ) ) {
356 return add_query_arg( [ 'redirect_to' => rawurlencode( $redirect ) ], $permalink );
357 }
358
359 return $permalink;
360 }
361
362 public static function get_cart_url() {
363 return apply_filters( 'storeengine/get_cart_url', self::maybe_force_ssl_protocol( self::get_page_permalink( 'cart_page' ) ) );
364 }
365
366 public static function get_checkout_url() {
367 return apply_filters( 'storeengine/checkout/get_checkout_url', self::maybe_force_ssl_protocol( self::get_page_permalink( 'checkout_page' ) ) );
368 }
369
370 public static function get_thankyou_page_url() {
371 return apply_filters( 'storeengine/checkout/thankyou_page_url', self::maybe_force_ssl_protocol(self::get_page_permalink( 'thankyou_page' )) );
372 }
373
374 public static function get_terms_page_url() {
375 return apply_filters( 'storeengine/terms_page_url', self::maybe_force_ssl_protocol( self::get_page_permalink( 'terms_page' ) ) );
376 }
377
378 public static function get_privacy_page_url() {
379 return apply_filters( 'storeengine/privacy_page_url', self::maybe_force_ssl_protocol(self::get_page_permalink( 'privacy_page' )) );
380 }
381
382 public static function get_shop_url() {
383 return apply_filters( 'storeengine/get_shop_url', self::maybe_force_ssl_protocol( self::get_page_permalink( 'shop_page' ) ) );
384 }
385
386 public static function maybe_force_ssl_protocol( $url ) {
387 if ( $url ) {
388 // Force SSL if needed.
389 if ( is_ssl() || self::get_settings( 'force_ssl_checkout' ) ) {
390 $url = str_replace( 'http:', 'https:', $url );
391 }
392 }
393
394 return $url;
395 }
396
397 public static function get_settings( $key, $default = null ) {
398 global $storeengine_settings;
399
400 $value = $storeengine_settings->{$key} ?? $default;
401 $value = apply_filters( "storeengine/get_{$key}_settings", $value, $key );
402
403 return apply_filters( 'storeengine/get_settings', $value, $key );
404 }
405
406 /**
407 * Resolve a per-user notification preference for the email-sending code.
408 *
409 * Stored as user_meta `_storeengine_notif_{key}`. Default is opt-in
410 * (returns true when the meta key has never been set), so existing
411 * customers keep receiving emails after this feature ships.
412 *
413 * Order-status emails are transactional (receipts, shipping updates) and
414 * always return true — the Notifications UI greys that toggle out, but
415 * defence-in-depth here in case anyone POSTs the form directly.
416 *
417 * @param int $user_id WP user id.
418 * @param string $key Notification key, e.g. `marketing`, `vendor_new_order`.
419 */
420 public static function should_send_notification( int $user_id, string $key ): bool {
421 if ( 'order_status' === $key ) {
422 return true;
423 }
424 if ( ! $user_id ) {
425 return true;
426 }
427 $meta = get_user_meta( $user_id, '_storeengine_notif_' . $key, true );
428 // Default-on: empty string means the user has never toggled it.
429 return '' === $meta || '1' === $meta;
430 }
431
432 public static function get_shop_address(): string {
433 global $storeengine_settings;
434
435 return Countries::init()->get_formatted_address( [
436 'address_1' => $storeengine_settings->store_address_1,
437 'address_2' => $storeengine_settings->store_address_2,
438 'city' => $storeengine_settings->store_city,
439 'state' => $storeengine_settings->store_state,
440 'postcode' => $storeengine_settings->store_postcode,
441 'country' => $storeengine_settings->store_country,
442 ] );
443 }
444
445 public static function get_addon_active_status( $addon_name, $is_pro = false ): bool {
446 global $storeengine_addons;
447 if ( $is_pro && ! self::is_active_storeengine_pro() ) {
448 return false;
449 }
450 if ( isset( $storeengine_addons->{$addon_name} ) ) {
451 return (bool) $storeengine_addons->{$addon_name};
452 }
453
454 return false;
455 }
456
457 public static function dif_from_human( $date ) {
458 $now = time();
459 if ( ! is_numeric( $date ) ) {
460 $date = strtotime( $date );
461 }
462 $diff = $now - $date;
463 if ( $diff < 60 ) {
464 /* translators: %s is the number of seconds */
465 return sprintf( __( '%s seconds ago', 'storeengine' ), $diff );
466 }
467 if ( $diff < 3600 ) {
468 /* translators: %s is the number of minutes */
469 return sprintf( __( '%s minutes ago', 'storeengine' ), round( $diff / 60 ) );
470 }
471 if ( $diff < 86400 ) {
472 /* translators: %s is the number of hours */
473 return sprintf( __( '%s hours ago', 'storeengine' ), round( $diff / 3600 ) );
474 }
475 if ( $diff < 604800 ) {
476 /* translators: %s is the number of days */
477 return sprintf( __( '%s days ago', 'storeengine' ), round( $diff / 86400 ) );
478 }
479 if ( $diff < 2419200 ) {
480 /* translators: %s is the number of weeks */
481 return sprintf( __( '%s weeks ago', 'storeengine' ), round( $diff / 604800 ) );
482 }
483 if ( $diff < 29030400 ) {
484 /* translators: %s is the number of months */
485 return sprintf( __( '%s months ago', 'storeengine' ), round( $diff / 2419200 ) );
486 }
487
488 /* translators: %s is the number of years */
489 return sprintf( __( '%s years ago', 'storeengine' ), round( $diff / 29030400 ) );
490 }
491
492 public static function get_tax_rate( string $postcode ): ?float {
493 $tax_rates = [
494 '1000' => 5.5,
495 '2000' => 6.3,
496 '3000' => 7.1,
497 '1206' => 7,
498 '1207' => 7,
499 '7300' => 7,
500 ];
501
502 return $tax_rates[ $postcode ] ?? null;
503 }
504
505 /**
506 * Group definitions for the frontend dashboard sidebar.
507 *
508 * Each group renders a non-clickable header before its first visible
509 * member. Items set `'group' => '<key>'` to opt in; ungrouped items
510 * sort by their own priority and render without a header.
511 *
512 * Group priority controls inter-group ordering; item priority controls
513 * order within a group.
514 *
515 * @return array<string,array{label:string,priority:int}>
516 */
517 public static function get_frontend_dashboard_menu_groups(): array {
518 return apply_filters( 'storeengine/frontend_dashboard_menu_groups', [
519 'orders' => [ 'label' => __( 'My orders', 'storeengine' ), 'priority' => 30 ],
520 'earnings' => [ 'label' => __( 'Earnings', 'storeengine' ), 'priority' => 60 ],
521 'account' => [ 'label' => __( 'Account', 'storeengine' ), 'priority' => 100 ],
522 ] );
523 }
524
525 /**
526 * Sort menu items by (group, item priority) and inject a separator entry
527 * before the first visible member of each labelled group. Headers are
528 * skipped for groups that have no visible items, and for items already
529 * acting as a manual separator (e.g. the vendor section break).
530 *
531 * This is invoked once by the sidebar renderer — other consumers
532 * (rewrite rules, request handler, REST exposure) keep working with
533 * the raw item list.
534 *
535 * @param array $items
536 * @return array
537 */
538 public static function apply_frontend_dashboard_menu_groups( array $items ): array {
539 $groups = self::get_frontend_dashboard_menu_groups();
540
541 $tagged = [];
542 foreach ( $items as $key => $item ) {
543 $group = $item['group'] ?? '';
544 $item_priority = (int) ( $item['priority'] ?? 0 );
545 // Ungrouped items use their own priority as the inter-group sort key
546 // so they slot wherever their priority places them (Dashboard at
547 // the top, Log out at the bottom, vendor separators inline).
548 $effective = isset( $groups[ $group ] )
549 ? (int) $groups[ $group ]['priority']
550 : $item_priority;
551
552 $tagged[ $key ] = [
553 'item' => $item,
554 'group' => $group,
555 'effective' => $effective,
556 'priority' => $item_priority,
557 ];
558 }
559
560 uasort( $tagged, static function ( $a, $b ) {
561 if ( $a['effective'] !== $b['effective'] ) {
562 return $a['effective'] <=> $b['effective'];
563 }
564 return $a['priority'] <=> $b['priority'];
565 } );
566
567 $output = [];
568 $last_emitted_group = null;
569 foreach ( $tagged as $key => $row ) {
570 $group = $row['group'];
571 $item = $row['item'];
572
573 $is_visible = ! empty( $item['public'] ) && empty( $item['hide_from_nav'] );
574
575 // Inject the header only when:
576 // - the item is visible,
577 // - the item isn't itself a manual separator,
578 // - the item belongs to a registered group with a non-empty label,
579 // - and we haven't already emitted that group's header.
580 //
581 // $last_emitted_group tracks the last header we emitted — NOT the
582 // last item's group. Third-party items that omit `group` (or use
583 // an unknown key) flow through unchanged: they appear in the
584 // sidebar at their own priority, get no header, and don't reset
585 // the tracker — so a labelled group split by such an item still
586 // renders its header exactly once.
587 if (
588 $is_visible
589 && empty( $item['is_separator'] )
590 && isset( $groups[ $group ]['label'] )
591 && '' !== $groups[ $group ]['label']
592 && $group !== $last_emitted_group
593 ) {
594 $output[ '__group_' . $group ] = [
595 'label' => $groups[ $group ]['label'],
596 'public' => true,
597 'priority' => $row['effective'] - 1,
598 'is_separator' => true,
599 ];
600 $last_emitted_group = $group;
601 }
602
603 $output[ $key ] = $item;
604 }
605
606 return $output;
607 }
608
609 /**
610 * @return array<array{
611 * label: string,
612 * icon: string,
613 * public: bool,
614 * priority: int|float,
615 * group?: string,
616 * children:array<{label: string, icon: string, public: bool, priority: int|float}>,
617 * }>
618 */
619 public static function get_frontend_dashboard_menu_items(): array {
620 $items = [
621 'index' => [
622 'label' => __( 'Dashboard', 'storeengine' ),
623 'icon' => 'storeengine-icon storeengine-icon--layout',
624 'public' => true,
625 'priority' => - 1,
626 ],
627 'orders' => [
628 'label' => __( 'Orders', 'storeengine' ),
629 'icon' => 'storeengine-icon storeengine-icon--box',
630 'public' => true,
631 'priority' => 30,
632 'group' => 'orders',
633 ],
634 'downloads' => [
635 'label' => __( 'Downloads', 'storeengine' ),
636 'icon' => 'storeengine-icon storeengine-icon--brand-style',
637 'public' => true,
638 'priority' => 70,
639 'group' => 'orders',
640 ],
641 'payment-methods' => [
642 'label' => __( 'Payment methods', 'storeengine' ),
643 'icon' => 'storeengine-icon storeengine-icon--payment',
644 'public' => true,
645 'priority' => 90,
646 'group' => 'account',
647 ],
648 'add-payment-method' => [
649 'label' => __( 'Add Payment method', 'storeengine' ),
650 'public' => false,
651 'priority' => 91,
652 ],
653 'delete-payment-method' => [
654 'label' => __( 'Delete Payment method', 'storeengine' ),
655 'public' => false,
656 'priority' => 92,
657 ],
658 'set-default-payment-method' => [
659 'label' => __( 'Set Default Payment method', 'storeengine' ),
660 'public' => false,
661 'priority' => 93,
662 ],
663 'edit-address' => [
664 'label' => __( 'Addresses', 'storeengine' ),
665 'icon' => 'storeengine-icon storeengine-icon--edit',
666 'public' => true,
667 'priority' => 110,
668 'group' => 'account',
669 'children' => [
670 'billing' => [
671 'label' => __( 'Edit Billing Address', 'storeengine' ),
672 'public' => false,
673 'priority' => 10,
674 ],
675 'shipping' => [
676 'label' => __( 'Edit Shipping Address', 'storeengine' ),
677 'public' => false,
678 'priority' => 20,
679 ],
680 ],
681 ],
682 'edit-account' => [
683 'label' => __( 'Account', 'storeengine' ),
684 'icon' => 'storeengine-icon storeengine-icon--build',
685 'public' => true,
686 'priority' => 130,
687 'group' => 'account',
688 'children' => [
689 // `account` is the default sub-tab when no sub_page is
690 // requested — same content as the legacy edit-account page
691 // (email, name, password). Notifications + Privacy are new.
692 'account' => [
693 'label' => __( 'Account', 'storeengine' ),
694 'public' => true,
695 'priority' => 10,
696 ],
697 'notifications' => [
698 'label' => __( 'Notifications', 'storeengine' ),
699 'public' => true,
700 'priority' => 20,
701 ],
702 'privacy' => [
703 'label' => __( 'Privacy', 'storeengine' ),
704 'public' => true,
705 'priority' => 30,
706 ],
707 ],
708 ],
709 'customer-logout' => [
710 'label' => __( 'Log out', 'storeengine' ),
711 'icon' => 'storeengine-icon storeengine-icon--logout',
712 'public' => true,
713 'priority' => 999,
714 ],
715 'forgot-password' => [
716 // public=true so the FrontendRequestHandler doesn't gate the
717 // page on login (logged-out users need to reach it), and so
718 // PermalinkRewrite generates the /dashboard/forgot-password/
719 // rewrite rule automatically.
720 //
721 // hide_from_nav keeps it out of the sidebar — it's a
722 // contextual entry point reached from the login form's "Lost
723 // your password?" link and from the reset email, not a
724 // destination customers navigate to.
725 //
726 // guest_accessible tells the [storeengine_dashboard] shortcode
727 // to route to this endpoint's content for logged-out visitors
728 // instead of falling back to the login form, which is what it
729 // does for every other endpoint.
730 'label' => __( 'Reset password', 'storeengine' ),
731 'public' => true,
732 'hide_from_nav' => true,
733 'guest_accessible' => true,
734 'priority' => 1000,
735 ],
736 'register' => [
737 // Same flag combination as forgot-password — public so the
738 // rewrite rule generates, hidden from nav (entry point is the
739 // login form's "Register" link), guest_accessible so the
740 // dashboard shortcode renders the form for logged-out visitors
741 // instead of redirecting them through the login flow.
742 'label' => __( 'Register', 'storeengine' ),
743 'public' => true,
744 'hide_from_nav' => true,
745 'guest_accessible' => true,
746 'priority' => 1001,
747 ],
748 ];
749
750 $support_payment_methods = false;
751 foreach ( self::get_payment_gateways()->get_available_payment_gateways() as $gateway ) {
752 if ( $gateway->supports( 'add_payment_method' ) || $gateway->supports( 'tokenization' ) ) {
753 $support_payment_methods = true;
754 break;
755 }
756 }
757
758 if ( ! $support_payment_methods ) {
759 unset( $items['payment-methods'] );
760 }
761
762 return apply_filters( 'storeengine/frontend_dashboard_menu_items', $items );
763 }
764
765 public static function get_frontend_dashboard_page_title( $path, $sub_path = '' ) {
766 $menu = self::get_frontend_dashboard_menu_items();
767
768 if ( empty( $menu[ $path ] ) ) {
769 return '';
770 }
771
772 if ( $sub_path ) {
773 if ( ! empty( $menu[ $path ]['children'][ $sub_path ] ) ) {
774 return $menu[ $path ]['children'][ $sub_path ]['label'];
775 }
776 }
777
778 return $menu[ $path ]['label'];
779 }
780
781 public static function round( $val, int $precision = 0, int $mode = PHP_ROUND_HALF_UP ): float {
782 if ( ! is_numeric( $val ) ) {
783 $val = floatval( $val );
784 }
785
786 return round( $val, $precision, $mode );
787 }
788
789 public static function meta_parser( $meta ) {
790 return array_map( function ( $i ) {
791 return $i[0];
792 }, $meta );
793 }
794
795 public static function get_cart_hash(): ?string {
796 $cart_hash = self::get_cart_hash_from_cookie();
797 if ( $cart_hash ) {
798 return $cart_hash;
799 }
800
801 return Cart::get_cart_hash_by_user_id( get_current_user_id() );
802 }
803
804 public static function get_cart_hash_from_cookie(): string {
805 return isset( $_COOKIE['storeengine_cart_hash'] ) ? sanitize_text_field( wp_unslash( $_COOKIE['storeengine_cart_hash'] ) ) : '';
806 }
807
808 /**
809 * Set cart has in cookie.
810 *
811 * @param string $cart_hash
812 *
813 * @return void
814 * @deprecated
815 */
816 public static function set_cart_hash_in_cookie( string $cart_hash ): void {
817 setcookie( 'storeengine_cart_hash', $cart_hash, [
818 'expires' => time() + YEAR_IN_SECONDS,
819 'path' => defined( 'COOKIEPATH' ) ? COOKIEPATH : '/',
820 'domain' => defined( 'COOKIE_DOMAIN' ) ? COOKIE_DOMAIN : '',
821 'secure' => is_ssl(),
822 'httponly' => true,
823 'samesite' => 'Strict',
824 ] );
825 }
826
827 /**
828 * Unset Cart hash cookie.
829 *
830 * @return void
831 * @deprecated
832 */
833 public static function unset_cart_hash_in_cookie(): void {
834 if ( headers_sent() ) {
835 return;
836 }
837
838 // @TODO delete cache on hash changes.
839 // wp_cache_delete 'order:draft:' . Helper::get_cart_hash_from_cookie(), 'storeengine_orders' ;
840 header( 'Set-Cookie: storeengine_cart_hash=; Path=/; HttpOnly; Max-Age=-1', false );
841 setcookie( 'storeengine_cart_hash', '', [
842 'expires' => - 1,
843 'path' => defined( 'COOKIEPATH' ) ? COOKIEPATH : '/',
844 'domain' => defined( 'COOKIE_DOMAIN' ) ? COOKIE_DOMAIN : '',
845 'secure' => is_ssl(),
846 'httponly' => true,
847 'samesite' => 'Strict',
848 ] );
849 }
850
851 public static function sanitize_referer_url( string $referer_url ): string {
852 $parse_url = wp_parse_url( $referer_url );
853
854 if ( isset( $parse_url['query'] ) ) {
855 // Parse query parameters
856 parse_str( $parse_url['query'], $query_params );
857 if ( ! empty( $query_params['redirect_to'] ) ) {
858 $referer_url = $query_params['redirect_to'];
859 }
860 if ( ! empty( $query_params['redirect_url'] ) ) {
861 $referer_url = $query_params['redirect_url'];
862 }
863 }
864
865 // Sanitize the input URL
866 $referer_url = esc_url_raw( $referer_url );
867 if ( filter_var( $referer_url, FILTER_VALIDATE_URL ) !== false && wp_http_validate_url( $referer_url ) && strpos( $referer_url, home_url() ) === 0 ) {
868 return esc_url( $referer_url );
869 } elseif ( ! empty( $parse_url['path'] ) ) {
870 return esc_url( home_url( sanitize_text_field( $parse_url['path'] ) ) );
871 }
872
873 return esc_url( home_url( '/' ) );
874 }
875
876 public static function asort_by_locale( &$data, $locale = '' ) {
877 // Use Collator if PHP Internationalization Functions (php-intl) is available.
878 if ( class_exists( 'Collator' ) ) {
879 try {
880 $locale = $locale ? $locale : get_locale();
881 $collator = new \Collator( $locale );
882 $collator->asort( $data, \Collator::SORT_STRING );
883
884 return $data;
885 } catch ( \Throwable $e ) {
886 Helper::log_error(
887 sprintf(
888 'An unexpected error occurred while trying to use PHP Intl Collator class, it may be caused by an incorrect installation of PHP Intl and ICU, and could be fixed by reinstalling PHP Intl, see more details about PHP Intl installation: %1$s. Error message: %2$s',
889 'https://www.php.net/manual/en/intl.installation.php',
890 $e->getMessage()
891 )
892 );
893 }
894 }
895
896 // Keep a reference to original data before removing accent marks
897 // as strcmp works better without accent marks and add the value back
898 // to the sorted array from this reference.
899 $raw_data = $data;
900
901 array_walk( $data, function ( &$value ) {
902 $value = remove_accents( html_entity_decode( $value ) );
903 } );
904
905 uasort( $data, 'strcmp' );
906
907 foreach ( $data as $key => $val ) {
908 $data[ $key ] = $raw_data[ $key ];
909 }
910
911 return $data;
912 }
913
914 public static function get_all_roles(): array {
915 global $wp_roles;
916
917 if ( ! class_exists( '\WP_Roles' ) ) {
918 return [];
919 }
920
921 if ( ! isset( $wp_roles ) ) {
922 $wp_roles = new \WP_Roles(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
923 }
924
925 $roles_array = [];
926
927 foreach ( $wp_roles->roles as $role_id => $role ) {
928 $roles_array[] = [
929 'role_id' => $role_id,
930 'role_name' => $role['name'],
931 ];
932 }
933
934 return $roles_array;
935 }
936
937 public static function get_sample_permalink_args( $id, $new_title = null, $new_slug = null ) {
938 $post = get_post( $id );
939 if ( ! $post ) {
940 return '';
941 }
942
943 list( $permalink, $post_name ) = get_sample_permalink( $post->ID, $new_title, $new_slug );
944 $view_link = false;
945
946 if ( current_user_can( 'read_post', $post->ID ) ) {
947 if ( 'draft' === $post->post_status || empty( $post->post_name ) ) {
948 $view_link = get_preview_post_link( $post );
949 } elseif ( 'publish' === $post->post_status || 'storeengine_product' === $post->post_type ) {
950 $view_link = get_permalink( $post );
951 } else {
952 $view_link = str_replace( [ '%pagename%', '%postname%' ], $post->post_name, $permalink );
953 }
954 }
955
956 return [
957 'view_link' => $view_link ? esc_url( $view_link ) : null,
958 'editable_postname' => $post_name,
959 'display_link' => rtrim( str_replace( '%pagename%', $post_name, $permalink ) ),
960 'post_name' => $post_name,
961 ];
962 }
963
964 /**
965 * Get Page by title.
966 *
967 * @param string $page_title
968 * @param string $post_type
969 *
970 * @return WP_Post|null
971 */
972 public static function get_page_by_title( string $page_title, string $post_type = 'page' ): ?WP_Post {
973 global $wpdb;
974
975 $page = wp_cache_get( 'storeengine:get_page_by_title:' . sanitize_title( $page_title ), $post_type );
976
977 if ( false === $page ) {
978 $page = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
979 $wpdb->prepare(
980 "SELECT ID FROM $wpdb->posts WHERE post_title = %s AND post_type = %s;",
981 $page_title,
982 $post_type
983 )
984 );
985
986 wp_cache_set( 'storeengine:get_page_by_title:' . sanitize_title( $page_title ), $page, $post_type );
987 }
988
989 if ( $page ) {
990 $page = get_post( $page, OBJECT );
991
992 if ( ! $page ) {
993 wp_cache_delete( 'storeengine:get_page_by_title:' . sanitize_title( $page_title ), $post_type );
994 }
995
996 return $page;
997 }
998
999 return null;
1000 }
1001
1002 /**
1003 * @return string
1004 *
1005 * @deprecated 1.5.6
1006 * @see Geolocation::get_user_ip()
1007 */
1008 public static function get_user_ip(): string {
1009 return Geolocation::get_user_ip();
1010 }
1011
1012 /**
1013 * Get user agent string.
1014 *
1015 * @return string
1016 */
1017 public static function get_user_agent(): string {
1018 return isset( $_SERVER['HTTP_USER_AGENT'] ) ? Formatting::clean( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized inside clean method.
1019 }
1020
1021 public static function is_bot(): bool {
1022 $ua = self::get_user_agent();
1023
1024 // Common bots by user agent.
1025 return apply_filters(
1026 'storeengine/is_bot',
1027 stripos( $ua, 'bot' ) !== false || stripos( $ua, 'spider' ) !== false || stripos( $ua, 'crawl' ) !== false,
1028 $ua
1029 );
1030 }
1031
1032 public static function get_email_template_name( string $template_name, ?string $template_sub_name = null ): string {
1033 if ( $template_sub_name ) {
1034 return str_replace( '_', '-', $template_name ) . '-' . $template_sub_name . '.php';
1035 }
1036
1037 return str_replace( '_', '-', $template_name ) . '.php';
1038 }
1039
1040 /**
1041 * Schedule rewrite rule flushing on next reload.
1042 *
1043 * @return void
1044 * @since 0.0.4
1045 */
1046 public static function flush_rewire_rules() {
1047 update_option( 'storeengine_required_rewrite_flush', 'yes' );
1048 }
1049
1050 /**
1051 * Checks whether the content passed contains a specific short code.
1052 *
1053 * @param string $tag Shortcode tag to check.
1054 *
1055 * @return bool
1056 */
1057 public static function post_content_has_shortcode( string $tag = '' ): bool {
1058 global $post;
1059
1060 return is_singular() && is_a( $post, 'WP_Post' ) && has_shortcode( $post->post_content, $tag );
1061 }
1062
1063 public static function is_storeengine(): bool {
1064 return apply_filters( 'storeengine/is_storeengine', self::is_shop() || self::is_product_taxonomy() || self::is_product() );
1065 }
1066
1067 public static function is_shop(): bool {
1068 return ( is_post_type_archive( self::PRODUCT_POST_TYPE ) || is_page( self::get_settings( 'shop_page' ) ) );
1069 }
1070
1071 public static function is_product(): bool {
1072 return is_singular( [ self::PRODUCT_POST_TYPE ] );
1073 }
1074
1075 public static function is_product_taxonomy(): bool {
1076 return is_tax( get_object_taxonomies( self::PRODUCT_POST_TYPE ) );
1077 }
1078
1079 public static function is_product_category( $term = '' ): bool {
1080 return is_tax( self::PRODUCT_CATEGORY_TAXONOMY, $term );
1081 }
1082
1083 public static function is_product_tag( $term = '' ): bool {
1084 return is_tax( self::PRODUCT_TAG_TAXONOMY, $term );
1085 }
1086
1087 public static function is_cart(): bool {
1088 $page_id = self::get_settings( 'cart_page' );
1089
1090 return ( $page_id && is_page( $page_id ) ) || defined( 'STOREENGINE_CART' ) || self::post_content_has_shortcode( 'storeengine_cart' );
1091 }
1092
1093 public static function is_checkout(): bool {
1094 $page_id = self::get_settings( 'checkout_page' );
1095
1096 return ( $page_id && is_page( $page_id ) ) || self::post_content_has_shortcode( 'storeengine_checkout' ) || apply_filters( 'storeengine_is_checkout', false ) || defined( 'STOREENGINE_CART' ) || defined( 'STOREENGINE_CHECKOUT' );
1097 }
1098
1099 public static function is_thank_you(): bool {
1100 $page_id = self::get_settings( 'thankyou_page' );
1101
1102 return ( $page_id && is_page( $page_id ) ) || apply_filters( 'storeengine_is_thankyou', false );
1103 }
1104
1105 public static function is_dashboard(): bool {
1106 $page_id = self::get_settings( 'dashboard_page' );
1107
1108 return ( $page_id && is_page( $page_id ) ) || self::post_content_has_shortcode( 'storeengine_dashboard' ) || apply_filters( 'storeengine_is_dashboard_page', false );
1109 }
1110
1111 /**
1112 * Check if current page is dashboard index or endpoint page.
1113 *
1114 * @return bool|int Returns zero (0) if called outside of dashboard/endpoint page. Returns (bool) true on
1115 * dashboard index or (bool) false on endpoint page.
1116 */
1117 public static function is_dashboard_index() {
1118 if ( null === self::$dashboard_index ) {
1119 self::$dashboard_index = self::is_dashboard() && null === self::get_current_dashboard_endpoint();
1120 }
1121
1122 return self::$dashboard_index;
1123 }
1124
1125 public static function is_account_page(): bool {
1126 return self::is_dashboard();
1127 }
1128
1129 public static function get_all_product_category_lists() {
1130 $categories = get_terms(
1131 array(
1132 'taxonomy' => 'storeengine_product_category',
1133 'hide_empty' => true,
1134 )
1135 );
1136
1137 return self::prepare_category_results( $categories );
1138 }
1139
1140 public static function prepare_category_results( $terms, $parent_id = 0 ) {
1141 $category = array();
1142 foreach ( $terms as $term ) {
1143 if ( $term->parent === $parent_id ) {
1144 $term->children = self::prepare_category_results( $terms, $term->term_id );
1145 $category[] = $term;
1146 }
1147 }
1148
1149 return $category;
1150 }
1151
1152 public static function is_endpoint( $endpoint = null ): bool {
1153 global $wp_query;
1154
1155 if ( empty( $wp_query->query['storeengine_dashboard_page'] ) ) {
1156 return false;
1157 }
1158
1159 if ( $endpoint ) {
1160 return $wp_query->query['storeengine_dashboard_page'] === $endpoint;
1161 }
1162
1163 return true;
1164 }
1165
1166 public static function is_add_payment_method_page(): bool {
1167 return self::is_dashboard() && self::is_endpoint( 'add-payment-method' );
1168 }
1169
1170 public static function is_payment_method_list_page(): bool {
1171 return self::is_dashboard() && self::is_endpoint( 'payment-methods' );
1172 }
1173
1174 public static function is_edit_address_page(): bool {
1175 return self::is_dashboard() && ( self::is_endpoint( 'edit-address' ) && ! empty( get_query_var( 'storeengine_dashboard_sub_page' ) ) );
1176 }
1177
1178 /**
1179 * Rest api permission check.
1180 *
1181 * @param string $capability
1182 * @param string|null $response
1183 *
1184 * @return WP_Error|bool
1185 */
1186 public static function check_rest_user_cap( string $capability, ?string $response = null ) {
1187 $permission = true;
1188 if ( ! is_user_logged_in() || ! current_user_can( $capability ) ) {
1189 $permission = new WP_Error(
1190 'storeengine_rest_forbidden_context',
1191 $response ? esc_html( $response ) : esc_html__( 'Sorry, insufficient permission.', 'storeengine' ),
1192 [ 'status' => rest_authorization_required_code() ]
1193 );
1194 }
1195
1196 return apply_filters( 'storeengine/rest_user_capability', $permission, $capability );
1197 }
1198
1199 public static function prepare_product_search_query_args( $data ) {
1200 $defaults = array(
1201 'search' => '',
1202 'category' => [],
1203 'tags' => [],
1204 'paged' => 1,
1205 'posts_per_page' => 12,
1206 );
1207 $data = wp_parse_args( $data, $defaults );
1208
1209 // base
1210 $args = array(
1211 //'post_type' => apply_filters( 'storeengine/get_product_archive_post_types', array( 'storeengine_product' ) ),
1212 'post_type' => 'storeengine_product', // Archive query compatibility. if WP_Query post-type is array then it won't marked as archive query (required for ajax filter on archive page).
1213 'post_status' => 'publish',
1214 'posts_per_page' => $data['posts_per_page'],
1215 'paged' => $data['paged'],
1216 );
1217
1218 // taxonomy
1219 $tax_query = array();
1220 if ( count( $data['category'] ) > 0 ) {
1221 $tax_query[] = array(
1222 'taxonomy' => 'storeengine_product_category',
1223 'field' => 'slug',
1224 'terms' => $data['category'],
1225 );
1226 }
1227 if ( count( $data['tags'] ) > 0 ) {
1228 $tax_query[] = array(
1229 'taxonomy' => 'storeengine_product_tag',
1230 'field' => 'slug',
1231 'terms' => $data['tags'],
1232 );
1233 }
1234 if ( count( $tax_query ) > 0 ) {
1235 $tax_query['relation'] = 'AND';
1236 $args['tax_query'] = $tax_query; // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
1237 }
1238
1239 // search
1240 if ( ! empty( $data['search'] ) ) {
1241 $args['s'] = $data['search'];
1242 }
1243
1244 // order by
1245 if ( isset( $data['orderby'] ) ) {
1246 switch ( $data['orderby'] ) {
1247 case 'name':
1248 case 'title':
1249 $args['orderby'] = 'post_title';
1250 $args['order'] = 'asc';
1251 break;
1252 case 'date':
1253 $args['orderby'] = 'publish_date';
1254 $args['order'] = 'desc';
1255 break;
1256 case 'modified':
1257 $args['orderby'] = 'modified';
1258 $args['order'] = 'desc';
1259 break;
1260 case 'menu_order':
1261 $args['orderby'] = 'menu_order';
1262 $args['order'] = 'desc';
1263 break;
1264 default:
1265 $args['orderby'] = 'ID';
1266 $args['order'] = 'desc';
1267 }//end switch
1268 }//end if
1269 return apply_filters( 'storeengine/get_product_archive_search_query_args', $args, $data );
1270 }
1271
1272 public static function get_responsive_column( $columns ): string {
1273 if ( is_array( $columns ) ) {
1274 $device = [
1275 'desktop' => 'lg',
1276 'tablet' => 'md',
1277 'mobile' => 'sm',
1278 ];
1279 $classes = '';
1280 foreach ( $columns as $mode => $column ) {
1281 if ( $column ) {
1282 $classes .= ' storeengine-col-' . $device[ $mode ] . '-' . ceil( 12 / $column );
1283 }
1284 }
1285
1286 return ltrim( $classes );
1287 }
1288
1289 return '';
1290 }
1291
1292 public static function get_permalink_structure() {
1293 $saved_permalinks = (array) get_option( 'storeengine_permalinks', array() );
1294 $permalinks = wp_parse_args(
1295 array_filter( $saved_permalinks ),
1296 array(
1297 'product_base' => _x( 'product', 'slug', 'storeengine' ),
1298 'category_base' => _x( 'product-category', 'slug', 'storeengine' ),
1299 'tag_base' => _x( 'product-tag', 'slug', 'storeengine' ),
1300 'use_verbose_page_rules' => false,
1301 )
1302 );
1303
1304 if ( $saved_permalinks !== $permalinks ) {
1305 update_option( 'storeengine_permalinks', $permalinks );
1306 }
1307
1308 $permalinks['product_rewrite_slug'] = untrailingslashit( $permalinks['product_base'] );
1309 $permalinks['category_rewrite_slug'] = untrailingslashit( $permalinks['category_base'] );
1310 $permalinks['tag_rewrite_slug'] = untrailingslashit( $permalinks['tag_base'] );
1311
1312 return $permalinks;
1313 }
1314
1315 /**
1316 * Switch plugin to site language.
1317 *
1318 * @return void
1319 */
1320 public static function switch_to_site_locale() {
1321 self::switch_to_locale( get_locale() );
1322 }
1323
1324 /**
1325 * Switch plugin to site language.
1326 *
1327 * @return void
1328 */
1329 public static function switch_to_locale( string $locale = 'en_US' ) {
1330 global $wp_locale_switcher;
1331
1332 if ( function_exists( 'switch_to_locale' ) && isset( $wp_locale_switcher ) ) {
1333 switch_to_locale( $locale );
1334
1335 // Filter on plugin_locale so load_plugin_textdomain loads the correct locale.
1336 add_filter( 'plugin_locale', 'get_locale' );
1337 }
1338 }
1339
1340 /**
1341 * Switch plugin language to original.
1342 *
1343 * @return void
1344 */
1345 public static function restore_locale() {
1346 global $wp_locale_switcher;
1347
1348 if ( function_exists( 'restore_previous_locale' ) && isset( $wp_locale_switcher ) ) {
1349 restore_previous_locale();
1350
1351 // Remove filter.
1352 remove_filter( 'plugin_locale', 'get_locale' );
1353 }
1354 }
1355
1356 /**
1357 * Simple check for validating a URL, it must start with http:// or https://.
1358 * and pass FILTER_VALIDATE_URL validation.
1359 *
1360 * @param string $url to check.
1361 *
1362 * @return bool
1363 */
1364 public static function is_valid_url( string $url ): bool {
1365
1366 // Must start with http:// or https://.
1367 /** @noinspection HttpUrlsUsage */
1368 if ( 0 !== strpos( $url, 'http://' ) && 0 !== strpos( $url, 'https://' ) ) {
1369 return false;
1370 }
1371
1372 // Must pass validation.
1373 if ( ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
1374 return false;
1375 }
1376
1377 return true;
1378 }
1379
1380 /**
1381 * Alias for is_valid_url()
1382 *
1383 * @param string $url
1384 *
1385 * @return bool
1386 * @see is_valid_url()
1387 */
1388 public static function is_url( string $url ): bool {
1389 return self::is_valid_url( $url );
1390 }
1391
1392 public static function is_valid_site_url( string $url ): bool {
1393 return str_starts_with( $url, get_option( 'siteurl' ) );
1394 }
1395
1396 public static function get_reveiw_survey_form_radios( $slug ) {
1397 $html = '';
1398 for ( $counter = 1; $counter <= 5; $counter ++ ) {
1399 $html .= sprintf(
1400 "<td><input type='radio' name='%s-rating' value='%s' class='storeengine-radio' /></td>",
1401 $slug,
1402 $counter
1403 );
1404 }
1405
1406 return $html;
1407 }
1408
1409 public static function get_date_format() {
1410 $date_format = get_option( 'date_format' );
1411 if ( empty( $date_format ) ) {
1412 // Return default date format if the option is empty.
1413 $date_format = 'F j, Y';
1414 }
1415
1416 return apply_filters( 'storeengine/date_format', $date_format );
1417 }
1418
1419 public static function single_star_rating_generator( $current_rating = 0.00 ) {
1420 $output = '<span class="storeengine-group-star">';
1421 if ( 5 < $current_rating && 0 > $current_rating ) {
1422 $output .= '<i class="storeengine-icon storeengine-icon--star-fill"></i>';
1423 } elseif ( 0 === $current_rating ) {
1424 $output .= '<i class="storeengine-icon storeengine-icon--star-fill"></i>';
1425 } else {
1426 $output .= '<i class="storeengine-icon storeengine-icon--star-fill"></i>';
1427 }
1428 $output .= '</span>';
1429
1430 return $output;
1431 }
1432
1433 public static function star_rating_generator( $current_rating = 0.00 ) {
1434 $output = '<span class="storeengine-group-star">';
1435
1436 for ( $i = 1; $i <= 5; $i ++ ) {
1437 $intRating = (int) $current_rating;
1438
1439 if ( $intRating >= $i ) {
1440 $output .= '<i class="storeengine-icon storeengine-icon--star-fill" data-rating-value="' . $i . '"></i>';
1441 } else {
1442 if ( ( $current_rating - $i ) === - 0.5 ) {
1443 $output .= '<i class="storeengine-icon storeengine-icon--star-half" data-rating-value="' . $i . '"></i>';
1444 } else {
1445 $output .= '<i class="storeengine-icon storeengine-icon--star-line" data-rating-value="' . $i . '"></i>';
1446 }
1447 }
1448 }
1449
1450 $output .= '</span>';
1451
1452 return $output;
1453 }
1454
1455 /**
1456 * @param $product_id
1457 * @param $user_id
1458 *
1459 * @return string|null
1460 *
1461 * @see wc_customer_bought_product()
1462 */
1463 public static function is_purchase_the_product( $product_id, $user_id = 0 ): ?string {
1464 global $wpdb;
1465
1466 if ( ! $user_id ) {
1467 $user_id = get_current_user_id();
1468 }
1469
1470 // @TODO cache user's purchase story for 30 days.
1471 // @see wc_customer_bought_product fn for details.
1472
1473 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1474 return $wpdb->get_var(
1475 $wpdb->prepare(
1476 "SELECT o.id
1477 FROM {$wpdb->prefix}storeengine_orders o
1478 JOIN {$wpdb->prefix}storeengine_order_product_lookup op ON o.id = op.order_id
1479 WHERE o.customer_id = %d
1480 AND op.product_id = %d
1481 AND o.status = 'completed'
1482 LIMIT 1;",
1483 $user_id,
1484 $product_id
1485 )
1486 );
1487 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1488 }
1489
1490 public static function is_purchase_the_membership( $product_id, $price_id, $customer_id = 0, $order_status = Constants::ORDER_STATUS_COMPLETED ) {
1491 if ( ! is_user_logged_in() ) {
1492 return false;
1493 }
1494
1495 global $wpdb;
1496
1497 if ( ! $customer_id ) {
1498 $customer_id = get_current_user_id();
1499 }
1500
1501 $user_meta = get_user_meta( $customer_id, '_storeengine_memberships', true );
1502
1503 if ( is_array( $user_meta ) ) {
1504 $result = false;
1505 foreach ( $user_meta as $u_meta ) {
1506 if ( $price_id === $u_meta['price_id'] && Constants::ORDER_STATUS_COMPLETED === $u_meta['order_status'] ) {
1507 $result = true;
1508 break;
1509 }
1510 }
1511
1512 return $result;
1513 }
1514
1515 $user_meta = [];
1516 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- query result cached in user meta
1517 $count = (int) $wpdb->get_var( $wpdb->prepare(
1518 "SELECT COUNT(*) FROM {$wpdb->prefix}storeengine_order_product_lookup op
1519 JOIN {$wpdb->prefix}storeengine_orders o ON op.order_id = o.id
1520 WHERE op.product_id = %d
1521 AND op.price_id = %d
1522 AND o.customer_id = %d
1523 AND o.status = %s",
1524 $product_id,
1525 $price_id,
1526 $customer_id,
1527 $order_status
1528 ) );
1529 // phpcs:enable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- query result cached in user meta
1530
1531 if ( $count ) {
1532 $user_meta[] = compact( 'customer_id', 'price_id', 'order_status' );
1533 }
1534 update_user_meta( $customer_id, '_storeengine_memberships', $user_meta );
1535
1536 return $count;
1537 }
1538
1539 /**
1540 * Used to sort shipping zone methods with uasort.
1541 *
1542 * @param ShippingMethod|\stdClass $a First shipping zone method to compare.
1543 * @param ShippingMethod|\stdClass $b Second shipping zone method to compare.
1544 *
1545 * @return int
1546 */
1547 public static function shipping_zone_method_order_uasort_comparison( $a, $b ): int {
1548 return self::uasort_comparison( $a->method_order, $b->method_order );
1549 }
1550
1551 /**
1552 * User to sort checkout fields based on priority with uasort.
1553 *
1554 * @param array $a First field to compare.
1555 * @param array $b Second field to compare.
1556 *
1557 * @return int
1558 */
1559 public static function checkout_fields_uasort_comparison( array $a, array $b ): int {
1560 /*
1561 * We are not guaranteed to get a priority
1562 * setting. So don't compare if they don't
1563 * exist.
1564 */
1565 if ( ! isset( $a['priority'], $b['priority'] ) ) {
1566 return 0;
1567 }
1568
1569 return self::uasort_comparison( $a['priority'], $b['priority'] );
1570 }
1571
1572 /**
1573 * User to sort two values with uasort.
1574 *
1575 * @param int $a First value to compare.
1576 * @param int $b Second value to compare.
1577 *
1578 * @return int
1579 */
1580 public static function uasort_comparison( int $a, int $b ): int {
1581 if ( $a === $b ) {
1582 return 0;
1583 }
1584
1585 return ( $a < $b ) ? - 1 : 1;
1586 }
1587
1588 /**
1589 * Merge two arrays.
1590 *
1591 * @param array $a1 First array to merge.
1592 * @param array $a2 Second array to merge.
1593 *
1594 * @return array
1595 */
1596 public static function array_overlay( array $a1, array $a2 ): array {
1597 foreach ( $a1 as $k => $v ) {
1598 if ( ! array_key_exists( $k, $a2 ) ) {
1599 continue;
1600 }
1601 if ( is_array( $v ) && is_array( $a2[ $k ] ) ) {
1602 $a1[ $k ] = self::array_overlay( $v, $a2[ $k ] );
1603 } else {
1604 $a1[ $k ] = $a2[ $k ];
1605 }
1606 }
1607
1608 return $a1;
1609 }
1610
1611 /**
1612 * Set a cookie - wrapper for setcookie using WP constants.
1613 *
1614 * @param string $name Name of the cookie being set.
1615 * @param string|int|float $value Value of the cookie.
1616 * @param integer $expire Expiry of the cookie.
1617 * @param bool $secure Whether the cookie should be served only over https.
1618 * @param bool $httponly Whether the cookie is only accessible over HTTP, not scripting languages like JavaScript.
1619 */
1620 public static function setcookie( string $name, $value, int $expire = 0, bool $secure = false, bool $httponly = false ): void {
1621 /**
1622 * Controls whether the cookie should be set via wc_setcookie().
1623 *
1624 * @param bool $set_cookie_enabled If wc_setcookie() should set the cookie.
1625 * @param string $name Cookie name.
1626 * @param string $value Cookie value.
1627 * @param integer $expire When the cookie should expire.
1628 * @param bool $secure If the cookie should only be served over HTTPS.
1629 */
1630 if ( ! apply_filters( 'storeengine/set_cookie_enabled', true, $name, $value, $expire, $secure ) ) {
1631 return;
1632 }
1633
1634 if ( ! headers_sent() ) {
1635 /**
1636 * Controls the options to be specified when setting the cookie.
1637 *
1638 * @see https://www.php.net/manual/en/function.setcookie.php
1639 *
1640 * @param array $cookie_options Cookie options.
1641 * @param string $name Cookie name.
1642 * @param string $value Cookie value.
1643 */
1644 $options = apply_filters(
1645 'storeengine/set_cookie_options',
1646 [
1647 'expires' => $expire,
1648 'secure' => $secure,
1649 'path' => COOKIEPATH ? COOKIEPATH : '/',
1650 'domain' => COOKIE_DOMAIN,
1651 /**
1652 * Controls whether the cookie should only be accessible via the HTTP protocol, or if it should also be
1653 * accessible to Javascript.
1654 *
1655 * @see https://www.php.net/manual/en/function.setcookie.php
1656 *
1657 * @param bool $httponly If the cookie should only be accessible via the HTTP protocol.
1658 * @param string $name Cookie name.
1659 * @param string $value Cookie value.
1660 * @param int $expire When the cookie should expire.
1661 * @param bool $secure If the cookie should only be served over HTTPS.
1662 */
1663 'httponly' => apply_filters( 'storeengine/cookie_httponly', $httponly, $name, $value, $expire, $secure ),
1664 ],
1665 $name,
1666 $value
1667 );
1668
1669 setcookie( $name, $value, $options );
1670 } elseif ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
1671 headers_sent( $file, $line );
1672 trigger_error( esc_html( "{$name} cookie cannot be set - headers already sent by {$file} on line {$line}" ), E_USER_NOTICE ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error, WordPress.Security.EscapeOutput.OutputNotEscaped
1673 }
1674 }
1675
1676 /**
1677 * What type of request is this?
1678 *
1679 * @param string $type admin, ajax, cron or frontend.
1680 *
1681 * @return bool
1682 */
1683 public static function is_request( string $type ): bool {
1684 switch ( $type ) {
1685 case 'ref-admin':
1686 return self::is_admin_request();
1687 case 'ref-frontend':
1688 return ! self::is_admin_request();
1689 case 'admin':
1690 return is_admin();
1691 case 'ajax':
1692 // self::is_request( 'admin' ) is always true here.
1693 // should be paired with ref-admin check to diff between admin/frontend ajax.
1694 return defined( 'DOING_AJAX' );
1695 case 'cron':
1696 return defined( 'DOING_CRON' );
1697 case 'frontend':
1698 return ( ! is_admin() || defined( 'DOING_AJAX' ) ) && ! defined( 'DOING_CRON' ) && ! self::is_rest_api_request();
1699 case 'rest':
1700 case 'restapi':
1701 return self::is_rest_api_request();
1702 default:
1703 return false;
1704 }
1705 }
1706
1707 public static function is_admin_request(): bool {
1708 if( defined( 'STOREENGINE_DOING_ADMIN_REFERER_REQUEST' ) ) {
1709 return STOREENGINE_DOING_ADMIN_REFERER_REQUEST;
1710 }
1711 if ( ! function_exists( 'wp_validate_redirect' ) ) {
1712 require_once ABSPATH . WPINC . '/pluggable.php';
1713 }
1714
1715 $is_ref_admin = str_starts_with( strtolower( wp_get_referer() ), strtolower( admin_url() ) );
1716
1717 define( 'STOREENGINE_DOING_ADMIN_REFERER_REQUEST', $is_ref_admin );
1718
1719 return $is_ref_admin;
1720 }
1721
1722 /**
1723 * Returns true if the request is a non-legacy REST API request.
1724 *
1725 * Legacy REST requests should still run some extra code for backwards compatibility.
1726 *
1727 * @todo: replace this function once core WP function is available: https://core.trac.wordpress.org/ticket/42061.
1728 *
1729 * @return bool
1730 */
1731 public static function is_rest_api_request(): bool {
1732 if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
1733 return true;
1734 }
1735
1736 if ( empty( $_SERVER['REQUEST_URI'] ) ) {
1737 return false;
1738 }
1739
1740 $rest_prefix = trailingslashit( rest_get_url_prefix() );
1741 $is_rest_api_request = ( false !== strpos( $_SERVER['REQUEST_URI'], $rest_prefix ) ); // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1742
1743 /**
1744 * Whether this is a REST API request.
1745 */
1746 return apply_filters( 'storeengine/is_rest_api_request', $is_rest_api_request );
1747 }
1748
1749 /**
1750 * Wrapper for set_time_limit to see if it is enabled.
1751 *
1752 * @param int $limit Time limit.
1753 */
1754 public static function set_time_limit( int $limit = 0 ) {
1755 if ( function_exists( 'set_time_limit' ) && false === strpos( ini_get( 'disable_functions' ), 'set_time_limit' ) && ! ini_get( 'safe_mode' ) ) { // phpcs:ignore PHPCompatibility.IniDirectives.RemovedIniDirectives.safe_modeDeprecatedRemoved
1756 @set_time_limit( $limit ); // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Discouraged, WordPress.PHP.NoSilencedErrors.Discouraged -- server may choose to disable this function.
1757 }
1758 }
1759
1760 public static function get_product_term_ids( $product_id, $taxonomy ) {
1761 $terms = get_the_terms( $product_id, $taxonomy );
1762
1763 return ( empty( $terms ) || is_wp_error( $terms ) ) ? array() : wp_list_pluck( $terms, 'term_id' );
1764 }
1765
1766 /**
1767 * Get all product cats for a product by ID, including hierarchy
1768 *
1769 * @param int $product_id Product ID.
1770 *
1771 * @return array
1772 */
1773 public static function get_product_cat_ids( int $product_id ): array {
1774 $product_cats = self::get_product_term_ids( $product_id, self::PRODUCT_CATEGORY_TAXONOMY );
1775
1776 foreach ( $product_cats as $product_cat ) {
1777 $product_cats = array_merge( $product_cats, get_ancestors( $product_cat, self::PRODUCT_CATEGORY_TAXONOMY, 'taxonomy' ) );
1778 }
1779
1780 return $product_cats;
1781 }
1782
1783 public static function get_coupon_types(): array {
1784 return (array) apply_filters( 'storeengine/product_coupon_types', [ 'percentage', 'fixedAmount' ] );
1785 }
1786
1787 /**
1788 * Return a list of potential postcodes for wildcard searching.
1789 *
1790 * @param string $postcode Postcode.
1791 * @param string $country Country to format postcode for matching.
1792 *
1793 * @return string[]
1794 */
1795 public static function get_wildcard_postcodes( $postcode, $country = '' ) {
1796 $formatted_postcode = Formatting::format_postcode( $postcode, $country );
1797 $length = function_exists( 'mb_strlen' ) ? mb_strlen( $formatted_postcode ) : strlen( $formatted_postcode );
1798 $postcodes = [
1799 $postcode,
1800 $formatted_postcode,
1801 $formatted_postcode . '*',
1802 ];
1803
1804 for ( $i = 0; $i < $length; $i ++ ) {
1805 $postcodes[] = ( function_exists( 'mb_substr' ) ? mb_substr( $formatted_postcode, 0, ( $i + 1 ) * - 1 ) : substr( $formatted_postcode, 0, ( $i + 1 ) * - 1 ) ) . '*';
1806 }
1807
1808 return $postcodes;
1809 }
1810
1811 /**
1812 * Used by shipping zones and taxes to compare a given $postcode to stored
1813 * postcodes to find matches for numerical ranges, and wildcards.
1814 *
1815 * @param string $postcode Postcode you want to match against stored postcodes.
1816 * @param array $objects Array of postcode objects from Database.
1817 * @param string $object_id_key DB column name for the ID.
1818 * @param string $object_compare_key DB column name for the value.
1819 * @param string $country Country from which this postcode belongs. Allows for formatting.
1820 *
1821 * @return array Array of matching object ID and matching values.
1822 */
1823 public static function postcode_location_matcher( $postcode, $objects, $object_id_key, $object_compare_key, $country = '' ) {
1824 $postcode = Formatting::normalize_postcode( $postcode );
1825 $wildcard_postcodes = array_map( [
1826 Formatting::class,
1827 'clean',
1828 ], self::get_wildcard_postcodes( $postcode, $country ) );
1829 $matches = [];
1830
1831 foreach ( $objects as $object ) {
1832 $object_id = $object->$object_id_key;
1833 $compare_against = $object->$object_compare_key;
1834
1835 // Handle postcodes containing ranges.
1836 if ( strstr( $compare_against, '...' ) ) {
1837 $range = array_map( 'trim', explode( '...', $compare_against ) );
1838
1839 if ( 2 !== count( $range ) ) {
1840 continue;
1841 }
1842
1843 list( $min, $max ) = $range;
1844
1845 // If the postcode is non-numeric, make it numeric.
1846 if ( ! is_numeric( $min ) || ! is_numeric( $max ) ) {
1847 $compare = Formatting::make_numeric_postcode( $postcode );
1848 $min = str_pad( Formatting::make_numeric_postcode( $min ), strlen( $compare ), '0' );
1849 $max = str_pad( Formatting::make_numeric_postcode( $max ), strlen( $compare ), '0' );
1850 } else {
1851 $compare = $postcode;
1852 }
1853
1854 if ( $compare >= $min && $compare <= $max ) {
1855 $matches[ $object_id ] = $matches[ $object_id ] ?? [];
1856 $matches[ $object_id ][] = $compare_against;
1857 }
1858 } elseif ( in_array( $compare_against, $wildcard_postcodes, true ) ) {
1859 // Wildcard and standard comparison.
1860 $matches[ $object_id ] = $matches[ $object_id ] ?? [];
1861 $matches[ $object_id ][] = $compare_against;
1862 }
1863 }
1864
1865 return $matches;
1866 }
1867
1868 /**
1869 * Based on wp_list_pluck, this calls a method instead of returning a property.
1870 *
1871 * @param array $list List of objects or arrays.
1872 * @param int|string $callback_or_field Callback method from the object to place instead of the entire object.
1873 * @param int|string $index_key Optional. Field from the object to use as keys for the new array.
1874 * Default null.
1875 *
1876 * @return array Array of values.
1877 */
1878 public static function list_pluck( array $list, $callback_or_field, $index_key = null ): array {
1879 // Use wp_list_pluck if this isn't a callback.
1880 $first_el = current( $list );
1881 if ( ! is_object( $first_el ) || ! is_callable( [ $first_el, $callback_or_field ] ) ) {
1882 return wp_list_pluck( $list, $callback_or_field, $index_key );
1883 }
1884 if ( ! $index_key ) {
1885 /*
1886 * This is simple. Could at some point wrap array_column()
1887 * if we knew we had an array of arrays.
1888 */
1889 foreach ( $list as $key => $value ) {
1890 $list[ $key ] = $value->{$callback_or_field}();
1891 }
1892
1893 return $list;
1894 }
1895
1896 /*
1897 * When index_key is not set for a particular item, push the value
1898 * to the end of the stack. This is how array_column() behaves.
1899 */
1900 $newlist = [];
1901 foreach ( $list as $value ) {
1902 // Get index. @since 3.2.0 this supports a callback.
1903 if ( is_callable( array( $value, $index_key ) ) ) {
1904 $newlist[ $value->{$index_key}() ] = $value->{$callback_or_field}();
1905 } elseif ( isset( $value->$index_key ) ) {
1906 $newlist[ $value->$index_key ] = $value->{$callback_or_field}();
1907 } else {
1908 $newlist[] = $value->{$callback_or_field}();
1909 }
1910 }
1911
1912 return $newlist;
1913 }
1914
1915 /**
1916 * Get an item of post data if set, otherwise return a default value.
1917 *
1918 * @param string $key Meta key.
1919 * @param mixed $default Default value.
1920 *
1921 * @return mixed Value sanitized by Formatting::clean.
1922 */
1923 public static function get_post_data_by_key( string $key, $default = '' ) {
1924 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput, WordPress.Security.NonceVerification.Missing
1925 return Formatting::clean( wp_unslash( self::get_var( $_POST[ $key ], $default ) ) );
1926 }
1927
1928 /**
1929 * Get data if set, otherwise return a default value or null. Prevents notices when data is not set.
1930 *
1931 * @param mixed $var Variable.
1932 * @param mixed $default Default value.
1933 *
1934 * @return mixed
1935 */
1936 public static function get_var( &$var, $default = null ) {
1937 return isset( $var ) ? $var : $default;
1938 }
1939
1940 public static function is_storeengine_page( int $post_id = 0 ): bool {
1941 if ( ! $post_id ) {
1942 $post_id = get_the_ID();
1943 }
1944
1945 return in_array( (int) $post_id, self::get_storeengine_page_ids(), true );
1946 }
1947
1948 public static function get_storeengine_page_ids(): array {
1949 $settings_keys = [
1950 'checkout_page',
1951 'shop_page',
1952 'store_shop',
1953 'cart_page',
1954 'thankyou_page',
1955 'dashboard_page',
1956 'membership_pricing_page',
1957 'affiliate_registration_page',
1958 ];
1959
1960 $page_ids = array_map( fn( $key ) => (int) self::get_settings( $key ), $settings_keys );
1961
1962 return array_filter( $page_ids );
1963 }
1964
1965 /**
1966 * Is registration required to checkout?
1967 *
1968 * @return boolean
1969 */
1970 public static function is_registration_required(): bool {
1971 /**
1972 * Controls if registration is required in order for checkout to be completed.
1973 *
1974 * @param bool $checkout_registration_required If customers must be registered to checkout.
1975 */
1976 return apply_filters( 'storeengine/checkout/registration_required', ! self::get_settings( 'enable_guest_checkout', true ) );
1977 }
1978
1979 /**
1980 * Define a constant if it is not already defined.
1981 *
1982 * @param string $name Constant name.
1983 * @param mixed $value Value.
1984 */
1985 public static function maybe_define_constant( string $name, $value ) {
1986 if ( ! defined( $name ) ) {
1987 define( $name, $value ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.VariableConstantNameFound
1988 }
1989 }
1990
1991 public static function get_assets_url( string $path = '' ): string {
1992 return STOREENGINE_ASSETS_URI . ltrim( $path, '/\\' );
1993 }
1994
1995 public static function get_plugin_url( string $path = '' ): string {
1996 return STOREENGINE_PLUGIN_ROOT_URI . ltrim( $path, '/\\' );
1997 }
1998
1999 public static function get_addons_url( string $addon, string $path = '' ): string {
2000 return self::get_plugin_url( 'addons/' . ltrim( rtrim( $addon, '/\\' ), '/\\' ) . '/' . ltrim( $path, '/\\' ) );
2001 }
2002
2003 public static function get_upload_dir(): string {
2004 $upload = wp_upload_dir();
2005
2006 return $upload['basedir'] . '/storeengine_uploads';
2007 }
2008
2009 /**
2010 * @param array $arr
2011 * @param callable $predicate
2012 *
2013 * @return bool
2014 * @deprecated 1.8.0
2015 * @see ArrayUtil::every()
2016 */
2017 public static function array_every( array $arr, callable $predicate ): bool {
2018 return ArrayUtil::every( $arr, $predicate );
2019 }
2020
2021 /**
2022 * @param array $arr
2023 * @param callable $predicate
2024 *
2025 * @return bool
2026 * @deprecated 1.8.0
2027 * @see ArrayUtil::any()
2028 */
2029 public static function array_any( array $arr, callable $predicate ): bool {
2030 return ArrayUtil::any( $arr, $predicate );
2031 }
2032
2033 public static function masked_key_preview( string $key, string $name = null, $args = [] ) {
2034 $args = wp_parse_args( $args, [
2035 'start' => 8,
2036 'end' => 2,
2037 'mask' => '',
2038 'size' => 5,
2039 'unmask' => true,
2040 'copy' => true,
2041 ] );
2042
2043 if ( empty( $name ) ) {
2044 $name = __( 'Key', 'storeengine' );
2045 }
2046
2047 $end = absint( $args['end'] );
2048 $masked = substr( $key, 0, absint( $args['start'] ) );
2049 $masked .= str_repeat( $args['mask'], absint( $args['size'] ) );
2050 if ( $end ) {
2051 $masked .= substr( $key, - 1 * $end );
2052 }
2053 ?>
2054 <div class="masked-key-preview storeengine-flex storeengine-flex-align-center">
2055 <?php if ( $args['unmask'] ) { ?>
2056 <button
2057 class="toggle-key-mask storeengine-btn storeengine-btn--md storeengine-btn--preset-transparent"
2058 style="--icon-size:1.1em;padding:10px" type="button"
2059 data-key-name="<?php echo esc_attr( $name ); ?>"
2060 aria-label="<?php printf(
2061 // translators: %s Masked key name.
2062 esc_attr__( 'Show %s', 'storeengine' ),
2063 esc_attr( $name ),
2064 ); ?>">
2065 <span class="storeengine-icon storeengine-icon--eye-alt" aria-hidden="true"></span>
2066 </button>
2067 <?php } ?>
2068 <span class="preview-masked"
2069 style="font-size:0.84em;font-weight:600;border: 1px solid transparent;outline:none;padding:5px;border-radius:4px;"><?php echo esc_html( $masked ); ?></span>
2070 <?php if ( $args['unmask'] ) { ?>
2071 <input class="preview-unmasked" type="text" value="<?php echo esc_attr( $key ); ?>"
2072 onclick="select(this)" readonly aria-label="<?php echo esc_attr( $name ); ?>"
2073 style="font-size:0.84em;font-weight:600;border: 1px solid var(--storeengine-border-color);outline:none;padding:5px;border-radius:4px;display:none;"/>
2074 <?php } ?>
2075 <?php if ( $args['copy'] ) { ?>
2076 <button
2077 class="copy-to-clipboard storeengine-btn storeengine-btn--md storeengine-btn--preset-transparent"
2078 style="--icon-size:1.1em;padding:10px" type="button"
2079 data-content="<?php echo esc_attr( $key ); ?>"
2080 data-content-name="<?php echo esc_attr( $name ); ?>"
2081 aria-label="<?php printf(
2082 // translators: %s Masked key name.
2083 esc_attr__( 'Copy %s', 'storeengine' ),
2084 esc_attr( $name ),
2085 ); ?>">
2086 <span class="storeengine-icon storeengine-icon--duplicate" aria-hidden="true"></span>
2087 </button>
2088 <?php } ?>
2089 </div>
2090 <?php
2091 }
2092
2093 /**
2094 * Implodes an array into a human-readable string with a localized conjunction before the last item.
2095 *
2096 * Examples:
2097 * - Helper::implode_with( ['a'] ); // Output: "a"
2098 * - Helper::implode_with( ['a', 'b'] ); // Output: "a or b"
2099 * - Helper::implode_with( ['a', 'b', 'c'] ); // Output: "a, b or c"
2100 * - Helper::implode_with( ['a', 'b', 'c'], 'and' ); // Output: "a, b and c"
2101 * - Helper::implode_with( ['a', 'b', 'c'], '', '-' ); // Output: "a-b or c"
2102 * - Helper::implode_with( ['a', 'b', 'c'], 'and', ' - ' ); // Output: "a - b and c"
2103 *
2104 * @param array $items The list of strings to join.
2105 * @param string $conjunction Optional. The conjunction to use before the last item (e.g. 'or', 'and').
2106 * If empty, defaults to localized 'or'.
2107 * @param string $glue Optional. Glue (separator) for joining the list of words.
2108 * Default to localized space after comma `, `.
2109 *
2110 * @return string The imploded string.
2111 */
2112 public static function implode_with( array $items, string $conjunction = '', string $glue = '' ): string {
2113 $count = count( $items );
2114
2115 if ( '' === $conjunction ) {
2116 $conjunction = _x( 'or', 'Conjunction before last word of an array.', 'storeengine' );
2117 }
2118
2119 if ( '' === $glue ) {
2120 $glue = _x( ', ', 'Glue/separator for joining word of an array (except last word).', 'storeengine' );
2121 }
2122
2123 if ( $count === 0 ) {
2124 return '';
2125 }
2126
2127 if ( $count === 1 ) {
2128 return $items[0];
2129 }
2130
2131 if ( $count === 2 ) {
2132 return $items[0] . " $conjunction " . $items[1];
2133 }
2134
2135 $last = array_pop( $items );
2136
2137 return implode( $glue, $items ) . ", $conjunction " . $last;
2138 }
2139
2140 public static function rename_array_keys( array $data, array $mapping ): array {
2141 $remapped = [];
2142
2143 foreach ( $data as $key => $value ) {
2144 $newKey = $mapping[ $key ] ?? $key; // fallback to original if not mapped
2145 $remapped[ $newKey ] = $value;
2146 }
2147
2148 return $remapped;
2149 }
2150
2151 public static function get_filename_without_extension( string $filename ) {
2152 return pathinfo( $filename, PATHINFO_FILENAME );
2153 }
2154
2155 /**
2156 * Log critical errors to the StoreEngine Logger.
2157 *
2158 * @param string|\Throwable $exception Exception object or string message.
2159 * @param bool $trace Whether to include the stack trace.
2160 */
2161 public static function log_error( $exception, bool $trace = true ) {
2162 $message = is_string( $exception ) ? $exception : $exception->getMessage();
2163
2164 $title = 'Error';
2165 $log_data = [ 'message' => $message ];
2166 $status = Logger::WARNING;
2167
2168 $context = [];
2169
2170 if ( $exception instanceof StoreEngineException ) {
2171 if ( $trace ) {
2172 $context = $exception->to_array( StoreEngineException::WITH_PREVIOUS & StoreEngineException::WITH_WP_TRACE );
2173 } else {
2174 $context = $exception->to_array( StoreEngineException::WITH_PREVIOUS );
2175 }
2176 } else {
2177 // If it is a Throwable/Exception object, add the code, file, and line number to the log.
2178 if ( $exception instanceof \Throwable ) {
2179 $title = 'Critical Error';
2180 $status = Logger::CRITICAL;
2181 $log_data['code'] = $exception->getCode();
2182 $log_data['file'] = $exception->getFile();
2183 $log_data['line'] = $exception->getLine();
2184 }
2185
2186 if ( $trace ) {
2187 $context['backtrace'] = wp_debug_backtrace_summary( self::class, 0, false );
2188 }
2189 }
2190
2191
2192
2193 if ( $context ) {
2194 $log_data['context'] = $context;
2195 }
2196
2197 // Save to the StoreEngine Logger (legacy error_log implementation has been removed).
2198 Logger::log( $title, $log_data, $status, 'system' );
2199 }
2200 }
2201