Няма описание

WooCommerce.php 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. <?php
  2. namespace MailPoet\Segments;
  3. if (!defined('ABSPATH')) exit;
  4. use MailPoet\Config\Env;
  5. use MailPoet\Models\ModelValidator;
  6. use MailPoet\Models\Segment;
  7. use MailPoet\Models\Subscriber;
  8. use MailPoet\Models\SubscriberSegment;
  9. use MailPoet\Settings\SettingsController;
  10. use MailPoet\Subscribers\Source;
  11. use MailPoet\Subscribers\SubscribersRepository;
  12. use MailPoet\WooCommerce\Helper as WCHelper;
  13. use MailPoet\WP\Functions as WPFunctions;
  14. use MailPoetVendor\Idiorm\ORM;
  15. class WooCommerce {
  16. /** @var SettingsController */
  17. private $settings;
  18. /** @var WPFunctions */
  19. private $wp;
  20. /** @var WP */
  21. private $wpSegment;
  22. /** @var string|null */
  23. private $mailpoetEmailCollation;
  24. /** @var string|null */
  25. private $wpPostmetaValueCollation;
  26. /** @var SubscribersRepository */
  27. private $subscribersRepository;
  28. /** @var WCHelper */
  29. private $woocommerceHelper;
  30. public function __construct(
  31. SettingsController $settings,
  32. WPFunctions $wp,
  33. WCHelper $woocommerceHelper,
  34. SubscribersRepository $subscribersRepository,
  35. WP $wpSegment
  36. ) {
  37. $this->settings = $settings;
  38. $this->wp = $wp;
  39. $this->wpSegment = $wpSegment;
  40. $this->subscribersRepository = $subscribersRepository;
  41. $this->woocommerceHelper = $woocommerceHelper;
  42. }
  43. public function shouldShowWooCommerceSegment() {
  44. $isWoocommerceActive = $this->woocommerceHelper->isWooCommerceActive();
  45. $woocommerceUserExists = $this->subscribersRepository->woocommerceUserExists();
  46. if (!$isWoocommerceActive && !$woocommerceUserExists) {
  47. return false;
  48. }
  49. return true;
  50. }
  51. public function synchronizeRegisteredCustomer($wpUserId, $currentFilter = null) {
  52. $wcSegment = Segment::getWooCommerceSegment();
  53. if ($wcSegment === false) return;
  54. $currentFilter = $currentFilter ?: $this->wp->currentFilter();
  55. switch ($currentFilter) {
  56. case 'woocommerce_delete_customer':
  57. // subscriber should be already deleted in WP users sync
  58. $this->unsubscribeUsersFromSegment(); // remove leftover association
  59. break;
  60. case 'woocommerce_new_customer':
  61. case 'woocommerce_created_customer':
  62. $newCustomer = true;
  63. case 'woocommerce_update_customer':
  64. default:
  65. $wpUser = $this->wp->getUserdata($wpUserId);
  66. $subscriber = Subscriber::where('wp_user_id', $wpUserId)
  67. ->findOne();
  68. if ($wpUser === false || $subscriber === false) {
  69. // registered customers should exist as WP users and WP segment subscribers
  70. return false;
  71. }
  72. $data = [
  73. 'is_woocommerce_user' => 1,
  74. ];
  75. if (!empty($newCustomer)) {
  76. $data['source'] = Source::WOOCOMMERCE_USER;
  77. }
  78. $data['id'] = $subscriber->id();
  79. if ($wpUser->first_name) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
  80. $data['first_name'] = $wpUser->first_name; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
  81. }
  82. if ($wpUser->last_name) { // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
  83. $data['last_name'] = $wpUser->last_name; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
  84. }
  85. $subscriber = Subscriber::createOrUpdate($data);
  86. if ($subscriber->getErrors() === false && $subscriber->id > 0) {
  87. // add subscriber to the WooCommerce Customers segment
  88. SubscriberSegment::subscribeToSegments(
  89. $subscriber,
  90. [$wcSegment->id]
  91. );
  92. }
  93. break;
  94. }
  95. return true;
  96. }
  97. public function synchronizeGuestCustomer($orderId) {
  98. $wcOrder = $this->woocommerceHelper->wcGetOrder($orderId);
  99. $wcSegment = Segment::getWooCommerceSegment();
  100. if ((!$wcOrder instanceof \WC_Order) || $wcSegment === false) return;
  101. $signupConfirmation = $this->settings->get('signup_confirmation');
  102. $status = Subscriber::STATUS_UNCONFIRMED;
  103. if ((bool)$signupConfirmation['enabled'] === false) {
  104. $status = Subscriber::STATUS_SUBSCRIBED;
  105. }
  106. $insertedEmails = $this->insertSubscribersFromOrders($orderId, $status);
  107. if (empty($insertedEmails[0]['email'])) {
  108. return false;
  109. }
  110. $subscriber = Subscriber::where('email', $insertedEmails[0]['email'])
  111. ->findOne();
  112. if ($subscriber !== false) {
  113. $firstName = $wcOrder->get_billing_first_name(); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
  114. $lastName = $wcOrder->get_billing_last_name(); // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
  115. if ($firstName) {
  116. $subscriber->firstName = $firstName;
  117. }
  118. if ($lastName) {
  119. $subscriber->lastName = $lastName;
  120. }
  121. if ($firstName || $lastName) {
  122. $subscriber->save();
  123. }
  124. // add subscriber to the WooCommerce Customers segment
  125. SubscriberSegment::subscribeToSegments(
  126. $subscriber,
  127. [$wcSegment->id]
  128. );
  129. }
  130. }
  131. public function synchronizeCustomers() {
  132. $this->wpSegment->synchronizeUsers(); // synchronize registered users
  133. $this->markRegisteredCustomers();
  134. $insertedUsersEmails = $this->insertSubscribersFromOrders();
  135. $this->removeUpdatedSubscribersWithInvalidEmail($insertedUsersEmails);
  136. unset($insertedUsersEmails);
  137. $this->updateFirstNames();
  138. $this->updateLastNames();
  139. $this->insertUsersToSegment();
  140. $this->unsubscribeUsersFromSegment();
  141. $this->removeOrphanedSubscribers();
  142. $this->updateStatus();
  143. $this->updateGlobalStatus();
  144. return true;
  145. }
  146. private function ensureColumnCollation(): void {
  147. if ($this->mailpoetEmailCollation && $this->wpPostmetaValueCollation) {
  148. return;
  149. }
  150. global $wpdb;
  151. $mailpoetEmailColumn = $wpdb->get_row(
  152. 'SHOW FULL COLUMNS FROM ' . MP_SUBSCRIBERS_TABLE . ' WHERE Field = "email"'
  153. );
  154. $this->mailpoetEmailCollation = $mailpoetEmailColumn->Collation; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
  155. $wpPostmetaValueColumn = $wpdb->get_row(
  156. 'SHOW FULL COLUMNS FROM ' . $wpdb->postmeta . ' WHERE Field = "meta_value"'
  157. );
  158. $this->wpPostmetaValueCollation = $wpPostmetaValueColumn->Collation; // phpcs:ignore Squiz.NamingConventions.ValidVariableName.MemberNotCamelCaps
  159. }
  160. private function needsCollationChange(): bool {
  161. $this->ensureColumnCollation();
  162. $collation1 = (string)$this->mailpoetEmailCollation;
  163. $collation2 = (string)$this->wpPostmetaValueCollation;
  164. if ($collation1 === $collation2) {
  165. return false;
  166. }
  167. $collation1UnderscorePos = strpos($collation1, '_');
  168. $collation2UnderscorePos = strpos($collation2, '_');
  169. $charset1 = substr($collation1, 0, $collation1UnderscorePos === false ? strlen($collation1) : $collation1UnderscorePos);
  170. $charset2 = substr($collation2, 0, $collation2UnderscorePos === false ? strlen($collation2) : $collation2UnderscorePos);
  171. return $charset1 === $charset2;
  172. }
  173. private function markRegisteredCustomers() {
  174. // Mark WP users having a customer role as WooCommerce subscribers
  175. global $wpdb;
  176. $subscribersTable = Subscriber::$_table;
  177. Subscriber::rawExecute(sprintf('
  178. UPDATE LOW_PRIORITY %1$s mps
  179. JOIN %2$s wu ON mps.wp_user_id = wu.id
  180. JOIN %3$s wpum ON wu.id = wpum.user_id AND wpum.meta_key = "' . $wpdb->prefix . 'capabilities"
  181. SET is_woocommerce_user = 1, source = "%4$s"
  182. WHERE wpum.meta_value LIKE "%%\"customer\"%%"
  183. ', $subscribersTable, $wpdb->users, $wpdb->usermeta, Source::WOOCOMMERCE_USER));
  184. }
  185. private function insertSubscribersFromOrders($orderId = null, $status = Subscriber::STATUS_SUBSCRIBED) {
  186. global $wpdb;
  187. $subscribersTable = Subscriber::$_table;
  188. $orderId = !is_null($orderId) ? (int)$orderId : null;
  189. $insertedUsersEmails = ORM::for_table($wpdb->users)->raw_query(
  190. 'SELECT DISTINCT wppm.meta_value as email FROM `' . $wpdb->prefix . 'postmeta` wppm
  191. JOIN `' . $wpdb->prefix . 'posts` p ON wppm.post_id = p.ID AND p.post_type = "shop_order"
  192. WHERE wppm.meta_key = "_billing_email" AND wppm.meta_value != ""
  193. ' . ($orderId ? ' AND p.ID = "' . $orderId . '"' : '') . '
  194. ')->findArray();
  195. Subscriber::rawExecute(sprintf('
  196. INSERT IGNORE INTO %1$s (is_woocommerce_user, email, status, created_at, last_subscribed_at, source)
  197. SELECT 1, wppm.meta_value, "%2$s", CURRENT_TIMESTAMP(), CURRENT_TIMESTAMP(), "%3$s" FROM `' . $wpdb->prefix . 'postmeta` wppm
  198. JOIN `' . $wpdb->prefix . 'posts` p ON wppm.post_id = p.ID AND p.post_type = "shop_order"
  199. WHERE wppm.meta_key = "_billing_email" AND wppm.meta_value != ""
  200. ' . ($orderId ? ' AND p.ID = "' . $orderId . '"' : '') . '
  201. ON DUPLICATE KEY UPDATE is_woocommerce_user = 1
  202. ', $subscribersTable, $status, Source::WOOCOMMERCE_USER));
  203. return $insertedUsersEmails;
  204. }
  205. private function removeUpdatedSubscribersWithInvalidEmail($updatedEmails) {
  206. $validator = new ModelValidator();
  207. $invalidIsWoocommerceUsers = array_map(function($item) {
  208. return $item['email'];
  209. },
  210. array_filter($updatedEmails, function($updatedEmail) use($validator) {
  211. return !$validator->validateEmail($updatedEmail['email']);
  212. }));
  213. if (!$invalidIsWoocommerceUsers) {
  214. return;
  215. }
  216. ORM::for_table(Subscriber::$_table)
  217. ->whereNull('wp_user_id')
  218. ->where('is_woocommerce_user', 1)
  219. ->whereIn('email', $invalidIsWoocommerceUsers)
  220. ->delete_many();
  221. }
  222. private function updateFirstNames() {
  223. global $wpdb;
  224. $collate = '';
  225. if ($this->needsCollationChange()) {
  226. $collate = ' COLLATE ' . $this->mailpoetEmailCollation;
  227. }
  228. $subscribersTable = Subscriber::$_table;
  229. Subscriber::rawExecute(sprintf('
  230. UPDATE LOW_PRIORITY %1$s mps
  231. JOIN %2$s wppm ON mps.email = wppm.meta_value %3$s AND wppm.meta_key = "_billing_email"
  232. JOIN %2$s wppm2 ON wppm2.post_id = wppm.post_id AND wppm2.meta_key = "_billing_first_name"
  233. JOIN (SELECT MAX(post_id) AS max_id FROM %2$s WHERE meta_key = "_billing_email" GROUP BY meta_value) AS tmaxid ON tmaxid.max_id = wppm.post_id
  234. SET mps.first_name = wppm2.meta_value
  235. WHERE mps.first_name = ""
  236. AND mps.is_woocommerce_user = 1
  237. AND wppm2.meta_value IS NOT NULL
  238. ', $subscribersTable, $wpdb->postmeta, $collate));
  239. }
  240. private function updateLastNames() {
  241. global $wpdb;
  242. $collate = '';
  243. if ($this->needsCollationChange()) {
  244. $collate = ' COLLATE ' . $this->mailpoetEmailCollation;
  245. }
  246. $subscribersTable = Subscriber::$_table;
  247. Subscriber::rawExecute(sprintf('
  248. UPDATE LOW_PRIORITY %1$s mps
  249. JOIN %2$s wppm ON mps.email = wppm.meta_value %3$s AND wppm.meta_key = "_billing_email"
  250. JOIN %2$s wppm2 ON wppm2.post_id = wppm.post_id AND wppm2.meta_key = "_billing_last_name"
  251. JOIN (SELECT MAX(post_id) AS max_id FROM %2$s WHERE meta_key = "_billing_email" GROUP BY meta_value) AS tmaxid ON tmaxid.max_id = wppm.post_id
  252. SET mps.last_name = wppm2.meta_value
  253. WHERE mps.last_name = ""
  254. AND mps.is_woocommerce_user = 1
  255. AND wppm2.meta_value IS NOT NULL
  256. ', $subscribersTable, $wpdb->postmeta, $collate));
  257. }
  258. private function insertUsersToSegment() {
  259. $wcSegment = Segment::getWooCommerceSegment();
  260. $subscribersTable = Subscriber::$_table;
  261. $wpMailpoetSubscriberSegmentTable = SubscriberSegment::$_table;
  262. // Subscribe WC users to segment
  263. Subscriber::rawExecute(sprintf('
  264. INSERT IGNORE INTO %s (subscriber_id, segment_id, created_at)
  265. SELECT mps.id, "%s", CURRENT_TIMESTAMP() FROM %s mps
  266. WHERE mps.is_woocommerce_user = 1
  267. ', $wpMailpoetSubscriberSegmentTable, $wcSegment->id, $subscribersTable));
  268. }
  269. private function unsubscribeUsersFromSegment() {
  270. $wcSegment = Segment::getWooCommerceSegment();
  271. $subscribersTable = Subscriber::$_table;
  272. $wpMailpoetSubscriberSegmentTable = SubscriberSegment::$_table;
  273. // Unsubscribe non-WC or invalid users from segment
  274. Subscriber::rawExecute(sprintf('
  275. DELETE mpss FROM %s mpss
  276. LEFT JOIN %s mps ON mpss.subscriber_id = mps.id
  277. WHERE mpss.segment_id = %s AND (mps.is_woocommerce_user = 0 OR mps.email = "" OR mps.email IS NULL)
  278. ', $wpMailpoetSubscriberSegmentTable, $subscribersTable, $wcSegment->id));
  279. }
  280. private function updateGlobalStatus() {
  281. $subscribersTable = Subscriber::$_table;
  282. $subscriberSegmentTable = SubscriberSegment::$_table;
  283. $wcSegment = Segment::getWooCommerceSegment();
  284. // Set global status unsubscribed to all woocommerce users without any segment
  285. $sql = sprintf('
  286. UPDATE %1$s mps
  287. LEFT JOIN %2$s mpss ON mpss.subscriber_id = mps.id
  288. SET mps.status = "unsubscribed"
  289. WHERE
  290. mpss.id IS NULL
  291. AND mps.is_woocommerce_user = 1
  292. ', $subscribersTable, $subscriberSegmentTable);
  293. Subscriber::rawExecute($sql);
  294. // SET global status unsubscribed to all woocommerce users who have only 1 segment and it is woocommerce segment and they are not subscribed
  295. // You can't specify target table 'mps' for update in FROM clause
  296. $sql = sprintf('
  297. UPDATE %1$s as mps
  298. JOIN %2$s as mpss on mps.id = mpss.subscriber_id AND mpss.segment_id = "%3$s" AND mpss.status = "unsubscribed"
  299. SET mps.status = "unsubscribed"
  300. WHERE mps.id IN (
  301. SELECT s.id -- get all subscribers with exactly 1 list
  302. FROM ( SELECT id FROM %1$s WHERE is_woocommerce_user = 1) as s
  303. JOIN %2$s as l on s.id=l.subscriber_id
  304. GROUP BY s.id
  305. HAVING COUNT(l.id) = 1
  306. )
  307. ', $subscribersTable, $subscriberSegmentTable, $wcSegment->id);
  308. Subscriber::rawExecute($sql);
  309. }
  310. private function removeOrphanedSubscribers() {
  311. // Remove orphaned WooCommerce segment subscribers (not having a matching WC customer email),
  312. // e.g. if WC orders were deleted directly from the database
  313. // or a customer role was revoked and a user has no orders
  314. global $wpdb;
  315. $wcSegment = Segment::getWooCommerceSegment();
  316. // Unmark registered customers
  317. // Insert WC customer IDs to a temporary table for left join to use an index
  318. $tmpTableName = Env::$dbPrefix . 'tmp_wc_ids';
  319. // Registered users with orders
  320. Subscriber::rawExecute(sprintf('
  321. CREATE TEMPORARY TABLE %1$s
  322. (`id` int(11) unsigned NOT NULL, UNIQUE(`id`)) AS
  323. SELECT DISTINCT wppm.meta_value AS id FROM %2$s wppm
  324. JOIN %3$s wpp ON wppm.post_id = wpp.ID
  325. AND wpp.post_type = "shop_order"
  326. WHERE wppm.meta_key = "_customer_user"
  327. ', $tmpTableName, $wpdb->postmeta, $wpdb->posts));
  328. // Registered users with a customer role
  329. Subscriber::rawExecute(sprintf('
  330. INSERT IGNORE INTO %1$s
  331. SELECT DISTINCT wpum.user_id AS id FROM %2$s wpum
  332. WHERE wpum.meta_key = "%3$s" AND wpum.meta_value LIKE "%%\"customer\"%%"
  333. ', $tmpTableName, $wpdb->usermeta, $wpdb->prefix . 'capabilities'));
  334. // Unmark WC list registered users which aren't WC customers anymore
  335. Subscriber::tableAlias('mps')
  336. ->select('mps.*')
  337. ->join(
  338. MP_SUBSCRIBER_SEGMENT_TABLE,
  339. 'mps.`id` = mpss.`subscriber_id` AND mpss.`segment_id` = "' . $wcSegment->id . '"',
  340. 'mpss'
  341. )
  342. ->leftOuterJoin(
  343. $tmpTableName,
  344. 'mps.`wp_user_id` = wctmp.`id`',
  345. 'wctmp'
  346. )
  347. ->where('is_woocommerce_user', 1)
  348. ->whereNull('wctmp.id')
  349. ->whereNotNull('wp_user_id')
  350. ->findResultSet()
  351. ->set('is_woocommerce_user', 0)
  352. ->save();
  353. Subscriber::rawExecute('DROP TABLE ' . $tmpTableName);
  354. // Remove guest customers
  355. // Insert WC customer emails to a temporary table and ensure matching collations
  356. // between MailPoet and WooCommerce emails for left join to use an index
  357. $tmpTableName = Env::$dbPrefix . 'tmp_wc_emails';
  358. Subscriber::rawExecute(sprintf('
  359. CREATE TEMPORARY TABLE %1$s
  360. (`email` varchar(150) NOT NULL, UNIQUE(`email`)) COLLATE %2$s AS
  361. SELECT DISTINCT wppm.meta_value AS email FROM %3$s wppm
  362. JOIN %4$s wpp ON wppm.post_id = wpp.ID
  363. AND wpp.post_type = "shop_order"
  364. WHERE wppm.meta_key = "_billing_email"
  365. ', $tmpTableName, $this->mailpoetEmailCollation, $wpdb->postmeta, $wpdb->posts));
  366. // Remove WC list guest users which aren't WC customers anymore
  367. Subscriber::tableAlias('mps')
  368. ->select('mps.*')
  369. ->join(
  370. MP_SUBSCRIBER_SEGMENT_TABLE,
  371. 'mps.`id` = mpss.`subscriber_id` AND mpss.`segment_id` = "' . $wcSegment->id . '"',
  372. 'mpss'
  373. )
  374. ->leftOuterJoin(
  375. $tmpTableName,
  376. 'mps.`email` = wctmp.`email`',
  377. 'wctmp'
  378. )
  379. ->where('is_woocommerce_user', 1)
  380. ->whereNull('wctmp.email')
  381. ->whereNull('wp_user_id')
  382. ->findResultSet()
  383. ->set('is_woocommerce_user', 0)
  384. ->delete();
  385. Subscriber::rawExecute('DROP TABLE ' . $tmpTableName);
  386. }
  387. private function updateStatus() {
  388. $subscribeOldCustomers = $this->settings->get('mailpoet_subscribe_old_woocommerce_customers.enabled', false);
  389. if ($subscribeOldCustomers !== "1") {
  390. $status = Subscriber::STATUS_UNSUBSCRIBED;
  391. } else {
  392. $status = Subscriber::STATUS_SUBSCRIBED;
  393. }
  394. $subscribersTable = Subscriber::$_table;
  395. $subscriberSegmentTable = SubscriberSegment::$_table;
  396. $wcSegment = Segment::getWooCommerceSegment();
  397. $sql = sprintf('
  398. UPDATE LOW_PRIORITY %1$s mpss
  399. JOIN %2$s mps ON mpss.subscriber_id = mps.id
  400. SET mpss.status = "%3$s"
  401. WHERE
  402. mpss.segment_id = %4$s
  403. AND mps.confirmed_at IS NULL
  404. AND mps.confirmed_ip IS NULL
  405. AND mps.is_woocommerce_user = 1
  406. ', $subscriberSegmentTable, $subscribersTable, $status, $wcSegment->id);
  407. Subscriber::rawExecute($sql);
  408. }
  409. }