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