HEX
Server: Apache/2.4.52 (Ubuntu)
System: Linux ip-10-0-8-47 6.8.0-1021-aws #23~22.04.1-Ubuntu SMP Tue Dec 10 16:31:58 UTC 2024 aarch64
User: ubuntu (1000)
PHP: 8.1.2-1ubuntu2.22
Disabled: NONE
Upload Files
File: /var/www/javaapp.co.uk/wp-content/plugins/klaviyo/includes/class-wck-api.php
<?php

/**
 * WooCommerceKlaviyo API
 *
 * Handles WCK-API endpoint requests
 *
 * @author      Klaviyo
 * @category    API
 * @package     WooCommerceKlaviyo/API
 * @since       2.0
 */

if (! defined('ABSPATH')) {
    exit; // Exit if accessed directly
}

class WCK_API
{
    const VERSION = '3.0.11';
    const KLAVIYO_BASE_URL = 'klaviyo/v1';
    const ORDERS_ENDPOINT = 'orders';
    const EXTENSION_VERSION_ENDPOINT = 'version';
    const PRODUCTS_ENDPOINT = 'products';
    const OPTIONS_ENDPOINT = 'options';
    const DISABLE_ENDPOINT = 'disable';

    // API RESPONSES
    const API_RESPONSE_CODE = 'status_code';
    const API_RESPONSE_ERROR = 'error';
    const API_RESPONSE_REASON = 'reason';
    const API_RESPONSE_SUCCESS = 'success';

    // HTTP CODES
    const STATUS_CODE_HTTP_OK = 200;
    const STATUS_CODE_NO_CONTENT = 204;
    const STATUS_CODE_BAD_REQUEST = 400;
    const STATUS_CODE_AUTHENTICATION_ERROR = 401;
    const STATUS_CODE_AUTHORIZATION_ERROR = 403;
    const STATUS_CODE_INTERNAL_SERVER_ERROR = 500;

    const DEFAULT_RECORDS_PER_PAGE = '50';
    const DATE_MODIFIED = 'post_modified_gmt';
    const POST_STATUS_ANY = 'any';

    const ERROR_KEYS_NOT_PASSED = 'consumer key or consumer secret not passed';
    const ERROR_CONSUMER_KEY_NOT_FOUND = 'consumer_key not found';

    const PERMISSION_READ = 'read';
    const PERMISSION_WRITE = 'write';
    const PERMISSION_READ_WRITE = 'read_write';
    const PERMISSION_METHOD_MAP = array(
        self::PERMISSION_READ => 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(),
        );
    }
}

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;
}

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 ])) {
        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 )
    );
}

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,
    );
}

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;
}

function get_orders_count(WP_REST_Request $request)
{
    $validated_request = validate_request($request);
    if ($validated_request['error'] === true) {
        return $validated_request;
    }

    $args = process_resource_args($request, 'shop_order');

    $loop = new WP_Query($args);
    $data = count_loop($loop);
    return array('order_count' => $loop->found_posts);
}

function get_products_count(WP_REST_Request $request)
{
    $validated_request = validate_request($request);
    if ($validated_request['error'] === true) {
        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);
}

function get_products(WP_REST_Request $request)
{
    $validated_request = validate_request($request);
    if ($validated_request['error'] === true) {
        return $validated_request;
    }

    $args = process_resource_args($request, 'product');

    $loop = new WP_Query($args);
    $data = count_loop($loop);
    return array('product_ids' => $data);
}

function get_orders(WP_REST_Request $request)
{
    $validated_request = validate_request($request);
    if ($validated_request['error'] === true) {
        return $validated_request;
    }

    $args = process_resource_args($request, 'shop_order');

    $loop = new WP_Query($args);
    $data = count_loop($loop);
    return array('order_ids' => $data);
}

/**
 * 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
 * @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' => 'get_orders_count',
        'permission_callback' => '__return_true',
    ));
    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' => 'get_orders',
        'args' => array(
            'id' => array(
                'validate_callback' => 'is_numeric'
            ),
        ),
        'permission_callback' => '__return_true',
    ));
    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 $transient_value stdClass The transient value to be saved, this is an object with various lists of plugins and their data.
 */
function klaviyo_check_for_plugin_update(stdClass $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');
}