PluginProbe
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More / 2.0.0
StoreEngine — Complete eCommerce Solution with Memberships, Licensing, Affiliates & More v2.0.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.0.0, at includes/utils/helper.php

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