Emails
2 months ago
Integrations
2 months ago
MultichannelMarketing
2 years ago
TransactionalEmails
10 months ago
WooCommerceBookings
1 month ago
WooCommerceSubscriptions
2 months ago
CouponPreProcessor.php
2 months ago
Emails.php
8 months ago
Helper.php
1 month ago
MailPoetTask.php
2 years ago
NonPersistablePreviewData.php
1 month ago
OrderAttributionFields.php
1 month ago
OrderAttributionRevenueReader.php
2 weeks ago
OrderAttributionWriter.php
1 month ago
RandomCouponCodeGenerator.php
2 months ago
Settings.php
1 year ago
SubscriberEngagement.php
3 years ago
Subscription.php
2 months ago
Tracker.php
2 years ago
TransactionalEmailHooks.php
1 year ago
TransactionalEmails.php
1 year ago
WooSystemInfo.php
1 month ago
WooSystemInfoController.php
1 year ago
index.php
3 years ago
Helper.php
488 lines
| 1 | <?php // phpcs:ignore SlevomatCodingStandard.TypeHints.DeclareStrictTypes.DeclareStrictTypesMissing |
| 2 | |
| 3 | namespace MailPoet\WooCommerce; |
| 4 | |
| 5 | if (!defined('ABSPATH')) exit; |
| 6 | |
| 7 | |
| 8 | use Automattic\WooCommerce\Admin\API\Reports\Customers\Stats\DataStore; |
| 9 | use MailPoet\DI\ContainerWrapper; |
| 10 | use MailPoet\RuntimeException; |
| 11 | use MailPoet\WP\Functions as WPFunctions; |
| 12 | |
| 13 | class Helper { |
| 14 | /** @var WPFunctions */ |
| 15 | private $wp; |
| 16 | |
| 17 | public function __construct( |
| 18 | WPFunctions $wp |
| 19 | ) { |
| 20 | $this->wp = $wp; |
| 21 | } |
| 22 | |
| 23 | public function isWooCommerceActive() { |
| 24 | return class_exists('WooCommerce') && $this->wp->isPluginActive('woocommerce/woocommerce.php'); |
| 25 | } |
| 26 | |
| 27 | public function getWooCommerceVersion() { |
| 28 | return $this->isWooCommerceActive() ? get_plugin_data(WP_PLUGIN_DIR . '/woocommerce/woocommerce.php', false, false)['Version'] : null; |
| 29 | } |
| 30 | |
| 31 | public function getPurchaseStates(): array { |
| 32 | |
| 33 | return (array)$this->wp->applyFilters( |
| 34 | 'mailpoet_purchase_order_states', |
| 35 | ['completed'] |
| 36 | ); |
| 37 | } |
| 38 | |
| 39 | public function isWooCommerceBlocksActive($min_version = '') { |
| 40 | if (!class_exists('\Automattic\WooCommerce\Blocks\Package')) { |
| 41 | return false; |
| 42 | } |
| 43 | if ($min_version) { |
| 44 | return version_compare(\Automattic\WooCommerce\Blocks\Package::get_version(), $min_version, '>='); |
| 45 | } |
| 46 | return true; |
| 47 | } |
| 48 | |
| 49 | public function isWooCommerceCustomOrdersTableEnabled(): bool { |
| 50 | if ( |
| 51 | $this->isWooCommerceActive() |
| 52 | // Stubs reflect the current WC version; the runtime check guards older WooCommerce installs that lack this helper. |
| 53 | // @phpstan-ignore-next-line function.alreadyNarrowedType |
| 54 | && method_exists('\Automattic\WooCommerce\Utilities\OrderUtil', 'custom_orders_table_usage_is_enabled') |
| 55 | && \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled() |
| 56 | ) { |
| 57 | return true; |
| 58 | } |
| 59 | |
| 60 | return false; |
| 61 | } |
| 62 | |
| 63 | public function WC() { |
| 64 | return WC(); |
| 65 | } |
| 66 | |
| 67 | public function isWooCommerceRestApiRequest(): bool { |
| 68 | $wc = $this->WC(); |
| 69 | return $wc instanceof \WooCommerce && $wc->is_rest_api_request(); |
| 70 | } |
| 71 | |
| 72 | public function isWooCommerceStoreApiRequest(): bool { |
| 73 | $wc = $this->WC(); |
| 74 | return $wc instanceof \WooCommerce && $wc->is_store_api_request(); |
| 75 | } |
| 76 | |
| 77 | public function wcGetCustomerOrderCount($userId) { |
| 78 | return wc_get_customer_order_count($userId); |
| 79 | } |
| 80 | |
| 81 | public function wcGetCustomer(int $userId): ?\WC_Customer { |
| 82 | if (!class_exists(\WC_Customer::class)) { |
| 83 | return null; |
| 84 | } |
| 85 | try { |
| 86 | return new \WC_Customer($userId); |
| 87 | } catch (\Throwable $e) { |
| 88 | return null; |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | public function wcGetOrder($order = false) { |
| 93 | return wc_get_order($order); |
| 94 | } |
| 95 | |
| 96 | public function wcGetReviewOrderUrl(\WC_Order $order): string { |
| 97 | $callback = $this->getCallableFunction('wc_get_review_order_url'); |
| 98 | if (!$callback) { |
| 99 | return ''; |
| 100 | } |
| 101 | |
| 102 | $url = $callback($order); |
| 103 | return is_string($url) ? $url : ''; |
| 104 | } |
| 105 | |
| 106 | public function wcSupportsOrderReviewUrl(): bool { |
| 107 | if ($this->getCallableFunction('wc_get_review_order_url') === null) { |
| 108 | return false; |
| 109 | } |
| 110 | |
| 111 | if ($this->getOrderReviewItemEligibilityCallback() === null) { |
| 112 | return false; |
| 113 | } |
| 114 | |
| 115 | if (!$this->isCustomerReviewRequestFeatureEnabled()) { |
| 116 | return false; |
| 117 | } |
| 118 | |
| 119 | $pageId = $this->wcGetPageId('review_order'); |
| 120 | if (!$pageId || $pageId < 1) { |
| 121 | return false; |
| 122 | } |
| 123 | |
| 124 | $permalink = $this->wp->getPermalink($pageId); |
| 125 | return is_string($permalink) && $permalink !== ''; |
| 126 | } |
| 127 | |
| 128 | public function wcOrderHasActionableReviewItems(\WC_Order $order): bool { |
| 129 | $callback = $this->getOrderReviewItemEligibilityCallback(); |
| 130 | if ($callback === null) { |
| 131 | return false; |
| 132 | } |
| 133 | |
| 134 | return (bool)call_user_func($callback, $order); |
| 135 | } |
| 136 | |
| 137 | private function getOrderReviewItemEligibilityCallback(): ?callable { |
| 138 | $callback = ['\Automattic\WooCommerce\Internal\OrderReviews\ItemEligibility', 'has_actionable_items']; |
| 139 | return is_callable($callback) ? $callback : null; |
| 140 | } |
| 141 | |
| 142 | private function getCallableFunction(string $functionName): ?callable { |
| 143 | return is_callable($functionName) ? $functionName : null; |
| 144 | } |
| 145 | |
| 146 | private function isCustomerReviewRequestFeatureEnabled(): bool { |
| 147 | if (!$this->canUseWooCommerceFeatureUtilities()) { |
| 148 | return false; |
| 149 | } |
| 150 | |
| 151 | return \Automattic\WooCommerce\Utilities\FeaturesUtil::feature_is_enabled('customer_review_request'); |
| 152 | } |
| 153 | |
| 154 | public function wcGetOrders(array $args) { |
| 155 | return wc_get_orders($args); |
| 156 | } |
| 157 | |
| 158 | public function wcCreateOrder(array $args) { |
| 159 | return wc_create_order($args); |
| 160 | } |
| 161 | |
| 162 | public function wcPrice($price, array $args = []) { |
| 163 | return wc_price($price, $args); |
| 164 | } |
| 165 | |
| 166 | public function wcGetProduct($theProduct = false) { |
| 167 | return wc_get_product($theProduct); |
| 168 | } |
| 169 | |
| 170 | public function wcGetProducts(array $args) { |
| 171 | return wc_get_products($args); |
| 172 | } |
| 173 | |
| 174 | public function wcGetFormattedVariation(\WC_Product_Variation $variation, bool $flat = false, bool $includeNames = true, bool $skipAttributesInName = false): string { |
| 175 | return wc_get_formatted_variation($variation, $flat, $includeNames, $skipAttributesInName); |
| 176 | } |
| 177 | |
| 178 | public function wcGetPageId(string $page): ?int { |
| 179 | if ($this->isWooCommerceActive()) { |
| 180 | return (int)wc_get_page_id($page); |
| 181 | } |
| 182 | return null; |
| 183 | } |
| 184 | |
| 185 | public function wcGetPriceDecimals(): int { |
| 186 | return wc_get_price_decimals(); |
| 187 | } |
| 188 | |
| 189 | public function wcGetPriceDecimalSeparator(): string { |
| 190 | return wc_get_price_decimal_separator(); |
| 191 | } |
| 192 | |
| 193 | public function wcGetPriceThousandSeparator(): string { |
| 194 | return wc_get_price_thousand_separator(); |
| 195 | } |
| 196 | |
| 197 | public function getWoocommercePriceFormat(): string { |
| 198 | return get_woocommerce_price_format(); |
| 199 | } |
| 200 | |
| 201 | public function getWoocommerceCurrency() { |
| 202 | return get_woocommerce_currency(); |
| 203 | } |
| 204 | |
| 205 | public function getWoocommerceCurrencySymbol() { |
| 206 | return get_woocommerce_currency_symbol(); |
| 207 | } |
| 208 | |
| 209 | public function woocommerceFormField($key, $args, $value) { |
| 210 | return woocommerce_form_field($key, $args, $value); |
| 211 | } |
| 212 | |
| 213 | public function wcLightOrDark($color, $dark, $light) { |
| 214 | return wc_light_or_dark($color, $dark, $light); |
| 215 | } |
| 216 | |
| 217 | public function wcHexIsLight($color) { |
| 218 | return wc_hex_is_light($color); |
| 219 | } |
| 220 | |
| 221 | public function getOrdersCountCreatedBefore(string $dateTime): int { |
| 222 | $ordersCount = $this->wcGetOrders([ |
| 223 | 'status' => 'all', |
| 224 | 'type' => 'shop_order', |
| 225 | 'date_created' => '<' . $dateTime, |
| 226 | 'limit' => 1, |
| 227 | 'paginate' => true, |
| 228 | ])->total; |
| 229 | |
| 230 | return intval($ordersCount); |
| 231 | } |
| 232 | |
| 233 | public function getRawPrice($price, array $args = []) { |
| 234 | $htmlPrice = $this->wcPrice($price, $args); |
| 235 | return html_entity_decode(strip_tags($htmlPrice), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401); |
| 236 | } |
| 237 | |
| 238 | public function getAllowedCountries(): array { |
| 239 | return (new \WC_Countries)->get_allowed_countries() ?? []; |
| 240 | } |
| 241 | |
| 242 | public function getCustomersCount(): int { |
| 243 | if (!$this->isWooCommerceActive() || !class_exists(DataStore::class)) { |
| 244 | return 0; |
| 245 | } |
| 246 | |
| 247 | $dataStore = new DataStore(); |
| 248 | $result = (array)$dataStore->get_data([ |
| 249 | 'fields' => ['customers_count'], |
| 250 | ]); |
| 251 | return isset($result['customers_count']) ? intval($result['customers_count']) : 0; |
| 252 | } |
| 253 | |
| 254 | public function wasMailPoetInstalledViaWooCommerceOnboardingWizard(): bool { |
| 255 | $wp = ContainerWrapper::getInstance()->get(WPFunctions::class); |
| 256 | $installedViaWooCommerce = false; |
| 257 | $wooCommerceOnboardingProfile = $wp->getOption('woocommerce_onboarding_profile'); |
| 258 | |
| 259 | if ( |
| 260 | is_array($wooCommerceOnboardingProfile) |
| 261 | && isset($wooCommerceOnboardingProfile['business_extensions']) |
| 262 | && is_array($wooCommerceOnboardingProfile['business_extensions']) |
| 263 | && in_array('mailpoet', $wooCommerceOnboardingProfile['business_extensions']) |
| 264 | ) { |
| 265 | $installedViaWooCommerce = true; |
| 266 | } |
| 267 | |
| 268 | return $installedViaWooCommerce; |
| 269 | } |
| 270 | |
| 271 | public function getOrdersTableName() { |
| 272 | // @phpstan-ignore function.impossibleType (WC stub omits this method; the runtime check is meaningful for older WC versions) |
| 273 | if (!method_exists('\Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore', 'get_orders_table_name')) { |
| 274 | throw new RuntimeException('Cannot get orders table name when running a WooCommerce version that doesn\'t support custom order tables.'); |
| 275 | } |
| 276 | |
| 277 | return \Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore::get_orders_table_name(); |
| 278 | } |
| 279 | |
| 280 | public function getAddressesTableName() { |
| 281 | // @phpstan-ignore function.impossibleType (WC stub omits this method; the runtime check is meaningful for older WC versions) |
| 282 | if (!method_exists('\Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore', 'get_addresses_table_name')) { |
| 283 | throw new RuntimeException('Cannot get addresses table name when running a WooCommerce version that doesn\'t support custom order tables.'); |
| 284 | } |
| 285 | |
| 286 | return \Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore::get_addresses_table_name(); |
| 287 | } |
| 288 | |
| 289 | public function wcGetCouponTypes(): array { |
| 290 | return wc_get_coupon_types(); |
| 291 | } |
| 292 | |
| 293 | public function wcGetCouponCodeById(int $id): string { |
| 294 | return wc_get_coupon_code_by_id($id); |
| 295 | } |
| 296 | |
| 297 | public function wcGetCouponIdByCode(string $code): int { |
| 298 | return (int)wc_get_coupon_id_by_code($code); |
| 299 | } |
| 300 | |
| 301 | /** |
| 302 | * @param mixed $data Coupon data, object, ID or code. |
| 303 | */ |
| 304 | public function createWcCoupon($data) { |
| 305 | return new \WC_Coupon($data); |
| 306 | } |
| 307 | |
| 308 | public function getOrderStatuses(): array { |
| 309 | return wc_get_order_statuses(); |
| 310 | } |
| 311 | |
| 312 | /** |
| 313 | * @return array|\WP_Post[] |
| 314 | */ |
| 315 | public function getCouponList( |
| 316 | int $pageSize = 10, |
| 317 | int $pageNumber = 1, |
| 318 | ?string $discountType = null, |
| 319 | ?string $search = null, |
| 320 | array $includeCouponIds = [] |
| 321 | ): array { |
| 322 | $args = [ |
| 323 | 'posts_per_page' => $pageSize, |
| 324 | 'orderby' => 'name', |
| 325 | 'order' => 'asc', |
| 326 | 'post_type' => 'shop_coupon', |
| 327 | 'post_status' => 'publish', |
| 328 | 'paged' => $pageNumber, |
| 329 | ]; |
| 330 | // Filtering coupons by discount type |
| 331 | if ($discountType) { |
| 332 | $args['meta_key'] = 'discount_type'; |
| 333 | $args['meta_value'] = $discountType; |
| 334 | } |
| 335 | // Search coupon by a query string |
| 336 | if ($search) { |
| 337 | $args['s'] = $search; |
| 338 | } |
| 339 | |
| 340 | $includeCoupons = []; |
| 341 | // We need to include the coupon with the given ID in the first page |
| 342 | if ($includeCouponIds && $pageNumber === 1) { |
| 343 | $includeArgs = $args; |
| 344 | $includeArgs['include'] = $includeCouponIds; |
| 345 | $includeCoupons = $this->wp->getPosts($includeArgs); |
| 346 | } |
| 347 | |
| 348 | // We remove duplicates because one of the remaining pages might contain the coupon with the given ID |
| 349 | $result = array_merge($includeCoupons, $this->wp->getPosts($args)); |
| 350 | $result = array_unique($result, SORT_REGULAR); |
| 351 | return array_values($result); |
| 352 | } |
| 353 | |
| 354 | public function getLatestCoupon(): ?string { |
| 355 | $coupons = $this->wp->getPosts([ |
| 356 | 'numberposts' => 1, |
| 357 | 'orderby' => 'date_created', |
| 358 | 'order' => 'desc', |
| 359 | 'post_type' => 'shop_coupon', |
| 360 | 'post_status' => 'publish', |
| 361 | ]); |
| 362 | $coupon = reset($coupons); |
| 363 | |
| 364 | return $coupon ? $coupon->post_title : null; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps |
| 365 | } |
| 366 | |
| 367 | public function getPaymentGateways() { |
| 368 | return $this->WC()->payment_gateways(); |
| 369 | } |
| 370 | |
| 371 | /** |
| 372 | * Returns a list of all available shipping methods formatted |
| 373 | * in a way to be used in the 'used shipping method' segment. |
| 374 | */ |
| 375 | public function getShippingMethodInstancesData(): array { |
| 376 | $shippingZones = \WC_Shipping_Zones::get_zones(); |
| 377 | $formattedShippingMethodData = []; |
| 378 | |
| 379 | foreach ($shippingZones as $shippingZone) { |
| 380 | $formattedShippingMethodData = array_merge( |
| 381 | $formattedShippingMethodData, |
| 382 | $this->formatShippingMethods($shippingZone['shipping_methods'], $shippingZone['zone_name']) |
| 383 | ); |
| 384 | } |
| 385 | |
| 386 | // special shipping zone that includes locations not covered by the configured shipping zones |
| 387 | $outOfCoverageShippingZone = new \WC_Shipping_Zone(0); |
| 388 | $formattedShippingMethodData = array_merge( |
| 389 | $formattedShippingMethodData, |
| 390 | $this->formatShippingMethods($outOfCoverageShippingZone->get_shipping_methods(), $outOfCoverageShippingZone->get_zone_name()) |
| 391 | ); |
| 392 | |
| 393 | $keyedZones = []; |
| 394 | |
| 395 | foreach ($formattedShippingMethodData as $shippingMethodArray) { |
| 396 | $keyedZones[$shippingMethodArray['instanceId']] = $shippingMethodArray; |
| 397 | } |
| 398 | |
| 399 | return $keyedZones; |
| 400 | } |
| 401 | |
| 402 | public function wcGetAttributeTaxonomies(): array { |
| 403 | return wc_get_attribute_taxonomies(); |
| 404 | } |
| 405 | |
| 406 | public function getWoocommerceStoreConfig(): array { |
| 407 | return [ |
| 408 | 'precision' => $this->wcGetPriceDecimals(), |
| 409 | 'decimalSeparator' => $this->wcGetPriceDecimalSeparator(), |
| 410 | 'thousandSeparator' => $this->wcGetPriceThousandSeparator(), |
| 411 | 'code' => $this->getWoocommerceCurrency(), |
| 412 | 'symbol' => html_entity_decode($this->getWoocommerceCurrencySymbol(), ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401), |
| 413 | 'symbolPosition' => $this->wp->getOption('woocommerce_currency_pos'), |
| 414 | 'priceFormat' => $this->getWoocommercePriceFormat(), |
| 415 | |
| 416 | ]; |
| 417 | } |
| 418 | |
| 419 | protected function formatShippingMethods(array $shippingMethods, string $shippingZoneName): array { |
| 420 | $formattedShippingMethods = []; |
| 421 | |
| 422 | foreach ($shippingMethods as $shippingMethod) { |
| 423 | $formattedShippingMethods[] = [ |
| 424 | 'instanceId' => (string)$shippingMethod->instance_id, // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps |
| 425 | 'name' => "{$shippingMethod->title} ({$shippingZoneName})", |
| 426 | ]; |
| 427 | } |
| 428 | |
| 429 | return $formattedShippingMethods; |
| 430 | } |
| 431 | |
| 432 | /** |
| 433 | * Check whether the current request is processing a WooCommerce checkout. |
| 434 | * Works for both the normal checkout and the block checkout. |
| 435 | * |
| 436 | * This solution is not ideal, but I checked with a few WooCommerce developers, |
| 437 | * and it is what they suggested. There is no helper function provided by Woo |
| 438 | * for this. |
| 439 | * |
| 440 | * @return bool |
| 441 | */ |
| 442 | public function isCheckoutRequest(): bool { |
| 443 | $requestUri = is_string($_SERVER['REQUEST_URI'] ?? null) && $_SERVER['REQUEST_URI'] !== '' |
| 444 | ? sanitize_text_field(wp_unslash($_SERVER['REQUEST_URI'])) |
| 445 | : ''; |
| 446 | $isRegularCheckout = is_checkout(); |
| 447 | $isBlockCheckout = WC()->is_rest_api_request() |
| 448 | && (strpos($requestUri, 'wc/store/checkout') !== false || strpos($requestUri, 'wc/store/v1/checkout') !== false); |
| 449 | |
| 450 | return $isRegularCheckout || $isBlockCheckout; |
| 451 | } |
| 452 | |
| 453 | public function isWooCommerceEmailImprovementsEnabled(): bool { |
| 454 | if (!$this->canUseWooCommerceFeatureUtilities()) { |
| 455 | return false; |
| 456 | } |
| 457 | return \Automattic\WooCommerce\Utilities\FeaturesUtil::feature_is_enabled('email_improvements'); |
| 458 | } |
| 459 | |
| 460 | private function canUseWooCommerceFeatureUtilities(): bool { |
| 461 | if (!$this->isWooCommerceActive()) { |
| 462 | return false; |
| 463 | } |
| 464 | |
| 465 | if (!class_exists('\Automattic\WooCommerce\Utilities\FeaturesUtil')) { |
| 466 | return false; |
| 467 | } |
| 468 | |
| 469 | return function_exists('wc_get_container'); |
| 470 | } |
| 471 | |
| 472 | public function wcPlaceholderImgSrc(string $size = 'woocommerce_thumbnail'): string { |
| 473 | if (!$this->isWooCommerceActive() || !function_exists('wc_placeholder_img_src')) { |
| 474 | return ''; |
| 475 | } |
| 476 | |
| 477 | return wc_placeholder_img_src($size); |
| 478 | } |
| 479 | |
| 480 | public function wcGetPagePermalink(string $page): string { |
| 481 | if (!$this->isWooCommerceActive() || !function_exists('wc_get_page_permalink')) { |
| 482 | return ''; |
| 483 | } |
| 484 | |
| 485 | return wc_get_page_permalink($page); |
| 486 | } |
| 487 | } |
| 488 |