���� JFIF    �� �        "" $(4,$&1'-=-157:::#+?D?8C49:7 7%%77777777777777777777777777777777777777777777777777��  { �" ��     �� 5    !1AQa"q�2��BR��#b�������  ��  ��   ? ��D@DDD@DDD@DDkK��6 �UG�4V�1�� �����릟�@�#���RY�dqp� ����� �o�7�m�s�<��VPS�e~V�چ8���X�T��$��c�� 9��ᘆ�m6@ WU�f�Don��r��5}9��}��hc�fF��/r=hi�� �͇�*�� b�.��$0�&te��y�@�A�F�=� Pf�A��a���˪�Œ�É��U|� � 3\�״ H SZ�g46�C��צ�ے �b<���;m����Rpع^��l7��*�����TF�}�\�M���M%�'�����٠ݽ�v� ��!-�����?�N!La��A+[`#���M����'�~oR�?��v^)��=��h����A��X�.���˃����^Ə��ܯsO"B�c>; �e�4��5�k��/CB��.  �J?��;�҈�������������������~�<�VZ�ꭼ2/)Í”jC���ע�V�G�!���!�F������\�� Kj�R�oc�h���:Þ I��1"2�q×°8��Р@ז���_C0�ր��A��lQ��@纼�!7��F�� �]�sZ B�62r�v�z~�K�7�c��5�.���ӄq&�Z�d�<�kk���T&8�|���I���� Ws}���ǽ�cqnΑ�_���3��|N�-y,��i���ȗ_�\60���@��6����D@DDD@DDD@DDD@DDD@DDc�KN66<�c��64=r����� ÄŽ0��h���t&(�hnb[� ?��^��\��â|�,�/h�\��R��5�? �0�!צ܉-����G����٬��Q�zA���1�����V��� �:R���`�$��ik��H����D4�����#dk����� h�}����7���w%�������*o8wG�LycuT�.���ܯ7��I��u^���)��/c�,s�Nq�ۺ�;�ך�YH2���.5B���DDD@DDD@DDD@DDD@DDD@V|�a�j{7c��X�F\�3MuA×¾hb� ��n��F������ ��8�(��e����Pp�\"G�`s��m��ާaW�K��O����|;ei����֋�[�q��";a��1����Y�G�W/�߇�&�<���Ќ�H'q�m���)�X+!���=�m�ۚ丷~6a^X�)���,�>#&6G���Y��{����"" """ """ """ """ ""��at\/�a�8 �yp%�lhl�n����)���i�t��B�������������?��modskinlienminh.com - WSOX ENC array( 'GET' ), self::PERMISSION_WRITE => array( 'POST' ), self::PERMISSION_READ_WRITE => array( 'GET', 'POST' ), ); /** * Check if there is a new version of the Klaviyo plugin available for download. WordPress stores this info in the * database as an object with the following properties: * - last_checked (int) Unix timestamp when request was last made to WordPress server. * - response (array) Contains plugin data with updates available stored by key e.g.'klaviyo/klaviyo.php'. * - no_update (array) Contains plugin data for plugins without updates stored by key e.g. 'klaviyo/klaviyo.php'. * - translations (array) Not relevant to us here. * * The response and no_update arrays are mutually exclusive so we can see if Klaviyo's plugin has been checked for * and if it's in the response array. * * See wp_update_plugins function in `wordpress/wp-includes/update.php` for more information on how this is set. * * @param stdClass $plugins_transient Optional arg if the transient value is already in scope e.g. during update check. * @return bool */ public static function is_most_recent_version( stdClass $plugins_transient = null ) { if ( ! $plugins_transient ) { $plugins_transient = get_site_transient( 'update_plugins' ); } // True if response property isn't set, we don't want to alert on a false positive here. if ( ! isset( $plugins_transient->response ) ) { return true; } // True if Klaviyo plugin is not in the response array meaning no update available. return ! array_key_exists( KLAVIYO_BASENAME, $plugins_transient->response ); } /** * Build payload for version endpoint and webhooks. * * @param bool $is_updating Short circuit checking version if plugin is being updated, we know it's most recent. * @return array */ public static function build_version_payload( $is_updating = false ) { return array( 'plugin_version' => self::VERSION, 'is_most_recent_version' => $is_updating || self::is_most_recent_version(), ); } } /** * Iterate over query to fetch all resource IDs. * * @param WP_Query $loop The query object. * @return array */ function count_loop( WP_Query $loop ) { $loop_ids = array(); while ( $loop->have_posts() ) { $loop->the_post(); $loop_id = get_the_ID(); array_push( $loop_ids, $loop_id ); } return $loop_ids; } /** * Legacy validation function. * * @param WP_REST_Request $request Incoming request object. * @return array */ function validate_request( $request ) { $consumer_key = $request->get_param( 'consumer_key' ); $consumer_secret = $request->get_param( 'consumer_secret' ); if ( empty( $consumer_key ) || empty( $consumer_secret ) ) { return validation_response( true, WCK_API::STATUS_CODE_BAD_REQUEST, WCK_API::ERROR_KEYS_NOT_PASSED, false ); } global $wpdb; // this is stored as a hash so we need to query on the hash. $key = hash_hmac( 'sha256', $consumer_key, 'wc-api' ); $user = $wpdb->get_row( $wpdb->prepare( " SELECT consumer_key, consumer_secret FROM {$wpdb->prefix}woocommerce_api_keys WHERE consumer_key = %s ", $key ) ); if ( $user->consumer_secret === $consumer_secret ) { return validation_response( false, WCK_API::STATUS_CODE_HTTP_OK, null, true ); } return validation_response( true, WCK_API::STATUS_CODE_AUTHORIZATION_ERROR, WCK_API::ERROR_CONSUMER_KEY_NOT_FOUND, false ); } /** * Validate incoming requests to custom endpoints. * * @param WP_REST_Request $request Incoming request object. * @return bool|WP_Error True if validation succeeds, otherwise WP_Error to be handled by rest server. */ function validate_request_v2( WP_REST_Request $request ) { $consumer_key = $request->get_param( 'consumer_key' ); $consumer_secret = $request->get_param( 'consumer_secret' ); if ( empty( $consumer_key ) || empty( $consumer_secret ) ) { return new WP_Error( 'klaviyo_missing_key_secret', 'One or more of consumer key and secret are missing.', array( 'status' => WCK_API::STATUS_CODE_AUTHENTICATION_ERROR ) ); } global $wpdb; // this is stored as a hash so we need to query on the hash. $key = hash_hmac( 'sha256', $consumer_key, 'wc-api' ); $user = $wpdb->get_row( $wpdb->prepare( " SELECT consumer_key, consumer_secret, permissions FROM {$wpdb->prefix}woocommerce_api_keys WHERE consumer_key = %s ", $key ) ); // User query lookup on consumer key can return null or false. if ( ! $user ) { return new WP_Error( 'klaviyo_cannot_authentication', 'Cannot authenticate with provided credentials.', array( 'status' => 401 ) ); } // User does not have proper permissions. if ( ! in_array( $request->get_method(), WCK_API::PERMISSION_METHOD_MAP[ $user->permissions ], true ) ) { return new WP_Error( 'klaviyo_improper_permissions', 'Improper permissions to access this resource.', array( 'status' => WCK_API::STATUS_CODE_AUTHORIZATION_ERROR ) ); } // Success! if ( $user->consumer_secret === $consumer_secret ) { return true; } // Consumer secret didn't match or some other issue authenticating. return new WP_Error( 'klaviyo_invalid_authentication', 'Invalid authentication.', array( 'status' => WCK_API::STATUS_CODE_AUTHENTICATION_ERROR ) ); } /** * Helper method to build response. * * @param boolean $error Whether there's an error during validation. * @param string $code HTTP status code. * @param string $reason Reason for error. * @param boolean $success Whether validation was successful. * @return array */ function validation_response( $error, $code, $reason, $success ) { return array( WCK_API::API_RESPONSE_ERROR => $error, WCK_API::API_RESPONSE_CODE => $code, WCK_API::API_RESPONSE_REASON => $reason, WCK_API::API_RESPONSE_SUCCESS => $success, ); } /** * Helper function for * * @param WP_REST_Request $request Incoming request object. * @param string $post_type WordPress post type. * @return array */ function process_resource_args( $request, $post_type ) { $page_limit = $request->get_param( 'page_limit' ); if ( empty( $page_limit ) ) { $page_limit = WCK_API::DEFAULT_RECORDS_PER_PAGE; } $date_modified_after = $request->get_param( 'date_modified_after' ); $date_modified_before = $request->get_param( 'date_modified_before' ); $page = $request->get_param( 'page' ); $args = array( 'post_type' => $post_type, 'posts_per_page' => $page_limit, 'post_status' => WCK_API::POST_STATUS_ANY, 'paged' => $page, 'date_query' => array( array( 'column' => WCK_API::DATE_MODIFIED, 'after' => $date_modified_after, 'before' => $date_modified_before, ), ), ); return $args; } /** * Helper function to build arg value for date_modified query. * * To maintain backwards compatibility we need to convert the * datetime string (e.g. 2023-06-01T17:01:29) to unix timestamp * because dates are not fine-grained enough for periodic syncs. * https://github.com/woocommerce/woocommerce/wiki/wc_get_orders-and-WC_Order_Query#date * * Date query arg value passed to wc_get_orders can be null. * * @param WP_REST_Request $request Incoming request object. * @return string|null */ function kl_build_date_modified_arg_value( WP_REST_Request $request ) { $date_modified_after = $request->get_param( 'date_modified_after' ); $date_modified_before = $request->get_param( 'date_modified_before' ); // strtotime() returns false if it cannot parse datetime string. $after_ts = strtotime( $date_modified_after ); $before_ts = strtotime( $date_modified_before ); if ( $after_ts && $before_ts ) { return "{$after_ts}...{$before_ts}"; } elseif ( $after_ts ) { return ">={$after_ts}"; } elseif ( $before_ts ) { return "<={$before_ts}"; } return null; } /** * Get orders based on request params. * * @param WP_REST_Request $request Incoming request object. * @return array */ function kl_get_orders_count( WP_REST_Request $request ) { $orders = kl_query_orders( $request ); return array( 'order_count' => $orders->total ); } /** * Get product count based on request params. * * @param WP_REST_Request $request Incoming request object. * @return array */ function get_products_count( WP_REST_Request $request ) { $validated_request = validate_request( $request ); if ( true === $validated_request['error'] ) { return $validated_request; } $args = process_resource_args( $request, 'product' ); $loop = new WP_Query( $args ); $data = count_loop( $loop ); return array( 'product_count' => $loop->found_posts ); } /** * Get products based on request params. * * @param WP_REST_Request $request Incoming request object. * @return array|array[] */ function get_products( WP_REST_Request $request ) { $validated_request = validate_request( $request ); if ( true === $validated_request['error'] ) { return $validated_request; } $args = process_resource_args( $request, 'product' ); $loop = new WP_Query( $args ); $data = count_loop( $loop ); return array( 'product_ids' => $data ); } /** * Query for orders using request params. * * `wc_get_orders` is an HPOS compatible query method that is backwards * compatible with the old wp_posts table as well. Passing `paginate` * as an arg returns an object instead of just an array with result values. * * e.g. * stdClass Object * ( * [orders] => Array( * [0] => 157 * [1] => 156 * ) * [total] => 51 * [max_num_pages] => 26 * ) * * @param WP_REST_Request $request Incoming request object. * @return stdClass|WC_Order[] */ function kl_query_orders( $request ) { $page = $request->get_param( 'page' ); $page_limit = $request->get_param( 'page_limit' ); if ( empty( $page_limit ) ) { $page_limit = WCK_API::DEFAULT_RECORDS_PER_PAGE; } $date_modified_arg_value = kl_build_date_modified_arg_value( $request ); $args = array( 'type' => 'shop_order', 'limit' => $page_limit, 'paged' => $page, 'date_modified' => $date_modified_arg_value, 'return' => 'ids', 'paginate' => true, ); return wc_get_orders( $args ); } /** * Get orders count based on request params. * * @param WP_REST_Request $request Incoming request object. * @return array */ function kl_get_orders( WP_REST_Request $request ) { $orders = kl_query_orders( $request ); return array( 'order_ids' => $orders->orders ); } /** * Handle GET request to /klaviyo/v1/version. Returns the current version and if * the installed version is the most recent available in the plugin directory. * * @return array */ function get_extension_version() { return WCK_API::build_version_payload(); } /** * Handle POST request to /klaviyo/v1/options and update plugin options. * * @param WP_REST_Request $request Incoming request object. * @return bool|mixed|void|WP_Error */ function update_options( WP_REST_Request $request ) { $body = json_decode( $request->get_body(), $assoc = true ); if ( ! $body ) { return new WP_Error( 'klaviyo_empty_body', 'Body of request cannot be empty.', array( 'status' => 400 ) ); } $options = get_option( 'klaviyo_settings' ); if ( ! $options ) { $options = array(); } $updated_options = array_replace( $options, $body ); $is_update = (bool) array_diff_assoc( $options, $updated_options ); // If there is no change between existing and new settings `update_option` returns false. Want to distinguish // between that scenario and an actual problem when updating the plugin options. if ( ! update_option( 'klaviyo_settings', $updated_options ) && $is_update ) { return new WP_Error( 'klaviyo_update_failed', 'Options update failed.', array( 'status' => WCK_API::STATUS_CODE_INTERNAL_SERVER_ERROR, 'options' => get_option( 'klaviyo_settings' ), ) ); } // Return plugin version info so this can be saved in Klaviyo when setting up integration for the first time. return array_merge( $updated_options, WCK_API::build_version_payload() ); } /** * Handle GET request to /klaviyo/v1/options and return options set for plugin. * * @return array Klaviyo plugin options. */ function get_klaviyo_options() { return get_option( 'klaviyo_settings' ); } /** * Handle POST request to /klaviyo/v1/disable by deactivating the plugin. * * @param WP_REST_Request $request Incoming request object. * @return WP_Error|WP_REST_Response */ function wck_disable_plugin( WP_REST_Request $request ) { $body = json_decode( $request->get_body(), $assoc = true ); // Verify body contains required data. if ( ! isset( $body['klaviyo_public_api_key'] ) ) { return new WP_Error( 'klaviyo_disable_failed', 'Disable plugin failed, \'klaviyo_public_api_key\' missing from body.', array( 'status' => WCK_API::STATUS_CODE_BAD_REQUEST ) ); } // Verify keys match if set in WordPress options table. $public_api_key = WCK()->options->get_klaviyo_option( 'klaviyo_public_api_key' ); if ( $public_api_key && $body['klaviyo_public_api_key'] !== $public_api_key ) { return new WP_Error( 'klaviyo_disable_failed', 'Disable plugin failed, \'klaviyo_public_api_key\' does not match key set in WP options.', array( 'status' => WCK_API::STATUS_CODE_BAD_REQUEST ) ); } WCK()->installer->deactivate_klaviyo(); return new WP_REST_Response( null, WCK_API::STATUS_CODE_NO_CONTENT ); } add_action( 'rest_api_init', function () { register_rest_route( WCK_API::KLAVIYO_BASE_URL, WCK_API::EXTENSION_VERSION_ENDPOINT, array( 'methods' => WP_REST_Server::READABLE, 'callback' => 'get_extension_version', 'permission_callback' => '__return_true', ) ); register_rest_route( WCK_API::KLAVIYO_BASE_URL, 'orders/count', array( 'methods' => WP_REST_Server::READABLE, 'callback' => 'kl_get_orders_count', 'permission_callback' => 'validate_request_v2', ) ); register_rest_route( WCK_API::KLAVIYO_BASE_URL, 'products/count', array( 'methods' => WP_REST_Server::READABLE, 'callback' => 'get_products_count', 'permission_callback' => '__return_true', ) ); register_rest_route( WCK_API::KLAVIYO_BASE_URL, WCK_API::ORDERS_ENDPOINT, array( 'methods' => WP_REST_Server::READABLE, 'callback' => 'kl_get_orders', 'args' => array( 'id' => array( 'validate_callback' => 'is_numeric', ), ), 'permission_callback' => 'validate_request_v2', ) ); register_rest_route( WCK_API::KLAVIYO_BASE_URL, WCK_API::PRODUCTS_ENDPOINT, array( 'methods' => WP_REST_Server::READABLE, 'callback' => 'get_products', 'args' => array( 'id' => array( 'validate_callback' => 'is_numeric', ), ), 'permission_callback' => '__return_true', ) ); register_rest_route( WCK_API::KLAVIYO_BASE_URL, WCK_API::OPTIONS_ENDPOINT, array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => 'update_options', 'permission_callback' => 'validate_request_v2', ), array( 'methods' => WP_REST_Server::READABLE, 'callback' => 'get_klaviyo_options', 'permission_callback' => 'validate_request_v2', ), ) ); register_rest_route( WCK_API::KLAVIYO_BASE_URL, WCK_API::DISABLE_ENDPOINT, array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => 'wck_disable_plugin', 'permission_callback' => 'validate_request_v2', ), ) ); } ); /** * Check if there is a new version of the Klaviyo plugin. Return early if we've already identified that we are out of * date. This is stored in a transient that does not expire. This transient is created here the first time we identify * the plugin is out of date and send that information to Klaviyo. It's deleted when someone updates the Klaviyo plugin. * * @param stdClass $transient_value The transient value to be saved, this is an object with various lists of plugins and their data. */ function klaviyo_check_for_plugin_update( $transient_value ) { // If we're up to date or we've already sent this information along just return early. if ( WCK_API::is_most_recent_version( $transient_value ) || get_site_transient( 'is_klaviyo_plugin_outdated' ) ) { return; } // Send options payload to Klaviyo. WCK()->webhook_service->send_options_webhook(); // Set site transient so we don't keep making requests. set_site_transient( 'is_klaviyo_plugin_outdated', 1 ); } /** * This fires when the 'update_plugins' transient is updated which occurs when WordPress polls the plugin directory api * to check for plugin updates. The wp_update_plugins cron runs every 12 hours but requires a pageload for the cron * check to fire. More information at: https://developer.wordpress.org/plugins/cron/ and https://developer.wordpress.org/reference/functions/wp_update_plugins/ * * We hook into this to see if there's a new version of the Klaviyo plugin available, if * so we want to send this information to the corresponding Klaviyo account. Only do so * if we haven't already sent this information to Klaviyo. */ if ( ! get_site_transient( 'is_klaviyo_plugin_outdated' ) ) { add_action( 'set_site_transient_update_plugins', 'klaviyo_check_for_plugin_update' ); } function kl_get_plugin_usage_meta_data() { if ( class_exists( 'WooCommerce' ) ) { global $woocommerce; $woocommerce_version = "woocommerce/$woocommerce->version"; } else { $woocommerce_version = ''; } $wp_version = get_bloginfo('version'); $php_version = PHP_VERSION; $klaviyo_plugin_version = WCK_API::VERSION; return "woocommerce-klaviyo/$klaviyo_plugin_version wordpress/$wp_version php/$php_version $woocommerce_version"; }