<?php class MailChimp_WooCommerce_Rest_Api { protected static $namespace = 'mailchimp-for-woocommerce/v1'; /** * @param $path * * @return string */ public static function url($path) { return esc_url_raw(rest_url(static::$namespace.'/'.ltrim($path, '/'))); } /** * Register all Mailchimp API routes. */ public function register_routes() { // ping register_rest_route(static::$namespace, '/ping', array( 'methods' => 'GET', 'callback' => array($this, 'ping'), 'permission_callback' => '__return_true', )); // Right now we only have a survey disconnect endpoint. register_rest_route(static::$namespace, "/survey/disconnect", array( 'methods' => 'POST', 'callback' => array($this, 'post_disconnect_survey'), 'permission_callback' => array($this, 'permission_callback'), )); // Sync Stats register_rest_route(static::$namespace, '/sync/stats', array( 'methods' => 'GET', 'callback' => array($this, 'get_sync_stats'), 'permission_callback' => array($this, 'permission_callback'), )); // remove review banner register_rest_route(static::$namespace, "/review-banner", array( 'methods' => 'GET', 'callback' => array($this, 'dismiss_review_banner'), 'permission_callback' => array($this, 'permission_callback'), )); //Member Sync register_rest_route(static::$namespace, "/member-sync", array( 'methods' => 'GET', 'callback' => array($this, 'member_sync_alive_signal'), 'permission_callback' => '__return_true', )); register_rest_route(static::$namespace, "/member-sync", array( 'methods' => 'POST', 'callback' => array($this, 'member_sync'), 'permission_callback' => '__return_true', )); // Tower report register_rest_route(static::$namespace, "/tower/report", array( 'methods' => 'POST', 'callback' => array($this, 'get_tower_report'), 'permission_callback' => '__return_true', )); // tower logs register_rest_route(static::$namespace, "/tower/logs", array( 'methods' => 'POST', 'callback' => array($this, 'get_tower_logs'), 'permission_callback' => '__return_true', )); register_rest_route(static::$namespace, "/tower/resource", array( 'methods' => 'POST', 'callback' => array($this, 'get_tower_resource'), 'permission_callback' => '__return_true', )); register_rest_route(static::$namespace, "/tower/action", array( 'methods' => 'POST', 'callback' => array($this, 'handle_tower_action'), 'permission_callback' => '__return_true', )); register_rest_route(static::$namespace, "/tower/sync_stats", array( 'methods' => 'POST', 'callback' => array($this, 'get_tower_sync_stats'), 'permission_callback' => '__return_true', )); } /** * @return bool */ public function permission_callback() { $cap = mailchimp_get_allowed_capability(); return ($cap === 'manage_woocommerce' || $cap === 'manage_options' ); } /** * @param WP_REST_Request $request * * @return WP_REST_Response */ public function ping(WP_REST_Request $request) { return $this->mailchimp_rest_response(array('success' => true)); } /** * @param WP_REST_Request $request * * @return WP_REST_Response */ public function post_disconnect_survey(WP_REST_Request $request) { // need to send a post request to $host = mailchimp_environment_variables()->environment === 'staging' ? 'https://staging.conduit.vextras.com' : 'https://conduit.mailchimpapp.com'; $route = "{$host}/survey/woocommerce"; $result = wp_remote_post(esc_url_raw($route), array( 'timeout' => 12, 'blocking' => true, 'method' => 'POST', 'data_format' => 'body', 'headers' => array('Content-Type' => 'application/json; charset=utf-8'), 'body' => json_encode($request->get_params()), )); return $this->mailchimp_rest_response($result); } /** * @param WP_REST_Request $request * * @return WP_REST_Response */ public function get_sync_stats(WP_REST_Request $request) { // if the queue is running in the console - we need to say tell the response why it's not going to fire this way. if (!mailchimp_is_configured() || !($api = mailchimp_get_api())) { return $this->mailchimp_rest_response(array('success' => false, 'reason' => 'not configured')); } $store_id = mailchimp_get_store_id(); $complete = array( 'coupons' => get_option('mailchimp-woocommerce-sync.coupons.completed_at'), 'products' => get_option('mailchimp-woocommerce-sync.products.completed_at'), 'orders' => get_option('mailchimp-woocommerce-sync.orders.completed_at') ); $promo_rules_count = mailchimp_get_coupons_count(); $product_count = mailchimp_get_product_count(); $order_count = mailchimp_get_order_count(); $mailchimp_total_promo_rules = $complete['coupons'] ? $promo_rules_count - mailchimp_get_remaining_jobs_count('MailChimp_WooCommerce_SingleCoupon') : 0; $mailchimp_total_products = $complete['products'] ? $product_count - mailchimp_get_remaining_jobs_count('MailChimp_WooCommerce_Single_Product') : 0; $mailchimp_total_orders = $complete['orders'] ? $order_count - mailchimp_get_remaining_jobs_count('MailChimp_WooCommerce_Single_Order') : 0; // try { // $promo_rules = $api->getPromoRules($store_id, 1, 1, 1); // $mailchimp_total_promo_rules = $promo_rules['total_items']; // if (isset($promo_rules_count['publish']) && $mailchimp_total_promo_rules > $promo_rules_count['publish']) $mailchimp_total_promo_rules = $promo_rules_count['publish']; // } catch (Exception $e) { $mailchimp_total_promo_rules = 0; } // try { // $products = $api->products($store_id, 1, 1); // $mailchimp_total_products = $products['total_items']; // if ($mailchimp_total_products > $product_count) $mailchimp_total_products = $product_count; // } catch (Exception $e) { $mailchimp_total_products = 0; } // try { // $orders = $api->orders($store_id, 1, 1); // $mailchimp_total_orders = $orders['total_items']; // if ($mailchimp_total_orders > $order_count) $mailchimp_total_orders = $order_count; // } catch (Exception $e) { $mailchimp_total_orders = 0; } $date = mailchimp_date_local('now'); // but we need to do it just in case. return $this->mailchimp_rest_response(array( 'success' => true, 'promo_rules_in_store' => $promo_rules_count, 'promo_rules_in_mailchimp' => $mailchimp_total_promo_rules, 'products_in_store' => $product_count, 'products_in_mailchimp' => $mailchimp_total_products, 'orders_in_store' => $order_count, 'orders_in_mailchimp' => $mailchimp_total_orders, // 'promo_rules_page' => get_option('mailchimp-woocommerce-sync.coupons.current_page'), // 'products_page' => get_option('mailchimp-woocommerce-sync.products.current_page'), // 'orders_page' => get_option('mailchimp-woocommerce-sync.orders.current_page'), 'date' => $date ? $date->format( __('D, M j, Y g:i A', 'mailchimp-for-woocommerce')) : '', 'has_started' => mailchimp_has_started_syncing() || ($order_count != $mailchimp_total_orders), 'has_finished' => mailchimp_is_done_syncing() && ($order_count == $mailchimp_total_orders), 'last_loop_at' => mailchimp_get_data('sync.last_loop_at'), )); } /** * @param WP_REST_Request $request * * @return WP_REST_Response */ public function dismiss_review_banner(WP_REST_Request $request) { return $this->mailchimp_rest_response(array('success' => delete_option('mailchimp-woocommerce-sync.initial_sync'))); } /** * @param WP_REST_Request $request * * @return WP_REST_Response */ public function member_sync(WP_REST_Request $request) { $this->authorize('webhook.token', $request); $data = $request->get_params(); if (!empty($data['type']) && !empty($data['data']['list_id']) && mailchimp_get_list_id() == $data['data']['list_id'] ){ $job = new MailChimp_WooCommerce_Subscriber_Sync($data); $job->handle(); return $this->mailchimp_rest_response(array('success' => true)); } return $this->mailchimp_rest_response(array('success' => false)); } /** * Returns an alive signal to confirm url exists to mailchimp system * @param WP_REST_Request $request * * @return WP_REST_Response */ public function member_sync_alive_signal(WP_REST_Request $request) { $this->authorize('webhook.token', $request); return $this->mailchimp_rest_response(array('success' => true)); } /** * @param WP_REST_Request $request * * @return WP_REST_Response * @throws MailChimp_WooCommerce_Error * @throws MailChimp_WooCommerce_ServerError */ public function get_tower_report(WP_REST_Request $request) { $this->authorize('tower.token', $request); return $this->mailchimp_rest_response( $this->tower($request->get_query_params())->handle() ); } /** * @param WP_REST_Request $request * * @return WP_REST_Response * @throws MailChimp_WooCommerce_Error * @throws MailChimp_WooCommerce_RateLimitError * @throws MailChimp_WooCommerce_ServerError * @throws Throwable */ public function handle_tower_action(WP_REST_Request $request) { $this->authorize('tower.token', $request); $body = $request->get_json_params(); $action = isset($body['action']) ? $body['action'] : null; $data = isset($body['data']) ? $body['data'] : null; $response = null; if (empty($action)) { return $this->mailchimp_rest_response(array( 'success' => false, 'message' => 'invalid action' )); } switch ($action) { case 'emergency_stop_syncing': mailchimp_set_data('emergency_stop', true); $response = [ 'title' => "Successfully stopped the sync.", 'description' => "Please note you'll need to have them reconnect.", 'type' => 'success', ]; break; case 'update_feature': $response = [ 'title' => "Features are not available for WooCommerce", 'type' => 'error', ]; break; case 'resync_orders': MailChimp_WooCommerce_Process_Orders::push(); $response = [ 'title' => "Successfully initiated the order resync", 'description' => "Please note that it will take a couple minutes to start this process. Check the store logs for details.", 'type' => 'success', ]; break; case 'resync_products': MailChimp_WooCommerce_Process_Products::push(); $response = [ 'title' => "Successfully initiated product resync", 'description' => "Please note that it will take a couple minutes to start this process. Check the store logs for details.", 'type' => 'success', ]; break; case 'resync_customers': $response = [ 'title' => "Customer resync", 'description' => "WooCommerce does not have customers to sync. Only orders.", 'type' => 'error', ]; break; case 'resync_promo_codes': MailChimp_WooCommerce_Process_Coupons::push(); $response = [ 'title' => "Successfully initiated promo code resync", 'description' => "Please note that it will take a couple minutes to start this process. Check the store logs for details.", 'type' => 'success', ]; break; case 'resync_chimpstatic_script': $response = [ 'title' => "Chimpstatic script", 'description' => 'Scripts are automatically injected at runtime.', 'type' => 'error', ]; break; case 'activate_webhooks': $api = mailchimp_get_api(); $list = mailchimp_get_list_id(); if (get_option('permalink_structure') === '') { $response = [ 'title' => "Store Webhooks", 'description' => "No store webhooks to apply", 'type' => 'error', ]; } else { $previous_url = mailchimp_get_webhook_url(); if (mailchimp_get_data('webhook.token') && $previous_url && $api->hasWebhook($list, $previous_url)) { $response = [ 'title' => "Store Webhooks", 'description' => "Store already has webhooks enabled!", 'type' => 'success', ]; } else { $key = mailchimp_create_webhook_token(); $url = mailchimp_build_webhook_url($key); mailchimp_set_data('webhook.token', $key); try { $webhook = $api->webHookSubscribe($list, $url); mailchimp_set_webhook_url($webhook['url']); mailchimp_log('webhooks', "added webhook to audience"); $response = [ 'title' => "Store Webhooks", 'description' => "Set up a new webhook at {$webhook['url']}", 'type' => 'success', ]; } catch (Exception $e) { $response = [ 'title' => "Store Webhooks", 'description' => $e->getMessage(), 'type' => 'error', ]; mailchimp_set_data('webhook.token', false); mailchimp_set_webhook_url(false); mailchimp_error('webhook', $e->getMessage()); } } } break; case 'resync_all': $service = new MailChimp_Service(); $service->removePointers(); MailChimp_WooCommerce_Admin::instance()->startSync(); $service->setData('sync.config.resync', true); $response = [ 'title' => "Successfully initiated the store resync", 'description' => "Please note that it will take a couple minutes to start this process. Check the store logs for details.", 'type' => 'success', ]; break; case 'resync_customer': $response = [ 'title' => "Error syncing custome", 'description' => "WooCommerce only works with orders.", 'type' => 'error', ]; break; case 'resync_order': $order = new WC_Order($data['id']); if (!$order->get_date_created()) { $response = [ 'title' => "Error syncing order", 'description' => "This order id does not exist.", 'type' => 'error', ]; } else { $job = new MailChimp_WooCommerce_Single_Order($order); $data = $job->handle(); $response = [ 'title' => "Executed order resync", 'description' => "Check the store logs for details.", 'type' => 'success', ]; } break; case 'resync_product': $product = new WC_Product($data['id']); if (!$product->get_date_created()) { $response = [ 'title' => "Error syncing product", 'description' => "This product id does not exist.", 'type' => 'error', ]; } else { $job = new MailChimp_WooCommerce_Single_Product($product); $data = $job->handle(); $response = [ 'title' => "Executed product resync", 'description' => "Check the store logs for details.", 'type' => 'success', ]; } break; case 'resync_cart': $response = [ 'title' => "Let's talk", 'description' => "This isn't supported by our system yet. If you really need this, please say something.", 'type' => 'error', ]; break; case 'fix_duplicate_store': $job = new MailChimp_WooCommerce_Fix_Duplicate_Store(mailchimp_get_store_id(), true, false); $job->handle(); $response = [ 'title' => "Successfully queued up store deletion.", 'description' => "This process may take a couple minutes to complete. Please check back by reloading the page after a minute.", 'type' => 'success', ]; break; case 'remove_legacy_app': $response = [ 'title' => "Error removing legacy app", 'description' => "WooCommerce doesn't have any legacy apps to delete.", 'type' => 'error', ]; break; } return $this->mailchimp_rest_response($response); } /** * @param WP_REST_Request $request * * @return WP_REST_Response */ public function get_tower_logs(WP_REST_Request $request) { $this->authorize('tower.token', $request); return $this->mailchimp_rest_response( $this->tower($request->get_query_params())->logs() ); } /** * @param WP_REST_Request $request * * @return WP_REST_Response * @throws MailChimp_WooCommerce_Error * @throws MailChimp_WooCommerce_RateLimitError * @throws MailChimp_WooCommerce_ServerError */ public function get_tower_resource(WP_REST_Request $request) { $this->authorize('tower.token', $request); $body = json_decode($request->get_body(), true); if (!isset($body['resource']) || !isset($body['resource_id'])) { return $this->mailchimp_rest_response(array( 'resource' => null, 'resource_error' => 'Resource not found because post request was wrong', 'mailchimp' => null, 'mailchimp_error' => 'Resource not found because post request was wrong', )); } $platform = null; $mc = null; $store_id = mailchimp_get_store_id(); switch ($body['resource']) { case 'order': $order = get_post($body['resource_id']); $mc = !$order->ID ? null : mailchimp_get_api()->getStoreOrder($store_id, $order->ID); if ($order->ID) { $transformer = new MailChimp_WooCommerce_Transform_Orders(); $platform = $transformer->transform($order)->toArray(); } if ($mc) $mc = $mc->toArray(); break; case 'customer': $body['resource_id'] = urldecode($body['resource_id']); $field = is_email($body['resource_id']) ? 'email' : 'id'; $platform = get_user_by($field, $body['resource_id']); if ($platform) { $platform->mailchimp_woocommerce_is_subscribed = (bool) get_user_meta($platform->ID, 'mailchimp_woocommerce_is_subscribed', true); } $hashed = mailchimp_hash_trim_lower($platform->user_email); if ($mc = mailchimp_get_api()->getCustomer($store_id, $hashed)) { try { $member = mailchimp_get_api()->member(mailchimp_get_list_id(), $mc->getEmailAddress()); } catch (Exception $e) { $member = null; } $mc = array( 'customer' => $mc->toArray(), 'member' => $member, ); } break; case 'product': $platform = get_post($body['resource_id']); if ($platform) { $transformer = new MailChimp_WooCommerce_Transform_Products(); $platform = $transformer->transform($platform)->toArray(); } if ($mc = mailchimp_get_api()->getStoreProduct($store_id, $body['resource_id'])) { $mc = $mc->toArray(); } break; case 'cart': global $wpdb; $uid = mailchimp_hash_trim_lower($body['resource_id']); $table = "{$wpdb->prefix}mailchimp_carts"; $sql = $wpdb->prepare("SELECT * FROM $table WHERE id = %s", $uid); $platform = $wpdb->get_row($sql); if ($mc = mailchimp_get_api()->getCart($store_id, $uid)) { $mc = $mc->toArray(); } break; case 'promo_code': $platform = new WC_Coupon($body['resource_id']); $mc = mailchimp_get_api()->getPromoRuleWithCodes($store_id, $body['resource_id']); break; } return $this->mailchimp_rest_response(array( 'resource' => $platform, 'resource_error' => empty($platform) ? 'Resource not found' : false, 'mailchimp' => $mc, 'mailchimp_error' => empty($mc) ? 'Resource not found' : false, )); } /** * @param WP_REST_Request $request * * @return WP_REST_Response */ public function get_tower_sync_stats(WP_REST_Request $request) { $this->authorize('tower.token', $request); // if the queue is running in the console - we need to say tell the response why it's not going to fire this way. if (!mailchimp_is_configured() || !($api = mailchimp_get_api())) { return $this->mailchimp_rest_response(array('success' => false, 'reason' => 'not configured')); } $store_id = mailchimp_get_store_id(); $product_count = mailchimp_get_product_count(); $order_count = mailchimp_get_order_count(); try { $products = $api->products($store_id, 1, 1); $mailchimp_total_products = $products['total_items']; if ($mailchimp_total_products > $product_count) { $mailchimp_total_products = $product_count; } } catch (Exception $e) { $mailchimp_total_products = 0; } try { $mailchimp_total_customers = $api->getCustomerCount($store_id); } catch (Exception $e) { $mailchimp_total_customers = 0; } try { $orders = $api->orders($store_id, 1, 1); $mailchimp_total_orders = $orders['total_items']; if ($mailchimp_total_orders > $order_count) { $mailchimp_total_orders = $order_count; } } catch (Exception $e) { $mailchimp_total_orders = 0; } // but we need to do it just in case. return $this->mailchimp_rest_response(array( 'platform' => array( 'products' => $product_count, 'customers' => 0, 'orders' => $order_count, ), 'mailchimp' => array( 'products' => $mailchimp_total_products, 'customers' => $mailchimp_total_customers, 'orders' => $mailchimp_total_orders, ), )); } /** * @param null $params * * @return MailChimp_WooCommerce_Tower */ private function tower($params = null) { if (!is_array($params)) $params = array(); $job = new MailChimp_WooCommerce_Tower(mailchimp_get_store_id()); $job->withLogFile(!empty($params['log_view']) ? $params['log_view'] : null); $job->withLogSearch(!empty($params['search']) ? $params['search'] : null); return $job; } /** * @param $data * @param int $status * * @return WP_REST_Response */ private function mailchimp_rest_response($data, $status = 200) { if (!is_array($data)) $data = array(); $response = new WP_REST_Response($data); $response->set_status($status); return $response; } /** * @param $key * @param WP_REST_Request $request * * @return bool */ private function authorize($key, WP_REST_Request $request) { $allowed_keys = array( 'tower.token', 'webhook.token', ); // this is just a safeguard against people trying to do wonky things. if (!in_array($key, $allowed_keys, true)) { wp_send_json_error(array('message' => 'unauthorized token type'), 403); } // get the auth token from either a header, or the query string $token = $this->getAuthToken($request); // pull the saved data $saved = mailchimp_get_data($key); // if we don't have a token - or we don't have the saved comparison // or the token doesn't equal the saved token, throw an error. if (empty($token) || empty($saved) || ($token !== $saved && base64_decode($token) !== $saved)) { wp_send_json_error(array('message' => 'unauthorized'), 403); } return true; } /** * @param WP_REST_Request $request * * @return false|mixed|string */ private function getAuthToken(WP_REST_Request $request) { if (($token = $this->getBearerTokenHeader($request))) { return $token; } return $this->getAuthQueryStringParam($request); } /** * @param WP_REST_Request $request * * @return false|string */ private function getBearerTokenHeader(WP_REST_Request $request) { $header = $request->get_header('Authorization'); $position = strrpos($header, 'Bearer '); if ($position !== false) { $header = substr($header, $position + 7); return strpos($header, ',') !== false ? strstr(',', $header, true) : $header; } return false; } /** * @param WP_REST_Request $request * * @return false|mixed */ private function getAuthQueryStringParam(WP_REST_Request $request) { $params = $request->get_query_params(); return empty($params['auth']) ? false : $params['auth']; } }